@medalsocial/sdk 1.5.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 +213 -1
- package/dist/openapi/medal-social.openapi.json +3739 -1411
- package/dist/pilot/index.d.mts +2 -2
- package/dist/pilot/index.d.ts +2 -2
- package/dist/src/index.d.mts +1376 -258
- package/dist/src/index.d.ts +1376 -258
- package/dist/src/index.js +667 -57
- package/dist/src/index.js.map +1 -1
- package/dist/src/index.mjs +661 -57
- package/dist/src/index.mjs.map +1 -1
- package/dist/src/openapi.generated.d.mts +1287 -113
- package/dist/src/openapi.generated.d.ts +1287 -113
- package/dist/src/openapi.generated.js.map +1 -1
- package/openapi/medal-social.openapi.yaml +1394 -27
- package/package.json +18 -17
- package/skills/client/SKILL.md +2 -2
- package/skills/resources/SKILL.md +105 -3
package/dist/src/index.mjs
CHANGED
|
@@ -13,6 +13,21 @@ var MedalApiError = class extends Error {
|
|
|
13
13
|
};
|
|
14
14
|
|
|
15
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
|
+
}
|
|
16
31
|
var BaseClient = class {
|
|
17
32
|
/** Resolved client configuration. */
|
|
18
33
|
config;
|
|
@@ -32,6 +47,29 @@ var BaseClient = class {
|
|
|
32
47
|
body: body !== void 0 ? JSON.stringify(body) : void 0
|
|
33
48
|
});
|
|
34
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
|
+
}
|
|
35
73
|
/** Execute an authenticated PATCH request with a JSON body. */
|
|
36
74
|
async patch(path, body, options) {
|
|
37
75
|
return this.request(this.buildUrl(path), {
|
|
@@ -83,12 +121,22 @@ var BaseClient = class {
|
|
|
83
121
|
const controller = new AbortController();
|
|
84
122
|
const timeout = setTimeout(() => controller.abort(), this.config.timeout);
|
|
85
123
|
let res;
|
|
124
|
+
let text = "";
|
|
125
|
+
let retrying = false;
|
|
86
126
|
try {
|
|
87
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
|
+
}
|
|
88
136
|
} finally {
|
|
89
137
|
clearTimeout(timeout);
|
|
90
138
|
}
|
|
91
|
-
if (
|
|
139
|
+
if (retrying) {
|
|
92
140
|
const retryAfter = res.headers.get("retry-after");
|
|
93
141
|
let delayMs = 0;
|
|
94
142
|
if (retryAfter) {
|
|
@@ -101,7 +149,6 @@ var BaseClient = class {
|
|
|
101
149
|
await new Promise((r) => setTimeout(r, delayMs));
|
|
102
150
|
continue;
|
|
103
151
|
}
|
|
104
|
-
const text = await res.text();
|
|
105
152
|
let parsed;
|
|
106
153
|
try {
|
|
107
154
|
parsed = text ? JSON.parse(text) : void 0;
|
|
@@ -123,12 +170,304 @@ var BaseClient = class {
|
|
|
123
170
|
}
|
|
124
171
|
};
|
|
125
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
|
+
|
|
433
|
+
// src/resources/capability-confirmations.ts
|
|
434
|
+
var CapabilityConfirmations = class {
|
|
435
|
+
constructor(client) {
|
|
436
|
+
this.client = client;
|
|
437
|
+
}
|
|
438
|
+
client;
|
|
439
|
+
/**
|
|
440
|
+
* Issue a confirmation token for one pending write.
|
|
441
|
+
*
|
|
442
|
+
* The token is bound to the workspace, the auth subject, the capability's
|
|
443
|
+
* method + path, its required scopes, and `idempotency_key` — so it is
|
|
444
|
+
* usable exactly once, for exactly the write it describes, and expires
|
|
445
|
+
* within 15 minutes.
|
|
446
|
+
*
|
|
447
|
+
* Setting `user_approved: true` asserts that a human on your side approved
|
|
448
|
+
* this specific action. `preview_summary` is what they approved, and is
|
|
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.
|
|
457
|
+
*/
|
|
458
|
+
async create(input) {
|
|
459
|
+
return this.client.post("/api/v1/capability-confirmations", input);
|
|
460
|
+
}
|
|
461
|
+
};
|
|
462
|
+
|
|
126
463
|
// src/resources/channels.ts
|
|
127
464
|
var ChannelConnectLinks = class {
|
|
128
|
-
constructor(client) {
|
|
465
|
+
constructor(client, confirmer) {
|
|
129
466
|
this.client = client;
|
|
467
|
+
this.confirmer = confirmer;
|
|
130
468
|
}
|
|
131
469
|
client;
|
|
470
|
+
confirmer;
|
|
132
471
|
/**
|
|
133
472
|
* Mint a single-use hosted connect link. Returns HTTP 201.
|
|
134
473
|
*
|
|
@@ -139,30 +478,73 @@ var ChannelConnectLinks = class {
|
|
|
139
478
|
*
|
|
140
479
|
* Requires the `channel.connect.manage` scope; OAuth callers additionally
|
|
141
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.
|
|
142
488
|
*/
|
|
143
489
|
async create(input, options) {
|
|
144
|
-
|
|
490
|
+
const resolved = await this.confirmer.prepare(
|
|
491
|
+
{ capabilityId: "channel.connect_link.create.execute", body: input },
|
|
492
|
+
void 0,
|
|
493
|
+
options
|
|
494
|
+
);
|
|
495
|
+
return this.client.postOnce("/api/v1/channels/connect-links", input, resolved);
|
|
145
496
|
}
|
|
146
|
-
/**
|
|
497
|
+
/**
|
|
498
|
+
* List the workspace's connect links (tokens are never returned), newest
|
|
499
|
+
* first, with cursor-based pagination.
|
|
500
|
+
*
|
|
501
|
+
* `limit` defaults to 50 server-side and is capped at 100. Follow
|
|
502
|
+
* `pagination.next_cursor` while `pagination.has_more` is true.
|
|
503
|
+
*
|
|
504
|
+
* The `channel_type` / `status` filters are applied **within** each page,
|
|
505
|
+
* so a page may hold fewer than `limit` items while `has_more` is still
|
|
506
|
+
* true — drive the loop off `has_more`, never off the item count.
|
|
507
|
+
*/
|
|
147
508
|
async list(options) {
|
|
148
509
|
const params = {};
|
|
510
|
+
if (options?.limit !== void 0) params.limit = String(options.limit);
|
|
511
|
+
if (options?.cursor) params.cursor = options.cursor;
|
|
149
512
|
if (options?.channel_type) params.channel_type = options.channel_type;
|
|
150
513
|
if (options?.status) params.status = options.status;
|
|
151
514
|
return this.client.get("/api/v1/channels/connect-links", params);
|
|
152
515
|
}
|
|
153
516
|
/** Revoke a pending connect link so it can no longer be consumed. */
|
|
154
517
|
async revoke(id, options) {
|
|
155
|
-
|
|
518
|
+
const resolved = await this.confirmer.prepare(
|
|
519
|
+
{ capabilityId: "channel.connect_link.revoke.execute", body: void 0 },
|
|
520
|
+
{ id },
|
|
521
|
+
options
|
|
522
|
+
);
|
|
523
|
+
return this.client.delete(`/api/v1/channels/connect-links/${encodeURIComponent(id)}`, resolved);
|
|
156
524
|
}
|
|
157
525
|
};
|
|
158
526
|
var ChannelConnections = class {
|
|
159
|
-
constructor(client) {
|
|
527
|
+
constructor(client, confirmer) {
|
|
160
528
|
this.client = client;
|
|
529
|
+
this.confirmer = confirmer;
|
|
161
530
|
}
|
|
162
531
|
client;
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
532
|
+
confirmer;
|
|
533
|
+
/**
|
|
534
|
+
* List the workspace's channel connections (generic, channel-agnostic
|
|
535
|
+
* shape), newest first, with cursor-based pagination.
|
|
536
|
+
*
|
|
537
|
+
* `limit` defaults to 50 server-side and is capped at 100. Follow
|
|
538
|
+
* `pagination.next_cursor` while `pagination.has_more` is true. Rows that
|
|
539
|
+
* are not projectable as connections are dropped within the page, so a page
|
|
540
|
+
* may hold fewer than `limit` items while `has_more` is still true — drive
|
|
541
|
+
* the loop off `has_more`, never off the item count.
|
|
542
|
+
*/
|
|
543
|
+
async list(options) {
|
|
544
|
+
const params = {};
|
|
545
|
+
if (options?.limit !== void 0) params.limit = String(options.limit);
|
|
546
|
+
if (options?.cursor) params.cursor = options.cursor;
|
|
547
|
+
return this.client.get("/api/v1/channels/connections", params);
|
|
166
548
|
}
|
|
167
549
|
/**
|
|
168
550
|
* Disconnect a connected channel account (best-effort platform logout, then
|
|
@@ -170,15 +552,21 @@ var ChannelConnections = class {
|
|
|
170
552
|
* `reason: "api_disconnect"` if the account was previously connected.
|
|
171
553
|
*/
|
|
172
554
|
async disconnect(id, options) {
|
|
173
|
-
|
|
555
|
+
const resolved = await this.confirmer.prepare(
|
|
556
|
+
{ capabilityId: "channel.connection.disconnect.execute", body: void 0 },
|
|
557
|
+
{ id },
|
|
558
|
+
options
|
|
559
|
+
);
|
|
560
|
+
return this.client.delete(`/api/v1/channels/connections/${encodeURIComponent(id)}`, resolved);
|
|
174
561
|
}
|
|
175
562
|
};
|
|
176
563
|
var Channels = class {
|
|
177
564
|
connectLinks;
|
|
178
565
|
connections;
|
|
179
|
-
constructor(client) {
|
|
180
|
-
|
|
181
|
-
this.
|
|
566
|
+
constructor(client, confirmer) {
|
|
567
|
+
const resolved = confirmer ?? new CapabilityConfirmer(new CapabilityConfirmations(client));
|
|
568
|
+
this.connectLinks = new ChannelConnectLinks(client, resolved);
|
|
569
|
+
this.connections = new ChannelConnections(client, resolved);
|
|
182
570
|
}
|
|
183
571
|
};
|
|
184
572
|
|
|
@@ -199,9 +587,17 @@ var Contacts = class {
|
|
|
199
587
|
if (options?.search) params.search = options.search;
|
|
200
588
|
return this.client.get("/api/v1/contacts", params);
|
|
201
589
|
}
|
|
202
|
-
/**
|
|
203
|
-
|
|
204
|
-
|
|
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);
|
|
205
601
|
}
|
|
206
602
|
/** Get a contact by ID. */
|
|
207
603
|
async get(id) {
|
|
@@ -222,13 +618,26 @@ var Contacts = class {
|
|
|
222
618
|
if (options?.cursor) params.cursor = options.cursor;
|
|
223
619
|
return this.client.get(`/api/v1/contacts/${encodeURIComponent(id)}/activities`, params);
|
|
224
620
|
}
|
|
225
|
-
/**
|
|
226
|
-
|
|
227
|
-
|
|
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);
|
|
228
630
|
}
|
|
229
|
-
/**
|
|
230
|
-
|
|
231
|
-
|
|
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);
|
|
232
641
|
}
|
|
233
642
|
};
|
|
234
643
|
|
|
@@ -247,9 +656,15 @@ var Deals = class {
|
|
|
247
656
|
if (options?.search) params.search = options.search;
|
|
248
657
|
return this.client.get("/api/v1/deals", params);
|
|
249
658
|
}
|
|
250
|
-
/**
|
|
251
|
-
|
|
252
|
-
|
|
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);
|
|
253
668
|
}
|
|
254
669
|
/** Get a deal by ID. */
|
|
255
670
|
async get(id) {
|
|
@@ -293,9 +708,18 @@ var Emails = class {
|
|
|
293
708
|
/**
|
|
294
709
|
* Send a transactional email using a template (HTTP 202). The returned `id`
|
|
295
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.
|
|
296
720
|
*/
|
|
297
|
-
async send(input) {
|
|
298
|
-
return this.client.
|
|
721
|
+
async send(input, options) {
|
|
722
|
+
return this.client.postOnce("/api/v1/emails", input, options);
|
|
299
723
|
}
|
|
300
724
|
/** Get the delivery status of a sent email. */
|
|
301
725
|
async get(id) {
|
|
@@ -304,9 +728,14 @@ var Emails = class {
|
|
|
304
728
|
/**
|
|
305
729
|
* Send the same template to multiple recipients (max 100, HTTP 202). Each
|
|
306
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.
|
|
307
736
|
*/
|
|
308
|
-
async batch(input) {
|
|
309
|
-
return this.client.
|
|
737
|
+
async batch(input, options) {
|
|
738
|
+
return this.client.postOnce("/api/v1/emails/batch", input, options);
|
|
310
739
|
}
|
|
311
740
|
};
|
|
312
741
|
|
|
@@ -316,9 +745,17 @@ var Gdpr = class {
|
|
|
316
745
|
this.client = client;
|
|
317
746
|
}
|
|
318
747
|
client;
|
|
319
|
-
/**
|
|
320
|
-
|
|
321
|
-
|
|
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);
|
|
322
759
|
}
|
|
323
760
|
/** List all workspace export requests. */
|
|
324
761
|
async listExports() {
|
|
@@ -328,7 +765,13 @@ var Gdpr = class {
|
|
|
328
765
|
async getExport(id) {
|
|
329
766
|
return this.client.get(`/api/v1/gdpr/exports/${encodeURIComponent(id)}`);
|
|
330
767
|
}
|
|
331
|
-
/**
|
|
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
|
+
*/
|
|
332
775
|
async recordConsent(input) {
|
|
333
776
|
return this.client.post("/api/v1/gdpr/consent", input);
|
|
334
777
|
}
|
|
@@ -336,7 +779,15 @@ var Gdpr = class {
|
|
|
336
779
|
async getConsent(email) {
|
|
337
780
|
return this.client.get(`/api/v1/gdpr/consent/${encodeURIComponent(email)}`);
|
|
338
781
|
}
|
|
339
|
-
/**
|
|
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
|
+
*/
|
|
340
791
|
async cookieConsent(input) {
|
|
341
792
|
return this.client.post("/api/cookie-consent", input);
|
|
342
793
|
}
|
|
@@ -344,10 +795,12 @@ var Gdpr = class {
|
|
|
344
795
|
|
|
345
796
|
// src/resources/helpdesk.ts
|
|
346
797
|
var HelpdeskConversations = class {
|
|
347
|
-
constructor(client) {
|
|
798
|
+
constructor(client, confirmer) {
|
|
348
799
|
this.client = client;
|
|
800
|
+
this.confirmer = confirmer;
|
|
349
801
|
}
|
|
350
802
|
client;
|
|
803
|
+
confirmer;
|
|
351
804
|
/** List/search conversations with cursor-based pagination and optional filters. */
|
|
352
805
|
async list(options) {
|
|
353
806
|
const params = {};
|
|
@@ -366,10 +819,15 @@ var HelpdeskConversations = class {
|
|
|
366
819
|
}
|
|
367
820
|
/** Update a conversation's status and/or assignee (pass `assignee_user_id: null` to unassign). */
|
|
368
821
|
async update(id, input, options) {
|
|
822
|
+
const resolved = await this.confirmer.prepare(
|
|
823
|
+
{ capabilityId: "helpdesk.conversation.update.execute", body: input },
|
|
824
|
+
{ id },
|
|
825
|
+
options
|
|
826
|
+
);
|
|
369
827
|
return this.client.patch(
|
|
370
828
|
`/api/v1/helpdesk/conversations/${encodeURIComponent(id)}`,
|
|
371
829
|
input,
|
|
372
|
-
|
|
830
|
+
resolved
|
|
373
831
|
);
|
|
374
832
|
}
|
|
375
833
|
/** Read a conversation's messages with cursor-based pagination. */
|
|
@@ -384,26 +842,42 @@ var HelpdeskConversations = class {
|
|
|
384
842
|
}
|
|
385
843
|
};
|
|
386
844
|
var HelpdeskReplies = class {
|
|
387
|
-
constructor(client) {
|
|
845
|
+
constructor(client, confirmer) {
|
|
388
846
|
this.client = client;
|
|
847
|
+
this.confirmer = confirmer;
|
|
389
848
|
}
|
|
390
849
|
client;
|
|
850
|
+
confirmer;
|
|
391
851
|
/**
|
|
392
852
|
* Send an operator reply or internal note. Returns HTTP 201.
|
|
393
853
|
*
|
|
394
|
-
*
|
|
395
|
-
*
|
|
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).
|
|
396
864
|
*/
|
|
397
865
|
async create(input, options) {
|
|
398
|
-
|
|
866
|
+
const resolved = await this.confirmer.prepare(
|
|
867
|
+
{ capabilityId: "helpdesk.conversation.reply.execute", body: input },
|
|
868
|
+
void 0,
|
|
869
|
+
options
|
|
870
|
+
);
|
|
871
|
+
return this.client.postOnce("/api/v1/helpdesk/replies", input, resolved);
|
|
399
872
|
}
|
|
400
873
|
};
|
|
401
874
|
var Helpdesk = class {
|
|
402
875
|
conversations;
|
|
403
876
|
replies;
|
|
404
|
-
constructor(client) {
|
|
405
|
-
|
|
406
|
-
this.
|
|
877
|
+
constructor(client, confirmer) {
|
|
878
|
+
const resolved = confirmer ?? new CapabilityConfirmer(new CapabilityConfirmations(client));
|
|
879
|
+
this.conversations = new HelpdeskConversations(client, resolved);
|
|
880
|
+
this.replies = new HelpdeskReplies(client, resolved);
|
|
407
881
|
}
|
|
408
882
|
};
|
|
409
883
|
|
|
@@ -422,9 +896,15 @@ var Posts = class {
|
|
|
422
896
|
if (options?.type) params.type = options.type;
|
|
423
897
|
return this.client.get("/api/v1/posts", params);
|
|
424
898
|
}
|
|
425
|
-
/**
|
|
426
|
-
|
|
427
|
-
|
|
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);
|
|
428
908
|
}
|
|
429
909
|
/** Get a post by ID, including its per-channel variants. */
|
|
430
910
|
async get(id) {
|
|
@@ -438,11 +918,26 @@ var Posts = class {
|
|
|
438
918
|
async remove(id) {
|
|
439
919
|
return this.client.delete(`/api/v1/posts/${encodeURIComponent(id)}`);
|
|
440
920
|
}
|
|
441
|
-
/**
|
|
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
|
+
*/
|
|
442
929
|
async schedule(id, input) {
|
|
443
930
|
return this.client.post(`/api/v1/posts/${encodeURIComponent(id)}/schedule`, input);
|
|
444
931
|
}
|
|
445
|
-
/**
|
|
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
|
+
*/
|
|
446
941
|
async publish(id) {
|
|
447
942
|
return this.client.post(`/api/v1/posts/${encodeURIComponent(id)}/publish`);
|
|
448
943
|
}
|
|
@@ -452,12 +947,76 @@ var Posts = class {
|
|
|
452
947
|
}
|
|
453
948
|
};
|
|
454
949
|
|
|
950
|
+
// src/resources/scan.ts
|
|
951
|
+
var sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
952
|
+
var Scan = class {
|
|
953
|
+
constructor(client) {
|
|
954
|
+
this.client = client;
|
|
955
|
+
}
|
|
956
|
+
client;
|
|
957
|
+
/**
|
|
958
|
+
* Queue a scan. Provide exactly one of `url`, `orgnr`, or `name`.
|
|
959
|
+
* Runs asynchronously — poll with `get()` or use `waitForResult()`.
|
|
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
|
+
*
|
|
966
|
+
* @throws Error before any request when zero or several selectors are set —
|
|
967
|
+
* the server would reject the body anyway; failing locally is clearer.
|
|
968
|
+
*/
|
|
969
|
+
async create(input, options) {
|
|
970
|
+
const entries = ["url", "orgnr", "name"].filter(
|
|
971
|
+
(key2) => input[key2] !== void 0 && input[key2] !== ""
|
|
972
|
+
);
|
|
973
|
+
if (entries.length !== 1) {
|
|
974
|
+
throw new Error("scan.create requires exactly one of url, orgnr, or name");
|
|
975
|
+
}
|
|
976
|
+
const key = entries[0];
|
|
977
|
+
return this.client.postOnce("/api/v1/scan", { [key]: input[key] }, options);
|
|
978
|
+
}
|
|
979
|
+
/** Get a scan job's status and, once done, its findings payload. */
|
|
980
|
+
async get(id) {
|
|
981
|
+
return this.client.get(`/api/v1/scan/${encodeURIComponent(id)}`);
|
|
982
|
+
}
|
|
983
|
+
/** Search the Norwegian company registry by name (typeahead, top 5 hits). */
|
|
984
|
+
async companies(q) {
|
|
985
|
+
return this.client.get("/api/v1/scan/companies", { q });
|
|
986
|
+
}
|
|
987
|
+
/**
|
|
988
|
+
* Poll a scan until it settles. Resolves with the job for both `done` and
|
|
989
|
+
* `failed` (check `job.error`); throws only when the deadline passes while
|
|
990
|
+
* the scan is still pending/running.
|
|
991
|
+
*/
|
|
992
|
+
async waitForResult(id, options = {}) {
|
|
993
|
+
const rawInterval = options.intervalMs ?? 2500;
|
|
994
|
+
const rawTimeout = options.timeoutMs ?? 12e4;
|
|
995
|
+
const intervalMs = Number.isFinite(rawInterval) && rawInterval > 0 ? rawInterval : 2500;
|
|
996
|
+
const timeoutMs = Number.isFinite(rawTimeout) ? rawTimeout : 12e4;
|
|
997
|
+
const deadline = Date.now() + timeoutMs;
|
|
998
|
+
let lastStatus = "pending";
|
|
999
|
+
for (; ; ) {
|
|
1000
|
+
const { data } = await this.get(id);
|
|
1001
|
+
if (data.status === "done" || data.status === "failed") return data;
|
|
1002
|
+
lastStatus = data.status;
|
|
1003
|
+
const remaining = deadline - Date.now();
|
|
1004
|
+
if (remaining <= 0) break;
|
|
1005
|
+
await sleep(Math.min(intervalMs, remaining));
|
|
1006
|
+
if (Date.now() >= deadline) break;
|
|
1007
|
+
}
|
|
1008
|
+
throw new Error(`Scan ${id} timed out after ${timeoutMs}ms (status: ${lastStatus})`);
|
|
1009
|
+
}
|
|
1010
|
+
};
|
|
1011
|
+
|
|
455
1012
|
// src/resources/webhooks.ts
|
|
456
1013
|
var Webhooks = class {
|
|
457
|
-
constructor(client) {
|
|
1014
|
+
constructor(client, confirmer) {
|
|
458
1015
|
this.client = client;
|
|
1016
|
+
this.confirmer = confirmer ?? new CapabilityConfirmer(new CapabilityConfirmations(client));
|
|
459
1017
|
}
|
|
460
1018
|
client;
|
|
1019
|
+
confirmer;
|
|
461
1020
|
/** List all webhook endpoints in the workspace. */
|
|
462
1021
|
async list() {
|
|
463
1022
|
return this.client.get("/api/v1/webhooks");
|
|
@@ -473,9 +1032,21 @@ var Webhooks = class {
|
|
|
473
1032
|
* `secret` is typed optional because an idempotent replay (retrying with the
|
|
474
1033
|
* same `Idempotency-Key`, `X-Idempotent-Replayed: true`) returns the existing
|
|
475
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.
|
|
476
1042
|
*/
|
|
477
1043
|
async create(input, options) {
|
|
478
|
-
|
|
1044
|
+
const resolved = await this.confirmer.prepare(
|
|
1045
|
+
{ capabilityId: "helpdesk.webhook.create.execute", body: input },
|
|
1046
|
+
void 0,
|
|
1047
|
+
options
|
|
1048
|
+
);
|
|
1049
|
+
return this.client.postOnce("/api/v1/webhooks", input, resolved);
|
|
479
1050
|
}
|
|
480
1051
|
/** Get a webhook endpoint by ID. */
|
|
481
1052
|
async get(id) {
|
|
@@ -483,7 +1054,12 @@ var Webhooks = class {
|
|
|
483
1054
|
}
|
|
484
1055
|
/** Update a webhook endpoint (name, url, event types, filters, enabled). */
|
|
485
1056
|
async update(id, input, options) {
|
|
486
|
-
|
|
1057
|
+
const resolved = await this.confirmer.prepare(
|
|
1058
|
+
{ capabilityId: "helpdesk.webhook.update.execute", body: input },
|
|
1059
|
+
{ id },
|
|
1060
|
+
options
|
|
1061
|
+
);
|
|
1062
|
+
return this.client.patch(`/api/v1/webhooks/${encodeURIComponent(id)}`, input, resolved);
|
|
487
1063
|
}
|
|
488
1064
|
/**
|
|
489
1065
|
* Permanently delete a webhook endpoint (stops all outbound deliveries).
|
|
@@ -492,7 +1068,12 @@ var Webhooks = class {
|
|
|
492
1068
|
* grants on this route. API keys with legacy scopes may omit it.
|
|
493
1069
|
*/
|
|
494
1070
|
async delete(id, options) {
|
|
495
|
-
|
|
1071
|
+
const resolved = await this.confirmer.prepare(
|
|
1072
|
+
{ capabilityId: "helpdesk.webhook.delete.execute", body: void 0 },
|
|
1073
|
+
{ id },
|
|
1074
|
+
options
|
|
1075
|
+
);
|
|
1076
|
+
return this.client.delete(`/api/v1/webhooks/${encodeURIComponent(id)}`, resolved);
|
|
496
1077
|
}
|
|
497
1078
|
/** List recent deliveries for an endpoint (most recent first). */
|
|
498
1079
|
async deliveries(id, options) {
|
|
@@ -500,7 +1081,14 @@ var Webhooks = class {
|
|
|
500
1081
|
if (options?.limit !== void 0) params.limit = String(options.limit);
|
|
501
1082
|
return this.client.get(`/api/v1/webhooks/${encodeURIComponent(id)}/deliveries`, params);
|
|
502
1083
|
}
|
|
503
|
-
/**
|
|
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
|
+
*/
|
|
504
1092
|
async test(id) {
|
|
505
1093
|
return this.client.post(`/api/v1/webhooks/${encodeURIComponent(id)}/test`);
|
|
506
1094
|
}
|
|
@@ -590,6 +1178,8 @@ async function verifyWebhookSignature(input) {
|
|
|
590
1178
|
|
|
591
1179
|
// src/index.ts
|
|
592
1180
|
var Medal = class {
|
|
1181
|
+
bookings;
|
|
1182
|
+
capabilityConfirmations;
|
|
593
1183
|
channels;
|
|
594
1184
|
emails;
|
|
595
1185
|
contacts;
|
|
@@ -597,6 +1187,7 @@ var Medal = class {
|
|
|
597
1187
|
gdpr;
|
|
598
1188
|
helpdesk;
|
|
599
1189
|
posts;
|
|
1190
|
+
scan;
|
|
600
1191
|
webhooks;
|
|
601
1192
|
workspaces;
|
|
602
1193
|
constructor(token, options) {
|
|
@@ -612,14 +1203,21 @@ var Medal = class {
|
|
|
612
1203
|
timeout: options?.timeout ?? 3e4,
|
|
613
1204
|
userAgent: "medalsocial-sdk/1.0.0 (+https://github.com/Medal-Social/MedalSocial)"
|
|
614
1205
|
});
|
|
615
|
-
this.
|
|
1206
|
+
this.capabilityConfirmations = new CapabilityConfirmations(client);
|
|
1207
|
+
const confirmer = new CapabilityConfirmer(
|
|
1208
|
+
this.capabilityConfirmations,
|
|
1209
|
+
options?.autoConfirmCapabilities
|
|
1210
|
+
);
|
|
1211
|
+
this.bookings = new Bookings(client);
|
|
1212
|
+
this.channels = new Channels(client, confirmer);
|
|
616
1213
|
this.emails = new Emails(client);
|
|
617
1214
|
this.contacts = new Contacts(client);
|
|
618
1215
|
this.deals = new Deals(client);
|
|
619
1216
|
this.gdpr = new Gdpr(client);
|
|
620
|
-
this.helpdesk = new Helpdesk(client);
|
|
1217
|
+
this.helpdesk = new Helpdesk(client, confirmer);
|
|
621
1218
|
this.posts = new Posts(client);
|
|
622
|
-
this.
|
|
1219
|
+
this.scan = new Scan(client);
|
|
1220
|
+
this.webhooks = new Webhooks(client, confirmer);
|
|
623
1221
|
this.workspaces = new Workspaces(client);
|
|
624
1222
|
}
|
|
625
1223
|
};
|
|
@@ -629,6 +1227,11 @@ function createMedalClient(apiKey, options) {
|
|
|
629
1227
|
var src_default = Medal;
|
|
630
1228
|
export {
|
|
631
1229
|
BaseClient,
|
|
1230
|
+
Bookings,
|
|
1231
|
+
CAPABILITY_IDS,
|
|
1232
|
+
CAPABILITY_ROUTES,
|
|
1233
|
+
CapabilityConfirmations,
|
|
1234
|
+
CapabilityConfirmer,
|
|
632
1235
|
Channels,
|
|
633
1236
|
Contacts,
|
|
634
1237
|
DEFAULT_WEBHOOK_TOLERANCE_MS,
|
|
@@ -639,6 +1242,7 @@ export {
|
|
|
639
1242
|
Medal,
|
|
640
1243
|
MedalApiError,
|
|
641
1244
|
Posts,
|
|
1245
|
+
Scan,
|
|
642
1246
|
WebhookVerificationError,
|
|
643
1247
|
Webhooks,
|
|
644
1248
|
Workspaces,
|