Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
123 commits
Select commit Hold shift + click to select a range
1b8d902
add browser specific settings for Gecko compatibility
SSoggyTacoMan Mar 11, 2026
196015e
update README to clarify browser compatibility and add Firefox instal…
SSoggyTacoMan Mar 11, 2026
f44dab6
update README to include Firefox Add-on Store link and enhance usage …
SSoggyTacoMan Mar 11, 2026
3d8b6ef
Refactor ShareClaude extension for Firefox compatibility
SSoggyTacoMan Mar 12, 2026
e0a1a8c
Revise README for installation and platform badges
SSoggyTacoMan Mar 12, 2026
871de3b
enhance button styling and toolbar integration for share and export f…
SSoggyTacoMan Mar 12, 2026
ebd902e
Merge branch 'main' of https://github.com/SSoggyTacoMan/shareclaude-f…
SSoggyTacoMan Mar 12, 2026
08d2529
Revise installation instructions and download links
SSoggyTacoMan Mar 12, 2026
16272df
Implement conversation threading in ChatViewer and enhance ChatMessag…
SSoggyTacoMan Mar 12, 2026
5dc84b3
Merge branch 'main' of https://github.com/SSoggyTacoMan/shareclaude-f…
SSoggyTacoMan Mar 12, 2026
1d91073
Refactor MarkdownRenderer component for improved styling and structure
SSoggyTacoMan Mar 12, 2026
1675153
Update README and content.js to enhance sharing and exporting features
SSoggyTacoMan Mar 12, 2026
b800da5
Enhance README and update Share button functionality in content.js; a…
SSoggyTacoMan Mar 12, 2026
89875b3
Update .gitignore to include claude-example-2.html and adjust CodeBlo…
SSoggyTacoMan Mar 12, 2026
aafb714
Enhance README with export functionality description
SSoggyTacoMan Mar 12, 2026
7442117
Refactor button injection in content.js and remove web accessible res…
SSoggyTacoMan Mar 12, 2026
10973d5
Merge branch 'main' of https://github.com/SSoggyTacoMan/shareclaude-f…
SSoggyTacoMan Mar 12, 2026
98ff03e
Remove claude-example.html from .gitignore
SSoggyTacoMan Mar 12, 2026
10aed3a
Remove unused loader SVG from content.js
SSoggyTacoMan Mar 12, 2026
78022a8
Merge branch 'main' of https://github.com/SSoggyTacoMan/shareclaude-f…
SSoggyTacoMan Mar 12, 2026
e8c01b1
Enhance MarkdownRenderer to support excerpt parsing and rendering
SSoggyTacoMan Mar 12, 2026
82ffee6
Implement markdown to HTML conversion in convertToHTML function
SSoggyTacoMan Mar 12, 2026
07155a3
Enhance action bar detection and debugging in injectButtons function
SSoggyTacoMan Mar 12, 2026
04448e1
Refactor initialization logic to ensure proper execution on page load
SSoggyTacoMan Mar 12, 2026
84f191c
Enhance markdown conversion functions to support excerpt handling and…
SSoggyTacoMan Mar 12, 2026
048e6f0
Enhance RTF and DOCX conversion functions to handle excerpts from pre…
SSoggyTacoMan Mar 12, 2026
5fe8e16
Refactor message formatting in conversion functions to improve clarit…
SSoggyTacoMan Mar 12, 2026
b7a2922
Update role labels in conversion functions from 'Human' to 'You' for …
SSoggyTacoMan Mar 12, 2026
a834f9a
Implement raw chat retrieval API endpoint
SSoggyTacoMan Mar 12, 2026
0dd01df
Add link to raw chat data in ChatViewer component
SSoggyTacoMan Mar 12, 2026
a19ebd0
Update ChatViewer to use dynamic API URL and improve layout consistency
SSoggyTacoMan Mar 12, 2026
6044eb0
Update README with clearer instructions and formatting
SSoggyTacoMan Mar 12, 2026
5ddb0fd
Update meta tags and content in index.html for consistency; add Chrom…
SSoggyTacoMan Mar 12, 2026
fe313f5
Merge branch 'main' of https://github.com/SSoggyTacoMan/shareclaude-f…
SSoggyTacoMan Mar 12, 2026
3c63ccc
Add emphasis to browser compatibility message in Home component
SSoggyTacoMan Mar 12, 2026
205334b
Add Chrome icon to Footer component and update link attributes for ac…
SSoggyTacoMan Mar 12, 2026
30515a8
Add Star History section to README
SSoggyTacoMan Mar 12, 2026
e1ad907
Merge pull request #3 from SSoggyTacoMan/patch-1
SSoggyTacoMan Mar 12, 2026
0b2033b
Replace Globe icon with Flame icon in Footer component and adjust lay…
SSoggyTacoMan Mar 12, 2026
0f71701
Merge branch 'main' of https://github.com/SSoggyTacoMan/shareclaude-f…
SSoggyTacoMan Mar 12, 2026
34d4617
Update caniuse-lite version in package-lock.json to 1.0.30001778
SSoggyTacoMan Mar 12, 2026
36860ad
Add PropTypes validation to CodeBlock component
SSoggyTacoMan Mar 12, 2026
0b8d51c
Remove left margin from icon container in Footer component for better…
SSoggyTacoMan Mar 12, 2026
cf11e71
Reduce vertical spacing in Footer component for improved layout
SSoggyTacoMan Mar 12, 2026
7bb8585
Refactor spacing and layout in Footer and Home components for improve…
SSoggyTacoMan Mar 12, 2026
e376d81
if this doesnt fix the fucking spacing bro
SSoggyTacoMan Mar 12, 2026
3eaa7a8
IT FINALLY WORKED
SSoggyTacoMan Mar 12, 2026
9726e52
Update button label from 'Get Share Link' to 'Get Example Link' for c…
SSoggyTacoMan Mar 12, 2026
fbcc26a
Refactor API origin retrieval and enhance error handling in ChatViewe…
SSoggyTacoMan Mar 12, 2026
159593d
Update divider styles for improved visibility and aesthetics
SSoggyTacoMan Mar 12, 2026
c6cfa47
Enhance error handling in API calls and add RawViewer component for c…
SSoggyTacoMan Mar 12, 2026
466b066
Refactor button insertion logic in injectButtons function for improve…
SSoggyTacoMan Mar 12, 2026
05d18c4
Refactor injectButtons function to improve control positioning and st…
SSoggyTacoMan Mar 12, 2026
115044e
Refactor comments in MarkdownRenderer and remove unnecessary lines in…
SSoggyTacoMan Mar 12, 2026
bcfee8c
chore: update wrangler to version 4.72.0 and add API proxy configurat…
SSoggyTacoMan Mar 12, 2026
a0f3eb5
remove local host shit
SSoggyTacoMan Mar 12, 2026
da0b1a4
Add PropTypes validation for ChatMessage component
SSoggyTacoMan Mar 12, 2026
31d9247
remove claude example from git ignore
SSoggyTacoMan Mar 12, 2026
e8c7ee0
Initial plan
Copilot Mar 12, 2026
94f2c05
Fix review feedback: extract shared utils, simplify RawViewer fetch, …
Copilot Mar 12, 2026
46dcf30
Merge pull request #5 from SSoggyTacoMan/copilot/sub-pr-4
SSoggyTacoMan Mar 12, 2026
83d2b26
Update gitignore for temp files
SSoggyTacoMan Mar 12, 2026
88f6849
Refine README wording and links
SSoggyTacoMan Mar 12, 2026
abe3761
Remove unused catchall handler argument
SSoggyTacoMan Mar 12, 2026
0dcdc03
Remove hardcoded theme color meta tag
SSoggyTacoMan Mar 12, 2026
1b32c99
Add prop types to MarkdownRenderer
SSoggyTacoMan Mar 12, 2026
ced38ac
Remove unused React import from NotFound
SSoggyTacoMan Mar 12, 2026
a0cb03a
Improve popup link safety and styles
SSoggyTacoMan Mar 12, 2026
e9308d7
Remove temp claude export sample
SSoggyTacoMan Mar 12, 2026
450c304
Remove temp scraped chat sample
SSoggyTacoMan Mar 12, 2026
997ebb3
Remove unnecessary blank line in README.md
SSoggyTacoMan Mar 12, 2026
4bfbabf
Merge branch 'dev' of https://github.com/SSoggyTacoMan/shareclaude-fi…
SSoggyTacoMan Mar 12, 2026
6ea55b1
Simplify error handling in RawViewer component
SSoggyTacoMan Mar 12, 2026
466690f
Refactor PrivacyPolicy component for improved readability and structure
SSoggyTacoMan Mar 12, 2026
4e14a0a
Refactor Tailwind CSS configuration to use imported typography plugin
SSoggyTacoMan Mar 12, 2026
8166105
this commit changes nothing lol
SSoggyTacoMan Mar 12, 2026
fdd83e2
Initial plan
Copilot Mar 12, 2026
33a23f6
Address PR review comments: typo fix, RawViewer API base, prop-types …
Copilot Mar 12, 2026
c6266af
Merge pull request #6 from SSoggyTacoMan/copilot/sub-pr-4
SSoggyTacoMan Mar 12, 2026
5c2f926
Merge pull request #4 from SSoggyTacoMan/dev
SSoggyTacoMan Mar 12, 2026
ba86f5d
Update README and popup description to reflect JSON export option
SSoggyTacoMan Mar 12, 2026
61f101d
Remove sitemap entry from robots.txt
SSoggyTacoMan Mar 12, 2026
3942996
Refactor MarkdownRenderer to improve excerpt splitting logic
SSoggyTacoMan Mar 12, 2026
3d16ddb
Fix excerpt handling and improve newline normalization in Markdown pr…
SSoggyTacoMan Mar 12, 2026
5f03570
Refactor excerpt handling by moving logic to excerpt-utils.js and upd…
SSoggyTacoMan Mar 12, 2026
b2d495b
Merge pull request #7 from SSoggyTacoMan/dev
SSoggyTacoMan Mar 12, 2026
857678d
🔒 [security fix] Fix overly permissive CORS configuration in Chats API
SSoggyTacoMan May 5, 2026
ac8b9d7
perf: optimize convertToText by using map().join('')
SSoggyTacoMan May 5, 2026
7c16919
cleanup: remove unused debugCandidatesLogged variable
SSoggyTacoMan May 5, 2026
05616f8
Merge pull request #10 from SSoggyTacoMan/cleanup-debug-variable-1703…
SSoggyTacoMan May 5, 2026
05db68c
Update app/functions/api/utils.js
SSoggyTacoMan May 5, 2026
3a99ed8
Merge pull request #8 from SSoggyTacoMan/fix-overly-permissive-cors-c…
SSoggyTacoMan May 5, 2026
033ae6c
Update extension/content.js
SSoggyTacoMan May 5, 2026
b54e1f4
Merge pull request #9 from SSoggyTacoMan/perf-optimize-convert-to-tex…
SSoggyTacoMan May 5, 2026
4ff9190
refactor: remove leftover console.log from raw.js
SSoggyTacoMan May 5, 2026
095399d
🧹 code health improvement: Remove left-over console.log from chat API…
SSoggyTacoMan May 5, 2026
3fc1a9c
🧪 test(extension): add tests for splitTextOnExcerpts edge cases
SSoggyTacoMan May 5, 2026
8fa34d0
test(api): add tests for chat creation validation and error handling
SSoggyTacoMan May 5, 2026
ed93fa1
refactor: remove left-over console.log from API routes
SSoggyTacoMan May 5, 2026
a8d997f
test(chats): add comprehensive tests for chat retrieval
SSoggyTacoMan May 5, 2026
fead25b
🔒 fix: insufficient input validation for chat content
SSoggyTacoMan May 5, 2026
1d6907f
perf: optimize markdown table parsing by avoiding array allocations
SSoggyTacoMan May 5, 2026
ae69087
perf: optimize DOCX string concatenation in extension
SSoggyTacoMan May 5, 2026
d90e728
🧹 [code health improvement] Remove console.debug logging
SSoggyTacoMan May 6, 2026
65594bd
Merge pull request #22 from SSoggyTacoMan/remove-console-debug-144497…
SSoggyTacoMan May 6, 2026
c73f09e
Update app/functions/api/chats/__tests__/[id].test.js
SSoggyTacoMan May 6, 2026
90aa5a6
Update app/functions/api/chats/__tests__/[id].test.js
SSoggyTacoMan May 6, 2026
aaaf31c
Merge pull request #17 from SSoggyTacoMan/improve-testing-for-chats-i…
SSoggyTacoMan May 6, 2026
e626047
Merge pull request #14 from SSoggyTacoMan/testing-improvement-chat-cr…
SSoggyTacoMan May 6, 2026
c5a0a42
Update app/functions/api/chats/index.js
SSoggyTacoMan May 6, 2026
1249e55
Update app/functions/api/chats/index.js
SSoggyTacoMan May 6, 2026
1ccf97f
Merge pull request #19 from SSoggyTacoMan/security/fix-chat-content-v…
SSoggyTacoMan May 6, 2026
06ef59a
Update extension/content.js
SSoggyTacoMan May 6, 2026
47558d1
Update extension/content.js
SSoggyTacoMan May 6, 2026
5bc47fe
Merge pull request #20 from SSoggyTacoMan/optimize-markdown-table-par…
SSoggyTacoMan May 6, 2026
305b051
Update app/functions/api/chats/index.js
SSoggyTacoMan May 6, 2026
0970e53
Update app/functions/api/chats/index.js
SSoggyTacoMan May 6, 2026
c0b3995
Merge pull request #12 from SSoggyTacoMan/fix-remove-leftover-console…
SSoggyTacoMan May 6, 2026
fd5ea30
Merge branch 'main' into fix-code-health-remove-console-logs-17949073…
SSoggyTacoMan May 6, 2026
e4603ce
Merge pull request #15 from SSoggyTacoMan/fix-code-health-remove-cons…
SSoggyTacoMan May 6, 2026
649fbe2
Merge pull request #11 from SSoggyTacoMan/remove-leftover-console-log…
SSoggyTacoMan May 6, 2026
da7fd39
Merge pull request #13 from SSoggyTacoMan/add-excerpt-utils-tests-637…
SSoggyTacoMan May 6, 2026
62f6d99
Merge pull request #21 from SSoggyTacoMan/performance-docx-array-map-…
SSoggyTacoMan May 6, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -26,4 +26,6 @@ dist-ssr
.wrangler
assets
*.env
*.zip
*.zip
temp
*.tmp
75 changes: 54 additions & 21 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,60 +1,80 @@
# [ShareClaude](https://shareclaude.pages.dev)

