@peektravel/app-utilities 0.7.0 → 0.7.2

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
@@ -276,8 +276,9 @@ app.post("/waiver-webhook", (req, res) => {
276
276
  app.post("/install-webhook", async (req, res) => {
277
277
  let event: InstallWebhook;
278
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!);
279
+ // Token rides in the `x-peek-auth` header (a `Bearer ` prefix is stripped for you):
280
+ const token = req.header("x-peek-auth") ?? "";
281
+ event = parseInstallWebhook(token, req.body, process.env.PEEK_INTERNAL_SECRET!);
281
282
  } catch {
282
283
  return res.sendStatus(401); // bad signature / issuer / audience / expired
283
284
  }
@@ -301,16 +302,27 @@ is the receiver's job**. `parseWaiverWebhook` also takes an optional
301
302
  below.
302
303
 
303
304
  `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
+ signed `app_registry_v2` JWT plus a JSON body. Peek sends the token in the
306
+ `x-peek-auth` header — pass that value straight in (`token` accepts it with or
307
+ without a `Bearer ` prefix). It validates the HMAC signature,
305
308
  expiry, issuer, and `"Joken"` audience (the same checks as `verifyPeekAuthToken`,
306
309
  throwing the underlying `jsonwebtoken` error — or `InvalidPeekTokenError` when a
307
310
  present `user` block has no `id`), then returns one flat, JSON-safe
308
311
  `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
312
+ `accountName`, `platform`, `isTest`, `timezone`, `apiUrl`, `status`, `rawStatus`,
313
+ `displayVersion`, and a **nullable** `user`. The **JSON body is the source of the
314
+ event data**; the fields it shares with the token (`installId` / `accountId` /
315
+ `status` / `displayVersion` / `user`) fall back to the **verified token** when
316
+ the body omits them. The token authenticates the whole delivery, so the body is
317
+ trusted within a verified request. This webhook is the only source of
318
+ `accountId`, `timezone`, and `apiUrl` in the package, since no GraphQL read
319
+ returns them — **persist them per install**. `apiUrl` is the install's app
320
+ endpoint: use it **as given** (don't reconstruct it from `appId`) — pass it as the
321
+ access service's `apiUrl`, or hand the whole install to
322
+ `createAccessServiceForInstall(install, { jwtSecret, issuer })`, which picks the
323
+ service by `platform` and wires the endpoint for you. Every event is a **full
324
+ snapshot** — an `update_installed` carries the same fields as the original
325
+ `installed`, so upsert by `installId` and take the latest `apiUrl`. `status` and
314
326
  `platform` are **nullable** — a value this version doesn't recognise stays `null`
315
327
  (wire value on `rawStatus`) instead of being coerced, so a newer registry can't be
316
328
  mistaken for an older one. (The older `verifyInstallWebhook(token, secret)` is
package/dist/index.cjs CHANGED
@@ -2044,7 +2044,8 @@ var GraphQLClient = class {
2044
2044
  );
2045
2045
  }
2046
2046
  endpoint(endpointName) {
2047
- const { baseUrl, appId, endpointPathPrefix } = this.options;
2047
+ const { apiUrl, baseUrl, appId, endpointPathPrefix } = this.options;
2048
+ if (apiUrl) return apiUrl;
2048
2049
  const prefix = endpointPathPrefix ? `${endpointPathPrefix}/` : "";
2049
2050
  return `${baseUrl}/${appId}/${prefix}${endpointName}`;
2050
2051
  }
@@ -2061,8 +2062,13 @@ var GraphQLClient = class {
2061
2062
  };
2062
2063
  var PEEK_TOKEN_ISSUER = "app_registry_v2";
2063
2064
  var PEEK_TOKEN_AUDIENCE = "Joken";
2065
+ var BEARER_PREFIX = "bearer ";
2066
+ function stripBearer(token) {
2067
+ const trimmed = token.trim();
2068
+ return trimmed.toLowerCase().startsWith(BEARER_PREFIX) ? trimmed.slice(BEARER_PREFIX.length).trim() : trimmed;
2069
+ }
2064
2070
  function verifyPeekJwt(token, secret) {
2065
- return jwt__namespace.verify(token, secret, {
2071
+ return jwt__namespace.verify(stripBearer(token), secret, {
2066
2072
  issuer: PEEK_TOKEN_ISSUER,
2067
2073
  audience: PEEK_TOKEN_AUDIENCE
2068
2074
  });
@@ -3556,25 +3562,27 @@ var PeekAccessService = class {
3556
3562
  bookingService;
3557
3563
  reviewService;
3558
3564
  constructor(config) {
3565
+ const hasApiUrl = !!config.apiUrl;
3559
3566
  const isV2 = config.mode === "v2";
3560
3567
  requireNonEmpty(config.installId, "installId", "PeekAccessService");
3561
3568
  requireNonEmpty(config.jwtSecret, "jwtSecret", "PeekAccessService");
3562
3569
  requireNonEmpty(config.issuer, "issuer", "PeekAccessService");
3563
- requireNonEmpty(config.appId, "appId", "PeekAccessService");
3564
- if (!isV2) requireNonEmpty(config.gatewayKey ?? "", "gatewayKey", "PeekAccessService");
3570
+ if (!hasApiUrl) requireNonEmpty(config.appId ?? "", "appId", "PeekAccessService");
3571
+ if (!hasApiUrl && !isV2) requireNonEmpty(config.gatewayKey ?? "", "gatewayKey", "PeekAccessService");
3565
3572
  this.jwtSecret = config.jwtSecret;
3566
3573
  const logger = config.logger ?? noopLogger;
3567
3574
  const tokens = createTokenManager(config);
3568
3575
  const defaultBaseUrl = isV2 ? DEFAULT_V2_BASE_URL : DEFAULT_BASE_URL;
3569
3576
  this.client = new GraphQLClient({
3570
- baseUrl: config.baseUrl ?? defaultBaseUrl,
3571
- appId: config.appId,
3577
+ apiUrl: config.apiUrl,
3578
+ baseUrl: hasApiUrl ? void 0 : config.baseUrl ?? defaultBaseUrl,
3579
+ appId: hasApiUrl ? void 0 : config.appId,
3572
3580
  gatewayKey: config.gatewayKey,
3573
3581
  getToken: () => tokens.getToken(),
3574
3582
  retryDelaysMs: config.retryDelaysMs ?? DEFAULT_RETRY_DELAYS_MS,
3575
3583
  logger,
3576
3584
  fetchFn: config.fetch ?? globalThis.fetch,
3577
- endpointPathPrefix: isV2 ? V2_EXTENDABLE_SLUG : void 0
3585
+ endpointPathPrefix: !hasApiUrl && isV2 ? V2_EXTENDABLE_SLUG : void 0
3578
3586
  });
3579
3587
  this.productServiceOptions = {
3580
3588
  itemOptionsPageSize: config.itemOptionsPageSize
@@ -3586,9 +3594,10 @@ var PeekAccessService = class {
3586
3594
  * decoded claims.
3587
3595
  *
3588
3596
  * Validates the HMAC signature (using this service's `jwtSecret`), the token
3589
- * expiry, the `"app_registry_v2"` issuer, and the `"Joken"` audience. Throws
3590
- * from the `jsonwebtoken` library on any failure — callers should catch to
3591
- * distinguish error kinds:
3597
+ * expiry, the `"app_registry_v2"` issuer, and the `"Joken"` audience. Pass the
3598
+ * raw `x-peek-auth` request header value — a leading `Bearer ` prefix, if
3599
+ * present, is stripped for you. Throws from the `jsonwebtoken` library on any
3600
+ * failure — callers should catch to distinguish error kinds:
3592
3601
  *
3593
3602
  * - `JsonWebTokenError` — signature invalid, wrong issuer/audience, or token
3594
3603
  * malformed
@@ -4001,7 +4010,8 @@ var RestClient = class {
4001
4010
  );
4002
4011
  }
4003
4012
  endpoint(path) {
4004
- const { baseUrl, appId, extendableSlug } = this.options;
4013
+ const { apiUrl, baseUrl, appId, extendableSlug } = this.options;
4014
+ if (apiUrl) return `${apiUrl}/${path}`;
4005
4015
  return `${baseUrl}/${appId}/${extendableSlug}/${path}`;
4006
4016
  }
4007
4017
  buildHeaders() {
@@ -4018,15 +4028,17 @@ var CngAccessService = class {
4018
4028
  client;
4019
4029
  productService;
4020
4030
  constructor(config) {
4031
+ const hasApiUrl = !!config.apiUrl;
4021
4032
  requireNonEmpty(config.installId, "installId", "CngAccessService");
4022
4033
  requireNonEmpty(config.jwtSecret, "jwtSecret", "CngAccessService");
4023
4034
  requireNonEmpty(config.issuer, "issuer", "CngAccessService");
4024
- requireNonEmpty(config.appId, "appId", "CngAccessService");
4035
+ if (!hasApiUrl) requireNonEmpty(config.appId ?? "", "appId", "CngAccessService");
4025
4036
  const logger = config.logger ?? noopLogger;
4026
4037
  const tokens = createTokenManager(config);
4027
4038
  this.client = new RestClient({
4028
- baseUrl: config.baseUrl ?? DEFAULT_BASE_URL2,
4029
- appId: config.appId,
4039
+ apiUrl: config.apiUrl,
4040
+ baseUrl: hasApiUrl ? void 0 : config.baseUrl ?? DEFAULT_BASE_URL2,
4041
+ appId: hasApiUrl ? void 0 : config.appId,
4030
4042
  extendableSlug: CNG_EXTENDABLE_SLUG,
4031
4043
  getToken: () => tokens.getToken(),
4032
4044
  retryDelaysMs: config.retryDelaysMs ?? DEFAULT_RETRY_DELAYS_MS,
@@ -4132,7 +4144,8 @@ var RestClient2 = class {
4132
4144
  );
4133
4145
  }
4134
4146
  endpoint(path) {
4135
- const { baseUrl, appId, extendableSlug } = this.options;
4147
+ const { apiUrl, baseUrl, appId, extendableSlug } = this.options;
4148
+ if (apiUrl) return `${apiUrl}/${path}`;
4136
4149
  return `${baseUrl}/${appId}/${extendableSlug}/${path}`;
4137
4150
  }
4138
4151
  buildHeaders() {
@@ -4149,15 +4162,17 @@ var AcmeAccessService = class {
4149
4162
  client;
4150
4163
  productService;
4151
4164
  constructor(config) {
4165
+ const hasApiUrl = !!config.apiUrl;
4152
4166
  requireNonEmpty(config.installId, "installId", "AcmeAccessService");
4153
4167
  requireNonEmpty(config.jwtSecret, "jwtSecret", "AcmeAccessService");
4154
4168
  requireNonEmpty(config.issuer, "issuer", "AcmeAccessService");
4155
- requireNonEmpty(config.appId, "appId", "AcmeAccessService");
4169
+ if (!hasApiUrl) requireNonEmpty(config.appId ?? "", "appId", "AcmeAccessService");
4156
4170
  const logger = config.logger ?? noopLogger;
4157
4171
  const tokens = createTokenManager(config);
4158
4172
  this.client = new RestClient2({
4159
- baseUrl: config.baseUrl ?? DEFAULT_BASE_URL3,
4160
- appId: config.appId,
4173
+ apiUrl: config.apiUrl,
4174
+ baseUrl: hasApiUrl ? void 0 : config.baseUrl ?? DEFAULT_BASE_URL3,
4175
+ appId: hasApiUrl ? void 0 : config.appId,
4161
4176
  extendableSlug: ACME_EXTENDABLE_SLUG,
4162
4177
  getToken: () => tokens.getToken(),
4163
4178
  retryDelaysMs: config.retryDelaysMs ?? DEFAULT_RETRY_DELAYS_MS,
@@ -4181,6 +4196,23 @@ var AcmeAccessService = class {
4181
4196
  }
4182
4197
  };
4183
4198
 
4199
+ // src/access-service-factory.ts
4200
+ function createAccessServiceForInstall(install, config) {
4201
+ const shared = { ...config, installId: install.installId, apiUrl: install.apiUrl };
4202
+ switch (install.platform) {
4203
+ case "peek":
4204
+ return new PeekAccessService(shared);
4205
+ case "cng":
4206
+ return new CngAccessService(shared);
4207
+ case "acme":
4208
+ return new AcmeAccessService(shared);
4209
+ default:
4210
+ throw new Error(
4211
+ `createAccessServiceForInstall: unknown or missing platform "${install.platform}" for install "${install.installId}"`
4212
+ );
4213
+ }
4214
+ }
4215
+
4184
4216
  // src/internal/peek/bookings/booking-webhook.ts
4185
4217
  var PAYLOAD_BOOKING_KEY = "booking";
4186
4218
  var VALUE_OPEN_TOKEN = "value {";
@@ -4304,20 +4336,27 @@ function toPeekPlatform(raw) {
4304
4336
  // src/internal/peek/installs/install-webhook.ts
4305
4337
  function parseInstallWebhook(token, body, secret) {
4306
4338
  const payload = verifyPeekJwt(token, secret);
4307
- const account = extractInstallWebhookBody(body).account ?? {};
4308
- const rawStatus = payload.status ?? "";
4339
+ const parsed = extractInstallWebhookBody(body);
4340
+ const account = parsed.account ?? {};
4341
+ const rawStatus = parsed.status || payload.status || "";
4309
4342
  return {
4310
- installId: payload.sub ?? "",
4311
- accountId: payload.account?.id ?? "",
4343
+ installId: parsed.install_id || payload.sub || "",
4344
+ accountId: account.id || payload.account?.id || "",
4312
4345
  accountName: account.name || "",
4313
4346
  platform: toPeekPlatform(account.platform || ""),
4314
4347
  isTest: Boolean(account.is_test),
4348
+ timezone: account.timezone || "",
4349
+ apiUrl: parsed.api?.url || "",
4315
4350
  status: toInstallStatus(rawStatus),
4316
4351
  rawStatus,
4317
- displayVersion: payload.display_version ?? "",
4318
- user: payload.user ? mapPeekTokenUser(payload.user) : null
4352
+ displayVersion: parsed.display_version || payload.display_version || "",
4353
+ user: resolveInstallUser(parsed.modified_by, payload.user)
4319
4354
  };
4320
4355
  }
4356
+ function resolveInstallUser(bodyUser, tokenUser) {
4357
+ if (bodyUser?.id) return mapPeekTokenUser(bodyUser);
4358
+ return tokenUser ? mapPeekTokenUser(tokenUser) : null;
4359
+ }
4321
4360
  function verifyInstallWebhook(token, secret) {
4322
4361
  const payload = verifyPeekJwt(token, secret);
4323
4362
  return {
@@ -4358,6 +4397,7 @@ exports.ResellerService = ResellerService;
4358
4397
  exports.ResourcePoolService = ResourcePoolService;
4359
4398
  exports.ReviewService = ReviewService;
4360
4399
  exports.TimeslotService = TimeslotService;
4400
+ exports.createAccessServiceForInstall = createAccessServiceForInstall;
4361
4401
  exports.noopLogger = noopLogger;
4362
4402
  exports.parseBookingWebhook = parseBookingWebhook;
4363
4403
  exports.parseInstallWebhook = parseInstallWebhook;