@peektravel/app-utilities 0.6.1 → 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 CHANGED
@@ -124,7 +124,8 @@ Two kinds of failures surface as exceptions:
124
124
  - `PiiAccessDisabledError` — a payment / booking-modification operation was
125
125
  called on an access service created without `fullCustomerAccess` (see [Access options
126
126
  / PII](#access-options--pii)). Carries `.operation` (the blocked method name).
127
- - `InvalidPeekTokenError` — `verifyPeekAuthToken` (or `verifyInstallWebhook`)
127
+ - `InvalidPeekTokenError` — `verifyPeekAuthToken` (or `parseInstallWebhook` /
128
+ `verifyInstallWebhook`)
128
129
  received a token whose signature is valid but whose `user` block has no `id`.
129
130
  Carries `.field` (`"user.id"`). Thrown alongside the `jsonwebtoken` errors.
130
131
 
@@ -245,22 +246,20 @@ Functions runtime) resolve correctly. Its only runtime dependency is
245
246
 
246
247
  ## Webhooks
247
248
 
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:
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:
253
254
 
254
255
  ```ts
255
256
  import {
256
257
  parseBookingWebhook,
257
- parseInstallEvent,
258
+ parseInstallWebhook,
258
259
  parseWaiverWebhook,
259
- verifyInstallWebhook,
260
260
  type Booking,
261
- type InstallEvent,
261
+ type InstallWebhook,
262
262
  type Waiver,
263
- type InstallWebhookClaims,
264
263
  } from "@peektravel/app-utilities";
265
264
 
266
265
  app.post("/booking-webhook", (req, res) => {
@@ -274,57 +273,56 @@ app.post("/waiver-webhook", (req, res) => {
274
273
  res.sendStatus(200);
275
274
  });
276
275
 
277
- app.post("/install-event", async (req, res) => {
278
- const event: InstallEvent = parseInstallEvent(req.body);
276
+ app.post("/install-webhook", async (req, res) => {
277
+ let event: InstallWebhook;
278
+ try {
279
+ // Verifies the signed token, then merges in the JSON body:
280
+ event = parseInstallWebhook(req.token, req.body, process.env.PEEK_INTERNAL_SECRET!);
281
+ } catch {
282
+ return res.sendStatus(401); // bad signature / issuer / audience / expired
283
+ }
279
284
  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;
285
+ case "installed": await provision(event); break;
286
+ case "uninstalled": await deprovision(event); break;
287
+ case "update_installed": await recordVersion(event); break;
283
288
  // `status` is null when Peek sends one this version doesn't know. Peek
284
289
  // doesn't redeliver a 2xx, so fail loudly rather than silently no-op.
285
290
  default: return res.status(500).json({ error: `unknown status: ${event.rawStatus}` });
286
291
  }
287
292
  res.sendStatus(200);
288
293
  });
289
-
290
- app.post("/install-webhook", (req, res) => {
291
- try {
292
- // The payload IS a signed JWT — verify signature/issuer/audience/expiry:
293
- const claims: InstallWebhookClaims = verifyInstallWebhook(req.body, process.env.PEEK_INTERNAL_SECRET!);
294
- if (claims.status === "uninstalled") {
295
- /* tear down this install */
296
- }
297
- res.sendStatus(200);
298
- } catch {
299
- res.sendStatus(401); // bad signature / issuer / audience / expired
300
- }
301
- });
302
294
  ```
303
295
 
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
296
+ The booking and waiver parsers tolerate the delivery envelope / a bare node / a
297
+ JSON string and never throw on malformed input; **authenticating those deliveries
298
+ is the receiver's job**. `parseWaiverWebhook` also takes an optional
314
299
  `AccessOptions` (`{ fullCustomerAccess }`) and redacts the participant `guestName`
315
300
  + document `fileUrl` by default — see [Access options / PII](#access-options--pii)
316
- below. `verifyInstallWebhook(token, secret)` is different: the payload is a signed
317
- `app_registry_v2` JWT, so it validates the HMAC signature, expiry, issuer, and
318
- `"Joken"` audience (the same checks as `verifyPeekAuthToken`) and returns typed
319
- claims (`installId`, `account.id`, `status`, `displayVersion`, and a **nullable**
320
- `user` for system-initiated events), throwing the underlying `jsonwebtoken` error
321
- on any failure — or `InvalidPeekTokenError` when a present `user` block has no
322
- `id`. The booking and waiver webhooks differ on registration: a
323
- **booking** webhook's payload shape is set by a GraphQL query configured **once in
324
- an external system** (the App Store `broadcast_to_url` config) — this package
325
- documents and drift-guards the exact query to paste there — whereas the **waiver**
326
- and **install-status** webhooks have fixed payloads, so you just subscribe to
327
- their event with no query. **The query to register and the full guide:
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:
328
326
  [`docs/webhooks.md`](docs/webhooks.md) (shipped).**
329
327
 
330
328
  ## UI components (`/ui`)
package/dist/index.cjs CHANGED
@@ -4271,18 +4271,6 @@ function extractWaiverNode(payload) {
4271
4271
  return record;
4272
4272
  }
4273
4273
 
4274
- // src/internal/peek/installs/install-webhook.ts
4275
- function verifyInstallWebhook(token, secret) {
4276
- const payload = verifyPeekJwt(token, secret);
4277
- return {
4278
- installId: payload.sub ?? "",
4279
- account: { id: payload.account?.id ?? "" },
4280
- status: payload.status ?? "",
4281
- displayVersion: payload.display_version ?? "",
4282
- user: payload.user ? mapPeekTokenUser(payload.user) : null
4283
- };
4284
- }
4285
-
4286
4274
  // src/models/peek/auth-token.ts
4287
4275
  var PEEK_PLATFORMS = ["peek", "cng", "acme"];
4288
4276
 
@@ -4294,48 +4282,51 @@ var INSTALL_STATUSES = [
4294
4282
  ];
4295
4283
 
4296
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
4297
  function toInstallStatus(raw) {
4298
4298
  return INSTALL_STATUSES.includes(raw) ? raw : null;
4299
4299
  }
4300
4300
  function toPeekPlatform(raw) {
4301
4301
  return PEEK_PLATFORMS.includes(raw) ? raw : null;
4302
4302
  }
4303
- function fromInstallIdentityNode(node) {
4304
- const data = node ?? {};
4305
- const account = data.account ?? {};
4303
+
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 ?? "";
4306
4309
  return {
4307
- installId: data.install_id || "",
4308
- accountId: account.id || "",
4310
+ installId: payload.sub ?? "",
4311
+ accountId: payload.account?.id ?? "",
4309
4312
  accountName: account.name || "",
4310
4313
  platform: toPeekPlatform(account.platform || ""),
4311
- isTest: Boolean(account.is_test)
4312
- };
4313
- }
4314
- function fromInstallEventNode(node) {
4315
- const rawStatus = node?.status || "";
4316
- return {
4314
+ isTest: Boolean(account.is_test),
4317
4315
  status: toInstallStatus(rawStatus),
4318
4316
  rawStatus,
4319
- displayVersion: node?.display_version || "",
4320
- identity: fromInstallIdentityNode(node)
4317
+ displayVersion: payload.display_version ?? "",
4318
+ user: payload.user ? mapPeekTokenUser(payload.user) : null
4321
4319
  };
4322
4320
  }
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;
4321
+ function verifyInstallWebhook(token, secret) {
4322
+ const payload = verifyPeekJwt(token, secret);
4323
+ return {
4324
+ installId: payload.sub ?? "",
4325
+ account: { id: payload.account?.id ?? "" },
4326
+ status: payload.status ?? "",
4327
+ displayVersion: payload.display_version ?? "",
4328
+ user: payload.user ? mapPeekTokenUser(payload.user) : null
4329
+ };
4339
4330
  }
4340
4331
 
4341
4332
  exports.ACTIVITY_PRODUCT_TYPE = ACTIVITY_PRODUCT_TYPE;
@@ -4369,7 +4360,7 @@ exports.ReviewService = ReviewService;
4369
4360
  exports.TimeslotService = TimeslotService;
4370
4361
  exports.noopLogger = noopLogger;
4371
4362
  exports.parseBookingWebhook = parseBookingWebhook;
4372
- exports.parseInstallEvent = parseInstallEvent;
4363
+ exports.parseInstallWebhook = parseInstallWebhook;
4373
4364
  exports.parseWaiverWebhook = parseWaiverWebhook;
4374
4365
  exports.verifyInstallWebhook = verifyInstallWebhook;
4375
4366
  //# sourceMappingURL=index.cjs.map