@peektravel/app-utilities 0.6.0 → 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 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**, and **install-status**
242
- webhooks without hand-writing the payload handling. The booking and waiver
243
- webhooks have a pure parser (construct nothing — no auth/network) that returns a
244
- clean model; the install-status webhook delivers a signed token, so its helper
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 waiver parsers tolerate the delivery envelope / a bare node / a
283
- JSON string and never throw on malformed input; **authenticating those deliveries
284
- is the receiver's job**. `parseWaiverWebhook` also takes an optional
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. The booking and waiver webhooks differ on registration: a
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**
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);
@@ -4268,6 +4283,61 @@ function verifyInstallWebhook(token, secret) {
4268
4283
  };
4269
4284
  }
4270
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
+
4271
4341
  exports.ACTIVITY_PRODUCT_TYPE = ACTIVITY_PRODUCT_TYPE;
4272
4342
  exports.ADD_ON_PRODUCT_TYPE = ADD_ON_PRODUCT_TYPE;
4273
4343
  exports.AccountUserService = AccountUserService;
@@ -4281,6 +4351,8 @@ exports.CngAccessService = CngAccessService;
4281
4351
  exports.CngApiError = CngApiError;
4282
4352
  exports.CngProductService = CngProductService;
4283
4353
  exports.DailyNoteService = DailyNoteService;
4354
+ exports.INSTALL_STATUSES = INSTALL_STATUSES;
4355
+ exports.InvalidPeekTokenError = InvalidPeekTokenError;
4284
4356
  exports.MembershipService = MembershipService;
4285
4357
  exports.PeekAccessService = PeekAccessService;
4286
4358
  exports.PeekGraphQLError = PeekGraphQLError;
@@ -4297,6 +4369,7 @@ exports.ReviewService = ReviewService;
4297
4369
  exports.TimeslotService = TimeslotService;
4298
4370
  exports.noopLogger = noopLogger;
4299
4371
  exports.parseBookingWebhook = parseBookingWebhook;
4372
+ exports.parseInstallEvent = parseInstallEvent;
4300
4373
  exports.parseWaiverWebhook = parseWaiverWebhook;
4301
4374
  exports.verifyInstallWebhook = verifyInstallWebhook;
4302
4375
  //# sourceMappingURL=index.cjs.map