# StorageHand public website integration v1

This is the stable v1 contract for the released `@fss/embed` Facility Hours,
Unit List, and Unit Compare tranche. It describes the product that is shipped;
internal classes, Shadow DOM structure, private `--_fss-*` variables, admin
routes, portal authentication, checkout implementation, and payment providers
are not public integration surfaces.

## Five-line quickstart

<!-- prettier-ignore -->
```html
<script type="module" src="https://embed.fullstackstorage.com/v1/fss-embed.js"></script>
<fss-config publishable-key="fss_pk_live_000000000000000000000000" business="your-business" facility="your-facility"></fss-config>
<fss-facility-hours show-status></fss-facility-hours>
<fss-unit-list show-compare></fss-unit-list>
<fss-unit-compare></fss-unit-compare>
```

Use the publishable key and selectors shown under **Settings → Website
Integration**. The selectors are assertions inside the key's server-owned
scope; they cannot broaden it.

## Load one browser module

A normal external website loads exactly one ESM browser module:

```html
<script
  type="module"
  src="https://embed.fullstackstorage.com/v1/fss-embed.js"
></script>
```

That module registers all released elements. Registration is duplicate-safe;
Hours, List, and Compare do not need separate scripts or stylesheets. `/v1` is
the mutable, backwards-compatible v1 channel and **must not be paired with a
fixed SRI hash**. An integrator that needs immutable bytes may pin the published
`/{version}/fss-embed.js` artifact and use its release SRI value.

## Configuration

`<fss-config>` is an optional shared page configuration element. It renders no
UI and has no public imperative methods.

| Attribute         | Property         | Values/default                                                                                    |
| ----------------- | ---------------- | ------------------------------------------------------------------------------------------------- |
| `publishable-key` | `publishableKey` | required locally or through shared config; `fss_pk_live_` or `fss_pk_test_` plus 24 alphanumerics |
| `business`        | `business`       | optional 2–50 character canonical selector                                                        |
| `facility`        | `facility`       | optional 2–50 character canonical selector                                                        |
| `theme`           | `theme`          | `auto` (default), `slate`, `warm`, `forest`, `ocean`, `plum`, `high-contrast`                     |
| `mode`            | `mode`           | `default` (default), `inverse`                                                                    |
| `appearance`      | `appearance`     | `contained` (default), `card`, `transparent`                                                      |
| `auto-brand`      | `autoBrand`      | `on` (default), `off`                                                                             |

Every visual element accepts those same seven attributes directly. Per-axis
precedence is: local element, nearest composed `<fss-theme>`, same-realm
`<fss-config>`, safe default. A direct configuration is useful for independent
widgets or pages without shared config:

```html
<fss-facility-hours
  publishable-key="fss_pk_live_000000000000000000000000"
  business="your-business"
  facility="your-facility"
  variant="compact"
  show-status
></fss-facility-hours>
```

`FSS.init({publishableKey, business?, facility?, theme?, mode?, appearance?,
autoBrand?})` is the existing programmatic configuration alternative. Its
registration supports `update()` and `dispose()`. The Custom Elements
themselves expose no public imperative methods.

The complete generated configuration/property/default/state manifest is
[`packages/fss-embed/generated/config-manifest.json`](./generated/config-manifest.json).

## Facility Hours

```html
<fss-facility-hours
  variant="table"
  show-status
  appearance="transparent"
></fss-facility-hours>
```

`variant` is `table` (default), `inline`, or `compact`. Boolean `show-status`
defaults off. All properties reflect. The component uses server hours and the
verified facility IANA timezone, not the browser timezone. Its public
`data-state` values are `waiting`, `loading`, `ready`, `empty`, and `error`.
Empty means the authoritative schedule is unavailable; it does not invent
closed days. Failures emit sanitized `fss:error`.

## Unit List

```html
<fss-unit-list page-size="12" default-sort="display-order" show-compare>
  <p><a href="YOUR_CANONICAL_FSS_RENTAL_PAGE">View units and pricing</a></p>
</fss-unit-list>
```

`page-size` / `pageSize` is an integer from 1 through 48 (default 12).
`default-sort` / `defaultSort` is `display-order` (default),
`price-ascending`, `price-descending`, `size-ascending`, or
`size-descending`. Boolean `show-compare` / `showCompare` defaults off and
enables compare toggles. Price/area filters, feature filters, availability,
sort, and pagination are component-owned over the authoritative bounded
catalogue. Public `data-state` values are `waiting`, `loading`, `slow`, `ready`,
`empty`, and `error`.

