@peektravel/app-utilities 0.6.1 → 0.7.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
@@ -124,7 +124,8 @@ Two kinds of failures surface as exceptions:
124
124
  - `PiiAccessDisabledError` — a payment / booking-modification operation was
125
125
  called on an access service created without `fullCustomerAccess` (see [Access options
126
126
  / PII](#access-options--pii)). Carries `.operation` (the blocked method name).
127
- - `InvalidPeekTokenError` — `verifyPeekAuthToken` (or `verifyInstallWebhook`)
127
+ - `InvalidPeekTokenError` — `verifyPeekAuthToken` (or `parseInstallWebhook` /
128
+ `verifyInstallWebhook`)
128
129
  received a token whose signature is valid but whose `user` block has no `id`.
129
130
  Carries `.field` (`"user.id"`). Thrown alongside the `jsonwebtoken` errors.
130
131
 
@@ -245,22 +246,20 @@ Functions runtime) resolve correctly. Its only runtime dependency is
245
246
 
246
247
  ## Webhooks
247
248
 
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:
249
+ Receiver apps can consume Peek **booking**, **waiver**, and **install** webhooks
250
+ without hand-writing the payload handling. The booking and waiver webhooks have a
251
+ pure parser (construct nothing — no auth/network) that returns a clean model; the
252
+ install webhook delivers a signed token alongside a JSON body, so its helper
253
+ **verifies** the token before merging the two:
253
254
 
254
255
  ```ts
255
256
  import {
256
257
  parseBookingWebhook,
257
- parseInstallEvent,
258
+ parseInstallWebhook,
258
259
  parseWaiverWebhook,
259
- verifyInstallWebhook,
260
260
  type Booking,
261
- type InstallEvent,
261
+ type InstallWebhook,
262
262
  type Waiver,
263
- type InstallWebhookClaims,
264
263
  } from "@peektravel/app-utilities";
265
264
 
266
265
  app.post("/booking-webhook", (req, res) => {
@@ -274,57 +273,59 @@ app.post("/waiver-webhook", (req, res) => {
274
273
  res.sendStatus(200);
275
274
  });
276
275
 
277
- app.post("/install-event", async (req, res) => {
278
- const event: InstallEvent = parseInstallEvent(req.body);
276
+ app.post("/install-webhook", async (req, res) => {
277
+ let event: InstallWebhook;
278
+ try {
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!);
282
+ } catch {
283
+ return res.sendStatus(401); // bad signature / issuer / audience / expired
284
+ }
279
285
  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;
286
+ case "installed": await provision(event); break;
287
+ case "uninstalled": await deprovision(event); break;
288
+ case "update_installed": await recordVersion(event); break;
283
289
  // `status` is null when Peek sends one this version doesn't know. Peek
284
290
  // doesn't redeliver a 2xx, so fail loudly rather than silently no-op.
285
291
  default: return res.status(500).json({ error: `unknown status: ${event.rawStatus}` });
286
292
  }
287
293
  res.sendStatus(200);
288
294
  });
289
-
290
- app.post("/install-webhook", (req, res) => {
291
- try {
292
- // The payload IS a signed JWT — verify signature/issuer/audience/expiry:
293
- const claims: InstallWebhookClaims = verifyInstallWebhook(req.body, process.env.PEEK_INTERNAL_SECRET!);
294
- if (claims.status === "uninstalled") {
295
- /* tear down this install */
296
- }
297
- res.sendStatus(200);
298
- } catch {
299
- res.sendStatus(401); // bad signature / issuer / audience / expired
300
- }
301
- });
302
295
  ```
303
296
 
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
297
+ The booking and waiver parsers tolerate the delivery envelope / a bare node / a
298
+ JSON string and never throw on malformed input; **authenticating those deliveries
299
+ is the receiver's job**. `parseWaiverWebhook` also takes an optional
314
300
  `AccessOptions` (`{ fullCustomerAccess }`) and redacts the participant `guestName`
315
301
  + document `fileUrl` by default — see [Access options / PII](#access-options--pii)
316
- below. `verifyInstallWebhook(token, secret)` is different: the payload is a signed
317
- `app_registry_v2` JWT, so it validates the HMAC signature, expiry, issuer, and
318
- `"Joken"` audience (the same checks as `verifyPeekAuthToken`) and returns typed
319
- claims (`installId`, `account.id`, `status`, `displayVersion`, and a **nullable**
320
- `user` for system-initiated events), throwing the underlying `jsonwebtoken` error
321
- on any failure — or `InvalidPeekTokenError` when a present `user` block has no
322
- `id`. The booking and waiver webhooks differ on registration: a
323
- **booking** webhook's payload shape is set by a GraphQL query configured **once in
324
- an external system** (the App Store `broadcast_to_url` config) — this package
325
- documents and drift-guards the exact query to paste there — whereas the **waiver**
326
- and **install-status** webhooks have fixed payloads, so you just subscribe to
327
- their event with no query. **The query to register and the full guide:
302
+ below.
303
+
304
+ `parseInstallWebhook(token, body, secret)` is different: the delivery carries a
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,
308
+ expiry, issuer, and `"Joken"` audience (the same checks as `verifyPeekAuthToken`,
309
+ throwing the underlying `jsonwebtoken` error — or `InvalidPeekTokenError` when a
310
+ present `user` block has no `id`), then returns one flat, JSON-safe
311
+ `InstallWebhook`: `installId`, `accountId` (**also called the partner ID**),
312
+ `accountName`, `platform`, `isTest`, `status`, `rawStatus`, `displayVersion`, and
313
+ a **nullable** `user`. The **verified token** is authoritative for `installId` /
314
+ `accountId` / `status` / `displayVersion` / `user`; the unsigned body supplies
315
+ only `accountName` / `platform` / `isTest`. This webhook is the only source of
316
+ `accountId` in the package, since no GraphQL read returns it. `status` and
317
+ `platform` are **nullable** — a value this version doesn't recognise stays `null`
318
+ (wire value on `rawStatus`) instead of being coerced, so a newer registry can't be
319
+ mistaken for an older one. (The older `verifyInstallWebhook(token, secret)` is
320
+ still exported but **`@deprecated`** — it reads only the token, missing the
321
+ account name/platform/test flag; `parseInstallEvent` has been **removed**.)
322
+
323
+ The booking and waiver webhooks differ on registration: a **booking** webhook's
324
+ payload shape is set by a GraphQL query configured **once in an external system**
325
+ (the App Store `broadcast_to_url` config) — this package documents and
326
+ drift-guards the exact query to paste there — whereas the **waiver** and
327
+ **install** webhooks have fixed payloads, so you just subscribe to their event
328
+ with no query. **The query to register and the full guide:
328
329
  [`docs/webhooks.md`](docs/webhooks.md) (shipped).**
329
330
 
330
331
  ## UI components (`/ui`)
package/dist/index.cjs CHANGED
@@ -2061,8 +2061,13 @@ var GraphQLClient = class {
2061
2061
  };
2062
2062
  var PEEK_TOKEN_ISSUER = "app_registry_v2";
2063
2063
  var PEEK_TOKEN_AUDIENCE = "Joken";
2064
+ var BEARER_PREFIX = "bearer ";
2065
+ function stripBearer(token) {
2066
+ const trimmed = token.trim();
2067
+ return trimmed.toLowerCase().startsWith(BEARER_PREFIX) ? trimmed.slice(BEARER_PREFIX.length).trim() : trimmed;
2068
+ }
2064
2069
  function verifyPeekJwt(token, secret) {
2065
- return jwt__namespace.verify(token, secret, {
2070
+ return jwt__namespace.verify(stripBearer(token), secret, {
2066
2071
  issuer: PEEK_TOKEN_ISSUER,
2067
2072
  audience: PEEK_TOKEN_AUDIENCE
2068
2073
  });
@@ -3586,9 +3591,10 @@ var PeekAccessService = class {
3586
3591
  * decoded claims.
3587
3592
  *
3588
3593
  * 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:
3594
+ * expiry, the `"app_registry_v2"` issuer, and the `"Joken"` audience. Pass the
3595
+ * raw `x-peek-auth` request header value — a leading `Bearer ` prefix, if
3596
+ * present, is stripped for you. Throws from the `jsonwebtoken` library on any
3597
+ * failure — callers should catch to distinguish error kinds:
3592
3598
  *
3593
3599
  * - `JsonWebTokenError` — signature invalid, wrong issuer/audience, or token
3594
3600
  * malformed
@@ -4271,18 +4277,6 @@ function extractWaiverNode(payload) {
4271
4277
  return record;
4272
4278
  }
4273
4279
 
4274
- // src/internal/peek/installs/install-webhook.ts
4275
- function verifyInstallWebhook(token, secret) {
4276
- const payload = verifyPeekJwt(token, secret);
4277
- return {
4278
- installId: payload.sub ?? "",
4279
- account: { id: payload.account?.id ?? "" },
4280
- status: payload.status ?? "",
4281
- displayVersion: payload.display_version ?? "",
4282
- user: payload.user ? mapPeekTokenUser(payload.user) : null
4283
- };
4284
- }
4285
-
4286
4280
  // src/models/peek/auth-token.ts
4287
4281
  var PEEK_PLATFORMS = ["peek", "cng", "acme"];
4288
4282
 
@@ -4294,48 +4288,51 @@ var INSTALL_STATUSES = [
4294
4288
  ];
4295
4289
 
4296
4290
  // src/internal/peek/installs/install-converter.ts
4291
+ function extractInstallWebhookBody(payload) {
4292
+ let body = payload;
4293
+ if (typeof body === "string") {
4294
+ try {
4295
+ body = JSON.parse(body);
4296
+ } catch {
4297
+ return {};
4298
+ }
4299
+ }
4300
+ if (!body || typeof body !== "object") return {};
4301
+ return body;
4302
+ }
4297
4303
  function toInstallStatus(raw) {
4298
4304
  return INSTALL_STATUSES.includes(raw) ? raw : null;
4299
4305
  }
4300
4306
  function toPeekPlatform(raw) {
4301
4307
  return PEEK_PLATFORMS.includes(raw) ? raw : null;
4302
4308
  }
4303
- function fromInstallIdentityNode(node) {
4304
- const data = node ?? {};
4305
- const account = data.account ?? {};
4309
+
4310
+ // src/internal/peek/installs/install-webhook.ts
4311
+ function parseInstallWebhook(token, body, secret) {
4312
+ const payload = verifyPeekJwt(token, secret);
4313
+ const account = extractInstallWebhookBody(body).account ?? {};
4314
+ const rawStatus = payload.status ?? "";
4306
4315
  return {
4307
- installId: data.install_id || "",
4308
- accountId: account.id || "",
4316
+ installId: payload.sub ?? "",
4317
+ accountId: payload.account?.id ?? "",
4309
4318
  accountName: account.name || "",
4310
4319
  platform: toPeekPlatform(account.platform || ""),
4311
- isTest: Boolean(account.is_test)
4312
- };
4313
- }
4314
- function fromInstallEventNode(node) {
4315
- const rawStatus = node?.status || "";
4316
- return {
4320
+ isTest: Boolean(account.is_test),
4317
4321
  status: toInstallStatus(rawStatus),
4318
4322
  rawStatus,
4319
- displayVersion: node?.display_version || "",
4320
- identity: fromInstallIdentityNode(node)
4323
+ displayVersion: payload.display_version ?? "",
4324
+ user: payload.user ? mapPeekTokenUser(payload.user) : null
4321
4325
  };
4322
4326
  }
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;
4327
+ function verifyInstallWebhook(token, secret) {
4328
+ const payload = verifyPeekJwt(token, secret);
4329
+ return {
4330
+ installId: payload.sub ?? "",
4331
+ account: { id: payload.account?.id ?? "" },
4332
+ status: payload.status ?? "",
4333
+ displayVersion: payload.display_version ?? "",
4334
+ user: payload.user ? mapPeekTokenUser(payload.user) : null
4335
+ };
4339
4336
  }
4340
4337
 
4341
4338
  exports.ACTIVITY_PRODUCT_TYPE = ACTIVITY_PRODUCT_TYPE;
@@ -4369,7 +4366,7 @@ exports.ReviewService = ReviewService;
4369
4366
  exports.TimeslotService = TimeslotService;
4370
4367
  exports.noopLogger = noopLogger;
4371
4368
  exports.parseBookingWebhook = parseBookingWebhook;
4372
- exports.parseInstallEvent = parseInstallEvent;
4369
+ exports.parseInstallWebhook = parseInstallWebhook;
4373
4370
  exports.parseWaiverWebhook = parseWaiverWebhook;
4374
4371
  exports.verifyInstallWebhook = verifyInstallWebhook;
4375
4372
  //# sourceMappingURL=index.cjs.map