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..126271a 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,32 @@ 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 = + process.env.INSFORGE_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`, { + // 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: '{}', + body: JSON.stringify({ slug }), + signal: AbortSignal.timeout(5000), }); if (!res.ok) { // Swallow โ€” best-effort counter ping.