@medalsocial/sdk 1.4.0 → 1.6.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.
@@ -1,3 +1,113 @@
1
+ // src/types/capabilities.ts
2
+ var CAPABILITY_IDS = [
3
+ "channel.connect_link.create.execute",
4
+ "channel.connect_link.revoke.execute",
5
+ "channel.connection.disconnect.execute",
6
+ "helpdesk.conversation.reply.execute",
7
+ "helpdesk.conversation.update.execute",
8
+ "helpdesk.webhook.create.execute",
9
+ "helpdesk.webhook.update.execute",
10
+ "helpdesk.webhook.delete.execute"
11
+ ];
12
+ var CAPABILITY_ROUTES = {
13
+ "channel.connect_link.create.execute": {
14
+ method: "POST",
15
+ path_template: "/api/v1/channels/connect-links"
16
+ },
17
+ "channel.connect_link.revoke.execute": {
18
+ method: "DELETE",
19
+ path_template: "/api/v1/channels/connect-links/{id}"
20
+ },
21
+ "channel.connection.disconnect.execute": {
22
+ method: "DELETE",
23
+ path_template: "/api/v1/channels/connections/{id}"
24
+ },
25
+ "helpdesk.conversation.reply.execute": {
26
+ method: "POST",
27
+ path_template: "/api/v1/helpdesk/replies"
28
+ },
29
+ "helpdesk.conversation.update.execute": {
30
+ method: "PATCH",
31
+ path_template: "/api/v1/helpdesk/conversations/{id}"
32
+ },
33
+ "helpdesk.webhook.create.execute": {
34
+ method: "POST",
35
+ path_template: "/api/v1/webhooks"
36
+ },
37
+ "helpdesk.webhook.update.execute": {
38
+ method: "PATCH",
39
+ path_template: "/api/v1/webhooks/{id}"
40
+ },
41
+ "helpdesk.webhook.delete.execute": {
42
+ method: "DELETE",
43
+ path_template: "/api/v1/webhooks/{id}"
44
+ }
45
+ };
46
+
47
+ // src/capability-confirmer.ts
48
+ function newIdempotencyKey() {
49
+ const cryptoRef = globalThis.crypto;
50
+ if (typeof cryptoRef?.randomUUID === "function") {
51
+ return cryptoRef.randomUUID();
52
+ }
53
+ return `idem_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 12)}`;
54
+ }
55
+ function resolvePath(template, pathParams) {
56
+ return template.replace(/\{([^}/]+)\}/g, (_match, name) => {
57
+ const value = pathParams?.[name];
58
+ return value === void 0 ? `{${name}}` : encodeURIComponent(String(value));
59
+ });
60
+ }
61
+ var CapabilityConfirmer = class {
62
+ constructor(confirmations, defaults) {
63
+ this.confirmations = confirmations;
64
+ this.defaults = defaults;
65
+ }
66
+ confirmations;
67
+ defaults;
68
+ /**
69
+ * Return the request options to use for a confirmable write, minting the
70
+ * idempotency key and confirmation token first when auto-confirm is active.
71
+ *
72
+ * `body` is the pending request payload (`undefined` for `DELETE` routes).
73
+ * It is handed to the `previewSummary` callback by reference so the summary
74
+ * can describe the specific action, not just the route — it is the caller's
75
+ * own payload, so it is passed through unmodified and unredacted.
76
+ */
77
+ async prepare(request, pathParams, options) {
78
+ const auto = options?.autoConfirm === false ? void 0 : options?.autoConfirm ?? this.defaults;
79
+ if (!auto) return options;
80
+ if (options?.idempotencyKey && options?.capabilityConfirmation) return options;
81
+ const route = CAPABILITY_ROUTES[request.capabilityId];
82
+ const idempotencyKey = options?.idempotencyKey ?? newIdempotencyKey();
83
+ const path = resolvePath(route.path_template, pathParams);
84
+ const previewSummary = auto.previewSummary({
85
+ ...request,
86
+ method: route.method,
87
+ path,
88
+ ...pathParams ? { pathParams } : {},
89
+ idempotencyKey
90
+ });
91
+ if (typeof previewSummary !== "string" || previewSummary.trim() === "") {
92
+ throw new Error(
93
+ `autoConfirm.previewSummary must return a non-empty summary for ${request.capabilityId}. The summary is the audit record of what your user approved \u2014 refusing to assert user_approved: true without one.`
94
+ );
95
+ }
96
+ const { data } = await this.confirmations.create({
97
+ capability_id: request.capabilityId,
98
+ ...pathParams ? { path_params: pathParams } : {},
99
+ idempotency_key: idempotencyKey,
100
+ preview_summary: previewSummary,
101
+ user_approved: true
102
+ });
103
+ return {
104
+ ...options,
105
+ idempotencyKey,
106
+ capabilityConfirmation: data.confirmation_token
107
+ };
108
+ }
109
+ };
110
+
1
111
  // src/types/common.ts
