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