diff --git a/apps/logicsrc-web/e2e/logicsrc.spec.ts b/apps/logicsrc-web/e2e/logicsrc.spec.ts index 04eb553..1b4e3db 100644 --- a/apps/logicsrc-web/e2e/logicsrc.spec.ts +++ b/apps/logicsrc-web/e2e/logicsrc.spec.ts @@ -24,15 +24,40 @@ test.describe("LogicSRC PWA", () => { await expect(page.getByText("logicsrc credentials providers")).toBeVisible(); }); - test("renders top-level docs and legal route targets", async ({ page }) => { - await page.goto("/privacy"); + test("links every Top-Level Pages card at its route", async ({ page }) => { + await page.goto("/"); await expect(page.getByRole("heading", { name: "Top-Level Pages" })).toBeVisible(); - await expect(page.getByText("/docs · Docs")).toBeVisible(); - await expect(page.getByText("/blog · Blog")).toBeVisible(); - await expect(page.getByText("/credential-sharing · Credential Sharing")).toBeVisible(); - await expect(page.getByText("/terms · Terms")).toBeVisible(); - await expect(page.getByText("/privacy · Privacy")).toBeVisible(); + + // Each card advertises a stable route, so each card has to be a link to it. + for (const route of [ + "docs", + "blog", + "openspec", + "credential-sharing", + "hire-us", + "about", + "terms", + "privacy", + ]) { + await expect( + page.locator(`#page-${route} a[href="/${route}"]`), + `/${route} card should link to /${route}`, + ).toBeVisible(); + } + }); + + test("renders a real Privacy page rather than the homepage", async ({ page }) => { + await page.goto("/privacy"); + + await expect(page.getByRole("heading", { name: "Privacy", exact: true })).toBeVisible(); + await expect(page.getByRole("heading", { name: "Analytics" })).toBeVisible(); + await expect(page.getByRole("heading", { name: "Cookies" })).toBeVisible(); + await expect(page.getByText("Secret values are encrypted end-to-end")).toBeVisible(); + + // The old /privacy fell through to [[...slug]] and served the whole + // homepage; the Top-Level Pages band is the tell that it regressed. + await expect(page.getByRole("heading", { name: "Top-Level Pages" })).toHaveCount(0); }); test("renders Hire Us project request flow", async ({ page }) => { diff --git a/apps/logicsrc-web/src/app/[[...slug]]/page.tsx b/apps/logicsrc-web/src/app/[[...slug]]/page.tsx index 0101e96..986ac65 100644 --- a/apps/logicsrc-web/src/app/[[...slug]]/page.tsx +++ b/apps/logicsrc-web/src/app/[[...slug]]/page.tsx @@ -8,9 +8,12 @@ import { HomeInteractivity } from "@/components/home-interactivity"; // scrolled to the matching section. We preserve those URLs (they are canonical // in sitemap.xml) by rendering the same page for each known route and 404ing // anything else. -// /about, /docs, /pricing, and /terms are now real routes (app/about, app/docs, -// app/pricing, app/terms); the rest still render the homepage SPA scrolled to -// their section. +// /about, /docs, /pricing, /privacy, and /terms are now real routes +// (app/about, app/docs, app/pricing, app/privacy, app/terms); the rest still +// render the homepage SPA scrolled to their section. Every key left here MUST +// have a matching section id in `renderPageMarkup` -- /privacy used to be +// listed without one, so it served the whole homepage and scrolled to a card +// that only described the page that did not exist. const ROUTE_META: Record = { openspec: { title: "LogicSRC vs OpenSpec.dev · LogicSRC", @@ -24,7 +27,6 @@ const ROUTE_META: Record = { title: "Hire Us · LogicSRC", description: "Implementation help for LogicSRC, AgentSwarm, and Credential Sharing at $400/hour for accepted work, paid via CoinPay.", }, - privacy: { title: "Privacy · LogicSRC", description: "LogicSRC privacy notes." }, "agent-swarm": { title: "AgentSwarm · LogicSRC", description: "Provider-neutral agent orchestration with model routing, cost controls, and GitHub integration.", diff --git a/apps/logicsrc-web/src/app/privacy/page.tsx b/apps/logicsrc-web/src/app/privacy/page.tsx new file mode 100644 index 0000000..f6ea683 --- /dev/null +++ b/apps/logicsrc-web/src/app/privacy/page.tsx @@ -0,0 +1,162 @@ +import type { ReactNode } from "react"; +import type { Metadata } from "next"; +import { SiteShell } from "@/components/site-shell"; + +export const metadata: Metadata = { + title: "Privacy · LogicSRC", + description: + "What logicsrc.com collects and what it does not: privacy-friendly analytics, the Hire Us project form, the CoinPay sign-in cookie, and the boundary that keeps credential values off our servers.", + alternates: { canonical: "/privacy" }, +}; + +export default function PrivacyPage(): ReactNode { + return ( + +
+
+

Privacy

+

+ This page describes what logicsrc.com does with data. It covers the + marketing site and the credentials app; it does not restrict how you + use the specification, schemas, or CLI, which run on your own + machines and report nothing back to us. +

+
+ +
+

Who is responsible

+

+ Profullstack, Inc. operates logicsrc.com and is responsible for the + data described here. For any privacy question, or to ask us to + delete something you sent us, email{" "} + privacy@profullstack.com + . Security reports go to{" "} + security@profullstack.com{" "} + (see security.txt). +

+ +

There is no account required to read this site

+

+ Browsing the specification, docs, blog, and schema pages requires no + account, no sign-in, and no cookie. We do not run advertising + trackers, we do not sell or share data with data brokers, and we do + not build advertising profiles. +

+ +

Analytics

+

+ Page views are measured with{" "} + CrawlProof, a + privacy-friendly analytics service that records aggregate page + traffic — pages visited, referrer, rough geography, and device + class. It does not set advertising cookies and does not follow you + across other sites. We use it to see which specs and docs people + actually read. +

+ +

The Hire Us form

+

+ If you submit a project through Hire Us, you + send us two things: the contact address you type, and your + description of the work. We use them only to evaluate and reply to + your request, and to scope an engagement if it is a fit. We do not + add the address to a marketing list. Ask us to delete a request and + we will. +

+ +

Payments and the CoinPay sign-in

+

+ Paid work is invoiced through CoinPay. If you + connect a CoinPay account, we complete an OAuth sign-in and store + the resulting session in a signed, HttpOnly,{" "} + SameSite=Lax cookie so the site can show you as + connected. That session holds your CoinPay identifier and the + profile fields CoinPay returns (typically name and email). Clearing + your cookies ends it. +

+

+ Card numbers and cryptocurrency wallet credentials are entered on + CoinPay, not here. We never receive them. CoinPay's handling of + your payment data is governed by CoinPay's own privacy policy. +

+ +

Credential Sharing: what we never receive

+

+ The LogicSRC credentials app exists to sync secrets between + providers, so the boundary matters. Secret values are + encrypted end-to-end on your device before they are stored. We hold + ciphertext we cannot read. Audit records — who synced which key + name, to which provider, when, and the resulting key fingerprint — + are stored in readable form so that a sync is reviewable; key names + and fingerprints are recorded, secret values are not. +

+

+ The logicsrc CLI and TUI run locally. Running a spec + validation, a schema check, or a credentials dry run sends nothing + to us. Only commands that explicitly talk to a hosted endpoint — for + example logicsrc login — make a network call. +

+ +

Server logs

+

+ Like any web service, our hosting produces operational logs + containing IP addresses, timestamps, request paths, and user-agent + strings. They are used to keep the service running and to + investigate abuse and outages, and they are not used to profile + visitors. +

+ +

Cookies

+

+ We set cookies only for functions you initiate: a short-lived + state cookie during a CoinPay OAuth round trip, and the CoinPay + session cookie described above. There are no advertising or + cross-site tracking cookies. The site also registers a service + worker for offline page caching; it stores pages in your own + browser and sends us nothing. +

+ +

Third parties we rely on

+

+ Our hosting provider, our database provider, CrawlProof for + analytics, and CoinPay for payments process data on our behalf in + order to run the service. We do not sell personal data, and we do + not share it with anyone else except where we are legally required + to. +

+ +

Retention

+

+ Project requests are kept while an engagement is live and afterward + only as long as we need them for tax and accounting records. + Operational logs roll off on our provider's normal schedule. + Analytics data is aggregate and is not tied back to you. +

+ +

Your choices

+

+ You can ask us for a copy of what we hold about you, ask us to + correct it, or ask us to delete it — write to{" "} + privacy@profullstack.com{" "} + and we will act on it. Deleting an encrypted credential removes the + ciphertext; the corresponding audit record is retained, because an + audit trail that can be edited is not an audit trail. +

+ +

Children

+

+ This is a developer tool and is not directed at children. We do not + knowingly collect data from anyone under 16. +

+ +

Changes

+

+ We will update this page as the hosted products grow. Material + changes will be noted in the blog. See also our{" "} + Terms. +

+
+
+
+ ); +} diff --git a/apps/logicsrc-web/src/components/home-interactivity.tsx b/apps/logicsrc-web/src/components/home-interactivity.tsx index f4c6874..e6c00eb 100644 --- a/apps/logicsrc-web/src/components/home-interactivity.tsx +++ b/apps/logicsrc-web/src/components/home-interactivity.tsx @@ -65,10 +65,11 @@ export function HomeInteractivity(): null { } else if (pathname === "/agentbyte") { document.querySelector("#agentbyte")?.scrollIntoView(); } else { + // Only the paths [[...slug]] still serves from the homepage belong here. + // /docs, /blog, /about, /privacy, and /terms are real routes now, and + // listing them just aimed scrollIntoView at a Top-Level Pages card. const pageRoute = pathname.slice(1); - if ( - ["docs", "blog", "openspec", "credential-sharing", "hire-us", "about", "terms", "privacy"].includes(pageRoute) - ) { + if (["openspec", "credential-sharing", "hire-us"].includes(pageRoute)) { document.querySelector(`#${pageRoute}`)?.scrollIntoView(); } } diff --git a/apps/logicsrc-web/src/lib/page-markup.ts b/apps/logicsrc-web/src/lib/page-markup.ts index 89a8234..9e2ca8a 100644 --- a/apps/logicsrc-web/src/lib/page-markup.ts +++ b/apps/logicsrc-web/src/lib/page-markup.ts @@ -65,6 +65,11 @@ const hireUsWork = [ { name: "Open infrastructure", detail: "Portable code and specs first: no closed workflow lock-in, no one-off agent scripts that cannot be audited." } ]; +// The stable top-level routes, each a real page. `id` is the route segment, so +// the card links straight to it. The rendered article ids are prefixed +// (`page-docs`) because three of these -- openspec, credential-sharing, hire-us +// -- are also section ids further up this same document, and duplicate ids made +// the in-page scroll targets ambiguous. const pages = [ { id: "docs", title: "Docs", detail: "Specification guides, CLI conventions, schemas, plugin contracts, SDK conventions, and MCP resources." }, { id: "blog", title: "Blog", detail: "Project notes for LogicSRC, AgentSwarm, AgentByte, OpenSpec workflows, and reference implementations." }, @@ -73,7 +78,7 @@ const pages = [ { id: "hire-us", title: "Hire Us", detail: "$400/hour LogicSRC work on open infrastructure, specs, AI agent workflows, and reference implementations paid through CoinPay after project acceptance." }, { id: "about", title: "About", detail: "LogicSRC is the Profullstack open specification project for human and AI agent coordination." }, { id: "terms", title: "Terms", detail: "Terms of engagement: the $400/hour rate, what is billable, how approved hours are invoiced, the 10-hour minimum, cancellation, acceptable use, and reference implementation boundaries." }, - { id: "privacy", title: "Privacy", detail: "Draft privacy notes will cover telemetry, audit events, identity data, and hosted-product data boundaries." } + { id: "privacy", title: "Privacy", detail: "What this site collects and what it does not: analytics, the Hire Us form, the CoinPay sign-in cookie, and where credential data does and does not go." } ]; const comparisonRows = [ @@ -419,10 +424,10 @@ COINPAY_STATUS=pending_acceptance
${pages.map((page) => ` -