@mordn/chat-widget 0.7.0 → 0.8.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/README.md +58 -23
- package/SECURITY.md +101 -0
- package/dist/chat-store-DERCPwhl.d.mts +278 -0
- package/dist/chat-store-DERCPwhl.d.ts +278 -0
- package/dist/cli/init.js +111 -346
- package/dist/index.d.mts +105 -13
- package/dist/index.d.ts +105 -13
- package/dist/index.js +642 -351
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +597 -303
- package/dist/index.mjs.map +1 -1
- package/dist/server/drizzle/index.d.mts +340 -0
- package/dist/server/drizzle/index.d.ts +340 -0
- package/dist/server/drizzle/index.js +238 -0
- package/dist/server/drizzle/index.js.map +1 -0
- package/dist/server/drizzle/index.mjs +207 -0
- package/dist/server/drizzle/index.mjs.map +1 -0
- package/dist/server/index.d.mts +217 -0
- package/dist/server/index.d.ts +217 -0
- package/dist/server/index.js +370 -0
- package/dist/server/index.js.map +1 -0
- package/dist/server/index.mjs +349 -0
- package/dist/server/index.mjs.map +1 -0
- package/dist/server/supabase/index.d.mts +50 -0
- package/dist/server/supabase/index.d.ts +50 -0
- package/dist/server/supabase/index.js +111 -0
- package/dist/server/supabase/index.js.map +1 -0
- package/dist/server/supabase/index.mjs +86 -0
- package/dist/server/supabase/index.mjs.map +1 -0
- package/dist/storage-adapter-DD8uqiAP.d.mts +126 -0
- package/dist/storage-adapter-DD8uqiAP.d.ts +126 -0
- package/dist/styles.css +1 -1
- package/package.json +21 -5
package/dist/cli/init.js
CHANGED
|
@@ -51,9 +51,17 @@ function detectAppDir() {
|
|
|
51
51
|
}
|
|
52
52
|
return path.join(process.cwd(), "src", "app");
|
|
53
53
|
}
|
|
54
|
+
function detectLibDir() {
|
|
55
|
+
if (fs.existsSync(path.join(process.cwd(), "src"))) {
|
|
56
|
+
return path.join(process.cwd(), "src", "lib");
|
|
57
|
+
}
|
|
58
|
+
return path.join(process.cwd(), "lib");
|
|
59
|
+
}
|
|
54
60
|
async function writeFileWithConfirm(filePath, content) {
|
|
55
61
|
if (fs.existsSync(filePath)) {
|
|
56
|
-
const overwrite = await confirm(
|
|
62
|
+
const overwrite = await confirm(
|
|
63
|
+
`File ${path.relative(process.cwd(), filePath)} already exists. Overwrite?`
|
|
64
|
+
);
|
|
57
65
|
if (!overwrite) {
|
|
58
66
|
console.log(` Skipped: ${path.relative(process.cwd(), filePath)}`);
|
|
59
67
|
return false;
|
|
@@ -64,320 +72,90 @@ async function writeFileWithConfirm(filePath, content) {
|
|
|
64
72
|
console.log(` Created: ${path.relative(process.cwd(), filePath)}`);
|
|
65
73
|
return true;
|
|
66
74
|
}
|
|
67
|
-
var
|
|
68
|
-
import {
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
//
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
if (!existingConv.length) {
|
|
103
|
-
await db.insert(conversations).values({
|
|
104
|
-
id,
|
|
105
|
-
userId,
|
|
106
|
-
title: 'New Chat',
|
|
107
|
-
metadata: {},
|
|
108
|
-
});
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
// Save the new user message
|
|
112
|
-
const userMessages = chatMessages.filter(msg => msg.role === 'user');
|
|
113
|
-
if (userMessages.length > 0) {
|
|
114
|
-
const newUserMessage = userMessages[userMessages.length - 1];
|
|
115
|
-
const textPart = newUserMessage.parts?.find(p => p.type === 'text') as { text: string } | undefined;
|
|
116
|
-
const fileParts = newUserMessage.parts?.filter(p => p.type === 'file') || [];
|
|
117
|
-
|
|
118
|
-
const existingMsg = await db
|
|
119
|
-
.select({ id: messages.id })
|
|
120
|
-
.from(messages)
|
|
121
|
-
.where(eq(messages.id, newUserMessage.id))
|
|
122
|
-
.limit(1);
|
|
123
|
-
|
|
124
|
-
if (!existingMsg.length) {
|
|
125
|
-
await db.insert(messages).values({
|
|
126
|
-
id: newUserMessage.id,
|
|
127
|
-
conversationId: id,
|
|
128
|
-
role: newUserMessage.role,
|
|
129
|
-
content: textPart?.text || '',
|
|
130
|
-
files: fileParts,
|
|
131
|
-
model: model,
|
|
132
|
-
metadata: { parts: newUserMessage.parts || [] },
|
|
133
|
-
});
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
// Update conversation title if needed
|
|
137
|
-
if (textPart?.text) {
|
|
138
|
-
const conv = await db
|
|
139
|
-
.select({ title: conversations.title })
|
|
140
|
-
.from(conversations)
|
|
141
|
-
.where(eq(conversations.id, id))
|
|
142
|
-
.limit(1);
|
|
143
|
-
|
|
144
|
-
if (conv[0]?.title === 'New Chat') {
|
|
145
|
-
await updateConversationTitle(id, textPart.text.slice(0, 100));
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
// Transform messages for AI (handle images)
|
|
151
|
-
const transformedMessages = chatMessages.map(msg => {
|
|
152
|
-
if (msg.role === 'user' && msg.parts) {
|
|
153
|
-
const textPart = msg.parts.find(p => p.type === 'text');
|
|
154
|
-
const fileParts = msg.parts.filter(p => p.type === 'file');
|
|
155
|
-
|
|
156
|
-
if (fileParts.length > 0) {
|
|
157
|
-
const content: any[] = [];
|
|
158
|
-
if (textPart && 'text' in textPart) {
|
|
159
|
-
content.push({ type: 'text', text: textPart.text });
|
|
160
|
-
}
|
|
161
|
-
for (const file of fileParts) {
|
|
162
|
-
if ('mediaType' in file && (file as any).mediaType?.startsWith('image/')) {
|
|
163
|
-
content.push({ type: 'image', image: (file as any).url });
|
|
164
|
-
}
|
|
165
|
-
}
|
|
166
|
-
return { ...msg, content };
|
|
167
|
-
}
|
|
168
|
-
}
|
|
169
|
-
return msg;
|
|
170
|
-
});
|
|
171
|
-
|
|
172
|
-
const result = streamText({
|
|
173
|
-
model: model,
|
|
174
|
-
messages: convertToModelMessages(transformedMessages),
|
|
175
|
-
system: systemPrompt,
|
|
176
|
-
temperature: temperature,
|
|
177
|
-
});
|
|
178
|
-
|
|
179
|
-
return result.toUIMessageStreamResponse({
|
|
180
|
-
sendSources: true,
|
|
181
|
-
sendReasoning: true,
|
|
182
|
-
onFinish: ({ messages: finalMessages }) => {
|
|
183
|
-
if (finalMessages.length > 0) {
|
|
184
|
-
saveChat({ chatId: id, messages: finalMessages, model, userId });
|
|
185
|
-
}
|
|
186
|
-
},
|
|
187
|
-
});
|
|
188
|
-
} catch (error) {
|
|
189
|
-
console.error('Chat API error:', error);
|
|
190
|
-
return new Response(JSON.stringify({ error: 'Internal server error' }), {
|
|
191
|
-
status: 500,
|
|
192
|
-
headers: { 'Content-Type': 'application/json' },
|
|
193
|
-
});
|
|
194
|
-
}
|
|
195
|
-
}
|
|
196
|
-
`;
|
|
197
|
-
var HISTORY_ROUTE = `import { NextResponse } from 'next/server';
|
|
198
|
-
import { getConversations } from '@mordn/chat-widget/api';
|
|
199
|
-
|
|
200
|
-
export async function GET(request: Request) {
|
|
201
|
-
try {
|
|
202
|
-
const url = new URL(request.url);
|
|
203
|
-
const userId = url.searchParams.get('userId') || request.headers.get('X-User-Id');
|
|
204
|
-
|
|
205
|
-
if (!userId) {
|
|
206
|
-
return NextResponse.json({ error: 'userId is required' }, { status: 400 });
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
const conversationsData = await getConversations(userId);
|
|
210
|
-
|
|
211
|
-
const conversations = conversationsData.map(conv => ({
|
|
212
|
-
id: conv.id,
|
|
213
|
-
title: conv.title,
|
|
214
|
-
created_at: conv.createdAt,
|
|
215
|
-
updated_at: conv.updatedAt,
|
|
216
|
-
metadata: conv.metadata,
|
|
217
|
-
message_count: conv.messageCount,
|
|
218
|
-
}));
|
|
219
|
-
|
|
220
|
-
return NextResponse.json({ conversations });
|
|
221
|
-
} catch (error) {
|
|
222
|
-
console.error('Error in chat history API:', error);
|
|
223
|
-
return NextResponse.json({ error: 'Internal server error' }, { status: 500 });
|
|
224
|
-
}
|
|
225
|
-
}
|
|
226
|
-
`;
|
|
227
|
-
var CONVERSATION_ROUTE = `import { NextResponse } from 'next/server';
|
|
228
|
-
import { db, conversations, messages, eq, and, asc } from '@mordn/chat-widget/api';
|
|
229
|
-
|
|
230
|
-
export async function GET(
|
|
231
|
-
request: Request,
|
|
232
|
-
{ params }: { params: Promise<{ conversationId: string }> }
|
|
233
|
-
) {
|
|
234
|
-
try {
|
|
235
|
-
const { conversationId } = await params;
|
|
236
|
-
const url = new URL(request.url);
|
|
237
|
-
const userId = url.searchParams.get('userId') || request.headers.get('X-User-Id');
|
|
238
|
-
|
|
239
|
-
if (!userId) {
|
|
240
|
-
return NextResponse.json({ error: 'userId is required' }, { status: 400 });
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
// Verify the conversation belongs to the user
|
|
244
|
-
const conv = await db
|
|
245
|
-
.select({
|
|
246
|
-
id: conversations.id,
|
|
247
|
-
title: conversations.title,
|
|
248
|
-
metadata: conversations.metadata,
|
|
249
|
-
})
|
|
250
|
-
.from(conversations)
|
|
251
|
-
.where(and(
|
|
252
|
-
eq(conversations.id, conversationId),
|
|
253
|
-
eq(conversations.userId, userId)
|
|
254
|
-
))
|
|
255
|
-
.limit(1);
|
|
256
|
-
|
|
257
|
-
if (!conv.length) {
|
|
258
|
-
return NextResponse.json({ error: 'Conversation not found' }, { status: 404 });
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
const conversation = conv[0];
|
|
262
|
-
|
|
263
|
-
const dbMessages = await db
|
|
264
|
-
.select()
|
|
265
|
-
.from(messages)
|
|
266
|
-
.where(eq(messages.conversationId, conversationId))
|
|
267
|
-
.orderBy(asc(messages.createdAt))
|
|
268
|
-
.limit(1000);
|
|
269
|
-
|
|
270
|
-
const transformedMessages = dbMessages.map(msg => {
|
|
271
|
-
const metadata = msg.metadata as { parts?: any[] } | null;
|
|
272
|
-
|
|
273
|
-
if (metadata?.parts && Array.isArray(metadata.parts)) {
|
|
274
|
-
return {
|
|
275
|
-
id: msg.id,
|
|
276
|
-
role: msg.role,
|
|
277
|
-
content: msg.content,
|
|
278
|
-
created_at: msg.createdAt,
|
|
279
|
-
parts: metadata.parts
|
|
280
|
-
};
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
return {
|
|
284
|
-
id: msg.id,
|
|
285
|
-
role: msg.role,
|
|
286
|
-
content: msg.content,
|
|
287
|
-
created_at: msg.createdAt,
|
|
288
|
-
parts: msg.content ? [{ type: 'text', text: msg.content }] : undefined
|
|
289
|
-
};
|
|
290
|
-
});
|
|
291
|
-
|
|
292
|
-
return NextResponse.json({ conversation, messages: transformedMessages });
|
|
293
|
-
} catch (error) {
|
|
294
|
-
console.error('Error loading conversation:', error);
|
|
295
|
-
return NextResponse.json({ error: 'Internal server error' }, { status: 500 });
|
|
296
|
-
}
|
|
297
|
-
}
|
|
75
|
+
var CATCHALL_ROUTE = `import { createChatHandler } from '@mordn/chat-widget/server';
|
|
76
|
+
import { createDrizzleChatStore } from '@mordn/chat-widget/server/drizzle';
|
|
77
|
+
import { createSupabaseStorage } from '@mordn/chat-widget/server/supabase';
|
|
78
|
+
import { anthropic } from '@ai-sdk/anthropic';
|
|
79
|
+
import { getChatUserId } from '@/lib/chat-auth';
|
|
80
|
+
|
|
81
|
+
// Allow tool-using turns to stream beyond the default 30s.
|
|
82
|
+
export const maxDuration = 300;
|
|
83
|
+
|
|
84
|
+
export const { GET, POST, DELETE } = createChatHandler({
|
|
85
|
+
// REQUIRED: derive the user id from your SERVER session. See lib/chat-auth.ts.
|
|
86
|
+
getUserId: getChatUserId,
|
|
87
|
+
|
|
88
|
+
// Which model to stream from. Swap for your provider/model.
|
|
89
|
+
model: anthropic('claude-sonnet-4-5'),
|
|
90
|
+
|
|
91
|
+
// Persistence. The default Drizzle store uses DATABASE_URL. Replace with
|
|
92
|
+
// your own ChatStore to bring your own database.
|
|
93
|
+
store: createDrizzleChatStore(),
|
|
94
|
+
|
|
95
|
+
// Attachments. The default uses a PRIVATE Supabase bucket + signed URLs.
|
|
96
|
+
// Remove this line to disable uploads, or pass your own StorageAdapter.
|
|
97
|
+
storage: createSupabaseStorage(),
|
|
98
|
+
|
|
99
|
+
// A system prompt. Make it a function of ctx to personalise per user.
|
|
100
|
+
buildSystemPrompt: () => 'You are a helpful assistant.',
|
|
101
|
+
|
|
102
|
+
// Add your tools here. buildTools is async and receives the request context
|
|
103
|
+
// (userId, conversationId, request) so tools can be user-scoped. If a tool
|
|
104
|
+
// holds a per-request resource (e.g. an MCP client), return a \`cleanup\`
|
|
105
|
+
// and the handler will tear it down exactly once when the turn ends.
|
|
106
|
+
//
|
|
107
|
+
// buildTools: async (ctx) => ({ tools: { /* ... */ }, cleanup: async () => {} }),
|
|
108
|
+
});
|
|
298
109
|
`;
|
|
299
|
-
var
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
const timestamp = Date.now();
|
|
341
|
-
const randomId = nanoid(8);
|
|
342
|
-
const safeFilename = file.name.replace(/[^a-zA-Z0-9.-]/g, '_');
|
|
343
|
-
const filePath = \`\${userId}/\${conversationId || 'default'}/\${timestamp}-\${randomId}-\${safeFilename}\`;
|
|
344
|
-
|
|
345
|
-
const fileBuffer = await file.arrayBuffer();
|
|
346
|
-
|
|
347
|
-
const { error: uploadError } = await supabase.storage
|
|
348
|
-
.from('chat-attachments')
|
|
349
|
-
.upload(filePath, fileBuffer, {
|
|
350
|
-
contentType: file.type,
|
|
351
|
-
upsert: false,
|
|
352
|
-
});
|
|
353
|
-
|
|
354
|
-
if (uploadError) {
|
|
355
|
-
console.error('Upload error:', uploadError);
|
|
356
|
-
return Response.json({ error: 'Failed to upload file' }, { status: 500 });
|
|
357
|
-
}
|
|
358
|
-
|
|
359
|
-
const { data: urlData } = supabase.storage
|
|
360
|
-
.from('chat-attachments')
|
|
361
|
-
.getPublicUrl(filePath);
|
|
362
|
-
|
|
363
|
-
return Response.json({
|
|
364
|
-
url: urlData.publicUrl,
|
|
365
|
-
filename: file.name,
|
|
366
|
-
mediaType: file.type,
|
|
367
|
-
size: file.size,
|
|
368
|
-
type: 'file',
|
|
369
|
-
});
|
|
370
|
-
} catch (error) {
|
|
371
|
-
console.error('Upload API error:', error);
|
|
372
|
-
return Response.json({ error: 'Internal server error' }, { status: 500 });
|
|
373
|
-
}
|
|
110
|
+
var CHAT_AUTH_STUB = `/**
|
|
111
|
+
* Chat identity \u2014 the security boundary.
|
|
112
|
+
*
|
|
113
|
+
* Return the authenticated user's id derived from your SERVER session: a
|
|
114
|
+
* verified cookie / JWT, Clerk \`auth()\`, NextAuth \`getServerSession()\`,
|
|
115
|
+
* \`supabase.auth.getUser()\`, etc. Return \`null\` for an unauthenticated
|
|
116
|
+
* request (the handler responds 401).
|
|
117
|
+
*
|
|
118
|
+
* SECURITY \u2014 read this once:
|
|
119
|
+
* \u2022 NEVER read the id from the request body, query string, or a header the
|
|
120
|
+
* browser controls (e.g. X-User-Id). Those are forgeable; trusting them
|
|
121
|
+
* lets any user read/write another user's conversations (IDOR).
|
|
122
|
+
* \u2022 The widget DOES send an X-User-Id header \u2014 ignore it for authorization.
|
|
123
|
+
* It is not, and must never be treated as, proof of identity.
|
|
124
|
+
*
|
|
125
|
+
* This stub throws on purpose. Replace its body with your real session lookup
|
|
126
|
+
* before going to production.
|
|
127
|
+
*/
|
|
128
|
+
export async function getChatUserId(request: Request): Promise<string | null> {
|
|
129
|
+
// \u2500\u2500 Example (Clerk) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
130
|
+
// import { auth } from '@clerk/nextjs/server';
|
|
131
|
+
// const { userId } = await auth();
|
|
132
|
+
// return userId;
|
|
133
|
+
//
|
|
134
|
+
// \u2500\u2500 Example (NextAuth) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
135
|
+
// import { getServerSession } from 'next-auth';
|
|
136
|
+
// const session = await getServerSession(authOptions);
|
|
137
|
+
// return session?.user?.id ?? null;
|
|
138
|
+
//
|
|
139
|
+
// \u2500\u2500 Example (Supabase Auth) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
140
|
+
// const supabase = createServerClient(/* ... */);
|
|
141
|
+
// const { data: { user } } = await supabase.auth.getUser();
|
|
142
|
+
// return user?.id ?? null;
|
|
143
|
+
|
|
144
|
+
void request;
|
|
145
|
+
throw new Error(
|
|
146
|
+
'[chat-widget] getChatUserId is not implemented. Derive the user id from ' +
|
|
147
|
+
'your server session and return it (or null). See the examples in this ' +
|
|
148
|
+
'file. Do NOT read the id from request headers/query/body.',
|
|
149
|
+
);
|
|
374
150
|
}
|
|
375
151
|
`;
|
|
376
152
|
var DRIZZLE_CONFIG = `import 'dotenv/config';
|
|
377
153
|
import { defineConfig } from 'drizzle-kit';
|
|
378
154
|
|
|
379
155
|
export default defineConfig({
|
|
380
|
-
|
|
156
|
+
// The default store's schema lives in the package. drizzle-kit reads it from
|
|
157
|
+
// the built dist so it can generate/push migrations for the chat tables.
|
|
158
|
+
schema: './node_modules/@mordn/chat-widget/dist/server/drizzle/index.js',
|
|
381
159
|
out: './drizzle',
|
|
382
160
|
dialect: 'postgresql',
|
|
383
161
|
dbCredentials: {
|
|
@@ -385,43 +163,35 @@ export default defineConfig({
|
|
|
385
163
|
},
|
|
386
164
|
});
|
|
387
165
|
`;
|
|
388
|
-
var ENV_EXAMPLE = `# Database (
|
|
166
|
+
var ENV_EXAMPLE = `# Database (required for the default Drizzle store)
|
|
389
167
|
DATABASE_URL="postgresql://postgres.xxx:[PASSWORD]@aws-0-region.pooler.supabase.com:6543/postgres"
|
|
390
168
|
|
|
391
|
-
#
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
# Supabase Storage (Optional - for file uploads)
|
|
169
|
+
# Attachments (required only if you keep createSupabaseStorage)
|
|
170
|
+
# The bucket MUST be created as a PRIVATE bucket.
|
|
395
171
|
NEXT_PUBLIC_SUPABASE_URL="https://xxx.supabase.co"
|
|
396
172
|
SUPABASE_SERVICE_ROLE_KEY="your-service-role-key"
|
|
397
173
|
`;
|
|
398
174
|
async function init() {
|
|
399
175
|
console.log("\n@mordn/chat-widget init\n");
|
|
400
|
-
console.log(
|
|
176
|
+
console.log(
|
|
177
|
+
"Scaffolds a secure-by-default chat backend: one catch-all route + an\nauth stub you implement. All chat logic lives in the package.\n"
|
|
178
|
+
);
|
|
401
179
|
const appDir = detectAppDir();
|
|
402
|
-
const
|
|
403
|
-
console.log(`Detected app directory: ${path.relative(process.cwd(), appDir)}
|
|
180
|
+
const libDir = detectLibDir();
|
|
181
|
+
console.log(`Detected app directory: ${path.relative(process.cwd(), appDir)}`);
|
|
182
|
+
console.log(`Detected lib directory: ${path.relative(process.cwd(), libDir)}
|
|
404
183
|
`);
|
|
405
184
|
let filesCreated = 0;
|
|
406
|
-
console.log("Creating
|
|
407
|
-
if (await writeFileWithConfirm(
|
|
185
|
+
console.log("Creating files...");
|
|
186
|
+
if (await writeFileWithConfirm(
|
|
187
|
+
path.join(appDir, "api", "chat", "[[...chat]]", "route.ts"),
|
|
188
|
+
CATCHALL_ROUTE
|
|
189
|
+
)) {
|
|
408
190
|
filesCreated++;
|
|
409
191
|
}
|
|
410
|
-
if (await writeFileWithConfirm(path.join(
|
|
192
|
+
if (await writeFileWithConfirm(path.join(libDir, "chat-auth.ts"), CHAT_AUTH_STUB)) {
|
|
411
193
|
filesCreated++;
|
|
412
194
|
}
|
|
413
|
-
if (await writeFileWithConfirm(path.join(apiChatDir, "history", "[conversationId]", "route.ts"), CONVERSATION_ROUTE)) {
|
|
414
|
-
filesCreated++;
|
|
415
|
-
}
|
|
416
|
-
const createUpload = await confirm("\nCreate file upload route? (requires Supabase Storage)");
|
|
417
|
-
let uploadRouteCreated = false;
|
|
418
|
-
if (createUpload) {
|
|
419
|
-
if (await writeFileWithConfirm(path.join(apiChatDir, "upload", "route.ts"), UPLOAD_ROUTE)) {
|
|
420
|
-
filesCreated++;
|
|
421
|
-
uploadRouteCreated = true;
|
|
422
|
-
}
|
|
423
|
-
}
|
|
424
|
-
console.log("\nCreating configuration files...");
|
|
425
195
|
if (await writeFileWithConfirm(path.join(process.cwd(), "drizzle.config.ts"), DRIZZLE_CONFIG)) {
|
|
426
196
|
filesCreated++;
|
|
427
197
|
}
|
|
@@ -433,21 +203,16 @@ async function init() {
|
|
|
433
203
|
`);
|
|
434
204
|
console.log("Next steps:");
|
|
435
205
|
console.log(" 1. Copy .env.example to .env.local and fill in your credentials");
|
|
436
|
-
console.log(" 2.
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
console.log(" import { ChatWidget } from '@mordn/chat-widget';");
|
|
447
|
-
console.log(" import '@mordn/chat-widget/styles.css';");
|
|
448
|
-
console.log("");
|
|
449
|
-
console.log(' <ChatWidget userId="user-123" />\n');
|
|
450
|
-
}
|
|
206
|
+
console.log(" 2. Implement getChatUserId() in lib/chat-auth.ts");
|
|
207
|
+
console.log(" \u26A0 Until you do, every chat request will throw \u2014 by design.");
|
|
208
|
+
console.log(" 3. Run: npx drizzle-kit push (creates the chat tables)");
|
|
209
|
+
console.log(' 4. If using uploads: create a PRIVATE "chat-attachments" bucket in Supabase');
|
|
210
|
+
console.log(" 5. Mount the widget in your app:\n");
|
|
211
|
+
console.log(" import { ChatWidget } from '@mordn/chat-widget';");
|
|
212
|
+
console.log(" import '@mordn/chat-widget/styles.css';");
|
|
213
|
+
console.log(" <ChatWidget userId={/* your user id */} />\n");
|
|
214
|
+
console.log("Security: see SECURITY.md \u2014 userId is established on the server,");
|
|
215
|
+
console.log("never trusted from the client.\n");
|
|
451
216
|
rl.close();
|
|
452
217
|
}
|
|
453
218
|
init().catch((error) => {
|