@eventmodelers/node-kit 0.0.11 → 0.0.12
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/package.json +1 -1
- package/templates/.claude/skills/build-automation/SKILL.md +260 -0
- package/templates/.claude/skills/build-state-change/SKILL.md +329 -0
- package/templates/.claude/skills/build-state-view/SKILL.md +384 -0
- package/templates/.claude/skills/learn-eventmodelers-api/SKILL.md +609 -0
- package/templates/.claude/skills/load-slice/SKILL.md +69 -14
- package/templates/realtime-agent/src/index.js +11 -1
- package/templates/root/.env.example +22 -0
- package/templates/root/Claude.md +58 -0
- package/templates/root/agent.sh +15 -0
- package/templates/root/backend-prompt.md +139 -0
- package/templates/root/flyway.conf +17 -0
- package/templates/root/package.json +52 -0
- package/templates/root/ralph.sh +47 -26
- package/templates/root/server.ts +213 -0
- package/templates/root/setup-env.sh +55 -0
- package/templates/root/src/common/assertions.ts +6 -0
- package/templates/root/src/common/db.ts +32 -0
- package/templates/root/src/common/loadPostgresEventstore.ts +39 -0
- package/templates/root/src/common/parseEndpoint.ts +51 -0
- package/templates/root/src/common/processorDlq.ts +28 -0
- package/templates/root/src/common/realtimeBroadcast.ts +19 -0
- package/templates/root/src/common/replay.ts +16 -0
- package/templates/root/src/common/routes.ts +19 -0
- package/templates/root/src/common/testHelpers.ts +54 -0
- package/templates/root/src/slices/example/routes.ts +134 -0
- package/templates/root/src/supabase/LoginHandler.ts +36 -0
- package/templates/root/src/supabase/ProtectedPageProps.ts +21 -0
- package/templates/root/src/supabase/README.md +171 -0
- package/templates/root/src/supabase/api.ts +56 -0
- package/templates/root/src/supabase/component.ts +12 -0
- package/templates/root/src/supabase/requireOrgaAdmin.ts +32 -0
- package/templates/root/src/supabase/requireUser.ts +72 -0
- package/templates/root/src/supabase/serverProps.ts +25 -0
- package/templates/root/src/supabase/staticProps.ts +10 -0
- package/templates/root/src/swagger.ts +34 -0
- package/templates/root/src/util/assertions.ts +6 -0
- package/templates/root/src/util/hash.ts +9 -0
- package/templates/root/src/util/sanitize.ts +23 -0
- package/templates/root/supabase/config.toml +295 -0
- package/templates/root/supabase/migrations/V1__schema.sql.example +12 -0
- package/templates/root/supabase/seed.sql +1 -0
- package/templates/root/tsconfig.json +32 -0
- package/templates/root/vercel.json +8 -0
- package/templates/root/model.md +0 -1
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import {type EmailOtpType} from '@supabase/supabase-js'
|
|
2
|
+
import {Request, Response} from "express";
|
|
3
|
+
|
|
4
|
+
import createClient from './api'
|
|
5
|
+
|
|
6
|
+
function stringOrFirstString(item: string | string[] | undefined) {
|
|
7
|
+
return Array.isArray(item) ? item[0] : item
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export default async function LoginHandler(req: Request, res: Response) {
|
|
11
|
+
if (req.method !== 'GET') {
|
|
12
|
+
res.status(405).appendHeader('Allow', 'GET').end()
|
|
13
|
+
return
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const queryParams = req.query
|
|
17
|
+
const token_hash = stringOrFirstString(queryParams["token_hash"]?.toString())
|
|
18
|
+
const type = stringOrFirstString(queryParams["type"]?.toString())
|
|
19
|
+
|
|
20
|
+
let next = '/error'
|
|
21
|
+
|
|
22
|
+
if (token_hash && type) {
|
|
23
|
+
const supabase = createClient()
|
|
24
|
+
const {error} = await supabase.auth.verifyOtp({
|
|
25
|
+
type: type as EmailOtpType,
|
|
26
|
+
token_hash,
|
|
27
|
+
})
|
|
28
|
+
if (error) {
|
|
29
|
+
console.error(error)
|
|
30
|
+
} else {
|
|
31
|
+
next = stringOrFirstString(queryParams["next"]?.toString()) || '/'
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
res.redirect(next)
|
|
36
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import {type Request, type Response} from "express";
|
|
2
|
+
import {createClient} from "./serverProps";
|
|
3
|
+
|
|
4
|
+
export async function getAuthenticatedUser(req: Request, res: Response) {
|
|
5
|
+
const supabase = createClient(req, res)
|
|
6
|
+
const {data, error} = await supabase.auth.getUser()
|
|
7
|
+
if (error || !data) {
|
|
8
|
+
return null
|
|
9
|
+
}
|
|
10
|
+
return data.user
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export async function requireAuthMiddleware(req: Request, res: Response, next: () => void) {
|
|
14
|
+
const user = await getAuthenticatedUser(req, res)
|
|
15
|
+
if (!user) {
|
|
16
|
+
res.redirect('/auth/login')
|
|
17
|
+
return
|
|
18
|
+
}
|
|
19
|
+
(req as any).user = user
|
|
20
|
+
next()
|
|
21
|
+
}
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
# Supabase JWT Authentication for Backend API
|
|
2
|
+
|
|
3
|
+
This backend API uses Supabase JWT tokens for authentication. Clients must include a valid JWT token in the
|
|
4
|
+
`Authorization` header.
|
|
5
|
+
|
|
6
|
+
## Quick Start
|
|
7
|
+
|
|
8
|
+
1. **Set up environment variables** in `.env.local`:
|
|
9
|
+
```env
|
|
10
|
+
NEXT_PUBLIC_SUPABASE_URL=http://127.0.0.1:54321
|
|
11
|
+
NEXT_PUBLIC_SUPABASE_ANON_KEY=your-anon-key-here
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
2. **Start the server**: `npm run dev`
|
|
15
|
+
|
|
16
|
+
3. **Get a test JWT token**:
|
|
17
|
+
- Visit http://localhost:3000/auth/login
|
|
18
|
+
- Create an account or login
|
|
19
|
+
- Copy the JWT token displayed
|
|
20
|
+
- Use it in your API requests
|
|
21
|
+
|
|
22
|
+
## Test Login Page
|
|
23
|
+
|
|
24
|
+
A simple test login page is available at `/auth/login` that:
|
|
25
|
+
|
|
26
|
+
- Allows you to create accounts or login
|
|
27
|
+
- Displays the JWT token after authentication
|
|
28
|
+
- Provides a "Test API Call" button
|
|
29
|
+
- Shows a cURL example for testing
|
|
30
|
+
|
|
31
|
+
This page is for testing purposes only.
|
|
32
|
+
|
|
33
|
+
## How It Works
|
|
34
|
+
|
|
35
|
+
1. **Client obtains JWT token** from Supabase (via your frontend app)
|
|
36
|
+
2. **Client sends requests** with `Authorization: Bearer <jwt-token>` header
|
|
37
|
+
3. **Backend verifies JWT** using Supabase and extracts user info
|
|
38
|
+
4. **Protected routes** return user data or 401 Unauthorized
|
|
39
|
+
|
|
40
|
+
## Usage Examples
|
|
41
|
+
|
|
42
|
+
### Option 1: Using `requireUser` function
|
|
43
|
+
|
|
44
|
+
```typescript
|
|
45
|
+
import {requireUser} from './src/supabase/requireUser';
|
|
46
|
+
import {Request, Response} from 'express';
|
|
47
|
+
|
|
48
|
+
app.get('/api/protected', async (req: Request, res: Response) => {
|
|
49
|
+
const result = await requireUser(req, res, false);
|
|
50
|
+
|
|
51
|
+
if (result.error) {
|
|
52
|
+
return res.status(401).json({error: result.error});
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const user = result.user;
|
|
56
|
+
res.json({
|
|
57
|
+
message: 'Protected data',
|
|
58
|
+
userId: user.id,
|
|
59
|
+
email: user.email
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
### Option 2: Using `authMiddleware`
|
|
65
|
+
|
|
66
|
+
```typescript
|
|
67
|
+
import {authMiddleware} from './src/supabase/authMiddleware';
|
|
68
|
+
|
|
69
|
+
app.get('/api/protected', authMiddleware, (req, res) => {
|
|
70
|
+
// User is available on req.user after middleware
|
|
71
|
+
const user = (req as any).user;
|
|
72
|
+
|
|
73
|
+
res.json({
|
|
74
|
+
message: 'Protected data',
|
|
75
|
+
user: user
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
### Testing with curl
|
|
81
|
+
|
|
82
|
+
```bash
|
|
83
|
+
# Get JWT token from your Supabase client first
|
|
84
|
+
TOKEN="your-jwt-token-here"
|
|
85
|
+
|
|
86
|
+
# Test protected endpoint
|
|
87
|
+
curl -H "Authorization: Bearer $TOKEN" http://localhost:3000/api/user
|
|
88
|
+
|
|
89
|
+
# Expected response:
|
|
90
|
+
# {
|
|
91
|
+
# "userId": "...",
|
|
92
|
+
# "email": "user@example.com",
|
|
93
|
+
# "metadata": { ... }
|
|
94
|
+
# }
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
### Testing with JavaScript fetch
|
|
98
|
+
|
|
99
|
+
```javascript
|
|
100
|
+
const token = supabase.auth.session()?.access_token;
|
|
101
|
+
|
|
102
|
+
fetch('http://localhost:3000/api/user', {
|
|
103
|
+
headers: {
|
|
104
|
+
'Authorization': `Bearer ${token}`
|
|
105
|
+
}
|
|
106
|
+
})
|
|
107
|
+
.then(res => res.json())
|
|
108
|
+
.then(data => console.log(data));
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
## API Endpoints
|
|
112
|
+
|
|
113
|
+
### `GET /api/user`
|
|
114
|
+
|
|
115
|
+
Returns current authenticated user information.
|
|
116
|
+
|
|
117
|
+
**Headers:**
|
|
118
|
+
|
|
119
|
+
- `Authorization: Bearer <jwt-token>` (required)
|
|
120
|
+
|
|
121
|
+
**Success Response (200):**
|
|
122
|
+
|
|
123
|
+
```json
|
|
124
|
+
{
|
|
125
|
+
"userId": "uuid",
|
|
126
|
+
"email": "user@example.com",
|
|
127
|
+
"metadata": { ... }
|
|
128
|
+
}
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
**Error Responses:**
|
|
132
|
+
|
|
133
|
+
- `401 Unauthorized`: Missing or invalid token
|
|
134
|
+
- `500 Internal Server Error`: Server error
|
|
135
|
+
|
|
136
|
+
## Files
|
|
137
|
+
|
|
138
|
+
- **`api.ts`**: Supabase client creation
|
|
139
|
+
- **`requireUser.ts`**: JWT verification function
|
|
140
|
+
- **`authMiddleware.ts`**: Express middleware for protecting routes
|
|
141
|
+
- **`README.md`**: This documentation
|
|
142
|
+
|
|
143
|
+
## Architecture
|
|
144
|
+
|
|
145
|
+
```
|
|
146
|
+
Client Request
|
|
147
|
+
|
|
|
148
|
+
v
|
|
149
|
+
Authorization: Bearer <JWT>
|
|
150
|
+
|
|
|
151
|
+
v
|
|
152
|
+
Express Route
|
|
153
|
+
|
|
|
154
|
+
v
|
|
155
|
+
requireUser() / authMiddleware
|
|
156
|
+
|
|
|
157
|
+
v
|
|
158
|
+
Supabase JWT Verification
|
|
159
|
+
|
|
|
160
|
+
+---> Valid: Continue with user data
|
|
161
|
+
|
|
|
162
|
+
+---> Invalid: Return 401 Unauthorized
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
## Security Notes
|
|
166
|
+
|
|
167
|
+
- JWT tokens are verified with Supabase on every request
|
|
168
|
+
- No session storage on the backend (stateless)
|
|
169
|
+
- Tokens expire based on Supabase configuration
|
|
170
|
+
- Always use HTTPS in production
|
|
171
|
+
- Store the anon key securely (use environment variables)
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import {createClient as createSupabaseClient} from '@supabase/supabase-js'
|
|
2
|
+
import {Request} from 'express'
|
|
3
|
+
|
|
4
|
+
let _serviceClient: ReturnType<typeof createSupabaseClient> | null = null;
|
|
5
|
+
|
|
6
|
+
export const createServiceClient = () => {
|
|
7
|
+
if (!_serviceClient) {
|
|
8
|
+
_serviceClient = createSupabaseClient(
|
|
9
|
+
process.env.SUPABASE_URL!,
|
|
10
|
+
process.env.SUPABASE_SECRET_KEY!
|
|
11
|
+
);
|
|
12
|
+
}
|
|
13
|
+
return _serviceClient;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Creates a Supabase client for verifying JWT tokens
|
|
18
|
+
* Used in backend API endpoints
|
|
19
|
+
*/
|
|
20
|
+
export default function createClient() {
|
|
21
|
+
return createSupabaseClient(
|
|
22
|
+
process.env.SUPABASE_URL!,
|
|
23
|
+
process.env.SUPABASE_PUBLISHABLE_KEY!,
|
|
24
|
+
{
|
|
25
|
+
auth: {
|
|
26
|
+
persistSession: false,
|
|
27
|
+
autoRefreshToken: false,
|
|
28
|
+
detectSessionInUrl: false,
|
|
29
|
+
},
|
|
30
|
+
}
|
|
31
|
+
)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Creates an authenticated Supabase client with the user's JWT token
|
|
36
|
+
* This ensures Row Level Security (RLS) policies are applied with the user's context
|
|
37
|
+
*/
|
|
38
|
+
export async function createAuthenticatedClient(req: Request) {
|
|
39
|
+
const token = req.headers.authorization?.replace('Bearer ', '') || '';
|
|
40
|
+
return createSupabaseClient(
|
|
41
|
+
process.env.SUPABASE_URL!,
|
|
42
|
+
process.env.SUPABASE_PUBLISHABLE_KEY!,
|
|
43
|
+
{
|
|
44
|
+
auth: {
|
|
45
|
+
persistSession: false,
|
|
46
|
+
autoRefreshToken: false,
|
|
47
|
+
detectSessionInUrl: false,
|
|
48
|
+
},
|
|
49
|
+
global: {
|
|
50
|
+
headers: {
|
|
51
|
+
Authorization: `Bearer ${token}`,
|
|
52
|
+
},
|
|
53
|
+
},
|
|
54
|
+
}
|
|
55
|
+
);
|
|
56
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import {createBrowserClient} from '@supabase/ssr'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Creates a Supabase client for browser/client-side operations
|
|
5
|
+
* Used in React components for authentication flows
|
|
6
|
+
*/
|
|
7
|
+
export function createClient() {
|
|
8
|
+
return createBrowserClient(
|
|
9
|
+
process.env.NEXT_PUBLIC_SUPABASE_URL!,
|
|
10
|
+
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
|
|
11
|
+
)
|
|
12
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import {Response} from 'express';
|
|
2
|
+
import {SupabaseClient} from '@supabase/supabase-js';
|
|
3
|
+
|
|
4
|
+
type RequireOrgaAdminResult = {
|
|
5
|
+
ok: true;
|
|
6
|
+
error: null;
|
|
7
|
+
} | {
|
|
8
|
+
ok: false;
|
|
9
|
+
error: string;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export async function requireOrgaAdmin(
|
|
13
|
+
supabase: SupabaseClient,
|
|
14
|
+
orgaId: string,
|
|
15
|
+
userId: string,
|
|
16
|
+
res: Response,
|
|
17
|
+
): Promise<RequireOrgaAdminResult> {
|
|
18
|
+
const {data, error} = await supabase
|
|
19
|
+
.from('user_organizations')
|
|
20
|
+
.select('role')
|
|
21
|
+
.eq('orga_id', orgaId)
|
|
22
|
+
.eq('user_id', userId)
|
|
23
|
+
.eq('role', 'admin')
|
|
24
|
+
.single();
|
|
25
|
+
|
|
26
|
+
if (error || !data) {
|
|
27
|
+
res.status(403).json({error: 'Forbidden: admin role required'});
|
|
28
|
+
return {ok: false, error: 'FORBIDDEN'};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return {ok: true, error: null};
|
|
32
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import {createAuthenticatedClient} from "./api";
|
|
2
|
+
import {Request, Response} from "express"
|
|
3
|
+
|
|
4
|
+
type RequireUserResult = {
|
|
5
|
+
user: any;
|
|
6
|
+
error: null;
|
|
7
|
+
} | {
|
|
8
|
+
user: null;
|
|
9
|
+
error: string;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Extracts JWT token from Authorization header
|
|
14
|
+
* Supports "Bearer <token>" format
|
|
15
|
+
*/
|
|
16
|
+
function extractTokenFromHeader(req: Request): string | null {
|
|
17
|
+
const authHeader = req.headers.authorization;
|
|
18
|
+
|
|
19
|
+
if (!authHeader) {
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// Check for "Bearer <token>" format
|
|
24
|
+
const parts = authHeader.split(' ');
|
|
25
|
+
if (parts.length === 2 && parts[0].toLowerCase() === 'bearer') {
|
|
26
|
+
return parts[1];
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// If no Bearer prefix, assume the entire header is the token
|
|
30
|
+
return authHeader;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Verifies JWT token from Authorization header and returns user info
|
|
35
|
+
* This is for backend API use - does not use cookies or redirects
|
|
36
|
+
*/
|
|
37
|
+
export async function requireUser(req: Request, resp: Response, sendUnauthorized: boolean = true): Promise<RequireUserResult> {
|
|
38
|
+
const token = extractTokenFromHeader(req);
|
|
39
|
+
|
|
40
|
+
if (!token) {
|
|
41
|
+
if (sendUnauthorized) {
|
|
42
|
+
resp.status(401).json({error: 'Missing authorization token'});
|
|
43
|
+
}
|
|
44
|
+
return {
|
|
45
|
+
user: null,
|
|
46
|
+
error: 'MISSING_TOKEN',
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const supabase = await createAuthenticatedClient(req);
|
|
51
|
+
|
|
52
|
+
// Verify the JWT token
|
|
53
|
+
const {
|
|
54
|
+
data: {user},
|
|
55
|
+
error
|
|
56
|
+
} = await supabase.auth.getUser(token);
|
|
57
|
+
|
|
58
|
+
if (error || !user) {
|
|
59
|
+
if (sendUnauthorized) {
|
|
60
|
+
resp.status(401).json({error: 'Invalid or expired token'});
|
|
61
|
+
}
|
|
62
|
+
return {
|
|
63
|
+
user: null,
|
|
64
|
+
error: error?.message || 'UNAUTHORIZED',
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return {
|
|
69
|
+
user: user,
|
|
70
|
+
error: null
|
|
71
|
+
}
|
|
72
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import {type Request, type Response} from 'express'
|
|
2
|
+
import {createServerClient, serializeCookieHeader} from '@supabase/ssr'
|
|
3
|
+
|
|
4
|
+
export function createClient(req: Request, res: Response) {
|
|
5
|
+
const supabase = createServerClient(
|
|
6
|
+
process.env.NEXT_PUBLIC_SUPABASE_URL!,
|
|
7
|
+
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
|
|
8
|
+
{
|
|
9
|
+
cookies: {
|
|
10
|
+
getAll() {
|
|
11
|
+
return Object.keys(req.cookies).map((name) => ({name, value: req.cookies[name] || ''}))
|
|
12
|
+
},
|
|
13
|
+
setAll(cookiesToSet: any[]) {
|
|
14
|
+
res.setHeader(
|
|
15
|
+
'Set-Cookie',
|
|
16
|
+
cookiesToSet.map(({name, value, options}: any) =>
|
|
17
|
+
serializeCookieHeader(name, value, options)
|
|
18
|
+
)
|
|
19
|
+
)
|
|
20
|
+
},
|
|
21
|
+
},
|
|
22
|
+
}
|
|
23
|
+
)
|
|
24
|
+
return supabase
|
|
25
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import {createClient as createClientPrimitive} from '@supabase/supabase-js'
|
|
2
|
+
|
|
3
|
+
export function createClient() {
|
|
4
|
+
const supabase = createClientPrimitive(
|
|
5
|
+
process.env.NEXT_PUBLIC_SUPABASE_URL!,
|
|
6
|
+
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
|
|
7
|
+
)
|
|
8
|
+
|
|
9
|
+
return supabase
|
|
10
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import swaggerJsdoc from 'swagger-jsdoc';
|
|
2
|
+
|
|
3
|
+
const options = {
|
|
4
|
+
definition: {
|
|
5
|
+
openapi: '3.0.0',
|
|
6
|
+
info: {
|
|
7
|
+
title: 'Context API',
|
|
8
|
+
version: '1.0.0',
|
|
9
|
+
description: 'Event-driven API for shift, clerk, and task management',
|
|
10
|
+
},
|
|
11
|
+
servers: [
|
|
12
|
+
{
|
|
13
|
+
url: 'http://localhost:3000',
|
|
14
|
+
description: 'Development server',
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
url: process.env.API_URL || 'http://localhost:3000',
|
|
18
|
+
description: 'Production server',
|
|
19
|
+
},
|
|
20
|
+
],
|
|
21
|
+
components: {
|
|
22
|
+
securitySchemes: {
|
|
23
|
+
bearerAuth: {
|
|
24
|
+
type: 'http',
|
|
25
|
+
scheme: 'bearer',
|
|
26
|
+
bearerFormat: 'JWT',
|
|
27
|
+
},
|
|
28
|
+
},
|
|
29
|
+
},
|
|
30
|
+
},
|
|
31
|
+
apis: ['./src/slices/**/routes.ts'],
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
export const specs = swaggerJsdoc(options);
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
// Fast djb2-style hash of any object — used to detect drift
|
|
2
|
+
export function hashMeta(entry: unknown): string {
|
|
3
|
+
const str = JSON.stringify(entry);
|
|
4
|
+
let h = 5381;
|
|
5
|
+
for (let i = 0; i < str.length; i++) {
|
|
6
|
+
h = (((h << 5) + h) ^ str.charCodeAt(i)) >>> 0;
|
|
7
|
+
}
|
|
8
|
+
return h.toString(36);
|
|
9
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
function sanitizeValue(value: unknown): unknown {
|
|
2
|
+
if (typeof value === 'bigint') {
|
|
3
|
+
return Number.isSafeInteger(Number(value)) ? Number(value) : value.toString();
|
|
4
|
+
}
|
|
5
|
+
if (Array.isArray(value)) {
|
|
6
|
+
return value.map(sanitizeValue);
|
|
7
|
+
}
|
|
8
|
+
if (value !== null && typeof value === 'object') {
|
|
9
|
+
return Object.fromEntries(
|
|
10
|
+
Object.entries(value).map(([k, v]) => [k, sanitizeValue(v)])
|
|
11
|
+
);
|
|
12
|
+
}
|
|
13
|
+
return value;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function sanitize<T>(value: T): unknown {
|
|
17
|
+
return sanitizeValue(value);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export const jsonBigIntReplacer = (_key: string, value: unknown): unknown =>
|
|
21
|
+
typeof value === 'bigint'
|
|
22
|
+
? (Number.isSafeInteger(Number(value)) ? Number(value) : value.toString())
|
|
23
|
+
: value;
|