@peektravel/app-utilities 0.6.0 → 0.7.0
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 +54 -26
- package/dist/index.cjs +64 -0
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +159 -5
- package/dist/index.d.ts +159 -5
- package/dist/index.js +62 -1
- package/dist/index.js.map +1 -1
- package/docs/webhooks.md +130 -51
- package/llms.txt +29 -13
- 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,10 @@ 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 `parseInstallWebhook` /
|
|
128
|
+
`verifyInstallWebhook`)
|
|
129
|
+
received a token whose signature is valid but whose `user` block has no `id`.
|
|
130
|
+
Carries `.field` (`"user.id"`). Thrown alongside the `jsonwebtoken` errors.
|
|
123
131
|
|
|
124
132
|
**Plain `Error` validation/precondition failures** thrown by the service layer
|
|
125
133
|
*before* any network call — e.g. an empty config field, a `bookingId` that
|
|
@@ -238,20 +246,20 @@ Functions runtime) resolve correctly. Its only runtime dependency is
|
|
|
238
246
|
|
|
239
247
|
## Webhooks
|
|
240
248
|
|
|
241
|
-
Receiver apps can consume Peek **booking**, **waiver**, and **install
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
**verifies**
|
|
249
|
+
Receiver apps can consume Peek **booking**, **waiver**, and **install** webhooks
|
|
250
|
+
without hand-writing the payload handling. The booking and waiver webhooks have a
|
|
251
|
+
pure parser (construct nothing — no auth/network) that returns a clean model; the
|
|
252
|
+
install webhook delivers a signed token alongside a JSON body, so its helper
|
|
253
|
+
**verifies** the token before merging the two:
|
|
246
254
|
|
|
247
255
|
```ts
|
|
248
256
|
import {
|
|
249
257
|
parseBookingWebhook,
|
|
258
|
+
parseInstallWebhook,
|
|
250
259
|
parseWaiverWebhook,
|
|
251
|
-
verifyInstallWebhook,
|
|
252
260
|
type Booking,
|
|
261
|
+
type InstallWebhook,
|
|
253
262
|
type Waiver,
|
|
254
|
-
type InstallWebhookClaims,
|
|
255
263
|
} from "@peektravel/app-utilities";
|
|
256
264
|
|
|
257
265
|
app.post("/booking-webhook", (req, res) => {
|
|
@@ -265,17 +273,23 @@ app.post("/waiver-webhook", (req, res) => {
|
|
|
265
273
|
res.sendStatus(200);
|
|
266
274
|
});
|
|
267
275
|
|
|
268
|
-
app.post("/install-webhook", (req, res) => {
|
|
276
|
+
app.post("/install-webhook", async (req, res) => {
|
|
277
|
+
let event: InstallWebhook;
|
|
269
278
|
try {
|
|
270
|
-
//
|
|
271
|
-
|
|
272
|
-
if (claims.status === "uninstalled") {
|
|
273
|
-
/* tear down this install */
|
|
274
|
-
}
|
|
275
|
-
res.sendStatus(200);
|
|
279
|
+
// Verifies the signed token, then merges in the JSON body:
|
|
280
|
+
event = parseInstallWebhook(req.token, req.body, process.env.PEEK_INTERNAL_SECRET!);
|
|
276
281
|
} catch {
|
|
277
|
-
res.sendStatus(401); // bad signature / issuer / audience / expired
|
|
282
|
+
return res.sendStatus(401); // bad signature / issuer / audience / expired
|
|
283
|
+
}
|
|
284
|
+
switch (event.status) {
|
|
285
|
+
case "installed": await provision(event); break;
|
|
286
|
+
case "uninstalled": await deprovision(event); break;
|
|
287
|
+
case "update_installed": await recordVersion(event); break;
|
|
288
|
+
// `status` is null when Peek sends one this version doesn't know. Peek
|
|
289
|
+
// doesn't redeliver a 2xx, so fail loudly rather than silently no-op.
|
|
290
|
+
default: return res.status(500).json({ error: `unknown status: ${event.rawStatus}` });
|
|
278
291
|
}
|
|
292
|
+
res.sendStatus(200);
|
|
279
293
|
});
|
|
280
294
|
```
|
|
281
295
|
|
|
@@ -284,17 +298,31 @@ JSON string and never throw on malformed input; **authenticating those deliverie
|
|
|
284
298
|
is the receiver's job**. `parseWaiverWebhook` also takes an optional
|
|
285
299
|
`AccessOptions` (`{ fullCustomerAccess }`) and redacts the participant `guestName`
|
|
286
300
|
+ document `fileUrl` by default — see [Access options / PII](#access-options--pii)
|
|
287
|
-
below.
|
|
288
|
-
|
|
289
|
-
`
|
|
290
|
-
|
|
291
|
-
`
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
301
|
+
below.
|
|
302
|
+
|
|
303
|
+
`parseInstallWebhook(token, body, secret)` is different: the delivery carries a
|
|
304
|
+
signed `app_registry_v2` JWT plus a JSON body. It validates the HMAC signature,
|
|
305
|
+
expiry, issuer, and `"Joken"` audience (the same checks as `verifyPeekAuthToken`,
|
|
306
|
+
throwing the underlying `jsonwebtoken` error — or `InvalidPeekTokenError` when a
|
|
307
|
+
present `user` block has no `id`), then returns one flat, JSON-safe
|
|
308
|
+
`InstallWebhook`: `installId`, `accountId` (**also called the partner ID**),
|
|
309
|
+
`accountName`, `platform`, `isTest`, `status`, `rawStatus`, `displayVersion`, and
|
|
310
|
+
a **nullable** `user`. The **verified token** is authoritative for `installId` /
|
|
311
|
+
`accountId` / `status` / `displayVersion` / `user`; the unsigned body supplies
|
|
312
|
+
only `accountName` / `platform` / `isTest`. This webhook is the only source of
|
|
313
|
+
`accountId` in the package, since no GraphQL read returns it. `status` and
|
|
314
|
+
`platform` are **nullable** — a value this version doesn't recognise stays `null`
|
|
315
|
+
(wire value on `rawStatus`) instead of being coerced, so a newer registry can't be
|
|
316
|
+
mistaken for an older one. (The older `verifyInstallWebhook(token, secret)` is
|
|
317
|
+
still exported but **`@deprecated`** — it reads only the token, missing the
|
|
318
|
+
account name/platform/test flag; `parseInstallEvent` has been **removed**.)
|
|
319
|
+
|
|
320
|
+
The booking and waiver webhooks differ on registration: a **booking** webhook's
|
|
321
|
+
payload shape is set by a GraphQL query configured **once in an external system**
|
|
322
|
+
(the App Store `broadcast_to_url` config) — this package documents and
|
|
323
|
+
drift-guards the exact query to paste there — whereas the **waiver** and
|
|
324
|
+
**install** webhooks have fixed payloads, so you just subscribe to their event
|
|
325
|
+
with no query. **The query to register and the full guide:
|
|
298
326
|
[`docs/webhooks.md`](docs/webhooks.md) (shipped).**
|
|
299
327
|
|
|
300
328
|
## UI components (`/ui`)
|
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,
|
|
@@ -3582,10 +3594,13 @@ var PeekAccessService = class {
|
|
|
3582
3594
|
* malformed
|
|
3583
3595
|
* - `TokenExpiredError` — past `exp`
|
|
3584
3596
|
* - `NotBeforeError` — before `nbf`
|
|
3597
|
+
* - `InvalidPeekTokenError` — signature valid but the `user` block is missing
|
|
3598
|
+
* its `id`
|
|
3585
3599
|
*
|
|
3586
3600
|
* @throws {JsonWebTokenError} signature invalid or token malformed
|
|
3587
3601
|
* @throws {TokenExpiredError} token has expired
|
|
3588
3602
|
* @throws {NotBeforeError} token not yet valid
|
|
3603
|
+
* @throws {InvalidPeekTokenError} verified token has no `user.id`
|
|
3589
3604
|
*/
|
|
3590
3605
|
verifyPeekAuthToken(token) {
|
|
3591
3606
|
const payload = verifyPeekJwt(token, this.jwtSecret);
|
|
@@ -4256,7 +4271,53 @@ function extractWaiverNode(payload) {
|
|
|
4256
4271
|
return record;
|
|
4257
4272
|
}
|
|
4258
4273
|
|
|
4274
|
+
// src/models/peek/auth-token.ts
|
|
4275
|
+
var PEEK_PLATFORMS = ["peek", "cng", "acme"];
|
|
4276
|
+
|
|
4277
|
+
// src/models/peek/install.ts
|
|
4278
|
+
var INSTALL_STATUSES = [
|
|
4279
|
+
"installed",
|
|
4280
|
+
"uninstalled",
|
|
4281
|
+
"update_installed"
|
|
4282
|
+
];
|
|
4283
|
+
|
|
4284
|
+
// src/internal/peek/installs/install-converter.ts
|
|
4285
|
+
function extractInstallWebhookBody(payload) {
|
|
4286
|
+
let body = payload;
|
|
4287
|
+
if (typeof body === "string") {
|
|
4288
|
+
try {
|
|
4289
|
+
body = JSON.parse(body);
|
|
4290
|
+
} catch {
|
|
4291
|
+
return {};
|
|
4292
|
+
}
|
|
4293
|
+
}
|
|
4294
|
+
if (!body || typeof body !== "object") return {};
|
|
4295
|
+
return body;
|
|
4296
|
+
}
|
|
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
|
+
|
|
4259
4304
|
// src/internal/peek/installs/install-webhook.ts
|
|
4305
|
+
function parseInstallWebhook(token, body, secret) {
|
|
4306
|
+
const payload = verifyPeekJwt(token, secret);
|
|
4307
|
+
const account = extractInstallWebhookBody(body).account ?? {};
|
|
4308
|
+
const rawStatus = payload.status ?? "";
|
|
4309
|
+
return {
|
|
4310
|
+
installId: payload.sub ?? "",
|
|
4311
|
+
accountId: payload.account?.id ?? "",
|
|
4312
|
+
accountName: account.name || "",
|
|
4313
|
+
platform: toPeekPlatform(account.platform || ""),
|
|
4314
|
+
isTest: Boolean(account.is_test),
|
|
4315
|
+
status: toInstallStatus(rawStatus),
|
|
4316
|
+
rawStatus,
|
|
4317
|
+
displayVersion: payload.display_version ?? "",
|
|
4318
|
+
user: payload.user ? mapPeekTokenUser(payload.user) : null
|
|
4319
|
+
};
|
|
4320
|
+
}
|
|
4260
4321
|
function verifyInstallWebhook(token, secret) {
|
|
4261
4322
|
const payload = verifyPeekJwt(token, secret);
|
|
4262
4323
|
return {
|
|
@@ -4281,6 +4342,8 @@ exports.CngAccessService = CngAccessService;
|
|
|
4281
4342
|
exports.CngApiError = CngApiError;
|
|
4282
4343
|
exports.CngProductService = CngProductService;
|
|
4283
4344
|
exports.DailyNoteService = DailyNoteService;
|
|
4345
|
+
exports.INSTALL_STATUSES = INSTALL_STATUSES;
|
|
4346
|
+
exports.InvalidPeekTokenError = InvalidPeekTokenError;
|
|
4284
4347
|
exports.MembershipService = MembershipService;
|
|
4285
4348
|
exports.PeekAccessService = PeekAccessService;
|
|
4286
4349
|
exports.PeekGraphQLError = PeekGraphQLError;
|
|
@@ -4297,6 +4360,7 @@ exports.ReviewService = ReviewService;
|
|
|
4297
4360
|
exports.TimeslotService = TimeslotService;
|
|
4298
4361
|
exports.noopLogger = noopLogger;
|
|
4299
4362
|
exports.parseBookingWebhook = parseBookingWebhook;
|
|
4363
|
+
exports.parseInstallWebhook = parseInstallWebhook;
|
|
4300
4364
|
exports.parseWaiverWebhook = parseWaiverWebhook;
|
|
4301
4365
|
exports.verifyInstallWebhook = verifyInstallWebhook;
|
|
4302
4366
|
//# sourceMappingURL=index.cjs.map
|