@tangle-network/agent-integrations 0.73.0 → 0.74.0

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.
Files changed (35) hide show
  1. package/data/bundled-adapter-manifests.json +1 -1
  2. package/dist/{adapters-D8TvHRA1.js → adapters-CngYgl1W.js} +168 -605
  3. package/dist/{adapters-D8TvHRA1.js.map → adapters-CngYgl1W.js.map} +1 -1
  4. package/dist/{bundled-manifest-data-MdURriam.js → bundled-manifest-data-DituXLws.js} +197 -5
  5. package/dist/{bundled-manifest-data-MdURriam.js.map → bundled-manifest-data-DituXLws.js.map} +1 -1
  6. package/dist/cloudbeds-BL5EF3xl.js +1195 -0
  7. package/dist/cloudbeds-BL5EF3xl.js.map +1 -0
  8. package/dist/connect/index.js +1 -1
  9. package/dist/connectors/adapters/index.js +3 -2
  10. package/dist/connectors/index.js +5 -4
  11. package/dist/consumer.js +1 -1
  12. package/dist/index-udV3gGjx.d.ts.map +1 -1
  13. package/dist/index.js +7 -6
  14. package/dist/internal/bundled-manifest-runtime.js +1 -1
  15. package/dist/mcp.js +1 -1
  16. package/dist/middleware/index.js +1 -1
  17. package/dist/registry.js +1 -1
  18. package/dist/{specs-BYUQOsSC.js → specs-BeC45S22.js} +2 -2
  19. package/dist/{specs-BYUQOsSC.js.map → specs-BeC45S22.js.map} +1 -1
  20. package/dist/specs.js +1 -1
  21. package/dist/{src-c7Ru9_WT.js → src-B4-qkoxS.js} +4 -4
  22. package/dist/{src-c7Ru9_WT.js.map → src-B4-qkoxS.js.map} +1 -1
  23. package/dist/{tangle-id-D2kPF0rX.js → tangle-id-81ShqJ7k.js} +3 -262
  24. package/dist/tangle-id-81ShqJ7k.js.map +1 -0
  25. package/dist/types-BSDXGSi5.js +263 -0
  26. package/dist/types-BSDXGSi5.js.map +1 -0
  27. package/dist/webhooks/index.d.ts +5 -0
  28. package/dist/webhooks/index.d.ts.map +1 -1
  29. package/dist/webhooks/index.js +92 -2
  30. package/dist/webhooks/index.js.map +1 -1
  31. package/docs/hospitality-connectors.md +91 -11
  32. package/package.json +1 -1
  33. package/dist/tangle-id-D2kPF0rX.js.map +0 -1
  34. package/dist/webhooks-FsjqRN_0.js +0 -117
  35. package/dist/webhooks-FsjqRN_0.js.map +0 -1
