@peektravel/app-utilities 0.7.2 → 0.7.4
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 +24 -6
- package/dist/index.cjs +30 -28
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +38 -32
- package/dist/index.d.ts +38 -32
- package/dist/index.js +31 -28
- package/dist/index.js.map +1 -1
- package/dist/ui/index.cjs +11 -0
- package/dist/ui/index.cjs.map +1 -1
- package/dist/ui/index.d.cts +16 -1
- package/dist/ui/index.d.ts +16 -1
- package/dist/ui/index.js +10 -1
- package/dist/ui/index.js.map +1 -1
- package/dist/ui/odyssey.css +30 -0
- package/docs/ui.md +27 -0
- package/docs/webhooks.md +4 -3
- package/llms.txt +12 -4
- 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,8 +308,7 @@ 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
313
|
`accountName`, `platform`, `isTest`, `timezone`, `apiUrl`, `status`, `rawStatus`,
|
|
313
314
|
`displayVersion`, and a **nullable** `user`. The **JSON body is the source of the
|
|
@@ -373,6 +374,23 @@ attribute. Exported classes/types and helpers (`iconSvg`, `registerIcon`,
|
|
|
373
374
|
`portal`, `position`, `toast`) are available from `@peektravel/app-utilities/ui`
|
|
374
375
|
for subclassing or typing.
|
|
375
376
|
|
|
377
|
+
### Lazy registration & avoiding a flash of raw text
|
|
378
|
+
|
|
379
|
+
Registration is a side effect and **idempotent** — importing `/ui` twice (or from
|
|
380
|
+
two bundles) never throws. If you register lazily (a dynamic `import()`), two
|
|
381
|
+
things keep un-upgraded elements from flashing as their raw text: `odyssey.css`
|
|
382
|
+
ships a `:not(:defined) { visibility: hidden }` guard (load the CSS eagerly for it
|
|
383
|
+
to apply before first paint), and `whenOdysseyReady()` resolves once every
|
|
384
|
+
imported element has upgraded, so you can gate your first render on it:
|
|
385
|
+
|
|
386
|
+
```ts
|
|
387
|
+
import { whenOdysseyReady } from '@peektravel/app-utilities/ui';
|
|
388
|
+
await whenOdysseyReady();
|
|
389
|
+
```
|
|
390
|
+
|
|
391
|
+
A failed dynamic import still rejects at the `import()` call site — handle load
|
|
392
|
+
failures there.
|
|
393
|
+
|
|
376
394
|
### Using the components from React 19
|
|
377
395
|
|
|
378
396
|
React 19 sets JSX props on a custom element as DOM **properties**
|
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;
|
|
@@ -2060,6 +2051,11 @@ var GraphQLClient = class {
|
|
|
2060
2051
|
return headers;
|
|
2061
2052
|
}
|
|
2062
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
|
|
2063
2059
|
var PEEK_TOKEN_ISSUER = "app_registry_v2";
|
|
2064
2060
|
var PEEK_TOKEN_AUDIENCE = "Joken";
|
|
2065
2061
|
var BEARER_PREFIX = "bearer ";
|
|
@@ -2067,6 +2063,16 @@ function stripBearer(token) {
|
|
|
2067
2063
|
const trimmed = token.trim();
|
|
2068
2064
|
return trimmed.toLowerCase().startsWith(BEARER_PREFIX) ? trimmed.slice(BEARER_PREFIX.length).trim() : trimmed;
|
|
2069
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
|
+
}
|
|
2070
2076
|
function verifyPeekJwt(token, secret) {
|
|
2071
2077
|
return jwt__namespace.verify(stripBearer(token), secret, {
|
|
2072
2078
|
issuer: PEEK_TOKEN_ISSUER,
|
|
@@ -2074,16 +2080,14 @@ function verifyPeekJwt(token, secret) {
|
|
|
2074
2080
|
});
|
|
2075
2081
|
}
|
|
2076
2082
|
function mapPeekTokenUser(u) {
|
|
2077
|
-
if (!u
|
|
2078
|
-
throw new InvalidPeekTokenError("user.id");
|
|
2079
|
-
}
|
|
2083
|
+
if (!u) return null;
|
|
2080
2084
|
return {
|
|
2081
|
-
email: u.email,
|
|
2082
|
-
id: u.id,
|
|
2083
|
-
isAdmin: u.is_admin,
|
|
2084
|
-
locale: u.locale,
|
|
2085
|
-
name: u.name,
|
|
2086
|
-
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)
|
|
2087
2091
|
};
|
|
2088
2092
|
}
|
|
2089
2093
|
|
|
@@ -3603,13 +3607,15 @@ var PeekAccessService = class {
|
|
|
3603
3607
|
* malformed
|
|
3604
3608
|
* - `TokenExpiredError` — past `exp`
|
|
3605
3609
|
* - `NotBeforeError` — before `nbf`
|
|
3606
|
-
*
|
|
3607
|
-
*
|
|
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.
|
|
3608
3615
|
*
|
|
3609
3616
|
* @throws {JsonWebTokenError} signature invalid or token malformed
|
|
3610
3617
|
* @throws {TokenExpiredError} token has expired
|
|
3611
3618
|
* @throws {NotBeforeError} token not yet valid
|
|
3612
|
-
* @throws {InvalidPeekTokenError} verified token has no `user.id`
|
|
3613
3619
|
*/
|
|
3614
3620
|
verifyPeekAuthToken(token) {
|
|
3615
3621
|
const payload = verifyPeekJwt(token, this.jwtSecret);
|
|
@@ -4303,9 +4309,6 @@ function extractWaiverNode(payload) {
|
|
|
4303
4309
|
return record;
|
|
4304
4310
|
}
|
|
4305
4311
|
|
|
4306
|
-
// src/models/peek/auth-token.ts
|
|
4307
|
-
var PEEK_PLATFORMS = ["peek", "cng", "acme"];
|
|
4308
|
-
|
|
4309
4312
|
// src/models/peek/install.ts
|
|
4310
4313
|
var INSTALL_STATUSES = [
|
|
4311
4314
|
"installed",
|
|
@@ -4354,8 +4357,8 @@ function parseInstallWebhook(token, body, secret) {
|
|
|
4354
4357
|
};
|
|
4355
4358
|
}
|
|
4356
4359
|
function resolveInstallUser(bodyUser, tokenUser) {
|
|
4357
|
-
if (bodyUser?.id) return mapPeekTokenUser(bodyUser);
|
|
4358
|
-
return
|
|
4360
|
+
if (cleanTokenString(bodyUser?.id) !== null) return mapPeekTokenUser(bodyUser);
|
|
4361
|
+
return mapPeekTokenUser(tokenUser);
|
|
4359
4362
|
}
|
|
4360
4363
|
function verifyInstallWebhook(token, secret) {
|
|
4361
4364
|
const payload = verifyPeekJwt(token, secret);
|
|
@@ -4364,7 +4367,7 @@ function verifyInstallWebhook(token, secret) {
|
|
|
4364
4367
|
account: { id: payload.account?.id ?? "" },
|
|
4365
4368
|
status: payload.status ?? "",
|
|
4366
4369
|
displayVersion: payload.display_version ?? "",
|
|
4367
|
-
user:
|
|
4370
|
+
user: mapPeekTokenUser(payload.user)
|
|
4368
4371
|
};
|
|
4369
4372
|
}
|
|
4370
4373
|
|
|
@@ -4382,7 +4385,6 @@ exports.CngApiError = CngApiError;
|
|
|
4382
4385
|
exports.CngProductService = CngProductService;
|
|
4383
4386
|
exports.DailyNoteService = DailyNoteService;
|
|
4384
4387
|
exports.INSTALL_STATUSES = INSTALL_STATUSES;
|
|
4385
|
-
exports.InvalidPeekTokenError = InvalidPeekTokenError;
|
|
4386
4388
|
exports.MembershipService = MembershipService;
|
|
4387
4389
|
exports.PeekAccessService = PeekAccessService;
|
|
4388
4390
|
exports.PeekGraphQLError = PeekGraphQLError;
|