@maravilla-labs/platform 0.1.35 → 0.1.38

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/types.ts CHANGED
@@ -559,6 +559,378 @@ export interface PresenceService {
559
559
  members(channel: string): Promise<Array<{ userId: string; metadata?: any; lastSeen?: number }>>;
560
560
  }
561
561
 
562
+ // ── Auth Types ──
563
+
564
+ /**
565
+ * Authenticated user record from the platform auth service.
566
+ */
567
+ export interface AuthUser {
568
+ /** Unique user ID (prefixed with "usr_") */
569
+ id: string;
570
+ /** User's email address */
571
+ email: string;
572
+ /** Whether the email has been verified */
573
+ email_verified: boolean;
574
+ /** Account status */
575
+ status: 'active' | 'suspended' | 'deactivated';
576
+ /** Authentication provider ("email", "google", "github", etc.) */
577
+ provider: string;
578
+ /** Group IDs the user belongs to */
579
+ groups: string[];
580
+ /** Unix timestamp when the user was created */
581
+ created_at: number;
582
+ /** Unix timestamp when the user was last updated */
583
+ updated_at: number;
584
+ /** Unix timestamp of last login (if any) */
585
+ last_login_at?: number;
586
+ }
587
+
588
+ /**
589
+ * Snapshot of whoever is currently bound to the request as the caller.
590
+ *
591
+ * Populated by {@link AuthService.login} (implicit), {@link AuthService.setCurrentUser}
592
+ * (explicit), or left anonymous if neither has run for this request.
593
+ * This is exactly what per-resource policies see as `auth.*` when they run.
594
+ */
595
+ export interface AuthCaller {
596
+ /** Caller's user id, or `""` if anonymous */
597
+ user_id: string;
598
+ /** Caller's email, or `""` if anonymous */
599
+ email: string;
600
+ /** Admin flag from the session */
601
+ is_admin: boolean;
602
+ /** Role names (project-scoped) */
603
+ roles: string[];
604
+ /** `true` when no identity is bound to this request */
605
+ is_anonymous: boolean;
606
+ }
607
+
608
+ /**
609
+ * Session returned after successful login or token refresh.
610
+ */
611
+ export interface AuthSession {
612
+ /** Short-lived JWT access token (default 15 min) */
613
+ access_token: string;
614
+ /** Single-use opaque refresh token (default 30 days) */
615
+ refresh_token: string;
616
+ /** Access token lifetime in seconds */
617
+ expires_in: number;
618
+ /** The authenticated user */
619
+ user: AuthUser;
620
+ }
621
+
622
+ /**
623
+ * Custom registration field defined in project auth settings.
624
+ */
625
+ export interface AuthField {
626
+ /** Field key (used as form field name) */
627
+ key: string;
628
+ /** Display label */
629
+ label: string;
630
+ /** Field type: text, email, phone, date, number, select, boolean, url, textarea */
631
+ field_type: string;
632
+ /** Whether the field is required */
633
+ required: boolean;
634
+ /** Whether the field appears on the registration form */
635
+ show_on_register: boolean;
636
+ }
637
+
638
+ /**
639
+ * Options for registering a new user.
640
+ */
641
+ export interface RegisterOptions {
642
+ /** User's email address */
643
+ email: string;
644
+ /** Password (minimum 8 characters) */
645
+ password: string;
646
+ /** Optional profile data (custom fields) */
647
+ profile?: Record<string, any>;
648
+ }
649
+
650
+ /**
651
+ * Options for logging in.
652
+ */
653
+ export interface LoginOptions {
654
+ /** User's email address */
655
+ email: string;
656
+ /** User's password */
657
+ password: string;
658
+ }
659
+
660
+ /**
661
+ * Filter options for listing users.
662
+ */
663
+ export interface UserListFilter {
664
+ /** Max results per page (default 50) */
665
+ limit?: number;
666
+ /** Number of results to skip */
667
+ offset?: number;
668
+ /** Filter by account status */
669
+ status?: 'active' | 'suspended' | 'deactivated';
670
+ /** Filter by email (partial match) */
671
+ email_contains?: string;
672
+ /** Filter by group ID */
673
+ group_id?: string;
674
+ }
675
+
676
+ /**
677
+ * Paginated user list response.
678
+ */
679
+ export interface UserListResponse {
680
+ /** Users in this page */
681
+ users: AuthUser[];
682
+ /** Total number of matching users */
683
+ total: number;
684
+ /** Page size */
685
+ limit: number;
686
+ /** Offset */
687
+ offset: number;
688
+ }
689
+
690
+ /**
691
+ * Options for updating a user.
692
+ */
693
+ export interface UpdateUserOptions {
694
+ /** New email address */
695
+ email?: string;
696
+ /** New status */
697
+ status?: 'active' | 'suspended' | 'deactivated';
698
+ /** Profile data to merge */
699
+ profile?: Record<string, any>;
700
+ }
701
+
702
+ /**
703
+ * Auth service for end-user authentication and user management.
704
+ *
705
+ * @example
706
+ * ```typescript
707
+ * const platform = getPlatform();
708
+ *
709
+ * // Register a new user
710
+ * const user = await platform.auth.register({
711
+ * email: 'user@example.com',
712
+ * password: 'securePassword123'
713
+ * });
714
+ *
715
+ * // Login
716
+ * const session = await platform.auth.login({
717
+ * email: 'user@example.com',
718
+ * password: 'securePassword123'
719
+ * });
720
+ * // session.access_token — short-lived JWT
721
+ * // session.refresh_token — single-use refresh token
722
+ *
723
+ * // Validate a token (e.g. from Authorization header or cookie)
724
+ * const user = await platform.auth.validate(session.access_token);
725
+ *
726
+ * // Protect a route with withAuth middleware
727
+ * export default {
728
+ * fetch: platform.auth.withAuth(async (request) => {
729
+ * // request.user is guaranteed to be set
730
+ * return new Response(`Hello ${request.user.email}`);
731
+ * })
732
+ * };
733
+ * ```
734
+ */
735
+ export interface AuthService {
736
+ /**
737
+ * Register a new user with email and password.
738
+ * @returns The created user (not yet email-verified)
739
+ */
740
+ register(options: RegisterOptions): Promise<AuthUser>;
741
+
742
+ /**
743
+ * Authenticate a user and create a session.
744
+ * @returns Session with access token, refresh token, and user info
745
+ */
746
+ login(options: LoginOptions): Promise<AuthSession>;
747
+
748
+ /**
749
+ * Validate an access token and return the authenticated user.
750
+ * @param accessToken - JWT access token from login or refresh
751
+ * @throws If the token is invalid or expired
752
+ */
753
+ validate(accessToken: string): Promise<AuthUser>;
754
+
755
+ /**
756
+ * Refresh a session using a refresh token (single-use).
757
+ * @param refreshToken - The refresh token from a previous login/refresh
758
+ * @returns New session with fresh access and refresh tokens
759
+ */
760
+ refresh(refreshToken: string): Promise<AuthSession>;
761
+
762
+ /**
763
+ * Revoke a specific session.
764
+ */
765
+ logout(sessionId: string): Promise<void>;
766
+
767
+ /**
768
+ * Get a user by ID.
769
+ * @returns The user, or null if not found
770
+ */
771
+ getUser(userId: string): Promise<AuthUser | null>;
772
+
773
+ /**
774
+ * List users with optional filtering and pagination.
775
+ */
776
+ listUsers(filter?: UserListFilter): Promise<UserListResponse>;
777
+
778
+ /**
779
+ * Update a user's email, status, or profile data.
780
+ */
781
+ updateUser(userId: string, update: UpdateUserOptions): Promise<AuthUser>;
782
+
783
+ /**
784
+ * Delete a user and all their sessions.
785
+ */
786
+ deleteUser(userId: string): Promise<void>;
787
+
788
+ /**
789
+ * Create an email verification token.
790
+ * @returns The verification token (caller decides how to deliver it)
791
+ */
792
+ sendVerification(userId: string): Promise<{ token: string }>;
793
+
794
+ /**
795
+ * Verify an email address using a verification token.
796
+ */
797
+ verifyEmail(token: string): Promise<void>;
798
+
799
+ /**
800
+ * Create a password reset token for an email address.
801
+ * @returns The reset token (caller decides how to deliver it)
802
+ */
803
+ sendPasswordReset(email: string): Promise<{ token: string }>;
804
+
805
+ /**
806
+ * Reset a password using a reset token.
807
+ */
808
+ resetPassword(token: string, newPassword: string): Promise<void>;
809
+
810
+ /**
811
+ * Change a user's password (requires old password).
812
+ */
813
+ changePassword(userId: string, oldPassword: string, newPassword: string): Promise<void>;
814
+
815
+ /**
816
+ * Get the configured registration fields for this project.
817
+ */
818
+ getFieldConfig(): Promise<{ fields: AuthField[] }>;
819
+
820
+ /**
821
+ * Start an OAuth flow by generating an authorization URL.
822
+ * Redirect the user to the returned URL to begin authentication.
823
+ *
824
+ * @param provider - Provider name: "google", "github", "okta", or "custom_oidc"
825
+ * @param options - Optional configuration
826
+ * @returns Object with `auth_url` (redirect target) and `state` (for CSRF verification)
827
+ */
828
+ getOAuthUrl(provider: string, options?: { redirectUri?: string }): Promise<{ auth_url: string; state: string }>;
829
+
830
+ /**
831
+ * Complete an OAuth flow by exchanging the authorization code.
832
+ * Call this after the provider redirects back with a code and state.
833
+ *
834
+ * @param provider - Provider name
835
+ * @param params - The code and state from the OAuth callback
836
+ * @returns Either a session (user authenticated) or a link_required result
837
+ */
838
+ handleOAuthCallback(provider: string, params: { code: string; state: string }): Promise<
839
+ | AuthSession
840
+ | { type: 'LinkRequired'; email: string; provider: string; provider_id: string; existing_user_id: string }
841
+ >;
842
+
843
+ /**
844
+ * Middleware helper that validates auth and injects `request.user`.
845
+ * Returns 401 JSON response if no valid token is found.
846
+ *
847
+ * Extracts token from `Authorization: Bearer <token>` header
848
+ * or `__session` cookie.
849
+ *
850
+ * @example
851
+ * ```typescript
852
+ * export default {
853
+ * fetch: platform.auth.withAuth(async (request) => {
854
+ * const data = await platform.db.items.find({ owner: request.user.id });
855
+ * return Response.json(data);
856
+ * })
857
+ * };
858
+ * ```
859
+ */
860
+ withAuth<T extends (request: Request & { user: AuthUser }) => Promise<Response>>(
861
+ handler: T
862
+ ): (request: Request) => Promise<Response>;
863
+
864
+ // ── Request-scoped identity + authorization ──
865
+ //
866
+ // These methods operate on the **current request's** caller context.
867
+ // They're meaningful inside the platform runtime (one isolate serving a
868
+ // Deno request). When called from a remote client — code running outside
869
+ // the runtime — they throw, because there is no per-request context to
870
+ // bind.
871
+
872
+ /**
873
+ * Explicitly bind the caller for the remainder of this request.
874
+ * Pass a JWT to validate + bind, or `null` / `""` to clear.
875
+ *
876
+ * `login()` already binds implicitly on success; reach for `setCurrentUser`
877
+ * when you receive a JWT from an inbound `Authorization` header or cookie
878
+ * and want subsequent KV/DB/realtime/media ops to run as that user.
879
+ *
880
+ * Not available on remote clients — throws.
881
+ */
882
+ setCurrentUser(token: string | null): Promise<void>;
883
+
884
+ /**
885
+ * Snapshot of the currently bound caller. Returns an anonymous caller
886
+ * (`is_anonymous: true`) when no identity has been bound.
887
+ *
888
+ * Not available on remote clients — throws.
889
+ */
890
+ getCurrentUser(): AuthCaller;
891
+
892
+ /**
893
+ * Ask the policy engine whether the bound caller would be allowed to
894
+ * perform `action` on `resourceId`, given the supplied `node` payload.
895
+ * Returns a boolean — never throws on denial.
896
+ *
897
+ * The check runs the exact same evaluator that gates direct KV/DB/
898
+ * realtime/media ops, so `can(...)` is authoritative.
899
+ *
900
+ * @example
901
+ * ```typescript
902
+ * const ok = await platform.auth.can("delete", "documents", {
903
+ * owner: doc.owner,
904
+ * status: doc.status,
905
+ * });
906
+ * if (!ok) return new Response("Forbidden", { status: 403 });
907
+ * ```
908
+ *
909
+ * Not available on remote clients — throws.
910
+ */
911
+ can(action: string, resourceId: string, node?: Record<string, unknown> | null): Promise<boolean>;
912
+ }
913
+
914
+ /**
915
+ * Per-request opt-out toggle for the Layer 2 policy evaluator.
916
+ *
917
+ * Flipping `setEnabled(false)` disables **per-resource policies only** for
918
+ * the remainder of the current request. Layer 1 (tenant + owner isolation)
919
+ * is always enforced — no call can ever escape its tenant. Every flip is
920
+ * audit-logged server-side with the caller's identity.
921
+ *
922
+ * Intended for trusted in-app flows (first-run seeders, admin jobs). Do not
923
+ * toggle based on untrusted input.
924
+ *
925
+ * Not available on remote clients — throws.
926
+ */
927
+ export interface PolicyService {
928
+ /** Disable or re-enable Layer 2 policy checks for this request. */
929
+ setEnabled(enabled: boolean): void;
930
+ /** `true` when Layer 2 is active for this request. */
931
+ isEnabled(): boolean;
932
+ }
933
+
562
934
  export interface Platform {
563
935
  /** Environment containing all available platform services */
564
936
  env: PlatformEnv;
@@ -566,4 +938,8 @@ export interface Platform {
566
938
  media?: import('./media.js').MediaService;
567
939
  /** Realtime service for pub/sub channels and presence */
568
940
  realtime: RealtimeService;
941
+ /** Auth service for end-user authentication, identity binding, and authorization checks */
942
+ auth: AuthService;
943
+ /** Per-request Layer 2 policy toggle (Layer 1 isolation always applies) */
944
+ policy: PolicyService;
569
945
  }
package/tsup.config.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import { defineConfig } from 'tsup';
2
2
 
3
3
  export default defineConfig({
4
- entry: ['src/index.ts'],
4
+ entry: ['src/index.ts', 'src/config.ts'],
5
5
  format: ['esm'],
6
6
  dts: true,
7
7
  splitting: false,