2
112
  var MedalApiError = class extends Error {
3
113
  status;
@@ -123,6 +233,132 @@ var BaseClient = class {
123
233
  }
124
234
  };
125
235
 
236
+ // src/resources/capability-confirmations.ts
237
+ var CapabilityConfirmations = class {
238
+ constructor(client) {
239
+ this.client = client;
240
+ }
241
+ client;
242
+ /**
243
+ * Issue a confirmation token for one pending write.
244
+ *
245
+ * The token is bound to the workspace, the auth subject, the capability's
246
+ * method + path, its required scopes, and `idempotency_key` — so it is
247
+ * usable exactly once, for exactly the write it describes, and expires
248
+ * within 15 minutes.
249
+ *
250
+ * Setting `user_approved: true` asserts that a human on your side approved
251
+ * this specific action. `preview_summary` is what they approved, and is
252
+ * retained for audit — write it for a human reader, not a log parser.
253
+ */
254
+ async create(input) {
255
+ return this.client.post("/api/v1/capability-confirmations", input);
256
+ }
257
+ };
258
+
259
+ // src/resources/channels.ts
260
+ var ChannelConnectLinks = class {
261
+ constructor(client, confirmer) {
262
+ this.client = client;
263
+ this.confirmer = confirmer;
264
+ }
265
+ client;
266
+ confirmer;
267
+ /**
268
+ * Mint a single-use hosted connect link. Returns HTTP 201.
269
+ *
270
+ * **The response's `data.url` contains the one-time link token EXACTLY
271
+ * ONCE.** Send it to the person who should connect their account — an
272
+ * idempotent replay (same `Idempotency-Key`) returns the link WITHOUT
273
+ * `url`, so store it immediately (or revoke and mint a new link if lost).
274
+ *
275
+ * Requires the `channel.connect.manage` scope; OAuth callers additionally
276
+ * need the workspace `admin` role.
277
+ */
278
+ async create(input, options) {
279
+ const resolved = await this.confirmer.prepare(
280
+ { capabilityId: "channel.connect_link.create.execute", body: input },
281
+ void 0,
282
+ options
283
+ );
284
+ return this.client.post("/api/v1/channels/connect-links", input, resolved);
285
+ }
286
+ /**
287
+ * List the workspace's connect links (tokens are never returned), newest
288
+ * first, with cursor-based pagination.
289
+ *
290
+ * `limit` defaults to 50 server-side and is capped at 100. Follow
291
+ * `pagination.next_cursor` while `pagination.has_more` is true.
292
+ *
293
+ * The `channel_type` / `status` filters are applied **within** each page,
294
+ * so a page may hold fewer than `limit` items while `has_more` is still
295
+ * true — drive the loop off `has_more`, never off the item count.
296
+ */
297
+ async list(options) {
298
+ const params = {};
299
+ if (options?.limit !== void 0) params.limit = String(options.limit);
300
+ if (options?.cursor) params.cursor = options.cursor;
301
+ if (options?.channel_type) params.channel_type = options.channel_type;
302
+ if (options?.status) params.status = options.status;
303
+ return this.client.get("/api/v1/channels/connect-links", params);
304
+ }
305
+ /** Revoke a pending connect link so it can no longer be consumed. */
306
+ async revoke(id, options) {
307
+ const resolved = await this.confirmer.prepare(
308
+ { capabilityId: "channel.connect_link.revoke.execute", body: void 0 },
309
+ { id },
310
+ options
311
+ );
312
+ return this.client.delete(`/api/v1/channels/connect-links/${encodeURIComponent(id)}`, resolved);
313
+ }
314
+ };
315
+ var ChannelConnections = class {
316
+ constructor(client, confirmer) {
317
+ this.client = client;
318
+ this.confirmer = confirmer;
319
+ }
320
+ client;
321
+ confirmer;
322
+ /**
323
+ * List the workspace's channel connections (generic, channel-agnostic
324
+ * shape), newest first, with cursor-based pagination.
325
+ *
326
+ * `limit` defaults to 50 server-side and is capped at 100. Follow
327
+ * `pagination.next_cursor` while `pagination.has_more` is true. Rows that
328
+ * are not projectable as connections are dropped within the page, so a page
329
+ * may hold fewer than `limit` items while `has_more` is still true — drive
330
+ * the loop off `has_more`, never off the item count.
331
+ */
332
+ async list(options) {
333
+ const params = {};
334
+ if (options?.limit !== void 0) params.limit = String(options.limit);
335
+ if (options?.cursor) params.cursor = options.cursor;
336
+ return this.client.get("/api/v1/channels/connections", params);
337
+ }
338
+ /**
339
+ * Disconnect a connected channel account (best-effort platform logout, then
340
+ * local revoke). Emits a `helpdesk.channel_disconnected` webhook event with
341
+ * `reason: "api_disconnect"` if the account was previously connected.
342
+ */
343
+ async disconnect(id, options) {
344
+ const resolved = await this.confirmer.prepare(
345
+ { capabilityId: "channel.connection.disconnect.execute", body: void 0 },
346
+ { id },
347
+ options
348
+ );
349
+ return this.client.delete(`/api/v1/channels/connections/${encodeURIComponent(id)}`, resolved);
350
+ }
351
+ };
352
+ var Channels = class {
353
+ connectLinks;
354
+ connections;
355
+ constructor(client, confirmer) {
356
+ const resolved = confirmer ?? new CapabilityConfirmer(new CapabilityConfirmations(client));
357
+ this.connectLinks = new ChannelConnectLinks(client, resolved);
358
+ this.connections = new ChannelConnections(client, resolved);
359
+ }
360
+ };
361
+
126
362
  // src/resources/contacts.ts
