@maravilla-labs/platform 0.1.34 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -598,6 +598,278 @@ interface PresenceService {
598
598
  lastSeen?: number;
599
599
  }>>;
600
600
  }
601
+ /**
602
+ * Authenticated user record from the platform auth service.
603
+ */
604
+ interface AuthUser {
605
+ /** Unique user ID (prefixed with "usr_") */
606
+ id: string;
607
+ /** User's email address */
608
+ email: string;
609
+ /** Whether the email has been verified */
610
+ email_verified: boolean;
611
+ /** Account status */
612
+ status: 'active' | 'suspended' | 'deactivated';
613
+ /** Authentication provider ("email", "google", "github", etc.) */
614
+ provider: string;
615
+ /** Group IDs the user belongs to */
616
+ groups: string[];
617
+ /** Unix timestamp when the user was created */
618
+ created_at: number;
619
+ /** Unix timestamp when the user was last updated */
620
+ updated_at: number;
621
+ /** Unix timestamp of last login (if any) */
622
+ last_login_at?: number;
623
+ }
624
+ /**
625
+ * Session returned after successful login or token refresh.
626
+ */
627
+ interface AuthSession {
628
+ /** Short-lived JWT access token (default 15 min) */
629
+ access_token: string;
630
+ /** Single-use opaque refresh token (default 30 days) */
631
+ refresh_token: string;
632
+ /** Access token lifetime in seconds */
633
+ expires_in: number;
634
+ /** The authenticated user */
635
+ user: AuthUser;
636
+ }
637
+ /**
638
+ * Custom registration field defined in project auth settings.
639
+ */
640
+ interface AuthField {
641
+ /** Field key (used as form field name) */
642
+ key: string;
643
+ /** Display label */
644
+ label: string;
645
+ /** Field type: text, email, phone, date, number, select, boolean, url, textarea */
646
+ field_type: string;
647
+ /** Whether the field is required */
648
+ required: boolean;
649
+ /** Whether the field appears on the registration form */
650
+ show_on_register: boolean;
651
+ }
652
+ /**
653
+ * Options for registering a new user.
654
+ */
655
+ interface RegisterOptions {
656
+ /** User's email address */
657
+ email: string;
658
+ /** Password (minimum 8 characters) */
659
+ password: string;
660
+ /** Optional profile data (custom fields) */
661
+ profile?: Record<string, any>;
662
+ }
663
+ /**
664
+ * Options for logging in.
665
+ */
666
+ interface LoginOptions {
667
+ /** User's email address */
668
+ email: string;
669
+ /** User's password */
670
+ password: string;
671
+ }
672
+ /**
673
+ * Filter options for listing users.
674
+ */
675
+ interface UserListFilter {
676
+ /** Max results per page (default 50) */
677
+ limit?: number;
678
+ /** Number of results to skip */
679
+ offset?: number;
680
+ /** Filter by account status */
681
+ status?: 'active' | 'suspended' | 'deactivated';
682
+ /** Filter by email (partial match) */
683
+ email_contains?: string;
684
+ /** Filter by group ID */
685
+ group_id?: string;
686
+ }
687
+ /**
688
+ * Paginated user list response.
689
+ */
690
+ interface UserListResponse {
691
+ /** Users in this page */
692
+ users: AuthUser[];
693
+ /** Total number of matching users */
694
+ total: number;
695
+ /** Page size */
696
+ limit: number;
697
+ /** Offset */
698
+ offset: number;
699
+ }
700
+ /**
701
+ * Options for updating a user.
702
+ */
703
+ interface UpdateUserOptions {
704
+ /** New email address */
705
+ email?: string;
706
+ /** New status */
707
+ status?: 'active' | 'suspended' | 'deactivated';
708
+ /** Profile data to merge */
709
+ profile?: Record<string, any>;
710
+ }
711
+ /**
712
+ * Auth service for end-user authentication and user management.
713
+ *
714
+ * @example
715
+ * ```typescript
716
+ * const platform = getPlatform();
717
+ *
718
+ * // Register a new user
719
+ * const user = await platform.auth.register({
720
+ * email: 'user@example.com',
721
+ * password: 'securePassword123'
722
+ * });
723
+ *
724
+ * // Login
725
+ * const session = await platform.auth.login({
726
+ * email: 'user@example.com',
727
+ * password: 'securePassword123'
728
+ * });
729
+ * // session.access_token — short-lived JWT
730
+ * // session.refresh_token — single-use refresh token
731
+ *
732
+ * // Validate a token (e.g. from Authorization header or cookie)
733
+ * const user = await platform.auth.validate(session.access_token);
734
+ *
735
+ * // Protect a route with withAuth middleware
736
+ * export default {
737
+ * fetch: platform.auth.withAuth(async (request) => {
738
+ * // request.user is guaranteed to be set
739
+ * return new Response(`Hello ${request.user.email}`);
740
+ * })
741
+ * };
742
+ * ```
743
+ */
744
+ interface AuthService {
745
+ /**
746
+ * Register a new user with email and password.
747
+ * @returns The created user (not yet email-verified)
748
+ */
749
+ register(options: RegisterOptions): Promise<AuthUser>;
750
+ /**
751
+ * Authenticate a user and create a session.
752
+ * @returns Session with access token, refresh token, and user info
753
+ */
754
+ login(options: LoginOptions): Promise<AuthSession>;
755
+ /**
756
+ * Validate an access token and return the authenticated user.
757
+ * @param accessToken - JWT access token from login or refresh
758
+ * @throws If the token is invalid or expired
759
+ */
760
+ validate(accessToken: string): Promise<AuthUser>;
761
+ /**
762
+ * Refresh a session using a refresh token (single-use).
763
+ * @param refreshToken - The refresh token from a previous login/refresh
764
+ * @returns New session with fresh access and refresh tokens
765
+ */
766
+ refresh(refreshToken: string): Promise<AuthSession>;
767
+ /**
768
+ * Revoke a specific session.
769
+ */
770
+ logout(sessionId: string): Promise<void>;
771
+ /**
772
+ * Get a user by ID.
773
+ * @returns The user, or null if not found
774
+ */
775
+ getUser(userId: string): Promise<AuthUser | null>;
776
+ /**
777
+ * List users with optional filtering and pagination.
778
+ */
779
+ listUsers(filter?: UserListFilter): Promise<UserListResponse>;
780
+ /**
781
+ * Update a user's email, status, or profile data.
782
+ */
783
+ updateUser(userId: string, update: UpdateUserOptions): Promise<AuthUser>;
784
+ /**
785
+ * Delete a user and all their sessions.
786
+ */
787
+ deleteUser(userId: string): Promise<void>;
788
+ /**
789
+ * Create an email verification token.
790
+ * @returns The verification token (caller decides how to deliver it)
791
+ */
792
+ sendVerification(userId: string): Promise<{
793
+ token: string;
794
+ }>;
795
+ /**
796
+ * Verify an email address using a verification token.
797
+ */
798
+ verifyEmail(token: string): Promise<void>;
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<{
804
+ token: string;
805
+ }>;
806
+ /**
807
+ * Reset a password using a reset token.
808
+ */
809
+ resetPassword(token: string, newPassword: string): Promise<void>;
810
+ /**
811
+ * Change a user's password (requires old password).
812
+ */
813
+ changePassword(userId: string, oldPassword: string, newPassword: string): Promise<void>;
814
+ /**
815
+ * Get the configured registration fields for this project.
816
+ */
817
+ getFieldConfig(): Promise<{
818
+ 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?: {
829
+ redirectUri?: string;
830
+ }): Promise<{
831
+ auth_url: string;
832
+ state: string;
833
+ }>;
834
+ /**
835
+ * Complete an OAuth flow by exchanging the authorization code.
836
+ * Call this after the provider redirects back with a code and state.
837
+ *
838
+ * @param provider - Provider name
839
+ * @param params - The code and state from the OAuth callback
840
+ * @returns Either a session (user authenticated) or a link_required result
841
+ */
842
+ handleOAuthCallback(provider: string, params: {
843
+ code: string;
844
+ state: string;
845
+ }): Promise<AuthSession | {
846
+ type: 'LinkRequired';
847
+ email: string;
848
+ provider: string;
849
+ provider_id: string;
850
+ existing_user_id: string;
851
+ }>;
852
+ /**
853
+ * Middleware helper that validates auth and injects `request.user`.
854
+ * Returns 401 JSON response if no valid token is found.
855
+ *
856
+ * Extracts token from `Authorization: Bearer <token>` header
857
+ * or `__session` cookie.
858
+ *
859
+ * @example
860
+ * ```typescript
861
+ * export default {
862
+ * fetch: platform.auth.withAuth(async (request) => {
863
+ * const data = await platform.db.items.find({ owner: request.user.id });
864
+ * return Response.json(data);
865
+ * })
866
+ * };
867
+ * ```
868
+ */
869
+ withAuth<T extends (request: Request & {
870
+ user: AuthUser;
871
+ }) => Promise<Response>>(handler: T): (request: Request) => Promise<Response>;
872
+ }
601
873
  interface Platform {
602
874
  /** Environment containing all available platform services */
603
875
  env: PlatformEnv;
@@ -605,6 +877,8 @@ interface Platform {
605
877
  media?: MediaService;
606
878
  /** Realtime service for pub/sub channels and presence */
607
879
  realtime: RealtimeService;
880
+ /** Auth service for end-user authentication and user management */
881
+ auth: AuthService;
608
882
  }
609
883
 
610
884
  interface RenEvent {
@@ -721,10 +995,14 @@ declare class RealtimeClient {
721
995
  presence(channel: string): {
722
996
  /** Join the channel with presence (auto-rejoins on reconnect) */
723
997
  join: (userId: string, metadata?: any) => void;
998
+ /** Update metadata for the current presence (must have joined first) */
999
+ update: (metadata?: any) => void;
724
1000
  /** Leave the channel */
725
1001
  leave: () => void;
726
1002
  /** Listen for users joining */
727
1003
  onJoin: (callback: (member: PresenceMember) => void) => Unsubscribe;
1004
+ /** Listen for metadata updates */
1005
+ onUpdate: (callback: (member: PresenceMember) => void) => Unsubscribe;
728
1006
  /** Listen for users leaving */
729
1007
  onLeave: (callback: (member: PresenceMember) => void) => Unsubscribe;
730
1008
  };
@@ -963,4 +1241,4 @@ declare function getPlatform(options?: {
963
1241
  */
964
1242
  declare function clearPlatformCache(): void;
965
1243
 
966
- export { type Database, type DbFindOptions, type KvListResult, type KvNamespace, MediaLocalParticipant, type MediaParticipant, type MediaParticipantInfo, MediaRoom, MediaRoomEvent, type MediaRoomInfo, type MediaRoomInfoSettings, type MediaRoomOptions, type MediaService, type MediaTokenResult, type MediaTrackPublication, type Platform, type PlatformEnv, type PresenceMember, type PresenceService, RealtimeClient, type RealtimeClientOptions, type RealtimeEvent, type RealtimeService, RemoteMediaService, RenClient, type RenClientOptions, type RenEvent, type Storage$1 as Storage, type StoragePutStreamSource, type TrackKind, type TrackSource, type VideoResolution, attachTrack, clearPlatformCache, detachTrack, getOrCreateClientId, getPlatform, renFetch, storageDelete, storageUpload };
1244
+ export { type AuthField, type AuthService, type AuthSession, type AuthUser, type Database, type DbFindOptions, type KvListResult, type KvNamespace, type LoginOptions, MediaLocalParticipant, type MediaParticipant, type MediaParticipantInfo, MediaRoom, MediaRoomEvent, type MediaRoomInfo, type MediaRoomInfoSettings, type MediaRoomOptions, type MediaService, type MediaTokenResult, type MediaTrackPublication, type Platform, type PlatformEnv, type PresenceMember, type PresenceService, RealtimeClient, type RealtimeClientOptions, type RealtimeEvent, type RealtimeService, type RegisterOptions, RemoteMediaService, RenClient, type RenClientOptions, type RenEvent, type Storage$1 as Storage, type StoragePutStreamSource, type TrackKind, type TrackSource, type UpdateUserOptions, type UserListFilter, type UserListResponse, type VideoResolution, attachTrack, clearPlatformCache, detachTrack, getOrCreateClientId, getPlatform, renFetch, storageDelete, storageUpload };
package/dist/index.js CHANGED
@@ -380,6 +380,108 @@ var RemoteRealtimeService = class {
380
380
  return data.channels ?? [];
381
381
  }
382
382
  };
383
+ var RemoteAuthService = class {
384
+ constructor(baseUrl, headers) {
385
+ this.baseUrl = baseUrl;
386
+ this.headers = headers;
387
+ }
388
+ baseUrl;
389
+ headers;
390
+ async post(path, body) {
391
+ const res = await fetch(`${this.baseUrl}/api/platform/auth${path}`, {
392
+ method: "POST",
393
+ headers: this.headers,
394
+ body: body !== void 0 ? JSON.stringify(body) : void 0
395
+ });
396
+ if (!res.ok) {
397
+ const text = await res.text();
398
+ throw new Error(`Auth error (${res.status}): ${text}`);
399
+ }
400
+ if (res.status === 204) return void 0;
401
+ return res.json();
402
+ }
403
+ async register(options) {
404
+ return this.post("/register", options);
405
+ }
406
+ async login(options) {
407
+ return this.post("/login", options);
408
+ }
409
+ async validate(accessToken) {
410
+ return this.post("/validate", { token: accessToken });
411
+ }
412
+ async refresh(refreshToken) {
413
+ return this.post("/refresh", { token: refreshToken });
414
+ }
415
+ async logout(sessionId) {
416
+ await this.post("/logout", { session_id: sessionId });
417
+ }
418
+ async getUser(userId) {
419
+ return this.post("/get-user", { user_id: userId });
420
+ }
421
+ async listUsers(filter) {
422
+ return this.post("/list-users", filter ?? {});
423
+ }
424
+ async updateUser(userId, update) {
425
+ return this.post("/update-user", { user_id: userId, ...update });
426
+ }
427
+ async deleteUser(userId) {
428
+ await this.post("/delete-user", { user_id: userId });
429
+ }
430
+ async sendVerification(userId) {
431
+ return this.post("/send-verification", { user_id: userId });
432
+ }
433
+ async verifyEmail(token) {
434
+ await this.post("/verify-email", { token });
435
+ }
436
+ async sendPasswordReset(email) {
437
+ return this.post("/send-password-reset", { email });
438
+ }
439
+ async resetPassword(token, newPassword) {
440
+ await this.post("/reset-password", { token, new_password: newPassword });
441
+ }
442
+ async changePassword(userId, oldPassword, newPassword) {
443
+ await this.post("/change-password", { user_id: userId, old_password: oldPassword, new_password: newPassword });
444
+ }
445
+ async getFieldConfig() {
446
+ return this.post("/field-config");
447
+ }
448
+ async getOAuthUrl(provider, options) {
449
+ return this.post("/oauth/start", { provider, redirect_uri: options?.redirectUri || `/_auth/callback/${provider}` });
450
+ }
451
+ async handleOAuthCallback(provider, params) {
452
+ return this.post("/oauth/callback", { provider, code: params.code, state: params.state });
453
+ }
454
+ withAuth(handler) {
455
+ const self = this;
456
+ return async function(request) {
457
+ let token = null;
458
+ const authHeader = request.headers.get("Authorization");
459
+ if (authHeader?.startsWith("Bearer ")) {
460
+ token = authHeader.slice(7);
461
+ } else {
462
+ const cookies = request.headers.get("Cookie") ?? "";
463
+ const match = cookies.match(/__session=([^;]+)/);
464
+ if (match) token = match[1];
465
+ }
466
+ if (!token) {
467
+ return new Response(JSON.stringify({ error: "Authentication required" }), {
468
+ status: 401,
469
+ headers: { "Content-Type": "application/json" }
470
+ });
471
+ }
472
+ try {
473
+ const user = await self.validate(token);
474
+ request.user = user;
475
+ return handler(request);
476
+ } catch {
477
+ return new Response(JSON.stringify({ error: "Invalid or expired token" }), {
478
+ status: 401,
479
+ headers: { "Content-Type": "application/json" }
480
+ });
481
+ }
482
+ };
483
+ }
484
+ };
383
485
  function createRemoteClient(baseUrl, tenant) {
384
486
  const headers = {
385
487
  "Content-Type": "application/json",
@@ -394,6 +496,7 @@ function createRemoteClient(baseUrl, tenant) {
394
496
  const storage = new RemoteStorage(baseUrl, headers);
395
497
  const media = new RemoteMediaService(baseUrl, headers);
396
498
  const realtime = new RemoteRealtimeService(baseUrl, headers);
499
+ const auth = new RemoteAuthService(baseUrl, headers);
397
500
  return {
398
501
  env: {
399
502
  KV: kvProxy,
@@ -401,7 +504,8 @@ function createRemoteClient(baseUrl, tenant) {
401
504
  STORAGE: storage
402
505
  },
403
506
  media,
404
- realtime
507
+ realtime,
508
+ auth
405
509
  };
406
510
  }
407
511
 
@@ -677,7 +781,7 @@ var RealtimeClient = class {
677
781
  listeners.forEach((cb) => cb(event));
678
782
  }
679
783
  }
680
- if (event.event === "presence:join" || event.event === "presence:leave") {
784
+ if (event.event === "presence:join" || event.event === "presence:leave" || event.event === "presence:update") {
681
785
  const presenceSet = this.presenceListeners.get(event.channel);
682
786
  if (presenceSet) {
683
787
  const member = {
@@ -687,6 +791,8 @@ var RealtimeClient = class {
687
791
  };
688
792
  if (event.event === "presence:join") {
689
793
  presenceSet.onJoin.forEach((cb) => cb(member));
794
+ } else if (event.event === "presence:update") {
795
+ presenceSet.onUpdate.forEach((cb) => cb(member));
690
796
  } else {
691
797
  presenceSet.onLeave.forEach((cb) => cb(member));
692
798
  }
@@ -740,7 +846,8 @@ var RealtimeClient = class {
740
846
  if (!this.presenceListeners.has(channel)) {
741
847
  this.presenceListeners.set(channel, {
742
848
  onJoin: /* @__PURE__ */ new Set(),
743
- onLeave: /* @__PURE__ */ new Set()
849
+ onLeave: /* @__PURE__ */ new Set(),
850
+ onUpdate: /* @__PURE__ */ new Set()
744
851
  });
745
852
  }
746
853
  const listeners = this.presenceListeners.get(channel);
@@ -755,6 +862,18 @@ var RealtimeClient = class {
755
862
  metadata
756
863
  });
757
864
  },
865
+ /** Update metadata for the current presence (must have joined first) */
866
+ update: (metadata) => {
867
+ const info = this.joinedPresence.get(channel);
868
+ if (!info) return;
869
+ info.metadata = metadata;
870
+ this.sendRaw({
871
+ action: "presence:update",
872
+ channel,
873
+ userId: info.userId,
874
+ metadata
875
+ });
876
+ },
758
877
  /** Leave the channel */
759
878
  leave: () => {
760
879
  this.joinedPresence.delete(channel);
@@ -767,6 +886,13 @@ var RealtimeClient = class {
767
886
  listeners.onJoin.delete(callback);
768
887
  };
769
888
  },
889
+ /** Listen for metadata updates */
890
+ onUpdate: (callback) => {
891
+ listeners.onUpdate.add(callback);
892
+ return () => {
893
+ listeners.onUpdate.delete(callback);
894
+ };
895
+ },
770
896
  /** Listen for users leaving */
771
897
  onLeave: (callback) => {
772
898
  listeners.onLeave.add(callback);
@@ -1048,7 +1174,7 @@ var MediaRoom = class _MediaRoom {
1048
1174
  };
1049
1175
 
1050
1176
  // src/index.ts
1051
- var cachedPlatform = null;
1177
+ var cachedPlatform = void 0;
1052
1178
  function getPlatform(options) {
1053
1179
  if (cachedPlatform) {
1054
1180
  return cachedPlatform;
@@ -1075,7 +1201,7 @@ function getPlatform(options) {
1075
1201
  return cachedPlatform;
1076
1202
  }
1077
1203
  function clearPlatformCache() {
1078
- cachedPlatform = null;
1204
+ cachedPlatform = void 0;
1079
1205
  if (typeof globalThis !== "undefined") {
1080
1206
  globalThis.__maravilla_platform = void 0;
1081
1207
  }