@peektravel/app-utilities 1.0.0 → 1.1.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
@@ -163,6 +163,12 @@ Two kinds of failures surface as exceptions:
163
163
  - `PiiAccessDisabledError` — a payment / booking-modification operation was
164
164
  called on an access service created without `fullCustomerAccess` (see [Access options
165
165
  / PII](#access-options--pii)). Carries `.operation` (the blocked method name).
166
+ - `InstallWebhookError` — `parseInstallWebhook` got a validly signed token that
167
+ does not name the delivered install: the token has no `sub`
168
+ (`.reason === "missing_subject"`), or the body `install_id` is not equal to it
169
+ (`.reason === "install_id_mismatch"`). It extends `jsonwebtoken`'s
170
+ `JsonWebTokenError`, so an `instanceof JsonWebTokenError` check also matches
171
+ it. Respond `401`.
166
172
 
167
173
  Once a token's signature verifies, claim *extraction* never throws: the whole
168
174
  `user` block may be absent (`claims.user === null`) and every field inside it is
@@ -321,7 +327,7 @@ app.post("/install-webhook", async (req, res) => {
321
327
  const token = req.header("x-peek-auth") ?? "";
322
328
  event = parseInstallWebhook(token, req.body, process.env.PEEK_INTERNAL_SECRET!);
323
329
  } catch {
324
- return res.sendStatus(401); // bad signature / issuer / audience / expired
330
+ return res.sendStatus(401); // bad token, or the body names another install
325
331
  }
326
332
  switch (event.status) {
327
333
  case "installed": await provision(event); break;
@@ -350,11 +356,12 @@ expiry, issuer, and `"Joken"` audience (the same checks as `verifyPeekAuthToken`
350
356
  throwing the underlying `jsonwebtoken` error), then returns one flat, JSON-safe
351
357
  `InstallWebhook`: `installId`, `accountId` (**also called the partner ID**),
352
358
  `accountName`, `platform`, `isTest`, `timezone`, `apiUrl`, `status`, `rawStatus`,
353
- `displayVersion`, and a **nullable** `user`. The **JSON body is the source of the
354
- event data**; the fields it shares with the token (`installId` / `accountId` /
355
- `status` / `displayVersion` / `user`) fall back to the **verified token** when
356
- the body omits them. The token authenticates the whole delivery, so the body is
357
- trusted within a verified request. This webhook is the only source of
359
+ `displayVersion`, and a **nullable** `user`. **`installId` is always the token
360
+ `sub`**: a body `install_id` that is present and not equal to it throws
361
+ `InstallWebhookError`, and so does a token with no `sub`. The **JSON body is the
362
+ source of the other event data**; the fields it shares with the token
363
+ (`accountId` / `status` / `displayVersion` / `user`) fall back to the **verified
364
+ token** when the body omits them. This webhook is the only source of
358
365
  `accountId`, `timezone`, and `apiUrl` in the package, since no GraphQL read
359
366
  returns them — **persist them per install**. `apiUrl` is the install's **base API
360
367
  URL**: pass it straight through (don't reconstruct it from `appId`) as the access
package/dist/index.cjs CHANGED
@@ -191,6 +191,12 @@ var AVAILABILITY_TIMES_QUERY = `
191
191
  }
192
192
  }
193
193
  status
194
+ minimumTicketsRequired
195
+ price {
196
+ amount
197
+ currency
198
+ formatted
199
+ }
194
200
  availability {
195
201
  qty
196
202
  taken
@@ -200,6 +206,24 @@ var AVAILABILITY_TIMES_QUERY = `
200
206
  }
201
207
  `;
202
208
 
209
+ // src/internal/peek/availability/availability-converter.ts
210
+ var DEFAULT_MINIMUM_TICKETS = 1;
211
+ function toAvailabilityTime(node) {
212
+ const rawMinimum = node.minimumTicketsRequired;
213
+ const minimumTicketsRequired = typeof rawMinimum === "number" && rawMinimum > 0 ? rawMinimum : DEFAULT_MINIMUM_TICKETS;
214
+ return {
215
+ id: node.id,
216
+ time: node.time,
217
+ from: node.from,
218
+ end: node.end,
219
+ duration: node.duration,
220
+ status: node.status,
221
+ minimumTicketsRequired,
222
+ price: node.price ?? null,
223
+ availability: node.availability ?? []
224
+ };
225
+ }
226
+
203
227
  // src/internal/peek/availability/availability-service.ts
204
228
  var AvailabilityService = class {
205
229
  constructor(client) {
@@ -217,7 +241,7 @@ var AvailabilityService = class {
217
241
  date: query.date
218
242
  }
219
243
  );
220
- return body.data?.availabilityTimes ?? [];
244
+ return (body.data?.availabilityTimes ?? []).map(toAvailabilityTime);
221
245
  }
222
246
  };
223
247
 
@@ -225,8 +249,6 @@ var AvailabilityService = class {
225
249
  function resolveAccessOptions(options) {
226
250
  return { fullCustomerAccess: options?.fullCustomerAccess ?? false };
227
251
  }
228
-
229
- // src/errors.ts
230
252
  var FORBIDDEN_STATUS = 403;
231
253
  var PERMISSION_MESSAGE_PREFIX = "CNG request forbidden: the app is missing the required permission";
232
254
  var PERMISSION_MESSAGE_UNKNOWN = "CNG request forbidden: the app is missing a required permission";
@@ -326,6 +348,21 @@ var AcmeApiError = class extends Error {
326
348
  this.body = body;
327
349
  }
328
350
  };
351
+ var INSTALL_WEBHOOK_INSTALL_ID_MISMATCH = "install_id_mismatch";
352
+ var INSTALL_WEBHOOK_MISSING_SUBJECT = "missing_subject";
353
+ var INSTALL_WEBHOOK_MESSAGES = {
354
+ [INSTALL_WEBHOOK_INSTALL_ID_MISMATCH]: "Install webhook body install_id does not match the token subject",
355
+ [INSTALL_WEBHOOK_MISSING_SUBJECT]: "Install webhook token has no subject (install id)"
356
+ };
357
+ var InstallWebhookError = class extends jwt__namespace.default.JsonWebTokenError {
358
+ /** Why the delivery was rejected. */
359
+ reason;
360
+ constructor(reason) {
361
+ super(INSTALL_WEBHOOK_MESSAGES[reason]);
362
+ this.name = "InstallWebhookError";
363
+ this.reason = reason;
364
+ }
365
+ };
329
366
 
330
367
  // src/models/peek/product.ts
331
368
  var ACTIVITY_PRODUCT_TYPE = "ACTIVITY";
@@ -2791,7 +2828,7 @@ async function requestWithRetry(http, url, init, label, onResponse) {
2791
2828
  // src/internal/sdk-headers.ts
2792
2829
  var SDK_HEADER_NAME = "x-peek-sdk";
2793
2830
  var SDK_PLATFORM = "js";
2794
- var SDK_VERSION = "1.0.0";
2831
+ var SDK_VERSION = "1.1.0";
2795
2832
  var SDK_HEADER_VALUE = `${SDK_PLATFORM}-${SDK_VERSION}`;
2796
2833
 
2797
2834
  // src/internal/peek/graphql-client.ts
@@ -5281,10 +5318,11 @@ function toPeekPlatform(raw) {
5281
5318
  function parseInstallWebhook(token, body, secret) {
5282
5319
  const payload = verifyPeekJwt(token, secret);
5283
5320
  const parsed = extractInstallWebhookBody(body);
5321
+ const installId = resolveInstallId(payload.sub, parsed.install_id);
5284
5322
  const account = parsed.account ?? {};
5285
5323
  const rawStatus = parsed.status || payload.status || "";
5286
5324
  return {
5287
- installId: parsed.install_id || payload.sub || "",
5325
+ installId,
5288
5326
  accountId: account.id || payload.account?.id || "",
5289
5327
  accountName: account.name || "",
5290
5328
  platform: toPeekPlatform(account.platform || ""),
@@ -5297,6 +5335,14 @@ function parseInstallWebhook(token, body, secret) {
5297
5335
  user: resolveInstallUser(parsed.modified_by, payload.user)
5298
5336
  };
5299
5337
  }
5338
+ function resolveInstallId(rawSubject, bodyInstallId) {
5339
+ const tokenSubject = cleanTokenString(rawSubject);
5340
+ if (tokenSubject === null) throw new InstallWebhookError(INSTALL_WEBHOOK_MISSING_SUBJECT);
5341
+ if (bodyInstallId && bodyInstallId !== tokenSubject) {
5342
+ throw new InstallWebhookError(INSTALL_WEBHOOK_INSTALL_ID_MISMATCH);
5343
+ }
5344
+ return tokenSubject;
5345
+ }
5300
5346
  function resolveInstallUser(bodyUser, tokenUser) {
5301
5347
  if (cleanTokenString(bodyUser?.id) !== null) return mapPeekTokenUser(bodyUser);
5302
5348
  return mapPeekTokenUser(tokenUser);
@@ -5328,6 +5374,7 @@ exports.CngProductService = CngProductService;
5328
5374
  exports.CustomQuestionService = CustomQuestionService;
5329
5375
  exports.DailyNoteService = DailyNoteService;
5330
5376
  exports.INSTALL_STATUSES = INSTALL_STATUSES;
5377
+ exports.InstallWebhookError = InstallWebhookError;
5331
5378
  exports.MembershipService = MembershipService;
5332
5379
  exports.PeekAccessService = PeekAccessService;
5333
5380
  exports.PeekGraphQLError = PeekGraphQLError;