diff --git a/.github/workflows/deploy-dev2.yml b/.github/workflows/deploy-dev2.yml index 3fef8d9..ad16cbf 100644 --- a/.github/workflows/deploy-dev2.yml +++ b/.github/workflows/deploy-dev2.yml @@ -53,7 +53,7 @@ jobs: - name: Verify the site answers run: | for i in $(seq 1 10); do - code=$(curl -s -o /dev/null -w '%{http_code}' "https://saasrow.com/" || true) + code=$(curl -s -o /dev/null -w '%{http_code}' "https://saasrow.pwa.pit.world/" || true) case "$code" in 2*|3*|401|403) echo "saasrow.com $code"; exit 0;; esac echo "attempt $i: $code"; sleep 10 done diff --git a/README.md b/README.md index 3b48508..6820f5c 100644 --- a/README.md +++ b/README.md @@ -62,7 +62,7 @@ you can create and manage free listings and API keys through the same API, the MCP write tools, or the CLI in `cli/`: ```bash -curl -fsSL https://saasrow.com/install.sh | sh # or: npm install -g @profullstack/saasrow +curl -fsSL https://saasrow.pwa.pit.world/install.sh | sh # or: npm install -g @profullstack/saasrow saasrow login ``` diff --git a/__tests__/functions/approvalNotify.test.ts b/__tests__/functions/approvalNotify.test.ts index 58b1307..47c4a1e 100644 --- a/__tests__/functions/approvalNotify.test.ts +++ b/__tests__/functions/approvalNotify.test.ts @@ -36,11 +36,11 @@ describe('network promo', () => { }) describe('approvalEmail', () => { - const email = approvalEmail('Acme [CRM](https://evil.test)', 'https://saasrow.com/software/abc', 'https://saasrow.com') + const email = approvalEmail('Acme [CRM](https://evil.test)', 'https://saasrow.pwa.pit.world/software/abc', 'https://saasrow.pwa.pit.world') it('names the listing without letting the title become a link', () => { expect(email.html).not.toContain('href="https://evil.test"') - expect(email.html).toContain('href="https://saasrow.com/software/abc"') + expect(email.html).toContain('href="https://saasrow.pwa.pit.world/software/abc"') }) it('carries the promo in both parts', () => { @@ -56,7 +56,7 @@ describe('notifyListingApproved', () => { it('sends one email per contact through the Resend batch API', async () => { const fetchMock = vi.fn().mockResolvedValue({ ok: true }) vi.stubGlobal('fetch', fetchMock) - vi.stubGlobal('Deno', { env: { get: (k: string) => ({ RESEND_API_KEY: 're_x', SITE_URL: 'https://saasrow.com/' } as any)[k] } }) + vi.stubGlobal('Deno', { env: { get: (k: string) => ({ RESEND_API_KEY: 're_x', SITE_URL: 'https://saasrow.pwa.pit.world/' } as any)[k] } }) await notifyListingApproved(fakeDb('Acme', ['a@acme.io', 'b@example.com', 'c@acme.io']), 'abc') @@ -66,7 +66,7 @@ describe('notifyListingApproved', () => { const batch = JSON.parse(init.body) expect(batch.map((m: any) => m.to)).toEqual(['a@acme.io', 'c@acme.io']) expect(batch[0].subject).toBe('Acme is live on SaaSRow') - expect(batch[0].html).toContain('href="https://saasrow.com/software/abc"') + expect(batch[0].html).toContain('href="https://saasrow.pwa.pit.world/software/abc"') }) it('does nothing without a Resend key', async () => { diff --git a/__tests__/functions/coinpay.test.ts b/__tests__/functions/coinpay.test.ts index 0d8a15e..ce76039 100644 --- a/__tests__/functions/coinpay.test.ts +++ b/__tests__/functions/coinpay.test.ts @@ -113,8 +113,8 @@ describe('buildSessionRequest', () => { mode: 'subscription', interval: 'month', customer_email: ' Riotcoder+saasrow-cs-test@profullstack.com ', - success_url: 'https://saasrow.com/featured?success=true&session={CHECKOUT_SESSION_ID}', - cancel_url: 'https://saasrow.com/featured?cancelled=true', + success_url: 'https://saasrow.pwa.pit.world/featured?success=true&session={CHECKOUT_SESSION_ID}', + cancel_url: 'https://saasrow.pwa.pit.world/featured?cancelled=true', } it('prices on the server and sends the CoinPay contract fields', () => { diff --git a/__tests__/functions/managementAccess.test.ts b/__tests__/functions/managementAccess.test.ts index c7aef6b..a54c2f3 100644 --- a/__tests__/functions/managementAccess.test.ts +++ b/__tests__/functions/managementAccess.test.ts @@ -176,6 +176,6 @@ describe('listingIdsForEmail', () => { describe('managementUrlFor', () => { it('keeps a base64 user token in one path segment', () => { - expect(managementUrlFor('https://saasrow.com/', 'Jm8g/e0V+A=')).toBe('https://saasrow.com/manage/Jm8g%2Fe0V%2BA%3D') + expect(managementUrlFor('https://saasrow.pwa.pit.world/', 'Jm8g/e0V+A=')).toBe('https://saasrow.pwa.pit.world/manage/Jm8g%2Fe0V%2BA%3D') }) }) diff --git a/__tests__/lib/crawlGateway.test.ts b/__tests__/lib/crawlGateway.test.ts index fcb5384..965ed94 100644 --- a/__tests__/lib/crawlGateway.test.ts +++ b/__tests__/lib/crawlGateway.test.ts @@ -6,7 +6,7 @@ const CHROME = 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.0.0 Safari/537.36' function req(method: string, ip: string, path = SUBMIT_PATH) { - return new Request(`https://saasrow.com${path}`, { + return new Request(`https://saasrow.pwa.pit.world${path}`, { method, headers: { 'user-agent': CHROME, diff --git a/__tests__/lib/credits.test.ts b/__tests__/lib/credits.test.ts index 521fed2..9b40a8c 100644 --- a/__tests__/lib/credits.test.ts +++ b/__tests__/lib/credits.test.ts @@ -108,7 +108,7 @@ describe('settleBonus', () => { describe('acceptInvite', () => { it('refuses an established account before touching the invite', async () => { - const req = new Request('https://saasrow.com/api/v1/me/invites/accept') + const req = new Request('https://saasrow.pwa.pit.world/api/v1/me/invites/accept') const r = await acceptInvite({ userId: 'u-alice', email: 'alice@example.com' }, 'INV-AAAA-BBBB-CCCC', req) expect(r.accepted).toBe(false) expect(rpc).not.toHaveBeenCalled() @@ -117,7 +117,7 @@ describe('acceptInvite', () => { rpc.mockImplementation(async (fn: string) => fn === 'aff_invited_by' ? { data: 'u-alice', error: null } : { data: null, error: null }, ) - const req = new Request('https://saasrow.com/api/v1/me/invites/accept') + const req = new Request('https://saasrow.pwa.pit.world/api/v1/me/invites/accept') const r = await acceptInvite({ userId: 'u-carol', email: 'carol@example.org' }, 'INV-AAAA-BBBB-CCCC', req) expect(r).toEqual({ accepted: false, message: 'You have already joined on an invite.' }) expect(rpc.mock.calls.map((c) => c[0])).not.toContain('aff_claim_invite') @@ -132,14 +132,14 @@ describe('acceptInvite', () => { return { data: null, error: null } }) tables.invite_bonuses = [] - const req = new Request('https://saasrow.com/api/v1/me/invites/accept') + const req = new Request('https://saasrow.pwa.pit.world/api/v1/me/invites/accept') const r = await acceptInvite({ userId: 'u-carol', email: 'carol@example.org' }, 'INV-AAAA-BBBB-CCCC', req) expect(r).toEqual({ accepted: false, message: 'You have already joined on an invite.' }) expect(rpc.mock.calls.map((c) => c[0])).toContain('aff_claim_invite') expect(rpc.mock.calls.map((c) => c[0])).not.toContain('settle_invite_bonus') }) it('refuses something that is not an invite code', async () => { - const req = new Request('https://saasrow.com/') + const req = new Request('https://saasrow.pwa.pit.world/') expect((await acceptInvite({ userId: 'u-carol', email: 'c' }, 'CR-AAAA-BBBB-CCCC-DDDD', req)).accepted).toBe(false) }) }) diff --git a/__tests__/lib/emailMarkdown.test.ts b/__tests__/lib/emailMarkdown.test.ts index 7472570..9296f94 100644 --- a/__tests__/lib/emailMarkdown.test.ts +++ b/__tests__/lib/emailMarkdown.test.ts @@ -41,10 +41,10 @@ describe('markdownToEmailHtml', () => { }) it('renders markdown links and bare URLs', () => { - const html = markdownToEmailHtml('See [SaaSRow](https://saasrow.com) or https://saasrow.com/pricing') - expect(html).toContain('SaaSRow') - expect(html).toContain(' { @@ -82,8 +82,8 @@ describe('markdownToEmailHtml', () => { }) it('renders images', () => { - const html = markdownToEmailHtml('![logo](https://saasrow.com/logo.png)') - expect(html).toContain('logo { @@ -114,9 +114,9 @@ describe('markdownToEmailHtml', () => { describe('markdownToPlainText', () => { it('strips markdown syntax but keeps the text readable', () => { - const text = markdownToPlainText('# Title\n\n**Bold** and [a link](https://saasrow.com)\n\n- one\n- two') + const text = markdownToPlainText('# Title\n\n**Bold** and [a link](https://saasrow.pwa.pit.world)\n\n- one\n- two') expect(text).toContain('Title') - expect(text).toContain('Bold and a link (https://saasrow.com)') + expect(text).toContain('Bold and a link (https://saasrow.pwa.pit.world)') expect(text).toContain('- one') expect(text).not.toContain('**') expect(text).not.toContain('#') @@ -130,16 +130,16 @@ describe('markdownToPlainText', () => { describe('buildEmailParts', () => { const footer = [ { text: 'You subscribed.' }, - { text: 'Unsubscribe', href: 'https://saasrow.com/unsubscribe?email=a%40b.co' }, + { text: 'Unsubscribe', href: 'https://saasrow.pwa.pit.world/unsubscribe?email=a%40b.co' }, ] it('builds an HTML document and a matching text/plain part from one markdown source', () => { - const { html, text } = buildEmailParts({ markdown: '## Hi\n\nSee [the site](https://saasrow.com).', heading: 'SaaSRow', footer }) + const { html, text } = buildEmailParts({ markdown: '## Hi\n\nSee [the site](https://saasrow.pwa.pit.world).', heading: 'SaaSRow', footer }) expect(html.startsWith('')).toBe(true) expect(html).toContain('>Hi') - expect(html).toContain('href="https://saasrow.com/unsubscribe?email=a%40b.co"') - expect(text).toContain('Hi\n\nSee the site (https://saasrow.com).') - expect(text).toContain('\n--\nYou subscribed.\nUnsubscribe: https://saasrow.com/unsubscribe?email=a%40b.co\n') + expect(html).toContain('href="https://saasrow.pwa.pit.world/unsubscribe?email=a%40b.co"') + expect(text).toContain('Hi\n\nSee the site (https://saasrow.pwa.pit.world).') + expect(text).toContain('\n--\nYou subscribed.\nUnsubscribe: https://saasrow.pwa.pit.world/unsubscribe?email=a%40b.co\n') expect(text).not.toContain('<') }) diff --git a/__tests__/lib/inviteRules.test.ts b/__tests__/lib/inviteRules.test.ts index feba29d..5bed83e 100644 --- a/__tests__/lib/inviteRules.test.ts +++ b/__tests__/lib/inviteRules.test.ts @@ -78,13 +78,13 @@ describe('helpers', () => { expect(looksLikeInviteCode(42)).toBe(false) }) it('siteOriginFrom uses the request host, never a foreign one', () => { - expect(siteOriginFrom(new Headers({ host: 'saasrow.com' }))).toBe('https://saasrow.com') - expect(siteOriginFrom(new Headers({ host: 'www.saasrow.com' }))).toBe('https://saasrow.com') + expect(siteOriginFrom(new Headers({ host: 'saasrow.pwa.pit.world' }))).toBe('https://saasrow.pwa.pit.world') + expect(siteOriginFrom(new Headers({ host: 'www.saasrow.com' }))).toBe('https://saasrow.pwa.pit.world') expect(siteOriginFrom(new Headers({ host: 'localhost:3000' }))).toBe('http://localhost:3000') - expect(siteOriginFrom(new Headers({ 'x-forwarded-host': 'evil.example', host: 'saasrow.com' }))).toBe('https://saasrow.com') - expect(siteOriginFrom(new Headers({ 'x-forwarded-host': 'saasrow.com', 'x-forwarded-proto': 'https', host: '127.0.0.1:3000' }))).toBe('https://saasrow.com') + expect(siteOriginFrom(new Headers({ 'x-forwarded-host': 'evil.example', host: 'saasrow.pwa.pit.world' }))).toBe('https://saasrow.pwa.pit.world') + expect(siteOriginFrom(new Headers({ 'x-forwarded-host': 'saasrow.pwa.pit.world', 'x-forwarded-proto': 'https', host: '127.0.0.1:3000' }))).toBe('https://saasrow.pwa.pit.world') }) it('inviteUrl', () => { - expect(inviteUrl('https://saasrow.com', 'INV-AAAA-BBBB-CCCC')).toBe('https://saasrow.com/i/INV-AAAA-BBBB-CCCC') + expect(inviteUrl('https://saasrow.pwa.pit.world', 'INV-AAAA-BBBB-CCCC')).toBe('https://saasrow.pwa.pit.world/i/INV-AAAA-BBBB-CCCC') }) }) diff --git a/__tests__/lib/memberSession.test.ts b/__tests__/lib/memberSession.test.ts index a19b2e1..d75a57d 100644 --- a/__tests__/lib/memberSession.test.ts +++ b/__tests__/lib/memberSession.test.ts @@ -1,7 +1,7 @@ import { describe, expect, it } from 'vitest' import { clearMemberCookie, readMemberCookie, setMemberCookie } from '@/lib/memberSession' -const req = (cookie?: string) => new Request('https://saasrow.com/', { headers: cookie ? { cookie } : {} }) +const req = (cookie?: string) => new Request('https://saasrow.pwa.pit.world/', { headers: cookie ? { cookie } : {} }) const valueOf = (setCookie: string) => setCookie.split(';')[0] describe('member session cookie', () => { diff --git a/__tests__/lib/submissionGuard.test.ts b/__tests__/lib/submissionGuard.test.ts index 5aece40..57e7779 100644 --- a/__tests__/lib/submissionGuard.test.ts +++ b/__tests__/lib/submissionGuard.test.ts @@ -3,7 +3,7 @@ import { describe, expect, it } from 'vitest' import { screenSubmission } from '@/lib/submission-guard' const post = (ip: string, d: string, path = '/api/fn/submissions') => - new Request(`https://saasrow.com${path}`, { + new Request(`https://saasrow.pwa.pit.world${path}`, { method: 'POST', headers: { 'x-real-ip': ip, 'content-type': 'application/json' }, body: JSON.stringify({ title: d, url: `https://${d}.com`, email: `hello@${d}.com`, description: 'x', category: 'Software' }), @@ -24,12 +24,12 @@ describe('screenSubmission', () => { }) it('leaves reads and other routes alone', async () => { - expect(await screenSubmission(new Request('https://saasrow.com/api/fn/submissions'))).toBeNull() + expect(await screenSubmission(new Request('https://saasrow.pwa.pit.world/api/fn/submissions'))).toBeNull() expect(await screenSubmission(post('192.0.2.67', 'x', '/api/fn/vote'))).toBeNull() }) it('passes a malformed body through to the edge function', async () => { - const r = new Request('https://saasrow.com/api/fn/submissions', { method: 'POST', body: 'not json' }) + const r = new Request('https://saasrow.pwa.pit.world/api/fn/submissions', { method: 'POST', body: 'not json' }) expect(await screenSubmission(r)).toBeNull() }) }) diff --git a/__tests__/lib/webhooks/api.test.ts b/__tests__/lib/webhooks/api.test.ts index ea86833..774ee83 100644 --- a/__tests__/lib/webhooks/api.test.ts +++ b/__tests__/lib/webhooks/api.test.ts @@ -19,7 +19,7 @@ vi.mock('@/lib/webhooks/hub', async (orig) => ({ ...(await orig()), getH const { GET, POST, DELETE } = await import('../../../app/api/v1/webhooks/[[...path]]/route') -const req = (path: string, init: RequestInit = {}) => new Request(`https://saasrow.com/api/v1/webhooks${path}`, init) +const req = (path: string, init: RequestInit = {}) => new Request(`https://saasrow.pwa.pit.world/api/v1/webhooks${path}`, init) describe('/api/v1/webhooks', () => { beforeEach(() => { diff --git a/app/layout.tsx b/app/layout.tsx index fe74ab4..2ed5e88 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -8,7 +8,7 @@ import JsonLd from '@/components/JsonLd' import { websiteLd, organizationLd } from '@/lib/structuredData' import './globals.css' -const SITE_URL = 'https://saasrow.com' +const SITE_URL = 'https://saasrow.pwa.pit.world' // Re-render hourly so @profullstack/footer and the pit.world footer stay fresh. export const revalidate = 3600 @@ -60,7 +60,7 @@ export default function RootLayout({ children }: { children: React.ReactNode }) diff --git a/cli/README.md b/cli/README.md index 5cdc2b3..2f8e370 100644 --- a/cli/README.md +++ b/cli/README.md @@ -5,7 +5,7 @@ terminal. Search the directory without an account; create and manage your own free listings and API keys with one. ```bash -curl -fsSL https://saasrow.com/install.sh | sh +curl -fsSL https://saasrow.pwa.pit.world/install.sh | sh # or with npm npm install -g @profullstack/saasrow # or run it without installing diff --git a/cli/test/cli.test.mjs b/cli/test/cli.test.mjs index cfcb2bd..11b507e 100644 --- a/cli/test/cli.test.mjs +++ b/cli/test/cli.test.mjs @@ -282,11 +282,11 @@ test('redeem shows the refusal and exits non-zero', async () => { test('invites create prints the link once; invites lists them', async () => { const h = harness({ - 'POST /api/v1/me/invites': (c) => [201, { id: 'i1', code: 'INV-AAAA-BBBB-CCCC', url: 'https://saasrow.com/i/INV-AAAA-BBBB-CCCC', expires_at: '2026-11-25T00:00:00Z', quota: { total: 3, used: 1, left: 2 }, note: c.body.note }], + 'POST /api/v1/me/invites': (c) => [201, { id: 'i1', code: 'INV-AAAA-BBBB-CCCC', url: 'https://saasrow.pwa.pit.world/i/INV-AAAA-BBBB-CCCC', expires_at: '2026-11-25T00:00:00Z', quota: { total: 3, used: 1, left: 2 }, note: c.body.note }], 'GET /api/v1/me/invites': () => [200, { quota: { total: 3, used: 1, left: 2 }, can_send: true, can_send_reason: null, bonus_days: 7, invites: [{ id: 'i1', note: 'for Carol', status: 'unused', bonus: null, created_at: '2026-10-26T00:00:00Z', expires_at: null }], - affiliate_link: 'https://saasrow.com/?ref=ABCD1234', + affiliate_link: 'https://saasrow.pwa.pit.world/?ref=ABCD1234', }], }, { env: { SAASROW_API_KEY: 'sr_env' } }) try { diff --git a/public/.well-known/openaccess.json b/public/.well-known/openaccess.json index 9d4bf43..c16994f 100644 --- a/public/.well-known/openaccess.json +++ b/public/.well-known/openaccess.json @@ -1,10 +1,10 @@ { "openaccess": "0.1", "name": "saasrow", - "url": "https://saasrow.com", + "url": "https://saasrow.pwa.pit.world", "operator": "https://logicsrc.com/.well-known/openprofile.md", "redirect_uris": [ - "https://saasrow.com/api/v1/openaccess/callback" + "https://saasrow.pwa.pit.world/api/v1/openaccess/callback" ], "jwks": { "keys": [ @@ -22,7 +22,7 @@ "honours": [ "profullstack.com/all-access" ], - "webhooks": "https://saasrow.com/api/v1/openaccess/events", + "webhooks": "https://saasrow.pwa.pit.world/api/v1/openaccess/events", "hubs": [ "https://openaccess.logicsrc.com" ] diff --git a/public/.well-known/openmcp.json b/public/.well-known/openmcp.json index 47f4906..c725687 100644 --- a/public/.well-known/openmcp.json +++ b/public/.well-known/openmcp.json @@ -1,9 +1,9 @@ { "openmcp": "0.1", - "mcp": "https://saasrow.com/api/mcp", + "mcp": "https://saasrow.pwa.pit.world/api/mcp", "name": "saasrow", "description": "A directory of SaaS products, searchable by what they do.", - "url": "https://saasrow.com", + "url": "https://saasrow.pwa.pit.world", "auth": { "kind": "none" }, diff --git a/public/.well-known/openwebring.json b/public/.well-known/openwebring.json index ad161c9..dedd2a9 100644 --- a/public/.well-known/openwebring.json +++ b/public/.well-known/openwebring.json @@ -1,6 +1,6 @@ { "openwebring": "0.1", - "site": { "url": "https://saasrow.com/", "name": "SaaSRow" }, + "site": { "url": "https://saasrow.pwa.pit.world/", "name": "SaaSRow" }, "made_by": "both", "rings": [{ "ring": "https://rssamplifier.com/ring/profullstack", "slug": "saasrow-blog" }] } diff --git a/public/install.sh b/public/install.sh index ac37846..ba4ade7 100755 --- a/public/install.sh +++ b/public/install.sh @@ -1,7 +1,7 @@ #!/bin/sh # Install saasrow, the saasrow.com CLI (npm: @profullstack/saasrow). # -# curl -fsSL https://saasrow.com/install.sh | sh +# curl -fsSL https://saasrow.pwa.pit.world/install.sh | sh # # Uses bun if present, otherwise npm (Node >= 20.11). When the global npm # prefix is not writable it installs into ~/.local instead of asking for sudo. diff --git a/src/lib/broadcastDraft.ts b/src/lib/broadcastDraft.ts index 6fdadb7..61c6ef4 100644 --- a/src/lib/broadcastDraft.ts +++ b/src/lib/broadcastDraft.ts @@ -31,7 +31,7 @@ subject: 35-70 characters, plain text, no emojis, no clickbait, no trailing peri content: the email body in Markdown. Allowed syntax: ## headings, **bold**, *italic*, [links](https://...), - bullets, 1. numbered lists, > quotes, --- rules. No HTML, no images, no tables, no code fences. - Open with the point, not a greeting paragraph. A short "Hi there," line is fine. - Keep it to what the description asks for; never invent numbers, dates, prices, features or quotes. If a detail is needed but missing, leave a clear [placeholder] in square brackets. -- One clear call to action, linked, when the description implies one. Use https://saasrow.com links unless the description gives others. +- One clear call to action, linked, when the description implies one. Use https://saasrow.pwa.pit.world links unless the description gives others. - 80-250 words unless the description asks for more. - Sign off as "The SaaSRow team". - Never use em dashes or en dashes; use commas or hyphens. No emojis. diff --git a/src/lib/crawl-gateway.ts b/src/lib/crawl-gateway.ts index 79300b1..ba2ba6b 100644 --- a/src/lib/crawl-gateway.ts +++ b/src/lib/crawl-gateway.ts @@ -35,7 +35,7 @@ export function submitQuotaKey(request: Request): string | null { } export const gateway = createGateway({ - siteUrl: env("SITE_URL") || env("NEXT_PUBLIC_SITE_URL") || "https://saasrow.com", + siteUrl: env("SITE_URL") || env("NEXT_PUBLIC_SITE_URL") || "https://saasrow.pwa.pit.world", siteName: "saasrow", coinpay: { apiKey: env("COINPAY_X402_KEY") }, payTo: env("CRAWL_PAY_TO"), diff --git a/src/lib/inviteRules.ts b/src/lib/inviteRules.ts index 69bdb45..8bc275f 100644 --- a/src/lib/inviteRules.ts +++ b/src/lib/inviteRules.ts @@ -139,10 +139,10 @@ const ALLOWED_HOSTS = /^(?:www\.)?saasrow\.com$|^localhost(?::\d+)?$|^127\.0\.0\ * dev or preview host builds links to itself), falling back to the canonical * apex for anything we do not recognise. */ -export function siteOriginFrom(headers: Headers, fallback = 'https://saasrow.com'): string { +export function siteOriginFrom(headers: Headers, fallback = 'https://saasrow.pwa.pit.world'): string { const host = (headers.get('x-forwarded-host') ?? headers.get('host') ?? '').split(',')[0].trim().toLowerCase() if (!host || !ALLOWED_HOSTS.test(host)) return fallback - if (host === 'www.saasrow.com') return 'https://saasrow.com' + if (host === 'www.saasrow.com') return 'https://saasrow.pwa.pit.world' const local = host.startsWith('localhost') || host.startsWith('127.0.0.1') const proto = (headers.get('x-forwarded-proto') ?? (local ? 'http' : 'https')).split(',')[0].trim() return `${proto === 'http' ? 'http' : 'https'}://${host}` diff --git a/src/lib/mcpServer.ts b/src/lib/mcpServer.ts index ab33723..992d2a8 100644 --- a/src/lib/mcpServer.ts +++ b/src/lib/mcpServer.ts @@ -37,7 +37,7 @@ import { siteOriginFrom } from './inviteRules' import { WebhookError } from '@profullstack/webhooks' import { getHooks, WEBHOOK_EVENT_HELP, WEBHOOK_EVENT_TYPES } from './webhooks/hub' -const SITE_ORIGIN = 'https://saasrow.com' +const SITE_ORIGIN = 'https://saasrow.pwa.pit.world' const mcpOrigin = (ctx: McpContext) => (ctx.request ? siteOriginFrom(ctx.request.headers) : SITE_ORIGIN) export const SUPPORTED_PROTOCOL_VERSIONS = ['2025-06-18', '2025-03-26', '2024-11-05'] diff --git a/src/lib/webhooks/hub.ts b/src/lib/webhooks/hub.ts index b81a106..05be848 100644 --- a/src/lib/webhooks/hub.ts +++ b/src/lib/webhooks/hub.ts @@ -21,7 +21,7 @@ import { WEBHOOK_EVENT_TYPES } from './events' export { WEBHOOK_EVENT_TYPES, WEBHOOK_EVENT_HELP, type WebhookEventType } from './events' -export const WEBHOOK_SOURCE = 'https://saasrow.com' +export const WEBHOOK_SOURCE = 'https://saasrow.pwa.pit.world' export function createHooks(store: WebhookStore, options: Partial = {}): Webhooks { return new Webhooks({ diff --git a/src/views/EmailBroadcast.tsx b/src/views/EmailBroadcast.tsx index aa8cae0..9993cb0 100644 --- a/src/views/EmailBroadcast.tsx +++ b/src/views/EmailBroadcast.tsx @@ -39,7 +39,7 @@ const AUDIENCE_OPTIONS: AudienceOption[] = [ /** Mirrors the footer send-email-broadcast appends, for the plain-text preview. */ const BROADCAST_FOOTER = [ { text: "You're receiving this email because you have an account on SaaSRow." }, - { text: 'Visit SaaSRow', href: 'https://saasrow.com' }, + { text: 'Visit SaaSRow', href: 'https://saasrow.pwa.pit.world' }, { text: `Ā© ${new Date().getFullYear()} Profullstack, Inc. All rights reserved.` }, ] @@ -355,7 +355,7 @@ export default function EmailBroadcast() { id="content" value={content} onChange={(e) => setContent(e.target.value)} - placeholder={'Paste or write markdown — plain text works too.\n\n# Heading\n\nSome **bold** text with a [link](https://saasrow.com).\n\n- bullet one\n- bullet two'} + placeholder={'Paste or write markdown — plain text works too.\n\n# Heading\n\nSome **bold** text with a [link](https://saasrow.pwa.pit.world).\n\n- bullet one\n- bullet two'} required disabled={sending || drafting} rows={14} diff --git a/supabase/functions/_shared/approvalNotify.ts b/supabase/functions/_shared/approvalNotify.ts index 49ff4b9..5d4b684 100644 --- a/supabase/functions/_shared/approvalNotify.ts +++ b/supabase/functions/_shared/approvalNotify.ts @@ -46,7 +46,7 @@ export function approvalEmail(title: string, listingUrl: string, siteUrl: string export async function notifyListingApproved(supabase: Db, submissionId: string): Promise { const apiKey = Deno.env.get('RESEND_API_KEY') if (!apiKey) return - const siteUrl = (Deno.env.get('SITE_URL') || 'https://saasrow.com').replace(/\/$/, '') + const siteUrl = (Deno.env.get('SITE_URL') || 'https://saasrow.pwa.pit.world').replace(/\/$/, '') try { const [{ data: submission }, { data: contacts }] = await Promise.all([ diff --git a/supabase/functions/_shared/broadcastDraft.ts b/supabase/functions/_shared/broadcastDraft.ts index 6fdadb7..61c6ef4 100644 --- a/supabase/functions/_shared/broadcastDraft.ts +++ b/supabase/functions/_shared/broadcastDraft.ts @@ -31,7 +31,7 @@ subject: 35-70 characters, plain text, no emojis, no clickbait, no trailing peri content: the email body in Markdown. Allowed syntax: ## headings, **bold**, *italic*, [links](https://...), - bullets, 1. numbered lists, > quotes, --- rules. No HTML, no images, no tables, no code fences. - Open with the point, not a greeting paragraph. A short "Hi there," line is fine. - Keep it to what the description asks for; never invent numbers, dates, prices, features or quotes. If a detail is needed but missing, leave a clear [placeholder] in square brackets. -- One clear call to action, linked, when the description implies one. Use https://saasrow.com links unless the description gives others. +- One clear call to action, linked, when the description implies one. Use https://saasrow.pwa.pit.world links unless the description gives others. - 80-250 words unless the description asks for more. - Sign off as "The SaaSRow team". - Never use em dashes or en dashes; use commas or hyphens. No emojis. diff --git a/supabase/functions/_shared/coinpay.ts b/supabase/functions/_shared/coinpay.ts index 270c086..5b34e01 100644 --- a/supabase/functions/_shared/coinpay.ts +++ b/supabase/functions/_shared/coinpay.ts @@ -41,7 +41,7 @@ export type SessionRequest = { expires_in_minutes: number } -const ALLOWED_REDIRECT_HOSTS = new Set(['saasrow.com', 'www.saasrow.com', 'localhost']) +const ALLOWED_REDIRECT_HOSTS = new Set(['saasrow.pwa.pit.world', 'www.saasrow.com', 'localhost']) function safeRedirect(raw: unknown, siteUrl: string): string | null { if (typeof raw !== 'string' || !raw) return null diff --git a/supabase/functions/_shared/commentNotify.ts b/supabase/functions/_shared/commentNotify.ts index 0ce8a5f..a69db0b 100644 --- a/supabase/functions/_shared/commentNotify.ts +++ b/supabase/functions/_shared/commentNotify.ts @@ -60,7 +60,7 @@ export async function commentRecipients(supabase: Db, comment: CommentRow): Prom export async function notifyCommentPublished(supabase: Db, commentIds: string[]): Promise { const apiKey = Deno.env.get('RESEND_API_KEY') if (!apiKey || commentIds.length === 0) return - const siteUrl = (Deno.env.get('SITE_URL') || 'https://saasrow.com').replace(/\/$/, '') + const siteUrl = (Deno.env.get('SITE_URL') || 'https://saasrow.pwa.pit.world').replace(/\/$/, '') try { const { data: comments, error } = await supabase diff --git a/supabase/functions/cleanup-expired-listings/index.ts b/supabase/functions/cleanup-expired-listings/index.ts index e11d1af..cf54bdf 100644 --- a/supabase/functions/cleanup-expired-listings/index.ts +++ b/supabase/functions/cleanup-expired-listings/index.ts @@ -18,8 +18,8 @@ Deno.serve(async (req: Request) => { const supabaseUrl = Deno.env.get('SUPABASE_URL')! const supabaseServiceKey = Deno.env.get('SUPABASE_SERVICE_ROLE_KEY')! const resendApiKey = Deno.env.get('RESEND_API_KEY') - const fromDomain = Deno.env.get('RESEND_DOMAIN') || 'saasrow.com' - const siteUrl = Deno.env.get('SITE_URL') || 'https://saasrow.com' + const fromDomain = Deno.env.get('RESEND_DOMAIN') || 'saasrow.pwa.pit.world' + const siteUrl = Deno.env.get('SITE_URL') || 'https://saasrow.pwa.pit.world' const supabase = createClient(supabaseUrl, supabaseServiceKey) diff --git a/supabase/functions/coinpay-checkout/index.ts b/supabase/functions/coinpay-checkout/index.ts index 150582a..63b5831 100644 --- a/supabase/functions/coinpay-checkout/index.ts +++ b/supabase/functions/coinpay-checkout/index.ts @@ -62,7 +62,7 @@ Deno.serve(async (req) => { const input = await req.json().catch(() => ({})); const built = buildSessionRequest(input, { accountId: await signedInUserId(req), - siteUrl: Deno.env.get('SITE_URL') || 'https://saasrow.com', + siteUrl: Deno.env.get('SITE_URL') || 'https://saasrow.pwa.pit.world', }); if (!built.ok) return json({ error: built.error }, 400); diff --git a/supabase/functions/newsletter/index.ts b/supabase/functions/newsletter/index.ts index 528e467..75d076f 100644 --- a/supabase/functions/newsletter/index.ts +++ b/supabase/functions/newsletter/index.ts @@ -140,7 +140,7 @@ Deno.serve(async (req: Request) => { if (resendApiKey) { const siteUrl = Deno.env.get('SITE_URL') || 'http://localhost:3000' - const emailHtml = `Welcome to SaaSRow Newsletter!

Welcome to Our Newsletter! šŸŽ‰

šŸ‘‹ Thanks for subscribing!

You're now part of our community and will receive weekly updates about the latest self-hosted software, news, and exclusive content every Friday.

šŸŽ Here's Your Exclusive Discount Code

50OFF

Get 50% off Featured or Premium listings!

Use this code when submitting your software to SaaSRow to get 50% off any Featured or Premium tier.

` + const emailHtml = `Welcome to SaaSRow Newsletter!

Welcome to Our Newsletter! šŸŽ‰

šŸ‘‹ Thanks for subscribing!

You're now part of our community and will receive weekly updates about the latest self-hosted software, news, and exclusive content every Friday.

šŸŽ Here's Your Exclusive Discount Code

50OFF

Get 50% off Featured or Premium listings!

Use this code when submitting your software to SaaSRow to get 50% off any Featured or Premium tier.

` const emailText = `Welcome to SaaSRow Newsletter! šŸŽ‰\n\nThanks for subscribing!\n\nYou're now part of our community and will receive weekly updates about the latest self-hosted software, news, and exclusive content every Friday.\n\nšŸŽ Here's Your Exclusive Discount Code: 50OFF\n\nGet 50% off Featured or Premium listings!\n\nUse this code when submitting your software to SaaSRow to get 50% off any Featured or Premium tier.\n\nSubmit your software: ${siteUrl}/submit\n\n---\nSaaSRow - The World's Best Place to Find Software\nDon't want to receive these emails? Unsubscribe here: ${siteUrl}/unsubscribe` diff --git a/supabase/functions/send-admin-link/index.ts b/supabase/functions/send-admin-link/index.ts index e72978e..dd27004 100644 --- a/supabase/functions/send-admin-link/index.ts +++ b/supabase/functions/send-admin-link/index.ts @@ -202,7 +202,7 @@ Deno.serve(async (req: Request) => {
- +

Admin Login

diff --git a/supabase/functions/send-admin-notification/index.ts b/supabase/functions/send-admin-notification/index.ts index d7ce648..fbb6003 100644 --- a/supabase/functions/send-admin-notification/index.ts +++ b/supabase/functions/send-admin-notification/index.ts @@ -120,7 +120,7 @@ Deno.serve(async (req: Request) => {

Tier: ${data.tier || 'free'}

A new software has been submitted and is pending review.

- Review in Admin Dashboard + Review in Admin Dashboard
@@ -136,7 +136,7 @@ Tier: ${(data.tier || 'free').toUpperCase()} A new software has been submitted and is pending review. -Review in Admin Dashboard: ${Deno.env.get("SITE_URL") || "https://saasrow.com"}/admin +Review in Admin Dashboard: ${Deno.env.get("SITE_URL") || "https://saasrow.pwa.pit.world"}/admin `; } else if (type === "new_subscription") { subject = `šŸ’° New ${data.tier} Subscription!`; diff --git a/supabase/functions/send-email-broadcast/index.ts b/supabase/functions/send-email-broadcast/index.ts index e9240a4..7c21764 100644 --- a/supabase/functions/send-email-broadcast/index.ts +++ b/supabase/functions/send-email-broadcast/index.ts @@ -48,8 +48,8 @@ Deno.serve(async (req: Request) => { } const resendApiKey = Deno.env.get('RESEND_API_KEY') - const fromDomain = Deno.env.get('RESEND_DOMAIN') || 'saasrow.com' - const siteUrl = Deno.env.get('SITE_URL') || 'https://saasrow.com' + const fromDomain = Deno.env.get('RESEND_DOMAIN') || 'saasrow.pwa.pit.world' + const siteUrl = Deno.env.get('SITE_URL') || 'https://saasrow.pwa.pit.world' if (!resendApiKey) { return new Response( diff --git a/supabase/functions/send-management-link/index.ts b/supabase/functions/send-management-link/index.ts index 28496d4..419229a 100644 --- a/supabase/functions/send-management-link/index.ts +++ b/supabase/functions/send-management-link/index.ts @@ -185,7 +185,7 @@ Deno.serve(async (req: Request) => {
- +

Manage Your Listings

diff --git a/supabase/functions/send-newsletter/index.ts b/supabase/functions/send-newsletter/index.ts index 8594c51..8a77287 100644 --- a/supabase/functions/send-newsletter/index.ts +++ b/supabase/functions/send-newsletter/index.ts @@ -54,8 +54,8 @@ Deno.serve(async (req: Request) => { } const resendApiKey = Deno.env.get('RESEND_API_KEY') - const fromDomain = Deno.env.get('RESEND_DOMAIN') || 'saasrow.com' - const siteUrl = Deno.env.get('SITE_URL') || 'https://saasrow.com' + const fromDomain = Deno.env.get('RESEND_DOMAIN') || 'saasrow.pwa.pit.world' + const siteUrl = Deno.env.get('SITE_URL') || 'https://saasrow.pwa.pit.world' if (!resendApiKey) { return new Response( diff --git a/supabase/migrations/20261028160000_webhooks_hub.sql b/supabase/migrations/20261028160000_webhooks_hub.sql index 36d822f..a745e51 100644 --- a/supabase/migrations/20261028160000_webhooks_hub.sql +++ b/supabase/migrations/20261028160000_webhooks_hub.sql @@ -188,7 +188,7 @@ as $function$ 'tier', s.tier, 'upvotes', coalesce(s.upvotes, 0), 'downvotes', coalesce(s.downvotes, 0), - 'saasrow_url', 'https://saasrow.com/software/' || s.id + 'saasrow_url', 'https://saasrow.pwa.pit.world/software/' || s.id ) from public.software_submissions s where s.id = p_submission