@medalsocial/sdk 1.6.0 → 1.7.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.
- package/README.md +68 -0
- package/dist/openapi/medal-social.openapi.json +3097 -1489
- package/dist/pilot/index.d.mts +2 -2
- package/dist/pilot/index.d.ts +2 -2
- package/dist/src/index.d.mts +593 -23
- package/dist/src/index.d.ts +593 -23
- package/dist/src/index.js +472 -146
- package/dist/src/index.js.map +1 -1
- package/dist/src/index.mjs +471 -146
- package/dist/src/index.mjs.map +1 -1
- package/dist/src/openapi.generated.d.mts +905 -45
- package/dist/src/openapi.generated.d.ts +905 -45
- package/dist/src/openapi.generated.js.map +1 -1
- package/openapi/medal-social.openapi.yaml +944 -0
- package/package.json +18 -17
- package/skills/client/SKILL.md +2 -2
- package/skills/resources/SKILL.md +104 -3
package/dist/src/index.mjs
CHANGED
|
@@ -1,113 +1,3 @@
|
|
|
1
|
-
// src/types/capabilities.ts
|
|
2
|
-
var CAPABILITY_IDS = [
|
|
3
|
-
"channel.connect_link.create.execute",
|
|
4
|
-
"channel.connect_link.revoke.execute",
|
|
5
|
-
"channel.connection.disconnect.execute",
|
|
6
|
-
"helpdesk.conversation.reply.execute",
|
|
7
|
-
"helpdesk.conversation.update.execute",
|
|
8
|
-
"helpdesk.webhook.create.execute",
|
|
9
|
-
"helpdesk.webhook.update.execute",
|
|
10
|
-
"helpdesk.webhook.delete.execute"
|
|
11
|
-
];
|
|
12
|
-
var CAPABILITY_ROUTES = {
|
|
13
|
-
"channel.connect_link.create.execute": {
|
|
14
|
-
method: "POST",
|
|
15
|
-
path_template: "/api/v1/channels/connect-links"
|
|
16
|
-
},
|
|
17
|
-
"channel.connect_link.revoke.execute": {
|
|
18
|
-
method: "DELETE",
|
|
19
|
-
path_template: "/api/v1/channels/connect-links/{id}"
|
|
20
|
-
},
|
|
21
|
-
"channel.connection.disconnect.execute": {
|
|
22
|
-
method: "DELETE",
|
|
23
|
-
path_template: "/api/v1/channels/connections/{id}"
|
|
24
|
-
},
|
|
25
|
-
"helpdesk.conversation.reply.execute": {
|
|
26
|
-
method: "POST",
|
|
27
|
-
path_template: "/api/v1/helpdesk/replies"
|
|
28
|
-
},
|
|
29
|
-
"helpdesk.conversation.update.execute": {
|
|
30
|
-
method: "PATCH",
|
|
31
|
-
path_template: "/api/v1/helpdesk/conversations/{id}"
|
|
32
|
-
},
|
|
33
|
-
"helpdesk.webhook.create.execute": {
|
|
34
|
-
method: "POST",
|
|
35
|
-
path_template: "/api/v1/webhooks"
|
|
36
|
-
},
|
|
37
|
-
"helpdesk.webhook.update.execute": {
|
|
38
|
-
method: "PATCH",
|
|
39
|
-
path_template: "/api/v1/webhooks/{id}"
|
|
40
|
-
},
|
|
41
|
-
"helpdesk.webhook.delete.execute": {
|
|
42
|
-
method: "DELETE",
|
|
43
|
-
path_template: "/api/v1/webhooks/{id}"
|
|
44
|
-
}
|
|
45
|
-
};
|
|
46
|
-
|
|
47
|
-
// src/capability-confirmer.ts
|
|
48
|
-
function newIdempotencyKey() {
|
|
49
|
-
const cryptoRef = globalThis.crypto;
|
|
50
|
-
if (typeof cryptoRef?.randomUUID === "function") {
|
|
51
|
-
return cryptoRef.randomUUID();
|
|
52
|
-
}
|
|
53
|
-
return `idem_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 12)}`;
|
|
54
|
-
}
|
|
55
|
-
function resolvePath(template, pathParams) {
|
|
56
|
-
return template.replace(/\{([^}/]+)\}/g, (_match, name) => {
|
|
57
|
-
const value = pathParams?.[name];
|
|
58
|
-
return value === void 0 ? `{${name}}` : encodeURIComponent(String(value));
|
|
59
|
-
});
|
|
60
|
-
}
|
|
61
|
-
var CapabilityConfirmer = class {
|
|
62
|
-
constructor(confirmations, defaults) {
|
|
63
|
-
this.confirmations = confirmations;
|
|
64
|
-
this.defaults = defaults;
|
|
65
|
-
}
|
|
66
|
-
confirmations;
|
|
67
|
-
defaults;
|
|
68
|
-
/**
|
|
69
|
-
* Return the request options to use for a confirmable write, minting the
|
|
70
|
-
* idempotency key and confirmation token first when auto-confirm is active.
|
|
71
|
-
*
|
|
72
|
-
* `body` is the pending request payload (`undefined` for `DELETE` routes).
|
|
73
|
-
* It is handed to the `previewSummary` callback by reference so the summary
|
|
74
|
-
* can describe the specific action, not just the route — it is the caller's
|
|
75
|
-
* own payload, so it is passed through unmodified and unredacted.
|
|
76
|
-
*/
|
|
77
|
-
async prepare(request, pathParams, options) {
|
|
78
|
-
const auto = options?.autoConfirm === false ? void 0 : options?.autoConfirm ?? this.defaults;
|
|
79
|
-
if (!auto) return options;
|
|
80
|
-
if (options?.idempotencyKey && options?.capabilityConfirmation) return options;
|
|
81
|
-
const route = CAPABILITY_ROUTES[request.capabilityId];
|
|
82
|
-
const idempotencyKey = options?.idempotencyKey ?? newIdempotencyKey();
|
|
83
|
-
const path = resolvePath(route.path_template, pathParams);
|
|
84
|
-
const previewSummary = auto.previewSummary({
|
|
85
|
-
...request,
|
|
86
|
-
method: route.method,
|
|
87
|
-
path,
|
|
88
|
-
...pathParams ? { pathParams } : {},
|
|
89
|
-
idempotencyKey
|
|
90
|
-
});
|
|
91
|
-
if (typeof previewSummary !== "string" || previewSummary.trim() === "") {
|
|
92
|
-
throw new Error(
|
|
93
|
-
`autoConfirm.previewSummary must return a non-empty summary for ${request.capabilityId}. The summary is the audit record of what your user approved \u2014 refusing to assert user_approved: true without one.`
|
|
94
|
-
);
|
|
95
|
-
}
|
|
96
|
-
const { data } = await this.confirmations.create({
|
|
97
|
-
capability_id: request.capabilityId,
|
|
98
|
-
...pathParams ? { path_params: pathParams } : {},
|
|
99
|
-
idempotency_key: idempotencyKey,
|
|
100
|
-
preview_summary: previewSummary,
|
|
101
|
-
user_approved: true
|
|
102
|
-
});
|
|
103
|
-
return {
|
|
104
|
-
...options,
|
|
105
|
-
idempotencyKey,
|
|
106
|
-
capabilityConfirmation: data.confirmation_token
|
|
107
|
-
};
|
|
108
|
-
}
|
|
109
|
-
};
|
|
110
|
-
|
|
111
1
|
// src/types/common.ts
|
|
112
2
|
var MedalApiError = class extends Error {
|
|
113
3
|
status;
|
|
@@ -123,6 +13,21 @@ var MedalApiError = class extends Error {
|
|
|
123
13
|
};
|
|
124
14
|
|
|
125
15
|
// src/client.ts
|
|
16
|
+
function randomIdempotencyKey() {
|
|
17
|
+
const webCrypto = globalThis.crypto;
|
|
18
|
+
if (typeof webCrypto.randomUUID === "function") {
|
|
19
|
+
return webCrypto.randomUUID();
|
|
20
|
+
}
|
|
21
|
+
const bytes = new Uint8Array(16);
|
|
22
|
+
webCrypto.getRandomValues(bytes);
|
|
23
|
+
bytes[6] = bytes[6] & 15 | 64;
|
|
24
|
+
bytes[8] = bytes[8] & 63 | 128;
|
|
25
|
+
const hex = Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
26
|
+
return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
|
|
27
|
+
}
|
|
28
|
+
function resolveIdempotencyKey(supplied) {
|
|
29
|
+
return (supplied ?? "").trim() || randomIdempotencyKey();
|
|
30
|
+
}
|
|
126
31
|
var BaseClient = class {
|
|
127
32
|
/** Resolved client configuration. */
|
|
128
33
|
config;
|
|
@@ -142,6 +47,29 @@ var BaseClient = class {
|
|
|
142
47
|
body: body !== void 0 ? JSON.stringify(body) : void 0
|
|
143
48
|
});
|
|
144
49
|
}
|
|
50
|
+
/**
|
|
51
|
+
* Execute a POST that must never execute twice, guaranteeing an
|
|
52
|
+
* `Idempotency-Key`.
|
|
53
|
+
*
|
|
54
|
+
* {@link BaseClient.post} retries 429 and 5xx automatically, so a write
|
|
55
|
+
* whose transaction committed before the gateway failed would otherwise be
|
|
56
|
+
* submitted a second time — booking the same slot twice. A key turns that
|
|
57
|
+
* retry into a replay: the server keys on the key, the workspace, and the
|
|
58
|
+
* method+path, and answers a repeat with the stored response, or 409 while
|
|
59
|
+
* the first attempt is still in flight. Either way the write happens once.
|
|
60
|
+
*
|
|
61
|
+
* The key is minted ONCE here, outside the retry loop in `request`, so every
|
|
62
|
+
* attempt of the same logical call carries the same value — a key minted per
|
|
63
|
+
* attempt would deduplicate nothing. A caller-supplied key always wins, so
|
|
64
|
+
* callers keeping their own records stay in control. See
|
|
65
|
+
* {@link resolveIdempotencyKey} for what counts as supplied.
|
|
66
|
+
*/
|
|
67
|
+
async postOnce(path, body, options) {
|
|
68
|
+
return this.post(path, body, {
|
|
69
|
+
...options,
|
|
70
|
+
idempotencyKey: resolveIdempotencyKey(options?.idempotencyKey)
|
|
71
|
+
});
|
|
72
|
+
}
|
|
145
73
|
/** Execute an authenticated PATCH request with a JSON body. */
|
|
146
74
|
async patch(path, body, options) {
|
|
147
75
|
return this.request(this.buildUrl(path), {
|
|
@@ -193,12 +121,22 @@ var BaseClient = class {
|
|
|
193
121
|
const controller = new AbortController();
|
|
194
122
|
const timeout = setTimeout(() => controller.abort(), this.config.timeout);
|
|
195
123
|
let res;
|
|
124
|
+
let text = "";
|
|
125
|
+
let retrying = false;
|
|
196
126
|
try {
|
|
197
127
|
res = await fetch(url, { ...init, headers, signal: controller.signal });
|
|
128
|
+
retrying = (res.status === 429 || res.status >= 500 && res.status <= 599) && attempt < maxAttempts;
|
|
129
|
+
if (retrying) {
|
|
130
|
+
const drained = res.body ? res.body.pipeTo(new WritableStream()) : res.text();
|
|
131
|
+
await drained.catch(() => {
|
|
132
|
+
});
|
|
133
|
+
} else {
|
|
134
|
+
text = await res.text();
|
|
135
|
+
}
|
|
198
136
|
} finally {
|
|
199
137
|
clearTimeout(timeout);
|
|
200
138
|
}
|
|
201
|
-
if (
|
|
139
|
+
if (retrying) {
|
|
202
140
|
const retryAfter = res.headers.get("retry-after");
|
|
203
141
|
let delayMs = 0;
|
|
204
142
|
if (retryAfter) {
|
|
@@ -211,7 +149,6 @@ var BaseClient = class {
|
|
|
211
149
|
await new Promise((r) => setTimeout(r, delayMs));
|
|
212
150
|
continue;
|
|
213
151
|
}
|
|
214
|
-
const text = await res.text();
|
|
215
152
|
let parsed;
|
|
216
153
|
try {
|
|
217
154
|
parsed = text ? JSON.parse(text) : void 0;
|
|
@@ -233,6 +170,266 @@ var BaseClient = class {
|
|
|
233
170
|
}
|
|
234
171
|
};
|
|
235
172
|
|
|
173
|
+
// src/types/capabilities.ts
|
|
174
|
+
var CAPABILITY_IDS = [
|
|
175
|
+
"channel.connect_link.create.execute",
|
|
176
|
+
"channel.connect_link.revoke.execute",
|
|
177
|
+
"channel.connection.disconnect.execute",
|
|
178
|
+
"helpdesk.conversation.reply.execute",
|
|
179
|
+
"helpdesk.conversation.update.execute",
|
|
180
|
+
"helpdesk.webhook.create.execute",
|
|
181
|
+
"helpdesk.webhook.update.execute",
|
|
182
|
+
"helpdesk.webhook.delete.execute"
|
|
183
|
+
];
|
|
184
|
+
var CAPABILITY_ROUTES = {
|
|
185
|
+
"channel.connect_link.create.execute": {
|
|
186
|
+
method: "POST",
|
|
187
|
+
path_template: "/api/v1/channels/connect-links"
|
|
188
|
+
},
|
|
189
|
+
"channel.connect_link.revoke.execute": {
|
|
190
|
+
method: "DELETE",
|
|
191
|
+
path_template: "/api/v1/channels/connect-links/{id}"
|
|
192
|
+
},
|
|
193
|
+
"channel.connection.disconnect.execute": {
|
|
194
|
+
method: "DELETE",
|
|
195
|
+
path_template: "/api/v1/channels/connections/{id}"
|
|
196
|
+
},
|
|
197
|
+
"helpdesk.conversation.reply.execute": {
|
|
198
|
+
method: "POST",
|
|
199
|
+
path_template: "/api/v1/helpdesk/replies"
|
|
200
|
+
},
|
|
201
|
+
"helpdesk.conversation.update.execute": {
|
|
202
|
+
method: "PATCH",
|
|
203
|
+
path_template: "/api/v1/helpdesk/conversations/{id}"
|
|
204
|
+
},
|
|
205
|
+
"helpdesk.webhook.create.execute": {
|
|
206
|
+
method: "POST",
|
|
207
|
+
path_template: "/api/v1/webhooks"
|
|
208
|
+
},
|
|
209
|
+
"helpdesk.webhook.update.execute": {
|
|
210
|
+
method: "PATCH",
|
|
211
|
+
path_template: "/api/v1/webhooks/{id}"
|
|
212
|
+
},
|
|
213
|
+
"helpdesk.webhook.delete.execute": {
|
|
214
|
+
method: "DELETE",
|
|
215
|
+
path_template: "/api/v1/webhooks/{id}"
|
|
216
|
+
}
|
|
217
|
+
};
|
|
218
|
+
|
|
219
|
+
// src/capability-confirmer.ts
|
|
220
|
+
function resolvePath(template, pathParams) {
|
|
221
|
+
return template.replace(/\{([^}/]+)\}/g, (_match, name) => {
|
|
222
|
+
const value = pathParams?.[name];
|
|
223
|
+
return value === void 0 ? `{${name}}` : encodeURIComponent(String(value));
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
var CapabilityConfirmer = class {
|
|
227
|
+
constructor(confirmations, defaults) {
|
|
228
|
+
this.confirmations = confirmations;
|
|
229
|
+
this.defaults = defaults;
|
|
230
|
+
}
|
|
231
|
+
confirmations;
|
|
232
|
+
defaults;
|
|
233
|
+
/**
|
|
234
|
+
* Return the request options to use for a confirmable write, minting the
|
|
235
|
+
* idempotency key and confirmation token first when auto-confirm is active.
|
|
236
|
+
*
|
|
237
|
+
* `body` is the pending request payload (`undefined` for `DELETE` routes).
|
|
238
|
+
* It is handed to the `previewSummary` callback by reference so the summary
|
|
239
|
+
* can describe the specific action, not just the route — it is the caller's
|
|
240
|
+
* own payload, so it is passed through unmodified and unredacted.
|
|
241
|
+
*/
|
|
242
|
+
async prepare(request, pathParams, options) {
|
|
243
|
+
const auto = options?.autoConfirm === false ? void 0 : options?.autoConfirm ?? this.defaults;
|
|
244
|
+
if (!auto) return options;
|
|
245
|
+
const idempotencyKey = resolveIdempotencyKey(options?.idempotencyKey);
|
|
246
|
+
const callerKeyIsUsable = idempotencyKey === options?.idempotencyKey;
|
|
247
|
+
if (callerKeyIsUsable && options?.capabilityConfirmation) return options;
|
|
248
|
+
const route = CAPABILITY_ROUTES[request.capabilityId];
|
|
249
|
+
const path = resolvePath(route.path_template, pathParams);
|
|
250
|
+
const previewSummary = auto.previewSummary({
|
|
251
|
+
...request,
|
|
252
|
+
method: route.method,
|
|
253
|
+
path,
|
|
254
|
+
...pathParams ? { pathParams } : {},
|
|
255
|
+
idempotencyKey
|
|
256
|
+
});
|
|
257
|
+
if (typeof previewSummary !== "string" || previewSummary.trim() === "") {
|
|
258
|
+
throw new Error(
|
|
259
|
+
`autoConfirm.previewSummary must return a non-empty summary for ${request.capabilityId}. The summary is the audit record of what your user approved \u2014 refusing to assert user_approved: true without one.`
|
|
260
|
+
);
|
|
261
|
+
}
|
|
262
|
+
const { data } = await this.confirmations.create({
|
|
263
|
+
capability_id: request.capabilityId,
|
|
264
|
+
...pathParams ? { path_params: pathParams } : {},
|
|
265
|
+
idempotency_key: idempotencyKey,
|
|
266
|
+
preview_summary: previewSummary,
|
|
267
|
+
user_approved: true
|
|
268
|
+
});
|
|
269
|
+
return {
|
|
270
|
+
...options,
|
|
271
|
+
idempotencyKey,
|
|
272
|
+
capabilityConfirmation: data.confirmation_token
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
};
|
|
276
|
+
|
|
277
|
+
// src/resources/bookings.ts
|
|
278
|
+
var BookingsManage = class {
|
|
279
|
+
constructor(client) {
|
|
280
|
+
this.client = client;
|
|
281
|
+
}
|
|
282
|
+
client;
|
|
283
|
+
/**
|
|
284
|
+
* Read what the holder of a manage token may see and do. Honour
|
|
285
|
+
* `can_cancel` / `can_reschedule` — they already apply the policy windows.
|
|
286
|
+
*/
|
|
287
|
+
async get(token) {
|
|
288
|
+
return this.client.get(`/api/v1/bookings/manage/${encodeURIComponent(token)}`);
|
|
289
|
+
}
|
|
290
|
+
/** Cancel on the customer's behalf. Rejected outside the cancel window. */
|
|
291
|
+
async cancel(token, input, options) {
|
|
292
|
+
return this.client.postOnce(
|
|
293
|
+
`/api/v1/bookings/manage/${encodeURIComponent(token)}/cancel`,
|
|
294
|
+
input ?? {},
|
|
295
|
+
options
|
|
296
|
+
);
|
|
297
|
+
}
|
|
298
|
+
/**
|
|
299
|
+
* Move the booking on the customer's behalf. Rejected outside the reschedule
|
|
300
|
+
* window. Returns a NEW booking id and a new manage token — the old token
|
|
301
|
+
* stops working, so relay the new one into whatever link you send next.
|
|
302
|
+
*/
|
|
303
|
+
async reschedule(token, input, options) {
|
|
304
|
+
return this.client.postOnce(
|
|
305
|
+
`/api/v1/bookings/manage/${encodeURIComponent(token)}/reschedule`,
|
|
306
|
+
input,
|
|
307
|
+
options
|
|
308
|
+
);
|
|
309
|
+
}
|
|
310
|
+
};
|
|
311
|
+
var Bookings = class {
|
|
312
|
+
constructor(client) {
|
|
313
|
+
this.client = client;
|
|
314
|
+
this.manage = new BookingsManage(client);
|
|
315
|
+
}
|
|
316
|
+
client;
|
|
317
|
+
/** Customer-side actions addressed by manage token. */
|
|
318
|
+
manage;
|
|
319
|
+
/** List the bookable service catalogue. Active-only unless asked otherwise. */
|
|
320
|
+
async listServices(options) {
|
|
321
|
+
const params = {};
|
|
322
|
+
if (options?.include_inactive !== void 0) {
|
|
323
|
+
params.include_inactive = String(options.include_inactive);
|
|
324
|
+
}
|
|
325
|
+
return this.client.get("/api/v1/bookings/services", params);
|
|
326
|
+
}
|
|
327
|
+
/** List the bookable resources — staff, rooms, and equipment. */
|
|
328
|
+
async listResources() {
|
|
329
|
+
return this.client.get("/api/v1/bookings/resources");
|
|
330
|
+
}
|
|
331
|
+
/**
|
|
332
|
+
* List free slots for a service over a window. Slots reflect opening hours,
|
|
333
|
+
* time off, buffers, and existing bookings at the moment of the call — they
|
|
334
|
+
* are not held, so a slot can be taken before you book it.
|
|
335
|
+
*/
|
|
336
|
+
async availability(options) {
|
|
337
|
+
const params = {
|
|
338
|
+
service_id: options.service_id,
|
|
339
|
+
from_ts: String(options.from_ts),
|
|
340
|
+
to_ts: String(options.to_ts)
|
|
341
|
+
};
|
|
342
|
+
if (options.resource_id) params.resource_id = options.resource_id;
|
|
343
|
+
return this.client.get("/api/v1/bookings/availability", params);
|
|
344
|
+
}
|
|
345
|
+
/**
|
|
346
|
+
* The dates a service can be booked on — the half `availability` cannot
|
|
347
|
+
* answer. Availability returns free slots and nothing else, so a closed day,
|
|
348
|
+
* an evening past closing and a fully booked day are all the same empty
|
|
349
|
+
* array. A date absent from this list is closed; on a listed date, compare
|
|
350
|
+
* `last_start_ts` against the clock to tell "too late today" from "full".
|
|
351
|
+
*/
|
|
352
|
+
async schedule(options) {
|
|
353
|
+
const params = {
|
|
354
|
+
service_id: options.service_id,
|
|
355
|
+
from_ts: String(options.from_ts),
|
|
356
|
+
to_ts: String(options.to_ts)
|
|
357
|
+
};
|
|
358
|
+
if (options.resource_id) params.resource_id = options.resource_id;
|
|
359
|
+
return this.client.get("/api/v1/bookings/schedule", params);
|
|
360
|
+
}
|
|
361
|
+
/**
|
|
362
|
+
* List bookings with cursor-based pagination and optional filters.
|
|
363
|
+
*
|
|
364
|
+
* Check `pagination.truncated`: when true the read window was clipped and
|
|
365
|
+
* matching bookings exist that no cursor reaches — narrow `from_ts`/`to_ts`.
|
|
366
|
+
*/
|
|
367
|
+
async list(options) {
|
|
368
|
+
const params = {};
|
|
369
|
+
if (options?.limit !== void 0) params.limit = String(options.limit);
|
|
370
|
+
if (options?.cursor) params.cursor = options.cursor;
|
|
371
|
+
if (options?.status) params.status = options.status;
|
|
372
|
+
if (options?.resource_id) params.resource_id = options.resource_id;
|
|
373
|
+
if (options?.from_ts !== void 0) params.from_ts = String(options.from_ts);
|
|
374
|
+
if (options?.to_ts !== void 0) params.to_ts = String(options.to_ts);
|
|
375
|
+
return this.client.get("/api/v1/bookings", params);
|
|
376
|
+
}
|
|
377
|
+
/**
|
|
378
|
+
* Book a party — every item succeeds or none do (max 50).
|
|
379
|
+
*
|
|
380
|
+
* Each created booking comes back with a `manage_token` exactly once; only
|
|
381
|
+
* its hash is stored, so persist it if you need the customer's manage link.
|
|
382
|
+
*
|
|
383
|
+
* Automatically idempotent: the SDK mints an `Idempotency-Key` so its own
|
|
384
|
+
* 5xx retries replay rather than book the slot twice. Supply
|
|
385
|
+
* `options.idempotencyKey` to deduplicate across your OWN retries too — the
|
|
386
|
+
* server keys on it for 24 hours, so re-sending the same key after a network
|
|
387
|
+
* timeout returns the original bookings instead of a second set.
|
|
388
|
+
*/
|
|
389
|
+
async create(input, options) {
|
|
390
|
+
return this.client.postOnce("/api/v1/bookings", input, options);
|
|
391
|
+
}
|
|
392
|
+
/** Get a booking by ID. */
|
|
393
|
+
async get(id) {
|
|
394
|
+
return this.client.get(`/api/v1/bookings/${encodeURIComponent(id)}`);
|
|
395
|
+
}
|
|
396
|
+
/**
|
|
397
|
+
* Annotate a booking. At least one of `notes` (customer-visible) or
|
|
398
|
+
* `internal_notes` (staff-only) is required; `""` clears a field.
|
|
399
|
+
*/
|
|
400
|
+
async update(id, input, options) {
|
|
401
|
+
return this.client.patch(`/api/v1/bookings/${encodeURIComponent(id)}`, input, options);
|
|
402
|
+
}
|
|
403
|
+
/** Cancel as the business — the cancel window is bypassed. */
|
|
404
|
+
async cancel(id, input, options) {
|
|
405
|
+
return this.client.postOnce(
|
|
406
|
+
`/api/v1/bookings/${encodeURIComponent(id)}/cancel`,
|
|
407
|
+
input ?? {},
|
|
408
|
+
options
|
|
409
|
+
);
|
|
410
|
+
}
|
|
411
|
+
/**
|
|
412
|
+
* Move a booking as the business — the reschedule window is bypassed.
|
|
413
|
+
* Returns a NEW booking id and a new manage token; the old booking is
|
|
414
|
+
* cancelled and its token stops working.
|
|
415
|
+
*/
|
|
416
|
+
async reschedule(id, input, options) {
|
|
417
|
+
return this.client.postOnce(
|
|
418
|
+
`/api/v1/bookings/${encodeURIComponent(id)}/reschedule`,
|
|
419
|
+
input,
|
|
420
|
+
options
|
|
421
|
+
);
|
|
422
|
+
}
|
|
423
|
+
/** Mark a booking as a no-show. */
|
|
424
|
+
async markNoShow(id, options) {
|
|
425
|
+
return this.client.postOnce(
|
|
426
|
+
`/api/v1/bookings/${encodeURIComponent(id)}/no-show`,
|
|
427
|
+
void 0,
|
|
428
|
+
options
|
|
429
|
+
);
|
|
430
|
+
}
|
|
431
|
+
};
|
|
432
|
+
|
|
236
433
|
// src/resources/capability-confirmations.ts
|
|
237
434
|
var CapabilityConfirmations = class {
|
|
238
435
|
constructor(client) {
|
|
@@ -250,6 +447,13 @@ var CapabilityConfirmations = class {
|
|
|
250
447
|
* Setting `user_approved: true` asserts that a human on your side approved
|
|
251
448
|
* this specific action. `preview_summary` is what they approved, and is
|
|
252
449
|
* retained for audit — write it for a human reader, not a log parser.
|
|
450
|
+
*
|
|
451
|
+
* Deliberately unkeyed, unlike the writes it authorizes. Minting is not the
|
|
452
|
+
* state change the guarantee exists to protect: the write itself is already
|
|
453
|
+
* bound to `idempotency_key`, so a retry that mints a second token cannot
|
|
454
|
+
* produce a second write. Keying this call would instead park a credential
|
|
455
|
+
* designed to expire in 15 minutes inside a replay cache that answers for 24
|
|
456
|
+
* hours — a worse trade than the duplicate token it would avoid.
|
|
253
457
|
*/
|
|
254
458
|
async create(input) {
|
|
255
459
|
return this.client.post("/api/v1/capability-confirmations", input);
|
|
@@ -274,6 +478,13 @@ var ChannelConnectLinks = class {
|
|
|
274
478
|
*
|
|
275
479
|
* Requires the `channel.connect.manage` scope; OAuth callers additionally
|
|
276
480
|
* need the workspace `admin` role.
|
|
481
|
+
*
|
|
482
|
+
* Automatically idempotent: an unkeyed retry mints a SECOND live single-use
|
|
483
|
+
* link for the same person, and only one of the two can ever be consumed —
|
|
484
|
+
* the other stays outstanding until it is revoked or expires. The key the
|
|
485
|
+
* confirmer chose is the key that goes out — a capability confirmation is
|
|
486
|
+
* bound to its idempotency key, so minting a fresh one here would invalidate
|
|
487
|
+
* the confirmation.
|
|
277
488
|
*/
|
|
278
489
|
async create(input, options) {
|
|
279
490
|
const resolved = await this.confirmer.prepare(
|
|
@@ -281,7 +492,7 @@ var ChannelConnectLinks = class {
|
|
|
281
492
|
void 0,
|
|
282
493
|
options
|
|
283
494
|
);
|
|
284
|
-
return this.client.
|
|
495
|
+
return this.client.postOnce("/api/v1/channels/connect-links", input, resolved);
|
|
285
496
|
}
|
|
286
497
|
/**
|
|
287
498
|
* List the workspace's connect links (tokens are never returned), newest
|
|
@@ -376,9 +587,17 @@ var Contacts = class {
|
|
|
376
587
|
if (options?.search) params.search = options.search;
|
|
377
588
|
return this.client.get("/api/v1/contacts", params);
|
|
378
589
|
}
|
|
379
|
-
/**
|
|
380
|
-
|
|
381
|
-
|
|
590
|
+
/**
|
|
591
|
+
* Create a new contact. Email must be unique in the workspace.
|
|
592
|
+
*
|
|
593
|
+
* Automatically idempotent: the SDK mints an `Idempotency-Key` so its own
|
|
594
|
+
* 5xx retries replay rather than run the create a second time. Uniqueness
|
|
595
|
+
* alone would not save you here — it turns the retry of a committed create
|
|
596
|
+
* into a spurious conflict, which reads as "the contact was not created".
|
|
597
|
+
* Supply `options.idempotencyKey` to deduplicate across your OWN retries too.
|
|
598
|
+
*/
|
|
599
|
+
async create(input, options) {
|
|
600
|
+
return this.client.postOnce("/api/v1/contacts", input, options);
|
|
382
601
|
}
|
|
383
602
|
/** Get a contact by ID. */
|
|
384
603
|
async get(id) {
|
|
@@ -399,13 +618,26 @@ var Contacts = class {
|
|
|
399
618
|
if (options?.cursor) params.cursor = options.cursor;
|
|
400
619
|
return this.client.get(`/api/v1/contacts/${encodeURIComponent(id)}/activities`, params);
|
|
401
620
|
}
|
|
402
|
-
/**
|
|
403
|
-
|
|
404
|
-
|
|
621
|
+
/**
|
|
622
|
+
* Add a note to a contact's timeline.
|
|
623
|
+
*
|
|
624
|
+
* Automatically idempotent: nothing about a note is unique, so an unkeyed
|
|
625
|
+
* retry appends the same text to the timeline twice. Supply
|
|
626
|
+
* `options.idempotencyKey` to deduplicate across your OWN retries too.
|
|
627
|
+
*/
|
|
628
|
+
async addNote(id, input, options) {
|
|
629
|
+
return this.client.postOnce(`/api/v1/contacts/${encodeURIComponent(id)}/notes`, input, options);
|
|
405
630
|
}
|
|
406
|
-
/**
|
|
407
|
-
|
|
408
|
-
|
|
631
|
+
/**
|
|
632
|
+
* Bulk import contacts (max 500). Duplicates are skipped.
|
|
633
|
+
*
|
|
634
|
+
* Automatically idempotent: the import is processed in chunks, so a retry
|
|
635
|
+
* after a partial failure re-walks the whole batch and reports `added` /
|
|
636
|
+
* `skipped` counts for a run that was not the first. Supply
|
|
637
|
+
* `options.idempotencyKey` to deduplicate across your OWN retries too.
|
|
638
|
+
*/
|
|
639
|
+
async import(contacts, options) {
|
|
640
|
+
return this.client.postOnce("/api/v1/contacts/import", { contacts }, options);
|
|
409
641
|
}
|
|
410
642
|
};
|
|
411
643
|
|
|
@@ -424,9 +656,15 @@ var Deals = class {
|
|
|
424
656
|
if (options?.search) params.search = options.search;
|
|
425
657
|
return this.client.get("/api/v1/deals", params);
|
|
426
658
|
}
|
|
427
|
-
/**
|
|
428
|
-
|
|
429
|
-
|
|
659
|
+
/**
|
|
660
|
+
* Create a new deal.
|
|
661
|
+
*
|
|
662
|
+
* Automatically idempotent: nothing about a deal is unique, so an unkeyed
|
|
663
|
+
* retry puts a second identical deal in the pipeline. Supply
|
|
664
|
+
* `options.idempotencyKey` to deduplicate across your OWN retries too.
|
|
665
|
+
*/
|
|
666
|
+
async create(input, options) {
|
|
667
|
+
return this.client.postOnce("/api/v1/deals", input, options);
|
|
430
668
|
}
|
|
431
669
|
/** Get a deal by ID. */
|
|
432
670
|
async get(id) {
|
|
@@ -470,9 +708,18 @@ var Emails = class {
|
|
|
470
708
|
/**
|
|
471
709
|
* Send a transactional email using a template (HTTP 202). The returned `id`
|
|
472
710
|
* is an email send id — poll `emails.get(id)` with it to track delivery.
|
|
711
|
+
*
|
|
712
|
+
* Automatically idempotent: the SDK mints an `Idempotency-Key` so its own
|
|
713
|
+
* 5xx retries replay rather than queue a second copy into someone's inbox —
|
|
714
|
+
* a send that already committed cannot be un-sent. Supply
|
|
715
|
+
* `options.idempotencyKey` to deduplicate across your OWN retries too.
|
|
716
|
+
*
|
|
717
|
+
* `input.idempotency_key` is the older, body-level form of the same control
|
|
718
|
+
* and still takes precedence server-side, so setting it keeps working
|
|
719
|
+
* unchanged.
|
|
473
720
|
*/
|
|
474
|
-
async send(input) {
|
|
475
|
-
return this.client.
|
|
721
|
+
async send(input, options) {
|
|
722
|
+
return this.client.postOnce("/api/v1/emails", input, options);
|
|
476
723
|
}
|
|
477
724
|
/** Get the delivery status of a sent email. */
|
|
478
725
|
async get(id) {
|
|
@@ -481,9 +728,14 @@ var Emails = class {
|
|
|
481
728
|
/**
|
|
482
729
|
* Send the same template to multiple recipients (max 100, HTTP 202). Each
|
|
483
730
|
* queued recipient gets its own send id in `results` for `emails.get(id)`.
|
|
731
|
+
*
|
|
732
|
+
* Automatically idempotent — and this is the call where it matters most: an
|
|
733
|
+
* unkeyed retry of a batch that already committed sends up to 100 duplicate
|
|
734
|
+
* emails. Supply `options.idempotencyKey` to deduplicate across your OWN
|
|
735
|
+
* retries too.
|
|
484
736
|
*/
|
|
485
|
-
async batch(input) {
|
|
486
|
-
return this.client.
|
|
737
|
+
async batch(input, options) {
|
|
738
|
+
return this.client.postOnce("/api/v1/emails/batch", input, options);
|
|
487
739
|
}
|
|
488
740
|
};
|
|
489
741
|
|
|
@@ -493,9 +745,17 @@ var Gdpr = class {
|
|
|
493
745
|
this.client = client;
|
|
494
746
|
}
|
|
495
747
|
client;
|
|
496
|
-
/**
|
|
497
|
-
|
|
498
|
-
|
|
748
|
+
/**
|
|
749
|
+
* Request a workspace data export. Runs asynchronously.
|
|
750
|
+
*
|
|
751
|
+
* Automatically idempotent: the request is recorded and the export is
|
|
752
|
+
* scheduled in one step with no de-duplication of its own, so an unkeyed
|
|
753
|
+
* retry files a second subject-access request and runs a second full export
|
|
754
|
+
* of the workspace. Supply `options.idempotencyKey` to deduplicate across
|
|
755
|
+
* your OWN retries too.
|
|
756
|
+
*/
|
|
757
|
+
async requestExport(options) {
|
|
758
|
+
return this.client.postOnce("/api/v1/gdpr/export", void 0, options);
|
|
499
759
|
}
|
|
500
760
|
/** List all workspace export requests. */
|
|
501
761
|
async listExports() {
|
|
@@ -505,7 +765,13 @@ var Gdpr = class {
|
|
|
505
765
|
async getExport(id) {
|
|
506
766
|
return this.client.get(`/api/v1/gdpr/exports/${encodeURIComponent(id)}`);
|
|
507
767
|
}
|
|
508
|
-
/**
|
|
768
|
+
/**
|
|
769
|
+
* Record a GDPR consent decision for a contact by email.
|
|
770
|
+
*
|
|
771
|
+
* Deliberately unkeyed: a decision is stored once per
|
|
772
|
+
* (workspace, email, consent type) and overwritten in place, so re-sending
|
|
773
|
+
* the same body reaches the same state and returns the same record id.
|
|
774
|
+
*/
|
|
509
775
|
async recordConsent(input) {
|
|
510
776
|
return this.client.post("/api/v1/gdpr/consent", input);
|
|
511
777
|
}
|
|
@@ -513,7 +779,15 @@ var Gdpr = class {
|
|
|
513
779
|
async getConsent(email) {
|
|
514
780
|
return this.client.get(`/api/v1/gdpr/consent/${encodeURIComponent(email)}`);
|
|
515
781
|
}
|
|
516
|
-
/**
|
|
782
|
+
/**
|
|
783
|
+
* Record cookie consent from an external site (legacy endpoint).
|
|
784
|
+
*
|
|
785
|
+
* Deliberately unkeyed: this legacy route predates the versioned API and
|
|
786
|
+
* does not run the `Idempotency-Key` machinery, so a key here would be a
|
|
787
|
+
* header that changes nothing while implying a guarantee the endpoint cannot
|
|
788
|
+
* make. Treat a failed call as "unknown" and re-send only if a missing
|
|
789
|
+
* consent log matters more to you than a duplicate one.
|
|
790
|
+
*/
|
|
517
791
|
async cookieConsent(input) {
|
|
518
792
|
return this.client.post("/api/cookie-consent", input);
|
|
519
793
|
}
|
|
@@ -577,8 +851,16 @@ var HelpdeskReplies = class {
|
|
|
577
851
|
/**
|
|
578
852
|
* Send an operator reply or internal note. Returns HTTP 201.
|
|
579
853
|
*
|
|
580
|
-
*
|
|
581
|
-
*
|
|
854
|
+
* Automatically idempotent: a reply is a message to a real person, and an
|
|
855
|
+
* unkeyed retry sends it to them twice. The key the confirmer chose is the
|
|
856
|
+
* key that goes out — a capability confirmation is bound to its idempotency
|
|
857
|
+
* key, so minting a fresh one here would invalidate the confirmation.
|
|
858
|
+
*
|
|
859
|
+
* Pass `options.idempotencyKey` to deduplicate across your OWN retries too.
|
|
860
|
+
* It is REQUIRED for capability-scoped tokens, which need it paired with a
|
|
861
|
+
* `capabilityConfirmation` — a generated key satisfies the pairing's key
|
|
862
|
+
* half only; the confirmation is still yours to supply (or to let
|
|
863
|
+
* `autoConfirm` mint).
|
|
582
864
|
*/
|
|
583
865
|
async create(input, options) {
|
|
584
866
|
const resolved = await this.confirmer.prepare(
|
|
@@ -586,7 +868,7 @@ var HelpdeskReplies = class {
|
|
|
586
868
|
void 0,
|
|
587
869
|
options
|
|
588
870
|
);
|
|
589
|
-
return this.client.
|
|
871
|
+
return this.client.postOnce("/api/v1/helpdesk/replies", input, resolved);
|
|
590
872
|
}
|
|
591
873
|
};
|
|
592
874
|
var Helpdesk = class {
|
|
@@ -614,9 +896,15 @@ var Posts = class {
|
|
|
614
896
|
if (options?.type) params.type = options.type;
|
|
615
897
|
return this.client.get("/api/v1/posts", params);
|
|
616
898
|
}
|
|
617
|
-
/**
|
|
618
|
-
|
|
619
|
-
|
|
899
|
+
/**
|
|
900
|
+
* Create a new post with content and target channels.
|
|
901
|
+
*
|
|
902
|
+
* Automatically idempotent: the SDK mints an `Idempotency-Key` so its own
|
|
903
|
+
* 5xx retries replay rather than draft the post twice. Supply
|
|
904
|
+
* `options.idempotencyKey` to deduplicate across your OWN retries too.
|
|
905
|
+
*/
|
|
906
|
+
async create(input, options) {
|
|
907
|
+
return this.client.postOnce("/api/v1/posts", input, options);
|
|
620
908
|
}
|
|
621
909
|
/** Get a post by ID, including its per-channel variants. */
|
|
622
910
|
async get(id) {
|
|
@@ -630,11 +918,26 @@ var Posts = class {
|
|
|
630
918
|
async remove(id) {
|
|
631
919
|
return this.client.delete(`/api/v1/posts/${encodeURIComponent(id)}`);
|
|
632
920
|
}
|
|
633
|
-
/**
|
|
921
|
+
/**
|
|
922
|
+
* Schedule a post for future publication.
|
|
923
|
+
*
|
|
924
|
+
* Deliberately unkeyed: re-sending the same `scheduled_at` for an
|
|
925
|
+
* already-scheduled post returns the original `workflow_id` rather than
|
|
926
|
+
* starting a second one, so a retried schedule cannot double-publish. A
|
|
927
|
+
* *different* time is rejected — unschedule first.
|
|
928
|
+
*/
|
|
634
929
|
async schedule(id, input) {
|
|
635
930
|
return this.client.post(`/api/v1/posts/${encodeURIComponent(id)}/schedule`, input);
|
|
636
931
|
}
|
|
637
|
-
/**
|
|
932
|
+
/**
|
|
933
|
+
* Publish a post immediately to all target channels.
|
|
934
|
+
*
|
|
935
|
+
* Deliberately unkeyed: publishing moves the post out of the set of statuses
|
|
936
|
+
* that may be published, so the retry of a publish that already committed is
|
|
937
|
+
* refused rather than posting a second time. It is refused with a 400 though
|
|
938
|
+
* — treat an error here as "check the post's status", not as "nothing
|
|
939
|
+
* happened".
|
|
940
|
+
*/
|
|
638
941
|
async publish(id) {
|
|
639
942
|
return this.client.post(`/api/v1/posts/${encodeURIComponent(id)}/publish`);
|
|
640
943
|
}
|
|
@@ -655,10 +958,15 @@ var Scan = class {
|
|
|
655
958
|
* Queue a scan. Provide exactly one of `url`, `orgnr`, or `name`.
|
|
656
959
|
* Runs asynchronously — poll with `get()` or use `waitForResult()`.
|
|
657
960
|
*
|
|
961
|
+
* Automatically idempotent: a scan job is queued the moment it is created,
|
|
962
|
+
* so an unkeyed retry starts a second crawl of the same site and returns an
|
|
963
|
+
* id for a job that duplicates one already running. Supply
|
|
964
|
+
* `options.idempotencyKey` to deduplicate across your OWN retries too.
|
|
965
|
+
*
|
|
658
966
|
* @throws Error before any request when zero or several selectors are set —
|
|
659
967
|
* the server would reject the body anyway; failing locally is clearer.
|
|
660
968
|
*/
|
|
661
|
-
async create(input) {
|
|
969
|
+
async create(input, options) {
|
|
662
970
|
const entries = ["url", "orgnr", "name"].filter(
|
|
663
971
|
(key2) => input[key2] !== void 0 && input[key2] !== ""
|
|
664
972
|
);
|
|
@@ -666,7 +974,7 @@ var Scan = class {
|
|
|
666
974
|
throw new Error("scan.create requires exactly one of url, orgnr, or name");
|
|
667
975
|
}
|
|
668
976
|
const key = entries[0];
|
|
669
|
-
return this.client.
|
|
977
|
+
return this.client.postOnce("/api/v1/scan", { [key]: input[key] }, options);
|
|
670
978
|
}
|
|
671
979
|
/** Get a scan job's status and, once done, its findings payload. */
|
|
672
980
|
async get(id) {
|
|
@@ -724,6 +1032,13 @@ var Webhooks = class {
|
|
|
724
1032
|
* `secret` is typed optional because an idempotent replay (retrying with the
|
|
725
1033
|
* same `Idempotency-Key`, `X-Idempotent-Replayed: true`) returns the existing
|
|
726
1034
|
* endpoint WITHOUT the secret — handle that case (rotate if you lost it).
|
|
1035
|
+
*
|
|
1036
|
+
* Automatically idempotent: a duplicate endpoint is not a stray row, it is a
|
|
1037
|
+
* second copy of every future delivery to the same URL, forever. The key the
|
|
1038
|
+
* confirmer chose is the key that goes out — a capability confirmation is
|
|
1039
|
+
* bound to its idempotency key, so minting a fresh one here would invalidate
|
|
1040
|
+
* the confirmation. That the SDK now always sends a key is also what makes
|
|
1041
|
+
* the replay-without-secret case above reachable on a plain 5xx retry.
|
|
727
1042
|
*/
|
|
728
1043
|
async create(input, options) {
|
|
729
1044
|
const resolved = await this.confirmer.prepare(
|
|
@@ -731,7 +1046,7 @@ var Webhooks = class {
|
|
|
731
1046
|
void 0,
|
|
732
1047
|
options
|
|
733
1048
|
);
|
|
734
|
-
return this.client.
|
|
1049
|
+
return this.client.postOnce("/api/v1/webhooks", input, resolved);
|
|
735
1050
|
}
|
|
736
1051
|
/** Get a webhook endpoint by ID. */
|
|
737
1052
|
async get(id) {
|
|
@@ -766,7 +1081,14 @@ var Webhooks = class {
|
|
|
766
1081
|
if (options?.limit !== void 0) params.limit = String(options.limit);
|
|
767
1082
|
return this.client.get(`/api/v1/webhooks/${encodeURIComponent(id)}/deliveries`, params);
|
|
768
1083
|
}
|
|
769
|
-
/**
|
|
1084
|
+
/**
|
|
1085
|
+
* Queue a signed `test.ping` delivery to the endpoint. Returns HTTP 202.
|
|
1086
|
+
*
|
|
1087
|
+
* Deliberately unkeyed: a duplicate ping is the one duplicate that costs
|
|
1088
|
+
* nothing. Real deliveries are retried too, so any endpoint worth pointing at
|
|
1089
|
+
* already tolerates receiving the same event twice — that is what this call
|
|
1090
|
+
* exists to prove.
|
|
1091
|
+
*/
|
|
770
1092
|
async test(id) {
|
|
771
1093
|
return this.client.post(`/api/v1/webhooks/${encodeURIComponent(id)}/test`);
|
|
772
1094
|
}
|
|
@@ -856,6 +1178,7 @@ async function verifyWebhookSignature(input) {
|
|
|
856
1178
|
|
|
857
1179
|
// src/index.ts
|
|
858
1180
|
var Medal = class {
|
|
1181
|
+
bookings;
|
|
859
1182
|
capabilityConfirmations;
|
|
860
1183
|
channels;
|
|
861
1184
|
emails;
|
|
@@ -885,6 +1208,7 @@ var Medal = class {
|
|
|
885
1208
|
this.capabilityConfirmations,
|
|
886
1209
|
options?.autoConfirmCapabilities
|
|
887
1210
|
);
|
|
1211
|
+
this.bookings = new Bookings(client);
|
|
888
1212
|
this.channels = new Channels(client, confirmer);
|
|
889
1213
|
this.emails = new Emails(client);
|
|
890
1214
|
this.contacts = new Contacts(client);
|
|
@@ -903,6 +1227,7 @@ function createMedalClient(apiKey, options) {
|
|
|
903
1227
|
var src_default = Medal;
|
|
904
1228
|
export {
|
|
905
1229
|
BaseClient,
|
|
1230
|
+
Bookings,
|
|
906
1231
|
CAPABILITY_IDS,
|
|
907
1232
|
CAPABILITY_ROUTES,
|
|
908
1233
|
CapabilityConfirmations,
|