@@ -57,10 +57,13 @@ GraphQL) and gateway routing (`cng_backoffice_api-v1` /
5757- Exposes ** top-level short-form methods** that delegate directly to the
5858 underlying service, e.g. ` peek.getAllProducts() ` → ` peek.getProductService().getAllProducts() ` . Every public service method has a named proxy on ` PeekAccessService ` ; the names are prefixed with the resource noun where disambiguation is needed (e.g. ` getBookingById ` , ` getTimeslotById ` ).
5959- Exposes ` verifyPeekAuthToken(token) ` to verify HMAC-signed JWTs issued by
60- the Peek app registry (` iss: "app_registry_v2" ` ), returning
60+ the Peek app registry (` iss: "app_registry_v2" ` , ` aud: "Joken" ` ), returning
6161 a fully typed ` PeekAuthTokenClaims ` (including the nested ` PeekAuthTokenUser `
6262 object). Throws ` JsonWebTokenError ` / ` TokenExpiredError ` / ` NotBeforeError `
63- from ` jsonwebtoken ` on failure.
63+ from ` jsonwebtoken ` on failure. The signature-check core (issuer + audience +
64+ user mapping) lives in the internal ` peek-auth-token.ts ` module, shared with
65+ the standalone ` verifyInstallWebhook ` (§ install webhooks) so the two can't
66+ drift apart.
6467- Composes dependencies between services where needed:
6568 - ` TimeslotService ` receives the resource-pool and account-user services (for
6669 guide resolution).
@@ -104,12 +107,16 @@ Responsibilities:
104107- Collapses query whitespace (` \s+ ` → single space) before sending.
105108- Retries HTTP 429 using the configured backoff delays, then throws
106109 ` RateLimitError ` .
110+ - Reads the response body via the shared ` parseBody ` helper (` text() ` → try
111+ ` JSON.parse ` , falling back to raw text) * before* branching on status, so a
112+ non-JSON error page never throws a ` SyntaxError ` that hides the real status.
107113- Maps known failures to typed errors:
108114 - HTTP 418 → ` AdminAccountRequiredError `
109115 - HTTP 429 (after retries) → ` RateLimitError `
110116 - GraphQL ` errors ` array present → ` PeekGraphQLError ` (raw errors preserved on
111117 ` .graphqlErrors ` )
112- - other non-2xx → generic ` Error ` with the status.
118+ - other non-2xx → ` PeekHttpError ` (carries ` .statusCode ` , ` .url ` , and raw
119+ ` .body ` ).
113120
114121### 4. Per-resource services
115122` src/internal/peek/<resource>/ `
@@ -213,7 +220,20 @@ pure `fromWaiverNode` converter, which maps the fixed `snake_case` payload to th
213220flat clean ` Waiver ` model (defaulting missing fields to ` "" ` /` null ` /` false ` , so
214221it never throws). Same standalone-pure-function rationale as bookings. Because
215222there are no reads, ` waivers ` carries no queries/service triad — just the
216- webhook module and the model. The detailed ` AddonItem `
223+ webhook module and the model.
224+
225+ The ** install-status** webhook (` installs/install-webhook.ts ` ) is the third and
226+ most distinct: its payload is not a data node but a ** signed ` app_registry_v2 `
227+ JWT** , so ` verifyInstallWebhook(token, secret) ` * verifies* it (signature +
228+ expiry + issuer + ` Joken ` audience, via the shared ` peek-auth-token.ts ` core)
229+ before mapping to the clean ` InstallWebhookClaims ` (` installId ` , ` account.id ` ,
230+ ` status ` , ` displayVersion ` , and a ** nullable** ` user ` — install lifecycle events
231+ are often system-initiated). Standalone-function rationale as above, with an
232+ extra reason: the receiver has no per-install service to inherit a secret from
233+ yet (the webhook can precede the first session or describe a tear-down), so the
234+ app secret is passed directly. Like ` waivers ` , ` installs ` carries no
235+ queries/service triad — just the verifier and the (shared ` auth-token.ts ` ) model.
236+ The detailed ` AddonItem `
217237model (refids + reservation statuses) is ** internal only** — consumers see just
218238the grouped ` BookingAddons ` ; the internal model exists solely so add/remove can
219239build their mutation payloads.
@@ -315,14 +335,16 @@ pinned by the drift-guard test; `fullCustomerAccess` governs only the runtime re
315335The barrel re-exports only the public contract: ` PeekAccessService ` + its config,
316336the ` AccessOptions ` type (see §4b), each resource service class (and the
317337options/result types callers need), all data-model ** types** (including
318- ` PeekAuthTokenClaims ` and ` PeekAuthTokenUser ` ), the ` Logger ` interface +
338+ ` PeekAuthTokenClaims ` , ` PeekAuthTokenUser ` , and the install-webhook
339+ ` InstallWebhookClaims ` /` InstallWebhookAccount ` ), the ` Logger ` interface +
319340` noopLogger ` , and the typed error classes (` AdminAccountRequiredError ` ,
320- ` RateLimitError ` , ` PeekGraphQLError ` , ` PiiAccessDisabledError ` , ` CngApiError ` ,
321- ` AcmeApiError ` ). Query strings and raw response interfaces are deliberately kept
341+ ` RateLimitError ` , ` PeekGraphQLError ` , ` PeekHttpError ` , ` PiiAccessDisabledError ` ,
342+ ` CngApiError ` , ` AcmeApiError ` ). Query strings and raw response interfaces are deliberately kept
322343internal — including the booking-webhook registration query
323344(` BOOKING_WEBHOOK_GQL_QUERY ` stays internal, documented via ` docs/webhooks.md ` ).
324345The webhook-related public exports are the two parsers ` parseBookingWebhook ` and
325- ` parseWaiverWebhook ` (plus the ` Waiver ` model type; see the webhook notes above).
346+ ` parseWaiverWebhook ` plus the install-status verifier ` verifyInstallWebhook `
347+ (and the ` Waiver ` / ` InstallWebhookClaims ` model types; see the webhook notes above).
326348
327349### 5b. CNG accessor (REST)
328350` src/cng-access-service.ts ` , ` src/internal/cng/ ` , ` src/models/cng/product.ts `
@@ -340,8 +362,10 @@ plumbing rather than forking the package.
340362 ` GraphQLClient ` . Builds ` ${baseUrl}/${appId}/${extendableSlug}/${path} ` with
341363 ` extendableSlug = cng_backoffice_api-v1 ` , GETs it with ` X-Peek-Auth: Bearer `
342364 (no ` pk-api-key ` , no ` {query,variables} ` body), and runs through the shared
343- ` requestWithRetry ` loop. Parses the body as JSON, falling back to raw text
344- when unparseable; non-2xx (other than 418/429) → ` CngApiError ` (status + body).
365+ ` requestWithRetry ` loop. Reads the body with the shared ` parseBody ` helper
366+ (` http-transport.ts ` ) — JSON with a raw-text fallback when unparseable — the
367+ same helper the Peek ` GraphQLClient ` and ACME ` RestClient ` use; non-2xx (other
368+ than 418/429) → ` CngApiError ` (status + body).
345369- ** Products triad** (` src/internal/cng/products/ ` ) — same shape as every Peek
346370 resource: ` product-queries.ts ` (raw REST ` ProductNode ` /` ProductsResponse `
347371 interfaces, internal), ` product-converter.ts ` (pure ` fromProductNodes ` →
@@ -465,6 +489,17 @@ Load-bearing rules:
465489 component file). ` package.json ` ` "sideEffects" ` is therefore an allow-list
466490 (` **/ui/** ` , ` **/*.css ` ) rather than ` false ` , so bundlers don't tree-shake the
467491 registrations away.
492+ - ** React 19 safety at registration.** ` define() ` (in ` base.ts ` ) runs
493+ ` addReactSafeSetters ` on the class before ` customElements.define ` : it walks the
494+ component's own prototypes (up to ` OdyElement ` ) and gives every getter-only
495+ accessor a setter, so React 19 — which assigns JSX props as DOM * properties*
496+ (` el.searchable = true ` ) — can't throw ` only a getter ` . When the property name
497+ maps to an ` observedAttribute ` the setter ** reflects** the value onto that
498+ attribute (booleans as presence, objects as JSON, scalars as strings) so the
499+ prop takes effect; otherwise it is a no-op for derived/imperative state
500+ (` isOpen ` ). The static ` .d.ts ` types keep these accessors read-only — the
501+ setters are a runtime-only safety net. Documented for consumers in ` docs/ui.md `
502+ §3.5 and the README.
468503- ** Dependency-free & token-based.** No ` ember-power-select ` /` -calendar ` ,
469504 ` svg-jar ` , or bootstrap. Colours/spacing reference the ` tokens.css ` custom
470505 properties; icons are inlined; button variant colours (which live in a
0 commit comments