@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.
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
+ CAPABILITY_IDS: () => CAPABILITY_IDS,
25
+ CAPABILITY_ROUTES: () => CAPABILITY_ROUTES,
26
+ CapabilityConfirmations: () => CapabilityConfirmations,
27
+ CapabilityConfirmer: () => CapabilityConfirmer,
28
+ Channels: () => Channels,
24
29
  Contacts: () => Contacts,
25
30
  DEFAULT_WEBHOOK_TOLERANCE_MS: () => DEFAULT_WEBHOOK_TOLERANCE_MS,
26
31
  Deals: () => Deals,
@@ -30,6 +35,7 @@ __export(src_exports, {
30
35
  Medal: () => Medal,
31
36
  MedalApiError: () => MedalApiError,
32
37
  Posts: () => Posts,
38
+ Scan: () => Scan,
33
39
  WebhookVerificationError: () => WebhookVerificationError,
34
40
  Webhooks: () => Webhooks,
35
41
  Workspaces: () => Workspaces,
@@ -39,6 +45,116 @@ __export(src_exports, {
39
45
  });
40
46
  module.exports = __toCommonJS(src_exports);
41
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
+
42
158
  // src/types/common.ts
43
159
  var MedalApiError = class extends Error {
44
160
  status;
@@ -164,6 +280,132 @@ var BaseClient = class {
164
280
  }
165
281
  };
166
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
+
306
+ // src/resources/channels.ts
307
+ var ChannelConnectLinks = class {
308
+ constructor(client, confirmer) {
309
+ this.client = client;
310
+ this.confirmer = confirmer;
311
+ }
312
+ client;
313
+ confirmer;
314
+ /**
315
+ * Mint a single-use hosted connect link. Returns HTTP 201.
316
+ *
317
+ * **The response's `data.url` contains the one-time link token EXACTLY
318
+ * ONCE.** Send it to the person who should connect their account — an
319
+ * idempotent replay (same `Idempotency-Key`) returns the link WITHOUT
320
+ * `url`, so store it immediately (or revoke and mint a new link if lost).
321
+ *
322
+ * Requires the `channel.connect.manage` scope; OAuth callers additionally
323
+ * need the workspace `admin` role.
324
+ */
325
+ async create(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);
332
+ }
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
+ */
344
+ async list(options) {
345
+ const params = {};
346
+ if (options?.limit !== void 0) params.limit = String(options.limit);
347
+ if (options?.cursor) params.cursor = options.cursor;
348
+ if (options?.channel_type) params.channel_type = options.channel_type;
349
+ if (options?.status) params.status = options.status;
350
+ return this.client.get("/api/v1/channels/connect-links", params);
351
+ }
352
+ /** Revoke a pending connect link so it can no longer be consumed. */
353
+ async revoke(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);
360
+ }
361
+ };
362
+ var ChannelConnections = class {
363
+ constructor(client, confirmer) {
364
+ this.client = client;
365
+ this.confirmer = confirmer;
366
+ }
367
+ client;
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);
384
+ }
385
+ /**
386
+ * Disconnect a connected channel account (best-effort platform logout, then
387
+ * local revoke). Emits a `helpdesk.channel_disconnected` webhook event with
388
+ * `reason: "api_disconnect"` if the account was previously connected.
389
+ */
390
+ async disconnect(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);
397
+ }
398
+ };
399
+ var Channels = class {
400
+ connectLinks;
401
+ connections;
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);
406
+ }
407
+ };
408
+
167
409
  // src/resources/contacts.ts
