@medalsocial/sdk 1.5.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,12 +233,37 @@ 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
+
126
259
  // src/resources/channels.ts
127
260
  var ChannelConnectLinks = class {
128
- constructor(client) {
261
+ constructor(client, confirmer) {
129
262
  this.client = client;
263
+ this.confirmer = confirmer;
130
264
  }
131
265
  client;
266
+ confirmer;
132
267
  /**
133
268
  * Mint a single-use hosted connect link. Returns HTTP 201.
134
269
  *
@@ -141,28 +276,64 @@ var ChannelConnectLinks = class {
141
276
  * need the workspace `admin` role.
142
277
  */
143
278
  async create(input, options) {
144
- return this.client.post("/api/v1/channels/connect-links", 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);
145
285
  }
146
- /** List the workspace's connect links (tokens are never returned). */
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
+ */
147
297
  async list(options) {
148
298
  const params = {};
299
+ if (options?.limit !== void 0) params.limit = String(options.limit);
300
+ if (options?.cursor) params.cursor = options.cursor;
149
301
  if (options?.channel_type) params.channel_type = options.channel_type;
150
302
  if (options?.status) params.status = options.status;
151
303
  return this.client.get("/api/v1/channels/connect-links", params);
152
304
  }
153
305
  /** Revoke a pending connect link so it can no longer be consumed. */
154
306
  async revoke(id, options) {
155
- return this.client.delete(`/api/v1/channels/connect-links/${encodeURIComponent(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);
156
313
  }
157
314
  };
158
315
  var ChannelConnections = class {
159
- constructor(client) {
316
+ constructor(client, confirmer) {
160
317
  this.client = client;
318
+ this.confirmer = confirmer;
161
319
  }
162
320
  client;
163
- /** List the workspace's channel connections (generic, channel-agnostic shape). */
164
- async list() {
165
- return this.client.get("/api/v1/channels/connections");
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);
166
337
  }
167
338
  /**
168
339
  * Disconnect a connected channel account (best-effort platform logout, then
@@ -170,15 +341,21 @@ var ChannelConnections = class {
170
341
  * `reason: "api_disconnect"` if the account was previously connected.
171
342
  */
172
343
  async disconnect(id, options) {
173
- return this.client.delete(`/api/v1/channels/connections/${encodeURIComponent(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);
174
350
  }
175
351
  };
176
352
  var Channels = class {
177
353
  connectLinks;
178
354
  connections;
179
- constructor(client) {
180
- this.connectLinks = new ChannelConnectLinks(client);
181
- this.connections = new ChannelConnections(client);
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);
182
359
  }
183
360
  };
184
361
 
@@ -344,10 +521,12 @@ var Gdpr = class {
344
521
 
345
522
  // src/resources/helpdesk.ts
346
523
  var HelpdeskConversations = class {
347
- constructor(client) {
524
+ constructor(client, confirmer) {
348
525
  this.client = client;
526
+ this.confirmer = confirmer;
349
527
  }
350
528
  client;
529
+ confirmer;
351
530
  /** List/search conversations with cursor-based pagination and optional filters. */
352
531
  async list(options) {
353
532
  const params = {};
@@ -366,10 +545,15 @@ var HelpdeskConversations = class {
366
545
  }
367
546
  /** Update a conversation's status and/or assignee (pass `assignee_user_id: null` to unassign). */
368
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
+ );
369
553
  return this.client.patch(
370
554
  `/api/v1/helpdesk/conversations/${encodeURIComponent(id)}`,
371
555
  input,
372
- options
556
+ resolved
373
557
  );
374
558
  }
375
559
  /** Read a conversation's messages with cursor-based pagination. */
@@ -384,10 +568,12 @@ var HelpdeskConversations = class {
384
568
  }
385
569
  };
386
570
  var HelpdeskReplies = class {
387
- constructor(client) {
571
+ constructor(client, confirmer) {
388
572
  this.client = client;
573
+ this.confirmer = confirmer;
389
574
  }
390
575
  client;
576
+ confirmer;
391
577
  /**
392
578
  * Send an operator reply or internal note. Returns HTTP 201.
393
579
  *
@@ -395,15 +581,21 @@ var HelpdeskReplies = class {
395
581
  * messages — it is REQUIRED for capability-scoped tokens.
396
582
  */
397
583
  async create(input, options) {
398
- 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);
399
590
  }
400
591
  };
401
592
  var Helpdesk = class {
402
593
  conversations;
403
594
  replies;
404
- constructor(client) {
405
- this.conversations = new HelpdeskConversations(client);
406
- 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);
407
599
  }
408
600
  };
409
601
 
@@ -452,12 +644,71 @@ var Posts = class {
452
644
  }
453
645
  };
454
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
+
455
704
  // src/resources/webhooks.ts
456
705
  var Webhooks = class {
457
- constructor(client) {
706
+ constructor(client, confirmer) {
458
707
  this.client = client;
708
+ this.confirmer = confirmer ?? new CapabilityConfirmer(new CapabilityConfirmations(client));
459
709
  }
460
710
  client;
711
+ confirmer;
461
712
  /** List all webhook endpoints in the workspace. */
462
713
  async list() {
463
714
  return this.client.get("/api/v1/webhooks");
@@ -475,7 +726,12 @@ var Webhooks = class {
475
726
  * endpoint WITHOUT the secret — handle that case (rotate if you lost it).
476
727
  */
477
728
  async create(input, options) {
478
- 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);
479
735
  }
480
736
  /** Get a webhook endpoint by ID. */
481
737
  async get(id) {
@@ -483,7 +739,12 @@ var Webhooks = class {
483
739
  }
484
740
  /** Update a webhook endpoint (name, url, event types, filters, enabled). */
485
741
  async update(id, input, options) {
486
- 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);
487
748
  }
488
749
  /**
489
750
  * Permanently delete a webhook endpoint (stops all outbound deliveries).
@@ -492,7 +753,12 @@ var Webhooks = class {
492
753
  * grants on this route. API keys with legacy scopes may omit it.
493
754
  */
494
755
  async delete(id, options) {
495
- 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);
496
762
  }
497
763
  /** List recent deliveries for an endpoint (most recent first). */
498
764
  async deliveries(id, options) {
@@ -590,6 +856,7 @@ async function verifyWebhookSignature(input) {
590
856
 
591
857
  // src/index.ts
592
858
  var Medal = class {
859
+ capabilityConfirmations;
593
860
  channels;
594
861
  emails;
595
862
  contacts;
@@ -597,6 +864,7 @@ var Medal = class {
597
864
  gdpr;
598
865
  helpdesk;
599
866
  posts;
867
+ scan;
600
868
  webhooks;
601
869
  workspaces;
602
870
  constructor(token, options) {
@@ -612,14 +880,20 @@ var Medal = class {
612
880
  timeout: options?.timeout ?? 3e4,
613
881
  userAgent: "medalsocial-sdk/1.0.0 (+https://github.com/Medal-Social/MedalSocial)"
614
882
  });
615
- this.channels = new Channels(client);
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);
616
889
  this.emails = new Emails(client);
617
890
  this.contacts = new Contacts(client);
618
891
  this.deals = new Deals(client);
619
892
  this.gdpr = new Gdpr(client);
620
- this.helpdesk = new Helpdesk(client);
893
+ this.helpdesk = new Helpdesk(client, confirmer);
621
894
  this.posts = new Posts(client);
622
- this.webhooks = new Webhooks(client);
895
+ this.scan = new Scan(client);
896
+ this.webhooks = new Webhooks(client, confirmer);
623
897
  this.workspaces = new Workspaces(client);
624
898
  }
625
899
  };
@@ -629,6 +903,10 @@ function createMedalClient(apiKey, options) {
629
903
  var src_default = Medal;
630
904
  export {
631
905
  BaseClient,
906
+ CAPABILITY_IDS,
907
+ CAPABILITY_ROUTES,
908
+ CapabilityConfirmations,
909
+ CapabilityConfirmer,
632
910
  Channels,
633
911
  Contacts,
634
912
  DEFAULT_WEBHOOK_TOLERANCE_MS,
@@ -639,6 +917,7 @@ export {
639
917
  Medal,
640
918
  MedalApiError,
641
919
  Posts,
920
+ Scan,
642
921
  WebhookVerificationError,
643
922
  Webhooks,
644
923
  Workspaces,