@peektravel/app-utilities 0.7.1 → 0.7.3
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 +21 -11
- package/dist/index.cjs +82 -46
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +208 -69
- package/dist/index.d.ts +208 -69
- package/dist/index.js +82 -46
- package/dist/index.js.map +1 -1
- package/dist/ui/index.cjs +26 -0
- package/dist/ui/index.cjs.map +1 -1
- package/dist/ui/index.d.cts +77 -3
- package/dist/ui/index.d.ts +77 -3
- package/dist/ui/index.js +25 -1
- package/dist/ui/index.js.map +1 -1
- package/dist/ui/odyssey.css +24 -2
- package/dist/ui/tokens.css +10 -1
- package/docs/ui.md +63 -14
- package/docs/webhooks.md +110 -26
- package/llms.txt +38 -17
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -124,10 +124,12 @@ 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
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
127
|
+
|
|
128
|
+
Once a token's signature verifies, claim *extraction* never throws: the whole
|
|
129
|
+
`user` block may be absent (`claims.user === null`) and every field inside it is
|
|
130
|
+
independently nullable, with a sentinel value (`""` or `"null"`) normalized to
|
|
131
|
+
`null`. `user.id` is the signed-in person's own id — not an account or partner
|
|
132
|
+
id — so its absence is not an error.
|
|
131
133
|
|
|
132
134
|
**Plain `Error` validation/precondition failures** thrown by the service layer
|
|
133
135
|
*before* any network call — e.g. an empty config field, a `bookingId` that
|
|
@@ -306,14 +308,22 @@ signed `app_registry_v2` JWT plus a JSON body. Peek sends the token in the
|
|
|
306
308
|
`x-peek-auth` header — pass that value straight in (`token` accepts it with or
|
|
307
309
|
without a `Bearer ` prefix). It validates the HMAC signature,
|
|
308
310
|
expiry, issuer, and `"Joken"` audience (the same checks as `verifyPeekAuthToken`,
|
|
309
|
-
throwing the underlying `jsonwebtoken` error
|
|
310
|
-
present `user` block has no `id`), then returns one flat, JSON-safe
|
|
311
|
+
throwing the underlying `jsonwebtoken` error), then returns one flat, JSON-safe
|
|
311
312
|
`InstallWebhook`: `installId`, `accountId` (**also called the partner ID**),
|
|
312
|
-
`accountName`, `platform`, `isTest`, `
|
|
313
|
-
a **nullable** `user`. The **
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
313
|
+
`accountName`, `platform`, `isTest`, `timezone`, `apiUrl`, `status`, `rawStatus`,
|
|
314
|
+
`displayVersion`, and a **nullable** `user`. The **JSON body is the source of the
|
|
315
|
+
event data**; the fields it shares with the token (`installId` / `accountId` /
|
|
316
|
+
`status` / `displayVersion` / `user`) fall back to the **verified token** when
|
|
317
|
+
the body omits them. The token authenticates the whole delivery, so the body is
|
|
318
|
+
trusted within a verified request. This webhook is the only source of
|
|
319
|
+
`accountId`, `timezone`, and `apiUrl` in the package, since no GraphQL read
|
|
320
|
+
returns them — **persist them per install**. `apiUrl` is the install's app
|
|
321
|
+
endpoint: use it **as given** (don't reconstruct it from `appId`) — pass it as the
|
|
322
|
+
access service's `apiUrl`, or hand the whole install to
|
|
323
|
+
`createAccessServiceForInstall(install, { jwtSecret, issuer })`, which picks the
|
|
324
|
+
service by `platform` and wires the endpoint for you. Every event is a **full
|
|
325
|
+
snapshot** — an `update_installed` carries the same fields as the original
|
|
326
|
+
`installed`, so upsert by `installId` and take the latest `apiUrl`. `status` and
|
|
317
327
|
`platform` are **nullable** — a value this version doesn't recognise stays `null`
|
|
318
328
|
(wire value on `rawStatus`) instead of being coerced, so a newer registry can't be
|
|
319
329
|
mistaken for an older one. (The older `verifyInstallWebhook(token, secret)` is
|
package/dist/index.cjs
CHANGED
|
@@ -247,15 +247,6 @@ 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
|
-
};
|
|
259
250
|
var PiiAccessDisabledError = class extends Error {
|
|
260
251
|
/** The name of the operation that was blocked (e.g. `"makePayment"`). */
|
|
261
252
|
operation;
|
|
@@ -2044,7 +2035,8 @@ var GraphQLClient = class {
|
|
|
2044
2035
|
);
|
|
2045
2036
|
}
|
|
2046
2037
|
endpoint(endpointName) {
|
|
2047
|
-
const { baseUrl, appId, endpointPathPrefix } = this.options;
|
|
2038
|
+
const { apiUrl, baseUrl, appId, endpointPathPrefix } = this.options;
|
|
2039
|
+
if (apiUrl) return apiUrl;
|
|
2048
2040
|
const prefix = endpointPathPrefix ? `${endpointPathPrefix}/` : "";
|
|
2049
2041
|
return `${baseUrl}/${appId}/${prefix}${endpointName}`;
|
|
2050
2042
|
}
|
|
@@ -2059,6 +2051,11 @@ var GraphQLClient = class {
|
|
|
2059
2051
|
return headers;
|
|
2060
2052
|
}
|
|
2061
2053
|
};
|
|
2054
|
+
|
|
2055
|
+
// src/models/peek/auth-token.ts
|
|
2056
|
+
var PEEK_PLATFORMS = ["peek", "cng", "acme"];
|
|
2057
|
+
|
|
2058
|
+
// src/internal/peek/peek-auth-token.ts
|
|
2062
2059
|
var PEEK_TOKEN_ISSUER = "app_registry_v2";
|
|
2063
2060
|
var PEEK_TOKEN_AUDIENCE = "Joken";
|
|
2064
2061
|
var BEARER_PREFIX = "bearer ";
|
|
@@ -2066,6 +2063,16 @@ function stripBearer(token) {
|
|
|
2066
2063
|
const trimmed = token.trim();
|
|
2067
2064
|
return trimmed.toLowerCase().startsWith(BEARER_PREFIX) ? trimmed.slice(BEARER_PREFIX.length).trim() : trimmed;
|
|
2068
2065
|
}
|
|
2066
|
+
var NULLISH_STRINGS = /* @__PURE__ */ new Set(["", "null"]);
|
|
2067
|
+
function cleanTokenString(value) {
|
|
2068
|
+
if (typeof value !== "string") return null;
|
|
2069
|
+
const trimmed = value.trim();
|
|
2070
|
+
return NULLISH_STRINGS.has(trimmed.toLowerCase()) ? null : trimmed;
|
|
2071
|
+
}
|
|
2072
|
+
function cleanTokenPlatform(value) {
|
|
2073
|
+
const cleaned = cleanTokenString(value);
|
|
2074
|
+
return cleaned !== null && PEEK_PLATFORMS.includes(cleaned) ? cleaned : null;
|
|
2075
|
+
}
|
|
2069
2076
|
function verifyPeekJwt(token, secret) {
|
|
2070
2077
|
return jwt__namespace.verify(stripBearer(token), secret, {
|
|
2071
2078
|
issuer: PEEK_TOKEN_ISSUER,
|
|
@@ -2073,16 +2080,14 @@ function verifyPeekJwt(token, secret) {
|
|
|
2073
2080
|
});
|
|
2074
2081
|
}
|
|
2075
2082
|
function mapPeekTokenUser(u) {
|
|
2076
|
-
if (!u
|
|
2077
|
-
throw new InvalidPeekTokenError("user.id");
|
|
2078
|
-
}
|
|
2083
|
+
if (!u) return null;
|
|
2079
2084
|
return {
|
|
2080
|
-
email: u.email,
|
|
2081
|
-
id: u.id,
|
|
2082
|
-
isAdmin: u.is_admin,
|
|
2083
|
-
locale: u.locale,
|
|
2084
|
-
name: u.name,
|
|
2085
|
-
platform: u.platform
|
|
2085
|
+
email: cleanTokenString(u.email),
|
|
2086
|
+
id: cleanTokenString(u.id),
|
|
2087
|
+
isAdmin: typeof u.is_admin === "boolean" ? u.is_admin : null,
|
|
2088
|
+
locale: cleanTokenString(u.locale),
|
|
2089
|
+
name: cleanTokenString(u.name),
|
|
2090
|
+
platform: cleanTokenPlatform(u.platform)
|
|
2086
2091
|
};
|
|
2087
2092
|
}
|
|
2088
2093
|
|
|
@@ -3561,25 +3566,27 @@ var PeekAccessService = class {
|
|
|
3561
3566
|
bookingService;
|
|
3562
3567
|
reviewService;
|
|
3563
3568
|
constructor(config) {
|
|
3569
|
+
const hasApiUrl = !!config.apiUrl;
|
|
3564
3570
|
const isV2 = config.mode === "v2";
|
|
3565
3571
|
requireNonEmpty(config.installId, "installId", "PeekAccessService");
|
|
3566
3572
|
requireNonEmpty(config.jwtSecret, "jwtSecret", "PeekAccessService");
|
|
3567
3573
|
requireNonEmpty(config.issuer, "issuer", "PeekAccessService");
|
|
3568
|
-
requireNonEmpty(config.appId, "appId", "PeekAccessService");
|
|
3569
|
-
if (!isV2) requireNonEmpty(config.gatewayKey ?? "", "gatewayKey", "PeekAccessService");
|
|
3574
|
+
if (!hasApiUrl) requireNonEmpty(config.appId ?? "", "appId", "PeekAccessService");
|
|
3575
|
+
if (!hasApiUrl && !isV2) requireNonEmpty(config.gatewayKey ?? "", "gatewayKey", "PeekAccessService");
|
|
3570
3576
|
this.jwtSecret = config.jwtSecret;
|
|
3571
3577
|
const logger = config.logger ?? noopLogger;
|
|
3572
3578
|
const tokens = createTokenManager(config);
|
|
3573
3579
|
const defaultBaseUrl = isV2 ? DEFAULT_V2_BASE_URL : DEFAULT_BASE_URL;
|
|
3574
3580
|
this.client = new GraphQLClient({
|
|
3575
|
-
|
|
3576
|
-
|
|
3581
|
+
apiUrl: config.apiUrl,
|
|
3582
|
+
baseUrl: hasApiUrl ? void 0 : config.baseUrl ?? defaultBaseUrl,
|
|
3583
|
+
appId: hasApiUrl ? void 0 : config.appId,
|
|
3577
3584
|
gatewayKey: config.gatewayKey,
|
|
3578
3585
|
getToken: () => tokens.getToken(),
|
|
3579
3586
|
retryDelaysMs: config.retryDelaysMs ?? DEFAULT_RETRY_DELAYS_MS,
|
|
3580
3587
|
logger,
|
|
3581
3588
|
fetchFn: config.fetch ?? globalThis.fetch,
|
|
3582
|
-
endpointPathPrefix: isV2 ? V2_EXTENDABLE_SLUG : void 0
|
|
3589
|
+
endpointPathPrefix: !hasApiUrl && isV2 ? V2_EXTENDABLE_SLUG : void 0
|
|
3583
3590
|
});
|
|
3584
3591
|
this.productServiceOptions = {
|
|
3585
3592
|
itemOptionsPageSize: config.itemOptionsPageSize
|
|
@@ -3600,13 +3607,15 @@ var PeekAccessService = class {
|
|
|
3600
3607
|
* malformed
|
|
3601
3608
|
* - `TokenExpiredError` — past `exp`
|
|
3602
3609
|
* - `NotBeforeError` — before `nbf`
|
|
3603
|
-
*
|
|
3604
|
-
*
|
|
3610
|
+
*
|
|
3611
|
+
* Claim *extraction* is defensive: once the signature verifies, a token with
|
|
3612
|
+
* no `user` block yields `claims.user === null`, and any individual user field
|
|
3613
|
+
* that is absent or a sentinel (`""`, `"null"`) is normalized to `null`. This
|
|
3614
|
+
* method never throws on a shape problem — only on a verification failure.
|
|
3605
3615
|
*
|
|
3606
3616
|
* @throws {JsonWebTokenError} signature invalid or token malformed
|
|
3607
3617
|
* @throws {TokenExpiredError} token has expired
|
|
3608
3618
|
* @throws {NotBeforeError} token not yet valid
|
|
3609
|
-
* @throws {InvalidPeekTokenError} verified token has no `user.id`
|
|
3610
3619
|
*/
|
|
3611
3620
|
verifyPeekAuthToken(token) {
|
|
3612
3621
|
const payload = verifyPeekJwt(token, this.jwtSecret);
|
|
@@ -4007,7 +4016,8 @@ var RestClient = class {
|
|
|
4007
4016
|
);
|
|
4008
4017
|
}
|
|
4009
4018
|
endpoint(path) {
|
|
4010
|
-
const { baseUrl, appId, extendableSlug } = this.options;
|
|
4019
|
+
const { apiUrl, baseUrl, appId, extendableSlug } = this.options;
|
|
4020
|
+
if (apiUrl) return `${apiUrl}/${path}`;
|
|
4011
4021
|
return `${baseUrl}/${appId}/${extendableSlug}/${path}`;
|
|
4012
4022
|
}
|
|
4013
4023
|
buildHeaders() {
|
|
@@ -4024,15 +4034,17 @@ var CngAccessService = class {
|
|
|
4024
4034
|
client;
|
|
4025
4035
|
productService;
|
|
4026
4036
|
constructor(config) {
|
|
4037
|
+
const hasApiUrl = !!config.apiUrl;
|
|
4027
4038
|
requireNonEmpty(config.installId, "installId", "CngAccessService");
|
|
4028
4039
|
requireNonEmpty(config.jwtSecret, "jwtSecret", "CngAccessService");
|
|
4029
4040
|
requireNonEmpty(config.issuer, "issuer", "CngAccessService");
|
|
4030
|
-
requireNonEmpty(config.appId, "appId", "CngAccessService");
|
|
4041
|
+
if (!hasApiUrl) requireNonEmpty(config.appId ?? "", "appId", "CngAccessService");
|
|
4031
4042
|
const logger = config.logger ?? noopLogger;
|
|
4032
4043
|
const tokens = createTokenManager(config);
|
|
4033
4044
|
this.client = new RestClient({
|
|
4034
|
-
|
|
4035
|
-
|
|
4045
|
+
apiUrl: config.apiUrl,
|
|
4046
|
+
baseUrl: hasApiUrl ? void 0 : config.baseUrl ?? DEFAULT_BASE_URL2,
|
|
4047
|
+
appId: hasApiUrl ? void 0 : config.appId,
|
|
4036
4048
|
extendableSlug: CNG_EXTENDABLE_SLUG,
|
|
4037
4049
|
getToken: () => tokens.getToken(),
|
|
4038
4050
|
retryDelaysMs: config.retryDelaysMs ?? DEFAULT_RETRY_DELAYS_MS,
|
|
@@ -4138,7 +4150,8 @@ var RestClient2 = class {
|
|
|
4138
4150
|
);
|
|
4139
4151
|
}
|
|
4140
4152
|
endpoint(path) {
|
|
4141
|
-
const { baseUrl, appId, extendableSlug } = this.options;
|
|
4153
|
+
const { apiUrl, baseUrl, appId, extendableSlug } = this.options;
|
|
4154
|
+
if (apiUrl) return `${apiUrl}/${path}`;
|
|
4142
4155
|
return `${baseUrl}/${appId}/${extendableSlug}/${path}`;
|
|
4143
4156
|
}
|
|
4144
4157
|
buildHeaders() {
|
|
@@ -4155,15 +4168,17 @@ var AcmeAccessService = class {
|
|
|
4155
4168
|
client;
|
|
4156
4169
|
productService;
|
|
4157
4170
|
constructor(config) {
|
|
4171
|
+
const hasApiUrl = !!config.apiUrl;
|
|
4158
4172
|
requireNonEmpty(config.installId, "installId", "AcmeAccessService");
|
|
4159
4173
|
requireNonEmpty(config.jwtSecret, "jwtSecret", "AcmeAccessService");
|
|
4160
4174
|
requireNonEmpty(config.issuer, "issuer", "AcmeAccessService");
|
|
4161
|
-
requireNonEmpty(config.appId, "appId", "AcmeAccessService");
|
|
4175
|
+
if (!hasApiUrl) requireNonEmpty(config.appId ?? "", "appId", "AcmeAccessService");
|
|
4162
4176
|
const logger = config.logger ?? noopLogger;
|
|
4163
4177
|
const tokens = createTokenManager(config);
|
|
4164
4178
|
this.client = new RestClient2({
|
|
4165
|
-
|
|
4166
|
-
|
|
4179
|
+
apiUrl: config.apiUrl,
|
|
4180
|
+
baseUrl: hasApiUrl ? void 0 : config.baseUrl ?? DEFAULT_BASE_URL3,
|
|
4181
|
+
appId: hasApiUrl ? void 0 : config.appId,
|
|
4167
4182
|
extendableSlug: ACME_EXTENDABLE_SLUG,
|
|
4168
4183
|
getToken: () => tokens.getToken(),
|
|
4169
4184
|
retryDelaysMs: config.retryDelaysMs ?? DEFAULT_RETRY_DELAYS_MS,
|
|
@@ -4187,6 +4202,23 @@ var AcmeAccessService = class {
|
|
|
4187
4202
|
}
|
|
4188
4203
|
};
|
|
4189
4204
|
|
|
4205
|
+
// src/access-service-factory.ts
|
|
4206
|
+
function createAccessServiceForInstall(install, config) {
|
|
4207
|
+
const shared = { ...config, installId: install.installId, apiUrl: install.apiUrl };
|
|
4208
|
+
switch (install.platform) {
|
|
4209
|
+
case "peek":
|
|
4210
|
+
return new PeekAccessService(shared);
|
|
4211
|
+
case "cng":
|
|
4212
|
+
return new CngAccessService(shared);
|
|
4213
|
+
case "acme":
|
|
4214
|
+
return new AcmeAccessService(shared);
|
|
4215
|
+
default:
|
|
4216
|
+
throw new Error(
|
|
4217
|
+
`createAccessServiceForInstall: unknown or missing platform "${install.platform}" for install "${install.installId}"`
|
|
4218
|
+
);
|
|
4219
|
+
}
|
|
4220
|
+
}
|
|
4221
|
+
|
|
4190
4222
|
// src/internal/peek/bookings/booking-webhook.ts
|
|
4191
4223
|
var PAYLOAD_BOOKING_KEY = "booking";
|
|
4192
4224
|
var VALUE_OPEN_TOKEN = "value {";
|
|
@@ -4277,9 +4309,6 @@ function extractWaiverNode(payload) {
|
|
|
4277
4309
|
return record;
|
|
4278
4310
|
}
|
|
4279
4311
|
|
|
4280
|
-
// src/models/peek/auth-token.ts
|
|
4281
|
-
var PEEK_PLATFORMS = ["peek", "cng", "acme"];
|
|
4282
|
-
|
|
4283
4312
|
// src/models/peek/install.ts
|
|
4284
4313
|
var INSTALL_STATUSES = [
|
|
4285
4314
|
"installed",
|
|
@@ -4310,20 +4339,27 @@ function toPeekPlatform(raw) {
|
|
|
4310
4339
|
// src/internal/peek/installs/install-webhook.ts
|
|
4311
4340
|
function parseInstallWebhook(token, body, secret) {
|
|
4312
4341
|
const payload = verifyPeekJwt(token, secret);
|
|
4313
|
-
const
|
|
4314
|
-
const
|
|
4342
|
+
const parsed = extractInstallWebhookBody(body);
|
|
4343
|
+
const account = parsed.account ?? {};
|
|
4344
|
+
const rawStatus = parsed.status || payload.status || "";
|
|
4315
4345
|
return {
|
|
4316
|
-
installId: payload.sub
|
|
4317
|
-
accountId: payload.account?.id
|
|
4346
|
+
installId: parsed.install_id || payload.sub || "",
|
|
4347
|
+
accountId: account.id || payload.account?.id || "",
|
|
4318
4348
|
accountName: account.name || "",
|
|
4319
4349
|
platform: toPeekPlatform(account.platform || ""),
|
|
4320
4350
|
isTest: Boolean(account.is_test),
|
|
4351
|
+
timezone: account.timezone || "",
|
|
4352
|
+
apiUrl: parsed.api?.url || "",
|
|
4321
4353
|
status: toInstallStatus(rawStatus),
|
|
4322
4354
|
rawStatus,
|
|
4323
|
-
displayVersion: payload.display_version
|
|
4324
|
-
user:
|
|
4355
|
+
displayVersion: parsed.display_version || payload.display_version || "",
|
|
4356
|
+
user: resolveInstallUser(parsed.modified_by, payload.user)
|
|
4325
4357
|
};
|
|
4326
4358
|
}
|
|
4359
|
+
function resolveInstallUser(bodyUser, tokenUser) {
|
|
4360
|
+
if (cleanTokenString(bodyUser?.id) !== null) return mapPeekTokenUser(bodyUser);
|
|
4361
|
+
return mapPeekTokenUser(tokenUser);
|
|
4362
|
+
}
|
|
4327
4363
|
function verifyInstallWebhook(token, secret) {
|
|
4328
4364
|
const payload = verifyPeekJwt(token, secret);
|
|
4329
4365
|
return {
|
|
@@ -4331,7 +4367,7 @@ function verifyInstallWebhook(token, secret) {
|
|
|
4331
4367
|
account: { id: payload.account?.id ?? "" },
|
|
4332
4368
|
status: payload.status ?? "",
|
|
4333
4369
|
displayVersion: payload.display_version ?? "",
|
|
4334
|
-
user:
|
|
4370
|
+
user: mapPeekTokenUser(payload.user)
|
|
4335
4371
|
};
|
|
4336
4372
|
}
|
|
4337
4373
|
|
|
@@ -4349,7 +4385,6 @@ exports.CngApiError = CngApiError;
|
|
|
4349
4385
|
exports.CngProductService = CngProductService;
|
|
4350
4386
|
exports.DailyNoteService = DailyNoteService;
|
|
4351
4387
|
exports.INSTALL_STATUSES = INSTALL_STATUSES;
|
|
4352
|
-
exports.InvalidPeekTokenError = InvalidPeekTokenError;
|
|
4353
4388
|
exports.MembershipService = MembershipService;
|
|
4354
4389
|
exports.PeekAccessService = PeekAccessService;
|
|
4355
4390
|
exports.PeekGraphQLError = PeekGraphQLError;
|
|
@@ -4364,6 +4399,7 @@ exports.ResellerService = ResellerService;
|
|
|
4364
4399
|
exports.ResourcePoolService = ResourcePoolService;
|
|
4365
4400
|
exports.ReviewService = ReviewService;
|
|
4366
4401
|
exports.TimeslotService = TimeslotService;
|
|
4402
|
+
exports.createAccessServiceForInstall = createAccessServiceForInstall;
|
|
4367
4403
|
exports.noopLogger = noopLogger;
|
|
4368
4404
|
exports.parseBookingWebhook = parseBookingWebhook;
|
|
4369
4405
|
exports.parseInstallWebhook = parseInstallWebhook;
|