@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 +279 -1
- package/dist/index.js +131 -5
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/index.ts +2 -2
- package/src/realtime.ts +24 -1
- package/src/remote-client.ts +128 -1
- package/src/types.ts +285 -0
package/src/remote-client.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { KvNamespace, KvListResult, Database, DbFindOptions, Storage, RealtimeService, PresenceService } from './types.js';
|
|
1
|
+
import type { KvNamespace, KvListResult, Database, DbFindOptions, Storage, RealtimeService, PresenceService, AuthService, AuthUser, AuthSession, AuthField, RegisterOptions, LoginOptions, UserListFilter, UserListResponse, UpdateUserOptions } from './types.js';
|
|
2
2
|
import { RemoteMediaService } from './media.js';
|
|
3
3
|
|
|
4
4
|
/**
|
|
@@ -405,6 +405,131 @@ class RemoteRealtimeService implements RealtimeService {
|
|
|
405
405
|
}
|
|
406
406
|
}
|
|
407
407
|
|
|
408
|
+
/**
|
|
409
|
+
* Remote auth service for development environments.
|
|
410
|
+
* Calls the dev-server's platform auth ops via HTTP.
|
|
411
|
+
* @internal
|
|
412
|
+
*/
|
|
413
|
+
class RemoteAuthService implements AuthService {
|
|
414
|
+
constructor(
|
|
415
|
+
private baseUrl: string,
|
|
416
|
+
private headers: Record<string, string>
|
|
417
|
+
) {}
|
|
418
|
+
|
|
419
|
+
private async post(path: string, body?: any): Promise<any> {
|
|
420
|
+
const res = await fetch(`${this.baseUrl}/api/platform/auth${path}`, {
|
|
421
|
+
method: 'POST',
|
|
422
|
+
headers: this.headers,
|
|
423
|
+
body: body !== undefined ? JSON.stringify(body) : undefined,
|
|
424
|
+
});
|
|
425
|
+
if (!res.ok) {
|
|
426
|
+
const text = await res.text();
|
|
427
|
+
throw new Error(`Auth error (${res.status}): ${text}`);
|
|
428
|
+
}
|
|
429
|
+
if (res.status === 204) return undefined;
|
|
430
|
+
return res.json();
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
async register(options: RegisterOptions): Promise<AuthUser> {
|
|
434
|
+
return this.post('/register', options);
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
async login(options: LoginOptions): Promise<AuthSession> {
|
|
438
|
+
return this.post('/login', options);
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
async validate(accessToken: string): Promise<AuthUser> {
|
|
442
|
+
return this.post('/validate', { token: accessToken });
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
async refresh(refreshToken: string): Promise<AuthSession> {
|
|
446
|
+
return this.post('/refresh', { token: refreshToken });
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
async logout(sessionId: string): Promise<void> {
|
|
450
|
+
await this.post('/logout', { session_id: sessionId });
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
async getUser(userId: string): Promise<AuthUser | null> {
|
|
454
|
+
return this.post('/get-user', { user_id: userId });
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
async listUsers(filter?: UserListFilter): Promise<UserListResponse> {
|
|
458
|
+
return this.post('/list-users', filter ?? {});
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
async updateUser(userId: string, update: UpdateUserOptions): Promise<AuthUser> {
|
|
462
|
+
return this.post('/update-user', { user_id: userId, ...update });
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
async deleteUser(userId: string): Promise<void> {
|
|
466
|
+
await this.post('/delete-user', { user_id: userId });
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
async sendVerification(userId: string): Promise<{ token: string }> {
|
|
470
|
+
return this.post('/send-verification', { user_id: userId });
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
async verifyEmail(token: string): Promise<void> {
|
|
474
|
+
await this.post('/verify-email', { token });
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
async sendPasswordReset(email: string): Promise<{ token: string }> {
|
|
478
|
+
return this.post('/send-password-reset', { email });
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
async resetPassword(token: string, newPassword: string): Promise<void> {
|
|
482
|
+
await this.post('/reset-password', { token, new_password: newPassword });
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
async changePassword(userId: string, oldPassword: string, newPassword: string): Promise<void> {
|
|
486
|
+
await this.post('/change-password', { user_id: userId, old_password: oldPassword, new_password: newPassword });
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
async getFieldConfig(): Promise<{ fields: AuthField[] }> {
|
|
490
|
+
return this.post('/field-config');
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
async getOAuthUrl(provider: string, options?: { redirectUri?: string }): Promise<{ auth_url: string; state: string }> {
|
|
494
|
+
return this.post('/oauth/start', { provider, redirect_uri: options?.redirectUri || `/_auth/callback/${provider}` });
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
async handleOAuthCallback(provider: string, params: { code: string; state: string }): Promise<any> {
|
|
498
|
+
return this.post('/oauth/callback', { provider, code: params.code, state: params.state });
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
withAuth<T extends (request: Request & { user: AuthUser }) => Promise<Response>>(
|
|
502
|
+
handler: T
|
|
503
|
+
): (request: Request) => Promise<Response> {
|
|
504
|
+
const self = this;
|
|
505
|
+
return async function(request: Request): Promise<Response> {
|
|
506
|
+
let token: string | null = null;
|
|
507
|
+
const authHeader = request.headers.get('Authorization');
|
|
508
|
+
if (authHeader?.startsWith('Bearer ')) {
|
|
509
|
+
token = authHeader.slice(7);
|
|
510
|
+
} else {
|
|
511
|
+
const cookies = request.headers.get('Cookie') ?? '';
|
|
512
|
+
const match = cookies.match(/__session=([^;]+)/);
|
|
513
|
+
if (match) token = match[1];
|
|
514
|
+
}
|
|
515
|
+
if (!token) {
|
|
516
|
+
return new Response(JSON.stringify({ error: 'Authentication required' }), {
|
|
517
|
+
status: 401, headers: { 'Content-Type': 'application/json' },
|
|
518
|
+
});
|
|
519
|
+
}
|
|
520
|
+
try {
|
|
521
|
+
const user = await self.validate(token);
|
|
522
|
+
(request as any).user = user;
|
|
523
|
+
return handler(request as Request & { user: AuthUser });
|
|
524
|
+
} catch {
|
|
525
|
+
return new Response(JSON.stringify({ error: 'Invalid or expired token' }), {
|
|
526
|
+
status: 401, headers: { 'Content-Type': 'application/json' },
|
|
527
|
+
});
|
|
528
|
+
}
|
|
529
|
+
};
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
|
|
408
533
|
/**
|
|
409
534
|
* Create a remote platform client for development environments.
|
|
410
535
|
*
|
|
@@ -446,6 +571,7 @@ export function createRemoteClient(baseUrl: string, tenant: string) {
|
|
|
446
571
|
const storage = new RemoteStorage(baseUrl, headers);
|
|
447
572
|
const media = new RemoteMediaService(baseUrl, headers);
|
|
448
573
|
const realtime = new RemoteRealtimeService(baseUrl, headers);
|
|
574
|
+
const auth = new RemoteAuthService(baseUrl, headers);
|
|
449
575
|
|
|
450
576
|
return {
|
|
451
577
|
env: {
|
|
@@ -455,5 +581,6 @@ export function createRemoteClient(baseUrl: string, tenant: string) {
|
|
|
455
581
|
},
|
|
456
582
|
media,
|
|
457
583
|
realtime,
|
|
584
|
+
auth,
|
|
458
585
|
};
|
|
459
586
|
}
|
package/src/types.ts
CHANGED
|
@@ -559,6 +559,289 @@ 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
|
+
* Session returned after successful login or token refresh.
|
|
590
|
+
*/
|
|
591
|
+
export interface AuthSession {
|
|
592
|
+
/** Short-lived JWT access token (default 15 min) */
|
|
593
|
+
access_token: string;
|
|
594
|
+
/** Single-use opaque refresh token (default 30 days) */
|
|
595
|
+
refresh_token: string;
|
|
596
|
+
/** Access token lifetime in seconds */
|
|
597
|
+
expires_in: number;
|
|
598
|
+
/** The authenticated user */
|
|
599
|
+
user: AuthUser;
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
/**
|
|
603
|
+
* Custom registration field defined in project auth settings.
|
|
604
|
+
*/
|
|
605
|
+
export interface AuthField {
|
|
606
|
+
/** Field key (used as form field name) */
|
|
607
|
+
key: string;
|
|
608
|
+
/** Display label */
|
|
609
|
+
label: string;
|
|
610
|
+
/** Field type: text, email, phone, date, number, select, boolean, url, textarea */
|
|
611
|
+
field_type: string;
|
|
612
|
+
/** Whether the field is required */
|
|
613
|
+
required: boolean;
|
|
614
|
+
/** Whether the field appears on the registration form */
|
|
615
|
+
show_on_register: boolean;
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
/**
|
|
619
|
+
* Options for registering a new user.
|
|
620
|
+
*/
|
|
621
|
+
export interface RegisterOptions {
|
|
622
|
+
/** User's email address */
|
|
623
|
+
email: string;
|
|
624
|
+
/** Password (minimum 8 characters) */
|
|
625
|
+
password: string;
|
|
626
|
+
/** Optional profile data (custom fields) */
|
|
627
|
+
profile?: Record<string, any>;
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
/**
|
|
631
|
+
* Options for logging in.
|
|
632
|
+
*/
|
|
633
|
+
export interface LoginOptions {
|
|
634
|
+
/** User's email address */
|
|
635
|
+
email: string;
|
|
636
|
+
/** User's password */
|
|
637
|
+
password: string;
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
/**
|
|
641
|
+
* Filter options for listing users.
|
|
642
|
+
*/
|
|
643
|
+
export interface UserListFilter {
|
|
644
|
+
/** Max results per page (default 50) */
|
|
645
|
+
limit?: number;
|
|
646
|
+
/** Number of results to skip */
|
|
647
|
+
offset?: number;
|
|
648
|
+
/** Filter by account status */
|
|
649
|
+
status?: 'active' | 'suspended' | 'deactivated';
|
|
650
|
+
/** Filter by email (partial match) */
|
|
651
|
+
email_contains?: string;
|
|
652
|
+
/** Filter by group ID */
|
|
653
|
+
group_id?: string;
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
/**
|
|
657
|
+
* Paginated user list response.
|
|
658
|
+
*/
|
|
659
|
+
export interface UserListResponse {
|
|
660
|
+
/** Users in this page */
|
|
661
|
+
users: AuthUser[];
|
|
662
|
+
/** Total number of matching users */
|
|
663
|
+
total: number;
|
|
664
|
+
/** Page size */
|
|
665
|
+
limit: number;
|
|
666
|
+
/** Offset */
|
|
667
|
+
offset: number;
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
/**
|
|
671
|
+
* Options for updating a user.
|
|
672
|
+
*/
|
|
673
|
+
export interface UpdateUserOptions {
|
|
674
|
+
/** New email address */
|
|
675
|
+
email?: string;
|
|
676
|
+
/** New status */
|
|
677
|
+
status?: 'active' | 'suspended' | 'deactivated';
|
|
678
|
+
/** Profile data to merge */
|
|
679
|
+
profile?: Record<string, any>;
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
/**
|
|
683
|
+
* Auth service for end-user authentication and user management.
|
|
684
|
+
*
|
|
685
|
+
* @example
|
|
686
|
+
* ```typescript
|
|
687
|
+
* const platform = getPlatform();
|
|
688
|
+
*
|
|
689
|
+
* // Register a new user
|
|
690
|
+
* const user = await platform.auth.register({
|
|
691
|
+
* email: 'user@example.com',
|
|
692
|
+
* password: 'securePassword123'
|
|
693
|
+
* });
|
|
694
|
+
*
|
|
695
|
+
* // Login
|
|
696
|
+
* const session = await platform.auth.login({
|
|
697
|
+
* email: 'user@example.com',
|
|
698
|
+
* password: 'securePassword123'
|
|
699
|
+
* });
|
|
700
|
+
* // session.access_token — short-lived JWT
|
|
701
|
+
* // session.refresh_token — single-use refresh token
|
|
702
|
+
*
|
|
703
|
+
* // Validate a token (e.g. from Authorization header or cookie)
|
|
704
|
+
* const user = await platform.auth.validate(session.access_token);
|
|
705
|
+
*
|
|
706
|
+
* // Protect a route with withAuth middleware
|
|
707
|
+
* export default {
|
|
708
|
+
* fetch: platform.auth.withAuth(async (request) => {
|
|
709
|
+
* // request.user is guaranteed to be set
|
|
710
|
+
* return new Response(`Hello ${request.user.email}`);
|
|
711
|
+
* })
|
|
712
|
+
* };
|
|
713
|
+
* ```
|
|
714
|
+
*/
|
|
715
|
+
export interface AuthService {
|
|
716
|
+
/**
|
|
717
|
+
* Register a new user with email and password.
|
|
718
|
+
* @returns The created user (not yet email-verified)
|
|
719
|
+
*/
|
|
720
|
+
register(options: RegisterOptions): Promise<AuthUser>;
|
|
721
|
+
|
|
722
|
+
/**
|
|
723
|
+
* Authenticate a user and create a session.
|
|
724
|
+
* @returns Session with access token, refresh token, and user info
|
|
725
|
+
*/
|
|
726
|
+
login(options: LoginOptions): Promise<AuthSession>;
|
|
727
|
+
|
|
728
|
+
/**
|
|
729
|
+
* Validate an access token and return the authenticated user.
|
|
730
|
+
* @param accessToken - JWT access token from login or refresh
|
|
731
|
+
* @throws If the token is invalid or expired
|
|
732
|
+
*/
|
|
733
|
+
validate(accessToken: string): Promise<AuthUser>;
|
|
734
|
+
|
|
735
|
+
/**
|
|
736
|
+
* Refresh a session using a refresh token (single-use).
|
|
737
|
+
* @param refreshToken - The refresh token from a previous login/refresh
|
|
738
|
+
* @returns New session with fresh access and refresh tokens
|
|
739
|
+
*/
|
|
740
|
+
refresh(refreshToken: string): Promise<AuthSession>;
|
|
741
|
+
|
|
742
|
+
/**
|
|
743
|
+
* Revoke a specific session.
|
|
744
|
+
*/
|
|
745
|
+
logout(sessionId: string): Promise<void>;
|
|
746
|
+
|
|
747
|
+
/**
|
|
748
|
+
* Get a user by ID.
|
|
749
|
+
* @returns The user, or null if not found
|
|
750
|
+
*/
|
|
751
|
+
getUser(userId: string): Promise<AuthUser | null>;
|
|
752
|
+
|
|
753
|
+
/**
|
|
754
|
+
* List users with optional filtering and pagination.
|
|
755
|
+
*/
|
|
756
|
+
listUsers(filter?: UserListFilter): Promise<UserListResponse>;
|
|
757
|
+
|
|
758
|
+
/**
|
|
759
|
+
* Update a user's email, status, or profile data.
|
|
760
|
+
*/
|
|
761
|
+
updateUser(userId: string, update: UpdateUserOptions): Promise<AuthUser>;
|
|
762
|
+
|
|
763
|
+
/**
|
|
764
|
+
* Delete a user and all their sessions.
|
|
765
|
+
*/
|
|
766
|
+
deleteUser(userId: string): Promise<void>;
|
|
767
|
+
|
|
768
|
+
/**
|
|
769
|
+
* Create an email verification token.
|
|
770
|
+
* @returns The verification token (caller decides how to deliver it)
|
|
771
|
+
*/
|
|
772
|
+
sendVerification(userId: string): Promise<{ token: string }>;
|
|
773
|
+
|
|
774
|
+
/**
|
|
775
|
+
* Verify an email address using a verification token.
|
|
776
|
+
*/
|
|
777
|
+
verifyEmail(token: string): Promise<void>;
|
|
778
|
+
|
|
779
|
+
/**
|
|
780
|
+
* Create a password reset token for an email address.
|
|
781
|
+
* @returns The reset token (caller decides how to deliver it)
|
|
782
|
+
*/
|
|
783
|
+
sendPasswordReset(email: string): Promise<{ token: string }>;
|
|
784
|
+
|
|
785
|
+
/**
|
|
786
|
+
* Reset a password using a reset token.
|
|
787
|
+
*/
|
|
788
|
+
resetPassword(token: string, newPassword: string): Promise<void>;
|
|
789
|
+
|
|
790
|
+
/**
|
|
791
|
+
* Change a user's password (requires old password).
|
|
792
|
+
*/
|
|
793
|
+
changePassword(userId: string, oldPassword: string, newPassword: string): Promise<void>;
|
|
794
|
+
|
|
795
|
+
/**
|
|
796
|
+
* Get the configured registration fields for this project.
|
|
797
|
+
*/
|
|
798
|
+
getFieldConfig(): Promise<{ fields: AuthField[] }>;
|
|
799
|
+
|
|
800
|
+
/**
|
|
801
|
+
* Start an OAuth flow by generating an authorization URL.
|
|
802
|
+
* Redirect the user to the returned URL to begin authentication.
|
|
803
|
+
*
|
|
804
|
+
* @param provider - Provider name: "google", "github", "okta", or "custom_oidc"
|
|
805
|
+
* @param options - Optional configuration
|
|
806
|
+
* @returns Object with `auth_url` (redirect target) and `state` (for CSRF verification)
|
|
807
|
+
*/
|
|
808
|
+
getOAuthUrl(provider: string, options?: { redirectUri?: string }): Promise<{ auth_url: string; state: string }>;
|
|
809
|
+
|
|
810
|
+
/**
|
|
811
|
+
* Complete an OAuth flow by exchanging the authorization code.
|
|
812
|
+
* Call this after the provider redirects back with a code and state.
|
|
813
|
+
*
|
|
814
|
+
* @param provider - Provider name
|
|
815
|
+
* @param params - The code and state from the OAuth callback
|
|
816
|
+
* @returns Either a session (user authenticated) or a link_required result
|
|
817
|
+
*/
|
|
818
|
+
handleOAuthCallback(provider: string, params: { code: string; state: string }): Promise<
|
|
819
|
+
| AuthSession
|
|
820
|
+
| { type: 'LinkRequired'; email: string; provider: string; provider_id: string; existing_user_id: string }
|
|
821
|
+
>;
|
|
822
|
+
|
|
823
|
+
/**
|
|
824
|
+
* Middleware helper that validates auth and injects `request.user`.
|
|
825
|
+
* Returns 401 JSON response if no valid token is found.
|
|
826
|
+
*
|
|
827
|
+
* Extracts token from `Authorization: Bearer <token>` header
|
|
828
|
+
* or `__session` cookie.
|
|
829
|
+
*
|
|
830
|
+
* @example
|
|
831
|
+
* ```typescript
|
|
832
|
+
* export default {
|
|
833
|
+
* fetch: platform.auth.withAuth(async (request) => {
|
|
834
|
+
* const data = await platform.db.items.find({ owner: request.user.id });
|
|
835
|
+
* return Response.json(data);
|
|
836
|
+
* })
|
|
837
|
+
* };
|
|
838
|
+
* ```
|
|
839
|
+
*/
|
|
840
|
+
withAuth<T extends (request: Request & { user: AuthUser }) => Promise<Response>>(
|
|
841
|
+
handler: T
|
|
842
|
+
): (request: Request) => Promise<Response>;
|
|
843
|
+
}
|
|
844
|
+
|
|
562
845
|
export interface Platform {
|
|
563
846
|
/** Environment containing all available platform services */
|
|
564
847
|
env: PlatformEnv;
|
|
@@ -566,4 +849,6 @@ export interface Platform {
|
|
|
566
849
|
media?: import('./media.js').MediaService;
|
|
567
850
|
/** Realtime service for pub/sub channels and presence */
|
|
568
851
|
realtime: RealtimeService;
|
|
852
|
+
/** Auth service for end-user authentication and user management */
|
|
853
|
+
auth: AuthService;
|
|
569
854
|
}
|