@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.
package/dist/src/index.js CHANGED
@@ -21,6 +21,10 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
21
21
  var src_exports = {};
22
22
  __export(src_exports, {
23
23
  BaseClient: () => BaseClient,
24
+ CAPABILITY_IDS: () => CAPABILITY_IDS,
25
+ CAPABILITY_ROUTES: () => CAPABILITY_ROUTES,
26
+ CapabilityConfirmations: () => CapabilityConfirmations,
27
+ CapabilityConfirmer: () => CapabilityConfirmer,
24
28
  Channels: () => Channels,
25
29
  Contacts: () => Contacts,
26
30
  DEFAULT_WEBHOOK_TOLERANCE_MS: () => DEFAULT_WEBHOOK_TOLERANCE_MS,
@@ -31,6 +35,7 @@ __export(src_exports, {
31
35
  Medal: () => Medal,
32
36
  MedalApiError: () => MedalApiError,
33
37
  Posts: () => Posts,
38
+ Scan: () => Scan,
34
39
  WebhookVerificationError: () => WebhookVerificationError,
35
40
  Webhooks: () => Webhooks,
36
41
  Workspaces: () => Workspaces,
@@ -40,6 +45,116 @@ __export(src_exports, {
40
45
  });
41
46
  module.exports = __toCommonJS(src_exports);
42
47
 
48
+ // src/types/capabilities.ts
49
+ var CAPABILITY_IDS = [
50
+ "channel.connect_link.create.execute",
51
+ "channel.connect_link.revoke.execute",
52
+ "channel.connection.disconnect.execute",
53
+ "helpdesk.conversation.reply.execute",
54
+ "helpdesk.conversation.update.execute",
55
+ "helpdesk.webhook.create.execute",
56
+ "helpdesk.webhook.update.execute",
57
+ "helpdesk.webhook.delete.execute"
58
+ ];
59
+ var CAPABILITY_ROUTES = {
60
+ "channel.connect_link.create.execute": {
61
+ method: "POST",
62
+ path_template: "/api/v1/channels/connect-links"
63
+ },
64
+ "channel.connect_link.revoke.execute": {
65
+ method: "DELETE",
66
+ path_template: "/api/v1/channels/connect-links/{id}"
67
+ },
68
+ "channel.connection.disconnect.execute": {
69
+ method: "DELETE",
70
+ path_template: "/api/v1/channels/connections/{id}"
71
+ },
72
+ "helpdesk.conversation.reply.execute": {
73
+ method: "POST",
74
+ path_template: "/api/v1/helpdesk/replies"
75
+ },
76
+ "helpdesk.conversation.update.execute": {
77
+ method: "PATCH",
78
+ path_template: "/api/v1/helpdesk/conversations/{id}"
79
+ },
80
+ "helpdesk.webhook.create.execute": {
81
+ method: "POST",
82
+ path_template: "/api/v1/webhooks"
83
+ },
84
+ "helpdesk.webhook.update.execute": {
85
+ method: "PATCH",
86
+ path_template: "/api/v1/webhooks/{id}"
87
+ },
88
+ "helpdesk.webhook.delete.execute": {
89
+ method: "DELETE",
90
+ path_template: "/api/v1/webhooks/{id}"
91
+ }
92
+ };
93
+
94
+ // src/capability-confirmer.ts
95
+ function newIdempotencyKey() {
96
+ const cryptoRef = globalThis.crypto;
97
+ if (typeof cryptoRef?.randomUUID === "function") {
98
+ return cryptoRef.randomUUID();
99
+ }
100
+ return `idem_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 12)}`;
101
+ }
102
+ function resolvePath(template, pathParams) {
103
+ return template.replace(/\{([^}/]+)\}/g, (_match, name) => {
104
+ const value = pathParams?.[name];
105
+ return value === void 0 ? `{${name}}` : encodeURIComponent(String(value));
106
+ });
107
+ }
108
+ var CapabilityConfirmer = class {
109
+ constructor(confirmations, defaults) {
110
+ this.confirmations = confirmations;
111
+ this.defaults = defaults;
112
+ }
113
+ confirmations;
114
+ defaults;
115
+ /**
116
+ * Return the request options to use for a confirmable write, minting the
117
+ * idempotency key and confirmation token first when auto-confirm is active.
118
+ *
119
+ * `body` is the pending request payload (`undefined` for `DELETE` routes).
120
+ * It is handed to the `previewSummary` callback by reference so the summary
121
+ * can describe the specific action, not just the route — it is the caller's
122
+ * own payload, so it is passed through unmodified and unredacted.
123
+ */
124
+ async prepare(request, pathParams, options) {
125
+ const auto = options?.autoConfirm === false ? void 0 : options?.autoConfirm ?? this.defaults;
126
+ if (!auto) return options;
127
+ if (options?.idempotencyKey && options?.capabilityConfirmation) return options;
128
+ const route = CAPABILITY_ROUTES[request.capabilityId];
129
+ const idempotencyKey = options?.idempotencyKey ?? newIdempotencyKey();
130
+ const path = resolvePath(route.path_template, pathParams);
131
+ const previewSummary = auto.previewSummary({
132
+ ...request,
133
+ method: route.method,
134
+ path,
135
+ ...pathParams ? { pathParams } : {},
136
+ idempotencyKey
137
+ });
138
+ if (typeof previewSummary !== "string" || previewSummary.trim() === "") {
139
+ throw new Error(
140
+ `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.`
141
+ );
142
+ }
143
+ const { data } = await this.confirmations.create({
144
+ capability_id: request.capabilityId,
145
+ ...pathParams ? { path_params: pathParams } : {},
146
+ idempotency_key: idempotencyKey,
147
+ preview_summary: previewSummary,
148
+ user_approved: true
149
+ });
150
+ return {
151
+ ...options,
152
+ idempotencyKey,
153
+ capabilityConfirmation: data.confirmation_token
154
+ };
155
+ }
156
+ };
157
+
43
158
  // src/types/common.ts
44
159
  var MedalApiError = class extends Error {
45
160
  status;
@@ -165,12 +280,37 @@ var BaseClient = class {
165
280
  }
166
281
  };
167
282
 
283
+ // src/resources/capability-confirmations.ts
284
+ var CapabilityConfirmations = class {
285
+ constructor(client) {
286
+ this.client = client;
287
+ }
288
+ client;
289
+ /**
290
+ * Issue a confirmation token for one pending write.
291
+ *
292
+ * The token is bound to the workspace, the auth subject, the capability's
293
+ * method + path, its required scopes, and `idempotency_key` — so it is
294
+ * usable exactly once, for exactly the write it describes, and expires
295
+ * within 15 minutes.
296
+ *
297
+ * Setting `user_approved: true` asserts that a human on your side approved
298
+ * this specific action. `preview_summary` is what they approved, and is
299
+ * retained for audit — write it for a human reader, not a log parser.
300
+ */
301
+ async create(input) {
302
+ return this.client.post("/api/v1/capability-confirmations", input);
303
+ }
304
+ };
305
+
168
306
  // src/resources/channels.ts
169
307
  var ChannelConnectLinks = class {
170
- constructor(client) {
308
+ constructor(client, confirmer) {
171
309
  this.client = client;
310
+ this.confirmer = confirmer;
172
311
  }
173
312
  client;
313
+ confirmer;
174
314
  /**
175
315
  * Mint a single-use hosted connect link. Returns HTTP 201.
176
316
  *
@@ -183,28 +323,64 @@ var ChannelConnectLinks = class {
183
323
  * need the workspace `admin` role.
184
324
  */
185
325
  async create(input, options) {
186
- return this.client.post("/api/v1/channels/connect-links", input, options);
326
+ const resolved = await this.confirmer.prepare(
327
+ { capabilityId: "channel.connect_link.create.execute", body: input },
328
+ void 0,
329
+ options
330
+ );
331
+ return this.client.post("/api/v1/channels/connect-links", input, resolved);
187
332
  }
188
- /** List the workspace's connect links (tokens are never returned). */
333
+ /**
334
+ * List the workspace's connect links (tokens are never returned), newest
335
+ * first, with cursor-based pagination.
336
+ *
337
+ * `limit` defaults to 50 server-side and is capped at 100. Follow
338
+ * `pagination.next_cursor` while `pagination.has_more` is true.
339
+ *
340
+ * The `channel_type` / `status` filters are applied **within** each page,
341
+ * so a page may hold fewer than `limit` items while `has_more` is still
342
+ * true — drive the loop off `has_more`, never off the item count.
343
+ */
189
344
  async list(options) {
190
345
  const params = {};
346
+ if (options?.limit !== void 0) params.limit = String(options.limit);
347
+ if (options?.cursor) params.cursor = options.cursor;
191
348
  if (options?.channel_type) params.channel_type = options.channel_type;
192
349
  if (options?.status) params.status = options.status;
193
350
  return this.client.get("/api/v1/channels/connect-links", params);
194
351
  }
195
352
  /** Revoke a pending connect link so it can no longer be consumed. */
196
353
  async revoke(id, options) {
197
- return this.client.delete(`/api/v1/channels/connect-links/${encodeURIComponent(id)}`, options);
354
+ const resolved = await this.confirmer.prepare(
355
+ { capabilityId: "channel.connect_link.revoke.execute", body: void 0 },
356
+ { id },
357
+ options
358
+ );
359
+ return this.client.delete(`/api/v1/channels/connect-links/${encodeURIComponent(id)}`, resolved);
198
360
  }
199
361
  };
200
362
  var ChannelConnections = class {
201
- constructor(client) {
363
+ constructor(client, confirmer) {
202
364
  this.client = client;
365
+ this.confirmer = confirmer;
203
366
  }
204
367
  client;
205
- /** List the workspace's channel connections (generic, channel-agnostic shape). */
206
- async list() {
207
- return this.client.get("/api/v1/channels/connections");
368
+ confirmer;
369
+ /**
370
+ * List the workspace's channel connections (generic, channel-agnostic
371
+ * shape), newest first, with cursor-based pagination.
372
+ *
373
+ * `limit` defaults to 50 server-side and is capped at 100. Follow
374
+ * `pagination.next_cursor` while `pagination.has_more` is true. Rows that
375
+ * are not projectable as connections are dropped within the page, so a page
376
+ * may hold fewer than `limit` items while `has_more` is still true — drive
377
+ * the loop off `has_more`, never off the item count.
378
+ */
379
+ async list(options) {
380
+ const params = {};
381
+ if (options?.limit !== void 0) params.limit = String(options.limit);
382
+ if (options?.cursor) params.cursor = options.cursor;
383
+ return this.client.get("/api/v1/channels/connections", params);
208
384
  }
209
385
  /**
210
386
  * Disconnect a connected channel account (best-effort platform logout, then
@@ -212,15 +388,21 @@ var ChannelConnections = class {
212
388
  * `reason: "api_disconnect"` if the account was previously connected.
213
389
  */
214
390
  async disconnect(id, options) {
215
- return this.client.delete(`/api/v1/channels/connections/${encodeURIComponent(id)}`, options);
391
+ const resolved = await this.confirmer.prepare(
392
+ { capabilityId: "channel.connection.disconnect.execute", body: void 0 },
393
+ { id },
394
+ options
395
+ );
396
+ return this.client.delete(`/api/v1/channels/connections/${encodeURIComponent(id)}`, resolved);
216
397
  }
217
398
  };
218
399
  var Channels = class {
219
400
  connectLinks;
220
401
  connections;
221
- constructor(client) {
222
- this.connectLinks = new ChannelConnectLinks(client);
223
- this.connections = new ChannelConnections(client);
402
+ constructor(client, confirmer) {
403
+ const resolved = confirmer ?? new CapabilityConfirmer(new CapabilityConfirmations(client));
404
+ this.connectLinks = new ChannelConnectLinks(client, resolved);
405
+ this.connections = new ChannelConnections(client, resolved);
224
406
  }
225
407
  };
226
408
 
@@ -386,10 +568,12 @@ var Gdpr = class {
386
568
 
387
569
  // src/resources/helpdesk.ts
388
570
  var HelpdeskConversations = class {
389
- constructor(client) {
571
+ constructor(client, confirmer) {
390
572
  this.client = client;
573
+ this.confirmer = confirmer;
391
574
  }
392
575
  client;
576
+ confirmer;
393
577
  /** List/search conversations with cursor-based pagination and optional filters. */
394
578
  async list(options) {
395
579
  const params = {};
@@ -408,10 +592,15 @@ var HelpdeskConversations = class {
408
592
  }
409
593
  /** Update a conversation's status and/or assignee (pass `assignee_user_id: null` to unassign). */
410
594
  async update(id, input, options) {
595
+ const resolved = await this.confirmer.prepare(
596
+ { capabilityId: "helpdesk.conversation.update.execute", body: input },
597
+ { id },
598
+ options
599
+ );
411
600
  return this.client.patch(
412
601
  `/api/v1/helpdesk/conversations/${encodeURIComponent(id)}`,
413
602
  input,
414
- options
603
+ resolved
415
604
  );
416
605
  }
417
606
  /** Read a conversation's messages with cursor-based pagination. */
@@ -426,10 +615,12 @@ var HelpdeskConversations = class {
426
615
  }
427
616
  };
428
617
  var HelpdeskReplies = class {
429
- constructor(client) {
618
+ constructor(client, confirmer) {
430
619
  this.client = client;
620
+ this.confirmer = confirmer;
431
621
  }
432
622
  client;
623
+ confirmer;
433
624
  /**
434
625
  * Send an operator reply or internal note. Returns HTTP 201.
435
626
  *
@@ -437,15 +628,21 @@ var HelpdeskReplies = class {
437
628
  * messages — it is REQUIRED for capability-scoped tokens.
438
629
  */
439
630
  async create(input, options) {
440
- return this.client.post("/api/v1/helpdesk/replies", input, options);
631
+ const resolved = await this.confirmer.prepare(
632
+ { capabilityId: "helpdesk.conversation.reply.execute", body: input },
633
+ void 0,
634
+ options
635
+ );
636
+ return this.client.post("/api/v1/helpdesk/replies", input, resolved);
441
637
  }
442
638
  };
443
639
  var Helpdesk = class {
444
640
  conversations;
445
641
  replies;
446
- constructor(client) {
447
- this.conversations = new HelpdeskConversations(client);
448
- this.replies = new HelpdeskReplies(client);
642
+ constructor(client, confirmer) {
643
+ const resolved = confirmer ?? new CapabilityConfirmer(new CapabilityConfirmations(client));
644
+ this.conversations = new HelpdeskConversations(client, resolved);
645
+ this.replies = new HelpdeskReplies(client, resolved);
449
646
  }
450
647
  };
451
648
 
@@ -494,12 +691,71 @@ var Posts = class {
494
691
  }
495
692
  };
496
693
 
694
+ // src/resources/scan.ts
695
+ var sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
696
+ var Scan = class {
697
+ constructor(client) {
698
+ this.client = client;
699
+ }
700
+ client;
701
+ /**
702
+ * Queue a scan. Provide exactly one of `url`, `orgnr`, or `name`.
703
+ * Runs asynchronously — poll with `get()` or use `waitForResult()`.
704
+ *
705
+ * @throws Error before any request when zero or several selectors are set —
706
+ * the server would reject the body anyway; failing locally is clearer.
707
+ */
708
+ async create(input) {
709
+ const entries = ["url", "orgnr", "name"].filter(
710
+ (key2) => input[key2] !== void 0 && input[key2] !== ""
711
+ );
712
+ if (entries.length !== 1) {
713
+ throw new Error("scan.create requires exactly one of url, orgnr, or name");
714
+ }
715
+ const key = entries[0];
716
+ return this.client.post("/api/v1/scan", { [key]: input[key] });
717
+ }
718
+ /** Get a scan job's status and, once done, its findings payload. */
719
+ async get(id) {
720
+ return this.client.get(`/api/v1/scan/${encodeURIComponent(id)}`);
721
+ }
722
+ /** Search the Norwegian company registry by name (typeahead, top 5 hits). */
723
+ async companies(q) {
724
+ return this.client.get("/api/v1/scan/companies", { q });
725
+ }
726
+ /**
727
+ * Poll a scan until it settles. Resolves with the job for both `done` and
728
+ * `failed` (check `job.error`); throws only when the deadline passes while
729
+ * the scan is still pending/running.
730
+ */
731
+ async waitForResult(id, options = {}) {
732
+ const rawInterval = options.intervalMs ?? 2500;
733
+ const rawTimeout = options.timeoutMs ?? 12e4;
734
+ const intervalMs = Number.isFinite(rawInterval) && rawInterval > 0 ? rawInterval : 2500;
735
+ const timeoutMs = Number.isFinite(rawTimeout) ? rawTimeout : 12e4;
736
+ const deadline = Date.now() + timeoutMs;
737
+ let lastStatus = "pending";
738
+ for (; ; ) {
739
+ const { data } = await this.get(id);
740
+ if (data.status === "done" || data.status === "failed") return data;
741
+ lastStatus = data.status;
742
+ const remaining = deadline - Date.now();
743
+ if (remaining <= 0) break;
744
+ await sleep(Math.min(intervalMs, remaining));
745
+ if (Date.now() >= deadline) break;
746
+ }
747
+ throw new Error(`Scan ${id} timed out after ${timeoutMs}ms (status: ${lastStatus})`);
748
+ }
749
+ };
750
+
497
751
  // src/resources/webhooks.ts
498
752
  var Webhooks = class {
499
- constructor(client) {
753
+ constructor(client, confirmer) {
500
754
  this.client = client;
755
+ this.confirmer = confirmer ?? new CapabilityConfirmer(new CapabilityConfirmations(client));
501
756
  }
502
757
  client;
758
+ confirmer;
503
759
  /** List all webhook endpoints in the workspace. */
504
760
  async list() {
505
761
  return this.client.get("/api/v1/webhooks");
@@ -517,7 +773,12 @@ var Webhooks = class {
517
773
  * endpoint WITHOUT the secret — handle that case (rotate if you lost it).
518
774
  */
519
775
  async create(input, options) {
520
- return this.client.post("/api/v1/webhooks", input, options);
776
+ const resolved = await this.confirmer.prepare(
777
+ { capabilityId: "helpdesk.webhook.create.execute", body: input },
778
+ void 0,
779
+ options
780
+ );
781
+ return this.client.post("/api/v1/webhooks", input, resolved);
521
782
  }
522
783
  /** Get a webhook endpoint by ID. */
523
784
  async get(id) {
@@ -525,7 +786,12 @@ var Webhooks = class {
525
786
  }
526
787
  /** Update a webhook endpoint (name, url, event types, filters, enabled). */
527
788
  async update(id, input, options) {
528
- return this.client.patch(`/api/v1/webhooks/${encodeURIComponent(id)}`, input, options);
789
+ const resolved = await this.confirmer.prepare(
790
+ { capabilityId: "helpdesk.webhook.update.execute", body: input },
791
+ { id },
792
+ options
793
+ );
794
+ return this.client.patch(`/api/v1/webhooks/${encodeURIComponent(id)}`, input, resolved);
529
795
  }
530
796
  /**
531
797
  * Permanently delete a webhook endpoint (stops all outbound deliveries).
@@ -534,7 +800,12 @@ var Webhooks = class {
534
800
  * grants on this route. API keys with legacy scopes may omit it.
535
801
  */
536
802
  async delete(id, options) {
537
- return this.client.delete(`/api/v1/webhooks/${encodeURIComponent(id)}`, options);
803
+ const resolved = await this.confirmer.prepare(
804
+ { capabilityId: "helpdesk.webhook.delete.execute", body: void 0 },
805
+ { id },
806
+ options
807
+ );
808
+ return this.client.delete(`/api/v1/webhooks/${encodeURIComponent(id)}`, resolved);
538
809
  }
539
810
  /** List recent deliveries for an endpoint (most recent first). */
540
811
  async deliveries(id, options) {
@@ -632,6 +903,7 @@ async function verifyWebhookSignature(input) {
632
903
 
633
904
  // src/index.ts
634
905
  var Medal = class {
906
+ capabilityConfirmations;
635
907
  channels;
636
908
  emails;
637
909
  contacts;
@@ -639,6 +911,7 @@ var Medal = class {
639
911
  gdpr;
640
912
  helpdesk;
641
913
  posts;
914
+ scan;
642
915
  webhooks;
643
916
  workspaces;
644
917
  constructor(token, options) {
@@ -654,14 +927,20 @@ var Medal = class {
654
927
  timeout: options?.timeout ?? 3e4,
655
928
  userAgent: "medalsocial-sdk/1.0.0 (+https://github.com/Medal-Social/MedalSocial)"
656
929
  });
657
- this.channels = new Channels(client);
930
+ this.capabilityConfirmations = new CapabilityConfirmations(client);
931
+ const confirmer = new CapabilityConfirmer(
932
+ this.capabilityConfirmations,
933
+ options?.autoConfirmCapabilities
934
+ );
935
+ this.channels = new Channels(client, confirmer);
658
936
  this.emails = new Emails(client);
659
937
  this.contacts = new Contacts(client);
660
938
  this.deals = new Deals(client);
661
939
  this.gdpr = new Gdpr(client);
662
- this.helpdesk = new Helpdesk(client);
940
+ this.helpdesk = new Helpdesk(client, confirmer);
663
941
  this.posts = new Posts(client);
664
- this.webhooks = new Webhooks(client);
942
+ this.scan = new Scan(client);
943
+ this.webhooks = new Webhooks(client, confirmer);
665
944
  this.workspaces = new Workspaces(client);
666
945
  }
667
946
  };
@@ -672,6 +951,10 @@ var src_default = Medal;
672
951
  // Annotate the CommonJS export names for ESM import in node:
673
952
  0 && (module.exports = {
674
953
  BaseClient,
954
+ CAPABILITY_IDS,
955
+ CAPABILITY_ROUTES,
956
+ CapabilityConfirmations,
957
+ CapabilityConfirmer,
675
958
  Channels,
676
959
  Contacts,
677
960
  DEFAULT_WEBHOOK_TOLERANCE_MS,
@@ -682,6 +965,7 @@ var src_default = Medal;
682
965
  Medal,
683
966
  MedalApiError,
684
967
  Posts,
968
+ Scan,
685
969
  WebhookVerificationError,
686
970
  Webhooks,
687
971
  Workspaces,