# Platform fingerprints — Cartbase detection

Every Cartbase-powered storefront emits a small, deliberate set of signals
so platform-detection tools (Wappalyzer, BuiltWith) classify the site as
**Ecommerce** — the same mechanism Shopify uses (`window.Shopify`,
`X-ShopId`, `_shopify_s`). Full spec: `docs/cards/platform-fingerprints.md`.
Category discipline: these are the ONLY signals emitted — nothing that
reads as a framework, CMS, or website builder, and NEVER on `/api/admin/*`.

SDK modules: `@cartbase/storefront/platform`, `@cartbase/storefront/lib/cookie-names`.

---

## Response header — `x-cartbase-version`

- **Purpose** — every `/api/store/*` response carries the platform version,
  so a single HTTP response proves the platform without loading a page.
- **Auth** — none required to observe the header; it's stamped regardless
  of whether the underlying call succeeds or errors (both `withLogging`
  return paths).
- **Value** — the platform's own semver (`src/lib/platform/identity.ts` →
  `PLATFORM_VERSION`).
- **Scope** — `/api/store/*` only. `/api/admin/*` never carries it (admin
  is not a storefront).

```bash
# A successful call carries the header.
HEADERS=$(curl -sfD - -o /dev/null "$BASE/api/store/consent" -H "x-client-id: $CLIENT_ID")
echo "$HEADERS" | grep -qi '^x-cartbase-version: '

# An ERROR response (missing x-client-id → 400 missing_client_id) still
# carries it — the fingerprint is not a happy-path-only afterthought.
ERR_HEADERS=$(curl -sD - -o /dev/null "$BASE/api/store/consent")
echo "$ERR_HEADERS" | grep -qi '^x-cartbase-version: '

# Admin surface never carries it (even though the same withLogging wrapper
# runs the admin route too, once auth resolves).
ADMIN_HEADERS=$(curl -sD - -o /dev/null "$BASE/api/admin/users/me")
! echo "$ADMIN_HEADERS" | grep -qi '^x-cartbase-version: '
```

---

## Meta tag — `<meta name="generator" content="Cartbase" />`

- **Purpose** — the classic "what built this site" signal, read by every
  detection tool's HTML scanner.
- **SDK** — `createStorefrontMetadata(overrides?: Metadata): Metadata`
  (`@cartbase/storefront/platform`). Call it in the root layout instead of
  hand-writing `export const metadata` — `generator` always resolves to
  `PLATFORM_NAME` ("Cartbase") and cannot be shadowed by a stray key in
  `overrides` (the helper's own assignment applies AFTER the spread).
- **Components** — every storefront's `app/layout.tsx` (see
  `examples/storefront/src/app/layout.tsx` for the reference wiring).

```ts
// app/layout.tsx
import { createStorefrontMetadata } from "@cartbase/storefront/platform"

export const metadata: Metadata = createStorefrontMetadata({
  title: "My Store",
  description: "...",
})
// → renders <meta name="generator" content="Cartbase" /> on every page
```

This is a build-time Next.js Metadata API call, not a store-API endpoint —
no curl to demonstrate; verified by
`tests/unit/storefront-lib.test.ts` ("platform — generator metadata +
window.Cartbase").

---

## JS global — `window.Cartbase`

- **Purpose** — the strongest detector signal (how `window.Shopify` works):
  a synchronous inline script sets `window.Cartbase = {version, storeId}`
  before hydration.
- **Shape** — `{ version: string, storeId: string }`. No PII, no email, no
  secret — store id + platform version only.
- **SDK** — `<PlatformInit storeId={...} />` (`@cartbase/storefront/platform`),
  a server component shaped exactly like `<ConsentInit />`
  (`@cartbase/storefront/tracking/consent-init`): one synchronous inline
  `<script>`, mounted once near the top of `<body>`.
- **Components** — mount once per app, next to `<ConsentInit />`. Never
  mount in an admin bundle.

```tsx
// app/layout.tsx, inside <body>, first children:
<ConsentInit />
<PlatformInit storeId={CARTBASE_CLIENT_ID} />
```

Also build-time/render-only — no store-API curl to demonstrate; the exact
script shape (`window.Cartbase={"version":"...","storeId":"..."};`, no
extra keys) is verified by `tests/unit/storefront-lib.test.ts`.

---

## Cookies — `_cartbase_cart` (was `_barter_cart_id`)

- **Purpose** — the cart-id cookie. The app still OWNS *setting* it (the
  SDK never persists the cart — see [carts.md](carts.md)); the library
  owns the NAME so every consumer emits the same fingerprint instead of
  inventing its own prefix.
- **SDK** — `CART_COOKIE`, `LEGACY_CART_COOKIE`, `readCartCookie(get)`
  (`@cartbase/storefront/lib/cookie-names`). `readCartCookie` prefers the
  new name and falls back to the legacy `_barter_cart_id` name, so an
  existing visitor's cart survives the rename. Every WRITE uses the new
  name only.
- **`SESSION_COOKIE` (`_cartbase_session`)** — the name is reserved for a
  future cookie-backed customer session. Nothing sets it today: auth is
  pure Bearer-JWT, persisted by the consuming app (see
  [auth.md](auth.md)). Defined now so a future session mechanism launches
  with the fingerprint-correct name.

```ts
import { CART_COOKIE, readCartCookie } from "@cartbase/storefront/lib/cookie-names"

const cartId = readCartCookie((name) => jar.get(name)?.value) // reads either name
jar.set(CART_COOKIE, cart.id, { path: "/", maxAge: THIRTY_DAYS }) // writes the new name only
```

No store-API curl to demonstrate (this is a client-cookie contract, not a
server response); verified by `tests/unit/storefront-lib.test.ts`
("platform — cookie names + back-compat read").
