@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 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**
@@ -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