# @funnelsgrove/runtime

Shared funnel runtime contracts and helpers.

Read this file before editing shared funnel mechanics. Deeper runtime behavior is
documented in [`docs/product-specs/funnel-template-runtime.md`](../../docs/product-specs/funnel-template-runtime.md).

## Owns funnel mechanics

`@funnelsgrove/runtime` owns the reusable mechanics that every generated or published funnel depends on: manifests, routing, env resolution, funnel state, preview/runtime detection, and generic runtime UI. Funnels bring their own copy, visual design, billing catalog, and step JSX.

## Use This Package For

- manifest types and validation
- route resolution and entry-point handling
- runtime mode and public env/config resolution
- experiment assignment helpers
- preview-bridge parsing and preview/runtime detection
- published theme contract and CSS variable helpers
- funnel context, flow controller, and generic runtime UI primitives
- funnel-scoped state/storage helpers
- browser-safe API client helpers used by funnels
- reactive browser query-string access through `useBrowserLocationSearch`
- subscription handoff and subscription management screens whose copy stays funnel-local
- test-mode developer info surfaces for funnel preview tooling

## Do Not Use This Package For

- funnel-specific step JSX
- brand/theme assets
- billing catalog data
- analytics transport
- Stripe checkout UI
- funnel-specific developer widget copy
- direct provider SDK calls

## Public Surfaces

- `config/*`: manifest, theme, font, env, and builder-preview contracts.
- `runtime/*`: flow, routing, attribution, feature flags, preview bridge, URL attributes, and manifest validation.
- `runtime/published-runtime-resolution`: running-experiment normalization and immutable offer-set selection from pricing assignments.
- `services/*`: API client, runtime mode, public env, project env, logger, and funnel state helpers.
- `components/*`: generic runtime context, subscription handoff/management, editor panel, dev info, and base controls.
- `content/*` and `steps/types`: shared step content and taxonomy contracts.

## Browser SDK structure

`funnelSdkService` remains the public entry point, with the same eighteen methods
and exported input/response types. It is a plain object of async functions; methods
can also be destructured without losing a `this` binding.

Implementation lives in `services/funnel-sdk/`, grouped by `users`, `uploads`,
`checkout`, and `subscriptions`. Each use case owns its endpoint and sequencing.
Keep request mappers beside their use cases (for example,
`checkout/requests.ts`), not in a generic mapping layer. Only runtime identity,
headers, JSON transport, and API error handling belong in `shared/`.

SDK HTTP requests serve the funnel. Server-to-server integration clients and direct
payment-provider calls do not belong here. Do not create workflow classes or files
for these short request sequences. Photo uploads prepare an upload through the API,
then send the original blob directly to its signed destination, without SDK headers.

From 0.12.0, payment and subscription methods always use Payments V2, including
hosted checkout. An inherited `paymentsApiVersion: 'v1'` no longer selects V1.
Pass published offer/profile/provider/plan selection; subscription mutations
require the listed `subscriptionRef`. `createOneTimePaymentIntent` is removed;
use `createOneTimeCheckout` with the Checkout Session UI instead of passing its
secret to a PaymentIntent form. Server V1 routes are removed. In 0.13.0 the public configuration discriminator
is V2-only and inherited runtime snapshots normalize to V2. Upgrade old published
bundles before deploying server retirement. Keyed checkout creation may replay one transport
failure with the same attempt; payment errors never fall back to V1.
`funnel-sdk.contract.test.ts` captures the wire contract, while
`funnel-sdk.http.test.ts` exercises real loopback HTTP and signed uploads.

`FunnelSdkHttpError` is exported alongside the facade so consumers can narrow
errors without importing transport internals. Its fields and replay semantics
are defined in the [public SDK reference](../../apps/docs/funnel-sdk/public-api.md#retryable-responses).

## Session API structure

`apiService` keeps its thirteen public methods and four public type exports. It is
also a plain object of functions, implemented in `services/session/`:

- `bootstrap/`: verify candidate identity, reuse a request-scoped idempotency key,
  bootstrap once, clear only the completed record, and map the resulting user.
- `users/` and `emailCapture/`: profile updates and email capture, including an
  optional versioned lifecycle-consent payload that the SDK API records before
  the capture event becomes eligible for side effects, with named request/response
  mappers and email-response validation beside the functions.
- `uploads/`: delegate the original file to the SDK signed upload, then validate
  its result; local object URLs are only the no-key fallback.
- `subscriptions/`: resolve identity from the management URL, validate it, and
  call the SDK. Never substitute the stored session identity for a missing link.
- `identity.ts` and `bootstrap/idempotency.ts`: own session storage mechanics.
- `ping/transport.ts`: owns the same-origin, best-effort ping and its error handling.

These are use-case folders, not another workflow or data layer. Keep request and
response mappers pure: pass runtime mode/revision explicitly instead of reading
browser configuration inside a mapping function. SDK requests still go through
`funnelSdkService`; only the same-origin ping owns a separate HTTP boundary.
Validate email/canonical identity before persistence. Preserve failed bootstrap
keys for explicit retry and never add an automatic retry to an uncertain write.

The pre-extraction session tests remain unchanged behavior baselines. Session
boundary tests protect the facade and infrastructure ownership; real loopback
tests exercise bootstrap/profile/email, URL-only subscription management, explicit
bootstrap retry, and binary uploads through both API facades.

## Storage Conventions

- runtime user ids stay funnel-scoped under `funnel:<funnelId>:user-id`
- runtime-generated user ids use the `u_` prefix
- paywall state is stored through runtime helpers under `fg_state:<funnelId>`
- developer reset controls should clear paywall state through `clearPaywallStateValue`, including funnel-provided compatibility keys when a live funnel still needs them

## Engineering Rules

- Do not import `@funnelsgrove/payments` or `@funnelsgrove/analytics` here for checkout or transport orchestration.
- Runtime UI must be generic and copy-injectable.
- Browser-only values must resolve after mount when SSR hydration can be affected.
- Subscription handoff app and universal links receive the known funnel-user email even when the
  configured template omits `{email}`. Store links never receive email, including case-insensitive
  `email` keys present in configured URLs or first-touch attribution.
- Funnel code must not copy `useSyncExternalStore`/`popstate` query-string hooks. Use
  `useBrowserLocationSearch`; the shared controller also notifies it after
  `history.pushState` and `history.replaceState` navigation.
- Keep env mapping centralized in `config/env.config.ts`.
- V2 checkout selection requires the offer set, payment profile, provider, provider
  plan and plan key. `runtimeConfigRevisionId` is optional and must not gate the
  request; a deployment does not invalidate an otherwise unchanged offer. Continue
  forwarding a supplied revision for older API versions. Deploy the compatible
  API before rolling out SDK callers that omit it.

## Build, Test, Publish

```bash
npm run test:run --workspace @funnelsgrove/runtime
npm run build --workspace @funnelsgrove/runtime
npm publish --workspace @funnelsgrove/runtime --access public
```

The build normalizes generated relative ESM imports to explicit `.js` files for published package consumers.