127
363
  var Contacts = class {
128
364
  constructor(client) {
@@ -285,10 +521,12 @@ var Gdpr = class {
285
521
 
286
522
  // src/resources/helpdesk.ts
287
523
  var HelpdeskConversations = class {
288
- constructor(client) {
524
+ constructor(client, confirmer) {
289
525
  this.client = client;
526
+ this.confirmer = confirmer;
290
527
  }
291
528
  client;
529
+ confirmer;
292
530
  /** List/search conversations with cursor-based pagination and optional filters. */
293
531
  async list(options) {
294
532
  const params = {};
@@ -307,10 +545,15 @@ var HelpdeskConversations = class {
307
545
  }
308
546
  /** Update a conversation's status and/or assignee (pass `assignee_user_id: null` to unassign). */
309
547
  async update(id, input, options) {
548
+ const resolved = await this.confirmer.prepare(
549
+ { capabilityId: "helpdesk.conversation.update.execute", body: input },
550
+ { id },
551
+ options
552
+ );
310
553
  return this.client.patch(
311
554
  `/api/v1/helpdesk/conversations/${encodeURIComponent(id)}`,
312
555
  input,
313
- options
556
+ resolved
314
557
  );
315
558
  }
316
559
  /** Read a conversation's messages with cursor-based pagination. */
@@ -325,10 +568,12 @@ var HelpdeskConversations = class {
325
568
  }
326
569
  };
327
570
  var HelpdeskReplies = class {
328
- constructor(client) {
571
+ constructor(client, confirmer) {
329
572
  this.client = client;
573
+ this.confirmer = confirmer;
330
574
  }
331
575
  client;
576
+ confirmer;
332
577
  /**
333
578
  * Send an operator reply or internal note. Returns HTTP 201.
334
579
  *
@@ -336,15 +581,21 @@ var HelpdeskReplies = class {
336
581
  * messages — it is REQUIRED for capability-scoped tokens.
337
582
  */
338
583
  async create(input, options) {
339
- return this.client.post("/api/v1/helpdesk/replies", input, options);
584
+ const resolved = await this.confirmer.prepare(
585
+ { capabilityId: "helpdesk.conversation.reply.execute", body: input },
586
+ void 0,
587
+ options
588
+ );
589
+ return this.client.post("/api/v1/helpdesk/replies", input, resolved);
340
590
  }
341
591
  };
342
592
  var Helpdesk = class {
343
593
  conversations;
344
594
  replies;
345
- constructor(client) {
346
- this.conversations = new HelpdeskConversations(client);
347
- this.replies = new HelpdeskReplies(client);
595
+ constructor(client, confirmer) {
596
+ const resolved = confirmer ?? new CapabilityConfirmer(new CapabilityConfirmations(client));
597
+ this.conversations = new HelpdeskConversations(client, resolved);
598
+ this.replies = new HelpdeskReplies(client, resolved);
348
599
  }
349
600
  };
350
601
 
@@ -393,12 +644,71 @@ var Posts = class {
393
644
  }
394
645
  };
395
646
 
647
+ // src/resources/scan.ts
648
+ var sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
649
+ var Scan = class {
650
+ constructor(client) {
651
+ this.client = client;
652
+ }
653
+ client;
654
+ /**
655
+ * Queue a scan. Provide exactly one of `url`, `orgnr`, or `name`.
656
+ * Runs asynchronously — poll with `get()` or use `waitForResult()`.
657
+ *
658
+ * @throws Error before any request when zero or several selectors are set —
659
+ * the server would reject the body anyway; failing locally is clearer.
660
+ */
661
+ async create(input) {
662
+ const entries = ["url", "orgnr", "name"].filter(
663
+ (key2) => input[key2] !== void 0 && input[key2] !== ""
664
+ );
665
+ if (entries.length !== 1) {
666
+ throw new Error("scan.create requires exactly one of url, orgnr, or name");
667
+ }
668
+ const key = entries[0];
669
+ return this.client.post("/api/v1/scan", { [key]: input[key] });
670
+ }
671
+ /** Get a scan job's status and, once done, its findings payload. */
672
+ async get(id) {
673
+ return this.client.get(`/api/v1/scan/${encodeURIComponent(id)}`);
674
+ }
675
+ /** Search the Norwegian company registry by name (typeahead, top 5 hits). */
676
+ async companies(q) {
677
+ return this.client.get("/api/v1/scan/companies", { q });
678
+ }
679
+ /**
680
+ * Poll a scan until it settles. Resolves with the job for both `done` and
681
+ * `failed` (check `job.error`); throws only when the deadline passes while
682
+ * the scan is still pending/running.
683
+ */
684
+ async waitForResult(id, options = {}) {
685
+ const rawInterval = options.intervalMs ?? 2500;
686
+ const rawTimeout = options.timeoutMs ?? 12e4;
687
+ const intervalMs = Number.isFinite(rawInterval) && rawInterval > 0 ? rawInterval : 2500;
688
+ const timeoutMs = Number.isFinite(rawTimeout) ? rawTimeout : 12e4;
689
+ const deadline = Date.now() + timeoutMs;
690
+ let lastStatus = "pending";
691
+ for (; ; ) {
692
+ const { data } = await this.get(id);
693
+ if (data.status === "done" || data.status === "failed") return data;
694
+ lastStatus = data.status;
695
+ const remaining = deadline - Date.now();
696
+ if (remaining <= 0) break;
697
+ await sleep(Math.min(intervalMs, remaining));
698
+ if (Date.now() >= deadline) break;
699
+ }
700
+ throw new Error(`Scan ${id} timed out after ${timeoutMs}ms (status: ${lastStatus})`);
701
+ }
702
+ };
703
+
396
704
  // src/resources/webhooks.ts
397
705
  var Webhooks = class {
398
- constructor(client) {
706
+ constructor(client, confirmer) {
399
707
  this.client = client;
708
+ this.confirmer = confirmer ?? new CapabilityConfirmer(new CapabilityConfirmations(client));
400
709
  }
401
710
  client;
711
+ confirmer;
402
712
  /** List all webhook endpoints in the workspace. */
403
713
  async list() {
404
714
  return this.client.get("/api/v1/webhooks");
@@ -416,7 +726,12 @@ var Webhooks = class {
416
726
  * endpoint WITHOUT the secret — handle that case (rotate if you lost it).
417
727
  */
418
728
  async create(input, options) {
419
- return this.client.post("/api/v1/webhooks", input, options);
729
+ const resolved = await this.confirmer.prepare(
730
+ { capabilityId: "helpdesk.webhook.create.execute", body: input },
731
+ void 0,
732
+ options
733
+ );
734
+ return this.client.post("/api/v1/webhooks", input, resolved);
420
735
  }
421
736
  /** Get a webhook endpoint by ID. */
422
737
  async get(id) {
@@ -424,7 +739,12 @@ var Webhooks = class {
424
739
  }
425
740
  /** Update a webhook endpoint (name, url, event types, filters, enabled). */
426
741
  async update(id, input, options) {
427
- return this.client.patch(`/api/v1/webhooks/${encodeURIComponent(id)}`, input, options);
742
+ const resolved = await this.confirmer.prepare(
743
+ { capabilityId: "helpdesk.webhook.update.execute", body: input },
744
+ { id },
745
+ options
746
+ );
747
+ return this.client.patch(`/api/v1/webhooks/${encodeURIComponent(id)}`, input, resolved);
428
748
  }
429
749
  /**
430
750
  * Permanently delete a webhook endpoint (stops all outbound deliveries).
@@ -433,7 +753,12 @@ var Webhooks = class {
433
753
  * grants on this route. API keys with legacy scopes may omit it.
434
754
  */
435
755
  async delete(id, options) {
436
- return this.client.delete(`/api/v1/webhooks/${encodeURIComponent(id)}`, options);
756
+ const resolved = await this.confirmer.prepare(
757
+ { capabilityId: "helpdesk.webhook.delete.execute", body: void 0 },
758
+ { id },
759
+ options
760
+ );
761
+ return this.client.delete(`/api/v1/webhooks/${encodeURIComponent(id)}`, resolved);
437
762
  }
438
763
  /** List recent deliveries for an endpoint (most recent first). */
439
764
  async deliveries(id, options) {
@@ -531,12 +856,15 @@ async function verifyWebhookSignature(input) {
531
856
 
532
857
  // src/index.ts
533
858
  var Medal = class {
859
+ capabilityConfirmations;
860
+ channels;
534
861
  emails;
535
862
  contacts;
536
863
  deals;
537
864
  gdpr;
538
865
  helpdesk;
539
866
  posts;
867
+ scan;
540
868
  webhooks;
541
869
  workspaces;
542
870
  constructor(token, options) {
@@ -552,13 +880,20 @@ var Medal = class {
552
880
  timeout: options?.timeout ?? 3e4,
553
881
  userAgent: "medalsocial-sdk/1.0.0 (+https://github.com/Medal-Social/MedalSocial)"
554
882
  });
883
+ this.capabilityConfirmations = new CapabilityConfirmations(client);
884
+ const confirmer = new CapabilityConfirmer(
885
+ this.capabilityConfirmations,
886
+ options?.autoConfirmCapabilities
887
+ );
888
+ this.channels = new Channels(client, confirmer);
555
889
  this.emails = new Emails(client);
556
890
  this.contacts = new Contacts(client);
557
891
  this.deals = new Deals(client);
558
892
  this.gdpr = new Gdpr(client);
559
- this.helpdesk = new Helpdesk(client);
893
+ this.helpdesk = new Helpdesk(client, confirmer);
560
894
  this.posts = new Posts(client);
561
- this.webhooks = new Webhooks(client);
895
+ this.scan = new Scan(client);
896
+ this.webhooks = new Webhooks(client, confirmer);
562
897
  this.workspaces = new Workspaces(client);
563
898
  }
564
899
  };
@@ -568,6 +903,11 @@ function createMedalClient(apiKey, options) {
568
903
  var src_default = Medal;
569
904
  export {
570
905
  BaseClient,
906
+ CAPABILITY_IDS,
907
+ CAPABILITY_ROUTES,
908
+ CapabilityConfirmations,
909
+ CapabilityConfirmer,
910
+ Channels,
571
911
  Contacts,
572
912
  DEFAULT_WEBHOOK_TOLERANCE_MS,
573
913
  Deals,
@@ -577,6 +917,7 @@ export {
577
917
  Medal,
578
918
  MedalApiError,
579
919
  Posts,
920
+ Scan,
580
921
  WebhookVerificationError,
581
922
  Webhooks,
582
923
  Workspaces,