@peektravel/app-utilities 0.7.1 → 0.7.2
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 +14 -5
- package/dist/index.cjs +54 -20
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +171 -38
- package/dist/index.d.ts +171 -38
- package/dist/index.js +54 -21
- package/dist/index.js.map +1 -1
- package/dist/ui/index.cjs +26 -0
- package/dist/ui/index.cjs.map +1 -1
- package/dist/ui/index.d.cts +77 -3
- package/dist/ui/index.d.ts +77 -3
- package/dist/ui/index.js +25 -1
- package/dist/ui/index.js.map +1 -1
- package/dist/ui/odyssey.css +24 -2
- package/dist/ui/tokens.css +10 -1
- package/docs/ui.md +63 -14
- package/docs/webhooks.md +109 -26
- package/llms.txt +33 -13
- package/package.json +1 -1
package/dist/ui/odyssey.css
CHANGED
|
@@ -10,13 +10,13 @@
|
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
12
|
/* ---- base ---------------------------------------------------------------- */
|
|
13
|
-
ody-icon, ody-button, ody-tag, ody-alert, ody-card, ody-divider,
|
|
13
|
+
ody-icon, ody-button, ody-tag, ody-alert, ody-card, ody-divider, ody-horizontal-divider,
|
|
14
14
|
ody-status-dot, ody-message, ody-loading-spinner, ody-loading-bar {
|
|
15
15
|
font-family: var(--ody-font-family);
|
|
16
16
|
box-sizing: border-box;
|
|
17
17
|
}
|
|
18
18
|
ody-button, ody-tag, ody-status-dot, ody-message, ody-loading-spinner { display: inline-block; }
|
|
19
|
-
ody-alert, ody-card, ody-divider, ody-loading-bar { display: block; }
|
|
19
|
+
ody-alert, ody-card, ody-divider, ody-horizontal-divider, ody-loading-bar { display: block; }
|
|
20
20
|
|
|
21
21
|
@keyframes ody-spin { from { transform: rotate(0); } to { transform: rotate(360deg); } }
|
|
22
22
|
.ody-spin { animation: ody-spin 1s linear infinite; }
|
|
@@ -183,6 +183,10 @@ ody-alert, ody-card, ody-divider, ody-loading-bar { display: block; }
|
|
|
183
183
|
|
|
184
184
|
/* ---- divider ------------------------------------------------------------- */
|
|
185
185
|
.ody-divider { display: block; width: 100%; height: 1px; background-color: var(--color-neutral-200); }
|
|
186
|
+
/* horizontal-divider — padded successor to divider; carries its own rhythm. */
|
|
187
|
+
.ody-horizontal-divider { display: block; width: 100%; height: 1px; background-color: var(--color-neutral-200); margin-block: var(--gap16); }
|
|
188
|
+
.ody-horizontal-divider--tight { margin-block: var(--gap8); }
|
|
189
|
+
.ody-horizontal-divider--none { margin-block: 0; }
|
|
186
190
|
|
|
187
191
|
/* ---- status-dot ---------------------------------------------------------- */
|
|
188
192
|
.status-dot-container { color: var(--color-neutral-350); font-size: 14px; display: inline-flex; align-items: center; }
|
|
@@ -385,6 +389,24 @@ ody-percentage-input, ody-checkbox, ody-radio-button-group, ody-checkbox-group {
|
|
|
385
389
|
ody-page-container { display: block; width: 100%; }
|
|
386
390
|
.ody-page-container { width: 100%; max-width: 1310px; margin: 0 auto; container-type: inline-size; container-name: ody-page; }
|
|
387
391
|
|
|
392
|
+
/* ===== app-page-container (padded successor to page-container) ===== */
|
|
393
|
+
/* The container context is on the host so the padded inner div is a descendant
|
|
394
|
+
of the `ody-page` container and its gutter can respond to the container's own
|
|
395
|
+
width (an element cannot query the container it establishes). */
|
|
396
|
+
ody-app-page-container {
|
|
397
|
+
display: block;
|
|
398
|
+
width: 100%;
|
|
399
|
+
max-width: var(--layout-page-width-wide);
|
|
400
|
+
margin: 0 auto;
|
|
401
|
+
container-type: inline-size;
|
|
402
|
+
container-name: ody-page;
|
|
403
|
+
}
|
|
404
|
+
.ody-app-page-container { box-sizing: border-box; width: 100%; padding: var(--gap24); }
|
|
405
|
+
@container ody-page (max-width: 868px) {
|
|
406
|
+
.ody-app-page-container { padding: var(--gap16); }
|
|
407
|
+
}
|
|
408
|
+
.ody-app-page-container--flush { padding: 0; }
|
|
409
|
+
|
|
388
410
|
/* ===== collapsible-section ===== */
|
|
389
411
|
.ody-collapsible-section .ody-collapsible-container .ody-collapsible { margin: 0; }
|
|
390
412
|
.ody-collapsible-section .ody-collapsible__header { display: flex; flex-wrap: wrap; justify-content: space-between; align-items: center; width: 100%; background: none; border: none; padding: 0; cursor: pointer; text-align: left; }
|
package/dist/ui/tokens.css
CHANGED
|
@@ -98,7 +98,16 @@
|
|
|
98
98
|
--ody-z-wormhole: 2002;
|
|
99
99
|
--ody-z-tooltip: 2004;
|
|
100
100
|
|
|
101
|
-
/*
|
|
101
|
+
/* spacing scale — the canonical gaps used across the design system. Prefer
|
|
102
|
+
these over raw px so spacing stays consistent across apps. */
|
|
103
|
+
--gap8: 8px;
|
|
104
|
+
--gap16: 16px;
|
|
105
|
+
--gap24: 24px;
|
|
106
|
+
--gap32: 32px;
|
|
107
|
+
|
|
108
|
+
/* layout — the two canonical settings-iframe widths the host renders at. */
|
|
109
|
+
--layout-page-width-narrow: 868px;
|
|
110
|
+
--layout-page-width-wide: 1310px;
|
|
102
111
|
--layout-breadcrumb-height: 26px;
|
|
103
112
|
--layout-top-bar-height: 56px;
|
|
104
113
|
}
|
package/docs/ui.md
CHANGED
|
@@ -50,6 +50,29 @@ import {
|
|
|
50
50
|
} from '@peektravel/app-utilities/ui';
|
|
51
51
|
```
|
|
52
52
|
|
|
53
|
+
### 2.1 Spacing & layout tokens
|
|
54
|
+
|
|
55
|
+
`tokens.css` publishes the canonical spacing scale and the two page widths.
|
|
56
|
+
Reach for these instead of magic numbers so spacing stays consistent across
|
|
57
|
+
apps even for one-off gaps:
|
|
58
|
+
|
|
59
|
+
| Token | Value | Use for |
|
|
60
|
+
| --- | --- | --- |
|
|
61
|
+
| `--gap8` | `8px` | tight gaps |
|
|
62
|
+
| `--gap16` | `16px` | the default gap between elements |
|
|
63
|
+
| `--gap24` | `24px` | section-level spacing / page gutters |
|
|
64
|
+
| `--gap32` | `32px` | large section spacing |
|
|
65
|
+
| `--layout-page-width-narrow` | `868px` | the narrow settings-iframe width (design for this first) |
|
|
66
|
+
| `--layout-page-width-wide` | `1310px` | the wide settings-iframe width |
|
|
67
|
+
|
|
68
|
+
```css
|
|
69
|
+
.my-card { padding: var(--gap24); gap: var(--gap16); }
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
The layout primitives already bake these in: `ody-app-page-container` uses the
|
|
73
|
+
gap tokens for its responsive gutter, and `ody-section-*` / `ody-horizontal-divider`
|
|
74
|
+
expose them through `gap-size` / `spacing` attributes.
|
|
75
|
+
|
|
53
76
|
---
|
|
54
77
|
|
|
55
78
|
## 3. Core conventions (read this — it is how all components work)
|
|
@@ -208,7 +231,7 @@ flagged — assign only via JSX props / refs as above.
|
|
|
208
231
|
| Empty / error / no-results view | `ody-empty-state` |
|
|
209
232
|
| KPI figures | `ody-stat-summary` + `ody-stat` |
|
|
210
233
|
| Expand/collapse | `ody-accordion`, `ody-collapsible`, `ody-collapsible-section` |
|
|
211
|
-
| Full-page wrapper for an **app settings UI** | `ody-page-container` (required — see below) |
|
|
234
|
+
| Full-page wrapper for an **app settings UI** | `ody-app-page-container` (required — see below; `ody-page-container` is the deprecated full-bleed predecessor) |
|
|
212
235
|
| Master/detail page layout | `ody-two-column` |
|
|
213
236
|
| Row/column section layout | `ody-section-rows` / `ody-section-columns` |
|
|
214
237
|
| Progress | `ody-loading-bar`, `ody-loading-spinner` |
|
|
@@ -299,8 +322,21 @@ carries meaning (e.g. status).
|
|
|
299
322
|
<ody-card bar-color="var(--color-interaction-300)"><p>Card body</p></ody-card>
|
|
300
323
|
```
|
|
301
324
|
|
|
302
|
-
#### `<ody-divider>`
|
|
325
|
+
#### `<ody-horizontal-divider>`
|
|
326
|
+
**Use when** you need a 1px horizontal rule that separates content. Unlike the
|
|
327
|
+
deprecated `<ody-divider>`, it carries its own vertical rhythm
|
|
328
|
+
(`margin-block: var(--gap16)`) so you don't have to add spacing around it.
|
|
329
|
+
**Attributes**
|
|
330
|
+
- `spacing` — `tight` (`--gap8`) | `none` (`0`); default is the normal `--gap16`.
|
|
331
|
+
```html
|
|
332
|
+
<ody-horizontal-divider></ody-horizontal-divider>
|
|
333
|
+
<ody-horizontal-divider spacing="tight"></ody-horizontal-divider>
|
|
334
|
+
```
|
|
335
|
+
|
|
336
|
+
#### `<ody-divider>` _(deprecated — use `<ody-horizontal-divider>`)_
|
|
303
337
|
**Use when** you need a 1px horizontal rule. No attributes, no content.
|
|
338
|
+
**Deprecated:** renders with **zero** margin, so every caller has to add its own
|
|
339
|
+
spacing. Prefer `<ody-horizontal-divider>`. Still works and unchanged.
|
|
304
340
|
```html
|
|
305
341
|
<ody-divider></ody-divider>
|
|
306
342
|
```
|
|
@@ -497,11 +533,11 @@ Extra content (e.g. a tag) is the child content.
|
|
|
497
533
|
</ody-two-column>
|
|
498
534
|
```
|
|
499
535
|
|
|
500
|
-
#### `<ody-page-container>`
|
|
536
|
+
#### `<ody-app-page-container>`
|
|
501
537
|
> [!IMPORTANT]
|
|
502
|
-
> **Every app settings UI must wrap its content in `<ody-page-container>`.**
|
|
503
|
-
> the standard responsive page wrapper that keeps the settings page sized
|
|
504
|
-
> two widths the settings host iframe renders at — **868px** (narrow) and
|
|
538
|
+
> **Every app settings UI must wrap its content in `<ody-app-page-container>`.**
|
|
539
|
+
> It is the standard responsive page wrapper that keeps the settings page sized
|
|
540
|
+
> to the two widths the settings host iframe renders at — **868px** (narrow) and
|
|
505
541
|
> **1310px** (wide).
|
|
506
542
|
>
|
|
507
543
|
> **Design for 868px first — it is the default.** Optimise every settings layout
|
|
@@ -514,20 +550,24 @@ Extra content (e.g. a tag) is the child content.
|
|
|
514
550
|
full-page UI embedded in the settings host).
|
|
515
551
|
The container is `width: 100%`, so it fills whichever of the two widths the
|
|
516
552
|
parent iframe gives it; a `max-width` of `1310px` caps and centres it if it is
|
|
517
|
-
ever embedded somewhere wider.
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
553
|
+
ever embedded somewhere wider. Unlike the deprecated `<ody-page-container>`, it
|
|
554
|
+
ships a **default responsive gutter** — `var(--gap24)` at the wide width,
|
|
555
|
+
tightening to `var(--gap16)` at/below 868px — so you no longer wrap the page in
|
|
556
|
+
a padded `<div>`. It also establishes a CSS **container context** named
|
|
557
|
+
`ody-page`, so the content inside can adapt between the two widths with container
|
|
558
|
+
queries instead of viewport media queries — treat 868px as the base styles and
|
|
559
|
+
use a container query to *enhance* for the wider view.
|
|
560
|
+
**Attributes**
|
|
561
|
+
- `flush` — drop the default gutter and render edge-to-edge, for the rare layout
|
|
562
|
+
that truly wants full-bleed content.
|
|
523
563
|
**Slots / content** — child nodes are the page content.
|
|
524
564
|
**Example**
|
|
525
565
|
```html
|
|
526
|
-
<ody-page-container>
|
|
566
|
+
<ody-app-page-container>
|
|
527
567
|
<ody-section-rows gap-size="24">
|
|
528
568
|
…settings UI…
|
|
529
569
|
</ody-section-rows>
|
|
530
|
-
</ody-page-container>
|
|
570
|
+
</ody-app-page-container>
|
|
531
571
|
```
|
|
532
572
|
```css
|
|
533
573
|
/* Base styles target the default 868px width… */
|
|
@@ -538,6 +578,15 @@ container query to *enhance* for the wider view.
|
|
|
538
578
|
}
|
|
539
579
|
```
|
|
540
580
|
|
|
581
|
+
#### `<ody-page-container>` _(deprecated — use `<ody-app-page-container>`)_
|
|
582
|
+
**Deprecated:** the original page wrapper is **full-bleed** (edge-to-edge, no
|
|
583
|
+
side gutters), so every settings UI had to add its own edge padding. Prefer
|
|
584
|
+
`<ody-app-page-container>`, which bakes in a responsive gutter (with a `flush`
|
|
585
|
+
opt-out) while keeping the same widths and `ody-page` container context. This
|
|
586
|
+
element still works and is unchanged; migrate by renaming the tag (add `flush`
|
|
587
|
+
if you genuinely relied on the full-bleed behaviour).
|
|
588
|
+
**Attributes** — none; the width is driven entirely by the parent.
|
|
589
|
+
|
|
541
590
|
#### `<ody-collapsible-section>` + `<ody-collapsible-collapsed>` + `<ody-collapsible-content>`
|
|
542
591
|
**Use when** a titled section that expands/collapses on header click.
|
|
543
592
|
- `ody-collapsible-section` **Attributes**: `header-text`, `secondary-header-text`, `icon` (icon name), `expanded` (boolean; reflected). **Events**: `expanded` → `{ expanded }`.
|
package/docs/webhooks.md
CHANGED
|
@@ -22,13 +22,12 @@ install webhook is different: it carries a signed JWT, so `parseInstallWebhook`
|
|
|
22
22
|
checks the signature for you before returning the merged event.
|
|
23
23
|
|
|
24
24
|
**The install webhook delivers two payloads at once.** A signed
|
|
25
|
-
`app_registry_v2` JWT authenticates the notification
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
`parseInstallWebhook` verifies the token and
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
test.
|
|
25
|
+
`app_registry_v2` JWT authenticates the notification; a plain JSON body alongside
|
|
26
|
+
it carries the **event data** — the full account block (name, platform,
|
|
27
|
+
timezone, test flag), the per-install `api.url`, the acting user, and the install
|
|
28
|
+
id / status / version. `parseInstallWebhook` verifies the token and reads the
|
|
29
|
+
event from the body — falling back to the token for the fields it also carries —
|
|
30
|
+
merging both into one flat `InstallWebhook`, so you make a single call.
|
|
32
31
|
|
|
33
32
|
# Booking webhooks
|
|
34
33
|
|
|
@@ -173,10 +172,11 @@ a **signed `app_registry_v2` JWT** and a plain **JSON body**.
|
|
|
173
172
|
|
|
174
173
|
- The **JWT** is the security boundary. Verifying its signature proves the
|
|
175
174
|
notification really came from Peek before your app acts on it (for example,
|
|
176
|
-
de-provisioning an uninstall)
|
|
177
|
-
|
|
178
|
-
- The **JSON body** is the
|
|
179
|
-
|
|
175
|
+
de-provisioning an uninstall) — it authenticates the whole delivery, body
|
|
176
|
+
included.
|
|
177
|
+
- The **JSON body** is the event payload. It carries the full account block
|
|
178
|
+
(name, platform, timezone, test flag), the per-install `api.url`, the acting
|
|
179
|
+
user, and the install id / status / version.
|
|
180
180
|
|
|
181
181
|
This webhook is also where an app first learns **who the install belongs to** —
|
|
182
182
|
and the only place it ever learns it: no GraphQL read returns the account id, so
|
|
@@ -184,9 +184,9 @@ whatever you persist from this delivery is all you will ever have.
|
|
|
184
184
|
|
|
185
185
|
`parseInstallWebhook(token, body, secret)` verifies the token (issuer, audience,
|
|
186
186
|
expiry, HMAC — the security-critical boilerplate you'd otherwise hand-roll) and
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
187
|
+
reads the event from the body, **falling back to the token** for the fields it
|
|
188
|
+
also carries (`installId`, `accountId`, `status`, `displayVersion`, `user`),
|
|
189
|
+
returning one flat, JSON-safe `InstallWebhook` you store as a unit.
|
|
190
190
|
|
|
191
191
|
## Step 1 — subscribe to the event (external, one-time)
|
|
192
192
|
|
|
@@ -249,21 +249,25 @@ The resulting `InstallWebhook` is flat:
|
|
|
249
249
|
|
|
250
250
|
| Field | Type | Source | From | Notes |
|
|
251
251
|
| --- | --- | --- | --- | --- |
|
|
252
|
-
| `installId` | `string` | token | `sub` | Peek-assigned install UUID |
|
|
253
|
-
| `accountId` | `string` | token | `account.id` | **also called the partner ID** |
|
|
252
|
+
| `installId` | `string` | body → token | `install_id` / `sub` | Peek-assigned install UUID |
|
|
253
|
+
| `accountId` | `string` | body → token | `account.id` | **also called the partner ID** |
|
|
254
254
|
| `accountName` | `string` | body | `account.name` | partner display name; `""` when absent |
|
|
255
255
|
| `platform` | `PeekPlatform \| null` | body | `account.platform` | **persist per install** — see below; `null` when unrecognised |
|
|
256
256
|
| `isTest` | `boolean` | body | `account.is_test` | defaults to `false` |
|
|
257
|
-
| `
|
|
258
|
-
| `
|
|
259
|
-
| `
|
|
260
|
-
| `
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
`
|
|
266
|
-
|
|
257
|
+
| `timezone` | `string` | body | `account.timezone` | IANA zone (e.g. `America/New_York`); **persist per install**; `""` when absent |
|
|
258
|
+
| `apiUrl` | `string` | body | `api.url` | per-install backoffice API base URL; **persist per install**; `""` when absent |
|
|
259
|
+
| `status` | `InstallStatus \| null` | body → token | `status` | `null` when unrecognised — see below |
|
|
260
|
+
| `rawStatus` | `string` | body → token | `status` | always the wire value, for logging |
|
|
261
|
+
| `displayVersion` | `string` | body → token | `display_version` | `""` when absent |
|
|
262
|
+
| `user` | `PeekAuthTokenUser \| null` | body → token | `modified_by` / `user` | `null` for system-initiated events |
|
|
263
|
+
|
|
264
|
+
The **JSON body is the source of the event data**; the fields it shares with the
|
|
265
|
+
token (`installId`, `accountId`, `status`, `displayVersion`, `user`) fall back to
|
|
266
|
+
the **verified token** when the body omits them. The token authenticates the
|
|
267
|
+
whole delivery — only Peek can mint a valid one — so the body is trusted within a
|
|
268
|
+
verified request. Unlike `verifyPeekAuthToken`, `user` is nullable: install
|
|
269
|
+
lifecycle events are often system-initiated (no acting user), so the parser
|
|
270
|
+
tolerates a missing `modified_by`/`user`.
|
|
267
271
|
|
|
268
272
|
> **Migrating from `verifyInstallWebhook` / `parseInstallEvent`?**
|
|
269
273
|
> `verifyInstallWebhook(token, secret)` still works but is `@deprecated` — it
|
|
@@ -288,6 +292,85 @@ Two installs of the same app can sit on different platforms, so this cannot be
|
|
|
288
292
|
inferred from app-level config — it is per install, it has no source other than
|
|
289
293
|
this event, and it must be stored alongside `installId` and `accountId`.
|
|
290
294
|
|
|
295
|
+
## Also persist `timezone` and `apiUrl` per install
|
|
296
|
+
|
|
297
|
+
Like `platform`, these are per-install facts with **no source other than this
|
|
298
|
+
event**, so persist them alongside `installId`/`accountId`:
|
|
299
|
+
|
|
300
|
+
- **`timezone`** — the account's IANA zone (e.g. `America/New_York`). Downstream
|
|
301
|
+
date/time handling for the install (scheduling, day boundaries, display) needs
|
|
302
|
+
the account's own zone, not the server's.
|
|
303
|
+
- **`apiUrl`** — the **app endpoint URL** the registry serves this install from.
|
|
304
|
+
Use it **as given** for this install's API calls: hit it unmodified, do not
|
|
305
|
+
decompose it or append your own app id. If the registry tags traffic with an
|
|
306
|
+
app id in the path, that is the registry's concern and opaque to you. Store it
|
|
307
|
+
and pass it as the access service's **`apiUrl`** (for Peek it is the sole
|
|
308
|
+
request URL; for CNG/ACME it is the base and the REST path is appended) — or
|
|
309
|
+
hand the whole install to `createAccessServiceForInstall` (below), which wires
|
|
310
|
+
it for you.
|
|
311
|
+
|
|
312
|
+
Both default to `""` when a delivery omits them, so treat empty as "not
|
|
313
|
+
reported" and keep any value you previously stored.
|
|
314
|
+
|
|
315
|
+
## Every install event is a full snapshot — upsert by `installId`
|
|
316
|
+
|
|
317
|
+
An install webhook is not just a one-time "who is this install" signal. **Every
|
|
318
|
+
event carries the complete, current record**, and an `update_installed` event
|
|
319
|
+
delivers the *same fields* as the original `installed` event. So the registry
|
|
320
|
+
uses the webhook to push changes: a new `apiUrl`, a renamed account, a new
|
|
321
|
+
`displayVersion`, a platform move.
|
|
322
|
+
|
|
323
|
+
Treat each delivery as an **upsert keyed by `installId`** and overwrite your
|
|
324
|
+
stored fields with the incoming ones:
|
|
325
|
+
|
|
326
|
+
```ts
|
|
327
|
+
const event = parseInstallWebhook(token, req.body, secret);
|
|
328
|
+
await installs.upsert(event.installId, {
|
|
329
|
+
accountId: event.accountId,
|
|
330
|
+
accountName: event.accountName,
|
|
331
|
+
platform: event.platform,
|
|
332
|
+
isTest: event.isTest,
|
|
333
|
+
timezone: event.timezone,
|
|
334
|
+
apiUrl: event.apiUrl, // may change between events — always take the latest
|
|
335
|
+
displayVersion: event.displayVersion,
|
|
336
|
+
});
|
|
337
|
+
```
|
|
338
|
+
|
|
339
|
+
A consumer that reads only the first `installed` event and ignores later
|
|
340
|
+
`update_installed` deliveries will keep stale data — most importantly a stale
|
|
341
|
+
`apiUrl`, which would send its API calls to the wrong endpoint.
|
|
342
|
+
|
|
343
|
+
## Build the install's access service from `platform` + `apiUrl`
|
|
344
|
+
|
|
345
|
+
A persisted install carries the two facts needed to reach it: `platform` picks
|
|
346
|
+
the access service class, and `apiUrl` is its endpoint. `createAccessServiceForInstall`
|
|
347
|
+
encodes that mapping, so you go from a stored install to the right client in one
|
|
348
|
+
call — no `switch` on platform, no URL wiring:
|
|
349
|
+
|
|
350
|
+
```ts
|
|
351
|
+
import { createAccessServiceForInstall } from "@peektravel/app-utilities";
|
|
352
|
+
|
|
353
|
+
const install = await installs.get(installId); // { platform, apiUrl, installId, … }
|
|
354
|
+
|
|
355
|
+
const service = createAccessServiceForInstall(install, {
|
|
356
|
+
jwtSecret: process.env.PEEK_INTERNAL_SECRET!,
|
|
357
|
+
issuer: process.env.APP_NAME!,
|
|
358
|
+
// gatewayKey?, logger?, fetch?, accessOptions?, … (per-app, not per-install)
|
|
359
|
+
});
|
|
360
|
+
// → PeekAccessService | CngAccessService | AcmeAccessService, wired to install.apiUrl.
|
|
361
|
+
// Narrow by install.platform (or `instanceof`) to reach a platform-specific surface.
|
|
362
|
+
```
|
|
363
|
+
|
|
364
|
+
It throws if `install.platform` is `null` or unrecognised — an install with an
|
|
365
|
+
unknown platform has no service to route to, and silently defaulting would point
|
|
366
|
+
it at the wrong gateway.
|
|
367
|
+
|
|
368
|
+
Prefer this (or the config's `apiUrl` field) over `baseUrl`/`appId`, which are
|
|
369
|
+
deprecated: they reconstruct the URL with a hardcoded gateway default that cannot
|
|
370
|
+
be correct for every install. **The hardcoded base-URL fallbacks will be removed
|
|
371
|
+
and a URL will become required in a future release** — source it from the install
|
|
372
|
+
webhook's `apiUrl` now.
|
|
373
|
+
|
|
291
374
|
## Why `status` and `platform` can be `null`
|
|
292
375
|
|
|
293
376
|
`parseInstallWebhook` never throws on the *body* (only token verification can
|
package/llms.txt
CHANGED
|
@@ -22,14 +22,24 @@ const peek = new PeekAccessService({
|
|
|
22
22
|
installId, // JWT subject
|
|
23
23
|
jwtSecret, // HMAC secret signing the JWT
|
|
24
24
|
issuer, // app name (JWT issuer)
|
|
25
|
-
|
|
26
|
-
gatewayKey,
|
|
27
|
-
// optional: baseUrl, tokenTtlSeconds, tokenRefreshLeewaySeconds,
|
|
25
|
+
apiUrl, // the install webhook's apiUrl — the app endpoint, used as given
|
|
26
|
+
// optional: gatewayKey, tokenTtlSeconds, tokenRefreshLeewaySeconds,
|
|
28
27
|
// retryDelaysMs, logger, fetch, itemOptionsPageSize,
|
|
29
28
|
// accessOptions: { fullCustomerAccess: false } // default — see "Access options / PII"
|
|
30
29
|
});
|
|
31
30
|
```
|
|
32
31
|
|
|
32
|
+
Targeting an install by URL: pass the install webhook's `apiUrl` as the config
|
|
33
|
+
`apiUrl` — for Peek it is the **sole** request URL (every call POSTs to it,
|
|
34
|
+
unmodified); for CNG/ACME it is the base and the REST path is appended. No
|
|
35
|
+
app-id/gateway segment is inserted (`appId` is unused). Any app id in the path is
|
|
36
|
+
the registry's own traffic tag, opaque to callers. `createAccessServiceForInstall(install, config)`
|
|
37
|
+
builds the right service for `install.platform` (`peek`/`cng`/`acme`) wired to
|
|
38
|
+
`install.apiUrl` in one call (throws on unknown/`null` platform). `appId` +
|
|
39
|
+
`baseUrl` (and the hardcoded gateway default) are **`@deprecated`** — they
|
|
40
|
+
reconstruct the URL with a compiled-in default that can't be right for every
|
|
41
|
+
install; the fallbacks will be removed and a URL will become **required**.
|
|
42
|
+
|
|
33
43
|
## Access options / PII — `fullCustomerAccess`
|
|
34
44
|
|
|
35
45
|
**The most commonly-misunderstood option in this package. Read this before
|
|
@@ -124,8 +134,10 @@ final authority and may still reject the request.
|
|
|
124
134
|
Two brand-parallel accessors sit alongside `PeekAccessService` and share the same
|
|
125
135
|
auth/transport/UI plumbing. Both are **REST** and deliberately minimal today
|
|
126
136
|
(products only). Construct one per install, same config shape **minus `gatewayKey`**
|
|
127
|
-
(`installId`, `jwtSecret`, `issuer`, `
|
|
128
|
-
|
|
137
|
+
(`installId`, `jwtSecret`, `issuer`, `apiUrl`). With `apiUrl` set, it is the REST
|
|
138
|
+
base and only the path is appended; the deprecated `baseUrl`/`appId` fall back to
|
|
139
|
+
the app-registry installations API default. `createAccessServiceForInstall` routes
|
|
140
|
+
`platform` `"cng"`/`"acme"` to these.
|
|
129
141
|
|
|
130
142
|
- `CngAccessService` — `getProductService().getAllActivities()` (short-form
|
|
131
143
|
`getAllActivities()`) → `Activity[]` (`Activity`/`ActivityTicket` models).
|
|
@@ -187,12 +199,14 @@ an external system** (App Store config), not from code.
|
|
|
187
199
|
`verifyPeekAuthToken`, throwing the underlying `jsonwebtoken` error on failure,
|
|
188
200
|
one `try/catch` → `401`) and merges both into one flat, JSON-safe
|
|
189
201
|
`InstallWebhook`: `installId`, `accountId` (**also called the partner ID**),
|
|
190
|
-
`accountName`, `platform`, `isTest`, `
|
|
191
|
-
`user` (nullable, for system-initiated events).
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
202
|
+
`accountName`, `platform`, `isTest`, `timezone`, `apiUrl`, `status`,
|
|
203
|
+
`rawStatus`, `displayVersion`, `user` (nullable, for system-initiated events).
|
|
204
|
+
The **JSON body is the source of the event data**; the fields it shares with
|
|
205
|
+
the token (`installId`/`accountId`/`status`/`displayVersion`/`user`) fall back
|
|
206
|
+
to the **verified token** when the body omits them. The token authenticates the
|
|
207
|
+
whole delivery, so the body is trusted within a verified request. This webhook
|
|
208
|
+
is the **only** source of `accountId`, `timezone`, and `apiUrl` in the package
|
|
209
|
+
— no GraphQL read returns them; **persist them per install**. `status` and `platform`
|
|
196
210
|
are **nullable**: a value this version does not recognise maps to `null` (wire
|
|
197
211
|
value kept on `rawStatus`) rather than being coerced, because Peek does not
|
|
198
212
|
redeliver a 2xx and a coerced status silently drops a lifecycle transition. Give
|
|
@@ -236,12 +250,18 @@ via a `ref`'s imperative methods). No `only a getter` crash. TS still types them
|
|
|
236
250
|
read-only — the setters are a runtime safety net.
|
|
237
251
|
|
|
238
252
|
Tags by tier (see `docs/ui.md` for attributes/events/examples per component):
|
|
239
|
-
- display: `ody-button`, `ody-tag`, `ody-alert`, `ody-card`, `ody-divider
|
|
253
|
+
- display: `ody-button`, `ody-tag`, `ody-alert`, `ody-card`, `ody-horizontal-divider`
|
|
254
|
+
(1px rule with a default `margin-block: var(--gap16)`; `spacing="tight"|"none"`),
|
|
240
255
|
`ody-status-dot`, `ody-message`, `ody-icon`, `ody-loading-spinner`, `ody-loading-bar`
|
|
241
256
|
- layout: `ody-empty-state`, `ody-breadcrumb`(+`-item`), `ody-stat-summary`(+`ody-stat`),
|
|
242
257
|
`ody-inline-list`(+`-item`), `ody-list-item`, `ody-product-indicator`,
|
|
243
258
|
`ody-toggle-button`, `ody-section-rows`/`-columns`(+items), `ody-two-column`(+parts),
|
|
244
|
-
`ody-collapsible-section`
|
|
259
|
+
`ody-collapsible-section`, `ody-app-page-container` (required page wrapper for a
|
|
260
|
+
settings UI — 868/1310px widths, `ody-page` container context, default responsive
|
|
261
|
+
gutter; `flush` opts out)
|
|
262
|
+
- deprecated (kept, unchanged): `ody-divider` → use `ody-horizontal-divider` (the old
|
|
263
|
+
one has zero margin); `ody-page-container` → use `ody-app-page-container` (the old
|
|
264
|
+
one is full-bleed with no gutter). Migrate by renaming the tag.
|
|
245
265
|
- form: `ody-input`, `ody-inline-input`, `ody-search-input`, `ody-money-input`,
|
|
246
266
|
`ody-percentage-input`, `ody-checkbox`, `ody-radio-button-group`, `ody-checkbox-group`
|
|
247
267
|
- interactive: `ody-accordion`, `ody-collapsible`, `ody-tabs`, `ody-copy-button`,
|