diff --git a/README.md b/README.md index 87b15fb..25a3291 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ V1 scope: -- accepts only public `https://chatgpt.com/share/...` and `https://chat.openai.com/share/...` URLs +- accepts public ChatGPT links under the legacy `/share/...` path and the current `/s/t_...` path - admin-only capture/delete UI at `${AISHARE_BASE_PATH}/admin` - public reader pages generated as static artifacts under `/data/html/` - screenshots under `/data/screenshots/` diff --git a/client/src/main.ts b/client/src/main.ts index b214e16..fa5eb67 100644 --- a/client/src/main.ts +++ b/client/src/main.ts @@ -164,7 +164,7 @@ function renderShell(): void {
- + ${isAdmin ? '' : ""} diff --git a/src/capture.ts b/src/capture.ts index 93623b1..ecedc90 100644 --- a/src/capture.ts +++ b/src/capture.ts @@ -13,6 +13,7 @@ import { artifactAssetsDir, artifactIndexPath, safeAssetName, screenshotPath, sc import { slugify } from "./slug.js"; import { writeConversationArtifact, writeFailureArtifact } from "./render.js"; import { finalizeKatexAssets, prepareKatexAssets } from "./katex-assets.js"; +import { isChatGptSharePath } from "./url.js"; import type { CaptureRow, NormalizedConversation, NormalizedMessage, ScreenshotVariant } from "./types.js"; interface ExtractedConversation { @@ -420,10 +421,17 @@ async function extractConversation(sourceUrl: string): Promise candidate.url().includes("/share/")) || context.pages()[0]; + const isSharePage = (candidate: { url(): string }): boolean => { + try { + return isChatGptSharePath(new URL(candidate.url()).pathname); + } catch { + return false; + } + }; + let page = context.pages().find(isSharePage) || context.pages()[0]; for (let attempt = 0; !page && attempt < 100; attempt += 1) { await new Promise((resolve) => setTimeout(resolve, 100)); - page = context.pages().find((candidate) => candidate.url().includes("/share/")) || context.pages()[0]; + page = context.pages().find(isSharePage) || context.pages()[0]; } if (!page) throw new Error("Chromium CDP page target was not created."); await page.setViewportSize({ width: 1360, height: 900 }).catch(() => undefined); @@ -527,7 +535,10 @@ async function extractConversation(sourceUrl: string): Promise { + const source = "https://chatgpt.com/s/t_6a5f58aa36d08191b4d794e9aac88146"; + assert.equal(validateChatGptShareUrl(source), source); + assert.equal(isChatGptSharePath("/s/t_6a5f58aa36d08191b4d794e9aac88146"), true); +}); + +test("keeps accepting legacy ChatGPT share URLs", () => { + assert.equal( + validateChatGptShareUrl("https://chatgpt.com/share/12345678-abcd-4321-abcd-1234567890ab"), + "https://chatgpt.com/share/12345678-abcd-4321-abcd-1234567890ab" + ); + assert.equal( + validateChatGptShareUrl("https://chat.openai.com/share/12345678-abcd-4321-abcd-1234567890ab"), + "https://chat.openai.com/share/12345678-abcd-4321-abcd-1234567890ab" + ); +}); + +test("rejects non-share and deceptive ChatGPT paths", () => { + for (const source of [ + "https://chatgpt.com/s/example", + "https://chatgpt.com/sharex/example", + "https://chatgpt.com/share/example/extra", + "https://example.com/s/t_6a5f58aa36d08191b4d794e9aac88146" + ]) { + assert.throws(() => validateChatGptShareUrl(source), /Only public ChatGPT share URLs/); + } +});