@peektravel/app-utilities 0.5.4 → 0.6.1
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 +43 -9
- package/dist/index.cjs +91 -0
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +171 -3
- package/dist/index.d.ts +171 -3
- package/dist/index.js +89 -1
- package/dist/index.js.map +1 -1
- package/dist/ui/index.cjs +362 -35
- package/dist/ui/index.cjs.map +1 -1
- package/dist/ui/index.d.cts +94 -15
- package/dist/ui/index.d.ts +94 -15
- package/dist/ui/index.js +362 -35
- package/dist/ui/index.js.map +1 -1
- package/docs/ui.md +26 -3
- package/docs/webhooks.md +115 -11
- package/llms.txt +28 -6
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -15,6 +15,10 @@ npm install @peektravel/app-utilities
|
|
|
15
15
|
npm update @peektravel/app-utilities
|
|
16
16
|
```
|
|
17
17
|
|
|
18
|
+
Before updating to a new version, consult [`changelog.md`](./changelog.md) —
|
|
19
|
+
it records every caller-visible change grouped by version, including any
|
|
20
|
+
breaking changes and the concrete action you need to take to migrate.
|
|
21
|
+
|
|
18
22
|
See [Releasing](#releasing) for how new versions are published.
|
|
19
23
|
|
|
20
24
|
## Usage
|
|
@@ -120,6 +124,9 @@ Two kinds of failures surface as exceptions:
|
|
|
120
124
|
- `PiiAccessDisabledError` — a payment / booking-modification operation was
|
|
121
125
|
called on an access service created without `fullCustomerAccess` (see [Access options
|
|
122
126
|
/ PII](#access-options--pii)). Carries `.operation` (the blocked method name).
|
|
127
|
+
- `InvalidPeekTokenError` — `verifyPeekAuthToken` (or `verifyInstallWebhook`)
|
|
128
|
+
received a token whose signature is valid but whose `user` block has no `id`.
|
|
129
|
+
Carries `.field` (`"user.id"`). Thrown alongside the `jsonwebtoken` errors.
|
|
123
130
|
|
|
124
131
|
**Plain `Error` validation/precondition failures** thrown by the service layer
|
|
125
132
|
*before* any network call — e.g. an empty config field, a `bookingId` that
|
|
@@ -238,18 +245,20 @@ Functions runtime) resolve correctly. Its only runtime dependency is
|
|
|
238
245
|
|
|
239
246
|
## Webhooks
|
|
240
247
|
|
|
241
|
-
Receiver apps can consume Peek **booking**, **waiver**,
|
|
242
|
-
webhooks without hand-writing the payload handling. The
|
|
243
|
-
webhooks have a pure parser (construct
|
|
244
|
-
clean model; the install-status webhook
|
|
245
|
-
**verifies** it:
|
|
248
|
+
Receiver apps can consume Peek **booking**, **waiver**, **install-event**, and
|
|
249
|
+
**install-status** webhooks without hand-writing the payload handling. The
|
|
250
|
+
booking, waiver, and install-event webhooks have a pure parser (construct
|
|
251
|
+
nothing — no auth/network) that returns a clean model; the install-status webhook
|
|
252
|
+
delivers a signed token, so its helper **verifies** it:
|
|
246
253
|
|
|
247
254
|
```ts
|
|
248
255
|
import {
|
|
249
256
|
parseBookingWebhook,
|
|
257
|
+
parseInstallEvent,
|
|
250
258
|
parseWaiverWebhook,
|
|
251
259
|
verifyInstallWebhook,
|
|
252
260
|
type Booking,
|
|
261
|
+
type InstallEvent,
|
|
253
262
|
type Waiver,
|
|
254
263
|
type InstallWebhookClaims,
|
|
255
264
|
} from "@peektravel/app-utilities";
|
|
@@ -265,6 +274,19 @@ app.post("/waiver-webhook", (req, res) => {
|
|
|
265
274
|
res.sendStatus(200);
|
|
266
275
|
});
|
|
267
276
|
|
|
277
|
+
app.post("/install-event", async (req, res) => {
|
|
278
|
+
const event: InstallEvent = parseInstallEvent(req.body);
|
|
279
|
+
switch (event.status) {
|
|
280
|
+
case "installed": await provision(event.identity); break;
|
|
281
|
+
case "uninstalled": await deprovision(event.identity); break;
|
|
282
|
+
case "update_installed": await recordVersion(event.identity, event.displayVersion); break;
|
|
283
|
+
// `status` is null when Peek sends one this version doesn't know. Peek
|
|
284
|
+
// doesn't redeliver a 2xx, so fail loudly rather than silently no-op.
|
|
285
|
+
default: return res.status(500).json({ error: `unknown status: ${event.rawStatus}` });
|
|
286
|
+
}
|
|
287
|
+
res.sendStatus(200);
|
|
288
|
+
});
|
|
289
|
+
|
|
268
290
|
app.post("/install-webhook", (req, res) => {
|
|
269
291
|
try {
|
|
270
292
|
// The payload IS a signed JWT — verify signature/issuer/audience/expiry:
|
|
@@ -279,9 +301,16 @@ app.post("/install-webhook", (req, res) => {
|
|
|
279
301
|
});
|
|
280
302
|
```
|
|
281
303
|
|
|
282
|
-
The booking and
|
|
283
|
-
JSON string and never throw on malformed input; **authenticating
|
|
284
|
-
is the receiver's job**. `
|
|
304
|
+
The booking, waiver, and install-event parsers tolerate the delivery envelope / a
|
|
305
|
+
bare node / a JSON string and never throw on malformed input; **authenticating
|
|
306
|
+
those deliveries is the receiver's job**. `parseInstallEvent` returns an
|
|
307
|
+
`InstallIdentity` (`installId`, `accountId` — **also called the partner ID** —
|
|
308
|
+
`accountName`, `platform`, `isTest`) that is flat and JSON-safe, so it persists
|
|
309
|
+
and rehydrates as a unit; that body is the only source of `accountId` in the
|
|
310
|
+
package, since no GraphQL read or token returns it. Its `status` and `platform`
|
|
311
|
+
are **nullable** — a value this version doesn't recognise stays `null` instead of
|
|
312
|
+
being coerced, so a newer registry can't be mistaken for an older one.
|
|
313
|
+
`parseWaiverWebhook` also takes an optional
|
|
285
314
|
`AccessOptions` (`{ fullCustomerAccess }`) and redacts the participant `guestName`
|
|
286
315
|
+ document `fileUrl` by default — see [Access options / PII](#access-options--pii)
|
|
287
316
|
below. `verifyInstallWebhook(token, secret)` is different: the payload is a signed
|
|
@@ -289,7 +318,8 @@ below. `verifyInstallWebhook(token, secret)` is different: the payload is a sign
|
|
|
289
318
|
`"Joken"` audience (the same checks as `verifyPeekAuthToken`) and returns typed
|
|
290
319
|
claims (`installId`, `account.id`, `status`, `displayVersion`, and a **nullable**
|
|
291
320
|
`user` for system-initiated events), throwing the underlying `jsonwebtoken` error
|
|
292
|
-
on any failure
|
|
321
|
+
on any failure — or `InvalidPeekTokenError` when a present `user` block has no
|
|
322
|
+
`id`. The booking and waiver webhooks differ on registration: a
|
|
293
323
|
**booking** webhook's payload shape is set by a GraphQL query configured **once in
|
|
294
324
|
an external system** (the App Store `broadcast_to_url` config) — this package
|
|
295
325
|
documents and drift-guards the exact query to paste there — whereas the **waiver**
|
|
@@ -349,6 +379,10 @@ property would otherwise cause. Two things worth knowing:
|
|
|
349
379
|
internal state (`isOpen`, `isVisible`) ignore writes — drive them through the
|
|
350
380
|
imperative methods on a `ref` (`ref.current.openPopover()`, `.show()`) and read
|
|
351
381
|
them back through the component's `CustomEvent`s, not by setting the prop.
|
|
382
|
+
- **Dynamic children reconcile correctly.** Slotting components (`ody-card`,
|
|
383
|
+
`ody-alert`, `ody-section`, …) forward child mutations to their internal slot,
|
|
384
|
+
so conditional, keyed, `.map()`-ed, or reordered **direct** children work
|
|
385
|
+
without a `NotFoundError` — no need to wrap dynamic children in a stable `<div>`.
|
|
352
386
|
|
|
353
387
|
TypeScript still types these accessors as read-only, so a direct
|
|
354
388
|
`el.isOpen = true` in TS is flagged — the setters are a runtime safety net for
|
package/dist/index.cjs
CHANGED
|
@@ -247,6 +247,15 @@ var PeekHttpError = class extends Error {
|
|
|
247
247
|
this.body = body;
|
|
248
248
|
}
|
|
249
249
|
};
|
|
250
|
+
var InvalidPeekTokenError = class extends Error {
|
|
251
|
+
/** The token field that was missing or empty (e.g. `"user.id"`). */
|
|
252
|
+
field;
|
|
253
|
+
constructor(field) {
|
|
254
|
+
super(`Peek token is missing required field "${field}"`);
|
|
255
|
+
this.name = "InvalidPeekTokenError";
|
|
256
|
+
this.field = field;
|
|
257
|
+
}
|
|
258
|
+
};
|
|
250
259
|
var PiiAccessDisabledError = class extends Error {
|
|
251
260
|
/** The name of the operation that was blocked (e.g. `"makePayment"`). */
|
|
252
261
|
operation;
|
|
@@ -2059,6 +2068,9 @@ function verifyPeekJwt(token, secret) {
|
|
|
2059
2068
|
});
|
|
2060
2069
|
}
|
|
2061
2070
|
function mapPeekTokenUser(u) {
|
|
2071
|
+
if (!u.id) {
|
|
2072
|
+
throw new InvalidPeekTokenError("user.id");
|
|
2073
|
+
}
|
|
2062
2074
|
return {
|
|
2063
2075
|
email: u.email,
|
|
2064
2076
|
id: u.id,
|
|
@@ -2876,6 +2888,13 @@ function toPricingMoney(raw) {
|
|
|
2876
2888
|
|
|
2877
2889
|
// src/internal/peek/products/product-converter.ts
|
|
2878
2890
|
var ADD_ON_COLOR = "#FFFFFF";
|
|
2891
|
+
function toMeetingLocation(activity) {
|
|
2892
|
+
const summary = activity.infoMeetingLocation ?? null;
|
|
2893
|
+
const address = activity.meetingLocationFormattedAddress ?? null;
|
|
2894
|
+
const url = activity.meetingLocationUrl ?? null;
|
|
2895
|
+
if (summary === null && address === null && url === null) return null;
|
|
2896
|
+
return { summary, address, url };
|
|
2897
|
+
}
|
|
2879
2898
|
function fromActivities(activities) {
|
|
2880
2899
|
return activities.map(fromActivity);
|
|
2881
2900
|
}
|
|
@@ -2887,6 +2906,9 @@ function fromActivity(activity) {
|
|
|
2887
2906
|
type: activity.type,
|
|
2888
2907
|
color: activity.colorHex || "",
|
|
2889
2908
|
currency: activity.currency || "",
|
|
2909
|
+
imageUrl: activity.imageUrl ?? null,
|
|
2910
|
+
description: activity.description ?? null,
|
|
2911
|
+
meetingLocation: toMeetingLocation(activity),
|
|
2890
2912
|
tickets: (activity.resourceOptions ?? []).map((option) => ({
|
|
2891
2913
|
id: option.id,
|
|
2892
2914
|
name: option.name,
|
|
@@ -2908,6 +2930,9 @@ function fromItemOptionNodes(nodes) {
|
|
|
2908
2930
|
type: ADD_ON_PRODUCT_TYPE,
|
|
2909
2931
|
color: ADD_ON_COLOR,
|
|
2910
2932
|
currency: "",
|
|
2933
|
+
imageUrl: null,
|
|
2934
|
+
description: null,
|
|
2935
|
+
meetingLocation: null,
|
|
2911
2936
|
tickets: []
|
|
2912
2937
|
};
|
|
2913
2938
|
grouped.set(itemId, product);
|
|
@@ -2933,6 +2958,11 @@ var PRODUCTS_QUERY = `
|
|
|
2933
2958
|
type
|
|
2934
2959
|
colorHex
|
|
2935
2960
|
currency
|
|
2961
|
+
imageUrl
|
|
2962
|
+
description
|
|
2963
|
+
meetingLocationFormattedAddress
|
|
2964
|
+
meetingLocationUrl
|
|
2965
|
+
infoMeetingLocation
|
|
2936
2966
|
resourceOptions {
|
|
2937
2967
|
id
|
|
2938
2968
|
name
|
|
@@ -3564,10 +3594,13 @@ var PeekAccessService = class {
|
|
|
3564
3594
|
* malformed
|
|
3565
3595
|
* - `TokenExpiredError` — past `exp`
|
|
3566
3596
|
* - `NotBeforeError` — before `nbf`
|
|
3597
|
+
* - `InvalidPeekTokenError` — signature valid but the `user` block is missing
|
|
3598
|
+
* its `id`
|
|
3567
3599
|
*
|
|
3568
3600
|
* @throws {JsonWebTokenError} signature invalid or token malformed
|
|
3569
3601
|
* @throws {TokenExpiredError} token has expired
|
|
3570
3602
|
* @throws {NotBeforeError} token not yet valid
|
|
3603
|
+
* @throws {InvalidPeekTokenError} verified token has no `user.id`
|
|
3571
3604
|
*/
|
|
3572
3605
|
verifyPeekAuthToken(token) {
|
|
3573
3606
|
const payload = verifyPeekJwt(token, this.jwtSecret);
|
|
@@ -4250,6 +4283,61 @@ function verifyInstallWebhook(token, secret) {
|
|
|
4250
4283
|
};
|
|
4251
4284
|
}
|
|
4252
4285
|
|
|
4286
|
+
// src/models/peek/auth-token.ts
|
|
4287
|
+
var PEEK_PLATFORMS = ["peek", "cng", "acme"];
|
|
4288
|
+
|
|
4289
|
+
// src/models/peek/install.ts
|
|
4290
|
+
var INSTALL_STATUSES = [
|
|
4291
|
+
"installed",
|
|
4292
|
+
"uninstalled",
|
|
4293
|
+
"update_installed"
|
|
4294
|
+
];
|
|
4295
|
+
|
|
4296
|
+
// src/internal/peek/installs/install-converter.ts
|
|
4297
|
+
function toInstallStatus(raw) {
|
|
4298
|
+
return INSTALL_STATUSES.includes(raw) ? raw : null;
|
|
4299
|
+
}
|
|
4300
|
+
function toPeekPlatform(raw) {
|
|
4301
|
+
return PEEK_PLATFORMS.includes(raw) ? raw : null;
|
|
4302
|
+
}
|
|
4303
|
+
function fromInstallIdentityNode(node) {
|
|
4304
|
+
const data = node ?? {};
|
|
4305
|
+
const account = data.account ?? {};
|
|
4306
|
+
return {
|
|
4307
|
+
installId: data.install_id || "",
|
|
4308
|
+
accountId: account.id || "",
|
|
4309
|
+
accountName: account.name || "",
|
|
4310
|
+
platform: toPeekPlatform(account.platform || ""),
|
|
4311
|
+
isTest: Boolean(account.is_test)
|
|
4312
|
+
};
|
|
4313
|
+
}
|
|
4314
|
+
function fromInstallEventNode(node) {
|
|
4315
|
+
const rawStatus = node?.status || "";
|
|
4316
|
+
return {
|
|
4317
|
+
status: toInstallStatus(rawStatus),
|
|
4318
|
+
rawStatus,
|
|
4319
|
+
displayVersion: node?.display_version || "",
|
|
4320
|
+
identity: fromInstallIdentityNode(node)
|
|
4321
|
+
};
|
|
4322
|
+
}
|
|
4323
|
+
|
|
4324
|
+
// src/internal/peek/installs/install-event.ts
|
|
4325
|
+
function parseInstallEvent(payload) {
|
|
4326
|
+
return fromInstallEventNode(extractInstallEventNode(payload));
|
|
4327
|
+
}
|
|
4328
|
+
function extractInstallEventNode(payload) {
|
|
4329
|
+
let body = payload;
|
|
4330
|
+
if (typeof body === "string") {
|
|
4331
|
+
try {
|
|
4332
|
+
body = JSON.parse(body);
|
|
4333
|
+
} catch {
|
|
4334
|
+
return {};
|
|
4335
|
+
}
|
|
4336
|
+
}
|
|
4337
|
+
if (!body || typeof body !== "object") return {};
|
|
4338
|
+
return body;
|
|
4339
|
+
}
|
|
4340
|
+
|
|
4253
4341
|
exports.ACTIVITY_PRODUCT_TYPE = ACTIVITY_PRODUCT_TYPE;
|
|
4254
4342
|
exports.ADD_ON_PRODUCT_TYPE = ADD_ON_PRODUCT_TYPE;
|
|
4255
4343
|
exports.AccountUserService = AccountUserService;
|
|
@@ -4263,6 +4351,8 @@ exports.CngAccessService = CngAccessService;
|
|
|
4263
4351
|
exports.CngApiError = CngApiError;
|
|
4264
4352
|
exports.CngProductService = CngProductService;
|
|
4265
4353
|
exports.DailyNoteService = DailyNoteService;
|
|
4354
|
+
exports.INSTALL_STATUSES = INSTALL_STATUSES;
|
|
4355
|
+
exports.InvalidPeekTokenError = InvalidPeekTokenError;
|
|
4266
4356
|
exports.MembershipService = MembershipService;
|
|
4267
4357
|
exports.PeekAccessService = PeekAccessService;
|
|
4268
4358
|
exports.PeekGraphQLError = PeekGraphQLError;
|
|
@@ -4279,6 +4369,7 @@ exports.ReviewService = ReviewService;
|
|
|
4279
4369
|
exports.TimeslotService = TimeslotService;
|
|
4280
4370
|
exports.noopLogger = noopLogger;
|
|
4281
4371
|
exports.parseBookingWebhook = parseBookingWebhook;
|
|
4372
|
+
exports.parseInstallEvent = parseInstallEvent;
|
|
4282
4373
|
exports.parseWaiverWebhook = parseWaiverWebhook;
|
|
4283
4374
|
exports.verifyInstallWebhook = verifyInstallWebhook;
|
|
4284
4375
|
//# sourceMappingURL=index.cjs.map
|