@peektravel/app-utilities 1.0.1 → 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
@@ -249,8 +249,6 @@ var AvailabilityService = class {
249
249
  function resolveAccessOptions(options) {
250
250
  return { fullCustomerAccess: options?.fullCustomerAccess ?? false };
251
251
  }
252
-
253
- // src/errors.ts
254
252
  var FORBIDDEN_STATUS = 403;
255
253
  var PERMISSION_MESSAGE_PREFIX = "CNG request forbidden: the app is missing the required permission";
256
254
  var PERMISSION_MESSAGE_UNKNOWN = "CNG request forbidden: the app is missing a required permission";
@@ -350,6 +348,21 @@ var AcmeApiError = class extends Error {
350
348
  this.body = body;
351
349
  }
352
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
+ };
353
366
 
354
367
  // src/models/peek/product.ts
355
368
  var ACTIVITY_PRODUCT_TYPE = "ACTIVITY";
@@ -2815,7 +2828,7 @@ async function requestWithRetry(http, url, init, label, onResponse) {
2815
2828
  // src/internal/sdk-headers.ts
2816
2829
  var SDK_HEADER_NAME = "x-peek-sdk";
2817
2830
  var SDK_PLATFORM = "js";
2818
- var SDK_VERSION = "1.0.1";
2831
+ var SDK_VERSION = "1.1.0";
2819
2832
  var SDK_HEADER_VALUE = `${SDK_PLATFORM}-${SDK_VERSION}`;
2820
2833
 
2821
2834
  // src/internal/peek/graphql-client.ts
@@ -5305,10 +5318,11 @@ function toPeekPlatform(raw) {
5305
5318
  function parseInstallWebhook(token, body, secret) {
5306
5319
  const payload = verifyPeekJwt(token, secret);
5307
5320
  const parsed = extractInstallWebhookBody(body);
5321
+ const installId = resolveInstallId(payload.sub, parsed.install_id);
5308
5322
  const account = parsed.account ?? {};
5309
5323
  const rawStatus = parsed.status || payload.status || "";
5310
5324
  return {
5311
- installId: parsed.install_id || payload.sub || "",
5325
+ installId,
5312
5326
  accountId: account.id || payload.account?.id || "",
5313
5327
  accountName: account.name || "",
5314
5328
  platform: toPeekPlatform(account.platform || ""),
@@ -5321,6 +5335,14 @@ function parseInstallWebhook(token, body, secret) {
5321
5335
  user: resolveInstallUser(parsed.modified_by, payload.user)
5322
5336
  };
5323
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
+ }
5324
5346
  function resolveInstallUser(bodyUser, tokenUser) {
5325
5347
  if (cleanTokenString(bodyUser?.id) !== null) return mapPeekTokenUser(bodyUser);
5326
5348
  return mapPeekTokenUser(tokenUser);
@@ -5352,6 +5374,7 @@ exports.CngProductService = CngProductService;
5352
5374
  exports.CustomQuestionService = CustomQuestionService;
5353
5375
  exports.DailyNoteService = DailyNoteService;
5354
5376
  exports.INSTALL_STATUSES = INSTALL_STATUSES;
5377
+ exports.InstallWebhookError = InstallWebhookError;
5355
5378
  exports.MembershipService = MembershipService;
5356
5379
  exports.PeekAccessService = PeekAccessService;
5357
5380
  exports.PeekGraphQLError = PeekGraphQLError;