@peektravel/app-utilities 0.5.2 → 0.5.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/docs/ui.md CHANGED
@@ -79,10 +79,10 @@ they work declaratively in plain HTML:
79
79
 
80
80
  | Component | JS property | JSON attribute fallback |
81
81
  |---|---|---|
82
- | `ody-dropdown-single` / `ody-dropdown-multi` | `el.options = [...]` | `options='[{"value":"a","label":"A"}]'` |
82
+ | `ody-dropdown-single` / `ody-dropdown-multi` / `ody-checkbox-group` | `el.options = [...]` | `options='[{"value":"a","label":"A"}]'` |
83
83
  | `ody-tabs` | — | `tabs='[{"id":"a","label":"A"}]'` |
84
84
  | `ody-toggle-button` | — | `options='[{"value":"a","label":"A"}]'` |
85
- | `ody-radio-button-group` / `ody-checkbox-group` | `el.options` (getter) | `options='[{"value":"a","label":"A"}]'` |
85
+ | `ody-radio-button-group` | `el.options` (getter) | `options='[{"value":"a","label":"A"}]'` |
86
86
  | `ody-datepicker` | — | `presets='[{"label":"…","value":"…"}]'` |
87
87
  | `ody-table` | `el.columns`, `el.data` | none — JS only |
88
88
 
@@ -109,8 +109,8 @@ placeholder. So the element's children become its body/label content:
109
109
  **Vanilla HTML / JS** — declarative; set rich data and add listeners via the
110
110
  element reference.
111
111
 
112
- **React** (especially < 19) does not set DOM properties or bind custom events
113
- from JSX attributes. Use lowercase tag names, a `ref` to set properties, and
112
+ **React < 19** does not set DOM properties or bind custom events from JSX
113
+ attributes. Use lowercase tag names, a `ref` to set properties, and
114
114
  `addEventListener` for events:
115
115
 
116
116
  ```jsx
@@ -128,6 +128,30 @@ function Guests({ columns, rows, onSort }) {
128
128
  }
129
129
  ```
130
130
 
131
+ **React 19** sets JSX props on a custom element as DOM **properties**
132
+ (`el.searchable = true`), not attributes. The components are built for this: every
133
+ reflected/read-only accessor accepts assignment, so passing props directly works
134
+ and does **not** throw the `Cannot set property … which has only a getter` error
135
+ a getter-only property would otherwise raise. Concretely:
136
+
137
+ - **Reflected props (booleans, and rich data) take effect.** `searchable`,
138
+ `disabled`, `options`, etc. reflect the assigned value onto the backing
139
+ attribute — booleans as presence, objects/arrays as a JSON string:
140
+
141
+ ```jsx
142
+ <ody-dropdown-single searchable options={items} value={value} />
143
+ ```
144
+
145
+ - **Read-only state accessors ignore assignment.** Props that mirror live
146
+ internal state (`isOpen`, `isVisible`) are inert to writes — they are not
147
+ controlled by re-render. Drive them imperatively through a `ref`
148
+ (`ref.current.openPopover()` / `.show()`) and observe changes via the
149
+ component's `CustomEvent`s.
150
+
151
+ TypeScript still types these accessors as read-only (the setters are a runtime
152
+ safety net for framework-driven assignment), so `el.isOpen = true` in TS code is
153
+ flagged — assign only via JSX props / refs as above.
154
+
131
155
  **Vue / Angular** support custom elements natively: bind properties with
