@peektravel/app-utilities 0.6.0 → 0.6.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 +39 -9
- package/dist/index.cjs +73 -0
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +146 -3
- package/dist/index.d.ts +146 -3
- package/dist/index.js +71 -1
- package/dist/index.js.map +1 -1
- package/docs/webhooks.md +115 -11
- package/llms.txt +23 -5
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -15,6 +15,10 @@ npm install @peektravel/app-utilities
|
|
|
15
15
|
npm update @peektravel/app-utilities
|
|
16
16
|
```
|
|
17
17
|
|
|
18
|
+
Before updating to a new version, consult [`changelog.md`](./changelog.md) —
|
|
19
|
+
it records every caller-visible change grouped by version, including any
|
|
20
|
+
breaking changes and the concrete action you need to take to migrate.
|
|
21
|
+
|
|
18
22
|
See [Releasing](#releasing) for how new versions are published.
|
|
19
23
|
|
|
20
24
|
## Usage
|
|
@@ -120,6 +124,9 @@ Two kinds of failures surface as exceptions:
|
|
|
120
124
|
- `PiiAccessDisabledError` — a payment / booking-modification operation was
|
|
121
125
|
called on an access service created without `fullCustomerAccess` (see [Access options
|
|
122
126
|
/ PII](#access-options--pii)). Carries `.operation` (the blocked method name).
|
|
127
|
+
- `InvalidPeekTokenError` — `verifyPeekAuthToken` (or `verifyInstallWebhook`)
|
|
128
|
+
received a token whose signature is valid but whose `user` block has no `id`.
|
|
129
|
+
Carries `.field` (`"user.id"`). Thrown alongside the `jsonwebtoken` errors.
|
|
123
130
|
|
|
124
131
|
**Plain `Error` validation/precondition failures** thrown by the service layer
|
|
125
132
|
*before* any network call — e.g. an empty config field, a `bookingId` that
|
|
@@ -238,18 +245,20 @@ Functions runtime) resolve correctly. Its only runtime dependency is
|
|
|
238
245
|
|
|
239
246
|
## Webhooks
|
|
240
247
|
|
|
241
|
-
Receiver apps can consume Peek **booking**, **waiver**,
|
|
242
|
-
webhooks without hand-writing the payload handling. The
|
|
243
|
-
webhooks have a pure parser (construct
|
|
244
|
-
clean model; the install-status webhook
|
|
245
|
-
**verifies** it:
|
|
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:
|
|
246
253
|
|
|
247
254
|
```ts
|
|
248
255
|
import {
|
|
249
256
|
parseBookingWebhook,
|
|
257
|
+
parseInstallEvent,
|
|
250
258
|
parseWaiverWebhook,
|
|
251
259
|
verifyInstallWebhook,
|
|
252
260
|
type Booking,
|
|
261
|
+
type InstallEvent,
|
|
253
262
|
type Waiver,
|
|
254
263
|
type InstallWebhookClaims,
|
|
255
264
|
} from "@peektravel/app-utilities";
|
|
@@ -265,6 +274,19 @@ app.post("/waiver-webhook", (req, res) => {
|
|
|
265
274
|
res.sendStatus(200);
|
|
266
275
|
});
|
|
267
276
|
|
|
277
|
+
app.post("/install-event", async (req, res) => {
|
|
278
|
+
const event: InstallEvent = parseInstallEvent(req.body);
|
|
279
|
+
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;
|
|
283
|
+
// `status` is null when Peek sends one this version doesn't know. Peek
|
|
284
|
+
// doesn't redeliver a 2xx, so fail loudly rather than silently no-op.
|
|
285
|
+
default: return res.status(500).json({ error: `unknown status: ${event.rawStatus}` });
|
|
286
|
+
}
|
|
287
|
+
res.sendStatus(200);
|
|
288
|
+
});
|
|
289
|
+
|
|
268
290
|
app.post("/install-webhook", (req, res) => {
|
|
269
291
|
try {
|
|
270
292
|
// The payload IS a signed JWT — verify signature/issuer/audience/expiry:
|
|
@@ -279,9 +301,16 @@ app.post("/install-webhook", (req, res) => {
|
|
|
279
301
|
});
|
|
280
302
|
```
|
|
281
303
|
|
|
282
|
-
The booking and
|
|
283
|
-
JSON string and never throw on malformed input; **authenticating
|
|
284
|
-
is the receiver's job**. `
|
|
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
|
|
285
314
|
`AccessOptions` (`{ fullCustomerAccess }`) and redacts the participant `guestName`
|
|
286
315
|
+ document `fileUrl` by default — see [Access options / PII](#access-options--pii)
|
|
287
316
|
below. `verifyInstallWebhook(token, secret)` is different: the payload is a signed
|
|
@@ -289,7 +318,8 @@ below. `verifyInstallWebhook(token, secret)` is different: the payload is a sign
|
|
|
289
318
|
`"Joken"` audience (the same checks as `verifyPeekAuthToken`) and returns typed
|
|
290
319
|
claims (`installId`, `account.id`, `status`, `displayVersion`, and a **nullable**
|
|
291
320
|
`user` for system-initiated events), throwing the underlying `jsonwebtoken` error
|
|
292
|
-
on any failure
|
|
321
|
+
on any failure — or `InvalidPeekTokenError` when a present `user` block has no
|
|
322
|
+
`id`. The booking and waiver webhooks differ on registration: a
|
|
293
323
|
**booking** webhook's payload shape is set by a GraphQL query configured **once in
|
|
294
324
|
an external system** (the App Store `broadcast_to_url` config) — this package
|
|
295
325
|
documents and drift-guards the exact query to paste there — whereas the **waiver**
|
package/dist/index.cjs
CHANGED
|
@@ -247,6 +247,15 @@ var PeekHttpError = class extends Error {
|
|
|
247
247
|
this.body = body;
|
|
248
248
|
}
|
|
249
249
|
};
|
|
250
|
+
var InvalidPeekTokenError = class extends Error {
|
|
251
|
+
/** The token field that was missing or empty (e.g. `"user.id"`). */
|
|
252
|
+
field;
|
|
253
|
+
constructor(field) {
|
|
254
|
+
super(`Peek token is missing required field "${field}"`);
|
|
255
|
+
this.name = "InvalidPeekTokenError";
|
|
256
|
+
this.field = field;
|
|
257
|
+
}
|
|
258
|
+
};
|
|
250
259
|
var PiiAccessDisabledError = class extends Error {
|
|
251
260
|
/** The name of the operation that was blocked (e.g. `"makePayment"`). */
|
|
252
261
|
operation;
|
|
@@ -2059,6 +2068,9 @@ function verifyPeekJwt(token, secret) {
|
|
|
2059
2068
|
});
|
|
2060
2069
|
}
|
|
2061
2070
|
function mapPeekTokenUser(u) {
|
|
2071
|
+
if (!u.id) {
|
|
2072
|
+
throw new InvalidPeekTokenError("user.id");
|
|
2073
|
+
}
|
|
2062
2074
|
return {
|
|
2063
2075
|
email: u.email,
|
|
2064
2076
|
id: u.id,
|
|
@@ -3582,10 +3594,13 @@ var PeekAccessService = class {
|
|
|
3582
3594
|
* malformed
|
|
3583
3595
|
* - `TokenExpiredError` — past `exp`
|
|
3584
3596
|
* - `NotBeforeError` — before `nbf`
|
|
3597
|
+
* - `InvalidPeekTokenError` — signature valid but the `user` block is missing
|
|
3598
|
+
* its `id`
|
|
3585
3599
|
*
|
|
3586
3600
|
* @throws {JsonWebTokenError} signature invalid or token malformed
|
|
3587
3601
|
* @throws {TokenExpiredError} token has expired
|
|
3588
3602
|
* @throws {NotBeforeError} token not yet valid
|
|
3603
|
+
* @throws {InvalidPeekTokenError} verified token has no `user.id`
|
|
3589
3604
|
*/
|
|
3590
3605
|
verifyPeekAuthToken(token) {
|
|
3591
3606
|
const payload = verifyPeekJwt(token, this.jwtSecret);
|
|
@@ -4268,6 +4283,61 @@ function verifyInstallWebhook(token, secret) {
|
|
|
4268
4283
|
};
|
|
4269
4284
|
}
|
|
4270
4285
|
|
|
4286
|
+
// src/models/peek/auth-token.ts
|
|
4287
|
+
var PEEK_PLATFORMS = ["peek", "cng", "acme"];
|
|
4288
|
+
|
|
4289
|
+
// src/models/peek/install.ts
|
|
4290
|
+
var INSTALL_STATUSES = [
|
|
4291
|
+
"installed",
|
|
4292
|
+
"uninstalled",
|
|
4293
|
+
"update_installed"
|
|
4294
|
+
];
|
|
4295
|
+
|
|
4296
|
+
// src/internal/peek/installs/install-converter.ts
|
|
4297
|
+
function toInstallStatus(raw) {
|
|
4298
|
+
return INSTALL_STATUSES.includes(raw) ? raw : null;
|
|
4299
|
+
}
|
|
4300
|
+
function toPeekPlatform(raw) {
|
|
4301
|
+
return PEEK_PLATFORMS.includes(raw) ? raw : null;
|
|
4302
|
+
}
|
|
4303
|
+
function fromInstallIdentityNode(node) {
|
|
4304
|
+
const data = node ?? {};
|
|
4305
|
+
const account = data.account ?? {};
|
|
4306
|
+
return {
|
|
4307
|
+
installId: data.install_id || "",
|
|
4308
|
+
accountId: account.id || "",
|
|
4309
|
+
accountName: account.name || "",
|
|
4310
|
+
platform: toPeekPlatform(account.platform || ""),
|
|
4311
|
+
isTest: Boolean(account.is_test)
|
|
4312
|
+
};
|
|
4313
|
+
}
|
|
4314
|
+
function fromInstallEventNode(node) {
|
|
4315
|
+
const rawStatus = node?.status || "";
|
|
4316
|
+
return {
|
|
4317
|
+
status: toInstallStatus(rawStatus),
|
|
4318
|
+
rawStatus,
|
|
4319
|
+
displayVersion: node?.display_version || "",
|
|
4320
|
+
identity: fromInstallIdentityNode(node)
|
|
4321
|
+
};
|
|
4322
|
+
}
|
|
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;
|
|
4339
|
+
}
|
|
4340
|
+
|
|
4271
4341
|
exports.ACTIVITY_PRODUCT_TYPE = ACTIVITY_PRODUCT_TYPE;
|
|
4272
4342
|
exports.ADD_ON_PRODUCT_TYPE = ADD_ON_PRODUCT_TYPE;
|
|
4273
4343
|
exports.AccountUserService = AccountUserService;
|
|
@@ -4281,6 +4351,8 @@ exports.CngAccessService = CngAccessService;
|
|
|
4281
4351
|
exports.CngApiError = CngApiError;
|
|
4282
4352
|
exports.CngProductService = CngProductService;
|
|
4283
4353
|
exports.DailyNoteService = DailyNoteService;
|
|
4354
|
+
exports.INSTALL_STATUSES = INSTALL_STATUSES;
|
|
4355
|
+
exports.InvalidPeekTokenError = InvalidPeekTokenError;
|
|
4284
4356
|
exports.MembershipService = MembershipService;
|
|
4285
4357
|
exports.PeekAccessService = PeekAccessService;
|
|
4286
4358
|
exports.PeekGraphQLError = PeekGraphQLError;
|
|
@@ -4297,6 +4369,7 @@ exports.ReviewService = ReviewService;
|
|
|
4297
4369
|
exports.TimeslotService = TimeslotService;
|
|
4298
4370
|
exports.noopLogger = noopLogger;
|
|
4299
4371
|
exports.parseBookingWebhook = parseBookingWebhook;
|
|
4372
|
+
exports.parseInstallEvent = parseInstallEvent;
|
|
4300
4373
|
exports.parseWaiverWebhook = parseWaiverWebhook;
|
|
4301
4374
|
exports.verifyInstallWebhook = verifyInstallWebhook;
|
|
4302
4375
|
//# sourceMappingURL=index.cjs.map
|