From aeb300da7c719c695b320a2c165c39c610f0280c Mon Sep 17 00:00:00 2001 From: CarmenDou <15951653662@163.com> Date: Tue, 26 May 2026 20:53:04 -0700 Subject: [PATCH 1/3] refactor(cli): report marketplace download via TemplateMarket edge function --- src/commands/create.marketplace.test.ts | 28 ++++++++++++------------- src/commands/create.ts | 18 +++++++++------- 2 files changed, 25 insertions(+), 21 deletions(-) diff --git a/src/commands/create.marketplace.test.ts b/src/commands/create.marketplace.test.ts index a21dcab..1605b8a 100644 --- a/src/commands/create.marketplace.test.ts +++ b/src/commands/create.marketplace.test.ts @@ -19,39 +19,39 @@ describe('reportMarketplaceDownload', () => { vi.unstubAllGlobals(); }); - it('POSTs to /templates/v1//downloads on the given apiUrl', async () => { + it('POSTs slug body to TemplateMarket /functions/report-download', async () => { fetchMock.mockResolvedValue({ ok: true, status: 200, json: async () => ({ count: 1 }) }); - await reportMarketplaceDownload('chatbot', 'https://api.insforge.dev'); + await reportMarketplaceDownload('chatbot'); const [url, init] = fetchMock.mock.calls[0]; - expect(url).toBe('https://api.insforge.dev/templates/v1/chatbot/downloads'); - expect(init).toMatchObject({ method: 'POST' }); + expect(url).toBe('https://p8n7m7ci.us-east.insforge.app/functions/report-download'); + expect(init).toMatchObject({ + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ slug: 'chatbot' }), + }); }); - it('URL-encodes the slug', async () => { + it('sends the slug verbatim in the JSON body (no URL encoding hazard)', async () => { fetchMock.mockResolvedValue({ ok: true, status: 200, json: async () => ({ count: 1 }) }); - await reportMarketplaceDownload('weird slug', 'https://api.insforge.dev'); + await reportMarketplaceDownload('weird slug'); - const [url] = fetchMock.mock.calls[0]; - expect(url).toContain('weird%20slug'); + const [, init] = fetchMock.mock.calls[0]; + expect(init.body).toBe(JSON.stringify({ slug: 'weird slug' })); }); it('swallows network errors (does not throw)', async () => { fetchMock.mockRejectedValue(new Error('ECONNREFUSED')); - await expect( - reportMarketplaceDownload('chatbot', 'https://api.insforge.dev'), - ).resolves.toBeUndefined(); + await expect(reportMarketplaceDownload('chatbot')).resolves.toBeUndefined(); }); it('swallows non-2xx responses (does not throw)', async () => { fetchMock.mockResolvedValue({ ok: false, status: 503, json: async () => ({}) }); - await expect( - reportMarketplaceDownload('chatbot', 'https://api.insforge.dev'), - ).resolves.toBeUndefined(); + await expect(reportMarketplaceDownload('chatbot')).resolves.toBeUndefined(); }); }); diff --git a/src/commands/create.ts b/src/commands/create.ts index bbc30cd..f2a1947 100644 --- a/src/commands/create.ts +++ b/src/commands/create.ts @@ -382,10 +382,7 @@ export function registerCreateCommand(program: Command): void { json, ); if (downloaded) { - void reportMarketplaceDownload( - opts.marketplace as string, - apiUrl ?? 'https://api.insforge.dev', - ); + void reportMarketplaceDownload(opts.marketplace as string); } } else if (githubTemplates.includes(template!)) { await downloadGitHubTemplate(template!, projectConfig, json); @@ -737,18 +734,25 @@ export async function downloadGitHubTemplate( } } +// TemplateMarket is a single-tenant InsForge project hosted independently +// of any per-user backend, so the counter URL is a constant rather than +// derived from `apiUrl`. The function takes only {slug}; auth + RPC +// dispatch is handled inside the edge function (no anon key needed here). +const MARKETPLACE_REPORT_URL = + 'https://p8n7m7ci.us-east.insforge.app/functions/report-download'; + /** * Fire-and-forget POST to the marketplace download counter. * Network errors and non-2xx responses are swallowed — a transient * counter blip must not kill the install. The DB counter is the source * of truth; PostHog is intentionally not used (per spec §6.3). */ -export async function reportMarketplaceDownload(slug: string, apiUrl: string): Promise { +export async function reportMarketplaceDownload(slug: string): Promise { try { - const res = await fetch(`${apiUrl}/templates/v1/${encodeURIComponent(slug)}/downloads`, { + const res = await fetch(MARKETPLACE_REPORT_URL, { method: 'POST', headers: { 'Content-Type': 'application/json' }, - body: '{}', + body: JSON.stringify({ slug }), }); if (!res.ok) { // Swallow — best-effort counter ping. From 2d2ec3cac8d8d6dc2db186d9cb072639a9a775db Mon Sep 17 00:00:00 2001 From: CarmenDou <15951653662@163.com> Date: Tue, 26 May 2026 22:37:46 -0700 Subject: [PATCH 2/3] chore(cli): allow INSFORGE_MARKETPLACE_REPORT_URL env override --- src/commands/create.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/commands/create.ts b/src/commands/create.ts index f2a1947..0c79ecb 100644 --- a/src/commands/create.ts +++ b/src/commands/create.ts @@ -739,6 +739,7 @@ export async function downloadGitHubTemplate( // derived from `apiUrl`. The function takes only {slug}; auth + RPC // dispatch is handled inside the edge function (no anon key needed here). const MARKETPLACE_REPORT_URL = + process.env.INSFORGE_MARKETPLACE_REPORT_URL ?? 'https://p8n7m7ci.us-east.insforge.app/functions/report-download'; /** From 3c2a947ad53045cd94e30847e90e51183d4e555a Mon Sep 17 00:00:00 2001 From: CarmenDou <15951653662@163.com> Date: Tue, 26 May 2026 23:00:47 -0700 Subject: [PATCH 3/3] fix(cli): bound marketplace download fetch with 5s AbortSignal.timeout --- src/commands/create.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/commands/create.ts b/src/commands/create.ts index 0c79ecb..126271a 100644 --- a/src/commands/create.ts +++ b/src/commands/create.ts @@ -750,10 +750,16 @@ const MARKETPLACE_REPORT_URL = */ export async function reportMarketplaceDownload(slug: string): Promise { try { + // Bounded timeout: the call is `void`-awaited at the call site so the + // install command returns immediately, but an unresolved fetch keeps + // the Node event loop alive — without a signal the CLI process would + // hang for the OS socket timeout (minutes) whenever the marketplace + // counter endpoint is unreachable. const res = await fetch(MARKETPLACE_REPORT_URL, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ slug }), + signal: AbortSignal.timeout(5000), }); if (!res.ok) { // Swallow — best-effort counter ping.