132
156
  `:prop` / `[prop]` and events with `@event` / `(event)`. (Configure Vue's
133
157
  `compilerOptions.isCustomElement` / Angular's `CUSTOM_ELEMENTS_SCHEMA`.)
@@ -616,12 +640,12 @@ are blocked; on blur the value is clamped/normalised.
616
640
  **Use when** several toggles, optionally with a "select all" parent (checked
617
641
  when all selected, indeterminate when some).
618
642
  **Attributes**
619
- - `options` — JSON array of `{ label, value }`.
643
+ - `options` — JSON array of `{ label, value }` (also a JS property, below).
620
644
  - `value` — comma-separated selected values.
621
645
  - `select-all-label` — when present, renders the parent select-all row.
622
646
  - `size` — `base` | `small` (default `small`).
623
647
  - `disabled` — boolean.
624
- **Properties (JS)** — `value: string[]` (get returns array, set takes array); `options: OdyCheckboxOption[]` (read-only getter).
648
+ **Properties (JS)** — `value: string[]` (get returns array, set takes array); `options: OdyCheckboxOption[]` (get parses the attribute, set writes the JSON attribute — assign `el.options = [...]` as with `<ody-dropdown-*>`).
625
649
  **Events** — `change` → `{ value: string[] }`.
626
650
  **Example**
627
651
  ```html
package/docs/webhooks.md CHANGED
@@ -1,19 +1,25 @@
1
1
  # Webhooks
2
2
 
3
3
  A guide for wiring a receiver app up to Peek "backoffice" webhooks using
4
- `@peektravel/app-utilities`. Two webhook types are supported today — **booking**
5
- and **waiver** — and each has a parser that turns the delivered payload into a
6
- clean data model. They differ in one important way: a booking webhook is
7
- configured with a GraphQL query (so the package documents the exact query to
8
- register), while a waiver webhook has a fixed payload (so there's nothing to
9
- register beyond subscribing to the event).
10
-
11
- | Webhook | Register | Parse the delivery |
4
+ `@peektravel/app-utilities`. Three webhook types are supported today —
5
+ **booking**, **waiver**, and **install-status** — and each has a helper that
6
+ turns the delivered payload into a clean data model. They differ in one important
7
+ way: a booking webhook is configured with a GraphQL query (so the package
8
+ documents the exact query to register), a waiver webhook has a fixed payload (so
9
+ there's nothing to register beyond subscribing to the event), and an
10
+ install-status webhook delivers a **signed token** (so the helper *verifies* it
11
+ rather than just parsing it).
12
+
13
+ | Webhook | Register | Handle the delivery |
12
14
  | --- | --- | --- |
13
15
  | Booking | paste a GraphQL query into the external config (below) | `parseBookingWebhook(body)` → `Booking` |
14
16
  | Waiver | subscribe to the event — **no query needed** | `parseWaiverWebhook(body)` → `Waiver` |
17
+ | Install-status | subscribe to the event — **no query needed** | `verifyInstallWebhook(token, secret)` → `InstallWebhookClaims` |
15
18
 
16
- Both parsers are pure transforms — no auth, no network, construct nothing.
19
+ The booking and waiver parsers are pure transforms — no auth, no network,
20
+ construct nothing; **authenticating those deliveries is the receiver's job**. The
21
+ install-status webhook is different: its payload *is* a signed JWT, so
22
+ `verifyInstallWebhook` checks the signature for you.
17
23
 
18
24
  # Booking webhooks
19
25
 
@@ -147,3 +153,68 @@ The resulting `Waiver` is flat:
147
153
  | `isOptinSms` | `boolean` | `waiver_data.participant_optin_sms` |
148
154
 
149
155
  Authenticating the delivery is the receiver's responsibility, as with bookings.
156
+
157
+ # Install-status webhooks
158
+
159
+ ## The problem this solves
160
+
161
+ Peek's app registry notifies your app of install lifecycle changes — installed,
162
+ uninstalled, status changed — by POSTing a **signed `app_registry_v2` JWT**.
163
+ Unlike a booking or waiver webhook, the whole payload *is* the token, so
164
+ verifying its signature is the point: it proves the notification really came from
165
+ Peek before your app acts on it (for example, de-provisioning an uninstall).
166
+ Hand-rolling that check (issuer, audience, expiry, HMAC) is exactly the
167
+ security-critical boilerplate `verifyInstallWebhook` removes.
168
+
169
+ ## Step 1 — subscribe to the event (external, one-time)
170
+
171
+ Nothing to register in code: subscribe your endpoint to the install-status event
172
+ in the App Store config. There is no GraphQL selection to paste.
173
+
174
+ ## Step 2 — verify the delivered token
175
+
176
+ Extract the JWT from the delivery (the request body, or the `Authorization`
177
+ header with the `Bearer ` prefix stripped) and pass it — plus your app's signing
178
+ secret (the same `jwtSecret` you construct `PeekAccessService` with) — to
179
+ `verifyInstallWebhook`:
180
+
181
+ ```ts
182
+ import { verifyInstallWebhook, type InstallWebhookClaims } from "@peektravel/app-utilities";
183
+
184
+ app.post("/webhooks/install", (req, res) => {
185
+ let claims: InstallWebhookClaims;
186
+ try {
187
+ claims = verifyInstallWebhook(req.body /* raw JWT string */, process.env.PEEK_INTERNAL_SECRET!);
188
+ } catch {
189
+ return res.sendStatus(401); // signature/issuer/audience/expiry failed
190
+ }
191
+
192
+ if (claims.status === "uninstalled") {
193
+ // tear down this install's resources
194
+ }
195
+ res.sendStatus(200);
196
+ });
197
+ ```
198
+
199
+ `verifyInstallWebhook` validates the HMAC signature, the token expiry, the
200
+ `"app_registry_v2"` issuer, and the `"Joken"` audience — the same checks as
201
+ [`PeekAccessService.verifyPeekAuthToken`](../README.md#access-options--pii) — then
202
+ maps the payload to the clean `InstallWebhookClaims`. It throws the underlying
203
+ `jsonwebtoken` error (`JsonWebTokenError` / `TokenExpiredError` /
204
+ `NotBeforeError`) on any verification failure, so a single `try/catch` → `401`
205
+ covers every bad-token case.
206
+
207
+ The signature checks are strict (they are the security boundary); claim
208
+ *extraction* is defensive, so a validly-signed token that omits an optional field
209
+ yields an empty value rather than throwing. The resulting `InstallWebhookClaims`:
210
+
211
+ | Field | Type | From | Notes |
212
+ | --- | --- | --- | --- |
213
+ | `installId` | `string` | `sub` | Peek-assigned install UUID |
214
+ | `account.id` | `string` | `account.id` | account that owns the install |
215
+ | `status` | `string` | `status` | e.g. `"installed"`, `"uninstalled"` |
216
+ | `displayVersion` | `string` | `display_version` | `""` when absent |
217
+ | `user` | `PeekAuthTokenUser \| null` | `user` | `null` for system-initiated events |
218
+
219
+ Unlike `verifyPeekAuthToken`, `user` is nullable: install lifecycle events are
220
+ often system-initiated (no acting user), so the verifier tolerates `user: null`.
package/llms.txt CHANGED
@@ -148,18 +148,21 @@ stay internal.
148
148
  ## Errors
149
149
 
150
150
  - `AdminAccountRequiredError` (HTTP 418), `RateLimitError` (HTTP 429 after
151
- retries), `PeekGraphQLError` (`.graphqlErrors` holds the raw array),
152
- `PiiAccessDisabledError` (a payment/booking-modification op was called without
153
- `fullCustomerAccess`), and the REST-sibling `CngApiError` / `AcmeApiError` —
154
- all importable; branch with `instanceof`.
151
+ retries), `PeekGraphQLError` (`.graphqlErrors` holds the raw array — a
152
+ resolver-level failure), `PeekHttpError` (a non-2xx HTTP status other than
153
+ 418/429 with no GraphQL `errors` — e.g. 401/404/5xx; carries `.statusCode`,
154
+ `.url`, and raw `.body`; surfaced before JSON parsing so non-JSON error pages
155
+ keep their status), `PiiAccessDisabledError` (a payment/booking-modification op
156
+ was called without `fullCustomerAccess`), and the REST-sibling `CngApiError` /
157
+ `AcmeApiError` — all importable; branch with `instanceof`.
155
158
  - Plain `Error` for validation/precondition failures thrown before any request.
156
159
 
157
- ## Webhooks (booking + waiver)
160
+ ## Webhooks (booking + waiver + install-status)
158
161
 
159
- Two webhook parsers, each a pure transform — needs no auth/network/
160
- `PeekAccessService`; accepts the `{<key>:…}` envelope, a bare node, or a JSON
161
- string; never throws on malformed input. Registration is done **once in an
162
- external system** (App Store config), not from code.
162
+ Two pure-transform parsers plus one signature-verifier. The parsers need no
163
+ auth/network/`PeekAccessService`; accept the `{<key>:…}` envelope, a bare node,
164
+ or a JSON string; never throw on malformed input. Registration is done **once in
165
+ an external system** (App Store config), not from code.
163
166
 
164
167
  - `parseBookingWebhook(body): Booking` — auto-detects guests/breakdown. Booking
165
168
  payload shape is defined by a GraphQL selection registered externally; paste
@@ -171,6 +174,15 @@ external system** (App Store config), not from code.
171
174
  register** — just subscribe to the `agreement_signature_created` event. Takes
172
175
  the same `AccessOptions`; by default (`fullCustomerAccess` off) `guestName` and
173
176
  `fileUrl` are redacted — pass `{ fullCustomerAccess: true }` to keep them.
177
+ - `verifyInstallWebhook(token, secret): InstallWebhookClaims` — for install
178
+ lifecycle events (installed/uninstalled/status changed). The payload *is* a
179
+ signed `app_registry_v2` JWT, so this **verifies** it (HMAC signature + expiry +
180
+ `app_registry_v2` issuer + `Joken` audience — same checks as
181
+ `verifyPeekAuthToken`) rather than just parsing. Returns clean claims
182
+ (`installId`, `account.id`, `status`, `displayVersion`, and `user` which is
183
+ **nullable** for system-initiated events). Throws the underlying `jsonwebtoken`
184
+ error on any failure — one `try/catch` → `401`. `secret` is the app's
185
+ `jwtSecret`. No query to register; just subscribe to the event.
174
186
 
175
187
  **Full guide + booking query-to-register: `docs/webhooks.md` (shipped).**
176
188
 
@@ -194,6 +206,13 @@ Conventions: scalar config = HTML **attributes**; rich data = JS **properties**
194
206
  accept JSON-string attrs like `options='[…]'` / `tabs='[…]'`); output = bubbling
195
207
  **`CustomEvent`s** (read `event.detail`); content = light-DOM children.
196
208
 
209
+ React 19: JSX props are set as DOM **properties**. Safe by design — every
210
+ getter-only accessor accepts assignment (registration adds a setter): reflected
211
+ props (`searchable`/`disabled`/`options`) reflect onto the attribute and take
212
+ effect; read-only state props (`isOpen`/`isVisible`) ignore the write (drive them
213
+ via a `ref`'s imperative methods). No `only a getter` crash. TS still types them
214
+ read-only — the setters are a runtime safety net.
215
+
197
216
  Tags by tier (see `docs/ui.md` for attributes/events/examples per component):
198
217
  - display: `ody-button`, `ody-tag`, `ody-alert`, `ody-card`, `ody-divider`,
199
218
  `ody-status-dot`, `ody-message`, `ody-icon`, `ody-loading-spinner`, `ody-loading-bar`
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@peektravel/app-utilities",
3
- "version": "0.5.2",
3
+ "version": "0.5.4",
4
4
  "description": "GraphQL JS mapping utilities extracted from the Peek Pro Autopilot connector.",
5
5
  "license": "MIT",
6
6
  "repository": {