@dialgem/sdk 0.1.0-beta.0 → 0.1.0-beta.1

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/index.cjs CHANGED
@@ -106,7 +106,7 @@ function isErrorBody(body) {
106
106
  }
107
107
 
108
108
  // src/version.ts
109
- var VERSION = "0.1.0-beta.0";
109
+ var VERSION = "0.1.0-beta.1";
110
110
 
111
111
  // src/core/http.ts
112
112
  var RETRYABLE_METHODS = /* @__PURE__ */ new Set(["GET", "PATCH", "DELETE"]);
@@ -132,6 +132,16 @@ var HttpClient = class {
132
132
  post(path2, body, options) {
133
133
  return this.request("POST", path2, { body, options });
134
134
  }
135
+ /**
136
+ * POST a `FormData` body — file uploads.
137
+ *
138
+ * The body is sent as-is and `Content-Type` is left unset, because only
139
+ * `fetch` knows the multipart boundary it is about to generate. Setting the
140
+ * header ourselves produces a request no server can parse.
141
+ */
142
+ postForm(path2, form, options) {
143
+ return this.request("POST", path2, { body: form, options });
144
+ }
135
145
  patch(path2, body, options) {
136
146
  return this.request("PATCH", path2, { body, options });
137
147
  }
@@ -140,10 +150,11 @@ var HttpClient = class {
140
150
  }
141
151
  async request(method, path2, { query, body, options = {} }) {
142
152
  const url = this.buildURL(path2, query);
143
- const headers = this.buildHeaders(body !== void 0, options.headers);
144
- const payload = body === void 0 ? void 0 : JSON.stringify(body);
153
+ const isForm = isFormData(body);
154
+ const headers = this.buildHeaders(body !== void 0 && !isForm, options.headers);
155
+ const payload = body === void 0 ? void 0 : isForm ? body : JSON.stringify(body);
145
156
  const timeout = options.timeout ?? this.#timeout;
146
- const maxRetries = options.maxRetries ?? (RETRYABLE_METHODS.has(method) ? this.#maxRetries : 0);
157
+ const maxRetries = isForm ? 0 : options.maxRetries ?? (RETRYABLE_METHODS.has(method) ? this.#maxRetries : 0);
147
158
  assertTimeout(timeout);
148
159
  assertMaxRetries(maxRetries);
149
160
  for (let attempt = 0; ; attempt++) {
@@ -197,7 +208,7 @@ var HttpClient = class {
197
208
  const params = new URLSearchParams();
198
209
  for (const [key, value] of Object.entries(query)) {
199
210
  if (value === void 0 || value === null) continue;
200
- params.append(key, String(value));
211
+ for (const item of Array.isArray(value) ? value : [value]) params.append(key, String(item));
201
212
  }
202
213
  const qs = params.toString();
203
214
  return qs ? `${url}?${qs}` : url;
@@ -220,6 +231,9 @@ var HttpClient = class {
220
231
  return Object.fromEntries(merged.values());
221
232
  }
222
233
  };
234
+ function isFormData(value) {
235
+ return typeof FormData !== "undefined" && value instanceof FormData;
236
+ }
223
237
  function assertTimeout(value) {
224
238
  if (!Number.isInteger(value) || value <= 0) {
225
239
  throw new DialgemError(`\`timeout\` must be a positive integer of milliseconds, got ${String(value)}.`);
@@ -316,11 +330,14 @@ var Agents = class extends APIResource {
316
330
  knowledge;
317
331
  /** Remote MCP servers whose tools are exposed to the agent. */
318
332
  mcpServers;
333
+ /** Frozen snapshots of the agent that a number or a call can pin. */
334
+ versions;
319
335
  constructor(client) {
320
336
  super(client);
321
337
  this.functions = new AgentFunctions(client);
322
338
  this.knowledge = new AgentKnowledge(client);
323
339
  this.mcpServers = new AgentMCPServers(client);
340
+ this.versions = new AgentVersions(client);
324
341
  }
325
342
  /** Create an agent. Only `name` is required; everything else has a server default. */
326
343
  create(body, options) {
@@ -341,6 +358,48 @@ var Agents = class extends APIResource {
341
358
  return this._client.delete(path`/v1/agents/${agentId}`, options);
342
359
  }
343
360
  };
361
+ var AgentVersions = class extends APIResource {
362
+ /** Freeze the agent as it is now. Returns the new version, snapshot included. */
363
+ publish(agentId, body = {}, options) {
364
+ return this._client.post(path`/v1/agents/${agentId}/versions`, body, options);
365
+ }
366
+ /**
367
+ * One page of published versions, newest first, without their snapshots.
368
+ *
369
+ * Returns the array. When you need to page — an agent published on every
370
+ * deploy accumulates versions, and nothing prunes them — read `has_more` from
371
+ * {@link listPage} instead and pass the last `version` as `before_version`.
372
+ */
373
+ async list(agentId, params, options) {
374
+ return (await this.listPage(agentId, params, options)).versions;
375
+ }
376
+ /** {@link list} with the `has_more` flag, for paging. */
377
+ listPage(agentId, params, options) {
378
+ return this._client.get(
379
+ path`/v1/agents/${agentId}/versions`,
380
+ // Spread into a plain query record: an `interface` has no index
381
+ // signature, so it does not structurally satisfy `Query`.
382
+ { ...params },
383
+ options
384
+ );
385
+ }
386
+ /** One version with its full snapshot. Version 0 is the draft: use `agents.retrieve`. */
387
+ retrieve(agentId, version, options) {
388
+ return this._client.get(
389
+ path`/v1/agents/${agentId}/versions/${versionSegment(version)}`,
390
+ void 0,
391
+ options
392
+ );
393
+ }
394
+ };
395
+ function versionSegment(version) {
396
+ if (!Number.isInteger(version) || version < 1) {
397
+ throw new DialgemError(
398
+ `Expected a published version number (a positive integer), got ${JSON.stringify(version)}. Version 0 is the draft \u2014 read it with agents.retrieve().`
399
+ );
400
+ }
401
+ return String(version);
402
+ }
344
403
  var AgentFunctions = class extends APIResource {
345
404
  list(agentId, options) {
346
405
  return this._client.get(path`/v1/agents/${agentId}/functions`, void 0, options);
@@ -393,35 +452,33 @@ var AgentMCPServers = class extends APIResource {
393
452
  // src/core/pagination.ts
394
453
  var Page = class _Page {
395
454
  data;
396
- total;
397
- page;
398
- page_size;
455
+ has_more;
456
+ pagination_key;
399
457
  #fetchPage;
400
458
  constructor(data, fetchPage) {
401
459
  this.data = data.data;
402
- this.total = data.total;
403
- this.page = data.page;
404
- this.page_size = data.page_size;
460
+ this.has_more = data.has_more;
461
+ this.pagination_key = data.pagination_key;
405
462
  this.#fetchPage = fetchPage;
406
463
  }
407
464
  hasNextPage() {
408
- return this.data.length > 0 && this.page * this.page_size < this.total;
465
+ return this.has_more && this.pagination_key !== null;
409
466
  }
410
467
  async getNextPage() {
411
468
  if (!this.hasNextPage()) throw new DialgemError("No next page; check hasNextPage() first.");
412
- return new _Page(await this.#fetchPage(this.page + 1), this.#fetchPage);
469
+ return new _Page(await this.#fetchPage(this.pagination_key), this.#fetchPage);
413
470
  }
414
471
  };
415
472
  var PagePromise = class {
416
473
  #fetchPage;
417
- #startPage;
474
+ #startKey;
418
475
  #first;
419
- constructor(fetchPage, startPage) {
476
+ constructor(fetchPage, startKey = null) {
420
477
  this.#fetchPage = fetchPage;
421
- this.#startPage = startPage;
478
+ this.#startKey = startKey;
422
479
  }
423
480
  #firstPage() {
424
- this.#first ??= this.#fetchPage(this.#startPage).then((data) => new Page(data, this.#fetchPage));
481
+ this.#first ??= this.#fetchPage(this.#startKey).then((data) => new Page(data, this.#fetchPage));
425
482
  return this.#first;
426
483
  }
427
484
  then(onfulfilled, onrejected) {
@@ -446,24 +503,50 @@ var PagePromise = class {
446
503
  // src/resources/calls.ts
447
504
  var Calls = class extends APIResource {
448
505
  /**
449
- * Calls, newest first. Await for one page, or iterate for every call:
506
+ * Calls, newest first by default. Await for one page, or iterate for every call:
450
507
  *
451
- * const page = await dialgem.calls.list({ page_size: 50 })
452
- * for await (const call of dialgem.calls.list({ status: "completed" })) {
453
- * if (call.started_at < cutoff) break // newest first: everything after is older
454
- * }
508
+ * const page = await dialgem.calls.list({ limit: 50 })
509
+ * for await (const call of dialgem.calls.list({ status: "completed", start_after: since })) { ... }
455
510
  */
456
511
  list(params = {}, options) {
457
- const fetchPage = async (page) => {
458
- const res = await this._client.get("/v1/calls", { ...params, page }, options);
459
- return { data: res.calls, total: res.total, page: res.page, page_size: res.page_size };
512
+ const query = toQuery(params);
513
+ const fetchPage = async (paginationKey) => {
514
+ const res = await this._client.get(
515
+ "/v1/calls",
516
+ { ...query, pagination_key: paginationKey ?? void 0 },
517
+ options
518
+ );
519
+ return { data: res.calls, has_more: res.has_more, pagination_key: res.pagination_key };
460
520
  };
461
- return new PagePromise(fetchPage, params.page ?? 1);
521
+ return new PagePromise(fetchPage);
462
522
  }
463
- /** One call, including its transcript, analysis and recording URL. */
523
+ /** One call, including its transcript, analysis, latency and recording URL. */
464
524
  retrieve(callId, options) {
465
525
  return this._client.get(path`/v1/calls/${callId}`, void 0, options);
466
526
  }
527
+ /** Replace the call's `metadata`. */
528
+ update(callId, body, options) {
529
+ return this._client.patch(path`/v1/calls/${callId}`, body, options);
530
+ }
531
+ /**
532
+ * Permanently delete a finished call: its record, transcript, analysis and
533
+ * recording. A call still in progress is refused with `ConflictError`; stop it first.
534
+ */
535
+ delete(callId, options) {
536
+ return this._client.delete(path`/v1/calls/${callId}`, options);
537
+ }
538
+ /**
539
+ * End an ongoing call now, as if the agent had hung up. The call becomes
540
+ * `completed` with `disconnection_reason: "manual_stopped"` a moment later, and
541
+ * the usual `call_ended` webhook follows.
542
+ *
543
+ * `ConflictError` with `errCode` `call_not_ongoing` if it already ended, or
544
+ * `call_not_connected` if no agent is on the call yet (e.g. still ringing).
545
+ * Not retried by default.
546
+ */
547
+ stop(callId, options) {
548
+ return this._client.post(path`/v1/calls/${callId}/stop`, void 0, options);
549
+ }
467
550
  /**
468
551
  * Dial `to` from one of your organization's numbers, with `agent_id` (or the
469
552
  * number's outbound agent) on the line.
@@ -483,13 +566,70 @@ var Calls = class extends APIResource {
483
566
  return this._client.post("/v1/calls/register", body, options);
484
567
  }
485
568
  };
569
+ function toQuery(params) {
570
+ const { metadata, ...rest } = params;
571
+ const query = { ...rest };
572
+ query["limit"] = params.limit ?? 10;
573
+ for (const [key, value] of Object.entries(metadata ?? {})) {
574
+ query[`metadata.${key}`] = String(value);
575
+ }
576
+ return query;
577
+ }
486
578
 
487
579
  // src/resources/catalog.ts
488
580
  var Voices = class extends APIResource {
489
- /** Voices available to agents, by name. Put a voice's `provider_voice_id` in an agent's `voice_id`. */
581
+ /**
582
+ * Voices available to agents: the shared catalog plus the ones your
583
+ * organization cloned, yours first. Put a voice's `provider_voice_id` in an
584
+ * agent's `voice_id`.
585
+ */
490
586
  list(params = {}, options) {
491
587
  return this._client.get("/v1/voices", params, options);
492
588
  }
589
+ /**
590
+ * Clone a voice from audio you upload. The new voice is private to your
591
+ * organization and never appears in another one's catalog.
592
+ *
593
+ * Consent is required: you are asserting that you hold the rights to the
594
+ * audio and have the speaker's consent, and that assertion is recorded
595
+ * against your API identity.
596
+ *
597
+ * The upload is never retried, whatever `maxRetries` says — a retried
598
+ * multipart body can send truncated audio, and a clone built from half the
599
+ * recording succeeds and sounds wrong.
600
+ */
601
+ clone(params, options) {
602
+ const { name, files, consent_confirmed, engine, description, language, filenames } = params;
603
+ if (!consent_confirmed) {
604
+ throw new DialgemError(
605
+ "consent_confirmed must be true: you are asserting that you hold the rights to this audio and have the speaker's consent to clone their voice."
606
+ );
607
+ }
608
+ if (!files?.length) {
609
+ throw new DialgemError("At least one audio file is required to clone a voice.");
610
+ }
611
+ const form = new FormData();
612
+ form.append("name", name);
613
+ form.append("consent_confirmed", "true");
614
+ if (engine) form.append("engine", engine);
615
+ if (description) form.append("description", description);
616
+ if (language) form.append("language", language);
617
+ files.forEach((file, i) => {
618
+ const fallback = file.name || `clip-${i + 1}.wav`;
619
+ form.append("files", file, filenames?.[i] ?? fallback);
620
+ });
621
+ return this._client.postForm("/v1/voices/clone", form, options);
622
+ }
623
+ /**
624
+ * Delete a voice your organization cloned, at the provider and here.
625
+ *
626
+ * Only your own cloned voices: the shared catalog is not deletable, and a
627
+ * voice an agent still uses is refused with a 409 rather than leaving that
628
+ * agent without one.
629
+ */
630
+ delete(voiceId, options) {
631
+ return this._client.delete(path`/v1/voices/${voiceId}`, options);
632
+ }
493
633
  };
494
634
  var FlowTemplates = class extends APIResource {
495
635
  /** Built-in conversation-flow templates (summaries, without `flow_config`). */
@@ -520,6 +660,20 @@ var PhoneNumbers = class extends APIResource {
520
660
  list(options) {
521
661
  return this._client.get("/v1/phone-numbers", void 0, options);
522
662
  }
663
+ /**
664
+ * One number by its E.164 value, e.g. `+14155550123`.
665
+ *
666
+ * Keyed on the number rather than on `id`, because the number is what you
667
+ * have when you are asking whether it is registered at all. Throws
668
+ * `NotFoundError` when it is not.
669
+ */
670
+ retrieve(phoneNumber, options) {
671
+ return this._client.get(
672
+ path`/v1/phone-numbers/by-number/${phoneNumber}`,
673
+ void 0,
674
+ options
675
+ );
676
+ }
523
677
  /**
524
678
  * Partial update.
525
679
  *
@@ -544,52 +698,84 @@ var PhoneNumbers = class extends APIResource {
544
698
  // src/webhooks.ts
545
699
  var WEBHOOK_SIGNATURE_HEADER = "x-dialgem-signature";
546
700
  var WEBHOOK_EVENT_HEADER = "x-dialgem-event";
701
+ var WEBHOOK_DELIVERY_HEADER = "x-dialgem-delivery";
702
+ var DEFAULT_SIGNATURE_TOLERANCE_MS = 5 * 60 * 1e3;
547
703
  var encoder = new TextEncoder();
548
704
  var decoder = new TextDecoder();
705
+ var SIGNATURE_RE = /^v=(\d{10,16}),d=([0-9a-f]{64})$/;
549
706
  var Webhooks = class {
550
707
  /**
551
- * Returns whether `signature` is the HMAC-SHA256 of `payload` under `secret`.
552
- * Constant-time. Returns `false` for a missing or malformed signature; throws only
553
- * if `secret` is empty, which is a configuration error rather than a bad request.
708
+ * Returns whether `signature` is a current, valid signature of `payload` under
709
+ * `secret`. Constant-time. Returns `false` for a missing, malformed, tampered or
710
+ * stale signature; throws only if `secret` is empty, which is a configuration
711
+ * error rather than a bad request.
554
712
  */
555
- async verify(payload, signature, secret) {
713
+ async verify(payload, signature, secret, options = {}) {
556
714
  if (!secret) {
557
715
  throw new DialgemError(
558
716
  "Webhook secret is empty. Set the agent's `webhook_signing_secret` and pass the same value here."
559
717
  );
560
718
  }
561
- const received = firstHeader(signature)?.trim().toLowerCase();
562
- if (!received || !/^[0-9a-f]{64}$/.test(received)) return false;
563
- const expected = await hmacSha256Hex(secret, toBytes(payload));
719
+ const match = SIGNATURE_RE.exec(firstHeader(signature)?.trim().toLowerCase() ?? "");
720
+ if (!match) return false;
721
+ const [, timestamp, received] = match;
722
+ const now = options.now ?? Date.now();
723
+ if (Math.abs(now - Number(timestamp)) > (options.toleranceMs ?? DEFAULT_SIGNATURE_TOLERANCE_MS)) {
724
+ return false;
725
+ }
726
+ const body = toBytes(payload);
727
+ const message = new Uint8Array(body.length + timestamp.length);
728
+ message.set(body, 0);
729
+ message.set(encoder.encode(timestamp), body.length);
730
+ const expected = await hmacSha256Hex(secret, message);
564
731
  return timingSafeEqual(expected, received);
565
732
  }
566
733
  /**
567
- * Verifies the signature, then parses the body.
734
+ * Verifies a webhook's signature, then parses its body.
568
735
  *
569
- * @throws WebhookVerificationError if the signature is missing or does not match.
736
+ * @throws WebhookVerificationError if the signature is missing, invalid or stale.
570
737
  */
571
- async constructEvent(payload, signature, secret) {
738
+ async constructEvent(payload, signature, secret, options = {}) {
739
+ const parsed = await this.#verifiedJson(payload, signature, secret, options);
740
+ if (typeof parsed !== "object" || parsed === null || typeof parsed.event !== "string") {
741
+ throw new WebhookVerificationError("Webhook body has no `event` field.");
742
+ }
743
+ return parsed;
744
+ }
745
+ /**
746
+ * Verifies a request Dialgem made to one of your custom functions, then parses
747
+ * its body. For functions with `args_at_root`, the body is the arguments object
748
+ * alone; use `verify` and parse it yourself.
749
+ *
750
+ * @throws WebhookVerificationError if the signature is missing, invalid or stale.
751
+ */
752
+ async constructFunctionCall(payload, signature, secret, options = {}) {
753
+ const parsed = await this.#verifiedJson(payload, signature, secret, options);
754
+ const body = parsed;
755
+ if (!body || typeof body.name !== "string" || typeof body.call !== "object" || body.call === null) {
756
+ throw new WebhookVerificationError(
757
+ "Function request body has no `name` and `call`. Does this function have args_at_root set?"
758
+ );
759
+ }
760
+ return body;
761
+ }
762
+ async #verifiedJson(payload, signature, secret, options) {
572
763
  if (!firstHeader(signature)) {
573
764
  throw new WebhookVerificationError(
574
765
  `Missing ${WEBHOOK_SIGNATURE_HEADER} header. Is a webhook_signing_secret set on the agent?`
575
766
  );
576
767
  }
577
- if (!await this.verify(payload, signature, secret)) {
768
+ if (!await this.verify(payload, signature, secret, options)) {
578
769
  throw new WebhookVerificationError(
579
- "Webhook signature does not match the body. Check the secret, and that the body was not re-serialized before verification."
770
+ "Signature is invalid or older than the allowed tolerance. Check the secret, the server clock, and that the body was not re-serialized before verification."
580
771
  );
581
772
  }
582
773
  const text = typeof payload === "string" ? payload : decoder.decode(toBytes(payload));
583
- let parsed;
584
774
  try {
585
- parsed = JSON.parse(text);
775
+ return JSON.parse(text);
586
776
  } catch {
587
- throw new WebhookVerificationError("Webhook body is signed but is not valid JSON.");
777
+ throw new WebhookVerificationError("Body is signed but is not valid JSON.");
588
778
  }
589
- if (typeof parsed !== "object" || parsed === null || typeof parsed.event !== "string") {
590
- throw new WebhookVerificationError("Webhook body has no `event` field.");
591
- }
592
- return parsed;
593
779
  }
594
780
  };
595
781
  var webhooks = new Webhooks();
@@ -619,11 +805,11 @@ function timingSafeEqual(a, b) {
619
805
  }
620
806
 
621
807
  // src/client.ts
622
- var DEFAULT_BASE_URL = "https://api.dialgem.com";
808
+ var BASE_URL = "https://api.dialgem.com";
623
809
  var DEFAULT_TIMEOUT_MS = 6e4;
624
810
  var DEFAULT_MAX_RETRIES = 2;
625
811
  var Dialgem = class {
626
- static DEFAULT_BASE_URL = DEFAULT_BASE_URL;
812
+ static BASE_URL = BASE_URL;
627
813
  /** Webhook verification without a client instance: `Dialgem.webhooks.constructEvent(...)`. */
628
814
  static webhooks = webhooks;
629
815
  agents;
@@ -633,8 +819,14 @@ var Dialgem = class {
633
819
  flowTemplates;
634
820
  promptTemplates;
635
821
  webhooks = webhooks;
636
- baseURL;
822
+ /** Always `https://api.dialgem.com`. */
823
+ baseURL = BASE_URL;
637
824
  constructor(options = {}) {
825
+ if ("baseURL" in options) {
826
+ throw new DialgemError(
827
+ "`baseURL` is not an option: the SDK always talks to https://api.dialgem.com. Remove it."
828
+ );
829
+ }
638
830
  if (isBrowser() && !options.dangerouslyAllowBrowser) {
639
831
  throw new DialgemError(
640
832
  "The Dialgem SDK is for server-side use: running it in a browser exposes your API key to every visitor. Call Dialgem from your backend instead, or pass `dangerouslyAllowBrowser: true` if you understand the risk."
@@ -651,10 +843,9 @@ var Dialgem = class {
651
843
  assertTimeout(timeout);
652
844
  assertMaxRetries(maxRetries);
653
845
  const fetchImpl = options.fetch ?? defaultFetch();
654
- this.baseURL = (options.baseURL ?? readEnv("DIALGEM_BASE_URL") ?? DEFAULT_BASE_URL).replace(/\/+$/, "");
655
846
  const http = new HttpClient({
656
847
  apiKey,
657
- baseURL: this.baseURL,
848
+ baseURL: BASE_URL,
658
849
  timeout,
659
850
  maxRetries,
660
851
  fetch: fetchImpl,
@@ -690,9 +881,10 @@ exports.APIConnectionTimeoutError = APIConnectionTimeoutError;
690
881
  exports.APIError = APIError;
691
882
  exports.APIUserAbortError = APIUserAbortError;
692
883
  exports.AuthenticationError = AuthenticationError;
884
+ exports.BASE_URL = BASE_URL;
693
885
  exports.BadRequestError = BadRequestError;
694
886
  exports.ConflictError = ConflictError;
695
- exports.DEFAULT_BASE_URL = DEFAULT_BASE_URL;
887
+ exports.DEFAULT_SIGNATURE_TOLERANCE_MS = DEFAULT_SIGNATURE_TOLERANCE_MS;
696
888
  exports.Dialgem = Dialgem;
697
889
  exports.DialgemError = DialgemError;
698
890
  exports.InternalServerError = InternalServerError;
@@ -703,6 +895,7 @@ exports.PermissionDeniedError = PermissionDeniedError;
703
895
  exports.RateLimitError = RateLimitError;
704
896
  exports.UnprocessableEntityError = UnprocessableEntityError;
705
897
  exports.VERSION = VERSION;
898
+ exports.WEBHOOK_DELIVERY_HEADER = WEBHOOK_DELIVERY_HEADER;
706
899
  exports.WEBHOOK_EVENT_HEADER = WEBHOOK_EVENT_HEADER;
707
900
  exports.WEBHOOK_SIGNATURE_HEADER = WEBHOOK_SIGNATURE_HEADER;
708
901
  exports.WebhookVerificationError = WebhookVerificationError;