@tangle-network/agent-integrations 0.73.0 → 0.74.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/data/bundled-adapter-manifests.json +1 -1
- package/dist/{adapters-D8TvHRA1.js → adapters-DVwQyQCI.js} +414 -631
- package/dist/{adapters-D8TvHRA1.js.map → adapters-DVwQyQCI.js.map} +1 -1
- package/dist/{bundled-manifest-data-MdURriam.js → bundled-manifest-data-Bj_ENela.js} +276 -12
- package/dist/{bundled-manifest-data-MdURriam.js.map → bundled-manifest-data-Bj_ENela.js.map} +1 -1
- package/dist/cloudbeds-BL5EF3xl.js +1195 -0
- package/dist/cloudbeds-BL5EF3xl.js.map +1 -0
- package/dist/connect/index.js +1 -1
- package/dist/connectors/adapters/index.d.ts +1 -1
- package/dist/connectors/adapters/index.js +3 -2
- package/dist/connectors/index.d.ts +2 -2
- package/dist/connectors/index.js +5 -4
- package/dist/{consumer-DaKqPDG-.d.ts → consumer-BslMZthR.d.ts} +2 -2
- package/dist/{consumer-DaKqPDG-.d.ts.map → consumer-BslMZthR.d.ts.map} +1 -1
- package/dist/consumer.d.ts +1 -1
- package/dist/consumer.js +1 -1
- package/dist/{index-b05PzTtX.d.ts → index-B227L17C.d.ts} +2 -2
- package/dist/{index-b05PzTtX.d.ts.map → index-B227L17C.d.ts.map} +1 -1
- package/dist/{index-udV3gGjx.d.ts → index-Gn72pQCA.d.ts} +10 -1
- package/dist/index-Gn72pQCA.d.ts.map +1 -0
- package/dist/index.d.ts +3 -3
- package/dist/index.js +7 -6
- package/dist/internal/bundled-manifest-runtime.js +1 -1
- package/dist/mcp.d.ts +1 -1
- package/dist/mcp.js +1 -1
- package/dist/middleware/index.js +1 -1
- package/dist/registry.js +1 -1
- package/dist/runtime.d.ts +1 -1
- package/dist/sandbox.d.ts +1 -1
- package/dist/{specs-BYUQOsSC.js → specs-BnY10gv2.js} +2 -2
- package/dist/{specs-BYUQOsSC.js.map → specs-BnY10gv2.js.map} +1 -1
- package/dist/specs.js +1 -1
- package/dist/{src-c7Ru9_WT.js → src-Bgyicg7S.js} +4 -4
- package/dist/{src-c7Ru9_WT.js.map → src-Bgyicg7S.js.map} +1 -1
- package/dist/{tangle-id-D2kPF0rX.js → tangle-id-81ShqJ7k.js} +3 -262
- package/dist/tangle-id-81ShqJ7k.js.map +1 -0
- package/dist/types-BSDXGSi5.js +263 -0
- package/dist/types-BSDXGSi5.js.map +1 -0
- package/dist/webhooks/index.d.ts +5 -0
- package/dist/webhooks/index.d.ts.map +1 -1
- package/dist/webhooks/index.js +92 -2
- package/dist/webhooks/index.js.map +1 -1
- package/docs/hospitality-connectors.md +91 -11
- package/docs/knowledge-provider-contracts.md +86 -0
- package/package.json +2 -1
- package/dist/index-udV3gGjx.d.ts.map +0 -1
- package/dist/tangle-id-D2kPF0rX.js.map +0 -1
- package/dist/webhooks-FsjqRN_0.js +0 -117
- 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
|