@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.js
CHANGED
|
@@ -21,6 +21,11 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
21
21
|
var src_exports = {};
|
|
22
22
|
__export(src_exports, {
|
|
23
23
|
BaseClient: () => BaseClient,
|
|
24
|
+
Bookings: () => Bookings,
|
|
25
|
+
CAPABILITY_IDS: () => CAPABILITY_IDS,
|
|
26
|
+
CAPABILITY_ROUTES: () => CAPABILITY_ROUTES,
|
|
27
|
+
CapabilityConfirmations: () => CapabilityConfirmations,
|
|
28
|
+
CapabilityConfirmer: () => CapabilityConfirmer,
|
|
24
29
|
Channels: () => Channels,
|
|
25
30
|
Contacts: () => Contacts,
|
|
26
31
|
DEFAULT_WEBHOOK_TOLERANCE_MS: () => DEFAULT_WEBHOOK_TOLERANCE_MS,
|
|
@@ -31,6 +36,7 @@ __export(src_exports, {
|
|
|
31
36
|
Medal: () => Medal,
|
|
32
37
|
MedalApiError: () => MedalApiError,
|
|
33
38
|
Posts: () => Posts,
|
|
39
|
+
Scan: () => Scan,
|
|
34
40
|
WebhookVerificationError: () => WebhookVerificationError,
|
|
35
41
|
Webhooks: () => Webhooks,
|
|
36
42
|
Workspaces: () => Workspaces,
|
|
@@ -55,6 +61,21 @@ var MedalApiError = class extends Error {
|
|
|
55
61
|
};
|
|
56
62
|
|
|
57
63
|
// src/client.ts
|
|
64
|
+
function randomIdempotencyKey() {
|
|
65
|
+
const webCrypto = globalThis.crypto;
|
|
66
|
+
if (typeof webCrypto.randomUUID === "function") {
|
|
67
|
+
return webCrypto.randomUUID();
|
|
68
|
+
}
|
|
69
|
+
const bytes = new Uint8Array(16);
|
|
70
|
+
webCrypto.getRandomValues(bytes);
|
|
71
|
+
bytes[6] = bytes[6] & 15 | 64;
|
|
72
|
+
bytes[8] = bytes[8] & 63 | 128;
|
|
73
|
+
const hex = Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
74
|
+
return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
|
|
75
|
+
}
|
|
76
|
+
function resolveIdempotencyKey(supplied) {
|
|
77
|
+
return (supplied ?? "").trim() || randomIdempotencyKey();
|
|
78
|
+
}
|
|
58
79
|
var BaseClient = class {
|
|
59
80
|
/** Resolved client configuration. */
|
|
60
81
|
config;
|
|
@@ -74,6 +95,29 @@ var BaseClient = class {
|
|
|
74
95
|
body: body !== void 0 ? JSON.stringify(body) : void 0
|
|
75
96
|
});
|
|
76
97
|
}
|
|
98
|
+
/**
|
|
99
|
+
* Execute a POST that must never execute twice, guaranteeing an
|
|
100
|
+
* `Idempotency-Key`.
|
|
101
|
+
*
|
|
102
|
+
* {@link BaseClient.post} retries 429 and 5xx automatically, so a write
|
|
103
|
+
* whose transaction committed before the gateway failed would otherwise be
|
|
104
|
+
* submitted a second time — booking the same slot twice. A key turns that
|
|
105
|
+
* retry into a replay: the server keys on the key, the workspace, and the
|
|
106
|
+
* method+path, and answers a repeat with the stored response, or 409 while
|
|
107
|
+
* the first attempt is still in flight. Either way the write happens once.
|
|
108
|
+
*
|
|
109
|
+
* The key is minted ONCE here, outside the retry loop in `request`, so every
|
|
110
|
+
* attempt of the same logical call carries the same value — a key minted per
|
|
111
|
+
* attempt would deduplicate nothing. A caller-supplied key always wins, so
|
|
112
|
+
* callers keeping their own records stay in control. See
|
|
113
|
+
* {@link resolveIdempotencyKey} for what counts as supplied.
|
|
114
|
+
*/
|
|
115
|
+
async postOnce(path, body, options) {
|
|
116
|
+
return this.post(path, body, {
|
|
117
|
+
...options,
|
|
118
|
+
idempotencyKey: resolveIdempotencyKey(options?.idempotencyKey)
|
|
119
|
+
});
|
|
120
|
+
}
|
|
77
121
|
/** Execute an authenticated PATCH request with a JSON body. */
|
|
78
122
|
async patch(path, body, options) {
|
|
79
123
|
return this.request(this.buildUrl(path), {
|
|
@@ -125,12 +169,22 @@ var BaseClient = class {
|
|
|
125
169
|
const controller = new AbortController();
|
|
126
170
|
const timeout = setTimeout(() => controller.abort(), this.config.timeout);
|
|
127
171
|
let res;
|
|
172
|
+
let text = "";
|
|
173
|
+
let retrying = false;
|
|
128
174
|
try {
|
|
129
175
|
res = await fetch(url, { ...init, headers, signal: controller.signal });
|
|
176
|
+
retrying = (res.status === 429 || res.status >= 500 && res.status <= 599) && attempt < maxAttempts;
|
|
177
|
+
if (retrying) {
|
|
178
|
+
const drained = res.body ? res.body.pipeTo(new WritableStream()) : res.text();
|
|
179
|
+
await drained.catch(() => {
|
|
180
|
+
});
|
|
181
|
+
} else {
|
|
182
|
+
text = await res.text();
|
|
183
|
+
}
|
|
130
184
|
} finally {
|
|
131
185
|
clearTimeout(timeout);
|
|
132
186
|
}
|
|
133
|
-
if (
|
|
187
|
+
if (retrying) {
|
|
134
188
|
const retryAfter = res.headers.get("retry-after");
|
|
135
189
|
let delayMs = 0;
|
|
136
190
|
if (retryAfter) {
|
|
@@ -143,7 +197,6 @@ var BaseClient = class {
|
|
|
143
197
|
await new Promise((r) => setTimeout(r, delayMs));
|
|
144
198
|
continue;
|
|
145
199
|
}
|
|
146
|
-
const text = await res.text();
|
|
147
200
|
let parsed;
|
|
148
201
|
try {
|
|
149
202
|
parsed = text ? JSON.parse(text) : void 0;
|
|
@@ -165,12 +218,304 @@ var BaseClient = class {
|
|
|
165
218
|
}
|
|
166
219
|
};
|
|
167
220
|
|
|
221
|
+
// src/types/capabilities.ts
|
|
222
|
+
var CAPABILITY_IDS = [
|
|
223
|
+
"channel.connect_link.create.execute",
|
|
224
|
+
"channel.connect_link.revoke.execute",
|
|
225
|
+
"channel.connection.disconnect.execute",
|
|
226
|
+
"helpdesk.conversation.reply.execute",
|
|
227
|
+
"helpdesk.conversation.update.execute",
|
|
228
|
+
"helpdesk.webhook.create.execute",
|
|
229
|
+
"helpdesk.webhook.update.execute",
|
|
230
|
+
"helpdesk.webhook.delete.execute"
|
|
231
|
+
];
|
|
232
|
+
var CAPABILITY_ROUTES = {
|
|
233
|
+
"channel.connect_link.create.execute": {
|
|
234
|
+
method: "POST",
|
|
235
|
+
path_template: "/api/v1/channels/connect-links"
|
|
236
|
+
},
|
|
237
|
+
"channel.connect_link.revoke.execute": {
|
|
238
|
+
method: "DELETE",
|
|
239
|
+
path_template: "/api/v1/channels/connect-links/{id}"
|
|
240
|
+
},
|
|
241
|
+
"channel.connection.disconnect.execute": {
|
|
242
|
+
method: "DELETE",
|
|
243
|
+
path_template: "/api/v1/channels/connections/{id}"
|
|
244
|
+
},
|
|
245
|
+
"helpdesk.conversation.reply.execute": {
|
|
246
|
+
method: "POST",
|
|
247
|
+
path_template: "/api/v1/helpdesk/replies"
|
|
248
|
+
},
|
|
249
|
+
"helpdesk.conversation.update.execute": {
|
|
250
|
+
method: "PATCH",
|
|
251
|
+
path_template: "/api/v1/helpdesk/conversations/{id}"
|
|
252
|
+
},
|
|
253
|
+
"helpdesk.webhook.create.execute": {
|
|
254
|
+
method: "POST",
|
|
255
|
+
path_template: "/api/v1/webhooks"
|
|
256
|
+
},
|
|
257
|
+
"helpdesk.webhook.update.execute": {
|
|
258
|
+
method: "PATCH",
|
|
259
|
+
path_template: "/api/v1/webhooks/{id}"
|
|
260
|
+
},
|
|
261
|
+
"helpdesk.webhook.delete.execute": {
|
|
262
|
+
method: "DELETE",
|
|
263
|
+
path_template: "/api/v1/webhooks/{id}"
|
|
264
|
+
}
|
|
265
|
+
};
|
|
266
|
+
|
|
267
|
+
// src/capability-confirmer.ts
|
|
268
|
+
function resolvePath(template, pathParams) {
|
|
269
|
+
return template.replace(/\{([^}/]+)\}/g, (_match, name) => {
|
|
270
|
+
const value = pathParams?.[name];
|
|
271
|
+
return value === void 0 ? `{${name}}` : encodeURIComponent(String(value));
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
var CapabilityConfirmer = class {
|
|
275
|
+
constructor(confirmations, defaults) {
|
|
276
|
+
this.confirmations = confirmations;
|
|
277
|
+
this.defaults = defaults;
|
|
278
|
+
}
|
|
279
|
+
confirmations;
|
|
280
|
+
defaults;
|
|
281
|
+
/**
|
|
282
|
+
* Return the request options to use for a confirmable write, minting the
|
|
283
|
+
* idempotency key and confirmation token first when auto-confirm is active.
|
|
284
|
+
*
|
|
285
|
+
* `body` is the pending request payload (`undefined` for `DELETE` routes).
|
|
286
|
+
* It is handed to the `previewSummary` callback by reference so the summary
|
|
287
|
+
* can describe the specific action, not just the route — it is the caller's
|
|
288
|
+
* own payload, so it is passed through unmodified and unredacted.
|
|
289
|
+
*/
|
|
290
|
+
async prepare(request, pathParams, options) {
|
|
291
|
+
const auto = options?.autoConfirm === false ? void 0 : options?.autoConfirm ?? this.defaults;
|
|
292
|
+
if (!auto) return options;
|
|
293
|
+
const idempotencyKey = resolveIdempotencyKey(options?.idempotencyKey);
|
|
294
|
+
const callerKeyIsUsable = idempotencyKey === options?.idempotencyKey;
|
|
295
|
+
if (callerKeyIsUsable && options?.capabilityConfirmation) return options;
|
|
296
|
+
const route = CAPABILITY_ROUTES[request.capabilityId];
|
|
297
|
+
const path = resolvePath(route.path_template, pathParams);
|
|
298
|
+
const previewSummary = auto.previewSummary({
|
|
299
|
+
...request,
|
|
300
|
+
method: route.method,
|
|
301
|
+
path,
|
|
302
|
+
...pathParams ? { pathParams } : {},
|
|
303
|
+
idempotencyKey
|
|
304
|
+
});
|
|
305
|
+
if (typeof previewSummary !== "string" || previewSummary.trim() === "") {
|
|
306
|
+
throw new Error(
|
|
307
|
+
`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.`
|
|
308
|
+
);
|
|
309
|
+
}
|
|
310
|
+
const { data } = await this.confirmations.create({
|
|
311
|
+
capability_id: request.capabilityId,
|
|
312
|
+
...pathParams ? { path_params: pathParams } : {},
|
|
313
|
+
idempotency_key: idempotencyKey,
|
|
314
|
+
preview_summary: previewSummary,
|
|
315
|
+
user_approved: true
|
|
316
|
+
});
|
|
317
|
+
return {
|
|
318
|
+
...options,
|
|
319
|
+
idempotencyKey,
|
|
320
|
+
capabilityConfirmation: data.confirmation_token
|
|
321
|
+
};
|
|
322
|
+
}
|
|
323
|
+
};
|
|
324
|
+
|
|
325
|
+
// src/resources/bookings.ts
|
|
326
|
+
var BookingsManage = class {
|
|
327
|
+
constructor(client) {
|
|
328
|
+
this.client = client;
|
|
329
|
+
}
|
|
330
|
+
client;
|
|
331
|
+
/**
|
|
332
|
+
* Read what the holder of a manage token may see and do. Honour
|
|
333
|
+
* `can_cancel` / `can_reschedule` — they already apply the policy windows.
|
|
334
|
+
*/
|
|
335
|
+
async get(token) {
|
|
336
|
+
return this.client.get(`/api/v1/bookings/manage/${encodeURIComponent(token)}`);
|
|
337
|
+
}
|
|
338
|
+
/** Cancel on the customer's behalf. Rejected outside the cancel window. */
|
|
339
|
+
async cancel(token, input, options) {
|
|
340
|
+
return this.client.postOnce(
|
|
341
|
+
`/api/v1/bookings/manage/${encodeURIComponent(token)}/cancel`,
|
|
342
|
+
input ?? {},
|
|
343
|
+
options
|
|
344
|
+
);
|
|
345
|
+
}
|
|
346
|
+
/**
|
|
347
|
+
* Move the booking on the customer's behalf. Rejected outside the reschedule
|
|
348
|
+
* window. Returns a NEW booking id and a new manage token — the old token
|
|
349
|
+
* stops working, so relay the new one into whatever link you send next.
|
|
350
|
+
*/
|
|
351
|
+
async reschedule(token, input, options) {
|
|
352
|
+
return this.client.postOnce(
|
|
353
|
+
`/api/v1/bookings/manage/${encodeURIComponent(token)}/reschedule`,
|
|
354
|
+
input,
|
|
355
|
+
options
|
|
356
|
+
);
|
|
357
|
+
}
|
|
358
|
+
};
|
|
359
|
+
var Bookings = class {
|
|
360
|
+
constructor(client) {
|
|
361
|
+
this.client = client;
|
|
362
|
+
this.manage = new BookingsManage(client);
|
|
363
|
+
}
|
|
364
|
+
client;
|
|
365
|
+
/** Customer-side actions addressed by manage token. */
|
|
366
|
+
manage;
|
|
367
|
+
/** List the bookable service catalogue. Active-only unless asked otherwise. */
|
|
368
|
+
async listServices(options) {
|
|
369
|
+
const params = {};
|
|
370
|
+
if (options?.include_inactive !== void 0) {
|
|
371
|
+
params.include_inactive = String(options.include_inactive);
|
|
372
|
+
}
|
|
373
|
+
return this.client.get("/api/v1/bookings/services", params);
|
|
374
|
+
}
|
|
375
|
+
/** List the bookable resources — staff, rooms, and equipment. */
|
|
376
|
+
async listResources() {
|
|
377
|
+
return this.client.get("/api/v1/bookings/resources");
|
|
378
|
+
}
|
|
379
|
+
/**
|
|
380
|
+
* List free slots for a service over a window. Slots reflect opening hours,
|
|
381
|
+
* time off, buffers, and existing bookings at the moment of the call — they
|
|
382
|
+
* are not held, so a slot can be taken before you book it.
|
|
383
|
+
*/
|
|
384
|
+
async availability(options) {
|
|
385
|
+
const params = {
|
|
386
|
+
service_id: options.service_id,
|
|
387
|
+
from_ts: String(options.from_ts),
|
|
388
|
+
to_ts: String(options.to_ts)
|
|
389
|
+
};
|
|
390
|
+
if (options.resource_id) params.resource_id = options.resource_id;
|
|
391
|
+
return this.client.get("/api/v1/bookings/availability", params);
|
|
392
|
+
}
|
|
393
|
+
/**
|
|
394
|
+
* The dates a service can be booked on — the half `availability` cannot
|
|
395
|
+
* answer. Availability returns free slots and nothing else, so a closed day,
|
|
396
|
+
* an evening past closing and a fully booked day are all the same empty
|
|
397
|
+
* array. A date absent from this list is closed; on a listed date, compare
|
|
398
|
+
* `last_start_ts` against the clock to tell "too late today" from "full".
|
|
399
|
+
*/
|
|
400
|
+
async schedule(options) {
|
|
401
|
+
const params = {
|
|
402
|
+
service_id: options.service_id,
|
|
403
|
+
from_ts: String(options.from_ts),
|
|
404
|
+
to_ts: String(options.to_ts)
|
|
405
|
+
};
|
|
406
|
+
if (options.resource_id) params.resource_id = options.resource_id;
|
|
407
|
+
return this.client.get("/api/v1/bookings/schedule", params);
|
|
408
|
+
}
|
|
409
|
+
/**
|
|
410
|
+
* List bookings with cursor-based pagination and optional filters.
|
|
411
|
+
*
|
|
412
|
+
* Check `pagination.truncated`: when true the read window was clipped and
|
|
413
|
+
* matching bookings exist that no cursor reaches — narrow `from_ts`/`to_ts`.
|
|
414
|
+
*/
|
|
415
|
+
async list(options) {
|
|
416
|
+
const params = {};
|
|
417
|
+
if (options?.limit !== void 0) params.limit = String(options.limit);
|
|
418
|
+
if (options?.cursor) params.cursor = options.cursor;
|
|
419
|
+
if (options?.status) params.status = options.status;
|
|
420
|
+
if (options?.resource_id) params.resource_id = options.resource_id;
|
|
421
|
+
if (options?.from_ts !== void 0) params.from_ts = String(options.from_ts);
|
|
422
|
+
if (options?.to_ts !== void 0) params.to_ts = String(options.to_ts);
|
|
423
|
+
return this.client.get("/api/v1/bookings", params);
|
|
424
|
+
}
|
|
425
|
+
/**
|
|
426
|
+
* Book a party — every item succeeds or none do (max 50).
|
|
427
|
+
*
|
|
428
|
+
* Each created booking comes back with a `manage_token` exactly once; only
|
|
429
|
+
* its hash is stored, so persist it if you need the customer's manage link.
|
|
430
|
+
*
|
|
431
|
+
* Automatically idempotent: the SDK mints an `Idempotency-Key` so its own
|
|
432
|
+
* 5xx retries replay rather than book the slot twice. Supply
|
|
433
|
+
* `options.idempotencyKey` to deduplicate across your OWN retries too — the
|
|
434
|
+
* server keys on it for 24 hours, so re-sending the same key after a network
|
|
435
|
+
* timeout returns the original bookings instead of a second set.
|
|
436
|
+
*/
|
|
437
|
+
async create(input, options) {
|
|
438
|
+
return this.client.postOnce("/api/v1/bookings", input, options);
|
|
439
|
+
}
|
|
440
|
+
/** Get a booking by ID. */
|
|
441
|
+
async get(id) {
|
|
442
|
+
return this.client.get(`/api/v1/bookings/${encodeURIComponent(id)}`);
|
|
443
|
+
}
|
|
444
|
+
/**
|
|
445
|
+
* Annotate a booking. At least one of `notes` (customer-visible) or
|
|
446
|
+
* `internal_notes` (staff-only) is required; `""` clears a field.
|
|
447
|
+
*/
|
|
448
|
+
async update(id, input, options) {
|
|
449
|
+
return this.client.patch(`/api/v1/bookings/${encodeURIComponent(id)}`, input, options);
|
|
450
|
+
}
|
|
451
|
+
/** Cancel as the business — the cancel window is bypassed. */
|
|
452
|
+
async cancel(id, input, options) {
|
|
453
|
+
return this.client.postOnce(
|
|
454
|
+
`/api/v1/bookings/${encodeURIComponent(id)}/cancel`,
|
|
455
|
+
input ?? {},
|
|
456
|
+
options
|
|
457
|
+
);
|
|
458
|
+
}
|
|
459
|
+
/**
|
|
460
|
+
* Move a booking as the business — the reschedule window is bypassed.
|
|
461
|
+
* Returns a NEW booking id and a new manage token; the old booking is
|
|
462
|
+
* cancelled and its token stops working.
|
|
463
|
+
*/
|
|
464
|
+
async reschedule(id, input, options) {
|
|
465
|
+
return this.client.postOnce(
|
|
466
|
+
`/api/v1/bookings/${encodeURIComponent(id)}/reschedule`,
|
|
467
|
+
input,
|
|
468
|
+
options
|
|
469
|
+
);
|
|
470
|
+
}
|
|
471
|
+
/** Mark a booking as a no-show. */
|
|
472
|
+
async markNoShow(id, options) {
|
|
473
|
+
return this.client.postOnce(
|
|
474
|
+
`/api/v1/bookings/${encodeURIComponent(id)}/no-show`,
|
|
475
|
+
void 0,
|
|
476
|
+
options
|
|
477
|
+
);
|
|
478
|
+
}
|
|
479
|
+
};
|
|
480
|
+
|
|
481
|
+
// src/resources/capability-confirmations.ts
|
|
482
|
+
var CapabilityConfirmations = class {
|
|
483
|
+
constructor(client) {
|
|
484
|
+
this.client = client;
|
|
485
|
+
}
|
|
486
|
+
client;
|
|
487
|
+
/**
|
|
488
|
+
* Issue a confirmation token for one pending write.
|
|
489
|
+
*
|
|
490
|
+
* The token is bound to the workspace, the auth subject, the capability's
|
|
491
|
+
* method + path, its required scopes, and `idempotency_key` — so it is
|
|
492
|
+
* usable exactly once, for exactly the write it describes, and expires
|
|
493
|
+
* within 15 minutes.
|
|
494
|
+
*
|
|
495
|
+
* Setting `user_approved: true` asserts that a human on your side approved
|
|
496
|
+
* this specific action. `preview_summary` is what they approved, and is
|
|
497
|
+
* retained for audit — write it for a human reader, not a log parser.
|
|
498
|
+
*
|
|
499
|
+
* Deliberately unkeyed, unlike the writes it authorizes. Minting is not the
|
|
500
|
+
* state change the guarantee exists to protect: the write itself is already
|
|
501
|
+
* bound to `idempotency_key`, so a retry that mints a second token cannot
|
|
502
|
+
* produce a second write. Keying this call would instead park a credential
|
|
503
|
+
* designed to expire in 15 minutes inside a replay cache that answers for 24
|
|
504
|
+
* hours — a worse trade than the duplicate token it would avoid.
|
|
505
|
+
*/
|
|
506
|
+
async create(input) {
|
|
507
|
+
return this.client.post("/api/v1/capability-confirmations", input);
|
|
508
|
+
}
|
|
509
|
+
};
|
|
510
|
+
|
|
168
511
|
// src/resources/channels.ts
|
|
169
512
|
var ChannelConnectLinks = class {
|
|
170
|
-
constructor(client) {
|
|
513
|
+
constructor(client, confirmer) {
|
|
171
514
|
this.client = client;
|
|
515
|
+
this.confirmer = confirmer;
|
|
172
516
|
}
|
|
173
517
|
client;
|
|
518
|
+
confirmer;
|
|
174
519
|
/**
|
|
175
520
|
* Mint a single-use hosted connect link. Returns HTTP 201.
|
|
176
521
|
*
|
|
@@ -181,30 +526,73 @@ var ChannelConnectLinks = class {
|
|
|
181
526
|
*
|
|
182
527
|
* Requires the `channel.connect.manage` scope; OAuth callers additionally
|
|
183
528
|
* need the workspace `admin` role.
|
|
529
|
+
*
|
|
530
|
+
* Automatically idempotent: an unkeyed retry mints a SECOND live single-use
|
|
531
|
+
* link for the same person, and only one of the two can ever be consumed —
|
|
532
|
+
* the other stays outstanding until it is revoked or expires. The key the
|
|
533
|
+
* confirmer chose is the key that goes out — a capability confirmation is
|
|
534
|
+
* bound to its idempotency key, so minting a fresh one here would invalidate
|
|
535
|
+
* the confirmation.
|
|
184
536
|
*/
|
|
185
537
|
async create(input, options) {
|
|
186
|
-
|
|
538
|
+
const resolved = await this.confirmer.prepare(
|
|
539
|
+
{ capabilityId: "channel.connect_link.create.execute", body: input },
|
|
540
|
+
void 0,
|
|
541
|
+
options
|
|
542
|
+
);
|
|
543
|
+
return this.client.postOnce("/api/v1/channels/connect-links", input, resolved);
|
|
187
544
|
}
|
|
188
|
-
/**
|
|
545
|
+
/**
|
|
546
|
+
* List the workspace's connect links (tokens are never returned), newest
|
|
547
|
+
* first, with cursor-based pagination.
|
|
548
|
+
*
|
|
549
|
+
* `limit` defaults to 50 server-side and is capped at 100. Follow
|
|
550
|
+
* `pagination.next_cursor` while `pagination.has_more` is true.
|
|
551
|
+
*
|
|
552
|
+
* The `channel_type` / `status` filters are applied **within** each page,
|
|
553
|
+
* so a page may hold fewer than `limit` items while `has_more` is still
|
|
554
|
+
* true — drive the loop off `has_more`, never off the item count.
|
|
555
|
+
*/
|
|
189
556
|
async list(options) {
|
|
190
557
|
const params = {};
|
|
558
|
+
if (options?.limit !== void 0) params.limit = String(options.limit);
|
|
559
|
+
if (options?.cursor) params.cursor = options.cursor;
|
|
191
560
|
if (options?.channel_type) params.channel_type = options.channel_type;
|
|
192
561
|
if (options?.status) params.status = options.status;
|
|
193
562
|
return this.client.get("/api/v1/channels/connect-links", params);
|
|
194
563
|
}
|
|
195
564
|
/** Revoke a pending connect link so it can no longer be consumed. */
|
|
196
565
|
async revoke(id, options) {
|
|
197
|
-
|
|
566
|
+
const resolved = await this.confirmer.prepare(
|
|
567
|
+
{ capabilityId: "channel.connect_link.revoke.execute", body: void 0 },
|
|
568
|
+
{ id },
|
|
569
|
+
options
|
|
570
|
+
);
|
|
571
|
+
return this.client.delete(`/api/v1/channels/connect-links/${encodeURIComponent(id)}`, resolved);
|
|
198
572
|
}
|
|
199
573
|
};
|
|
200
574
|
var ChannelConnections = class {
|
|
201
|
-
constructor(client) {
|
|
575
|
+
constructor(client, confirmer) {
|
|
202
576
|
this.client = client;
|
|
577
|
+
this.confirmer = confirmer;
|
|
203
578
|
}
|
|
204
579
|
client;
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
580
|
+
confirmer;
|
|
581
|
+
/**
|
|
582
|
+
* List the workspace's channel connections (generic, channel-agnostic
|
|
583
|
+
* shape), newest first, with cursor-based pagination.
|
|
584
|
+
*
|
|
585
|
+
* `limit` defaults to 50 server-side and is capped at 100. Follow
|
|
586
|
+
* `pagination.next_cursor` while `pagination.has_more` is true. Rows that
|
|
587
|
+
* are not projectable as connections are dropped within the page, so a page
|
|
588
|
+
* may hold fewer than `limit` items while `has_more` is still true — drive
|
|
589
|
+
* the loop off `has_more`, never off the item count.
|
|
590
|
+
*/
|
|
591
|
+
async list(options) {
|
|
592
|
+
const params = {};
|
|
593
|
+
if (options?.limit !== void 0) params.limit = String(options.limit);
|
|
594
|
+
if (options?.cursor) params.cursor = options.cursor;
|
|
595
|
+
return this.client.get("/api/v1/channels/connections", params);
|
|
208
596
|
}
|
|
209
597
|
/**
|
|
210
598
|
* Disconnect a connected channel account (best-effort platform logout, then
|
|
@@ -212,15 +600,21 @@ var ChannelConnections = class {
|
|
|
212
600
|
* `reason: "api_disconnect"` if the account was previously connected.
|
|
213
601
|
*/
|
|
214
602
|
async disconnect(id, options) {
|
|
215
|
-
|
|
603
|
+
const resolved = await this.confirmer.prepare(
|
|
604
|
+
{ capabilityId: "channel.connection.disconnect.execute", body: void 0 },
|
|
605
|
+
{ id },
|
|
606
|
+
options
|
|
607
|
+
);
|
|
608
|
+
return this.client.delete(`/api/v1/channels/connections/${encodeURIComponent(id)}`, resolved);
|
|
216
609
|
}
|
|
217
610
|
};
|
|
218
611
|
var Channels = class {
|
|
219
612
|
connectLinks;
|
|
220
613
|
connections;
|
|
221
|
-
constructor(client) {
|
|
222
|
-
|
|
223
|
-
this.
|
|
614
|
+
constructor(client, confirmer) {
|
|
615
|
+
const resolved = confirmer ?? new CapabilityConfirmer(new CapabilityConfirmations(client));
|
|
616
|
+
this.connectLinks = new ChannelConnectLinks(client, resolved);
|
|
617
|
+
this.connections = new ChannelConnections(client, resolved);
|
|
224
618
|
}
|
|
225
619
|
};
|
|
226
620
|
|
|
@@ -241,9 +635,17 @@ var Contacts = class {
|
|
|
241
635
|
if (options?.search) params.search = options.search;
|
|
242
636
|
return this.client.get("/api/v1/contacts", params);
|
|
243
637
|
}
|
|
244
|
-
/**
|
|
245
|
-
|
|
246
|
-
|
|
638
|
+
/**
|
|
639
|
+
* Create a new contact. Email must be unique in the workspace.
|
|
640
|
+
*
|
|
641
|
+
* Automatically idempotent: the SDK mints an `Idempotency-Key` so its own
|
|
642
|
+
* 5xx retries replay rather than run the create a second time. Uniqueness
|
|
643
|
+
* alone would not save you here — it turns the retry of a committed create
|
|
644
|
+
* into a spurious conflict, which reads as "the contact was not created".
|
|
645
|
+
* Supply `options.idempotencyKey` to deduplicate across your OWN retries too.
|
|
646
|
+
*/
|
|
647
|
+
async create(input, options) {
|
|
648
|
+
return this.client.postOnce("/api/v1/contacts", input, options);
|
|
247
649
|
}
|
|
248
650
|
/** Get a contact by ID. */
|
|
249
651
|
async get(id) {
|
|
@@ -264,13 +666,26 @@ var Contacts = class {
|
|
|
264
666
|
if (options?.cursor) params.cursor = options.cursor;
|
|
265
667
|
return this.client.get(`/api/v1/contacts/${encodeURIComponent(id)}/activities`, params);
|
|
266
668
|
}
|
|
267
|
-
/**
|
|
268
|
-
|
|
269
|
-
|
|
669
|
+
/**
|
|
670
|
+
* Add a note to a contact's timeline.
|
|
671
|
+
*
|
|
672
|
+
* Automatically idempotent: nothing about a note is unique, so an unkeyed
|
|
673
|
+
* retry appends the same text to the timeline twice. Supply
|
|
674
|
+
* `options.idempotencyKey` to deduplicate across your OWN retries too.
|
|
675
|
+
*/
|
|
676
|
+
async addNote(id, input, options) {
|
|
677
|
+
return this.client.postOnce(`/api/v1/contacts/${encodeURIComponent(id)}/notes`, input, options);
|
|
270
678
|
}
|
|
271
|
-
/**
|
|
272
|
-
|
|
273
|
-
|
|
679
|
+
/**
|
|
680
|
+
* Bulk import contacts (max 500). Duplicates are skipped.
|
|
681
|
+
*
|
|
682
|
+
* Automatically idempotent: the import is processed in chunks, so a retry
|
|
683
|
+
* after a partial failure re-walks the whole batch and reports `added` /
|
|
684
|
+
* `skipped` counts for a run that was not the first. Supply
|
|
685
|
+
* `options.idempotencyKey` to deduplicate across your OWN retries too.
|
|
686
|
+
*/
|
|
687
|
+
async import(contacts, options) {
|
|
688
|
+
return this.client.postOnce("/api/v1/contacts/import", { contacts }, options);
|
|
274
689
|
}
|
|
275
690
|
};
|
|
276
691
|
|
|
@@ -289,9 +704,15 @@ var Deals = class {
|
|
|
289
704
|
if (options?.search) params.search = options.search;
|
|
290
705
|
return this.client.get("/api/v1/deals", params);
|
|
291
706
|
}
|
|
292
|
-
/**
|
|
293
|
-
|
|
294
|
-
|
|
707
|
+
/**
|
|
708
|
+
* Create a new deal.
|
|
709
|
+
*
|
|
710
|
+
* Automatically idempotent: nothing about a deal is unique, so an unkeyed
|
|
711
|
+
* retry puts a second identical deal in the pipeline. Supply
|
|
712
|
+
* `options.idempotencyKey` to deduplicate across your OWN retries too.
|
|
713
|
+
*/
|
|
714
|
+
async create(input, options) {
|
|
715
|
+
return this.client.postOnce("/api/v1/deals", input, options);
|
|
295
716
|
}
|
|
296
717
|
/** Get a deal by ID. */
|
|
297
718
|
async get(id) {
|
|
@@ -335,9 +756,18 @@ var Emails = class {
|
|
|
335
756
|
/**
|
|
336
757
|
* Send a transactional email using a template (HTTP 202). The returned `id`
|
|
337
758
|
* is an email send id — poll `emails.get(id)` with it to track delivery.
|
|
759
|
+
*
|
|
760
|
+
* Automatically idempotent: the SDK mints an `Idempotency-Key` so its own
|
|
761
|
+
* 5xx retries replay rather than queue a second copy into someone's inbox —
|
|
762
|
+
* a send that already committed cannot be un-sent. Supply
|
|
763
|
+
* `options.idempotencyKey` to deduplicate across your OWN retries too.
|
|
764
|
+
*
|
|
765
|
+
* `input.idempotency_key` is the older, body-level form of the same control
|
|
766
|
+
* and still takes precedence server-side, so setting it keeps working
|
|
767
|
+
* unchanged.
|
|
338
768
|
*/
|
|
339
|
-
async send(input) {
|
|
340
|
-
return this.client.
|
|
769
|
+
async send(input, options) {
|
|
770
|
+
return this.client.postOnce("/api/v1/emails", input, options);
|
|
341
771
|
}
|
|
342
772
|
/** Get the delivery status of a sent email. */
|
|
343
773
|
async get(id) {
|
|
@@ -346,9 +776,14 @@ var Emails = class {
|
|
|
346
776
|
/**
|
|
347
777
|
* Send the same template to multiple recipients (max 100, HTTP 202). Each
|
|
348
778
|
* queued recipient gets its own send id in `results` for `emails.get(id)`.
|
|
779
|
+
*
|
|
780
|
+
* Automatically idempotent — and this is the call where it matters most: an
|
|
781
|
+
* unkeyed retry of a batch that already committed sends up to 100 duplicate
|
|
782
|
+
* emails. Supply `options.idempotencyKey` to deduplicate across your OWN
|
|
783
|
+
* retries too.
|
|
349
784
|
*/
|
|
350
|
-
async batch(input) {
|
|
351
|
-
return this.client.
|
|
785
|
+
async batch(input, options) {
|
|
786
|
+
return this.client.postOnce("/api/v1/emails/batch", input, options);
|
|
352
787
|
}
|
|
353
788
|
};
|
|
354
789
|
|
|
@@ -358,9 +793,17 @@ var Gdpr = class {
|
|
|
358
793
|
this.client = client;
|
|
359
794
|
}
|
|
360
795
|
client;
|
|
361
|
-
/**
|
|
362
|
-
|
|
363
|
-
|
|
796
|
+
/**
|
|
797
|
+
* Request a workspace data export. Runs asynchronously.
|
|
798
|
+
*
|
|
799
|
+
* Automatically idempotent: the request is recorded and the export is
|
|
800
|
+
* scheduled in one step with no de-duplication of its own, so an unkeyed
|
|
801
|
+
* retry files a second subject-access request and runs a second full export
|
|
802
|
+
* of the workspace. Supply `options.idempotencyKey` to deduplicate across
|
|
803
|
+
* your OWN retries too.
|
|
804
|
+
*/
|
|
805
|
+
async requestExport(options) {
|
|
806
|
+
return this.client.postOnce("/api/v1/gdpr/export", void 0, options);
|
|
364
807
|
}
|
|
365
808
|
/** List all workspace export requests. */
|
|
366
809
|
async listExports() {
|
|
@@ -370,7 +813,13 @@ var Gdpr = class {
|
|
|
370
813
|
async getExport(id) {
|
|
371
814
|
return this.client.get(`/api/v1/gdpr/exports/${encodeURIComponent(id)}`);
|
|
372
815
|
}
|
|
373
|
-
/**
|
|
816
|
+
/**
|
|
817
|
+
* Record a GDPR consent decision for a contact by email.
|
|
818
|
+
*
|
|
819
|
+
* Deliberately unkeyed: a decision is stored once per
|
|
820
|
+
* (workspace, email, consent type) and overwritten in place, so re-sending
|
|
821
|
+
* the same body reaches the same state and returns the same record id.
|
|
822
|
+
*/
|
|
374
823
|
async recordConsent(input) {
|
|
375
824
|
return this.client.post("/api/v1/gdpr/consent", input);
|
|
376
825
|
}
|
|
@@ -378,7 +827,15 @@ var Gdpr = class {
|
|
|
378
827
|
async getConsent(email) {
|
|
379
828
|
return this.client.get(`/api/v1/gdpr/consent/${encodeURIComponent(email)}`);
|
|
380
829
|
}
|
|
381
|
-
/**
|
|
830
|
+
/**
|
|
831
|
+
* Record cookie consent from an external site (legacy endpoint).
|
|
832
|
+
*
|
|
833
|
+
* Deliberately unkeyed: this legacy route predates the versioned API and
|
|
834
|
+
* does not run the `Idempotency-Key` machinery, so a key here would be a
|
|
835
|
+
* header that changes nothing while implying a guarantee the endpoint cannot
|
|
836
|
+
* make. Treat a failed call as "unknown" and re-send only if a missing
|
|
837
|
+
* consent log matters more to you than a duplicate one.
|
|
838
|
+
*/
|
|
382
839
|
async cookieConsent(input) {
|
|
383
840
|
return this.client.post("/api/cookie-consent", input);
|
|
384
841
|
}
|
|
@@ -386,10 +843,12 @@ var Gdpr = class {
|
|
|
386
843
|
|
|
387
844
|
// src/resources/helpdesk.ts
|
|
388
845
|
var HelpdeskConversations = class {
|
|
389
|
-
constructor(client) {
|
|
846
|
+
constructor(client, confirmer) {
|
|
390
847
|
this.client = client;
|
|
848
|
+
this.confirmer = confirmer;
|
|
391
849
|
}
|
|
392
850
|
client;
|
|
851
|
+
confirmer;
|
|
393
852
|
/** List/search conversations with cursor-based pagination and optional filters. */
|
|
394
853
|
async list(options) {
|
|
395
854
|
const params = {};
|
|
@@ -408,10 +867,15 @@ var HelpdeskConversations = class {
|
|
|
408
867
|
}
|
|
409
868
|
/** Update a conversation's status and/or assignee (pass `assignee_user_id: null` to unassign). */
|
|
410
869
|
async update(id, input, options) {
|
|
870
|
+
const resolved = await this.confirmer.prepare(
|
|
871
|
+
{ capabilityId: "helpdesk.conversation.update.execute", body: input },
|
|
872
|
+
{ id },
|
|
873
|
+
options
|
|
874
|
+
);
|
|
411
875
|
return this.client.patch(
|
|
412
876
|
`/api/v1/helpdesk/conversations/${encodeURIComponent(id)}`,
|
|
413
877
|
input,
|
|
414
|
-
|
|
878
|
+
resolved
|
|
415
879
|
);
|
|
416
880
|
}
|
|
417
881
|
/** Read a conversation's messages with cursor-based pagination. */
|
|
@@ -426,26 +890,42 @@ var HelpdeskConversations = class {
|
|
|
426
890
|
}
|
|
427
891
|
};
|
|
428
892
|
var HelpdeskReplies = class {
|
|
429
|
-
constructor(client) {
|
|
893
|
+
constructor(client, confirmer) {
|
|
430
894
|
this.client = client;
|
|
895
|
+
this.confirmer = confirmer;
|
|
431
896
|
}
|
|
432
897
|
client;
|
|
898
|
+
confirmer;
|
|
433
899
|
/**
|
|
434
900
|
* Send an operator reply or internal note. Returns HTTP 201.
|
|
435
901
|
*
|
|
436
|
-
*
|
|
437
|
-
*
|
|
902
|
+
* Automatically idempotent: a reply is a message to a real person, and an
|
|
903
|
+
* unkeyed retry sends it to them twice. The key the confirmer chose is the
|
|
904
|
+
* key that goes out — a capability confirmation is bound to its idempotency
|
|
905
|
+
* key, so minting a fresh one here would invalidate the confirmation.
|
|
906
|
+
*
|
|
907
|
+
* Pass `options.idempotencyKey` to deduplicate across your OWN retries too.
|
|
908
|
+
* It is REQUIRED for capability-scoped tokens, which need it paired with a
|
|
909
|
+
* `capabilityConfirmation` — a generated key satisfies the pairing's key
|
|
910
|
+
* half only; the confirmation is still yours to supply (or to let
|
|
911
|
+
* `autoConfirm` mint).
|
|
438
912
|
*/
|
|
439
913
|
async create(input, options) {
|
|
440
|
-
|
|
914
|
+
const resolved = await this.confirmer.prepare(
|
|
915
|
+
{ capabilityId: "helpdesk.conversation.reply.execute", body: input },
|
|
916
|
+
void 0,
|
|
917
|
+
options
|
|
918
|
+
);
|
|
919
|
+
return this.client.postOnce("/api/v1/helpdesk/replies", input, resolved);
|
|
441
920
|
}
|
|
442
921
|
};
|
|
443
922
|
var Helpdesk = class {
|
|
444
923
|
conversations;
|
|
445
924
|
replies;
|
|
446
|
-
constructor(client) {
|
|
447
|
-
|
|
448
|
-
this.
|
|
925
|
+
constructor(client, confirmer) {
|
|
926
|
+
const resolved = confirmer ?? new CapabilityConfirmer(new CapabilityConfirmations(client));
|
|
927
|
+
this.conversations = new HelpdeskConversations(client, resolved);
|
|
928
|
+
this.replies = new HelpdeskReplies(client, resolved);
|
|
449
929
|
}
|
|
450
930
|
};
|
|
451
931
|
|
|
@@ -464,9 +944,15 @@ var Posts = class {
|
|
|
464
944
|
if (options?.type) params.type = options.type;
|
|
465
945
|
return this.client.get("/api/v1/posts", params);
|
|
466
946
|
}
|
|
467
|
-
/**
|
|
468
|
-
|
|
469
|
-
|
|
947
|
+
/**
|
|
948
|
+
* Create a new post with content and target channels.
|
|
949
|
+
*
|
|
950
|
+
* Automatically idempotent: the SDK mints an `Idempotency-Key` so its own
|
|
951
|
+
* 5xx retries replay rather than draft the post twice. Supply
|
|
952
|
+
* `options.idempotencyKey` to deduplicate across your OWN retries too.
|
|
953
|
+
*/
|
|
954
|
+
async create(input, options) {
|
|
955
|
+
return this.client.postOnce("/api/v1/posts", input, options);
|
|
470
956
|
}
|
|
471
957
|
/** Get a post by ID, including its per-channel variants. */
|
|
472
958
|
async get(id) {
|
|
@@ -480,11 +966,26 @@ var Posts = class {
|
|
|
480
966
|
async remove(id) {
|
|
481
967
|
return this.client.delete(`/api/v1/posts/${encodeURIComponent(id)}`);
|
|
482
968
|
}
|
|
483
|
-
/**
|
|
969
|
+
/**
|
|
970
|
+
* Schedule a post for future publication.
|
|
971
|
+
*
|
|
972
|
+
* Deliberately unkeyed: re-sending the same `scheduled_at` for an
|
|
973
|
+
* already-scheduled post returns the original `workflow_id` rather than
|
|
974
|
+
* starting a second one, so a retried schedule cannot double-publish. A
|
|
975
|
+
* *different* time is rejected — unschedule first.
|
|
976
|
+
*/
|
|
484
977
|
async schedule(id, input) {
|
|
485
978
|
return this.client.post(`/api/v1/posts/${encodeURIComponent(id)}/schedule`, input);
|
|
486
979
|
}
|
|
487
|
-
/**
|
|
980
|
+
/**
|
|
981
|
+
* Publish a post immediately to all target channels.
|
|
982
|
+
*
|
|
983
|
+
* Deliberately unkeyed: publishing moves the post out of the set of statuses
|
|
984
|
+
* that may be published, so the retry of a publish that already committed is
|
|
985
|
+
* refused rather than posting a second time. It is refused with a 400 though
|
|
986
|
+
* — treat an error here as "check the post's status", not as "nothing
|
|
987
|
+
* happened".
|
|
988
|
+
*/
|
|
488
989
|
async publish(id) {
|
|
489
990
|
return this.client.post(`/api/v1/posts/${encodeURIComponent(id)}/publish`);
|
|
490
991
|
}
|
|
@@ -494,12 +995,76 @@ var Posts = class {
|
|
|
494
995
|
}
|
|
495
996
|
};
|
|
496
997
|
|
|
998
|
+
// src/resources/scan.ts
|
|
999
|
+
var sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
1000
|
+
var Scan = class {
|
|
1001
|
+
constructor(client) {
|
|
1002
|
+
this.client = client;
|
|
1003
|
+
}
|
|
1004
|
+
client;
|
|
1005
|
+
/**
|
|
1006
|
+
* Queue a scan. Provide exactly one of `url`, `orgnr`, or `name`.
|
|
1007
|
+
* Runs asynchronously — poll with `get()` or use `waitForResult()`.
|
|
1008
|
+
*
|
|
1009
|
+
* Automatically idempotent: a scan job is queued the moment it is created,
|
|
1010
|
+
* so an unkeyed retry starts a second crawl of the same site and returns an
|
|
1011
|
+
* id for a job that duplicates one already running. Supply
|
|
1012
|
+
* `options.idempotencyKey` to deduplicate across your OWN retries too.
|
|
1013
|
+
*
|
|
1014
|
+
* @throws Error before any request when zero or several selectors are set —
|
|
1015
|
+
* the server would reject the body anyway; failing locally is clearer.
|
|
1016
|
+
*/
|
|
1017
|
+
async create(input, options) {
|
|
1018
|
+
const entries = ["url", "orgnr", "name"].filter(
|
|
1019
|
+
(key2) => input[key2] !== void 0 && input[key2] !== ""
|
|
1020
|
+
);
|
|
1021
|
+
if (entries.length !== 1) {
|
|
1022
|
+
throw new Error("scan.create requires exactly one of url, orgnr, or name");
|
|
1023
|
+
}
|
|
1024
|
+
const key = entries[0];
|
|
1025
|
+
return this.client.postOnce("/api/v1/scan", { [key]: input[key] }, options);
|
|
1026
|
+
}
|
|
1027
|
+
/** Get a scan job's status and, once done, its findings payload. */
|
|
1028
|
+
async get(id) {
|
|
1029
|
+
return this.client.get(`/api/v1/scan/${encodeURIComponent(id)}`);
|
|
1030
|
+
}
|
|
1031
|
+
/** Search the Norwegian company registry by name (typeahead, top 5 hits). */
|
|
1032
|
+
async companies(q) {
|
|
1033
|
+
return this.client.get("/api/v1/scan/companies", { q });
|
|
1034
|
+
}
|
|
1035
|
+
/**
|
|
1036
|
+
* Poll a scan until it settles. Resolves with the job for both `done` and
|
|
1037
|
+
* `failed` (check `job.error`); throws only when the deadline passes while
|
|
1038
|
+
* the scan is still pending/running.
|
|
1039
|
+
*/
|
|
1040
|
+
async waitForResult(id, options = {}) {
|
|
1041
|
+
const rawInterval = options.intervalMs ?? 2500;
|
|
1042
|
+
const rawTimeout = options.timeoutMs ?? 12e4;
|
|
1043
|
+
const intervalMs = Number.isFinite(rawInterval) && rawInterval > 0 ? rawInterval : 2500;
|
|
1044
|
+
const timeoutMs = Number.isFinite(rawTimeout) ? rawTimeout : 12e4;
|
|
1045
|
+
const deadline = Date.now() + timeoutMs;
|
|
1046
|
+
let lastStatus = "pending";
|
|
1047
|
+
for (; ; ) {
|
|
1048
|
+
const { data } = await this.get(id);
|
|
1049
|
+
if (data.status === "done" || data.status === "failed") return data;
|
|
1050
|
+
lastStatus = data.status;
|
|
1051
|
+
const remaining = deadline - Date.now();
|
|
1052
|
+
if (remaining <= 0) break;
|
|
1053
|
+
await sleep(Math.min(intervalMs, remaining));
|
|
1054
|
+
if (Date.now() >= deadline) break;
|
|
1055
|
+
}
|
|
1056
|
+
throw new Error(`Scan ${id} timed out after ${timeoutMs}ms (status: ${lastStatus})`);
|
|
1057
|
+
}
|
|
1058
|
+
};
|
|
1059
|
+
|
|
497
1060
|
// src/resources/webhooks.ts
|
|
498
1061
|
var Webhooks = class {
|
|
499
|
-
constructor(client) {
|
|
1062
|
+
constructor(client, confirmer) {
|
|
500
1063
|
this.client = client;
|
|
1064
|
+
this.confirmer = confirmer ?? new CapabilityConfirmer(new CapabilityConfirmations(client));
|
|
501
1065
|
}
|
|
502
1066
|
client;
|
|
1067
|
+
confirmer;
|
|
503
1068
|
/** List all webhook endpoints in the workspace. */
|
|
504
1069
|
async list() {
|
|
505
1070
|
return this.client.get("/api/v1/webhooks");
|
|
@@ -515,9 +1080,21 @@ var Webhooks = class {
|
|
|
515
1080
|
* `secret` is typed optional because an idempotent replay (retrying with the
|
|
516
1081
|
* same `Idempotency-Key`, `X-Idempotent-Replayed: true`) returns the existing
|
|
517
1082
|
* endpoint WITHOUT the secret — handle that case (rotate if you lost it).
|
|
1083
|
+
*
|
|
1084
|
+
* Automatically idempotent: a duplicate endpoint is not a stray row, it is a
|
|
1085
|
+
* second copy of every future delivery to the same URL, forever. The key the
|
|
1086
|
+
* confirmer chose is the key that goes out — a capability confirmation is
|
|
1087
|
+
* bound to its idempotency key, so minting a fresh one here would invalidate
|
|
1088
|
+
* the confirmation. That the SDK now always sends a key is also what makes
|
|
1089
|
+
* the replay-without-secret case above reachable on a plain 5xx retry.
|
|
518
1090
|
*/
|
|
519
1091
|
async create(input, options) {
|
|
520
|
-
|
|
1092
|
+
const resolved = await this.confirmer.prepare(
|
|
1093
|
+
{ capabilityId: "helpdesk.webhook.create.execute", body: input },
|
|
1094
|
+
void 0,
|
|
1095
|
+
options
|
|
1096
|
+
);
|
|
1097
|
+
return this.client.postOnce("/api/v1/webhooks", input, resolved);
|
|
521
1098
|
}
|
|
522
1099
|
/** Get a webhook endpoint by ID. */
|
|
523
1100
|
async get(id) {
|
|
@@ -525,7 +1102,12 @@ var Webhooks = class {
|
|
|
525
1102
|
}
|
|
526
1103
|
/** Update a webhook endpoint (name, url, event types, filters, enabled). */
|
|
527
1104
|
async update(id, input, options) {
|
|
528
|
-
|
|
1105
|
+
const resolved = await this.confirmer.prepare(
|
|
1106
|
+
{ capabilityId: "helpdesk.webhook.update.execute", body: input },
|
|
1107
|
+
{ id },
|
|
1108
|
+
options
|
|
1109
|
+
);
|
|
1110
|
+
return this.client.patch(`/api/v1/webhooks/${encodeURIComponent(id)}`, input, resolved);
|
|
529
1111
|
}
|
|
530
1112
|
/**
|
|
531
1113
|
* Permanently delete a webhook endpoint (stops all outbound deliveries).
|
|
@@ -534,7 +1116,12 @@ var Webhooks = class {
|
|
|
534
1116
|
* grants on this route. API keys with legacy scopes may omit it.
|
|
535
1117
|
*/
|
|
536
1118
|
async delete(id, options) {
|
|
537
|
-
|
|
1119
|
+
const resolved = await this.confirmer.prepare(
|
|
1120
|
+
{ capabilityId: "helpdesk.webhook.delete.execute", body: void 0 },
|
|
1121
|
+
{ id },
|
|
1122
|
+
options
|
|
1123
|
+
);
|
|
1124
|
+
return this.client.delete(`/api/v1/webhooks/${encodeURIComponent(id)}`, resolved);
|
|
538
1125
|
}
|
|
539
1126
|
/** List recent deliveries for an endpoint (most recent first). */
|
|
540
1127
|
async deliveries(id, options) {
|
|
@@ -542,7 +1129,14 @@ var Webhooks = class {
|
|
|
542
1129
|
if (options?.limit !== void 0) params.limit = String(options.limit);
|
|
543
1130
|
return this.client.get(`/api/v1/webhooks/${encodeURIComponent(id)}/deliveries`, params);
|
|
544
1131
|
}
|
|
545
|
-
/**
|
|
1132
|
+
/**
|
|
1133
|
+
* Queue a signed `test.ping` delivery to the endpoint. Returns HTTP 202.
|
|
1134
|
+
*
|
|
1135
|
+
* Deliberately unkeyed: a duplicate ping is the one duplicate that costs
|
|
1136
|
+
* nothing. Real deliveries are retried too, so any endpoint worth pointing at
|
|
1137
|
+
* already tolerates receiving the same event twice — that is what this call
|
|
1138
|
+
* exists to prove.
|
|
1139
|
+
*/
|
|
546
1140
|
async test(id) {
|
|
547
1141
|
return this.client.post(`/api/v1/webhooks/${encodeURIComponent(id)}/test`);
|
|
548
1142
|
}
|
|
@@ -632,6 +1226,8 @@ async function verifyWebhookSignature(input) {
|
|
|
632
1226
|
|
|
633
1227
|
// src/index.ts
|
|
634
1228
|
var Medal = class {
|
|
1229
|
+
bookings;
|
|
1230
|
+
capabilityConfirmations;
|
|
635
1231
|
channels;
|
|
636
1232
|
emails;
|
|
637
1233
|
contacts;
|
|
@@ -639,6 +1235,7 @@ var Medal = class {
|
|
|
639
1235
|
gdpr;
|
|
640
1236
|
helpdesk;
|
|
641
1237
|
posts;
|
|
1238
|
+
scan;
|
|
642
1239
|
webhooks;
|
|
643
1240
|
workspaces;
|
|
644
1241
|
constructor(token, options) {
|
|
@@ -654,14 +1251,21 @@ var Medal = class {
|
|
|
654
1251
|
timeout: options?.timeout ?? 3e4,
|
|
655
1252
|
userAgent: "medalsocial-sdk/1.0.0 (+https://github.com/Medal-Social/MedalSocial)"
|
|
656
1253
|
});
|
|
657
|
-
this.
|
|
1254
|
+
this.capabilityConfirmations = new CapabilityConfirmations(client);
|
|
1255
|
+
const confirmer = new CapabilityConfirmer(
|
|
1256
|
+
this.capabilityConfirmations,
|
|
1257
|
+
options?.autoConfirmCapabilities
|
|
1258
|
+
);
|
|
1259
|
+
this.bookings = new Bookings(client);
|
|
1260
|
+
this.channels = new Channels(client, confirmer);
|
|
658
1261
|
this.emails = new Emails(client);
|
|
659
1262
|
this.contacts = new Contacts(client);
|
|
660
1263
|
this.deals = new Deals(client);
|
|
661
1264
|
this.gdpr = new Gdpr(client);
|
|
662
|
-
this.helpdesk = new Helpdesk(client);
|
|
1265
|
+
this.helpdesk = new Helpdesk(client, confirmer);
|
|
663
1266
|
this.posts = new Posts(client);
|
|
664
|
-
this.
|
|
1267
|
+
this.scan = new Scan(client);
|
|
1268
|
+
this.webhooks = new Webhooks(client, confirmer);
|
|
665
1269
|
this.workspaces = new Workspaces(client);
|
|
666
1270
|
}
|
|
667
1271
|
};
|
|
@@ -672,6 +1276,11 @@ var src_default = Medal;
|
|
|
672
1276
|
// Annotate the CommonJS export names for ESM import in node:
|
|
673
1277
|
0 && (module.exports = {
|
|
674
1278
|
BaseClient,
|
|
1279
|
+
Bookings,
|
|
1280
|
+
CAPABILITY_IDS,
|
|
1281
|
+
CAPABILITY_ROUTES,
|
|
1282
|
+
CapabilityConfirmations,
|
|
1283
|
+
CapabilityConfirmer,
|
|
675
1284
|
Channels,
|
|
676
1285
|
Contacts,
|
|
677
1286
|
DEFAULT_WEBHOOK_TOLERANCE_MS,
|
|
@@ -682,6 +1291,7 @@ var src_default = Medal;
|
|
|
682
1291
|
Medal,
|
|
683
1292
|
MedalApiError,
|
|
684
1293
|
Posts,
|
|
1294
|
+
Scan,
|
|
685
1295
|
WebhookVerificationError,
|
|
686
1296
|
Webhooks,
|
|
687
1297
|
Workspaces,
|