@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/README.md +69 -14
- package/dist/index.cjs +102 -39
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +76 -5
- package/dist/index.d.ts +76 -5
- package/dist/index.js +101 -40
- package/dist/index.js.map +1 -1
- package/dist/ui/index.cjs +44 -1
- package/dist/ui/index.cjs.map +1 -1
- package/dist/ui/index.d.cts +10 -1
- package/dist/ui/index.d.ts +10 -1
- package/dist/ui/index.js +44 -1
- package/dist/ui/index.js.map +1 -1
- package/docs/ui.md +30 -6
- package/docs/webhooks.md +80 -9
- package/llms.txt +28 -9
- package/package.json +1 -1
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`
|
|
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
|
|
113
|
-
|
|
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[]` (
|
|
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`.
|
|
5
|
-
|
|
6
|
-
clean data model. They differ in one important
|
|
7
|
-
configured with a GraphQL query (so the package
|
|
8
|
-
register),
|
|
9
|
-
register beyond subscribing to the event)
|
|
10
|
-
|
|
11
|
-
|
|
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
|
-
|
|
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
|
-
`
|
|
153
|
-
|
|
154
|
-
|
|
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
|
|
160
|
-
|
|
161
|
-
string; never
|
|
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`
|