168
410
  var Contacts = class {
169
411
  constructor(client) {
@@ -326,10 +568,12 @@ var Gdpr = class {
326
568
 
327
569
  // src/resources/helpdesk.ts
328
570
  var HelpdeskConversations = class {
329
- constructor(client) {
571
+ constructor(client, confirmer) {
330
572
  this.client = client;
573
+ this.confirmer = confirmer;
331
574
  }
332
575
  client;
576
+ confirmer;
333
577
  /** List/search conversations with cursor-based pagination and optional filters. */
334
578
  async list(options) {
335
579
  const params = {};
@@ -348,10 +592,15 @@ var HelpdeskConversations = class {
348
592
  }
349
593
  /** Update a conversation's status and/or assignee (pass `assignee_user_id: null` to unassign). */
350
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
+ );
351
600
  return this.client.patch(
352
601
  `/api/v1/helpdesk/conversations/${encodeURIComponent(id)}`,
353
602
  input,
354
- options
603
+ resolved
355
604
  );
356
605
  }
357
606
  /** Read a conversation's messages with cursor-based pagination. */
@@ -366,10 +615,12 @@ var HelpdeskConversations = class {
366
615
  }
367
616
  };
368
617
  var HelpdeskReplies = class {
369
- constructor(client) {
618
+ constructor(client, confirmer) {
370
619
  this.client = client;
620
+ this.confirmer = confirmer;
371
621
  }
372
622
  client;
623
+ confirmer;
373
624
  /**
374
625
  * Send an operator reply or internal note. Returns HTTP 201.
375
626
  *
@@ -377,15 +628,21 @@ var HelpdeskReplies = class {
377
628
  * messages — it is REQUIRED for capability-scoped tokens.
378
629
  */
379
630
  async create(input, options) {
380
- 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);
381
637
  }
382
638
  };
383
639
  var Helpdesk = class {
384
640
  conversations;
385
641
  replies;
386
- constructor(client) {
387
- this.conversations = new HelpdeskConversations(client);
388
- 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);
389
646
  }
390
647
  };
391
648
 
@@ -434,12 +691,71 @@ var Posts = class {
434
691
  }
435
692
  };
436
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
+
437
751
  // src/resources/webhooks.ts
438
752
  var Webhooks = class {
439
- constructor(client) {
753
+ constructor(client, confirmer) {
440
754
  this.client = client;
755
+ this.confirmer = confirmer ?? new CapabilityConfirmer(new CapabilityConfirmations(client));
441
756
  }
442
757
  client;
758
+ confirmer;
443
759
  /** List all webhook endpoints in the workspace. */
444
760
  async list() {
445
761
  return this.client.get("/api/v1/webhooks");
@@ -457,7 +773,12 @@ var Webhooks = class {
457
773
  * endpoint WITHOUT the secret — handle that case (rotate if you lost it).
458
774
  */
459
775
  async create(input, options) {
460
- 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);
461
782
  }
462
783
  /** Get a webhook endpoint by ID. */
463
784
  async get(id) {
@@ -465,7 +786,12 @@ var Webhooks = class {
465
786
  }
466
787
  /** Update a webhook endpoint (name, url, event types, filters, enabled). */
467
788
  async update(id, input, options) {
468
- 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);
469
795
  }
470
796
  /**
471
797
  * Permanently delete a webhook endpoint (stops all outbound deliveries).
@@ -474,7 +800,12 @@ var Webhooks = class {
474
800
  * grants on this route. API keys with legacy scopes may omit it.
475
801
  */
476
802
  async delete(id, options) {
477
- 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);
478
809
  }
479
810
  /** List recent deliveries for an endpoint (most recent first). */
480
811
  async deliveries(id, options) {
@@ -572,12 +903,15 @@ async function verifyWebhookSignature(input) {
572
903
 
573
904
  // src/index.ts
574
905
  var Medal = class {
906
+ capabilityConfirmations;
907
+ channels;
575
908
  emails;
576
909
  contacts;
577
910
  deals;
578
911
  gdpr;
579
912
  helpdesk;
580
913
  posts;
914
+ scan;
581
915
  webhooks;
582
916
  workspaces;
583
917
  constructor(token, options) {
@@ -593,13 +927,20 @@ var Medal = class {
593
927
  timeout: options?.timeout ?? 3e4,
594
928
  userAgent: "medalsocial-sdk/1.0.0 (+https://github.com/Medal-Social/MedalSocial)"
595
929
  });
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);
596
936
  this.emails = new Emails(client);
597
937
  this.contacts = new Contacts(client);
598
938
  this.deals = new Deals(client);
599
939
  this.gdpr = new Gdpr(client);
600
- this.helpdesk = new Helpdesk(client);
940
+ this.helpdesk = new Helpdesk(client, confirmer);
601
941
  this.posts = new Posts(client);
602
- this.webhooks = new Webhooks(client);
942
+ this.scan = new Scan(client);
943
+ this.webhooks = new Webhooks(client, confirmer);
603
944
  this.workspaces = new Workspaces(client);
604
945
  }
605
946
  };
@@ -610,6 +951,11 @@ var src_default = Medal;
610
951
  // Annotate the CommonJS export names for ESM import in node:
611
952
  0 && (module.exports = {
612
953
  BaseClient,
954
+ CAPABILITY_IDS,
955
+ CAPABILITY_ROUTES,
956
+ CapabilityConfirmations,
957
+ CapabilityConfirmer,
958
+ Channels,
613
959
  Contacts,
614
960
  DEFAULT_WEBHOOK_TOLERANCE_MS,
615
961
  Deals,
@@ -619,6 +965,7 @@ var src_default = Medal;
619
965
  Medal,
620
966
  MedalApiError,
621
967
  Posts,
968
+ Scan,
622
969
  WebhookVerificationError,
623
970
  Webhooks,
624
971
  Workspaces,