@peektravel/app-utilities 0.8.0 → 0.8.2
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/dist/index.cjs +74 -21
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +53 -19
- package/dist/index.d.ts +53 -19
- package/dist/index.js +74 -22
- package/dist/index.js.map +1 -1
- package/llms.txt +14 -3
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -207,6 +207,13 @@ function resolveAccessOptions(options) {
|
|
|
207
207
|
}
|
|
208
208
|
|
|
209
209
|
// src/errors.ts
|
|
210
|
+
var FORBIDDEN_STATUS = 403;
|
|
211
|
+
var PERMISSION_MESSAGE_PREFIX = "CNG request forbidden: the app is missing the required permission";
|
|
212
|
+
var PERMISSION_MESSAGE_UNKNOWN = "CNG request forbidden: the app is missing a required permission";
|
|
213
|
+
function buildPermissionMessage(permissions) {
|
|
214
|
+
if (permissions.length === 0) return PERMISSION_MESSAGE_UNKNOWN;
|
|
215
|
+
return `${PERMISSION_MESSAGE_PREFIX}${permissions.length > 1 ? "s" : ""}: ${permissions.join(", ")}`;
|
|
216
|
+
}
|
|
210
217
|
var AdminAccountRequiredError = class extends Error {
|
|
211
218
|
/** The HTTP status that triggered this error. */
|
|
212
219
|
statusCode = 418;
|
|
@@ -270,6 +277,23 @@ var CngApiError = class extends Error {
|
|
|
270
277
|
this.body = body;
|
|
271
278
|
}
|
|
272
279
|
};
|
|
280
|
+
var CngPermissionError = class extends Error {
|
|
281
|
+
/** The HTTP status that triggered this error. */
|
|
282
|
+
statusCode = FORBIDDEN_STATUS;
|
|
283
|
+
/**
|
|
284
|
+
* The permissions the gateway reported as missing, e.g. `["products:read"]`.
|
|
285
|
+
* Empty when the response body did not name any.
|
|
286
|
+
*/
|
|
287
|
+
permissions;
|
|
288
|
+
/** The raw response body (parsed JSON when possible, otherwise text). */
|
|
289
|
+
body;
|
|
290
|
+
constructor(permissions, body, message) {
|
|
291
|
+
super(message ?? buildPermissionMessage(permissions));
|
|
292
|
+
this.name = "CngPermissionError";
|
|
293
|
+
this.permissions = permissions;
|
|
294
|
+
this.body = body;
|
|
295
|
+
}
|
|
296
|
+
};
|
|
273
297
|
var AcmeApiError = class extends Error {
|
|
274
298
|
/** The HTTP status that triggered this error. */
|
|
275
299
|
statusCode;
|
|
@@ -1081,6 +1105,7 @@ var errInvalidCheckbox = (v, id) => `Checkbox answer for question "${id}" must b
|
|
|
1081
1105
|
var errUnknownOptionId = (v, id) => `No option matches id "${v}" for question "${id}"`;
|
|
1082
1106
|
var errNoOptionMatch = (v, id) => `No option matches "${v}" for question "${id}"`;
|
|
1083
1107
|
var errAmbiguousOption = (v, id) => `Option "${v}" matches more than one option for question "${id}"`;
|
|
1108
|
+
var errMissingRequired = (text, id) => `Custom question "${text}" (${id}) is required but was not answered`;
|
|
1084
1109
|
function normalize(value) {
|
|
1085
1110
|
return value.toLowerCase().replace(/[^a-z0-9]/g, "");
|
|
1086
1111
|
}
|
|
@@ -1145,8 +1170,20 @@ function resolveOne(answer, questions) {
|
|
|
1145
1170
|
throw new Error(errUnsupportedType(question.questionType, question.id));
|
|
1146
1171
|
}
|
|
1147
1172
|
}
|
|
1148
|
-
function resolveCustomQuestionAnswers(answers, questions) {
|
|
1149
|
-
|
|
1173
|
+
function resolveCustomQuestionAnswers(answers, questions, options) {
|
|
1174
|
+
const resolved = answers.map((answer) => resolveOne(answer, questions));
|
|
1175
|
+
if (options?.requireRequired) {
|
|
1176
|
+
assertRequiredAnswered(resolved, questions);
|
|
1177
|
+
}
|
|
1178
|
+
return resolved;
|
|
1179
|
+
}
|
|
1180
|
+
function assertRequiredAnswered(resolved, questions) {
|
|
1181
|
+
const answered = new Set(resolved.map((answer) => answer.questionId));
|
|
1182
|
+
for (const question of questions) {
|
|
1183
|
+
if (question.isRequired && !question.perGuest && !answered.has(question.id)) {
|
|
1184
|
+
throw new Error(errMissingRequired(question.questionText, question.id));
|
|
1185
|
+
}
|
|
1186
|
+
}
|
|
1150
1187
|
}
|
|
1151
1188
|
|
|
1152
1189
|
// src/internal/peek/bookings/booking-service.ts
|
|
@@ -1663,8 +1700,9 @@ ${note}`;
|
|
|
1663
1700
|
*
|
|
1664
1701
|
* @throws {Error} when `activityId`, `availabilityTimeId`, a ticket
|
|
1665
1702
|
* `resourceOptionId`/positive `quantity`, or the guest `name` is missing; when
|
|
1666
|
-
* `markAsPaid` is set without an `idempotencyKey`;
|
|
1667
|
-
*
|
|
1703
|
+
* `markAsPaid` is set without an `idempotencyKey`; when
|
|
1704
|
+
* `requireRequiredQuestions` is set and a required custom question is left
|
|
1705
|
+
* unanswered; or when the quote/order mutations fail.
|
|
1668
1706
|
*/
|
|
1669
1707
|
async create(input) {
|
|
1670
1708
|
validateCreateInput(input);
|
|
@@ -1744,15 +1782,22 @@ ${note}`;
|
|
|
1744
1782
|
* against; throws on the first unmatched/ambiguous/invalid answer.
|
|
1745
1783
|
*/
|
|
1746
1784
|
async buildQuestionAnswers(input) {
|
|
1747
|
-
const answers = input.customQuestionAnswers;
|
|
1748
|
-
|
|
1785
|
+
const answers = input.customQuestionAnswers ?? [];
|
|
1786
|
+
const requireRequired = input.requireRequiredQuestions ?? false;
|
|
1787
|
+
if (answers.length === 0 && !requireRequired) {
|
|
1749
1788
|
return null;
|
|
1750
1789
|
}
|
|
1751
1790
|
const questions = await this.deps.productService.getCustomQuestions(
|
|
1752
1791
|
input.activityId
|
|
1753
1792
|
);
|
|
1754
|
-
|
|
1755
|
-
|
|
1793
|
+
const resolved = resolveCustomQuestionAnswers(answers, questions, {
|
|
1794
|
+
requireRequired
|
|
1795
|
+
});
|
|
1796
|
+
if (resolved.length === 0) {
|
|
1797
|
+
return null;
|
|
1798
|
+
}
|
|
1799
|
+
return resolved.map((answer) => ({
|
|
1800
|
+
...answer,
|
|
1756
1801
|
refid: crypto.randomUUID()
|
|
1757
1802
|
}));
|
|
1758
1803
|
}
|
|
@@ -4135,7 +4180,7 @@ var PeekAccessService = class {
|
|
|
4135
4180
|
|
|
4136
4181
|
// src/internal/cng/endpoints.ts
|
|
4137
4182
|
var CNG_EXTENDABLE_SLUG = "cng_backoffice_api-v1";
|
|
4138
|
-
var PRODUCTS_PATH = "api/v2/
|
|
4183
|
+
var PRODUCTS_PATH = "api/v2/app-registry/products?active=1";
|
|
4139
4184
|
|
|
4140
4185
|
// src/models/cng/product.ts
|
|
4141
4186
|
var ACTIVITY_PRODUCT_TYPE2 = "ACTIVITY";
|
|
@@ -4146,14 +4191,11 @@ function fromProductNodes(nodes) {
|
|
|
4146
4191
|
}
|
|
4147
4192
|
function fromProductNode(node) {
|
|
4148
4193
|
return {
|
|
4149
|
-
productId: node.id
|
|
4194
|
+
productId: node.id == null ? "" : String(node.id),
|
|
4150
4195
|
name: node.name || "",
|
|
4151
|
-
type:
|
|
4152
|
-
color: node.
|
|
4153
|
-
tickets:
|
|
4154
|
-
id: ticket.id,
|
|
4155
|
-
name: ticket.name
|
|
4156
|
-
}))
|
|
4196
|
+
type: ACTIVITY_PRODUCT_TYPE2,
|
|
4197
|
+
color: node.access_control_color_hex || "",
|
|
4198
|
+
tickets: []
|
|
4157
4199
|
};
|
|
4158
4200
|
}
|
|
4159
4201
|
|
|
@@ -4172,15 +4214,13 @@ var CngProductService = class {
|
|
|
4172
4214
|
* ```
|
|
4173
4215
|
*/
|
|
4174
4216
|
async getAllActivities() {
|
|
4175
|
-
const body = await this.client.get(
|
|
4176
|
-
|
|
4177
|
-
);
|
|
4178
|
-
const nodes = Array.isArray(body) ? body : body?.products ?? [];
|
|
4179
|
-
return fromProductNodes(nodes ?? []);
|
|
4217
|
+
const body = await this.client.get(PRODUCTS_PATH);
|
|
4218
|
+
return fromProductNodes(body?.data ?? []);
|
|
4180
4219
|
}
|
|
4181
4220
|
};
|
|
4182
4221
|
|
|
4183
4222
|
// src/internal/cng/rest-client.ts
|
|
4223
|
+
var PERMISSION_SEPARATOR = ": ";
|
|
4184
4224
|
var RestClient = class {
|
|
4185
4225
|
constructor(options) {
|
|
4186
4226
|
this.options = options;
|
|
@@ -4193,6 +4233,7 @@ var RestClient = class {
|
|
|
4193
4233
|
*
|
|
4194
4234
|
* @throws {AdminAccountRequiredError} on HTTP 418
|
|
4195
4235
|
* @throws {RateLimitError} on HTTP 429 after retries are exhausted
|
|
4236
|
+
* @throws {CngPermissionError} on HTTP 403 (app missing a permission)
|
|
4196
4237
|
* @throws {CngApiError} on any other non-2xx response
|
|
4197
4238
|
*/
|
|
4198
4239
|
async get(path) {
|
|
@@ -4206,6 +4247,11 @@ var RestClient = class {
|
|
|
4206
4247
|
path,
|
|
4207
4248
|
async (response) => {
|
|
4208
4249
|
const body = await parseBody(response);
|
|
4250
|
+
if (response.status === FORBIDDEN_STATUS) {
|
|
4251
|
+
const permissions = extractMissingPermissions(body);
|
|
4252
|
+
logger.warn(`Missing permission for ${path} (HTTP 403)`, { url, permissions });
|
|
4253
|
+
throw new CngPermissionError(permissions, body);
|
|
4254
|
+
}
|
|
4209
4255
|
if (!response.ok) {
|
|
4210
4256
|
logger.error(`CNG request failed with HTTP ${response.status}`, { url });
|
|
4211
4257
|
throw new CngApiError(response.status, body);
|
|
@@ -4226,6 +4272,12 @@ var RestClient = class {
|
|
|
4226
4272
|
};
|
|
4227
4273
|
}
|
|
4228
4274
|
};
|
|
4275
|
+
function extractMissingPermissions(body) {
|
|
4276
|
+
if (typeof body !== "object" || body === null) return [];
|
|
4277
|
+
const raw = body.errors?.permission;
|
|
4278
|
+
if (!Array.isArray(raw)) return [];
|
|
4279
|
+
return raw.filter((entry) => typeof entry === "string").map((entry) => entry.split(PERMISSION_SEPARATOR).pop().trim()).filter((permission) => permission.length > 0);
|
|
4280
|
+
}
|
|
4229
4281
|
|
|
4230
4282
|
// src/cng-access-service.ts
|
|
4231
4283
|
var DEFAULT_BASE_URL2 = "https://app-registry.peeklabs.com/installations-api";
|
|
@@ -4581,6 +4633,7 @@ exports.AvailabilityService = AvailabilityService;
|
|
|
4581
4633
|
exports.BookingService = BookingService;
|
|
4582
4634
|
exports.CngAccessService = CngAccessService;
|
|
4583
4635
|
exports.CngApiError = CngApiError;
|
|
4636
|
+
exports.CngPermissionError = CngPermissionError;
|
|
4584
4637
|
exports.CngProductService = CngProductService;
|
|
4585
4638
|
exports.DailyNoteService = DailyNoteService;
|
|
4586
4639
|
exports.INSTALL_STATUSES = INSTALL_STATUSES;
|