@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 +51 -50
- package/dist/index.cjs +43 -46
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +100 -84
- package/dist/index.d.ts +100 -84
- package/dist/index.js +43 -46
- package/dist/index.js.map +1 -1
- package/docs/webhooks.md +107 -128
- package/llms.txt +27 -27
- package/package.json +1 -1
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 `
|
|
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
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
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
|
-
|
|
258
|
+
parseInstallWebhook,
|
|
258
259
|
parseWaiverWebhook,
|
|
259
|
-
verifyInstallWebhook,
|
|
260
260
|
type Booking,
|
|
261
|
-
type
|
|
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-
|
|
278
|
-
|
|
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
|
|
281
|
-
case "uninstalled": await deprovision(event
|
|
282
|
-
case "update_installed": await recordVersion(event
|
|
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
|
|
305
|
-
|
|
306
|
-
|
|
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.
|
|
317
|
-
|
|
318
|
-
`
|
|
319
|
-
|
|
320
|
-
`
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
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.
|
|
3590
|
-
*
|
|
3591
|
-
*
|
|
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
|
-
|
|
4304
|
-
|
|
4305
|
-
|
|
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:
|
|
4308
|
-
accountId: account
|
|
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:
|
|
4320
|
-
|
|
4323
|
+
displayVersion: payload.display_version ?? "",
|
|
4324
|
+
user: payload.user ? mapPeekTokenUser(payload.user) : null
|
|
4321
4325
|
};
|
|
4322
4326
|
}
|
|
4323
|
-
|
|
4324
|
-
|
|
4325
|
-
|
|
4326
|
-
|
|
4327
|
-
}
|
|
4328
|
-
|
|
4329
|
-
|
|
4330
|
-
|
|
4331
|
-
|
|
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.
|
|
4369
|
+
exports.parseInstallWebhook = parseInstallWebhook;
|
|
4373
4370
|
exports.parseWaiverWebhook = parseWaiverWebhook;
|
|
4374
4371
|
exports.verifyInstallWebhook = verifyInstallWebhook;
|
|
4375
4372
|
//# sourceMappingURL=index.cjs.map
|