@@ -0,0 +1,1195 @@
1
+ import { i as ProviderRateLimited, t as CredentialsExpired } from "./types-BSDXGSi5.js";
2
+ import { n as record, r as requestJson, t as ProviderProtocolError } from "./response-json-BnUINkTL.js";
3
+ import { createHmac, timingSafeEqual } from "node:crypto";
4
+ //#region src/connectors/webhooks.ts
5
+ /**
6
+ * Inbound webhook signature verifiers — provider-specific HMAC schemes.
7
+ *
8
+ * Each signature scheme is a pure function:
9
+ * (rawBody: string, headers, secret, now?) → boolean
10
+ *
11
+ * Constant-time comparison via `crypto.timingSafeEqual`. Timestamps are
12
+ * checked against a configurable tolerance to bound replay risk; the default
13
+ * mirrors the upstream provider's documented window (Stripe: 5 min, Slack: 5 min).
14
+ *
15
+ * These verifiers are the building blocks for any inbound-webhook receiver
16
+ * (a route + a `verify` call + a per-event handler). They live in this
17
+ * package so every consumer of the integration substrate gets correct
18
+ * verification — not just one product reimplementing it.
19
+ */
20
+ /** Default replay-protection window. Providers commonly use 5 minutes. */
21
+ const DEFAULT_SIGNATURE_TOLERANCE_SECONDS = 300;
22
+ function parseStripeSignatureHeader(header) {
23
+ const acc = { sigs: [] };
24
+ for (const part of header.split(",")) {
25
+ const idx = part.indexOf("=");
26
+ if (idx < 0) continue;
27
+ const key = part.slice(0, idx).trim();
28
+ const val = part.slice(idx + 1).trim();
29
+ if (key === "t") {
30
+ const n = Number(val);
31
+ if (Number.isFinite(n)) acc.ts = n;
32
+ } else if (key === "v1") acc.sigs.push(val);
33
+ }
34
+ if (acc.ts === void 0 || acc.sigs.length === 0) return null;
35
+ return {
36
+ t: acc.ts,
37
+ sigs: acc.sigs
38
+ };
39
+ }
40
+ /** Verify a Stripe webhook signature against the raw request body. */
41
+ function verifyStripeSignature(rawBody, signatureHeader, secret, options = {}) {
42
+ const parsed = parseStripeSignatureHeader(signatureHeader);
43
+ if (!parsed) return false;
44
+ const tolerance = options.toleranceSeconds ?? 300;
45
+ const now = options.now ?? Math.floor(Date.now() / 1e3);
46
+ if (Math.abs(now - parsed.t) > tolerance) return false;
47
+ const expected = createHmac("sha256", secret).update(`${parsed.t}.${rawBody}`).digest("hex");
48
+ const expectedBuf = Buffer.from(expected, "utf8");
49
+ for (const sig of parsed.sigs) {
50
+ const sigBuf = Buffer.from(sig, "utf8");
51
+ if (sigBuf.length !== expectedBuf.length) continue;
52
+ if (timingSafeEqual(sigBuf, expectedBuf)) return true;
53
+ }
54
+ return false;
55
+ }
56
+ function verifySlackSignature(rawBody, signatureHeader, timestampHeader, secret, options = {}) {
57
+ if (!signatureHeader.startsWith("v0=")) return false;
58
+ const ts = Number(timestampHeader);
59
+ if (!Number.isFinite(ts)) return false;
60
+ const tolerance = options.toleranceSeconds ?? 300;
61
+ const now = options.now ?? Math.floor(Date.now() / 1e3);
62
+ if (Math.abs(now - ts) > tolerance) return false;
63
+ const expected = "v0=" + createHmac("sha256", secret).update(`v0:${ts}:${rawBody}`).digest("hex");
64
+ const expectedBuf = Buffer.from(expected, "utf8");
65
+ const sigBuf = Buffer.from(signatureHeader, "utf8");
66
+ if (sigBuf.length !== expectedBuf.length) return false;
67
+ return timingSafeEqual(sigBuf, expectedBuf);
68
+ }
69
+ function verifyHmacSignature(rawBody, signatureHeader, secret, options = {}) {
70
+ const algorithm = options.algorithm ?? "sha256";
71
+ const prefix = options.signaturePrefix ?? "";
72
+ const lower = options.lowercaseHex ?? true;
73
+ let candidate = signatureHeader;
74
+ if (prefix) {
75
+ if (!candidate.startsWith(prefix)) return false;
76
+ candidate = candidate.slice(prefix.length);
77
+ }
78
+ if (lower) candidate = candidate.toLowerCase();
79
+ const expected = createHmac(algorithm, secret).update(rawBody).digest("hex");
80
+ const expectedBuf = Buffer.from(expected, "utf8");
81
+ const sigBuf = Buffer.from(candidate, "utf8");
82
+ if (sigBuf.length !== expectedBuf.length) return false;
83
+ return timingSafeEqual(sigBuf, expectedBuf);
84
+ }
85
+ /** Verify a Twilio webhook signature. */
86
+ function verifyTwilioSignature(input, options = {}) {
87
+ if (!input.authToken) return options.skipWhenAuthTokenMissing === true;
88
+ const signature = input.signatureHeader;
89
+ if (!signature || Array.isArray(signature)) return false;
90
+ if (!input.fullUrl) return false;
91
+ const data = options.bodyAsRaw === true ? input.fullUrl + (options.rawBody ?? "") : Object.keys(input.params ?? {}).sort().reduce((acc, key) => acc + key + (input.params[key] ?? ""), input.fullUrl);
92
+ const expected = createHmac("sha1", input.authToken).update(data).digest("base64");
93
+ const expectedBuf = Buffer.from(expected);
94
+ const sigBuf = Buffer.from(signature);
95
+ if (expectedBuf.length !== sigBuf.length) return false;
96
+ return timingSafeEqual(expectedBuf, sigBuf);
97
+ }
98
+ /** Verify a Tangle inbound-email signature against the raw request body. */
99
+ function verifyEmailSignature(rawBody, signatureHeader, timestampHeader, secret, options = {}) {
100
+ const ts = Number(timestampHeader);
101
+ if (!Number.isFinite(ts)) return false;
102
+ const tolerance = options.toleranceSeconds ?? 300;
103
+ const now = options.now ?? Math.floor(Date.now() / 1e3);
104
+ if (Math.abs(now - ts) > tolerance) return false;
105
+ const expected = createHmac("sha256", secret).update(`${ts}.${rawBody}`).digest("hex");
106
+ const expectedBuf = Buffer.from(expected, "utf8");
107
+ const sigBuf = Buffer.from(signatureHeader.toLowerCase(), "utf8");
108
+ if (sigBuf.length !== expectedBuf.length) return false;
109
+ return timingSafeEqual(sigBuf, expectedBuf);
110
+ }
111
+ function firstHeader(headers, name) {
112
+ const v = headers[name] ?? headers[name.toLowerCase()] ?? Object.entries(headers).find(([key]) => key.toLowerCase() === name.toLowerCase())?.[1];
113
+ if (Array.isArray(v)) return v[0];
114
+ return typeof v === "string" ? v : void 0;
115
+ }
116
+ //#endregion
117
+ //#region src/connectors/adapters/cloudbeds.ts
118
+ const API = "https://api.cloudbeds.com/api/v1.3";
119
+ const DATE = /^\d{4}-\d{2}-\d{2}$/;
120
+ const PROPERTY_ID = /^[1-9]\d{0,19}$/;
121
+ const OPERATION_ID = /^[A-Za-z0-9:._-]{1,128}$/;
122
+ const PROVIDER_ID = /^[A-Za-z0-9_-]{1,64}$/;
123
+ const HEADER_NAME = /^[A-Za-z0-9!#$%&'*+.^_`|~-]{1,64}$/;
124
+ const STATUSES = /* @__PURE__ */ new Set([
125
+ "not_confirmed",
126
+ "confirmed",
127
+ "canceled",
128
+ "checked_in",
129
+ "checked_out",
130
+ "no_show"
131
+ ]);
132
+ const ROOM_CONDITIONS = /* @__PURE__ */ new Set(["clean", "dirty"]);
133
+ const REPORTED_ROOM_CONDITIONS = /* @__PURE__ */ new Set([
134
+ "clean",
135
+ "dirty",
136
+ "inspected"
137
+ ]);
138
+ const FRONTDESK_STATUSES = /* @__PURE__ */ new Set([
139
+ "check-in",
140
+ "check-out",
141
+ "stayover",
142
+ "turnover",
143
+ "unused"
144
+ ]);
145
+ /**
146
+ * Webhook events the connector subscribes to and the Cloudbeds webhook provider
147
+ * parses. All five are `read:reservation` events in the Cloudbeds webhook guide.
148
+ * `reservation/status_changed` carries created-to-confirmed, `canceled`,
149
+ * `checked_in`, and `checked_out` transitions.
150
+ */
151
+ const CLOUDBEDS_WEBHOOK_EVENTS = [
152
+ {
153
+ object: "reservation",
154
+ action: "created"
155
+ },
156
+ {
157
+ object: "reservation",
158
+ action: "status_changed"
159
+ },
160
+ {
161
+ object: "reservation",
162
+ action: "dates_changed"
163
+ },
164
+ {
165
+ object: "reservation",
166
+ action: "accommodation_changed"
167
+ },
168
+ {
169
+ object: "reservation",
170
+ action: "deleted"
171
+ }
172
+ ];
173
+ function boundedString(value, name, max = 256) {
174
+ if (typeof value !== "string" || !value.trim() || value.length > max || /[\u0000-\u001f\u007f]/.test(value)) throw new Error(`cloudbeds: invalid ${name}`);
175
+ return value;
176
+ }
177
+ function providerId(value, name) {
178
+ if (typeof value !== "string" || !PROVIDER_ID.test(value)) throw new Error(`cloudbeds: invalid ${name}`);
179
+ return value;
180
+ }
181
+ function providerText(value, max = 256) {
182
+ return typeof value === "string" && value.length <= max && !/[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f]/.test(value) && (max > 256 || !/[\n\r\t]/.test(value)) ? value : null;
183
+ }
184
+ function providerNumber(value) {
185
+ return typeof value === "number" && Number.isFinite(value) ? value : null;
186
+ }
187
+ function providerCount(value) {
188
+ const parsed = typeof value === "string" && /^\d{1,4}$/.test(value) ? Number(value) : value;
189
+ return Number.isSafeInteger(parsed) && parsed >= 0 ? parsed : null;
190
+ }
191
+ function malformed(what) {
192
+ throw new ProviderProtocolError(`Cloudbeds returned malformed ${what}`, "invalid_response");
193
+ }
194
+ function propertyId(source) {
195
+ const value = source.metadata.propertyId;
196
+ if (typeof value !== "string" || !PROPERTY_ID.test(value)) throw new Error("cloudbeds: connection requires one numeric propertyId");
197
+ return value;
198
+ }
199
+ function apiKey(source) {
200
+ const value = source.credentials;
201
+ if (value.kind !== "api-key" || !value.apiKey || /[\u0000- \u007f]/.test(value.apiKey)) throw new Error("cloudbeds: connection requires an API key");
202
+ return value.apiKey;
203
+ }
204
+ async function call(source, path, init) {
205
+ const headers = {
206
+ "x-api-key": apiKey(source),
207
+ accept: "application/json",
208
+ ...init.headers
209
+ };
210
+ let result;
211
+ try {
212
+ result = await requestJson(`${API}/${path}`, {
213
+ ...init,
214
+ headers
215
+ }, { maxResponseBytes: 2e6 });
216
+ } catch (error) {
217
+ if (error instanceof ProviderProtocolError && error.status === 401) throw new CredentialsExpired("Cloudbeds rejected the API key", source.id);
218
+ if (error instanceof ProviderProtocolError && error.status === 429) throw new ProviderRateLimited("Cloudbeds rate limit (429)", source.id, {
219
+ status: 429,
220
+ retryAfterMs: error.retryAfterMs
221
+ });
222
+ throw error;
223
+ }
224
+ if (record(result) && result.success === false) throw new ProviderProtocolError("Cloudbeds rejected the request", "provider_rejected", 422, true);
225
+ if (!record(result) || result.success !== true) throw new ProviderProtocolError("Cloudbeds returned an unsuccessful or malformed response", "invalid_response");
226
+ return result;
227
+ }
228
+ const form = (body) => ({
229
+ method: "POST",
230
+ headers: { "content-type": "application/x-www-form-urlencoded" },
231
+ body: body.toString()
232
+ });
233
+ function date(value, label) {
234
+ if (typeof value !== "string" || !DATE.test(value) || Number.isNaN(Date.parse(`${value}T00:00:00Z`)) || (/* @__PURE__ */ new Date(`${value}T00:00:00Z`)).toISOString().slice(0, 10) !== value) throw new Error(`cloudbeds: invalid ${label}`);
235
+ return value;
236
+ }
237
+ function providerDate(value) {
238
+ try {
239
+ return date(value, "provider date");
240
+ } catch {
241
+ throw new ProviderProtocolError("Cloudbeds returned an invalid reservation date", "invalid_response");
242
+ }
243
+ }
244
+ function nights(startDate, endDate) {
245
+ return (Date.parse(`${endDate}T00:00:00Z`) - Date.parse(`${startDate}T00:00:00Z`)) / 864e5;
246
+ }
247
+ function page(value, label, fallback, max) {
248
+ const selected = value ?? fallback;
249
+ if (!Number.isSafeInteger(selected) || selected < 1 || selected > max) throw new Error(`cloudbeds: invalid ${label}`);
250
+ return selected;
251
+ }
252
+ function money(value, label, allowZero = false) {
253
+ if (typeof value !== "number" || !Number.isFinite(value) || value < (allowZero ? 0 : .01) || value > 1e6 || Math.abs(value * 100 - Math.round(value * 100)) > 1e-7) throw new Error(`cloudbeds: invalid ${label}`);
254
+ return value;
255
+ }
256
+ function pagination(result, pageSize, what) {
257
+ if (!Array.isArray(result.data) || !Number.isSafeInteger(result.count) || !Number.isSafeInteger(result.total) || result.count < 0 || result.total < 0 || result.count !== result.data.length || result.total < result.count || result.count > pageSize) malformed(`${what} pagination`);
258
+ return result.data;
259
+ }
260
+ function distinct(values) {
261
+ return [...new Set(values.filter((value) => value !== null))];
262
+ }
263
+ function reservationQuery(args, property) {
264
+ const query = new URLSearchParams({ propertyID: property });
265
+ const pairs = [["checkInFrom", "checkInTo"], ["checkOutFrom", "checkOutTo"]];
266
+ let found = false;
267
+ for (const [fromKey, toKey] of pairs) {
268
+ if (args[fromKey] === void 0 && args[toKey] === void 0) continue;
269
+ const from = date(args[fromKey], fromKey);
270
+ const to = date(args[toKey], toKey);
271
+ const days = nights(from, to);
272
+ if (days < 0 || days > 31) throw new Error("cloudbeds: reservation date range must be at most 31 days");
273
+ query.set(fromKey, from);
274
+ query.set(toKey, to);
275
+ found = true;
276
+ }
277
+ if (args.status !== void 0) {
278
+ if (typeof args.status !== "string" || !STATUSES.has(args.status)) throw new Error("cloudbeds: invalid status");
279
+ query.set("status", args.status);
280
+ }
281
+ if (!found && args.status !== "checked_in") throw new Error("cloudbeds: an arrival or departure date range is required unless status is checked_in");
282
+ query.set("includeAllRooms", "true");
283
+ query.set("pageNumber", String(page(args.pageNumber, "pageNumber", 1, 1e5)));
284
+ query.set("pageSize", String(page(args.pageSize, "pageSize", 100, 100)));
285
+ return query;
286
+ }
287
+ function reservationRooms(raw) {
288
+ if (raw === void 0 || raw === null) return {
289
+ roomTypeIds: [],
290
+ roomIds: [],
291
+ roomNames: []
292
+ };
293
+ if (!Array.isArray(raw)) malformed("reservation rooms");
294
+ const rooms = raw.map((room) => {
295
+ if (!record(room) || typeof room.roomTypeID !== "string" || !PROVIDER_ID.test(room.roomTypeID) || room.roomID !== void 0 && room.roomID !== null && (typeof room.roomID !== "string" || !PROVIDER_ID.test(room.roomID)) || room.roomName !== void 0 && room.roomName !== null && providerText(room.roomName) === null) malformed("reservation rooms");
296
+ return {
297
+ roomTypeId: room.roomTypeID,
298
+ roomId: typeof room.roomID === "string" ? room.roomID : null,
299
+ roomName: providerText(room.roomName)
300
+ };
301
+ });
302
+ return {
303
+ roomTypeIds: distinct(rooms.map((room) => room.roomTypeId)),
304
+ roomIds: distinct(rooms.map((room) => room.roomId)),
305
+ roomNames: distinct(rooms.map((room) => room.roomName))
306
+ };
307
+ }
308
+ function reservations(result, property, query) {
309
+ const pageSize = Number(query.get("pageSize"));
310
+ const items = pagination(result, pageSize, "reservation").map((raw) => {
311
+ if (!record(raw) || raw.propertyID !== property || typeof raw.reservationID !== "string" || !raw.reservationID || typeof raw.status !== "string" || !STATUSES.has(raw.status)) throw new ProviderProtocolError("Cloudbeds returned a reservation outside the connected property or with invalid fields", "invalid_response");
312
+ return {
313
+ propertyId: property,
314
+ reservationId: raw.reservationID,
315
+ status: raw.status,
316
+ arrivalDate: providerDate(raw.startDate),
317
+ departureDate: providerDate(raw.endDate),
318
+ guestId: typeof raw.guestID === "string" && PROVIDER_ID.test(raw.guestID) ? raw.guestID : null,
319
+ guestName: providerText(raw.guestName),
320
+ balance: providerNumber(raw.balance),
321
+ ...reservationRooms(raw.rooms)
322
+ };
323
+ });
324
+ const pageNumber = Number(query.get("pageNumber"));
325
+ return {
326
+ reservations: items,
327
+ pageNumber,
328
+ pageSize,
329
+ total: result.total,
330
+ hasMore: pageNumber * pageSize < result.total
331
+ };
332
+ }
333
+ async function readReservation(source, property, reservationId) {
334
+ const result = await call(source, `getReservation?${new URLSearchParams({
335
+ propertyID: property,
336
+ reservationID: reservationId
337
+ })}`, { method: "GET" });
338
+ if (!record(result.data) || result.data.propertyID !== property || result.data.reservationID !== reservationId) throw new ProviderProtocolError("Cloudbeds could not verify the reservation belongs to the connected property", "invalid_response");
339
+ return result.data;
340
+ }
341
+ function reservationRoom(raw, assigned) {
342
+ if (!record(raw) || typeof raw.roomTypeID !== "string" || !PROVIDER_ID.test(raw.roomTypeID) || assigned && (typeof raw.roomID !== "string" || !PROVIDER_ID.test(raw.roomID))) malformed("reservation rooms");
343
+ return {
344
+ ...assigned ? {
345
+ roomId: raw.roomID,
346
+ roomName: providerText(raw.roomName)
347
+ } : {},
348
+ roomTypeId: raw.roomTypeID,
349
+ roomTypeName: providerText(raw.roomTypeName),
350
+ startDate: providerDate(raw.startDate),
351
+ endDate: providerDate(raw.endDate),
352
+ adults: providerCount(raw.adults),
353
+ children: providerCount(raw.children)
354
+ };
355
+ }
356
+ function balanceDetailed(raw) {
357
+ const value = Array.isArray(raw) ? raw.length === 0 ? null : raw.length === 1 ? raw[0] : malformed("balance detail") : raw;
358
+ if (value === null || value === void 0) return null;
359
+ if (!record(value)) malformed("balance detail");
360
+ const suggested = typeof value.suggestedDeposit === "string" && /^-?\d+(\.\d+)?$/.test(value.suggestedDeposit) ? Number(value.suggestedDeposit) : providerNumber(value.suggestedDeposit);
361
+ return {
362
+ subTotal: providerNumber(value.subTotal),
363
+ additionalItems: providerNumber(value.additionalItems),
364
+ taxesFees: providerNumber(value.taxesFees),
365
+ grandTotal: providerNumber(value.grandTotal),
366
+ paid: providerNumber(value.paid),
367
+ suggestedDeposit: suggested
368
+ };
369
+ }
370
+ function reservationDetail(raw, property) {
371
+ if (typeof raw.status !== "string" || !STATUSES.has(raw.status)) malformed("reservation status");
372
+ if (raw.assigned !== void 0 && !Array.isArray(raw.assigned) || raw.unassigned !== void 0 && !Array.isArray(raw.unassigned) || raw.guestList !== void 0 && raw.guestList !== null && !record(raw.guestList)) malformed("reservation detail");
373
+ const assignedRooms = (raw.assigned ?? []).map((room) => reservationRoom(room, true));
374
+ const unassignedRooms = (raw.unassigned ?? []).map((room) => reservationRoom(room, false));
375
+ const mainGuest = (record(raw.guestList) ? Object.values(raw.guestList) : []).find((guest) => record(guest) && guest.isMainGuest === true);
376
+ const guestId = record(mainGuest) && typeof mainGuest.guestID === "string" && PROVIDER_ID.test(mainGuest.guestID) ? mainGuest.guestID : null;
377
+ const rooms = [...assignedRooms, ...unassignedRooms];
378
+ const sum = (field) => rooms.every((room) => typeof room[field] === "number") ? rooms.reduce((total, room) => total + room[field], 0) : null;
379
+ return {
380
+ propertyId: property,
381
+ reservationId: raw.reservationID,
382
+ status: raw.status,
383
+ arrivalDate: providerDate(raw.startDate),
384
+ departureDate: providerDate(raw.endDate),
385
+ guestId,
386
+ guestName: providerText(raw.guestName),
387
+ adults: sum("adults"),
388
+ children: sum("children"),
389
+ assignedRooms,
390
+ unassignedRooms,
391
+ total: providerNumber(raw.total),
392
+ balance: providerNumber(raw.balance),
393
+ balanceDetailed: balanceDetailed(raw.balanceDetailed),
394
+ source: providerText(raw.source),
395
+ sourceId: providerText(raw.sourceID)
396
+ };
397
+ }
398
+ function guestDetail(result, property, guestId) {
399
+ const raw = result.data;
400
+ if (!record(raw) || raw.firstName !== void 0 && typeof raw.firstName !== "string" || raw.lastName !== void 0 && typeof raw.lastName !== "string") malformed("guest");
401
+ const text = (value) => providerText(value) || null;
402
+ return {
403
+ propertyId: property,
404
+ guestId,
405
+ firstName: text(raw.firstName),
406
+ lastName: text(raw.lastName),
407
+ email: text(raw.email),
408
+ phone: text(raw.phone),
409
+ cellPhone: text(raw.cellPhone),
410
+ country: text(raw.country),
411
+ nationality: text(raw.guestNationality),
412
+ isAnonymized: raw.isAnonymized === true,
413
+ mergedIntoGuestId: raw.isMerged === true && typeof raw.newGuestID === "string" && PROVIDER_ID.test(raw.newGuestID) ? raw.newGuestID : null
414
+ };
415
+ }
416
+ function guestNotes(result, property, guestId) {
417
+ if (!Array.isArray(result.data)) malformed("guest notes");
418
+ return {
419
+ propertyId: property,
420
+ guestId,
421
+ notes: result.data.map((raw) => {
422
+ if (!record(raw) || typeof raw.guestNoteID !== "string" || !PROVIDER_ID.test(raw.guestNoteID) || typeof raw.guestNote !== "string") malformed("guest notes");
423
+ return {
424
+ noteId: raw.guestNoteID,
425
+ note: providerText(raw.guestNote, 4e3),
426
+ author: providerText(raw.userName),
427
+ createdAt: providerText(raw.dateCreated),
428
+ modifiedAt: providerText(raw.dateModified)
429
+ };
430
+ })
431
+ };
432
+ }
433
+ function housekeepingQuery(args, property) {
434
+ const query = new URLSearchParams({ propertyID: property });
435
+ if (args.roomId !== void 0) query.set("roomIDs", providerId(args.roomId, "roomId"));
436
+ if (args.roomCondition !== void 0) {
437
+ if (typeof args.roomCondition !== "string" || !ROOM_CONDITIONS.has(args.roomCondition)) throw new Error("cloudbeds: invalid roomCondition");
438
+ query.set("roomCondition", args.roomCondition);
439
+ }
440
+ query.set("pageNumber", String(page(args.pageNumber, "pageNumber", 1, 1e5)));
441
+ query.set("pageSize", String(page(args.pageSize, "pageSize", 100, 500)));
442
+ return query;
443
+ }
444
+ function housekeepingRoom(raw) {
445
+ if (!record(raw) || typeof raw.roomID !== "string" || !PROVIDER_ID.test(raw.roomID) || typeof raw.roomCondition !== "string" || !REPORTED_ROOM_CONDITIONS.has(raw.roomCondition) || typeof raw.roomOccupied !== "boolean" || raw.doNotDisturb !== void 0 && typeof raw.doNotDisturb !== "boolean" || raw.roomBlocked !== void 0 && typeof raw.roomBlocked !== "boolean") malformed("housekeeping status");
446
+ return {
447
+ roomId: raw.roomID,
448
+ roomName: providerText(raw.roomName),
449
+ roomTypeId: typeof raw.roomTypeID === "string" && PROVIDER_ID.test(raw.roomTypeID) ? raw.roomTypeID : null,
450
+ roomTypeName: providerText(raw.roomTypeName),
451
+ roomCondition: raw.roomCondition,
452
+ roomOccupied: raw.roomOccupied,
453
+ doNotDisturb: raw.doNotDisturb === true,
454
+ roomBlocked: raw.roomBlocked === true,
455
+ frontdeskStatus: typeof raw.frontdeskStatus === "string" && FRONTDESK_STATUSES.has(raw.frontdeskStatus) ? raw.frontdeskStatus : null,
456
+ housekeeperId: typeof raw.housekeeperID === "string" && PROVIDER_ID.test(raw.housekeeperID) ? raw.housekeeperID : null,
457
+ housekeeper: providerText(raw.housekeeper) || null,
458
+ conditionDate: typeof raw.date === "string" && DATE.test(raw.date) ? raw.date : null
459
+ };
460
+ }
461
+ function housekeeping(result, property, query) {
462
+ const pageSize = Number(query.get("pageSize"));
463
+ const rooms = pagination(result, pageSize, "housekeeping").map(housekeepingRoom);
464
+ const roomId = query.get("roomIDs");
465
+ if (roomId && rooms.some((room) => room.roomId !== roomId)) malformed("housekeeping status for another room");
466
+ const pageNumber = Number(query.get("pageNumber"));
467
+ return {
468
+ propertyId: property,
469
+ rooms,
470
+ pageNumber,
471
+ pageSize,
472
+ total: result.total,
473
+ hasMore: pageNumber * pageSize < result.total
474
+ };
475
+ }
476
+ function ratePlanQuery(args, property) {
477
+ const startDate = date(args.startDate, "startDate");
478
+ const endDate = date(args.endDate, "endDate");
479
+ const days = nights(startDate, endDate);
480
+ if (days < 1 || days > 31) throw new Error("cloudbeds: stay must be between 1 and 31 nights");
481
+ const query = new URLSearchParams({
482
+ propertyIDs: property,
483
+ startDate,
484
+ endDate,
485
+ detailedRates: "false"
486
+ });
487
+ if (args.roomTypeId !== void 0) query.set("roomTypeID", providerId(args.roomTypeId, "roomTypeId"));
488
+ return query;
489
+ }
490
+ function ratePlans(result, property, query) {
491
+ if (!Array.isArray(result.data) || result.data.length > 2e3) malformed("rate plans");
492
+ const roomTypeId = query.get("roomTypeID");
493
+ const rates = result.data.map((raw) => {
494
+ if (!record(raw) || raw.propertyID !== void 0 && raw.propertyID !== null && String(raw.propertyID) !== property) throw new ProviderProtocolError("Cloudbeds returned a rate outside the connected property", "invalid_response");
495
+ if (typeof raw.rateID !== "string" || !PROVIDER_ID.test(raw.rateID) || raw.roomRate !== void 0 && providerNumber(raw.roomRate) === null || raw.totalRate !== void 0 && providerNumber(raw.totalRate) === null || raw.roomsAvailable !== void 0 && (!Number.isSafeInteger(raw.roomsAvailable) || raw.roomsAvailable < 0)) malformed("rate plans");
496
+ const rowRoomType = typeof raw.roomTypeID === "string" && PROVIDER_ID.test(raw.roomTypeID) ? raw.roomTypeID : roomTypeId;
497
+ if (roomTypeId && rowRoomType !== roomTypeId) malformed("rate plans for another room type");
498
+ return {
499
+ rateId: raw.rateID,
500
+ ratePlanId: typeof raw.ratePlanID === "string" && PROVIDER_ID.test(raw.ratePlanID) ? raw.ratePlanID : null,
501
+ ratePlanName: providerText(raw.ratePlanNamePublic) || null,
502
+ roomTypeId: rowRoomType,
503
+ roomTypeName: providerText(typeof raw.roomTypeName === "number" ? String(raw.roomTypeName) : raw.roomTypeName),
504
+ roomRate: providerNumber(raw.roomRate),
505
+ totalRate: providerNumber(raw.totalRate),
506
+ roomsAvailable: typeof raw.roomsAvailable === "number" ? raw.roomsAvailable : null,
507
+ isDerived: raw.isDerived === true
508
+ };
509
+ });
510
+ return {
511
+ propertyId: property,
512
+ startDate: query.get("startDate"),
513
+ endDate: query.get("endDate"),
514
+ rates
515
+ };
516
+ }
517
+ function formForItem(inv, property) {
518
+ const args = inv.args;
519
+ const reservationId = boundedString(args.reservationId, "reservationId");
520
+ const appItemId = boundedString(args.appItemId, "appItemId");
521
+ const itemName = boundedString(args.itemName, "itemName");
522
+ const itemCategoryName = boundedString(args.itemCategoryName, "itemCategoryName");
523
+ const itemPrice = money(args.itemPrice, "itemPrice");
524
+ const itemQuantity = page(args.itemQuantity, "itemQuantity", 1, 1e3);
525
+ if (!OPERATION_ID.test(inv.idempotencyKey)) throw new Error("cloudbeds: stable bounded idempotencyKey is required");
526
+ if (!Array.isArray(args.taxes) || args.taxes.length > 10) throw new Error("cloudbeds: explicit taxes array is required");
527
+ const form = new URLSearchParams({
528
+ propertyID: property,
529
+ reservationID: reservationId,
530
+ referenceID: inv.idempotencyKey,
531
+ "items[0][appItemID]": appItemId,
532
+ "items[0][itemName]": itemName,
533
+ "items[0][itemCategoryName]": itemCategoryName,
534
+ "items[0][itemPrice]": itemPrice.toFixed(2),
535
+ "items[0][itemQuantity]": String(itemQuantity),
536
+ itemPaid: "false"
537
+ });
538
+ args.taxes.forEach((tax, index) => {
539
+ if (!record(tax)) throw new Error("cloudbeds: invalid tax");
540
+ form.set(`items[0][itemTaxes][${index}][taxName]`, boundedString(tax.taxName, "taxName"));
541
+ form.set(`items[0][itemTaxes][${index}][taxValue]`, money(tax.taxValue, "taxValue", true).toFixed(2));
542
+ });
543
+ return form;
544
+ }
545
+ async function postFolioItem(inv, property) {
546
+ const body = formForItem(inv, property);
547
+ await readReservation(inv.source, property, body.get("reservationID"));
548
+ const result = await call(inv.source, "postCustomItem", form(body));
549
+ if (!record(result.data)) throw new ProviderProtocolError("Cloudbeds returned a malformed folio item receipt", "invalid_response");
550
+ const soldProductId = typeof result.data.soldProductID === "string" && result.data.soldProductID ? result.data.soldProductID : null;
551
+ const notice = providerText(result.data.notice);
552
+ const duplicate = soldProductId === null && notice !== null && /referenceid/i.test(notice) && /nothing was created/i.test(notice);
553
+ if (!soldProductId && !duplicate) throw new ProviderProtocolError("Cloudbeds returned an indeterminate folio item receipt", "capability_outcome_indeterminate");
554
+ return {
555
+ status: "committed",
556
+ data: {
557
+ referenceId: inv.idempotencyKey,
558
+ soldProductId,
559
+ transactionId: typeof result.data.transactionID === "string" ? result.data.transactionID : null,
560
+ duplicate,
561
+ notice
562
+ },
563
+ committedAt: Date.now(),
564
+ idempotentReplay: duplicate
565
+ };
566
+ }
567
+ async function updateHousekeeping(inv, property) {
568
+ const roomId = providerId(inv.args.roomId, "roomId");
569
+ const roomCondition = inv.args.roomCondition;
570
+ if (typeof roomCondition !== "string" || !ROOM_CONDITIONS.has(roomCondition)) throw new Error("cloudbeds: invalid roomCondition");
571
+ if (inv.args.doNotDisturb !== void 0 && typeof inv.args.doNotDisturb !== "boolean") throw new Error("cloudbeds: invalid doNotDisturb");
572
+ const query = new URLSearchParams({
573
+ propertyID: property,
574
+ roomIDs: roomId,
575
+ pageNumber: "1",
576
+ pageSize: "1"
577
+ });
578
+ const before = housekeeping(await call(inv.source, `getHousekeepingStatus?${query}`, { method: "GET" }), property, query).rooms[0];
579
+ if (!before) throw new Error("cloudbeds: room is not part of the connected property");
580
+ const body = new URLSearchParams({
581
+ propertyID: property,
582
+ roomID: roomId,
583
+ roomCondition
584
+ });
585
+ if (typeof inv.args.doNotDisturb === "boolean") body.set("doNotDisturb", String(inv.args.doNotDisturb));
586
+ const result = await call(inv.source, "postHousekeepingStatus", form(body));
587
+ if (!record(result.data) || result.data.roomID !== roomId || result.data.roomCondition !== roomCondition || typeof inv.args.doNotDisturb === "boolean" && result.data.doNotDisturb !== void 0 && result.data.doNotDisturb !== null && result.data.doNotDisturb !== inv.args.doNotDisturb) throw new ProviderProtocolError("Cloudbeds returned an indeterminate housekeeping receipt", "capability_outcome_indeterminate");
588
+ return {
589
+ status: "committed",
590
+ committedAt: Date.now(),
591
+ idempotentReplay: false,
592
+ data: {
593
+ propertyId: property,
594
+ roomId,
595
+ roomCondition,
596
+ previousCondition: before.roomCondition,
597
+ doNotDisturb: typeof result.data.doNotDisturb === "boolean" ? result.data.doNotDisturb : before.doNotDisturb
598
+ }
599
+ };
600
+ }
601
+ async function listWebhooks(source, property) {
602
+ const result = await call(source, `getWebhooks?${new URLSearchParams({ propertyID: property })}`, { method: "GET" });
603
+ if (!Array.isArray(result.data) || result.data.length > 1e3) malformed("webhook subscriptions");
604
+ return result.data.map((raw) => {
605
+ if (!record(raw) || typeof raw.id !== "string" || !/^[A-Za-z0-9_-]{1,128}$/.test(raw.id) || !record(raw.event) || raw.key !== void 0 && raw.key !== null && !record(raw.key)) malformed("webhook subscriptions");
606
+ if (record(raw.key) && raw.key.type === "property" && String(raw.key.id) !== property) throw new ProviderProtocolError("Cloudbeds returned a webhook subscription outside the connected property", "invalid_response");
607
+ const data = record(raw.subscriptionData) ? raw.subscriptionData : {};
608
+ return {
609
+ subscriptionId: raw.id,
610
+ object: providerText(raw.event.entity),
611
+ action: providerText(raw.event.action),
612
+ endpointUrl: providerText(data.endpoint ?? data.url, 2048),
613
+ authHeaderName: record(raw.authHeader) ? providerText(raw.authHeader.name) : null
614
+ };
615
+ });
616
+ }
617
+ function webhookEndpoint(value) {
618
+ const text = boundedString(value, "endpointUrl", 2048);
619
+ let url;
620
+ try {
621
+ url = new URL(text);
622
+ } catch {
623
+ throw new Error("cloudbeds: invalid endpointUrl");
624
+ }
625
+ if (url.protocol !== "https:" || url.username || url.password || url.hash) throw new Error("cloudbeds: endpointUrl must be an https URL without credentials or fragment");
626
+ return text;
627
+ }
628
+ function redact(error, secret) {
629
+ if (secret && error instanceof Error && error.message.includes(secret)) error.message = error.message.split(secret).join("[redacted]");
630
+ return error;
631
+ }
632
+ async function subscribeWebhook(inv, property) {
633
+ const args = inv.args;
634
+ const event = CLOUDBEDS_WEBHOOK_EVENTS.find((item) => item.object === args.object && item.action === args.action);
635
+ if (!event) throw new Error("cloudbeds: unsupported webhook event");
636
+ const endpointUrl = webhookEndpoint(args.endpointUrl);
637
+ if (args.authHeaderName === void 0 !== (args.authHeaderValue === void 0)) throw new Error("cloudbeds: authHeaderName and authHeaderValue must be sent together");
638
+ let authHeaderName = null;
639
+ let authHeaderValue = null;
640
+ if (args.authHeaderName !== void 0) {
641
+ if (typeof args.authHeaderName !== "string" || !HEADER_NAME.test(args.authHeaderName)) throw new Error("cloudbeds: invalid authHeaderName");
642
+ if (typeof args.authHeaderValue !== "string" || !/^[!-~](?:[ -~]{0,1022}[!-~])?$/.test(args.authHeaderValue)) throw new Error("cloudbeds: invalid authHeaderValue");
643
+ authHeaderName = args.authHeaderName;
644
+ authHeaderValue = args.authHeaderValue;
645
+ }
646
+ try {
647
+ const existing = (await listWebhooks(inv.source, property)).find((row) => row.object === event.object && row.action === event.action && row.endpointUrl === endpointUrl);
648
+ const receipt = {
649
+ propertyId: property,
650
+ object: event.object,
651
+ action: event.action,
652
+ endpointUrl,
653
+ authHeaderName
654
+ };
655
+ if (existing && authHeaderValue === null) return {
656
+ status: "committed",
657
+ committedAt: Date.now(),
658
+ idempotentReplay: true,
659
+ data: {
660
+ ...receipt,
661
+ subscriptionId: existing.subscriptionId,
662
+ authHeaderName: existing.authHeaderName,
663
+ existing: true
664
+ }
665
+ };
666
+ const body = new URLSearchParams({
667
+ propertyID: property,
668
+ object: event.object,
669
+ action: event.action,
670
+ endpointUrl
671
+ });
672
+ if (authHeaderName && authHeaderValue) {
673
+ body.set("authHeaderName", authHeaderName);
674
+ body.set("authHeaderValue", authHeaderValue);
675
+ }
676
+ const result = await call(inv.source, "postWebhook", form(body));
677
+ if (!record(result.data) || typeof result.data.subscriptionID !== "string" || !/^[A-Za-z0-9_-]{1,128}$/.test(result.data.subscriptionID)) throw new ProviderProtocolError("Cloudbeds returned an indeterminate webhook subscription receipt", "capability_outcome_indeterminate");
678
+ return {
679
+ status: "committed",
680
+ committedAt: Date.now(),
681
+ idempotentReplay: false,
682
+ data: {
683
+ ...receipt,
684
+ subscriptionId: result.data.subscriptionID,
685
+ existing: existing !== void 0
686
+ }
687
+ };
688
+ } catch (error) {
689
+ throw redact(error, authHeaderValue);
690
+ }
691
+ }
692
+ async function deleteWebhook(inv, property) {
693
+ const subscriptionId = inv.args.subscriptionId;
694
+ if (typeof subscriptionId !== "string" || !/^[A-Za-z0-9_-]{1,128}$/.test(subscriptionId)) throw new Error("cloudbeds: invalid subscriptionId");
695
+ if (!(await listWebhooks(inv.source, property)).find((row) => row.subscriptionId === subscriptionId)) return {
696
+ status: "committed",
697
+ committedAt: Date.now(),
698
+ idempotentReplay: true,
699
+ data: {
700
+ propertyId: property,
701
+ subscriptionId,
702
+ deleted: false,
703
+ alreadyAbsent: true
704
+ }
705
+ };
706
+ await call(inv.source, `deleteWebhook?${new URLSearchParams({
707
+ propertyIDs: property,
708
+ subscriptionID: subscriptionId
709
+ })}`, { method: "DELETE" });
710
+ return {
711
+ status: "committed",
712
+ committedAt: Date.now(),
713
+ idempotentReplay: false,
714
+ data: {
715
+ propertyId: property,
716
+ subscriptionId,
717
+ deleted: true,
718
+ alreadyAbsent: false
719
+ }
720
+ };
721
+ }
722
+ const PAGE = {
723
+ pageNumber: {
724
+ type: "integer",
725
+ minimum: 1,
726
+ maximum: 1e5
727
+ },
728
+ pageSize: {
729
+ type: "integer",
730
+ minimum: 1,
731
+ maximum: 100
732
+ }
733
+ };
734
+ const ID = {
735
+ type: "string",
736
+ pattern: PROVIDER_ID.source
737
+ };
738
+ const STAY = {
739
+ startDate: {
740
+ type: "string",
741
+ format: "date"
742
+ },
743
+ endDate: {
744
+ type: "string",
745
+ format: "date"
746
+ }
747
+ };
748
+ const cloudbedsConnector = {
749
+ manifest: {
750
+ kind: "cloudbeds",
751
+ displayName: "Cloudbeds",
752
+ description: "Read reservations, guests, housekeeping, rates and room availability for one connected property, update room condition, manage reservation webhooks, and post an approved custom item to a guest folio.",
753
+ auth: {
754
+ kind: "api-key",
755
+ hint: "Property-scoped Cloudbeds API key with read:reservation, read:room, read:guest, read:housekeeping, write:housekeeping, read:rate and write:item scopes. Set the connection propertyId."
756
+ },
757
+ category: "other",
758
+ defaultConsistencyModel: "authoritative",
759
+ capabilities: [
760
+ {
761
+ name: "reservations.list",
762
+ class: "read",
763
+ requiredScopes: ["read:reservation"],
764
+ description: "List one page of arrivals, departures, or in-house guests (status checked_in with no dates) for the connected property, with room type, room ID and room name lists. Guest requirements are never requested.",
765
+ parameters: {
766
+ type: "object",
767
+ properties: {
768
+ checkInFrom: {
769
+ type: "string",
770
+ format: "date"
771
+ },
772
+ checkInTo: {
773
+ type: "string",
774
+ format: "date"
775
+ },
776
+ checkOutFrom: {
777
+ type: "string",
778
+ format: "date"
779
+ },
780
+ checkOutTo: {
781
+ type: "string",
782
+ format: "date"
783
+ },
784
+ status: {
785
+ type: "string",
786
+ enum: [...STATUSES]
787
+ },
788
+ ...PAGE
789
+ }
790
+ }
791
+ },
792
+ {
793
+ name: "reservations.get",
794
+ class: "read",
795
+ requiredScopes: ["read:reservation"],
796
+ description: "Read one reservation of the connected property: status, stay dates, main guest, assigned and unassigned rooms, guest counts, source, total, balance and balanceDetailed (paid and owed amounts; Cloudbeds v1.3 has no payments read).",
797
+ parameters: {
798
+ type: "object",
799
+ properties: { reservationId: {
800
+ type: "string",
801
+ minLength: 1,
802
+ maxLength: 256
803
+ } },
804
+ required: ["reservationId"]
805
+ }
806
+ },
807
+ {
808
+ name: "guests.get",
809
+ class: "read",
810
+ requiredScopes: ["read:guest"],
811
+ description: "Read one guest's name, email, phone, country and nationality at the connected property. Identity documents, address and birth date are never returned.",
812
+ parameters: {
813
+ type: "object",
814
+ properties: { guestId: ID },
815
+ required: ["guestId"]
816
+ }
817
+ },
818
+ {
819
+ name: "guests.notes",
820
+ class: "read",
821
+ requiredScopes: ["read:guest"],
822
+ description: "Read staff notes on one guest at the connected property.",
823
+ parameters: {
824
+ type: "object",
825
+ properties: { guestId: ID },
826
+ required: ["guestId"]
827
+ }
828
+ },
829
+ {
830
+ name: "housekeeping.status",
831
+ class: "read",
832
+ requiredScopes: ["read:housekeeping"],
833
+ description: "Read today's housekeeping status for the connected property: room condition, occupancy, do-not-disturb, front-desk status and housekeeper.",
834
+ parameters: {
835
+ type: "object",
836
+ properties: {
837
+ roomId: ID,
838
+ roomCondition: {
839
+ type: "string",
840
+ enum: [...ROOM_CONDITIONS]
841
+ },
842
+ pageNumber: PAGE.pageNumber,
843
+ pageSize: {
844
+ type: "integer",
845
+ minimum: 1,
846
+ maximum: 500
847
+ }
848
+ }
849
+ }
850
+ },
851
+ {
852
+ name: "housekeeping.update",
853
+ class: "mutation",
854
+ cas: "optimistic-read-verify",
855
+ externalEffect: false,
856
+ requiredScopes: ["read:housekeeping", "write:housekeeping"],
857
+ description: "Verify the room belongs to the connected property, then set today's room condition to clean or dirty and optionally do-not-disturb. Setting an explicit condition is idempotent.",
858
+ parameters: {
859
+ type: "object",
860
+ properties: {
861
+ roomId: ID,
862
+ roomCondition: {
863
+ type: "string",
864
+ enum: [...ROOM_CONDITIONS]
865
+ },
866
+ doNotDisturb: { type: "boolean" }
867
+ },
868
+ required: ["roomId", "roomCondition"]
869
+ }
870
+ },
871
+ {
872
+ name: "rate-plans.list",
873
+ class: "read",
874
+ requiredScopes: ["read:rate"],
875
+ description: "Read publicly sellable rate plans and stay rates for 1 to 31 nights at the connected property, optionally for one room type. This does not hold or price a booking.",
876
+ parameters: {
877
+ type: "object",
878
+ properties: {
879
+ ...STAY,
880
+ roomTypeId: ID
881
+ },
882
+ required: ["startDate", "endDate"]
883
+ }
884
+ },
885
+ {
886
+ name: "room-types.available",
887
+ class: "read",
888
+ requiredScopes: ["read:room"],
889
+ description: "Read provider-reported room-type availability for a stay at the connected property. This does not hold or confirm a booking.",
890
+ parameters: {
891
+ type: "object",
892
+ properties: {
893
+ ...STAY,
894
+ rooms: {
895
+ type: "integer",
896
+ minimum: 1,
897
+ maximum: 10
898
+ },
899
+ adults: {
900
+ type: "integer",
901
+ minimum: 1,
902
+ maximum: 20
903
+ },
904
+ children: {
905
+ type: "integer",
906
+ minimum: 0,
907
+ maximum: 20
908
+ },
909
+ ...PAGE
910
+ },
911
+ required: ["startDate", "endDate"]
912
+ }
913
+ },
914
+ {
915
+ name: "folio-items.post",
916
+ class: "mutation",
917
+ cas: "native-idempotency",
918
+ externalEffect: true,
919
+ requiredScopes: ["read:reservation", "write:item"],
920
+ description: "Verify reservation ownership, then post one unpaid custom item to the connected property guest folio. The Hub operation key becomes Cloudbeds referenceID.",
921
+ parameters: {
922
+ type: "object",
923
+ properties: {
924
+ reservationId: {
925
+ type: "string",
926
+ minLength: 1,
927
+ maxLength: 256
928
+ },
929
+ appItemId: {
930
+ type: "string",
931
+ minLength: 1,
932
+ maxLength: 256
933
+ },
934
+ itemName: {
935
+ type: "string",
936
+ minLength: 1,
937
+ maxLength: 256
938
+ },
939
+ itemCategoryName: {
940
+ type: "string",
941
+ minLength: 1,
942
+ maxLength: 256
943
+ },
944
+ itemPrice: {
945
+ type: "number",
946
+ minimum: .01,
947
+ maximum: 1e6,
948
+ multipleOf: .01
949
+ },
950
+ itemQuantity: {
951
+ type: "integer",
952
+ minimum: 1,
953
+ maximum: 1e3
954
+ },
955
+ taxes: {
956
+ type: "array",
957
+ description: "Explicit tax amounts in the property currency. Use [] only when no tax applies.",
958
+ maxItems: 10,
959
+ items: {
960
+ type: "object",
961
+ properties: {
962
+ taxName: {
963
+ type: "string",
964
+ minLength: 1,
965
+ maxLength: 256
966
+ },
967
+ taxValue: {
968
+ type: "number",
969
+ minimum: 0,
970
+ maximum: 1e6,
971
+ multipleOf: .01
972
+ }
973
+ },
974
+ required: ["taxName", "taxValue"]
975
+ }
976
+ }
977
+ },
978
+ required: [
979
+ "reservationId",
980
+ "appItemId",
981
+ "itemName",
982
+ "itemCategoryName",
983
+ "itemPrice",
984
+ "taxes"
985
+ ]
986
+ }
987
+ },
988
+ {
989
+ name: "webhooks.subscribe",
990
+ class: "mutation",
991
+ cas: "optimistic-read-verify",
992
+ externalEffect: true,
993
+ requiredScopes: ["read:reservation"],
994
+ description: "Subscribe an https endpoint to one reservation event for the connected property, reusing a matching subscription. An optional auth header is sent by Cloudbeds on every delivery and is never returned.",
995
+ parameters: {
996
+ type: "object",
997
+ properties: {
998
+ object: {
999
+ type: "string",
1000
+ enum: ["reservation"]
1001
+ },
1002
+ action: {
1003
+ type: "string",
1004
+ enum: CLOUDBEDS_WEBHOOK_EVENTS.map((event) => event.action)
1005
+ },
1006
+ endpointUrl: {
1007
+ type: "string",
1008
+ format: "uri",
1009
+ maxLength: 2048
1010
+ },
1011
+ authHeaderName: {
1012
+ type: "string",
1013
+ pattern: HEADER_NAME.source
1014
+ },
1015
+ authHeaderValue: {
1016
+ type: "string",
1017
+ minLength: 1,
1018
+ maxLength: 1024
1019
+ }
1020
+ },
1021
+ required: [
1022
+ "object",
1023
+ "action",
1024
+ "endpointUrl"
1025
+ ]
1026
+ }
1027
+ },
1028
+ {
1029
+ name: "webhooks.list",
1030
+ class: "read",
1031
+ requiredScopes: ["read:reservation"],
1032
+ description: "List webhook subscriptions visible to the connected property. Auth header values are never returned.",
1033
+ parameters: {
1034
+ type: "object",
1035
+ properties: {}
1036
+ }
1037
+ },
1038
+ {
1039
+ name: "webhooks.delete",
1040
+ class: "mutation",
1041
+ cas: "optimistic-read-verify",
1042
+ externalEffect: true,
1043
+ requiredScopes: ["read:reservation"],
1044
+ description: "Delete one webhook subscription after confirming it is visible to the connected property. A missing subscription is reported as already absent.",
1045
+ parameters: {
1046
+ type: "object",
1047
+ properties: { subscriptionId: {
1048
+ type: "string",
1049
+ pattern: "^[A-Za-z0-9_-]{1,128}$"
1050
+ } },
1051
+ required: ["subscriptionId"]
1052
+ }
1053
+ }
1054
+ ]
1055
+ },
1056
+ async executeRead(inv) {
1057
+ const property = propertyId(inv.source);
1058
+ const read = async (path) => call(inv.source, path, { method: "GET" });
1059
+ let data;
1060
+ switch (inv.capabilityName) {
1061
+ case "reservations.list": {
1062
+ const query = reservationQuery(inv.args, property);
1063
+ data = reservations(await read(`getReservations?${query}`), property, query);
1064
+ break;
1065
+ }
1066
+ case "reservations.get": {
1067
+ const reservationId = boundedString(inv.args.reservationId, "reservationId");
1068
+ data = reservationDetail(await readReservation(inv.source, property, reservationId), property);
1069
+ break;
1070
+ }
1071
+ case "guests.get": {
1072
+ const guestId = providerId(inv.args.guestId, "guestId");
1073
+ data = guestDetail(await read(`getGuest?${new URLSearchParams({
1074
+ propertyID: property,
1075
+ guestID: guestId
1076
+ })}`), property, guestId);
1077
+ break;
1078
+ }
1079
+ case "guests.notes": {
1080
+ const guestId = providerId(inv.args.guestId, "guestId");
1081
+ data = guestNotes(await read(`getGuestNotes?${new URLSearchParams({
1082
+ propertyID: property,
1083
+ guestID: guestId
1084
+ })}`), property, guestId);
1085
+ break;
1086
+ }
1087
+ case "housekeeping.status": {
1088
+ const query = housekeepingQuery(inv.args, property);
1089
+ data = housekeeping(await read(`getHousekeepingStatus?${query}`), property, query);
1090
+ break;
1091
+ }
1092
+ case "rate-plans.list": {
1093
+ const query = ratePlanQuery(inv.args, property);
1094
+ data = ratePlans(await read(`getRatePlans?${query}`), property, query);
1095
+ break;
1096
+ }
1097
+ case "room-types.available": {
1098
+ const query = roomAvailabilityQuery(inv.args, property);
1099
+ data = roomAvailability(await read(`getAvailableRoomTypes?${query}`), property, query);
1100
+ break;
1101
+ }
1102
+ case "webhooks.list":
1103
+ data = {
1104
+ propertyId: property,
1105
+ subscriptions: await listWebhooks(inv.source, property)
1106
+ };
1107
+ break;
1108
+ default: throw new Error(`cloudbeds: unknown read ${inv.capabilityName}`);
1109
+ }
1110
+ return {
1111
+ data,
1112
+ fetchedAt: Date.now()
1113
+ };
1114
+ },
1115
+ async executeMutation(inv) {
1116
+ const property = propertyId(inv.source);
1117
+ switch (inv.capabilityName) {
1118
+ case "folio-items.post": return postFolioItem(inv, property);
1119
+ case "housekeeping.update": return updateHousekeeping(inv, property);
1120
+ case "webhooks.subscribe": return subscribeWebhook(inv, property);
1121
+ case "webhooks.delete": return deleteWebhook(inv, property);
1122
+ default: throw new Error(`cloudbeds: unknown mutation ${inv.capabilityName}`);
1123
+ }
1124
+ },
1125
+ async test(source) {
1126
+ try {
1127
+ const property = propertyId(source);
1128
+ const result = await call(source, `getReservations?${new URLSearchParams({
1129
+ propertyID: property,
1130
+ status: "checked_in",
1131
+ pageNumber: "1",
1132
+ pageSize: "1"
1133
+ })}`, { method: "GET" });
1134
+ if (!Array.isArray(result.data)) throw new ProviderProtocolError("Cloudbeds returned malformed reservations", "invalid_response");
1135
+ return { ok: true };
1136
+ } catch (error) {
1137
+ return {
1138
+ ok: false,
1139
+ reason: error instanceof Error ? error.message : "Cloudbeds connection failed"
1140
+ };
1141
+ }
1142
+ }
1143
+ };
1144
+ function roomAvailabilityQuery(args, property) {
1145
+ const startDate = date(args.startDate, "startDate");
1146
+ const endDate = date(args.endDate, "endDate");
1147
+ const days = nights(startDate, endDate);
1148
+ if (days < 1 || days > 31) throw new Error("cloudbeds: stay must be between 1 and 31 nights");
1149
+ const children = args.children ?? 0;
1150
+ if (!Number.isSafeInteger(children) || children < 0 || children > 20) throw new Error("cloudbeds: invalid children");
1151
+ return new URLSearchParams({
1152
+ propertyIDs: property,
1153
+ startDate,
1154
+ endDate,
1155
+ rooms: String(page(args.rooms, "rooms", 1, 10)),
1156
+ adults: String(page(args.adults, "adults", 1, 20)),
1157
+ children: String(children),
1158
+ pageNumber: String(page(args.pageNumber, "pageNumber", 1, 1e5)),
1159
+ pageSize: String(page(args.pageSize, "pageSize", 100, 100)),
1160
+ detailedRates: "false",
1161
+ includeSharedRooms: "false"
1162
+ });
1163
+ }
1164
+ function roomAvailability(result, property, query) {
1165
+ if (!Array.isArray(result.data) || !Number.isSafeInteger(result.count) || !Number.isSafeInteger(result.total) || !Number.isSafeInteger(result.roomCount) || result.count < 0 || result.count > 1 || result.total < result.count || result.count !== result.data.length || result.roomCount < 0 || result.count > Number(query.get("pageSize"))) throw new ProviderProtocolError("Cloudbeds returned malformed room availability pagination", "invalid_response");
1166
+ const roomTypes = result.data.flatMap((raw) => {
1167
+ if (!record(raw) || raw.propertyID !== property || !Array.isArray(raw.propertyRooms)) throw new ProviderProtocolError("Cloudbeds returned availability outside the connected property", "invalid_response");
1168
+ return raw.propertyRooms.map((room) => {
1169
+ if (!record(room) || typeof room.roomTypeID !== "string" || !room.roomTypeID || !Number.isSafeInteger(room.roomsAvailable) || room.roomsAvailable < 0 || room.roomRate !== void 0 && (typeof room.roomRate !== "number" || !Number.isFinite(room.roomRate) || room.roomRate < 0)) throw new ProviderProtocolError("Cloudbeds returned malformed room-type availability", "invalid_response");
1170
+ return {
1171
+ roomTypeId: room.roomTypeID,
1172
+ roomTypeName: providerText(room.roomTypeName),
1173
+ roomsAvailable: room.roomsAvailable,
1174
+ roomRate: typeof room.roomRate === "number" ? room.roomRate : null
1175
+ };
1176
+ });
1177
+ });
1178
+ if (roomTypes.length !== result.roomCount) throw new ProviderProtocolError("Cloudbeds returned mismatched room-type count", "invalid_response");
1179
+ return {
1180
+ propertyId: property,
1181
+ startDate: query.get("startDate"),
1182
+ endDate: query.get("endDate"),
1183
+ rooms: Number(query.get("rooms")),
1184
+ adults: Number(query.get("adults")),
1185
+ children: Number(query.get("children")),
1186
+ roomTypes,
1187
+ pageNumber: Number(query.get("pageNumber")),
1188
+ pageSize: Number(query.get("pageSize")),
1189
+ mayHaveMore: Number(query.get("pageNumber")) * Number(query.get("pageSize")) < result.total
1190
+ };
1191
+ }
1192
+ //#endregion
1193
+ export { parseStripeSignatureHeader as a, verifySlackSignature as c, firstHeader as i, verifyStripeSignature as l, cloudbedsConnector as n, verifyEmailSignature as o, DEFAULT_SIGNATURE_TOLERANCE_SECONDS as r, verifyHmacSignature as s, CLOUDBEDS_WEBHOOK_EVENTS as t, verifyTwilioSignature as u };
1194
+
1195
+ //# sourceMappingURL=cloudbeds-BL5EF3xl.js.map