Skip to content

Commit 3124ac9

Browse files
Copilothotlong
andcommitted
Add auth endpoint specification and update client to use better-auth paths
Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com>
1 parent 9d23013 commit 3124ac9

5 files changed

Lines changed: 456 additions & 10 deletions

File tree

content/docs/references/api/auth.mdx

Lines changed: 121 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,136 @@
11
---
22
title: Auth
3-
description: Auth protocol schemas
3+
description: Auth protocol schemas and endpoints
44
---
55

66
Authentication Service Protocol
77

88
Defines the standard API contracts for Identity, Session Management,
9-
109
and Access Control.
1110

1211
<Callout type="info">
13-
**Source:** `packages/spec/src/api/auth.zod.ts`
12+
**Source:** `packages/spec/src/api/auth.zod.ts`, `packages/spec/src/api/auth-endpoints.zod.ts`
1413
</Callout>
1514

15+
## Endpoints
16+
17+
The authentication service uses [better-auth](https://www.better-auth.com/) endpoints as the canonical API contract.
18+
All endpoints are relative to the auth base path (default: `/api/v1/auth`).
19+
20+
### Email/Password Authentication
21+
22+
| Endpoint | Method | Path | Description |
23+
| :--- | :--- | :--- | :--- |
24+
| **Sign In** | `POST` | `/sign-in/email` | Sign in with email and password |
25+
| **Sign Up** | `POST` | `/sign-up/email` | Register new user with email and password |
26+
| **Sign Out** | `POST` | `/sign-out` | Sign out current user |
27+
28+
### Session Management
29+
30+
| Endpoint | Method | Path | Description |
31+
| :--- | :--- | :--- | :--- |
32+
| **Get Session** | `GET` | `/get-session` | Get current user session |
33+
34+
### Password Management
35+
36+
| Endpoint | Method | Path | Description |
37+
| :--- | :--- | :--- | :--- |
38+
| **Forget Password** | `POST` | `/forget-password` | Request password reset email |
39+
| **Reset Password** | `POST` | `/reset-password` | Reset password with token |
40+
41+
### Email Verification
42+
43+
| Endpoint | Method | Path | Description |
44+
| :--- | :--- | :--- | :--- |
45+
| **Send Verification** | `POST` | `/send-verification-email` | Send email verification link |
46+
| **Verify Email** | `GET` | `/verify-email` | Verify email with token |
47+
48+
### OAuth (when providers configured)
49+
50+
| Endpoint | Method | Path | Description |
51+
| :--- | :--- | :--- | :--- |
52+
| **Authorize** | `GET` | `/authorize/:provider` | Start OAuth flow |
53+
| **Callback** | `GET` | `/callback/:provider` | OAuth callback |
54+
55+
### 2FA (when enabled)
56+
57+
| Endpoint | Method | Path | Description |
58+
| :--- | :--- | :--- | :--- |
59+
| **Enable 2FA** | `POST` | `/two-factor/enable` | Enable two-factor authentication |
60+
| **Verify 2FA** | `POST` | `/two-factor/verify` | Verify 2FA code |
61+
62+
### Passkeys (when enabled)
63+
64+
| Endpoint | Method | Path | Description |
65+
| :--- | :--- | :--- | :--- |
66+
| **Register Passkey** | `POST` | `/passkey/register` | Register a passkey |
67+
| **Authenticate** | `POST` | `/passkey/authenticate` | Authenticate with passkey |
68+
69+
### Magic Links (when enabled)
70+
71+
| Endpoint | Method | Path | Description |
72+
| :--- | :--- | :--- | :--- |
73+
| **Send Magic Link** | `POST` | `/magic-link/send` | Send magic link email |
74+
| **Verify Magic Link** | `GET` | `/magic-link/verify` | Verify magic link |
75+
76+
## Usage Examples
77+
78+
### Using the ObjectStack Client
79+
80+
```typescript
81+
import { ObjectStackClient } from '@objectstack/client';
82+
83+
const client = new ObjectStackClient({
84+
baseUrl: 'http://localhost:3000'
85+
});
86+
87+
// Register
88+
await client.auth.register({
89+
email: 'user@example.com',
90+
password: 'SecurePassword123!',
91+
name: 'John Doe'
92+
});
93+
94+
// Login
95+
await client.auth.login({
96+
type: 'email',
97+
email: 'user@example.com',
98+
password: 'SecurePassword123!'
99+
});
100+
101+
// Get session
102+
const session = await client.auth.me();
103+
104+
// Logout
105+
await client.auth.logout();
106+
```
107+
108+
### Using Direct API Calls
109+
110+
```bash
111+
# Register
112+
curl -X POST http://localhost:3000/api/v1/auth/sign-up/email \
113+
-H "Content-Type: application/json" \
114+
-d '{"email":"user@example.com","password":"SecurePassword123!","name":"John Doe"}'
115+
116+
# Login
117+
curl -X POST http://localhost:3000/api/v1/auth/sign-in/email \
118+
-H "Content-Type: application/json" \
119+
-d '{"email":"user@example.com","password":"SecurePassword123!"}'
120+
121+
# Get session
122+
curl http://localhost:3000/api/v1/auth/get-session \
123+
-H "Authorization: Bearer YOUR_TOKEN"
124+
125+
# Logout
126+
curl -X POST http://localhost:3000/api/v1/auth/sign-out \
127+
-H "Authorization: Bearer YOUR_TOKEN"
128+
```
129+
130+
---
131+
132+
## Request/Response Schemas
133+
16134
## TypeScript Usage
17135

18136
```typescript

packages/client/src/index.ts

Lines changed: 22 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -479,9 +479,13 @@ export class ObjectStackClient {
479479
* Authentication Services
480480
*/
481481
auth = {
482+
/**
483+
* Login with email and password
484+
* Uses better-auth endpoint: POST /sign-in/email
485+
*/
482486
login: async (request: LoginRequest): Promise<SessionResponse> => {
483487
const route = this.getRoute('auth');
484-
const res = await this.fetch(`${this.baseUrl}${route}/login`, {
488+
const res = await this.fetch(`${this.baseUrl}${route}/sign-in/email`, {
485489
method: 'POST',
486490
body: JSON.stringify(request)
487491
});
@@ -493,24 +497,33 @@ export class ObjectStackClient {
493497
return data;
494498
},
495499

500+
/**
501+
* Logout current user
502+
* Uses better-auth endpoint: POST /sign-out
503+
*/
496504
logout: async () => {
497505
const route = this.getRoute('auth');
498-
await this.fetch(`${this.baseUrl}${route}/logout`, { method: 'POST' });
506+
await this.fetch(`${this.baseUrl}${route}/sign-out`, { method: 'POST' });
499507
this.token = undefined;
500508
},
501509

510+
/**
511+
* Get current user session
512+
* Uses better-auth endpoint: GET /get-session
513+
*/
502514
me: async (): Promise<SessionResponse> => {
503515
const route = this.getRoute('auth');
504-
const res = await this.fetch(`${this.baseUrl}${route}/me`);
516+
const res = await this.fetch(`${this.baseUrl}${route}/get-session`);
505517
return res.json();
506518
},
507519

508520
/**
509521
* Register a new user account
522+
* Uses better-auth endpoint: POST /sign-up/email
510523
*/
511524
register: async (request: RegisterRequest): Promise<SessionResponse> => {
512525
const route = this.getRoute('auth');
513-
const res = await this.fetch(`${this.baseUrl}${route}/register`, {
526+
const res = await this.fetch(`${this.baseUrl}${route}/sign-up/email`, {
514527
method: 'POST',
515528
body: JSON.stringify(request)
516529
});
@@ -523,12 +536,14 @@ export class ObjectStackClient {
523536

524537
/**
525538
* Refresh an authentication token
539+
* Note: better-auth handles token refresh automatically via /get-session
526540
*/
527541
refreshToken: async (refreshToken: string): Promise<SessionResponse> => {
528542
const route = this.getRoute('auth');
529-
const res = await this.fetch(`${this.baseUrl}${route}/refresh`, {
530-
method: 'POST',
531-
body: JSON.stringify({ refreshToken })
543+
// better-auth doesn't have a separate refresh endpoint
544+
// Session refresh is handled automatically when calling /get-session
545+
const res = await this.fetch(`${this.baseUrl}${route}/get-session`, {
546+
method: 'GET'
532547
});
533548
const data = await res.json();
534549
if (data.data?.token) {
Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
2+
3+
import { describe, it, expect } from 'vitest';
4+
import {
5+
AuthEndpointPaths,
6+
AuthEndpointSchema,
7+
AuthEndpointAliases,
8+
EndpointMapping,
9+
getAuthEndpointUrl,
10+
} from './auth-endpoints.zod';
11+
12+
describe('AuthEndpointPaths', () => {
13+
it('should define email/password authentication endpoints', () => {
14+
expect(AuthEndpointPaths.signInEmail).toBe('/sign-in/email');
15+
expect(AuthEndpointPaths.signUpEmail).toBe('/sign-up/email');
16+
expect(AuthEndpointPaths.signOut).toBe('/sign-out');
17+
});
18+
19+
it('should define session management endpoints', () => {
20+
expect(AuthEndpointPaths.getSession).toBe('/get-session');
21+
});
22+
23+
it('should define password management endpoints', () => {
24+
expect(AuthEndpointPaths.forgetPassword).toBe('/forget-password');
25+
expect(AuthEndpointPaths.resetPassword).toBe('/reset-password');
26+
});
27+
28+
it('should define email verification endpoints', () => {
29+
expect(AuthEndpointPaths.sendVerificationEmail).toBe('/send-verification-email');
30+
expect(AuthEndpointPaths.verifyEmail).toBe('/verify-email');
31+
});
32+
33+
it('should define 2FA endpoints', () => {
34+
expect(AuthEndpointPaths.twoFactorEnable).toBe('/two-factor/enable');
35+
expect(AuthEndpointPaths.twoFactorVerify).toBe('/two-factor/verify');
36+
});
37+
38+
it('should define passkey endpoints', () => {
39+
expect(AuthEndpointPaths.passkeyRegister).toBe('/passkey/register');
40+
expect(AuthEndpointPaths.passkeyAuthenticate).toBe('/passkey/authenticate');
41+
});
42+
43+
it('should define magic link endpoints', () => {
44+
expect(AuthEndpointPaths.magicLinkSend).toBe('/magic-link/send');
45+
expect(AuthEndpointPaths.magicLinkVerify).toBe('/magic-link/verify');
46+
});
47+
});
48+
49+
describe('AuthEndpointSchema', () => {
50+
it('should validate signInEmail endpoint', () => {
51+
const endpoint = AuthEndpointSchema.shape.signInEmail.parse({
52+
method: 'POST',
53+
path: '/sign-in/email',
54+
description: 'Sign in with email and password',
55+
});
56+
57+
expect(endpoint.method).toBe('POST');
58+
expect(endpoint.path).toBe('/sign-in/email');
59+
});
60+
61+
it('should validate signUpEmail endpoint', () => {
62+
const endpoint = AuthEndpointSchema.shape.signUpEmail.parse({
63+
method: 'POST',
64+
path: '/sign-up/email',
65+
description: 'Register new user with email and password',
66+
});
67+
68+
expect(endpoint.method).toBe('POST');
69+
expect(endpoint.path).toBe('/sign-up/email');
70+
});
71+
72+
it('should validate getSession endpoint', () => {
73+
const endpoint = AuthEndpointSchema.shape.getSession.parse({
74+
method: 'GET',
75+
path: '/get-session',
76+
description: 'Get current user session',
77+
});
78+
79+
expect(endpoint.method).toBe('GET');
80+
expect(endpoint.path).toBe('/get-session');
81+
});
82+
83+
it('should reject invalid HTTP method', () => {
84+
expect(() =>
85+
AuthEndpointSchema.shape.signInEmail.parse({
86+
method: 'GET', // Should be POST
87+
path: '/sign-in/email',
88+
description: 'Sign in with email and password',
89+
})
90+
).toThrow();
91+
});
92+
93+
it('should reject invalid path', () => {
94+
expect(() =>
95+
AuthEndpointSchema.shape.signInEmail.parse({
96+
method: 'POST',
97+
path: '/wrong-path', // Should be /sign-in/email
98+
description: 'Sign in with email and password',
99+
})
100+
).toThrow();
101+
});
102+
});
103+
104+
describe('AuthEndpointAliases', () => {
105+
it('should map common names to canonical endpoints', () => {
106+
expect(AuthEndpointAliases.login).toBe('/sign-in/email');
107+
expect(AuthEndpointAliases.register).toBe('/sign-up/email');
108+
expect(AuthEndpointAliases.logout).toBe('/sign-out');
109+
expect(AuthEndpointAliases.me).toBe('/get-session');
110+
});
111+
});
112+
113+
describe('EndpointMapping', () => {
114+
it('should map legacy paths to canonical paths', () => {
115+
expect(EndpointMapping['/login']).toBe('/sign-in/email');
116+
expect(EndpointMapping['/register']).toBe('/sign-up/email');
117+
expect(EndpointMapping['/logout']).toBe('/sign-out');
118+
expect(EndpointMapping['/me']).toBe('/get-session');
119+
expect(EndpointMapping['/refresh']).toBe('/get-session');
120+
});
121+
});
122+
123+
describe('getAuthEndpointUrl', () => {
124+
it('should construct full endpoint URLs', () => {
125+
const basePath = '/api/v1/auth';
126+
127+
expect(getAuthEndpointUrl(basePath, 'signInEmail')).toBe('/api/v1/auth/sign-in/email');
128+
expect(getAuthEndpointUrl(basePath, 'signUpEmail')).toBe('/api/v1/auth/sign-up/email');
129+
expect(getAuthEndpointUrl(basePath, 'getSession')).toBe('/api/v1/auth/get-session');
130+
});
131+
132+
it('should handle trailing slash in basePath', () => {
133+
const basePath = '/api/v1/auth/';
134+
135+
expect(getAuthEndpointUrl(basePath, 'signInEmail')).toBe('/api/v1/auth/sign-in/email');
136+
expect(getAuthEndpointUrl(basePath, 'getSession')).toBe('/api/v1/auth/get-session');
137+
});
138+
139+
it('should work with different base paths', () => {
140+
expect(getAuthEndpointUrl('/custom/auth', 'signInEmail')).toBe('/custom/auth/sign-in/email');
141+
expect(getAuthEndpointUrl('http://localhost:3000/api/auth', 'signUpEmail')).toBe(
142+
'http://localhost:3000/api/auth/sign-up/email'
143+
);
144+
});
145+
});

0 commit comments

Comments
 (0)