@chimerai/cli 0.2.73
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/LICENSE +21 -0
- package/README.md +293 -0
- package/dist/cli.d.ts +7 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +317 -0
- package/dist/commands/add.d.ts +11 -0
- package/dist/commands/add.d.ts.map +1 -0
- package/dist/commands/add.js +2126 -0
- package/dist/commands/create.d.ts +12 -0
- package/dist/commands/create.d.ts.map +1 -0
- package/dist/commands/create.js +1703 -0
- package/dist/commands/deploy.d.ts +11 -0
- package/dist/commands/deploy.d.ts.map +1 -0
- package/dist/commands/deploy.js +219 -0
- package/dist/commands/dev.d.ts +17 -0
- package/dist/commands/dev.d.ts.map +1 -0
- package/dist/commands/dev.js +206 -0
- package/dist/commands/doctor.d.ts +11 -0
- package/dist/commands/doctor.d.ts.map +1 -0
- package/dist/commands/doctor.js +728 -0
- package/dist/commands/generate.d.ts +19 -0
- package/dist/commands/generate.d.ts.map +1 -0
- package/dist/commands/generate.js +429 -0
- package/dist/commands/init.d.ts +11 -0
- package/dist/commands/init.d.ts.map +1 -0
- package/dist/commands/init.js +269 -0
- package/dist/commands/list.d.ts +12 -0
- package/dist/commands/list.d.ts.map +1 -0
- package/dist/commands/list.js +328 -0
- package/dist/commands/migrate.d.ts +14 -0
- package/dist/commands/migrate.d.ts.map +1 -0
- package/dist/commands/migrate.js +197 -0
- package/dist/commands/plugin.d.ts +10 -0
- package/dist/commands/plugin.d.ts.map +1 -0
- package/dist/commands/plugin.js +239 -0
- package/dist/commands/remove.d.ts +11 -0
- package/dist/commands/remove.d.ts.map +1 -0
- package/dist/commands/remove.js +472 -0
- package/dist/commands/secret.d.ts +12 -0
- package/dist/commands/secret.d.ts.map +1 -0
- package/dist/commands/secret.js +102 -0
- package/dist/commands/setup.d.ts +9 -0
- package/dist/commands/setup.d.ts.map +1 -0
- package/dist/commands/setup.js +788 -0
- package/dist/commands/update.d.ts +14 -0
- package/dist/commands/update.d.ts.map +1 -0
- package/dist/commands/update.js +211 -0
- package/dist/commands/use.d.ts +9 -0
- package/dist/commands/use.d.ts.map +1 -0
- package/dist/commands/use.js +51 -0
- package/dist/index.d.ts +22 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +45 -0
- package/dist/license.d.ts +55 -0
- package/dist/license.d.ts.map +1 -0
- package/dist/license.js +258 -0
- package/dist/scanner.d.ts +31 -0
- package/dist/scanner.d.ts.map +1 -0
- package/dist/scanner.js +113 -0
- package/dist/schema-manager.d.ts +26 -0
- package/dist/schema-manager.d.ts.map +1 -0
- package/dist/schema-manager.js +132 -0
- package/dist/templates/admin.d.ts +49 -0
- package/dist/templates/admin.d.ts.map +1 -0
- package/dist/templates/admin.js +1358 -0
- package/dist/templates/ai-routes.d.ts +17 -0
- package/dist/templates/ai-routes.d.ts.map +1 -0
- package/dist/templates/ai-routes.js +1130 -0
- package/dist/templates/ai-service-tools.d.ts +22 -0
- package/dist/templates/ai-service-tools.d.ts.map +1 -0
- package/dist/templates/ai-service-tools.js +1424 -0
- package/dist/templates/ai-service.d.ts +66 -0
- package/dist/templates/ai-service.d.ts.map +1 -0
- package/dist/templates/ai-service.js +2202 -0
- package/dist/templates/api-routes.d.ts +108 -0
- package/dist/templates/api-routes.d.ts.map +1 -0
- package/dist/templates/api-routes.js +1219 -0
- package/dist/templates/auth.d.ts +48 -0
- package/dist/templates/auth.d.ts.map +1 -0
- package/dist/templates/auth.js +381 -0
- package/dist/templates/billing.d.ts +44 -0
- package/dist/templates/billing.d.ts.map +1 -0
- package/dist/templates/billing.js +551 -0
- package/dist/templates/chat.d.ts +63 -0
- package/dist/templates/chat.d.ts.map +1 -0
- package/dist/templates/chat.js +1979 -0
- package/dist/templates/components.d.ts +22 -0
- package/dist/templates/components.d.ts.map +1 -0
- package/dist/templates/components.js +672 -0
- package/dist/templates/config.d.ts +6 -0
- package/dist/templates/config.d.ts.map +1 -0
- package/dist/templates/config.js +86 -0
- package/dist/templates/docker.d.ts +25 -0
- package/dist/templates/docker.d.ts.map +1 -0
- package/dist/templates/docker.js +165 -0
- package/dist/templates/gdpr.d.ts +16 -0
- package/dist/templates/gdpr.d.ts.map +1 -0
- package/dist/templates/gdpr.js +259 -0
- package/dist/templates/index.d.ts +77 -0
- package/dist/templates/index.d.ts.map +1 -0
- package/dist/templates/index.js +339 -0
- package/dist/templates/layout.d.ts +67 -0
- package/dist/templates/layout.d.ts.map +1 -0
- package/dist/templates/layout.js +670 -0
- package/dist/templates/mfa.d.ts +23 -0
- package/dist/templates/mfa.d.ts.map +1 -0
- package/dist/templates/mfa.js +353 -0
- package/dist/templates/middleware.d.ts +12 -0
- package/dist/templates/middleware.d.ts.map +1 -0
- package/dist/templates/middleware.js +116 -0
- package/dist/templates/prisma.d.ts +35 -0
- package/dist/templates/prisma.d.ts.map +1 -0
- package/dist/templates/prisma.js +724 -0
- package/dist/templates/provider-routes.d.ts +21 -0
- package/dist/templates/provider-routes.d.ts.map +1 -0
- package/dist/templates/provider-routes.js +1203 -0
- package/dist/templates/rag.d.ts +48 -0
- package/dist/templates/rag.d.ts.map +1 -0
- package/dist/templates/rag.js +532 -0
- package/dist/templates/widget.d.ts +64 -0
- package/dist/templates/widget.d.ts.map +1 -0
- package/dist/templates/widget.js +1360 -0
- package/dist/utils/provider-db.d.ts +63 -0
- package/dist/utils/provider-db.d.ts.map +1 -0
- package/dist/utils/provider-db.js +300 -0
- package/dist/utils.d.ts +78 -0
- package/dist/utils.d.ts.map +1 -0
- package/dist/utils.js +330 -0
- package/package.json +60 -0
|
@@ -0,0 +1,353 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* MFA (Multi-Factor Authentication) Templates
|
|
4
|
+
* TOTP-based 2FA using otpauth + qrcode
|
|
5
|
+
*/
|
|
6
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
|
+
exports.MFA_USER_FIELDS = exports.MFA_SCHEMA_EXTENSION = void 0;
|
|
8
|
+
exports.generateMfaLib = generateMfaLib;
|
|
9
|
+
exports.generateMfaSetupRoute = generateMfaSetupRoute;
|
|
10
|
+
exports.generateMfaVerifyRoute = generateMfaVerifyRoute;
|
|
11
|
+
exports.generateMfaDisableRoute = generateMfaDisableRoute;
|
|
12
|
+
exports.generateMfaPage = generateMfaPage;
|
|
13
|
+
/** lib/mfa.ts — TOTP helpers */
|
|
14
|
+
function generateMfaLib() {
|
|
15
|
+
return `// @chimerai component=MfaLib version=1.0
|
|
16
|
+
import * as OTPAuth from 'otpauth';
|
|
17
|
+
import QRCode from 'qrcode';
|
|
18
|
+
|
|
19
|
+
export interface MfaSetupResult {
|
|
20
|
+
secret: string;
|
|
21
|
+
otpauthUrl: string;
|
|
22
|
+
qrCodeDataUrl: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Generate a new TOTP secret and QR code for the user.
|
|
27
|
+
*/
|
|
28
|
+
export async function generateMfaSetup(userEmail: string, appName = 'ChimerAI'): Promise<MfaSetupResult> {
|
|
29
|
+
const totp = new OTPAuth.TOTP({
|
|
30
|
+
issuer: appName,
|
|
31
|
+
label: userEmail,
|
|
32
|
+
algorithm: 'SHA1',
|
|
33
|
+
digits: 6,
|
|
34
|
+
period: 30,
|
|
35
|
+
secret: new OTPAuth.Secret({ size: 20 }),
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
const otpauthUrl = totp.toString();
|
|
39
|
+
const secret = totp.secret.base32;
|
|
40
|
+
const qrCodeDataUrl = await QRCode.toDataURL(otpauthUrl);
|
|
41
|
+
|
|
42
|
+
return { secret, otpauthUrl, qrCodeDataUrl };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Verify a TOTP code against the stored secret.
|
|
47
|
+
* Returns true if valid (with ±1 period drift tolerance).
|
|
48
|
+
*/
|
|
49
|
+
export function verifyMfaCode(secret: string, code: string): boolean {
|
|
50
|
+
try {
|
|
51
|
+
const totp = new OTPAuth.TOTP({
|
|
52
|
+
algorithm: 'SHA1',
|
|
53
|
+
digits: 6,
|
|
54
|
+
period: 30,
|
|
55
|
+
secret: OTPAuth.Secret.fromBase32(secret),
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
const delta = totp.validate({ token: code, window: 1 });
|
|
59
|
+
return delta !== null;
|
|
60
|
+
} catch {
|
|
61
|
+
return false;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
`;
|
|
65
|
+
}
|
|
66
|
+
/** app/api/mfa/setup/route.ts — generate TOTP secret */
|
|
67
|
+
function generateMfaSetupRoute() {
|
|
68
|
+
return `// @chimerai component=MfaSetupRoute version=1.0
|
|
69
|
+
import { NextResponse } from 'next/server';
|
|
70
|
+
import { getServerSession } from 'next-auth';
|
|
71
|
+
import { authOptions } from '@/lib/auth';
|
|
72
|
+
import { generateMfaSetup } from '@/lib/mfa';
|
|
73
|
+
import { prisma } from '@/lib/prisma';
|
|
74
|
+
|
|
75
|
+
export async function POST() {
|
|
76
|
+
const session = await getServerSession(authOptions);
|
|
77
|
+
if (!session?.user?.email) {
|
|
78
|
+
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const { secret, qrCodeDataUrl } = await generateMfaSetup(session.user.email);
|
|
82
|
+
|
|
83
|
+
// Store the pending (unconfirmed) secret
|
|
84
|
+
await (prisma as any).user.update({
|
|
85
|
+
where: { email: session.user.email },
|
|
86
|
+
data: { mfaSecret: secret, mfaEnabled: false },
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
return NextResponse.json({ qrCodeDataUrl, secret });
|
|
90
|
+
}
|
|
91
|
+
`;
|
|
92
|
+
}
|
|
93
|
+
/** app/api/mfa/verify/route.ts — confirm TOTP code to enable MFA */
|
|
94
|
+
function generateMfaVerifyRoute() {
|
|
95
|
+
return `// @chimerai component=MfaVerifyRoute version=1.0
|
|
96
|
+
import { NextRequest, NextResponse } from 'next/server';
|
|
97
|
+
import { getServerSession } from 'next-auth';
|
|
98
|
+
import { authOptions } from '@/lib/auth';
|
|
99
|
+
import { verifyMfaCode } from '@/lib/mfa';
|
|
100
|
+
import { prisma } from '@/lib/prisma';
|
|
101
|
+
|
|
102
|
+
export async function POST(req: NextRequest) {
|
|
103
|
+
const session = await getServerSession(authOptions);
|
|
104
|
+
if (!session?.user?.email) {
|
|
105
|
+
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const { code } = await req.json() as { code: string };
|
|
109
|
+
|
|
110
|
+
const user = await (prisma as any).user.findUnique({
|
|
111
|
+
where: { email: session.user.email },
|
|
112
|
+
select: { mfaSecret: true },
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
if (!user?.mfaSecret) {
|
|
116
|
+
return NextResponse.json({ error: 'MFA not set up' }, { status: 400 });
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const valid = verifyMfaCode(user.mfaSecret, code);
|
|
120
|
+
if (!valid) {
|
|
121
|
+
return NextResponse.json({ error: 'Invalid code' }, { status: 400 });
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
await (prisma as any).user.update({
|
|
125
|
+
where: { email: session.user.email },
|
|
126
|
+
data: { mfaEnabled: true },
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
return NextResponse.json({ success: true });
|
|
130
|
+
}
|
|
131
|
+
`;
|
|
132
|
+
}
|
|
133
|
+
/** app/api/mfa/disable/route.ts — disable MFA */
|
|
134
|
+
function generateMfaDisableRoute() {
|
|
135
|
+
return `// @chimerai component=MfaDisableRoute version=1.0
|
|
136
|
+
import { NextRequest, NextResponse } from 'next/server';
|
|
137
|
+
import { getServerSession } from 'next-auth';
|
|
138
|
+
import { authOptions } from '@/lib/auth';
|
|
139
|
+
import { verifyMfaCode } from '@/lib/mfa';
|
|
140
|
+
import { prisma } from '@/lib/prisma';
|
|
141
|
+
|
|
142
|
+
export async function POST(req: NextRequest) {
|
|
143
|
+
const session = await getServerSession(authOptions);
|
|
144
|
+
if (!session?.user?.email) {
|
|
145
|
+
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const { code } = await req.json() as { code: string };
|
|
149
|
+
|
|
150
|
+
const user = await (prisma as any).user.findUnique({
|
|
151
|
+
where: { email: session.user.email },
|
|
152
|
+
select: { mfaSecret: true },
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
if (!user?.mfaSecret) {
|
|
156
|
+
return NextResponse.json({ error: 'MFA not configured' }, { status: 400 });
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const valid = verifyMfaCode(user.mfaSecret, code);
|
|
160
|
+
if (!valid) {
|
|
161
|
+
return NextResponse.json({ error: 'Invalid code' }, { status: 400 });
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
await (prisma as any).user.update({
|
|
165
|
+
where: { email: session.user.email },
|
|
166
|
+
data: { mfaEnabled: false, mfaSecret: null },
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
return NextResponse.json({ success: true });
|
|
170
|
+
}
|
|
171
|
+
`;
|
|
172
|
+
}
|
|
173
|
+
/** app/(app)/settings/mfa/page.tsx — MFA setup UI */
|
|
174
|
+
function generateMfaPage() {
|
|
175
|
+
return `// @chimerai component=MfaPage version=1.0
|
|
176
|
+
'use client';
|
|
177
|
+
|
|
178
|
+
import { useState } from 'react';
|
|
179
|
+
import Image from 'next/image';
|
|
180
|
+
|
|
181
|
+
export default function MfaPage() {
|
|
182
|
+
const [qrCode, setQrCode] = useState<string | null>(null);
|
|
183
|
+
const [secret, setSecret] = useState<string | null>(null);
|
|
184
|
+
const [code, setCode] = useState('');
|
|
185
|
+
const [step, setStep] = useState<'idle' | 'setup' | 'done' | 'disable'>('idle');
|
|
186
|
+
const [error, setError] = useState('');
|
|
187
|
+
const [loading, setLoading] = useState(false);
|
|
188
|
+
|
|
189
|
+
async function startSetup() {
|
|
190
|
+
setLoading(true);
|
|
191
|
+
setError('');
|
|
192
|
+
const res = await fetch('/api/mfa/setup', { method: 'POST' });
|
|
193
|
+
const data = await res.json();
|
|
194
|
+
if (res.ok) {
|
|
195
|
+
setQrCode(data.qrCodeDataUrl);
|
|
196
|
+
setSecret(data.secret);
|
|
197
|
+
setStep('setup');
|
|
198
|
+
} else {
|
|
199
|
+
setError(data.error ?? 'Failed to start setup');
|
|
200
|
+
}
|
|
201
|
+
setLoading(false);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
async function confirmCode(endpoint: string) {
|
|
205
|
+
setLoading(true);
|
|
206
|
+
setError('');
|
|
207
|
+
const res = await fetch(endpoint, {
|
|
208
|
+
method: 'POST',
|
|
209
|
+
headers: { 'Content-Type': 'application/json' },
|
|
210
|
+
body: JSON.stringify({ code }),
|
|
211
|
+
});
|
|
212
|
+
const data = await res.json();
|
|
213
|
+
if (res.ok) {
|
|
214
|
+
setStep('done');
|
|
215
|
+
setCode('');
|
|
216
|
+
} else {
|
|
217
|
+
setError(data.error ?? 'Invalid code');
|
|
218
|
+
}
|
|
219
|
+
setLoading(false);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
return (
|
|
223
|
+
<div className="max-w-md mx-auto py-10 px-4">
|
|
224
|
+
<h1 className="text-2xl font-bold mb-2">Two-Factor Authentication</h1>
|
|
225
|
+
<p className="text-gray-500 mb-8">
|
|
226
|
+
Protect your account with an authenticator app (Google Authenticator, Authy, etc.)
|
|
227
|
+
</p>
|
|
228
|
+
|
|
229
|
+
{step === 'idle' && (
|
|
230
|
+
<button
|
|
231
|
+
onClick={startSetup}
|
|
232
|
+
disabled={loading}
|
|
233
|
+
className="w-full bg-blue-600 text-white py-2 px-4 rounded-lg hover:bg-blue-700 disabled:opacity-50"
|
|
234
|
+
>
|
|
235
|
+
{loading ? 'Loading...' : 'Set up 2FA'}
|
|
236
|
+
</button>
|
|
237
|
+
)}
|
|
238
|
+
|
|
239
|
+
{step === 'setup' && qrCode && (
|
|
240
|
+
<div className="space-y-6">
|
|
241
|
+
<div>
|
|
242
|
+
<p className="text-sm font-medium mb-3">
|
|
243
|
+
1. Scan this QR code with your authenticator app:
|
|
244
|
+
</p>
|
|
245
|
+
<div className="flex justify-center bg-white p-4 rounded-lg">
|
|
246
|
+
<Image src={qrCode} alt="MFA QR Code" width={200} height={200} />
|
|
247
|
+
</div>
|
|
248
|
+
</div>
|
|
249
|
+
|
|
250
|
+
{secret && (
|
|
251
|
+
<div>
|
|
252
|
+
<p className="text-sm font-medium mb-1">Or enter this key manually:</p>
|
|
253
|
+
<code className="block bg-gray-100 rounded px-3 py-2 text-sm font-mono break-all">
|
|
254
|
+
{secret}
|
|
255
|
+
</code>
|
|
256
|
+
</div>
|
|
257
|
+
)}
|
|
258
|
+
|
|
259
|
+
<div>
|
|
260
|
+
<p className="text-sm font-medium mb-2">2. Enter the 6-digit code from your app:</p>
|
|
261
|
+
<input
|
|
262
|
+
type="text"
|
|
263
|
+
value={code}
|
|
264
|
+
onChange={(e) => setCode(e.target.value.replace(/\\D/g, '').slice(0, 6))}
|
|
265
|
+
placeholder="000000"
|
|
266
|
+
className="w-full border rounded-lg px-3 py-2 text-center text-xl tracking-widest font-mono"
|
|
267
|
+
maxLength={6}
|
|
268
|
+
/>
|
|
269
|
+
</div>
|
|
270
|
+
|
|
271
|
+
{error && <p className="text-red-500 text-sm">{error}</p>}
|
|
272
|
+
|
|
273
|
+
<button
|
|
274
|
+
onClick={() => confirmCode('/api/mfa/verify')}
|
|
275
|
+
disabled={loading || code.length !== 6}
|
|
276
|
+
className="w-full bg-green-600 text-white py-2 px-4 rounded-lg hover:bg-green-700 disabled:opacity-50"
|
|
277
|
+
>
|
|
278
|
+
{loading ? 'Verifying...' : 'Enable 2FA'}
|
|
279
|
+
</button>
|
|
280
|
+
</div>
|
|
281
|
+
)}
|
|
282
|
+
|
|
283
|
+
{step === 'done' && (
|
|
284
|
+
<div className="text-center space-y-4">
|
|
285
|
+
<div className="text-5xl">✅</div>
|
|
286
|
+
<p className="text-lg font-semibold text-green-700">2FA is now enabled!</p>
|
|
287
|
+
<p className="text-gray-500 text-sm">
|
|
288
|
+
Your account is now protected. You will be asked for a code each time you sign in.
|
|
289
|
+
</p>
|
|
290
|
+
<button
|
|
291
|
+
onClick={() => setStep('disable')}
|
|
292
|
+
className="text-sm text-red-500 hover:underline"
|
|
293
|
+
>
|
|
294
|
+
Disable 2FA
|
|
295
|
+
</button>
|
|
296
|
+
</div>
|
|
297
|
+
)}
|
|
298
|
+
|
|
299
|
+
{step === 'disable' && (
|
|
300
|
+
<div className="space-y-4">
|
|
301
|
+
<p className="text-sm font-medium">Enter your current 2FA code to disable:</p>
|
|
302
|
+
<input
|
|
303
|
+
type="text"
|
|
304
|
+
value={code}
|
|
305
|
+
onChange={(e) => setCode(e.target.value.replace(/\\D/g, '').slice(0, 6))}
|
|
306
|
+
placeholder="000000"
|
|
307
|
+
className="w-full border rounded-lg px-3 py-2 text-center text-xl tracking-widest font-mono"
|
|
308
|
+
maxLength={6}
|
|
309
|
+
/>
|
|
310
|
+
{error && <p className="text-red-500 text-sm">{error}</p>}
|
|
311
|
+
<button
|
|
312
|
+
onClick={() => confirmCode('/api/mfa/disable')}
|
|
313
|
+
disabled={loading || code.length !== 6}
|
|
314
|
+
className="w-full bg-red-600 text-white py-2 px-4 rounded-lg hover:bg-red-700 disabled:opacity-50"
|
|
315
|
+
>
|
|
316
|
+
{loading ? 'Disabling...' : 'Disable 2FA'}
|
|
317
|
+
</button>
|
|
318
|
+
<button onClick={() => setStep('done')} className="w-full text-sm text-gray-500 hover:underline">
|
|
319
|
+
Cancel
|
|
320
|
+
</button>
|
|
321
|
+
</div>
|
|
322
|
+
)}
|
|
323
|
+
</div>
|
|
324
|
+
);
|
|
325
|
+
}
|
|
326
|
+
`;
|
|
327
|
+
}
|
|
328
|
+
/**
|
|
329
|
+
* Prisma schema extension: adds mfaSecret + mfaEnabled to User model
|
|
330
|
+
* and creates a standalone MfaBackupCode model for recovery codes.
|
|
331
|
+
* SQLite-safe: no @db.Text, no Json, no arrays.
|
|
332
|
+
*/
|
|
333
|
+
exports.MFA_SCHEMA_EXTENSION = `
|
|
334
|
+
mfaSecret String?
|
|
335
|
+
mfaEnabled Boolean @default(false)
|
|
336
|
+
|
|
337
|
+
model MfaBackupCode {
|
|
338
|
+
id String @id @default(cuid())
|
|
339
|
+
userId String
|
|
340
|
+
codeHash String
|
|
341
|
+
usedAt DateTime?
|
|
342
|
+
createdAt DateTime @default(now())
|
|
343
|
+
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
|
344
|
+
|
|
345
|
+
@@index([userId])
|
|
346
|
+
}
|
|
347
|
+
`;
|
|
348
|
+
/** Fields to patch onto the User model (added by extendPrismaSchema) */
|
|
349
|
+
exports.MFA_USER_FIELDS = `
|
|
350
|
+
mfaSecret String?
|
|
351
|
+
mfaEnabled Boolean @default(false)
|
|
352
|
+
mfaBackupCodes MfaBackupCode[]
|
|
353
|
+
`;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Next.js middleware template with security headers and CORS for widget endpoints.
|
|
3
|
+
* Generates middleware.ts for the root of the Next.js project.
|
|
4
|
+
*/
|
|
5
|
+
/**
|
|
6
|
+
* Generates the Next.js middleware with security headers and CORS.
|
|
7
|
+
* Widget endpoints (/api/v1/*) get CORS headers from CORS_ALLOWED_ORIGINS env.
|
|
8
|
+
* Also reads WIDGET_ALLOWED_ORIGINS as legacy alias for backward compatibility.
|
|
9
|
+
* @returns TypeScript content for middleware.ts
|
|
10
|
+
*/
|
|
11
|
+
export declare function generateMiddleware(): string;
|
|
12
|
+
//# sourceMappingURL=middleware.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"middleware.d.ts","sourceRoot":"","sources":["../../src/templates/middleware.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH;;;;;GAKG;AACH,wBAAgB,kBAAkB,IAAI,MAAM,CAsG3C"}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Next.js middleware template with security headers and CORS for widget endpoints.
|
|
4
|
+
* Generates middleware.ts for the root of the Next.js project.
|
|
5
|
+
*/
|
|
6
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
|
+
exports.generateMiddleware = generateMiddleware;
|
|
8
|
+
/**
|
|
9
|
+
* Generates the Next.js middleware with security headers and CORS.
|
|
10
|
+
* Widget endpoints (/api/v1/*) get CORS headers from CORS_ALLOWED_ORIGINS env.
|
|
11
|
+
* Also reads WIDGET_ALLOWED_ORIGINS as legacy alias for backward compatibility.
|
|
12
|
+
* @returns TypeScript content for middleware.ts
|
|
13
|
+
*/
|
|
14
|
+
function generateMiddleware() {
|
|
15
|
+
return `// @chimerai component=Middleware version=1.2
|
|
16
|
+
import { NextResponse } from 'next/server';
|
|
17
|
+
import type { NextRequest } from 'next/server';
|
|
18
|
+
import { getToken } from 'next-auth/jwt';
|
|
19
|
+
|
|
20
|
+
/** Routes that require authentication */
|
|
21
|
+
const PROTECTED_PATHS = ['/dashboard', '/chat', '/admin', '/api/models', '/api/conversations', '/api/admin', '/api/billing'];
|
|
22
|
+
|
|
23
|
+
/** Routes that are always public (no auth needed) */
|
|
24
|
+
const PUBLIC_PATHS = ['/auth', '/api/auth', '/api/v1/', '/_next', '/favicon.ico', '/widget'];
|
|
25
|
+
|
|
26
|
+
/** Merge CORS_ALLOWED_ORIGINS + WIDGET_ALLOWED_ORIGINS (legacy alias) */
|
|
27
|
+
function getAllowedOrigins(): string[] {
|
|
28
|
+
const raw = [
|
|
29
|
+
process.env.CORS_ALLOWED_ORIGINS || '',
|
|
30
|
+
process.env.WIDGET_ALLOWED_ORIGINS || '',
|
|
31
|
+
].filter(Boolean).join(',');
|
|
32
|
+
if (!raw) return ['*'];
|
|
33
|
+
return raw.split(',').map(o => o.trim()).filter(Boolean);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export async function middleware(request: NextRequest) {
|
|
37
|
+
const { pathname } = request.nextUrl;
|
|
38
|
+
|
|
39
|
+
// --- CORS for Widget/Public API Endpoints (/api/v1/*) ---
|
|
40
|
+
if (pathname.startsWith('/api/v1/')) {
|
|
41
|
+
const allowedOrigins = getAllowedOrigins();
|
|
42
|
+
const origin = request.headers.get('origin') || '';
|
|
43
|
+
|
|
44
|
+
// Preflight (OPTIONS) — respond directly without further processing
|
|
45
|
+
if (request.method === 'OPTIONS') {
|
|
46
|
+
const res = new NextResponse(null, { status: 204 });
|
|
47
|
+
const allowOrigin = allowedOrigins.includes('*') ? '*' : (allowedOrigins.includes(origin) ? origin : '');
|
|
48
|
+
if (allowOrigin) {
|
|
49
|
+
res.headers.set('Access-Control-Allow-Origin', allowOrigin);
|
|
50
|
+
}
|
|
51
|
+
res.headers.set('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
|
|
52
|
+
res.headers.set('Access-Control-Allow-Headers', 'Content-Type, x-api-key, Authorization');
|
|
53
|
+
res.headers.set('Access-Control-Max-Age', '86400');
|
|
54
|
+
return res;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Actual request — add CORS headers
|
|
58
|
+
const response = NextResponse.next();
|
|
59
|
+
const allowOrigin = allowedOrigins.includes('*') ? '*' :
|
|
60
|
+
(allowedOrigins.includes(origin) ? origin : '');
|
|
61
|
+
if (allowOrigin) {
|
|
62
|
+
response.headers.set('Access-Control-Allow-Origin', allowOrigin);
|
|
63
|
+
}
|
|
64
|
+
return response;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// --- Auth protection for protected routes ---
|
|
68
|
+
const isPublic = PUBLIC_PATHS.some(p => pathname.startsWith(p));
|
|
69
|
+
const isProtected = PROTECTED_PATHS.some(p => pathname.startsWith(p));
|
|
70
|
+
|
|
71
|
+
if (!isPublic && isProtected) {
|
|
72
|
+
const token = await getToken({ req: request, secret: process.env.NEXTAUTH_SECRET });
|
|
73
|
+
if (!token) {
|
|
74
|
+
// API routes return 401, pages redirect to sign-in
|
|
75
|
+
if (pathname.startsWith('/api/')) {
|
|
76
|
+
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
|
77
|
+
}
|
|
78
|
+
const signInUrl = new URL('/auth/signin', request.url);
|
|
79
|
+
signInUrl.searchParams.set('callbackUrl', pathname);
|
|
80
|
+
return NextResponse.redirect(signInUrl);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// --- Standard security headers for all other routes ---
|
|
85
|
+
const response = NextResponse.next();
|
|
86
|
+
|
|
87
|
+
// X-Frame-Options — Prevent clickjacking
|
|
88
|
+
response.headers.set('X-Frame-Options', 'DENY');
|
|
89
|
+
|
|
90
|
+
// X-Content-Type-Options — Prevent MIME sniffing
|
|
91
|
+
response.headers.set('X-Content-Type-Options', 'nosniff');
|
|
92
|
+
|
|
93
|
+
// Referrer-Policy
|
|
94
|
+
response.headers.set('Referrer-Policy', 'strict-origin-when-cross-origin');
|
|
95
|
+
|
|
96
|
+
// Permissions-Policy
|
|
97
|
+
response.headers.set('Permissions-Policy', 'camera=(), microphone=(), geolocation=()');
|
|
98
|
+
|
|
99
|
+
// HSTS in production
|
|
100
|
+
if (process.env.NODE_ENV === 'production') {
|
|
101
|
+
response.headers.set(
|
|
102
|
+
'Strict-Transport-Security',
|
|
103
|
+
'max-age=31536000; includeSubDomains; preload'
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return response;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export const config = {
|
|
111
|
+
matcher: [
|
|
112
|
+
'/((?!_next/static|_next/image|favicon.ico).*)',
|
|
113
|
+
],
|
|
114
|
+
};
|
|
115
|
+
`;
|
|
116
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Prisma and library templates
|
|
3
|
+
* Generates Prisma client initialization, encryption utilities, and API key auth
|
|
4
|
+
*/
|
|
5
|
+
/**
|
|
6
|
+
* Generates the Prisma client singleton
|
|
7
|
+
* Uses global singleton pattern to prevent connection exhaustion in serverless environments
|
|
8
|
+
* @returns TypeScript content for lib/prisma.ts
|
|
9
|
+
*/
|
|
10
|
+
export declare function generatePrismaLib(): string;
|
|
11
|
+
/**
|
|
12
|
+
* Generates the encryption utility for API key storage
|
|
13
|
+
* Uses AES-256-GCM for authenticated symmetric encryption
|
|
14
|
+
* @returns TypeScript content for lib/encryption.ts
|
|
15
|
+
*/
|
|
16
|
+
export declare function generateEncryptionLib(): string;
|
|
17
|
+
/**
|
|
18
|
+
* Generates the API key authentication middleware
|
|
19
|
+
* Supports API key-based auth for standalone widgets and external integrations
|
|
20
|
+
* @returns TypeScript content for lib/api-key-auth.ts
|
|
21
|
+
*/
|
|
22
|
+
export declare function generateApiKeyAuthLib(): string;
|
|
23
|
+
/**
|
|
24
|
+
* Generates the API protection middleware
|
|
25
|
+
* Handles session/auth checks, permission verification, and usage tracking
|
|
26
|
+
* @returns TypeScript content for lib/api-protection.ts
|
|
27
|
+
*/
|
|
28
|
+
export declare function generateApiProtectionLib(): string;
|
|
29
|
+
/**
|
|
30
|
+
* Generates the Prisma schema for all features
|
|
31
|
+
* Includes User, Role, Provider, Model, Conversation, Message, AuditLog, and SystemSetting models
|
|
32
|
+
* @returns TypeScript content for prisma/schema.prisma (content)
|
|
33
|
+
*/
|
|
34
|
+
export declare function generatePrismaSchema(): string;
|
|
35
|
+
//# sourceMappingURL=prisma.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"prisma.d.ts","sourceRoot":"","sources":["../../src/templates/prisma.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH;;;;GAIG;AACH,wBAAgB,iBAAiB,IAAI,MAAM,CAY1C;AAED;;;;GAIG;AACH,wBAAgB,qBAAqB,IAAI,MAAM,CA8D9C;AAED;;;;GAIG;AACH,wBAAgB,qBAAqB,IAAI,MAAM,CAiJ9C;AAED;;;;GAIG;AACH,wBAAgB,wBAAwB,IAAI,MAAM,CAiLjD;AAED;;;;GAIG;AACH,wBAAgB,oBAAoB,IAAI,MAAM,CA+R7C"}
|