The rental action uses only each unit's validated, server-authored
`bookingUrl`. Host code must not construct a booking route or append a unit
identifier.

## Unit Compare

The default tray coordinates with compatible Unit Lists in the same
`Document` and facility catalogue. Selection is isolated per `Document`; an
iframe does not share it. Two to four units may be selected. If shared runtime
coordination is unavailable, each element retains its local selection fallback.

```html
<fss-unit-list show-compare></fss-unit-list>
<fss-unit-compare></fss-unit-compare>
```

Static/inline mode is declarative and preserves a 2–4 item order:

```html
<fss-unit-compare
  static
  unit-type-ids="11111111-1111-4111-8111-111111111111,22222222-2222-4222-8222-222222222222"
></fss-unit-compare>
```

The boolean `static` attribute maps to `staticMode`; `unit-type-ids` maps to
`unitTypeIds`. The default is tray mode with an empty selection. Public states
are `waiting`, `loading`, `ready`, `empty`, and `error`. Its additional
`data-compare-state` values are `waiting-config`, `loading`, `ready`,
`insufficient-selection`, `invalid-selection`, and `error`. There are no public
imperative methods.

## Shared configuration for all three

```html
<fss-config
  publishable-key="fss_pk_live_000000000000000000000000"
  business="your-business"
  facility="your-facility"
  theme="auto"
></fss-config>
<fss-facility-hours variant="compact" show-status></fss-facility-hours>
<fss-unit-list page-size="12" show-compare></fss-unit-list>
<fss-unit-compare></fss-unit-compare>
```

Shared config may appear before or after the elements. Multiple compatible
elements deduplicate configuration and catalogue work within their `Document`.

## Theme, mode, appearance, tokens, and parts

Theme, mode, appearance, and auto-brand are independent. `theme="auto"` with
`auto-brand="on"` may use a validated facility branding color as a private
fallback. Explicit public CSS tokens still win. Arbitrary author CSS can reduce
contrast, so the host remains responsible for its overrides.

```html
<fss-theme theme="ocean" mode="inverse" appearance="card">
  <fss-facility-hours show-status></fss-facility-hours>
  <fss-unit-list show-compare></fss-unit-list>
  <fss-unit-compare></fss-unit-compare>
</fss-theme>
```

Only the bounded generated surfaces are stable:

- [theme presets and axes](./generated/theme-manifest.json)
- [public semantic and component tokens](./generated/token-manifest.json)
- [public `::part` names](./generated/part-manifest.json)

Private CSS variables, classes, element internals, and Shadow DOM nodes are not
contracts. Compare tray/modal UI is rendered in a per-Document portal and is
themed through tokens; element-scoped `::part` selectors cannot reach that
portal UI.

## Events

Except for `fss:rental-request`, public events are `CustomEvent`s with `bubbles: true`, `composed: true`, and
`cancelable: false`. Details are shallow-frozen; `unitTypeIds` arrays are also
frozen. They contain only documented public/sanitized fields.

| Event                  | Source               | Detail                                                               |
| ---------------------- | -------------------- | -------------------------------------------------------------------- |
| `fss:error`            | Hours, List, Compare | `{code, message, origin?, remedy?, status?, retryable?, requestId?}` |
| `fss:unit-selected`    | List, Compare        | `{unitTypeId}`                                                       |
| `fss:checkout-started` | List, Compare        | `{unitTypeId, rate}` where `rate` is current monthly minor units     |
| `fss:compare-changed`  | List, Compare        | `{unitTypeIds}` (0–4 IDs)                                            |
| `fss:compare-opened`   | Compare              | `{unitTypeIds}` (2–4 IDs)                                            |

```js
document.addEventListener("fss:checkout-started", (event) => {
  console.log(event.detail.unitTypeId);
});
```

The exact schemas and error-code enum are generated in the
[event manifest](./generated/event-manifest.json). No
event includes raw API responses, secrets, catalogue objects, or DOM nodes.

## Public API and errors

The component runtime currently calls:

- `GET /api/v1/embed/bootstrap`
- `GET /api/v1/embed/{businessSlug}/facilities/{facilitySlug}/hours`
- `GET /api/v1/embed/{businessSlug}/facilities/{facilitySlug}/unit-types`

The same accepted public embed family also freezes the mounted facility `info`
and `features` reads. The canonical [OpenAPI 3.1 contract](../../docs/swagger/web-components-v1.yaml)
defines methods, parameters, bounds, nullable fields, classifications, and wire
shapes. Every request uses `X-FSS-Publishable-Key`; keys are never transported
in a query string. Browser requests require exact `Origin` authority and use no
cookies (`credentials: omit`).