<div align="center">

Chrome Extension to share your [Claude](https://claude.ai) chats with one click.
Browser extension to share and export your [Claude](https://claude.ai) chats with one click.

[![Visit ShareClaude](https://img.shields.io/badge/Visit-ShareClaude-blue.svg?logo=data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiBzdHJva2U9IiNEOTc3NTciIHN0cm9rZS13aWR0aD0iMiIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiBzdHJva2UtbGluZWpvaW49InJvdW5kIiBjbGFzcz0ibHVjaWRlIGx1Y2lkZS1zaGFyZS0yIj48Y2lyY2xlIGN4PSIxOCIgY3k9IjUiIHI9IjMiLz48Y2lyY2xlIGN4PSI2IiBjeT0iMTIiIHI9IjMiLz48Y2lyY2xlIGN4PSIxOCIgY3k9IjE5IiByPSIzIi8+PGxpbmUgeDE9IjguNTkiIHgyPSIxNS40MiIgeTE9IjEzLjUxIiB5Mj0iMTcuNDkiLz48bGluZSB4MT0iMTUuNDEiIHgyPSI4LjU5IiB5MT0iNi41MSIgeTI9IjEwLjQ5Ii8+PC9zdmc+)](https://shareclaude.pages.dev)
[![Platform Chrome](https://img.shields.io/badge/Platform-Chrome-yellow?logo=google-chrome&logoColor=yellow)](https://chrome.google.com/webstore/detail/shareclaude/pcpjdbnjhgofgjgegodlnebdnmiddmaa)
[![Platform Firefox](https://img.shields.io/badge/Platform-Firefox-orange?logo=firefox-browser&logoColor=orange)](https://addons.mozilla.org/en-US/firefox/addon/shareclaude/)

## [Download from Chrome Web Store](https://chromewebstore.google.com/detail/shareclaude/pcpjdbnjhgofgjgegodlnebdnmiddmaa)
</div>

## [Download from Chrome Web Store](https://chromewebstore.google.com/detail/shareclaude/pcpjdbnjhgofgjgegodlnebdnmiddmaa) OR [Download from Firefox Add-ons Store](https://addons.mozilla.org/en-US/firefox/addon/shareclaude/)

## Features

- One-click sharing of Claude AI conversations
- Instant URL generation
- Support syntax highlighting for code and Artifacts including Mermaid & JSON
- Supports syntax highlighting for code and artifacts, including Mermaid and JSON
- Works directly with Claude's web interface

## How It Works
When you share a conversation, the extension stores the converastions to ShareClaude's database (not Claude). Each conversation gets a unique URL, similar to an unlisted YouTube video. The URL can be shared with anyone, but it won’t show up in search results on Google.
Further conversations are served from ShareClaude’s database, not directly from Claude.

When you share a conversation, the extension stores the conversation in ShareClaude's database, not Claude's. Each conversation gets a unique URL, similar to an unlisted YouTube video. The URL can be shared with anyone, but it won't show up in Google search results.
Shared conversations are served from ShareClaude's database, not directly from Claude.

*Important: While the URL is private and not searchable, anyone with the URL can still view the conversation. Please avoid sharing sensitive or personal information.*

## How to Use

1. Open [Claude](https://claude.ai) in your browser
2. Start or continue a conversation with Claude
3. Click the **ShareClaude** button in the top-right corner (next to Claude's native Share button, separated by a divider)
4. A menu appears with options:
- **Share to ShareClaude:** Uploads the conversation and copies the link to your clipboard as an alternative to Claude's native share option
- **Export:** Downloads the conversation as HTML, Markdown, plain text, JSON, or Word (.docx)

## Tech Stack

- **Frontend**: React, TailwindCSS
- **Backend**: Cloudflare Workers
- **Database**: Cloudflare D1

## Installation

### Chrome

Install from [Chrome Web Store](https://chromewebstore.google.com/detail/shareclaude/pcpjdbnjhgofgjgegodlnebdnmiddmaa) **(Recommended)**

**OR** for development/debugging:

1. Clone this repository:

```bash
git clone https://github.com/rohit1kumar/shareclaude.git
```

2. Open Chrome and navigate to `chrome://extensions/`
3. Enable "Developer mode" in the top right corner
4. Click "Load unpacked" and select the `extension` folder from the cloned repository

## How to Use
### Firefox

1. Open [Claude](https://claude.ai) in your browser
2. Start or continue a conversation with Claude
3. Click the ![share_button](https://github.com/user-attachments/assets/08baed07-07be-496d-aa40-c232e6022204) share icon in the input box adjacent to the attachments button.
4. The sharing URL will be automatically copied to your clipboard
5. Share the URL with anyone you want!
Install from [Firefox Add-ons Store](https://addons.mozilla.org/en-US/firefox/addon/shareclaude/) **(Recommended)**

## Tech Stack
**OR** for development/debugging:

- **Frontend**: React, TailwindCSS
- **Backend**: Cloudflare Workers
- **Database**: Cloudflare D1
1. Clone this repository:

## Known Issues
```bash
git clone https://github.com/rohit1kumar/shareclaude.git
```

- ~~Extension icon may not appear on the first load. Refreshing the page resolves this issue~~
2. Open Firefox and navigate to `about:debugging#/runtime/this-firefox`
3. Click "Load Temporary Add-on..."
4. Select the `manifest.json` file inside the `extension` folder from the cloned repository

## Contributing

Contributions are welcome! Feel free to:

- Report bugs
- Suggest new features
- Submit pull requests
Expand All @@ -64,4 +84,17 @@ Contributions are welcome! Feel free to:
- [Website](https://shareclaude.pages.dev)

---

## Star History

<!-- markdownlint-disable MD033 -->
<a href="https://www.star-history.com/?repos=rohit1kumar%2Fshareclaude&type=date&legend=top-left">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/image?repos=rohit1kumar/shareclaude&type=date&theme=dark&legend=top-left" />
<source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/image?repos=rohit1kumar/shareclaude&type=date&legend=top-left" />
<img alt="Star History Chart" src="https://api.star-history.com/image?repos=rohit1kumar/shareclaude&type=date&legend=top-left" />
</picture>
</a>
<!-- markdownlint-enable MD033 -->

Made with ☕ for the Claude community
4 changes: 2 additions & 2 deletions app/functions/api/[[catchall]].js
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
export async function onRequest(context) {
return Response.json({ message: "Opps! This route does not exist." }, { status: 404 });
export async function onRequest() {
return Response.json({ message: "Oops! This route does not exist." }, { status: 404 });
}
16 changes: 9 additions & 7 deletions app/functions/api/_middleware.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,9 @@
const corsHeaders = {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': '*',
'Access-Control-Allow-Headers': '*',
'Access-Control-Max-Age': '86400'
};
import { getCorsHeaders } from './utils';

export async function onRequest(context) {
const origin = context.request.headers.get('Origin');
const corsHeaders = getCorsHeaders(origin);

if (context.request.method === 'OPTIONS') {
return new Response(null, {
headers: corsHeaders,
Expand All @@ -14,7 +12,11 @@ export async function onRequest(context) {
}

const response = await context.next();
Object.entries(corsHeaders).forEach(([key, value]) => response.headers.set(key, value));
Object.entries(corsHeaders).forEach(([key, value]) => {
if (value) {
response.headers.set(key, value);
}
});

return response;
}
15 changes: 11 additions & 4 deletions app/functions/api/chats/[id].js
Original file line number Diff line number Diff line change
@@ -1,19 +1,26 @@
import { drizzle } from 'drizzle-orm/d1';
import { eq } from 'drizzle-orm';
import { chatsSchema } from '../../../database/schema';
import { getReadableError, getCorsHeaders } from '../utils';

export async function onRequestOptions(context) {
const origin = context.request.headers.get('Origin');
return new Response(null, { headers: getCorsHeaders(origin) });
}

export async function onRequestGet(context) {
const id = context.params.id;
const origin = context.request.headers.get('Origin');
const corsHeaders = getCorsHeaders(origin);
try {
const db = drizzle(context.env.DB);
const [chat] = await db.select().from(chatsSchema).where(eq(chatsSchema.id, id)).limit(1)
if (!chat) {
return Response.json({ msg: 'chat not found' }, { status: 404 });
return Response.json({ msg: 'chat not found' }, { status: 404, headers: corsHeaders });
}

return Response.json(chat);
return Response.json(chat, { headers: corsHeaders });
} catch (error) {
console.log("Error getting a chat: ", error)
return Response.json({ msg: "something went wrong!" }, { status: 500 });
return Response.json({ msg: getReadableError(error) }, { status: 500, headers: corsHeaders });
}
}
40 changes: 40 additions & 0 deletions app/functions/api/chats/[id]/raw.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { drizzle } from 'drizzle-orm/d1';
import { eq } from 'drizzle-orm';
import { chatsSchema } from '../../../../database/schema';
import { getReadableError, getCorsHeaders } from '../../utils';

export async function onRequestGet(context) {
const id = context.params.id;
const origin = context.request.headers.get('Origin');
const corsHeaders = getCorsHeaders(origin);
try {
const db = drizzle(context.env.DB);
const [chat] = await db.select().from(chatsSchema).where(eq(chatsSchema.id, id)).limit(1);
if (!chat) {
return new Response('Chat not found', {
status: 404,
headers: {
...corsHeaders,
'Content-Type': 'text/plain; charset=utf-8'
}
});
}

const messages = Array.isArray(chat.content) ? chat.content : [];
const lines = [`# ${chat.title}`, ''];

for (const { source, message } of messages) {
const role = source === 'user' ? 'You' : 'Claude';
lines.push(`## ${role}`, '', message ?? '', '', '---', '');
}

return new Response(lines.join('\n'), {
headers: {
...corsHeaders,
'Content-Type': 'text/plain; charset=utf-8',
},
});
} catch (error) {
return new Response(getReadableError(error), { status: 500, headers: { 'Content-Type': 'text/plain; charset=utf-8' } });
}
}
92 changes: 92 additions & 0 deletions app/functions/api/chats/__tests__/[id].test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import { onRequestGet } from '../[id].js';
import * as drizzleD1 from 'drizzle-orm/d1';

vi.mock('drizzle-orm/d1', () => ({
drizzle: vi.fn()
}));

describe('chats/[id].js onRequestGet', () => {
let mockContext;
let mockDb;
let consoleLogSpy;

beforeEach(() => {
vi.clearAllMocks();

mockDb = {
select: vi.fn().mockReturnThis(),
from: vi.fn().mockReturnThis(),
where: vi.fn().mockReturnThis(),
limit: vi.fn()
};

drizzleD1.drizzle.mockReturnValue(mockDb);

mockContext = {
params: { id: 'test-id' },
env: { DB: {} },
request: {
headers: new Headers([['Origin', 'https://claude.ai']])
}
};

vi.stubGlobal('Response', {
json: vi.fn().mockImplementation((data, init) => ({ data, init }))
});

consoleLogSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
});

afterEach(() => {
consoleLogSpy.mockRestore();
});

it('returns 200 with chat data when chat is found', async () => {
const mockChat = { id: 'test-id', title: 'Test Chat' };
mockDb.limit.mockResolvedValue([mockChat]);

const response = await onRequestGet(mockContext);

expect(global.Response.json).toHaveBeenCalledWith(
mockChat,
expect.objectContaining({
headers: expect.any(Object)
})
);
expect(response.data).toEqual(mockChat);
});

it('returns 404 when chat is not found', async () => {
mockDb.limit.mockResolvedValue([]); // returns an empty array

const response = await onRequestGet(mockContext);

expect(global.Response.json).toHaveBeenCalledWith(
{ msg: 'chat not found' },
expect.objectContaining({
status: 404,
headers: expect.any(Object)
})
);
expect(response.init.status).toBe(404);
});

it('returns 500 when database throws an error', async () => {
const error = new Error('Database error');
mockDb.limit.mockRejectedValue(error);

const response = await onRequestGet(mockContext);

expect(consoleLogSpy).toHaveBeenCalledWith("Error getting a chat: ", error);

expect(global.Response.json).toHaveBeenCalledWith(
{ msg: 'Something went wrong!' },
expect.objectContaining({
status: 500,
headers: expect.any(Object)
})
);
expect(response.init.status).toBe(500);
});
});
30 changes: 26 additions & 4 deletions app/functions/api/chats/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,37 @@ export async function onRequestPost(context) {
const db = drizzle(context.env.DB);
try {
const { title, content } = await context.request.json();
if (!title.trim()) {
return Response.json({ msg: 'title is required' }, { status: 400 });
if (!title || typeof title !== 'string' || !title.trim() || title.length > 512) {
return Response.json({ msg: 'valid title (max 512 chars) is required' }, { status: 400 });
}
const [newChat] = await db.insert(chatsSchema).values({ title, content }).returning()

if (!Array.isArray(content)) {
return Response.json({ msg: 'content must be an array' }, { status: 400 });
}

const sanitizedContent = [];
for (const item of content) {
if (typeof item !== 'object' || item === null || Array.isArray(item)) {
return Response.json({ msg: 'content items must be objects' }, { status: 400 });
}
if (typeof item.source !== 'string' || !['user', 'claude'].includes(item.source)) {
return Response.json({ msg: 'content item source must be "user" or "claude"' }, { status: 400 });
}
if (typeof item.message !== 'string') {
return Response.json({ msg: 'content item message must be a string' }, { status: 400 });
}
sanitizedContent.push({
source: item.source,
message: item.message
});
}

const [newChat] = await db.insert(chatsSchema).values({ title, content: sanitizedContent }).returning()
return Response.json({
id: newChat.id,
}, { status: 201 });
} catch (error) {
console.log("Error creating chat: ", error)
console.error("Error creating chat:", error);
return Response.json({ msg: "something went wrong!" }, { status: 500 });
}
}
Loading