All public embed errors use `application/problem+json` and RFC 9457 fields:
`type`, `title`, `status`, `detail`, optional relative `instance`, optional safe
`requestId`, `retryable`, and optional bounded `code`. Only
`invalid_publishable_key`, `origin_not_allowed`, and `key_scope_mismatch` may
appear as `code`. The representation cannot contain stacks, SQL, service names,
private URLs, secrets, raw database/provider identifiers, or raw error text.
Every public embed success and error currently sends `Cache-Control: no-store`;
key-aware shared caching is not part of the v1 contract. Bootstrap contains no
payment-provider identity or other processor configuration.

Facility `info` keeps the stable-v1 required `address` object. When a facility's
operator-controlled publication policy hides its address, that object is empty
and the additive `addressPublished` field is `false`; consumers must not invent,
infer, or display placeholder location text. When `addressPublished` is absent,
the v1-compatible default is published. Bootstrap and the first-tranche Custom
Elements do not carry or require facility address fields. FSS Rent suppresses
the empty display address and asks the renter to choose their billing country;
it does not infer a facility location from hidden data.

The platform has four distinct route families and authorities:

| Family             | Routes                 | Browser component use                |
| ------------------ | ---------------------- | ------------------------------------ |
| Public embed       | `/api/v1/embed`        | yes; publishable key + exact Origin  |
| Portal BFF/actions | `/bff/v1`, `/actions`  | no; separate opaque-session boundary |
| Server integration | `/api/v1/integrations` | no; confidential server clients      |
| Provider webhook   | `/api/v1/webhooks`     | no; provider verification            |

Private/admin/legacy routes are not public component APIs merely because they
exist in the same service.

## Security and data classification

Publishable keys are browser-safe public credentials scoped by server-owned
business, optional facility, environment, and approved Origins. They are not
secrets. Business/facility selectors are assertions and cannot broaden the key.
There is no browser secret expected.

**NO CUSTOMER OAUTH FOR THIS WEBSITE COMPONENT TRANCHE.** Do not put a customer
access token, JWT, refresh token, session credential, admin key, or server
credential in ordinary website integration code. A later protected portal
capability uses its own approved host/session boundary and is not part of this
contract.

| Classification                        | Examples                                                                                                                                                                                                 |
| ------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Public / intended for website display | facility public identity, schedule/hours, dimensions, product names/descriptions, prices, display promotions, published features, availability, canonical rental destinations                            |
| Public configuration / authority      | publishable key, business/facility selector assertions, exact approved Origins, theme/configuration inputs                                                                                               |
| Not public                            | customer/tenant data, private database identity, billing/ledger data, processor/payment internals, privileged promotion rules or codes, secrets, admin APIs, browser session secrets, server credentials |

## CSP and exact Origin

The current runtime needs only these FSS hosts in host policy:

```text
script-src 'self' https://embed.fullstackstorage.com;
connect-src 'self' https://api.fullstackstorage.com;
```

Keep any nonce/hash policy your host already requires. The components load no
FSS font, tracker, stylesheet, theme asset, or payment-provider SDK. The website
component tranche does not send card data directly to processor hosts, so do
not add payment-provider hosts on its behalf.

Origin approval and CSP are different controls. Each scheme + hostname + port
is a distinct Origin. Approve every real production and preview Origin you use,
including apex and `www`, staging, hosted-builder preview, and temporary builder
domains. Do not assume one approval covers another. Do not use wildcard
production origins or publishable keys in query strings. Release rehearsal may
use `embed-test.fullstackstorage.com`; its Origin must still be explicitly
approved, and its CSP entry is separate from production.

## Diagnostics

`FSS.diagnose()` returns a safe snapshot for integration troubleshooting:

```js
console.table(FSS.diagnose());
```

Current fields cover runtime `version`, fixed API/route identity, configuration
status/source, duplicate config count, redacted key presence, bootstrap status,
safe HTTP/error code, current Origin and remedy only for an exact readable
origin denial, safe request ID/retryability, and bounded host-collision codes.
The key is reported only as `[redacted]` or `[not-configured]`. Diagnostics do
not expose tokens, raw responses, stack traces, private IDs, or provider data.
Inspect each element's public `data-state` and sanitized `fss:error` event for
component-level status. A CORS-blocked response is intentionally a generic
network failure because JavaScript cannot safely read its body.

## Rental handoff and safe return state

Since 1.6.0, List and Compare emit `fss:rental-request` after read-only
availability refresh and before checkout events/navigation. Its frozen detail is
`{unitTypeId}`; it bubbles, crosses shadow boundaries, and is cancelable.
Call `event.preventDefault()` synchronously to handle the intent in your website
(for example, show an informational dialog). Cancellation emits neither
`fss:unit-selected` nor `fss:checkout-started` and does not navigate. Without
cancellation, existing server-authored rental navigation is unchanged.
This is a presentation hook, not server authorization: integrators must separately
protect any tenant that prohibits direct public mutations.

```js
document.addEventListener("fss:rental-request", (event) => {
  event.preventDefault();
  document.querySelector("#demo-dialog").showModal();
});
```

Unit List and Compare navigate only to the exact validated, server-authored
`bookingUrl` in the catalogue. `fss:checkout-started` is an outbound handoff
signal, not evidence of a completed rental.

This released tranche does **not** define a host-site return query parameter,
completion flag, or customer/session token. A host must never infer a successful
rental from an unverified query string or redirect. Use the canonical FSS-hosted
rental destination as fallback navigation. Any future server-backed opaque
return-state contract requires its own explicit release; do not invent one in
host code.

## Adapter/CMS contract

CMS integrations supply public configuration to the same released Custom
Elements. The generated [adapter contract](./generated/adapter-contract.json)
and [JSON Schema](./generated/adapter-schema.json) are
canonical. An adapter must not reimplement rendering, fork business logic, copy
catalogue truth, create alternative authentication, construct booking URLs, or
inspect Shadow DOM. This contract does not ship WordPress, Squarespace, Wix, or
Shopify adapters.

## Accessibility and host responsibilities

The components own their internal labels, keyboard behavior, focus visibility,
minimum targets, live status, forced-colors handling, reduced motion, tray
dialog focus containment, and English `lang` boundary. The host must keep the
module and fallback links reachable, provide meaningful page headings/context,
avoid clipping fixed compare UI, preserve sufficient contrast when overriding
tokens, test zoom/reflow in its layout, and not remove focus indicators.

## Privacy

Hours, List, and Compare consume public facility/catalogue data. They require no
customer OAuth/session token and must not expose private customer data. Event
details contain only the documented public/sanitized fields. Treat the host's
own analytics, consent, forms, and contact systems separately; this engineering
guidance is not a replacement legal privacy policy.

## Compatibility and versioning

| Surface/change                                                                                                                            | Classification                                    |
| ----------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------- |
| Mutable CDN `/v1`                                                                                                                         | receives compatible v1 releases; bytes may change |
| Immutable `/{version}` artifact                                                                                                           | bytes, SHA-256, and published SRI do not change   |
| Add optional response field/attribute/event/token/part                                                                                    | normally non-breaking                             |
| Add enum value where consumers are explicitly required to tolerate unknown future values                                                  | non-breaking                                      |
| Add enum value to a closed enum used by exhaustive consumers                                                                              | breaking unless separately versioned              |
| Remove/rename element, attribute/property, event, API field, token, part, adapter field, or Problem field                                 | breaking / major contract change                  |
| Materially change defaults, event payload types, key transport, exact-Origin authority, booking destination semantics, or accepted ranges | breaking / major contract change                  |
| Narrow an accepted enum/range or make an optional field required                                                                          | breaking / major contract change                  |

Component names, current attributes/properties and defaults, existing events and
payload field types, OpenAPI required fields, the key header, exact-Origin
semantics, published tokens/parts, adapter schema, and Problem Details fields
are stable v1 surfaces. Additive fields must remain optional. Clients should
ignore additive events they do not subscribe to. The manifests themselves are
generated and checked for deterministic drift.
The checked-in stable-v1 compatibility baselines are compared as subsets of the
current generated component and OpenAPI contracts, so removals or incompatible
changes to names, defaults, required fields, enums, ranges, tokens, parts, event
details, adapter fields, or public API wire shapes fail targeted validation.

## Browser, rendering, and SEO expectations

The released module targets modern evergreen browsers with ES modules, Custom
Elements, Shadow DOM, `fetch`, `AbortController`, `CustomEvent`, `URL`,
`Intl`, and `structuredClone`. It is client-rendered and assumes a real browser
`Document`; server-side rendering of component internals is not part of v1.

Do not make crawlability promises for inventory rendered after JavaScript runs.
Sites that need a crawlable inventory destination should retain a normal link
to the canonical FSS-hosted rental page, including useful fallback content
inside Unit List where appropriate.
