@curviate/sdk 0.44.0 → 0.45.0-next.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
@@ -1,11 +1,11 @@
1
1
  var __typeError = (msg) => {
2
2
  throw TypeError(msg);
3
3
  };
4
- var __accessCheck = (obj2, member, msg) => member.has(obj2) || __typeError("Cannot " + msg);
5
- var __privateGet = (obj2, member, getter) => (__accessCheck(obj2, member, "read from private field"), getter ? getter.call(obj2) : member.get(obj2));
6
- var __privateAdd = (obj2, member, value) => member.has(obj2) ? __typeError("Cannot add the same private member more than once") : member instanceof WeakSet ? member.add(obj2) : member.set(obj2, value);
7
- var __privateSet = (obj2, member, value, setter) => (__accessCheck(obj2, member, "write to private field"), setter ? setter.call(obj2, value) : member.set(obj2, value), value);
8
- var __privateMethod = (obj2, member, method) => (__accessCheck(obj2, member, "access private method"), method);
4
+ var __accessCheck = (obj, member, msg) => member.has(obj) || __typeError("Cannot " + msg);
5
+ var __privateGet = (obj, member, getter) => (__accessCheck(obj, member, "read from private field"), getter ? getter.call(obj) : member.get(obj));
6
+ var __privateAdd = (obj, member, value) => member.has(obj) ? __typeError("Cannot add the same private member more than once") : member instanceof WeakSet ? member.add(obj) : member.set(obj, value);
7
+ var __privateSet = (obj, member, value, setter) => (__accessCheck(obj, member, "write to private field"), setter ? setter.call(obj, value) : member.set(obj, value), value);
8
+ var __privateMethod = (obj, member, method) => (__accessCheck(obj, member, "access private method"), method);
9
9
 
10
10
  // src/errors.ts
11
11
  var ERROR_CODES = [
@@ -67,8 +67,10 @@ var ERROR_CODES = [
67
67
  // Curviate's OWN account-safety ceiling, not a request-rate limit and not
68
68
  // LinkedIn refusing. A 429 that never reached LinkedIn and spent nothing:
69
69
  // the budget row named by `budgetRow` is at its ceiling, set by the account's
70
- // safety policy or else the tenant default (`GET`/`PATCH /v1/safety-policy`),
71
- // or the account is outside its activity window. Deliberately absent from
70
+ // safety policy or else the Team default (`GET`/`PATCH /v1/safety-policy`),
71
+ // or the account is outside its activity window (a live write only; reads
72
+ // are never refused for the hour). Limits are always
73
+ // enforced: to send more, raise the limit. Deliberately absent from
72
74
  // RETRYABLE_CODES: a monthly row's reset can be weeks out, so backing off
73
75
  // is the wrong recovery.
74
76
  // Read `resetAt`, `safetyReason` and `safetyHint` instead.
@@ -81,6 +83,9 @@ var ERROR_CODES = [
81
83
  // retrying will not help.
82
84
  "LINKEDIN_OPERATION_NOT_SUPPORTED",
83
85
  // Checkpoint (account connect flow)
86
+ // `CHECKPOINT_EXPIRED` is 409 on every path (it was 422 on some solve
87
+ // paths): past `expires_at`, solve, poll, resend and request answer it, and
88
+ // a pending checkpoint that times out does too. Start the connection again.
84
89
  "CHECKPOINT_NOT_FOUND",
85
90
  "CHECKPOINT_EXPIRED",
86
91
  "CHECKPOINT_INVALID_CODE",
@@ -150,6 +155,9 @@ var ERROR_CODES = [
150
155
  "REACTION_NOT_FOUND",
151
156
  // Upstream failures on the connect and billing paths. All transient in the
152
157
  // ordinary sense (502/503 from a dependency), so a retry is reasonable.
158
+ // `SUBSTRATE_CAP_REACHED` is no longer sent: the account cap answers
159
+ // `503 PLATFORM_ERROR` with `retry_likely_to_succeed: false`. Kept for older
160
+ // deployments; removed in a later release.
153
161
  "SUBSTRATE_LINK_FAILED",
154
162
  "SUBSTRATE_CAP_REACHED",
155
163
  "BILLING_CHECKOUT_FAILED",
@@ -219,14 +227,19 @@ var ERROR_CODES = [
219
227
  // error body names it (`feature`), the lowest Tier that has it
220
228
  // (`required_tier`) and the Tier the team has (`current_tier`), read as
221
229
  // `feature`, `requiredTier` and `currentTier` on {@link CurviateError}; a
222
- // team Owner also gets `upgradeUrl`. Writes in Drafts, Contacts and Motions
223
- // (and Network and Unibox) need Automate; reads, exports, deletes and
224
- // stopping running work pass on any Tier.
230
+ // team Owner also gets `upgradeUrl`. When one Account lacks the feature
231
+ // while the Team has it (an Account on a seat another Team lent this one
232
+ // carries that seat's Tier), `details.account_id` names it: leave it out,
233
+ // or ask the Owner who lent the seat. Writes in Drafts, Contacts and
234
+ // Motions (and Network and Unibox) need Automate; reads, exports, deletes
235
+ // and stopping running work pass on any Tier.
225
236
  "INSUFFICIENT_ROLE",
226
237
  "TIER_REQUIRED",
227
238
  // - `TEAM_ACCESS_DENIED` (403): the request names a team (the `team` client
228
- // option, header `X-Curviate-Team`) this credential cannot act in. An API
229
- // key works in exactly one team. Drop the option, or use that team's key.
239
+ // option or `withTeam`, header `X-Curviate-Team`) this credential cannot
240
+ // act in. A key may work in one Team or in all of a person's Teams: a Team
241
+ // key accepts only its own Team, an All-Teams key only the Teams its person
242
+ // belongs to. Name a Team the key works in, or use that Team's key.
230
243
  // - `ACCOUNT_NOT_SHARED` (403): the person who connected the account turned
231
244
  // its team access off; only they can use it.
232
245
  // - `ACCOUNT_OWNER_ONLY` (403): only the person who connected the account can
@@ -296,12 +309,12 @@ var ERROR_CODES = [
296
309
  //
297
310
  // - `MOTION_NOT_STARTABLE` (422): `start` or `resume` found gaps; `errors`
298
311
  // lists every one as `{path, rule}` (a Contact list, a sending account, a
299
- // timezone, a Sequence). Fix them and retry.
312
+ // Sequence). Fix them and retry.
300
313
  // - `MOTION_STATUS_CONFLICT` (409): the operation does not apply in the
301
314
  // Motion's status; the message names it and the operations it allows.
302
315
  // Only a draft can be deleted: archive instead.
303
316
  // - `ENROLLMENT_NOT_ACTIVE` (409): the Enrollment's status does not take
304
- // this operation (stop and pause: pending, active or paused; events:
317
+ // this operation (stop and pause: queued, active or paused; events:
305
318
  // active or paused; resume: active, paused or finished, not when the
306
319
  // account or Contact is gone).
307
320
  // - `STEP_IN_FLIGHT` (409): a Step of the Enrollment is being sent right
@@ -328,6 +341,64 @@ var ERROR_CODES = [
328
341
  "KNOWN_CHAT",
329
342
  "CONTACT_NOT_ENROLLABLE",
330
343
  "CHAT_NOT_ONE_TO_ONE",
344
+ // Dashboard v2 surface.
345
+ //
346
+ // - `CHAT_NOT_VISIBLE` (409): a start-chat or InMail to a person the account
347
+ // holds a private 1:1 with that you may not read. Nothing is sent; the body
348
+ // reveals nothing of the conversation.
349
+ // - `DO_NOT_CONTACT` (409): the person is on your team's do-not-contact list
350
+ // (an invitation, a chat start or send, InMail, follow, endorse, a reaction
351
+ // or comment, a live profile read). Nothing is sent.
352
+ // - `TEAM_REQUIRED` (400): a key that works in all of your Teams called a
353
+ // Team-level operation without naming the Team. Pass `team` in the client
354
+ // config, or call through `curviate.withTeam(team)`; `teams.list()` lists
355
+ // yours. A call on one of your accounts takes that account's Team.
356
+ // - `SEAT_LENT` (409): the seat is lent to another Team. A connect naming it,
357
+ // a cancel of it, and a connect that finishes just as a lend lands are all
358
+ // refused. Move it back first (`billing.seats.return`; only once no Account
359
+ // uses it). A suspended lent seat reads `NO_ACTIVE_SEAT` with its own
360
+ // message.
361
+ // - `LINKEDIN_SIGN_IN_REQUIRED` (403, user_fixable): LinkedIn wants the
362
+ // person to sign in on linkedin.com before it accepts this connect. Sign
363
+ // in there, then retry, or connect with cookies from a browser where it is
364
+ // signed in.
365
+ // - `EXPORT_IN_PROGRESS` (409): your team already has an export queued or
366
+ // running (`details.export_id`). Retry when it finishes.
367
+ // - `EXPORT_NOT_READY` (409): the export is queued, running or failed; its
368
+ // file is not there to download yet.
369
+ // - `EXPORT_EXPIRED` (410): the export's 24 h are over and its file is
370
+ // deleted. Run it again.
371
+ // - `EXPORT_REMOVED` (410): a Contact was deleted, which removes your team's
372
+ // Contact-level exports. Run it again.
373
+ // - `NOTHING_TO_REDELIVER` (409): a redeliver of a delivery that was never
374
+ // sent (`skipped_no_email`).
375
+ // - `REDELIVERY_EXPIRED` (410): a redeliver at or after the delivery's
376
+ // `redeliverable_until` (receipt + 24 h); its request is no longer kept.
377
+ // - `WEBHOOK_DISABLED` (409): a redeliver on a webhook that is switched off.
378
+ // Switch it on first.
379
+ // - `DELIVERY_IN_FLIGHT` (409): an attempt of that delivery is being sent
380
+ // right now. Retry shortly.
381
+ // - `WEBHOOK_TEMPLATE_INVALID` (422): the webhook's `body_template` fails
382
+ // the lint; nothing was saved. Every finding is in `errors`, each with
383
+ // `line` and `col` (1-based) into the template; `validateWebhookTemplate`
384
+ // gives the same verdict offline.
385
+ // - `INCOMING_URL_RETIRED` (410): a post to an incoming event URL that was
386
+ // rotated out (after its 24 h) or deleted.
387
+ "CHAT_NOT_VISIBLE",
388
+ "DO_NOT_CONTACT",
389
+ "TEAM_REQUIRED",
390
+ "SEAT_LENT",
391
+ "LINKEDIN_SIGN_IN_REQUIRED",
392
+ "EXPORT_IN_PROGRESS",
393
+ "EXPORT_NOT_READY",
394
+ "EXPORT_EXPIRED",
395
+ "EXPORT_REMOVED",
396
+ "NOTHING_TO_REDELIVER",
397
+ "REDELIVERY_EXPIRED",
398
+ "WEBHOOK_DISABLED",
399
+ "DELIVERY_IN_FLIGHT",
400
+ "WEBHOOK_TEMPLATE_INVALID",
401
+ "INCOMING_URL_RETIRED",
331
402
  // Client-side, never sent by the API. `CLIENT_TIMEOUT`: no response within
332
403
  // the client's `timeout` (default 30 s, per attempt). A read is retried
333
404
  // first; a write is not, and may still have reached the API, so read back
@@ -360,6 +431,7 @@ var CurviateError = class _CurviateError extends Error {
360
431
  this.requiredTier = init.requiredTier;
361
432
  this.currentTier = init.currentTier;
362
433
  this.upgradeUrl = init.upgradeUrl;
434
+ this.details = init.details;
363
435
  Object.setPrototypeOf(this, _CurviateError.prototype);
364
436
  }
365
437
  /**
@@ -391,6 +463,7 @@ var CurviateError = class _CurviateError extends Error {
391
463
  if (this.requiredTier !== void 0) json.requiredTier = this.requiredTier;
392
464
  if (this.currentTier !== void 0) json.currentTier = this.currentTier;
393
465
  if (this.upgradeUrl !== void 0) json.upgradeUrl = this.upgradeUrl;
466
+ if (this.details !== void 0) json.details = this.details;
394
467
  return json;
395
468
  }
396
469
  };
@@ -398,6 +471,9 @@ function isCurviateError(err) {
398
471
  return err instanceof CurviateError;
399
472
  }
400
473
 
474
+ // package.json
475
+ var version = "0.45.0-next.1";
476
+
401
477
  // src/config.ts
402
478
  var DEFAULT_BASE_URL = "https://api.curviate.com";
403
479
  var DEFAULT_TIMEOUT_MS = 3e4;
@@ -419,13 +495,22 @@ function resolveConfig(config) {
419
495
  retryLikelyToSucceed: false
420
496
  });
421
497
  }
498
+ if (config.client !== void 0 && !/^[\x21-\x7e]+$/.test(String(config.client))) {
499
+ throw new CurviateError({
500
+ code: "INVALID_REQUEST",
501
+ message: "client, when given, must be <name>/<version>: non-empty, no spaces.",
502
+ userFixable: true,
503
+ retryLikelyToSucceed: false
504
+ });
505
+ }
422
506
  return Object.freeze({
423
507
  apiKey: config.apiKey,
424
508
  baseUrl: config.baseUrl ?? DEFAULT_BASE_URL,
425
509
  timeout: config.timeout ?? DEFAULT_TIMEOUT_MS,
426
510
  maxRetries: config.maxRetries ?? DEFAULT_MAX_RETRIES,
427
511
  fetch: config.fetch,
428
- ...config.team !== void 0 ? { team: config.team } : {}
512
+ ...config.team !== void 0 ? { team: config.team } : {},
513
+ client: config.client ?? `sdk/${version}`
429
514
  });
430
515
  }
431
516
 
@@ -461,7 +546,7 @@ var RETRYABLE_CODES = /* @__PURE__ */ new Set([
461
546
  ]);
462
547
  var WRITE_METHODS = /* @__PURE__ */ new Set(["POST", "PATCH", "PUT", "DELETE"]);
463
548
  function defaultSleep(ms) {
464
- return new Promise((resolve) => setTimeout(resolve, ms));
549
+ return new Promise((resolve2) => setTimeout(resolve2, ms));
465
550
  }
466
551
  function parseRetryAfterMs(header) {
467
552
  if (header == null) return void 0;
@@ -523,7 +608,8 @@ async function errorFromResponse(res) {
523
608
  // which is the field that says what to do instead: switch work.
524
609
  ...typeof env?.retry_after === "number" && Number.isFinite(env.retry_after) ? { retryAfterSeconds: env.retry_after } : {},
525
610
  // `reset_at` is null-BEARING: null means "no clock frees this" (the
526
- // pending_invites gauge, or an InMail credit exhaustion), which is a
611
+ // pending_invites gauge, an InMail credit exhaustion, or an unknown zone
612
+ // with no candidate), which is a
527
613
  // different fact from the field being absent, so both survive the round
528
614
  // trip.
529
615
  ...typeof env?.reset_at === "string" || env?.reset_at === null ? { resetAt: env.reset_at } : {},
@@ -539,7 +625,8 @@ async function errorFromResponse(res) {
539
625
  ...typeof env?.feature === "string" ? { feature: env.feature } : {},
540
626
  ...typeof env?.required_tier === "string" || env?.required_tier === null ? { requiredTier: env.required_tier } : {},
541
627
  ...typeof env?.current_tier === "string" || env?.current_tier === null ? { currentTier: env.current_tier } : {},
542
- ...typeof env?.upgrade_url === "string" ? { upgradeUrl: env.upgrade_url } : {}
628
+ ...typeof env?.upgrade_url === "string" ? { upgradeUrl: env.upgrade_url } : {},
629
+ ...env?.details !== null && typeof env?.details === "object" && !Array.isArray(env.details) ? { details: env.details } : {}
543
630
  });
544
631
  return { err, refusesRetry: env?.retry_likely_to_succeed === false };
545
632
  }
@@ -552,7 +639,8 @@ function retryDelay(err, attempt, jitter) {
552
639
  }
553
640
  function buildInit(method, opts, signal) {
554
641
  const headers = {
555
- authorization: `Bearer ${opts.apiKey}`
642
+ authorization: `Bearer ${opts.apiKey}`,
643
+ "curviate-client": opts.client
556
644
  };
557
645
  if (opts.team !== void 0) headers["x-curviate-team"] = opts.team;
558
646
  const init = { method, headers, signal };
@@ -588,9 +676,9 @@ function buildUrl(path, opts) {
588
676
  }
589
677
  return url.toString();
590
678
  }
591
- async function parseSuccess(res) {
679
+ async function parseSuccess(res, bytes) {
592
680
  const ct = res.headers.get("content-type") ?? "";
593
- if (ct.includes("application/json")) {
681
+ if (!bytes && ct.includes("application/json")) {
594
682
  return await res.json();
595
683
  }
596
684
  return await res.arrayBuffer();
@@ -627,7 +715,7 @@ async function execute(method, path, opts) {
627
715
  }
628
716
  clearTimeout(timer);
629
717
  if (res.ok) {
630
- return parseSuccess(res);
718
+ return parseSuccess(res, opts.bytes);
631
719
  }
632
720
  const { err, refusesRetry } = await errorFromResponse(res);
633
721
  const namesBudgetRow = err.budgetRow !== void 0;
@@ -687,6 +775,7 @@ function createContext(config, accountId) {
687
775
  }
688
776
  return execute(args.method, path, {
689
777
  apiKey: config.apiKey,
778
+ client: config.client,
690
779
  baseUrl: config.baseUrl,
691
780
  timeout: config.timeout,
692
781
  maxRetries: config.maxRetries,
@@ -694,7 +783,8 @@ function createContext(config, accountId) {
694
783
  ...config.team !== void 0 ? { team: config.team } : {},
695
784
  ...args.query !== void 0 ? { query: args.query } : {},
696
785
  ...args.body !== void 0 ? { body: args.body } : {},
697
- ...args.rawBody !== void 0 ? { rawBody: args.rawBody } : {}
786
+ ...args.rawBody !== void 0 ? { rawBody: args.rawBody } : {},
787
+ ...args.bytes ? { bytes: true } : {}
698
788
  });
699
789
  };
700
790
  return { request, accountId };
@@ -730,21 +820,44 @@ function pageable(fetchPage, params) {
730
820
  });
731
821
  }
732
822
 
823
+ // src/internal/query.ts
824
+ function commaLists(params, ...keys) {
825
+ if (params === void 0) return void 0;
826
+ const out = { ...params };
827
+ for (const k of keys) {
828
+ const v = out[k];
829
+ if (Array.isArray(v)) out[k] = v.length ? v.join(",") : void 0;
830
+ }
831
+ return out;
832
+ }
833
+
733
834
  // src/resources/accounts.ts
734
835
  var AccountsResource = class {
735
836
  constructor(ctx) {
736
837
  this.ctx = ctx;
737
838
  }
738
839
  /**
739
- * List the tenant's connected LinkedIn accounts, cursor-paginated.
740
- *
741
- * Each item also carries a small set of cached account-detail fields
742
- * (`username`, `premium_id`, `public_identifier`, `substrate_created_at`,
743
- * `signatures`, `groups`) populated by an async background enrichment,
744
- * `null`/`[]` until the account's first enrichment pass completes.
745
- *
746
- * Each item carries `external_id` (your own id for the account's end user,
747
- * set on `auth.intent` or `update`) and `metadata`, both `null` when unset.
840
+ * List the tenant's connected LinkedIn accounts, cursor-paginated. An
841
+ * account disconnected when its free trial ended stays listed (`status_reason:
842
+ * "trial_ended"`); one you disconnected does not.
843
+ *
844
+ * Each item carries enough to render an account row without a second call:
845
+ * its profile (`full_name`, `headline`, `photo_url`, `profile_url`,
846
+ * `public_identifier`, `location`; `photo_url` is a copy we store and refresh
847
+ * daily, so it does not expire), `status` and
848
+ * `status_reason` (why it is disconnected or needs a reconnect: `trial_ended`
849
+ * or `proxy`), `warm_up` (the active ramp), `inmail_balance` (the credits
850
+ * LinkedIn last reported and when, `null` until observed), `motions_count`
851
+ * with the first 20 Motions in `motion_refs`, and `quotas`: three of the
852
+ * account's safety rows (`connection_requests_no_note`,
853
+ * `messages_first_degree`, `inmail`) with `remaining`, absent on an account
854
+ * you can see but not operate. `connection_requests_no_note` now counts
855
+ * every invitation, noted or not: read it as "Invitations". `get` carries every row. Reading the list
856
+ * never calls LinkedIn.
857
+ *
858
+ * Each item also carries `external_id` (your own id for the account's end
859
+ * user, set on `auth.intent` or `update`) and `metadata`, both `null` when
860
+ * unset.
748
861
  *
749
862
  * @param params - optional `limit` (1-250), `cursor` (from a prior page), and
750
863
  * `external_id` (exact match: only that end user's accounts).
@@ -794,25 +907,61 @@ var AccountsResource = class {
794
907
  * `quotas[]` REPLACED its earlier `quota_name` / `remaining` / `total` /
795
908
  * `reset_time` / `recommended_throttle_hint` shape. Each row now reports
796
909
  * `budget_row`, `kind`, `used`, `ceiling`, `effective_ceiling`, `band`,
797
- * `posture`, `over_default`, `halt` and the window fields. **Compare `used`
798
- * against `effective_ceiling`, not `ceiling`**: the effective one is the
799
- * ceiling with the account's warm-up ramp applied, and on a new account the
800
- * two differ by roughly a factor of seven. `posture` says what happens at the
801
- * ceiling and defaults to `warn`, so nothing here refuses until it is
802
- * configured to. Two rows are not counters and say so in `kind`:
910
+ * `over_default`, `halt` and the window fields. **Compare `used` against
911
+ * `effective_ceiling`, not `ceiling`**: the effective one is the ceiling
912
+ * with the account's warm-up ramp applied, and on a new account the two
913
+ * differ by roughly a factor of seven. Limits are always enforced: at the
914
+ * effective ceiling the action is refused with `BUDGET_EXHAUSTED`; to send
915
+ * more, raise the limit. Two rows are not counters and say so in `kind`:
803
916
  * `total_actions` (`tally`) and `pending_invites` (`gauge`, whose `used` is
804
917
  * `null` until something has observed it).
805
918
  *
919
+ * **Same name, new meaning:** `connection_requests_no_note` counts all
920
+ * invitations, so its `used` is the invitation total (a noted one also
921
+ * counts on `connection_requests_with_note` where that row has a ceiling:
922
+ * Free profiles, 3 a month). The strict rows (invitations, `inmail`,
923
+ * `profile_views`) also carry `target` (what the account aims at per
924
+ * window, below the ceiling; `green` is now this target) and
925
+ * `effective_target` (target times warm-up times health), both `null` on
926
+ * other rows and read-only. Aim at the target, not the ceiling.
927
+ *
928
+ * `health` is Curviate's inference of how LinkedIn sees the account:
929
+ * `{ state, factor, reasons, until }`, `state` one of `healthy`, `watch`,
930
+ * `slowed` or `stopped`. It multiplies with warm-up. `health` is absent on
931
+ * an account you can see but not operate.
932
+ *
933
+ * Curviate sees only what it sends. Invitations or messages sent by hand on
934
+ * LinkedIn, or through another automation tool on the same account, are
935
+ * invisible to it and to every limit here; LinkedIn's refusal is the only
936
+ * way they show up. Do not run another automation tool on a connected
937
+ * account.
938
+ *
806
939
  * The per-minute REQUEST ceiling is a different thing and is deliberately
807
940
  * NOT in this array: it protects Curviate's own servers rather than the
808
- * LinkedIn account, and it is always enforced with `429` whatever the
809
- * posture.
810
- *
811
- * `activity_window` applies to every account, with or without a zone set.
812
- * `timezone` and `timezone_unset` report what YOU set; read
813
- * `activity_window.effective_timezone` for the zone the window is actually
814
- * read in and `activity_window.source` (`account`, `browser`, `location` or
815
- * `default`) for where it came from.
941
+ * LinkedIn account, and it is answered with `429 RATE_LIMIT_ACCOUNT`.
942
+ *
943
+ * `activity_window` says which time zone the account's active hours are
944
+ * read in. Nobody sets it: it follows where the account connects from.
945
+ * `effective_timezone` is that IANA zone, or `null` while it is unknown;
946
+ * `source` says where it came from (`country`, `region`, `detected` or
947
+ * `none`); `zone_state` is `known`, `pinned` (a check agreed on one zone,
948
+ * kept until the next location change) or `unknown`, when the account acts
949
+ * only in the hours every zone in `fallback_timezones` shares (an empty list
950
+ * means the window is closed). A live write outside the window is refused
951
+ * with `BUDGET_EXHAUSTED` (`safetyReason: "activity_window"`); reads are
952
+ * never affected.
953
+ *
954
+ * `warm_up` is the account's ramp: `enabled` (the switch, default `true`;
955
+ * set it with `PATCH /v1/{account_id}/safety-policy` and
956
+ * `warm_up: { enabled }`), `dormant` (no action for more than 30 days),
957
+ * `state` and the `triggers` that apply now (`connect`, `dormant`,
958
+ * `post_restriction`). Off skips the connect and quiet ramps; recovery after
959
+ * a LinkedIn restriction still applies.
960
+ *
961
+ * `connection_location` carries `checked_at` (the last location check, or
962
+ * `null`), `detected_country` (where your own proxy was found to connect
963
+ * from, or `null`) and `flags` (`country_mismatch`, `zone_moved`). It never
964
+ * carries an IP.
816
965
  *
817
966
  * `account_states[]` lists platform conditions the account is in right now,
818
967
  * additive to `status` and independent of it: `recruiter_session_evicted`,
@@ -853,11 +1002,20 @@ var AccountsResource = class {
853
1002
  * proxy can only take a `country` together with `proxy: null`. The
854
1003
  * `proxy.password`, if given, is never returned.
855
1004
  *
1005
+ * `save_connections_as_contacts` (Automate; on by default) saves the
1006
+ * account's Connections as Contacts: each person becomes one Contact in your
1007
+ * team, an existing Contact is only filled in, do-not-contact people are
1008
+ * skipped. `true` also saves the Connections already synced; `false` keeps
1009
+ * the Contacts saved so far. Without Automate it throws `TIER_REQUIRED`, and
1010
+ * `get` omits the field. `save_connections_paused_reason` reads
1011
+ * `"contact_limit_reached"` while your team holds its 100,000 Contacts.
1012
+ *
856
1013
  * Returns the account (as `get` returns it, without `quotas` and
857
1014
  * `event_log`). After a location change its `connection_location` is re-read,
858
1015
  * so it shows where the account really connects from now. A change that did
859
1016
  * not land where asked throws `CONNECTION_LOCATION_UNAVAILABLE`, whose
860
- * `connectionLocation` carries the actual location.
1017
+ * `connectionLocation` carries the actual location. The account's time zone
1018
+ * follows its connection location: there is no zone to set.
861
1019
  *
862
1020
  * @example
863
1021
  * await curviate.accounts.update("acc_123", { external_id: "usr_42", metadata: { plan: "pro" } });
@@ -865,6 +1023,9 @@ var AccountsResource = class {
865
1023
  * @example
866
1024
  * const account = await curviate.accounts.update("acc_123", { country: "DE" });
867
1025
  * console.log(account.connection_location?.country); // "DE"
1026
+ *
1027
+ * @example
1028
+ * await curviate.accounts.update("acc_123", { save_connections_as_contacts: false });
868
1029
  */
869
1030
  update(accountId, body) {
870
1031
  return this.ctx.request({
@@ -957,6 +1118,101 @@ var AccountsResource = class {
957
1118
  path: apiPath`/v1/billing/seats/${seatId}/cancel/revert`
958
1119
  });
959
1120
  }
1121
+ /**
1122
+ * The account's own numbers, one snapshot per day, oldest first: 1st-degree
1123
+ * `connections`, `followers`, `profile_viewers_90d`,
1124
+ * `search_appearances_week`, `post_impressions_7d`, and `ssi` (Sales
1125
+ * Navigator accounts only, else `null`). Read once a day inside the
1126
+ * account's activity window and stored: this makes no LinkedIn call. `from`
1127
+ * and `to` are the account's local days (default the last 90 days; at most
1128
+ * 366 days across); a day with no snapshot has no item. One response holds
1129
+ * the whole range (`cursor` is always `null`).
1130
+ * `GET /v1/accounts/{account_id}/metrics`
1131
+ *
1132
+ * @example
1133
+ * const { items } = await curviate.accounts.metrics("acc_123", { from: "2026-08-01", to: "2026-10-28" });
1134
+ */
1135
+ metrics(accountId, params) {
1136
+ return this.ctx.request({
1137
+ method: "GET",
1138
+ path: apiPath`/v1/accounts/${accountId}/metrics`,
1139
+ ...params !== void 0 ? { query: params } : {}
1140
+ });
1141
+ }
1142
+ /**
1143
+ * One account at a glance, with no LinkedIn call: `profile`, its daily
1144
+ * `metrics` snapshot (`null` before the first one, or when you may not
1145
+ * operate the account), `limits` (its three outreach rows now),
1146
+ * `activity` (its latest 10 actions and their outcomes, metadata only), and
1147
+ * its results in the window (`performance`, `daily`, its `motions`) as the
1148
+ * team overview reads them scoped to it. `profile`, `metrics`, `limits` and
1149
+ * `activity` do not follow the window. The window, `motion_id`, `origin`,
1150
+ * `include`, `fields`, `sort`, `limit` and `compare` work as on
1151
+ * `analytics.motions.list`; with none of `include` and `fields` the body is
1152
+ * trimmed to a summary. `flags` includes `account_attention` and
1153
+ * `limit_reached` for this account.
1154
+ * `GET /v1/accounts/{account_id}/overview`
1155
+ *
1156
+ * @example
1157
+ * const { performance, flags } = await curviate.accounts.overview("acc_123", { range: "30d" });
1158
+ */
1159
+ overview(accountId, params) {
1160
+ return this.ctx.request({
1161
+ method: "GET",
1162
+ path: apiPath`/v1/accounts/${accountId}/overview`,
1163
+ query: commaLists(params, "motion_id", "origin", "include", "fields", "sort")
1164
+ });
1165
+ }
1166
+ /**
1167
+ * Read an account's safety policy: every configurable field for every
1168
+ * budget row, and where each value was inherited from. Also carries the
1169
+ * derived `warm_up` (`enabled`, `dormant`, `state`, `factor`, `triggers`)
1170
+ * and `activity_window` (the zone the hours are read in, from where the
1171
+ * account connects) and `auto_withdraw` (default `true`: Curviate withdraws
1172
+ * its own invitations older than 14 days once more than 400 are pending;
1173
+ * hand-sent ones are never touched). Rows carry `target` and
1174
+ * `effective_target` (read-only). Limits are always enforced.
1175
+ * `GET /v1/{account_id}/safety-policy`
1176
+ *
1177
+ * @example
1178
+ * const { warm_up } = await curviate.accounts.getSafetyPolicy("acc_123");
1179
+ */
1180
+ getSafetyPolicy(accountId) {
1181
+ return this.ctx.request({
1182
+ method: "GET",
1183
+ path: apiPath`/v1/${accountId}/safety-policy`
1184
+ });
1185
+ }
1186
+ /**
1187
+ * Update an account's safety policy. Send only the fields you are changing;
1188
+ * `null` clears an override back to the Team or Curviate default. Limits are
1189
+ * always enforced: to send more, raise the limit. Returns the policy as
1190
+ * `getSafetyPolicy` reads it.
1191
+ *
1192
+ * `auto_withdraw` (boolean) switches the automatic withdrawal of old
1193
+ * Curviate-sent invitations; a change is ledgered. A row's `green` is its
1194
+ * target. `warm_up: { enabled }` turns the account's warm-up ramp on or off (default
1195
+ * on; off skips the connect and quiet ramps, recovery after a LinkedIn
1196
+ * restriction still applies). `active_hours` sets the account's own days and
1197
+ * local hours; a row's own `activity_window_start` / `activity_window_end`
1198
+ * win over it. Read-only fields (`activity_window`, a row's
1199
+ * `effective_ceiling`, `over_default`, `warm_up_state` ...) are accepted and
1200
+ * ignored, so a read document can be sent back unchanged. A time zone is not
1201
+ * a setting: `timezone` and a row's `activity_window_timezone` other than
1202
+ * `null` are `400 INVALID_REQUEST`; change the account's `country` or own
1203
+ * `proxy` instead.
1204
+ * `PATCH /v1/{account_id}/safety-policy`
1205
+ *
1206
+ * @example
1207
+ * await curviate.accounts.updateSafetyPolicy("acc_123", { warm_up: { enabled: false } });
1208
+ */
1209
+ updateSafetyPolicy(accountId, body) {
1210
+ return this.ctx.request({
1211
+ method: "PATCH",
1212
+ path: apiPath`/v1/${accountId}/safety-policy`,
1213
+ body
1214
+ });
1215
+ }
960
1216
  };
961
1217
 
962
1218
  // src/resources/auth.ts
@@ -973,7 +1229,36 @@ var AuthResource = class {
973
1229
  * success (200 re-authenticated in place / 201 new), or a checkpoint
974
1230
  * challenge (202) when LinkedIn requires verification; resolve it with
975
1231
  * {@link solveCheckpoint} (code) or {@link pollCheckpoint} (mobile-app
976
- * approval).
1232
+ * approval, or `checking`: LinkedIn is checking the sign-in, nothing to
1233
+ * answer, keep polling).
1234
+ *
1235
+ * **Checkpoints.** `challenge_type` may be `checking` (poll it, no action)
1236
+ * or `phone_register` (LinkedIn wants a phone number: solve it with
1237
+ * `(+CC)number`, then answer the code check that follows). On `otp` and
1238
+ * `otp_or_mobile_app_approval`, `channel` says where LinkedIn sent the code
1239
+ * (`email`, `sms`, `whatsapp`, or `null` when not known). `expires_at` is
1240
+ * when Curviate stops accepting an answer; a chained checkpoint keeps the
1241
+ * sign-in's original `expires_at`. After it, solve, poll, resend and request
1242
+ * answer `409 CHECKPOINT_EXPIRED`: start the connection again.
1243
+ *
1244
+ * **Refusals.** A cookie connect LinkedIn rejects, for any reason, is
1245
+ * `401 LINKEDIN_COOKIE_INVALID`. `403 LINKEDIN_SIGN_IN_REQUIRED`: LinkedIn
1246
+ * wants the person to sign in on linkedin.com first; sign in there, then
1247
+ * retry, or connect with cookies from a browser where it is signed in. An
1248
+ * intent that times out before any checkpoint is
1249
+ * `503 LINKEDIN_SERVICE_UNAVAILABLE`; the platform's account cap is
1250
+ * `503 PLATFORM_ERROR` with `retryLikelyToSucceed: false`.
1251
+ *
1252
+ * `user_languages` (cookie connects only; a credentials connect ignores
1253
+ * it): the language preferences of the browser the cookies came from, most
1254
+ * preferred first, at most 5 BCP 47 tags (`en-US`), each at most 35
1255
+ * characters.
1256
+ *
1257
+ * `recruiter_contract_id`: the Sales Navigator or Recruiter contract the
1258
+ * account sends from, for a LinkedIn account that holds several. It is
1259
+ * selected once the Account is connected; the answer then carries
1260
+ * `contract_selected`, and `contract_selected: false` means connected, not
1261
+ * selected (select it again with a reconnect).
977
1262
  *
978
1263
  * For `auth_method: "cookie"`, `user_agent` is **required**; connecting by
979
1264
  * session cookie without one is rejected with `INVALID_REQUEST`. It stays
@@ -1021,7 +1306,14 @@ var AuthResource = class {
1021
1306
  * to keep the account's configured one. The account's `connection_location`
1022
1307
  * says where it connects from; when a connect re-attached an account you
1023
1308
  * already had and moved it, `connection_location.previous_country` names
1024
- * where it was.
1309
+ * where it was. The account's time zone follows its connection location;
1310
+ * `timezone` is deprecated and ignored.
1311
+ *
1312
+ * `warm_up: false` connects a NEW account with warm-up off (no connect ramp,
1313
+ * no ramp after a quiet spell; recovery after a LinkedIn restriction still
1314
+ * applies). Default `true`. Sent with `account_id` it is
1315
+ * `400 INVALID_REQUEST`: change an existing account's switch on its safety
1316
+ * policy instead.
1025
1317
  *
1026
1318
  * @example
1027
1319
  * const result = await curviate.auth.intent({
@@ -1040,12 +1332,21 @@ var AuthResource = class {
1040
1332
  });
1041
1333
  }
1042
1334
  /**
1043
- * Solve a checkpoint challenge by submitting an OTP / 2FA code (or the
1044
- * special value `TRY_ANOTHER_WAY` to switch challenge method).
1335
+ * Solve a checkpoint challenge by submitting an OTP / 2FA code, a phone
1336
+ * number for `phone_register` (`(+CC)number`; `code` accepts `( ) +`), or
1337
+ * the special value `TRY_ANOTHER_WAY`.
1045
1338
  *
1046
- * Returns the connected account (201) or, when LinkedIn chains a second
1339
+ * `TRY_ANOTHER_WAY` asks LinkedIn for another verification method and
1340
+ * counts no attempt; the answer is the method LinkedIn offers instead (202),
1341
+ * with the same `expires_at`. It is `400 INVALID_REQUEST` on
1342
+ * `contract_selection`, `challenge_selection`, `phone_register` and
1343
+ * `checking`.
1344
+ *
1345
+ * Returns the connected account (201, with `contract_selected` when
1346
+ * `recruiter_contract_id` was sent) or, when LinkedIn chains a second
1047
1347
  * challenge (another checkpoint, 202); resolve that one with a further
1048
- * `solveCheckpoint` call for the same `accountId`.
1348
+ * `solveCheckpoint` call for the same `accountId`. Past `expires_at` it is
1349
+ * `409 CHECKPOINT_EXPIRED` (409 on every path now).
1049
1350
  *
1050
1351
  * @param accountId - the (provisional) `account_id` from the 202 response.
1051
1352
  * @param body - the verification `code`.
@@ -1091,11 +1392,14 @@ var AuthResource = class {
1091
1392
  });
1092
1393
  }
1093
1394
  /**
1094
- * Poll for mobile-app approval of a pending checkpoint challenge until it
1095
- * leaves `pending`.
1395
+ * Poll an action-based checkpoint (`mobile_app_approval`,
1396
+ * `otp_or_mobile_app_approval`, `checking`) until it leaves `pending`.
1096
1397
  *
1097
- * On `status: "expired"`, the response carries `challenge_type`
1098
- * (`"mobile_app_approval"`) and a human-readable `recovery_hint`.
1398
+ * A transient error on Curviate's side answers `pending`: keep polling.
1399
+ * Only a refusal answers `failed`, and a terminal refusal throws its own
1400
+ * code. Past `expires_at` (also after the expiry sweep) the poll throws
1401
+ * `409 CHECKPOINT_EXPIRED`; there is no `status: "expired"` answer. Start
1402
+ * the connection again.
1099
1403
  *
1100
1404
  * @param accountId - the (provisional) `account_id` from the 202 response.
1101
1405
  */
@@ -1143,6 +1447,15 @@ var MessagingResource = class {
1143
1447
  * Company pages are reply-only and cannot start a conversation this way.
1144
1448
  * Reply to an existing one instead, with `sendMessage()` using a
1145
1449
  * `COMPANY_` chat id from `inboxes.listChats()`.
1450
+ *
1451
+ * When the Account already has a private
1452
+ * conversation with this person that you may not read, this throws
1453
+ * `CHAT_NOT_VISIBLE` (409) and sends nothing. Ask the person who connected
1454
+ * the Account to reply, or to sync the full inbox. It is detected from what
1455
+ * Curviate stored, so a thread older than 90 days that Curviate never
1456
+ * touched is not detected. A person on the do-not-contact list throws
1457
+ * `DO_NOT_CONTACT` (409). A send marks the chat read, and a disconnected
1458
+ * Account throws `REAUTH_REQUIRED` (409).
1146
1459
  */
1147
1460
  startChat(body) {
1148
1461
  return this.ctx.request({
@@ -1169,6 +1482,10 @@ var MessagingResource = class {
1169
1482
  /**
1170
1483
  * Mark a chat read or unread. `PATCH /v1/{account_id}/chats/{chat_id}`
1171
1484
  * Body: `{read}`.
1485
+ *
1486
+ * `read: false` makes the Unibox chat's `marked_unread` read
1487
+ * `{origin: "api", at}`. A later send through the API marks the chat read
1488
+ * again. A disconnected Account throws `REAUTH_REQUIRED` (409).
1172
1489
  */
1173
1490
  markChatRead(chatId, body) {
1174
1491
  return this.ctx.request({
@@ -1188,6 +1505,18 @@ var MessagingResource = class {
1188
1505
  * first message rather than from the chat. Ordinary (non-InMail) messages
1189
1506
  * return `specifics: {}`.
1190
1507
  *
1508
+ * A message the connected Account sent carries `origin`: `motion`, `unibox`,
1509
+ * `api` (any API key or MCP grant) or `linkedin` (sent on LinkedIn itself).
1510
+ * It is derived by Curviate, omitted when unknown, absent on a received
1511
+ * message and never on a chat's `last_message`. `is_deleted` is `true` for a
1512
+ * deleted message. For an API key or MCP grant, a message's `quoted` is
1513
+ * omitted when the quoted message is from before the Motion chat's visible
1514
+ * start, the same line the message list stops at.
1515
+ *
1516
+ * On a disconnected Account the page comes from Curviate's stored copy
1517
+ * alone: `coverage.complete` is `false` when the stored history is not
1518
+ * whole, and an attachment reads `unavailable: true`.
1519
+ *
1191
1520
  * A CHAT READ CARRIES NO SUBJECT AT ALL, including on its embedded
1192
1521
  * `last_message`: the preview is deliberately narrower than a message read, so
1193
1522
  * that a chat read returns the same fields whether it was answered from
@@ -1222,6 +1551,13 @@ var MessagingResource = class {
1222
1551
  * `{ kind: "personal" }`. Never infer the acting identity from a
1223
1552
  * message's `sender` field, only from `sent_as`.
1224
1553
  *
1554
+ *
1555
+ * A person on the Team's do-not-contact list throws `DO_NOT_CONTACT` (409);
1556
+ * nothing is sent.
1557
+ *
1558
+ * Marks the chat read (its `unread` turns `false`, `marked_unread` clears,
1559
+ * and `chat.updated` reports it). A disconnected Account throws
1560
+ * `REAUTH_REQUIRED` (409); nothing is sent.
1225
1561
  * @example
1226
1562
  * const acc = curviate.account("acc_YOUR_ACCOUNT_ID");
1227
1563
  * const result = await acc.messaging.sendMessage("COMPANY_83734124_2-YTQ3ODU3Njgt", {
@@ -1294,7 +1630,18 @@ var MessagingResource = class {
1294
1630
  path: apiPath`/v1/{account_id}/chats/${chatId}/messages/${messageId}/attachments/${attachmentId}`
1295
1631
  });
1296
1632
  }
1297
- /** Send an InMail. `POST /v1/{account_id}/messages/inmail` */
1633
+ /**
1634
+ * Send an InMail. `POST /v1/{account_id}/messages/inmail`
1635
+ *
1636
+ * When the Account already has a private
1637
+ * conversation with this person that you may not read, this throws
1638
+ * `CHAT_NOT_VISIBLE` (409) and sends nothing. Ask the person who connected
1639
+ * the Account to reply, or to sync the full inbox. It is detected from what
1640
+ * Curviate stored, so a thread older than 90 days that Curviate never
1641
+ * touched is not detected. A person on the do-not-contact list throws
1642
+ * `DO_NOT_CONTACT` (409). A send marks the chat read, and a disconnected
1643
+ * Account throws `REAUTH_REQUIRED` (409).
1644
+ */
1298
1645
  sendInMail(body) {
1299
1646
  return this.ctx.request({
1300
1647
  method: "POST",
@@ -1399,6 +1746,9 @@ var UsersResource = class {
1399
1746
  * Follow a user (bodyless). `POST /v1/{account_id}/users/{user_id}/follow`
1400
1747
  * If the target's profile is private, sends a connect request instead and
1401
1748
  * returns `connect_request_sent`.
1749
+ *
1750
+ * A person on the Team's do-not-contact list throws `DO_NOT_CONTACT` (409);
1751
+ * nothing is sent.
1402
1752
  */
1403
1753
  follow(userId) {
1404
1754
  return this.ctx.request({
@@ -1434,6 +1784,9 @@ var UsersResource = class {
1434
1784
  * `POST /v1/{account_id}/users/{user_id}/endorse-skill`
1435
1785
  * Body key is `endorsement_id` (was `skill_endorsement_id` on `profiles.endorse`).
1436
1786
  * Obtain it from `GET /v1/{account_id}/users/{user_id}?linkedin_sections=linkedin_skills`.
1787
+ *
1788
+ * A person on the Team's do-not-contact list throws `DO_NOT_CONTACT` (409);
1789
+ * nothing is sent.
1437
1790
  */
1438
1791
  endorseSkill(userId, body) {
1439
1792
  return this.ctx.request({
@@ -1449,7 +1802,12 @@ var InvitesResource = class {
1449
1802
  constructor(ctx) {
1450
1803
  this.ctx = ctx;
1451
1804
  }
1452
- /** Send a connect-request. `POST /v1/{account_id}/invites` */
1805
+ /**
1806
+ * Send a connect-request. `POST /v1/{account_id}/invites`
1807
+ *
1808
+ * A person on the Team's do-not-contact list throws `DO_NOT_CONTACT` (409);
1809
+ * nothing is sent.
1810
+ */
1453
1811
  send(body) {
1454
1812
  return this.ctx.request({
1455
1813
  method: "POST",
@@ -1745,7 +2103,12 @@ var PostsResource = class {
1745
2103
  ...params ? { query: params } : {}
1746
2104
  });
1747
2105
  }
1748
- /** React to a post. `POST /v1/{account_id}/posts/{post_id}/reactions` */
2106
+ /**
2107
+ * React to a post. `POST /v1/{account_id}/posts/{post_id}/reactions`
2108
+ *
2109
+ * A person on the Team's do-not-contact list throws `DO_NOT_CONTACT` (409);
2110
+ * nothing is sent.
2111
+ */
1749
2112
  react(postId, body) {
1750
2113
  return this.ctx.request({
1751
2114
  method: "POST",
@@ -1851,6 +2214,9 @@ var CommentsResource = class {
1851
2214
  * Publish a comment on a post. `POST /v1/{account_id}/posts/{post_id}/comments`
1852
2215
  * Was `posts.comment`. Attachments (at most one) travel as a base64
1853
2216
  * object, never `FormData`.
2217
+ *
2218
+ * A person on the Team's do-not-contact list throws `DO_NOT_CONTACT` (409);
2219
+ * nothing is sent.
1854
2220
  */
1855
2221
  create(postId, body) {
1856
2222
  return this.ctx.request({
@@ -1884,6 +2250,9 @@ var CommentsResource = class {
1884
2250
  * Reply to a comment. Shares its path with `edit`; POST creates a reply
1885
2251
  * under the target comment, PATCH edits the target comment's own text.
1886
2252
  * `POST /v1/{account_id}/posts/{post_id}/comments/{comment_id}`
2253
+ *
2254
+ * A person on the Team's do-not-contact list throws `DO_NOT_CONTACT` (409);
2255
+ * nothing is sent.
1887
2256
  */
1888
2257
  reply(postId, commentId, body) {
1889
2258
  return this.ctx.request({
@@ -1916,6 +2285,9 @@ var CommentsResource = class {
1916
2285
  }
1917
2286
  /**
1918
2287
  * React to a comment. `POST /v1/{account_id}/posts/{post_id}/comments/{comment_id}/reactions`
2288
+ *
2289
+ * A person on the Team's do-not-contact list throws `DO_NOT_CONTACT` (409);
2290
+ * nothing is sent.
1919
2291
  */
1920
2292
  addReaction(postId, commentId, body) {
1921
2293
  return this.ctx.request({
@@ -2344,6 +2716,9 @@ var ConnectRequestsResource = class {
2344
2716
  * A reconnect that signs in to a different LinkedIn account fails with
2345
2717
  * `LINKEDIN_ACCOUNT_MISMATCH` and leaves the account unchanged.
2346
2718
  *
2719
+ * `warm_up: false` connects the new account with warm-up off (default
2720
+ * `true`); with `account_id` it is `400 INVALID_REQUEST`.
2721
+ *
2347
2722
  * @example
2348
2723
  * const req = await curviate.connectRequests.create();
2349
2724
  * console.log(`Open this link to connect your LinkedIn account: ${req.url}`);
@@ -2392,11 +2767,23 @@ var TeamsResource = class {
2392
2767
  this.ctx = ctx;
2393
2768
  }
2394
2769
  /**
2395
- * List the teams you belong to, with your role in each.
2770
+ * List the teams you belong to, with your role in each. `is_current` marks
2771
+ * the team this credential acts in; `logo_url` is the team's public PNG or
2772
+ * `null`. Pages with an opaque `cursor` until it is `null`.
2773
+ *
2774
+ * `include: "counts"` adds to each row `tier` (or the Tier of a seat lent to
2775
+ * it; `null` with no subscription), `trial: { days_left }` while a trial is
2776
+ * all the team has, `accounts` and `active_motions` (what you would read in
2777
+ * that team), and `seats: { used, total, lent_out, lent_in }` on rows where
2778
+ * you are an Owner. Which rows get counts depends on the credential: a
2779
+ * dashboard session and an All-Teams key get them on every row; a Team API
2780
+ * key, or a token for one Team, only on its own row (the other rows come
2781
+ * back without them).
2396
2782
  * `GET /v1/teams`
2397
2783
  *
2398
2784
  * @example
2399
2785
  * const { items } = await curviate.teams.list();
2786
+ * const withCounts = await curviate.teams.list({ include: "counts" });
2400
2787
  */
2401
2788
  list(params) {
2402
2789
  return this.ctx.request({
@@ -2406,8 +2793,27 @@ var TeamsResource = class {
2406
2793
  });
2407
2794
  }
2408
2795
  /**
2409
- * The team this credential acts in: its name, your role, and the Owner who
2410
- * receives invoices.
2796
+ * Create a team with you as its Owner and the Owner who receives its
2797
+ * invoices. It has no subscription and no seat yet. Any role may call it,
2798
+ * and two identical calls create two teams. The result is the
2799
+ * {@link TeamsResource.list} item shape plus `next_step`, which depends on
2800
+ * the credential: an All-Teams key gets `{ team_header }`, the header line
2801
+ * to send (`X-Curviate-Team: <team_id>`, usable at once; with this SDK pass
2802
+ * the `team_id` to `curviate.withTeam(team_id)`); any other credential gets
2803
+ * `{ command, url }` (the CLI command that sets up a key in the new team,
2804
+ * and the dashboard page with your teams).
2805
+ * `POST /v1/teams`
2806
+ *
2807
+ * @example
2808
+ * const { team_id, next_step } = await curviate.teams.create({ name: "Agency client A" });
2809
+ * if (next_step?.command) console.log(next_step.command); // set up a key for the new team
2810
+ */
2811
+ create(body) {
2812
+ return this.ctx.request({ method: "POST", path: "/v1/teams", body });
2813
+ }
2814
+ /**
2815
+ * The team this credential acts in: its name, your role, its `logo_url`, and
2816
+ * the Owner who receives invoices.
2411
2817
  * `GET /v1/teams/current`
2412
2818
  *
2413
2819
  * @example
@@ -2417,8 +2823,9 @@ var TeamsResource = class {
2417
2823
  return this.ctx.request({ method: "GET", path: "/v1/teams/current" });
2418
2824
  }
2419
2825
  /**
2420
- * Rename the current team, or set `billing_owner_user_id` (an Owner of this
2421
- * team, whose email receives invoices). Owners only.
2826
+ * Rename the current team (Owners and Managers), or set
2827
+ * `billing_owner_user_id` (an Owner of this team, whose email receives
2828
+ * invoices; Owners only).
2422
2829
  * `PATCH /v1/teams/current`
2423
2830
  *
2424
2831
  * @example
@@ -2432,7 +2839,37 @@ var TeamsResource = class {
2432
2839
  });
2433
2840
  }
2434
2841
  /**
2435
- * List the current team's members and their roles.
2842
+ * Set the current team's logo from a PNG of at most 1 MB. The file's own
2843
+ * bytes are checked, whatever its content type: a file that is not a PNG
2844
+ * throws `UNSUPPORTED_MEDIA_TYPE` (415) and a larger one `PAYLOAD_TOO_LARGE`
2845
+ * (413). Replaces the previous logo and resolves with the team, whose
2846
+ * `logo_url` is the new public PNG URL. Owners and Managers.
2847
+ * `PUT /v1/teams/current/logo`
2848
+ *
2849
+ * @example
2850
+ * await curviate.teams.setLogo(await fs.promises.readFile("logo.png"));
2851
+ */
2852
+ setLogo(png) {
2853
+ return this.ctx.request({
2854
+ method: "PUT",
2855
+ path: "/v1/teams/current/logo",
2856
+ rawBody: { data: png, contentType: "image/png" }
2857
+ });
2858
+ }
2859
+ /**
2860
+ * Remove the current team's logo; the team resolves with `logo_url: null`.
2861
+ * Owners and Managers.
2862
+ * `DELETE /v1/teams/current/logo`
2863
+ *
2864
+ * @example
2865
+ * await curviate.teams.removeLogo();
2866
+ */
2867
+ removeLogo() {
2868
+ return this.ctx.request({ method: "DELETE", path: "/v1/teams/current/logo" });
2869
+ }
2870
+ /**
2871
+ * List the current team's members and their roles. A member's `name` is
2872
+ * their name, else their email (`null` only when neither is known).
2436
2873
  * `GET /v1/teams/current/members`
2437
2874
  *
2438
2875
  * @example
@@ -2463,7 +2900,7 @@ var TeamsResource = class {
2463
2900
  });
2464
2901
  }
2465
2902
  /**
2466
- * Remove a member (Owners only), or pass `"me"` to leave the team yourself.
2903
+ * Remove a member (an Owner removes anyone, a Manager removes Members), or pass `"me"` to leave the team yourself.
2467
2904
  * Their API key and connectors for this team stop working at once. Same
2468
2905
  * refusals as {@link TeamsResource.updateMember}.
2469
2906
  * `DELETE /v1/teams/current/members/{user_id}`
@@ -2481,7 +2918,8 @@ var TeamsResource = class {
2481
2918
  * Invite 1 to 20 people by email, each with a role (`member` by default).
2482
2919
  * Each gets a link valid for 7 days. All or nothing: if any row is refused
2483
2920
  * (`ALREADY_MEMBER`, `INVITE_ALREADY_PENDING`, `INVITE_LIMIT_REACHED`),
2484
- * nothing is created or sent and the error names the row. Owners only.
2921
+ * nothing is created or sent and the error names the row. Owners and Managers
2922
+ * (a Manager invites Members only).
2485
2923
  * `POST /v1/teams/current/invites`
2486
2924
  *
2487
2925
  * @example
@@ -2495,7 +2933,7 @@ var TeamsResource = class {
2495
2933
  });
2496
2934
  }
2497
2935
  /**
2498
- * List the current team's invites, optionally by `status`.
2936
+ * List the current team's invites, optionally by `status`. Owners and Managers.
2499
2937
  * `GET /v1/teams/current/invites`
2500
2938
  *
2501
2939
  * @example
@@ -2510,7 +2948,8 @@ var TeamsResource = class {
2510
2948
  }
2511
2949
  /**
2512
2950
  * Re-send a pending or expired invite; its link is valid for 7 more days.
2513
- * An accepted or revoked one is refused with `INVITE_NOT_PENDING`.
2951
+ * An accepted or revoked one is refused with `INVITE_NOT_PENDING`. Owners,
2952
+ * and Managers for Member invites.
2514
2953
  * `POST /v1/teams/current/invites/{invite_id}/resend`
2515
2954
  *
2516
2955
  * @example
@@ -2523,7 +2962,8 @@ var TeamsResource = class {
2523
2962
  });
2524
2963
  }
2525
2964
  /**
2526
- * Revoke an invite; its link stops working.
2965
+ * Revoke an invite; its link stops working. Owners, and Managers for Member
2966
+ * invites.
2527
2967
  * `DELETE /v1/teams/current/invites/{invite_id}`
2528
2968
  *
2529
2969
  * @example
@@ -2537,17 +2977,6 @@ var TeamsResource = class {
2537
2977
  }
2538
2978
  };
2539
2979
 
2540
- // src/internal/query.ts
2541
- function commaLists(params, ...keys) {
2542
- if (params === void 0) return void 0;
2543
- const out = { ...params };
2544
- for (const k of keys) {
2545
- const v = out[k];
2546
- if (Array.isArray(v)) out[k] = v.length ? v.join(",") : void 0;
2547
- }
2548
- return out;
2549
- }
2550
-
2551
2980
  // src/resources/contacts.ts
2552
2981
  var ContactImportsResource = class {
2553
2982
  constructor(ctx) {
@@ -2632,23 +3061,33 @@ var ContactsResource = class {
2632
3061
  this.imports = new ContactImportsResource(ctx);
2633
3062
  }
2634
3063
  /**
2635
- * List Contacts, newest first (`order: "asc"` for oldest). Filters combine:
2636
- * `list_id`, `tag` (comma list, any of them), `import_id`, `linkedin_url`,
3064
+ * List Contacts, newest first (`order: "asc"` for oldest). Filters combine
3065
+ * with AND: `list_id` (up to 20 Contact lists, as an array or a comma list:
3066
+ * Contacts in any of them, or in every one with `list_match: "all"`; one id
3067
+ * is one list), `not_list_id` (Contacts in none of those lists), `has_email`
3068
+ * (`true`: only Contacts with an email address, `false`: only those
3069
+ * without), `tag` (comma list, any of them), `import_id`, `linkedin_url`,
2637
3070
  * `q` (substring over names, headline, job title, company, email,
2638
3071
  * location), `enrichment_status`, `connected`, `account_id` (Connections of
2639
- * that account), `motion_id` + `enrollment_status` (any of the statuses
2640
- * given). Pages with an opaque
2641
- * `cursor` until it is `null`.
3072
+ * that account, or of any of up to 50, as an array or a comma list), `motion_id` + `enrollment_status` (any of the statuses
3073
+ * given). A list id that is not your team's throws `RESOURCE_NOT_FOUND`
3074
+ * naming the key. Pages with an opaque `cursor` until it is `null`.
3075
+ * `include_total: true` adds `total` to the page, the number of Contacts the
3076
+ * filters match (with `limit: 1` it is the count before a bulk call or a new
3077
+ * list); it is left out unless asked for.
2642
3078
  * `GET /v1/contacts`
2643
3079
  *
2644
3080
  * @example
2645
3081
  * const { items, cursor } = await curviate.contacts.list({ tag: "warm", limit: 100 });
3082
+ *
3083
+ * @example
3084
+ * const { total } = await curviate.contacts.list({ list_id: ["cl_1", "cl_2"], list_match: "all", has_email: true, include_total: true, limit: 1 });
2646
3085
  */
2647
3086
  list(params) {
2648
3087
  return this.ctx.request({
2649
3088
  method: "GET",
2650
3089
  path: "/v1/contacts",
2651
- query: commaLists(params, "enrollment_status")
3090
+ query: commaLists(params, "enrollment_status", "list_id", "not_list_id", "account_id")
2652
3091
  });
2653
3092
  }
2654
3093
  /**
@@ -2721,12 +3160,22 @@ var ContactsResource = class {
2721
3160
  });
2722
3161
  }
2723
3162
  /**
2724
- * Delete up to 1,000 Contacts. Ids that are not your team's come back in
2725
- * `not_found`. Owners and Managers only.
3163
+ * Delete up to 1,000 Contacts by `contact_ids`, or every Contact a `filter`
3164
+ * matches (up to 10,000). Send exactly one of the two. `filter` takes the
3165
+ * {@link ContactsResource.list} filters as JSON, comma lists as arrays
3166
+ * (`list_id`, `not_list_id`, `list_match`, `has_email`, `tag`, ...); a
3167
+ * filter matching more than 10,000 Contacts throws `INVALID_REQUEST`
3168
+ * naming the count and deletes nothing, so count first with
3169
+ * `list({ ...filter, include_total: true, limit: 1 })`. Ids that are not your
3170
+ * team's come back in `not_found` (always empty with a filter). Owners and
3171
+ * Managers only.
2726
3172
  * `POST /v1/contacts/delete`
2727
3173
  *
2728
3174
  * @example
2729
3175
  * const { deleted, not_found } = await curviate.contacts.deleteMany({ contact_ids: ["con_1", "con_2"] });
3176
+ *
3177
+ * @example
3178
+ * await curviate.contacts.deleteMany({ filter: { list_id: ["cl_1"], has_email: false } });
2730
3179
  */
2731
3180
  deleteMany(body) {
2732
3181
  return this.ctx.request({
@@ -2738,13 +3187,17 @@ var ContactsResource = class {
2738
3187
  /**
2739
3188
  * Add or remove Tags and list memberships on up to 1,000 Contacts at once:
2740
3189
  * `add_tags`, `remove_tags`, `add_list_id` or `add_list_name` (created if
2741
- * new), `remove_list_id`.
3190
+ * new), `remove_list_id`. Name the Contacts with exactly one of
3191
+ * `contact_ids` or `filter`: every Contact the {@link ContactsResource.list}
3192
+ * filters match (comma lists as arrays), up to 10,000. A filter over the
3193
+ * cap throws `INVALID_REQUEST` naming the count and changes nothing.
2742
3194
  * `POST /v1/contacts/update`
2743
3195
  *
2744
3196
  * Needs Automate: a Team below that Tier is refused with `TIER_REQUIRED`.
2745
3197
  *
2746
3198
  * @example
2747
3199
  * await curviate.contacts.updateMany({ contact_ids: ["con_1", "con_2"], add_tags: ["vip"], add_list_name: "Q4" });
3200
+ * await curviate.contacts.updateMany({ filter: { tag: ["warm"], has_email: true }, add_list_name: "Warm with email" });
2748
3201
  */
2749
3202
  updateMany(body) {
2750
3203
  return this.ctx.request({
@@ -2778,7 +3231,7 @@ var ContactsResource = class {
2778
3231
  return this.ctx.request({
2779
3232
  method: "GET",
2780
3233
  path: "/v1/contacts/export",
2781
- query: commaLists(params, "enrollment_status")
3234
+ query: commaLists(params, "enrollment_status", "list_id", "not_list_id", "account_id")
2782
3235
  });
2783
3236
  }
2784
3237
  };
@@ -2789,13 +3242,20 @@ var ContactListsResource = class {
2789
3242
  this.ctx = ctx;
2790
3243
  }
2791
3244
  /**
2792
- * Create an empty Contact list.
3245
+ * Create a static Contact list: empty, or with `from`, a filter as in
3246
+ * `contacts.updateMany({ filter })`, holding every Contact it matches right
3247
+ * now (up to 10,000; more throws `INVALID_REQUEST` naming the count and
3248
+ * creates nothing). `from.list_id` of two lists combines them (a Contact in
3249
+ * both counts once), `list_match: "all"` keeps those in both and
3250
+ * `not_list_id` leaves some out. The lists named in `from` are not changed,
3251
+ * and `contact_count` is how many Contacts matched.
2793
3252
  * `POST /v1/contact-lists`
2794
3253
  *
2795
3254
  * Needs Automate: a Team below that Tier is refused with `TIER_REQUIRED`.
2796
3255
  *
2797
3256
  * @example
2798
3257
  * const list = await curviate.contactLists.create({ name: "Q4 webinar" });
3258
+ * const both = await curviate.contactLists.create({ name: "Both", from: { list_id: ["cl_1", "cl_2"], list_match: "all" } });
2799
3259
  */
2800
3260
  create(body) {
2801
3261
  return this.ctx.request({
@@ -2922,7 +3382,7 @@ var TagsResource = class {
2922
3382
 
2923
3383
  // src/sequence/order.ts
2924
3384
  var DOC = ["$schema", "on_reply", "steps"];
2925
- var STEP = ["id", "do", "wait", "until", "if", "emit", "tag", "timeout", "variants", "with", "event", "then", "else", "next", "note"];
3385
+ var STEP = ["id", "do", "wait", "until", "if", "emit", "tag", "timeout", "save", "save_to_contact", "variants", "with", "event", "then", "else", "next", "note"];
2926
3386
  var WITH = ["post", "newer_than", "reaction", "subject", "text", "note", "note_fallback", "fallback", "withdraw_after"];
2927
3387
  var VARIANT = ["id", "subject", "text"];
2928
3388
  var FALLBACK = ["subject", "text"];
@@ -2957,7 +3417,7 @@ var schema_v1_default = {
2957
3417
  $schema: "https://json-schema.org/draft/2020-12/schema",
2958
3418
  $id: "https://docs.curviate.com/schemas/sequence/v1.json",
2959
3419
  title: "Sequence document v1",
2960
- description: 'A Motion\'s Sequence: a flat steps array run from the first step, ending explicitly in "end". Served documents use this key order: $schema, on_reply, steps; in a step id, the kind key, timeout, variants, with, event, then, else, next, note; in with post, newer_than, reaction, subject, text, note, note_fallback, fallback, withdraw_after; in a variant id, subject, text. A text holds at most 50 variables. Variables: `{{first_name}}` (any standard Contact field), `{{my_first_name}}`, `{{my_last_name}}`, `{{my_full_name}}` (the sending Account), any other `{{<key>}}` is the custom field `contact.extra.<key>`; the long forms `{{contact.<field>}}`, `{{contact.extra.<key>}}` and `{{account.<name>}}` stay valid. Delays are about their duration: every wait and until timeout gets a small random spread that only ever adds time.',
3420
+ description: 'A Motion\'s Sequence: a flat steps array run from the first step, ending explicitly in "end". Served documents use this key order: $schema, on_reply, steps; in a step id, the kind key, timeout, save, save_to_contact, variants, with, event, then, else, next, note; in with post, newer_than, reaction, subject, text, note, note_fallback, fallback, withdraw_after; in a variant id, subject, text. A text holds at most 50 variables. Variables: `{{first_name}}` (any standard Contact field), `{{my_first_name}}`, `{{my_last_name}}`, `{{my_full_name}}` (the sending Account), any other `{{<key>}}` is the custom field `contact.extra.<key>`; the long forms `{{contact.<field>}}`, `{{contact.extra.<key>}}` and `{{account.<name>}}` stay valid; `{{enrollment.<name>}}` is a value a save of this Sequence writes. Delays are about their duration: every wait and until timeout gets a small random spread that only ever adds time.',
2961
3421
  type: "object",
2962
3422
  additionalProperties: false,
2963
3423
  required: [
@@ -2972,7 +3432,7 @@ var schema_v1_default = {
2972
3432
  on_reply: {
2973
3433
  type: "string",
2974
3434
  pattern: "^(end|[a-z][a-z0-9_]{0,39})$",
2975
- description: "Where a reply interrupts to. Used only while stop_on_engagement is false."
3435
+ description: "Where a reply interrupts to."
2976
3436
  },
2977
3437
  steps: {
2978
3438
  type: "array",
@@ -3700,6 +4160,52 @@ var schema_v1_default = {
3700
4160
  pattern: "^[1-9][0-9]*(m|h|d)$",
3701
4161
  description: "Give up waiting after about this long (1h to 180d; a small random spread is added, never subtracted)."
3702
4162
  },
4163
+ save: {
4164
+ type: "object",
4165
+ minProperties: 1,
4166
+ maxProperties: 20,
4167
+ propertyNames: {
4168
+ pattern: "^[a-z][a-z0-9_]{0,63}$",
4169
+ not: {
4170
+ const: "id"
4171
+ }
4172
+ },
4173
+ additionalProperties: {
4174
+ type: "string",
4175
+ minLength: 1,
4176
+ maxLength: 256,
4177
+ description: 'A field path into the incoming payload: dots, items[0] and ["a.b"].'
4178
+ },
4179
+ description: "until event.<name> only: Enrollment values saved from the incoming event, name to path (at most 20). Read them as {{enrollment.<name>}} and if enrollment.<name>; they end with the Enrollment."
4180
+ },
4181
+ save_to_contact: {
4182
+ type: "object",
4183
+ minProperties: 1,
4184
+ maxProperties: 20,
4185
+ propertyNames: {
4186
+ pattern: "^(email|[A-Za-z0-9_]{1,64})$",
4187
+ not: {
4188
+ enum: [
4189
+ "full_name",
4190
+ "first_name",
4191
+ "last_name",
4192
+ "headline",
4193
+ "job_title",
4194
+ "company",
4195
+ "location",
4196
+ "linkedin_url",
4197
+ "about"
4198
+ ]
4199
+ }
4200
+ },
4201
+ additionalProperties: {
4202
+ type: "string",
4203
+ minLength: 1,
4204
+ maxLength: 256,
4205
+ description: 'A field path into the incoming payload: dots, items[0] and ["a.b"].'
4206
+ },
4207
+ description: "until event.<name> only: Contact custom fields (contact.extra.<key>) saved from the incoming event, key to path (at most 20); email fills only an empty email."
4208
+ },
3703
4209
  then: {
3704
4210
  type: "string",
3705
4211
  pattern: "^(end|[a-z][a-z0-9_]{0,39})$",
@@ -4325,12 +4831,73 @@ var schema_v1_default = {
4325
4831
  },
4326
4832
  exists: {
4327
4833
  type: "boolean"
4834
+ },
4835
+ gte: {
4836
+ type: "number"
4837
+ },
4838
+ lte: {
4839
+ type: "number"
4328
4840
  }
4329
4841
  },
4330
- description: "Exactly one operator. Strings compare trimmed, case-insensitive."
4842
+ description: "Exactly one operator. Strings compare trimmed, case-insensitive; gte and lte compare a number, or text that reads as one."
4331
4843
  }
4332
4844
  }
4333
4845
  },
4846
+ {
4847
+ type: "object",
4848
+ minProperties: 1,
4849
+ maxProperties: 1,
4850
+ additionalProperties: false,
4851
+ patternProperties: {
4852
+ "^enrollment\\.(?!id$)[a-z][a-z0-9_]{0,63}$": {
4853
+ type: "object",
4854
+ minProperties: 1,
4855
+ maxProperties: 1,
4856
+ additionalProperties: false,
4857
+ properties: {
4858
+ eq: {
4859
+ type: [
4860
+ "string",
4861
+ "number",
4862
+ "boolean"
4863
+ ]
4864
+ },
4865
+ neq: {
4866
+ type: [
4867
+ "string",
4868
+ "number",
4869
+ "boolean"
4870
+ ]
4871
+ },
4872
+ in: {
4873
+ type: "array",
4874
+ minItems: 1,
4875
+ items: {
4876
+ type: [
4877
+ "string",
4878
+ "number",
4879
+ "boolean"
4880
+ ]
4881
+ }
4882
+ },
4883
+ contains: {
4884
+ type: "string"
4885
+ },
4886
+ exists: {
4887
+ type: "boolean"
4888
+ },
4889
+ gte: {
4890
+ type: "number"
4891
+ },
4892
+ lte: {
4893
+ type: "number"
4894
+ }
4895
+ },
4896
+ description: "Exactly one operator. Strings compare trimmed, case-insensitive; gte and lte compare a number, or text that reads as one."
4897
+ }
4898
+ },
4899
+ description: "if only: an Enrollment value a save of this Sequence writes. Unset is false, except exists:false."
4900
+ },
4334
4901
  {
4335
4902
  type: "object",
4336
4903
  additionalProperties: false,
@@ -4956,12 +5523,73 @@ var schema_v1_default = {
4956
5523
  },
4957
5524
  exists: {
4958
5525
  type: "boolean"
5526
+ },
5527
+ gte: {
5528
+ type: "number"
5529
+ },
5530
+ lte: {
5531
+ type: "number"
4959
5532
  }
4960
5533
  },
4961
- description: "Exactly one operator. Strings compare trimmed, case-insensitive."
5534
+ description: "Exactly one operator. Strings compare trimmed, case-insensitive; gte and lte compare a number, or text that reads as one."
4962
5535
  }
4963
5536
  }
4964
5537
  },
5538
+ {
5539
+ type: "object",
5540
+ minProperties: 1,
5541
+ maxProperties: 1,
5542
+ additionalProperties: false,
5543
+ patternProperties: {
5544
+ "^enrollment\\.(?!id$)[a-z][a-z0-9_]{0,63}$": {
5545
+ type: "object",
5546
+ minProperties: 1,
5547
+ maxProperties: 1,
5548
+ additionalProperties: false,
5549
+ properties: {
5550
+ eq: {
5551
+ type: [
5552
+ "string",
5553
+ "number",
5554
+ "boolean"
5555
+ ]
5556
+ },
5557
+ neq: {
5558
+ type: [
5559
+ "string",
5560
+ "number",
5561
+ "boolean"
5562
+ ]
5563
+ },
5564
+ in: {
5565
+ type: "array",
5566
+ minItems: 1,
5567
+ items: {
5568
+ type: [
5569
+ "string",
5570
+ "number",
5571
+ "boolean"
5572
+ ]
5573
+ }
5574
+ },
5575
+ contains: {
5576
+ type: "string"
5577
+ },
5578
+ exists: {
5579
+ type: "boolean"
5580
+ },
5581
+ gte: {
5582
+ type: "number"
5583
+ },
5584
+ lte: {
5585
+ type: "number"
5586
+ }
5587
+ },
5588
+ description: "Exactly one operator. Strings compare trimmed, case-insensitive; gte and lte compare a number, or text that reads as one."
5589
+ }
5590
+ },
5591
+ description: "if only: an Enrollment value a save of this Sequence writes. Unset is false, except exists:false."
5592
+ },
4965
5593
  {
4966
5594
  type: "object",
4967
5595
  additionalProperties: false,
@@ -5026,7 +5654,7 @@ var schema_v1_default = {
5026
5654
  emit: {
5027
5655
  type: "string",
5028
5656
  pattern: "^[a-z][a-z0-9_.]{0,63}$",
5029
- description: "Legacy: a custom event name. New documents put event on the step that matters."
5657
+ description: "Send event: the custom event name this step sends (motion.custom_event)."
5030
5658
  },
5031
5659
  next: {
5032
5660
  type: "string",
@@ -5094,16 +5722,86 @@ function deepFreeze(value) {
5094
5722
  }
5095
5723
  var SEQUENCE_SCHEMA_V1 = deepFreeze(schema_v1_default);
5096
5724
 
5097
- // src/sequence/constants.ts
5725
+ // src/shared/sequence/limits.ts
5726
+ var SEQUENCE_SCHEMA_URL2 = "https://docs.curviate.com/schemas/sequence/v1.json";
5727
+ var MAX_STEPS = 100;
5728
+ var MIN_VARIANTS = 2;
5729
+ var MAX_VARIANTS = 2;
5730
+ var DEPRECATED_ACCEPTED_UNTIL = "2027-01-19";
5731
+ var MAX_VARIANTS_DEPRECATED = 5;
5732
+ var MAX_SEQUENCE_BYTES = 256 * 1024;
5733
+ var MAX_STEP_NOTE_CHARS = 500;
5734
+ var MAX_TAG_CHARS = 64;
5735
+ var MAX_PREDICATE_DEPTH = 10;
5736
+ var MAX_VARIABLE_REFS_PER_TEXT = 50;
5737
+ var MAX_QUOTED_TOKEN_CHARS = 64;
5738
+ var ID_PATTERN = "^[a-z][a-z0-9_]{0,39}$";
5739
+ var EVENT_NAME_PATTERN = "^[a-z][a-z0-9_.]{0,63}$";
5740
+ var EXTRA_KEY_PATTERN = "^[A-Za-z0-9_]{1,64}$";
5741
+ var SAVE_NAME_PATTERN = "^[a-z][a-z0-9_]{0,63}$";
5742
+ var MAX_SAVE_ENTRIES = 20;
5743
+ var DURATION_PATTERN = "^[1-9][0-9]*(m|h|d)$";
5744
+ var TAG_PATTERN = "^[^,;]*[^,;\\s][^,;]*$";
5098
5745
  var END = "end";
5746
+ var M = 1;
5099
5747
  var H = 60;
5100
- var D = 24 * H;
5748
+ var D = 24 * 60;
5101
5749
  var DURATION_BOUNDS = {
5102
- wait: { min: 1, max: 365 * D, label: "1m and 365d", example: "1d" },
5750
+ wait: { min: 1 * M, max: 365 * D, label: "1m and 365d", example: "1d" },
5103
5751
  timeout: { min: 1 * H, max: 180 * D, label: "1h and 180d", example: "21d" },
5104
5752
  withdraw_after: { min: 14 * D, max: 180 * D, label: "14d and 180d", example: "21d" },
5105
5753
  newer_than: { min: 1 * H, max: 365 * D, label: "1h and 365d", example: "30d" }
5106
5754
  };
5755
+ var STEP_KINDS = ["do", "wait", "until", "if", "emit", "tag"];
5756
+ var ACTIONS = [
5757
+ "profile.view",
5758
+ "profile.follow",
5759
+ "profile.unfollow",
5760
+ "invite.send",
5761
+ "invite.withdraw",
5762
+ "message.send",
5763
+ "inmail.send",
5764
+ "post.react",
5765
+ "post.comment",
5766
+ "skill.endorse"
5767
+ ];
5768
+ var ACTION_PARAMS = {
5769
+ "profile.view": [],
5770
+ "profile.follow": [],
5771
+ "profile.unfollow": [],
5772
+ "invite.send": ["note", "note_fallback", "withdraw_after"],
5773
+ "invite.withdraw": [],
5774
+ "message.send": ["text", "fallback"],
5775
+ "inmail.send": ["subject", "text", "fallback"],
5776
+ "post.react": ["post", "newer_than", "reaction"],
5777
+ "post.comment": ["post", "newer_than", "text", "fallback"],
5778
+ "skill.endorse": []
5779
+ };
5780
+ var MESSAGING_ACTIONS = ["invite.send", "message.send", "inmail.send", "post.comment"];
5781
+ var REACTIONS = ["like", "celebrate", "support", "love", "insightful", "funny"];
5782
+ var EVENTS = ["invite.accepted", "message.replied", "message.read"];
5783
+ var STANDARD_FIELDS = [
5784
+ "full_name",
5785
+ "first_name",
5786
+ "last_name",
5787
+ "headline",
5788
+ "job_title",
5789
+ "company",
5790
+ "email",
5791
+ "location",
5792
+ "linkedin_url",
5793
+ "about"
5794
+ ];
5795
+ var NAME_FIELDS = ["full_name", "first_name", "last_name"];
5796
+ var ACCOUNT_FIELDS = ["first_name", "last_name", "full_name"];
5797
+ var BOOLEAN_PREDICATES = [
5798
+ "contact.connected",
5799
+ "contact.open_profile",
5800
+ "contact.premium",
5801
+ "contact.has_photo"
5802
+ ];
5803
+ var FIELD_OPERATORS = ["eq", "neq", "in", "contains", "exists"];
5804
+ var NUMBER_OPERATORS = ["gte", "lte"];
5107
5805
  var TEXT_LIMITS = {
5108
5806
  message: 8e3,
5109
5807
  invite_note: 300,
@@ -5111,19 +5809,101 @@ var TEXT_LIMITS = {
5111
5809
  inmail_text: 1900,
5112
5810
  comment: 1250
5113
5811
  };
5114
- var MAX_SEQUENCE_BYTES = 256 * 1024;
5115
- var MAX_PREDICATE_DEPTH = 10;
5116
- var MAX_VARIABLE_REFS_PER_TEXT = 50;
5117
- var MAX_QUOTED_TOKEN_CHARS = 64;
5118
- var ACCOUNT_FIELDS = ["first_name", "last_name", "full_name"];
5119
- var NAME_FIELDS = ["full_name", "first_name", "last_name"];
5120
- var MOTION_KEY_PATTERN = "^[a-z][a-z0-9_]{0,39}$";
5121
- var MAX_VARIANTS = 2;
5122
- var DEPRECATED_ACCEPTED_UNTIL = "2027-01-19";
5812
+ var STEP_KEYS = {
5813
+ do: ["id", "do", "with", "variants", "event", "next", "else", "note"],
5814
+ wait: ["id", "wait", "next", "note"],
5815
+ until: ["id", "until", "timeout", "save", "save_to_contact", "then", "else", "note"],
5816
+ if: ["id", "if", "then", "else", "note"],
5817
+ emit: ["id", "emit", "next", "note"],
5818
+ tag: ["id", "tag", "event", "next", "note"]
5819
+ };
5123
5820
  var SHORT_VARIABLE_PATTERN = "^[a-z][a-z0-9_]{0,63}$";
5821
+ var SHORT_RE = new RegExp(SHORT_VARIABLE_PATTERN);
5124
5822
  var MY_FIELDS = ["my_first_name", "my_last_name", "my_full_name"];
5823
+ function longVariable(inner) {
5824
+ if (inner.includes("."))
5825
+ return inner;
5826
+ if (!SHORT_RE.test(inner))
5827
+ return null;
5828
+ if (MY_FIELDS.includes(inner))
5829
+ return `account.${inner.slice(3)}`;
5830
+ if (inner.startsWith("my_"))
5831
+ return null;
5832
+ if (STANDARD_FIELDS.includes(inner))
5833
+ return `contact.${inner}`;
5834
+ return `contact.extra.${inner}`;
5835
+ }
5836
+ var isObject = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
5837
+ var fallbackKeyword = (action) => action === "invite.send" ? "drop" : action === "post.comment" ? "skip" : null;
5125
5838
 
5126
- // src/sequence/rules.ts
5839
+ // src/shared/sequence/paths.ts
5840
+ function durationMinutes(v) {
5841
+ const m = typeof v === "string" ? /^([1-9][0-9]*)(m|h|d)$/.exec(v) : null;
5842
+ if (!m)
5843
+ return 0;
5844
+ return Number(m[1]) * { m: 1, h: 60, d: 1440 }[m[2]];
5845
+ }
5846
+
5847
+ // src/shared/field-paths.ts
5848
+ var PATH_MAX = 256;
5849
+ var PLAIN = /^[A-Za-z0-9_$-]+$/;
5850
+ function parsePath(input) {
5851
+ const s = input.trim();
5852
+ const out = [];
5853
+ let i = 0;
5854
+ const key = () => {
5855
+ const m = /^[A-Za-z0-9_$-]+/.exec(s.slice(i));
5856
+ if (!m)
5857
+ return false;
5858
+ out.push(m[0]);
5859
+ i += m[0].length;
5860
+ return true;
5861
+ };
5862
+ if (s === "" || s[0] !== "[" && !key())
5863
+ return null;
5864
+ while (i < s.length) {
5865
+ if (s[i] === ".") {
5866
+ i += 1;
5867
+ if (!key())
5868
+ return null;
5869
+ } else if (s[i] === "[") {
5870
+ const idx = /^\[(\d{1,6})\]/.exec(s.slice(i));
5871
+ const str = /^\[("(?:[^"\\]|\\.)*")\]/.exec(s.slice(i));
5872
+ if (idx) {
5873
+ out.push(Number(idx[1]));
5874
+ i += idx[0].length;
5875
+ } else if (str) {
5876
+ try {
5877
+ out.push(JSON.parse(str[1]));
5878
+ } catch {
5879
+ return null;
5880
+ }
5881
+ i += str[0].length;
5882
+ } else
5883
+ return null;
5884
+ } else
5885
+ return null;
5886
+ }
5887
+ return out;
5888
+ }
5889
+ function formatPath(segs) {
5890
+ return segs.map((g, n) => typeof g === "number" ? `[${g}]` : PLAIN.test(g) ? n === 0 ? g : `.${g}` : `[${JSON.stringify(g)}]`).join("");
5891
+ }
5892
+ function normalizePath(input) {
5893
+ const segs = parsePath(input);
5894
+ if (segs === null)
5895
+ return null;
5896
+ const p = formatPath(segs);
5897
+ return p.length > PATH_MAX ? null : p;
5898
+ }
5899
+
5900
+ // src/shared/sequence/references.ts
5901
+ function awaitedEventName(until) {
5902
+ const m = typeof until === "string" ? /^(?:event|signal)\.(.+)$/s.exec(until) : null;
5903
+ return m ? m[1] : null;
5904
+ }
5905
+
5906
+ // src/shared/sequence/rules.ts
5127
5907
  var SEQUENCE_RULES_DOCS_URL = "https://docs.curviate.com/reference/sequence-rules";
5128
5908
  var HINTS = {
5129
5909
  R1: () => "Keep only $schema, on_reply and steps at the top level; $schema, when present, is https://docs.curviate.com/schemas/sequence/v1.json.",
@@ -5145,203 +5925,124 @@ var HINTS = {
5145
5925
  R17: () => "Remove the part after | and add a variable-free fallback on the step instead.",
5146
5926
  R18: () => "Write a variable with double braces, like {{first_name}}, or remove the braces.",
5147
5927
  R19: () => 'Add a variable-free fallback: with.fallback, with.note_fallback (or "drop") on an invitation, "skip" on a comment.',
5148
- R20: () => "Use a Contact variable in the comment, for example a custom field {{comment}} your agent writes per Contact.",
5928
+ R20: () => "Use a Contact variable in the comment, for example a custom field {{comment}} your AI writes per Contact.",
5149
5929
  R21: () => "Set withdraw_after between 14d and 180d, for example 21d.",
5150
5930
  R22: () => "Shorten the text to the limit (variables are not counted) or use fewer variables.",
5151
5931
  R23: () => "Give the step exactly 2 variants (A/B) with ids unique in the document, or one text in with.",
5152
5932
  R24: () => "Name the event with lowercase letters, digits, _ and ., starting with a letter, like reply.received; a Tag is 1 to 64 characters without , or ;.",
5933
+ R25: () => "Put save and save_to_contact on an until event.<name> step, 1 to 20 entries each: save names are lowercase (not id), save_to_contact keys are custom fields or email.",
5934
+ R26: () => 'Write the path with dots, items[0] and ["a.b"], like lead.score, at most 256 characters.',
5935
+ R27: (t) => t ? `Use ${t}.` : "Save the value with save on an until event.<name> step first; a save_to_contact key reads as {{contact.extra.<key>}}.",
5936
+ R28: () => 'Use gte or lte with a number, like {"gte": 50}, on enrollment.<key> or contact.extra.<key>.',
5937
+ R29: () => "Save a field inside the object or list, or compare that value with eq, neq or exists instead.",
5153
5938
  W1: () => "If the merge is intended, keep it; otherwise give the jumping step its own continuation.",
5939
+ W2: () => "Add a sending Account with Sales Navigator or Recruiter, or remove the InMail step.",
5940
+ W3: () => 'Set note_fallback to "drop" so invitations go on without a note once the noted ones are used up.',
5941
+ W4: () => "Wait for message.replied instead, or add a sending Account that does not use Recruiter.",
5154
5942
  W5: () => "Remove the follow step: the invitation already follows the person.",
5155
5943
  W6: () => "Send an invitation on this path before waiting for its acceptance.",
5156
5944
  W7: () => "Write the comment per Contact, for example in a custom field {{comment}}.",
5157
5945
  W8: () => "Keep the link for a later message.",
5158
5946
  W9: () => "Motion variables are deprecated: write the text in place, or use a custom field.",
5947
+ W10: () => "Retired: never emitted. The Sequence's on_reply decides what a reply does.",
5159
5948
  W11: (t) => `Did you mean ${t}?`,
5160
5949
  W12: () => "Send a message on this path before checking whether it was seen.",
5161
- W13: (t) => `Use ${t} instead.`
5950
+ W13: (t) => `Use ${t} instead.`,
5951
+ W14: () => "Use the value in a message or an if step, or remove it from save if no webhook template sends it.",
5952
+ W16: (t) => `Use ${t} to read the saved value.`
5162
5953
  };
5954
+ var SEQUENCE_RULE_CODES = Object.keys(HINTS);
5163
5955
  function ruleHelp(code, token = "") {
5164
5956
  const hint = HINTS[code];
5165
- if (!hint) throw new Error(`no hint for ${code}`);
5957
+ if (!hint)
5958
+ throw new Error(`no hint for ${code}`);
5166
5959
  return { hint: hint(token), docs_url: `${SEQUENCE_RULES_DOCS_URL}#${code.toLowerCase()}` };
5167
5960
  }
5168
-
5169
- // src/sequence/vocab.ts
5170
- var SUPPORTED_KEYWORDS = /* @__PURE__ */ new Set([
5171
- "$id",
5172
- "$schema",
5173
- "additionalProperties",
5174
- "anyOf",
5175
- "const",
5176
- "default",
5177
- "description",
5178
- "enum",
5179
- "items",
5180
- "maxItems",
5181
- "maxLength",
5182
- "maxProperties",
5183
- "minItems",
5184
- "minLength",
5185
- "minProperties",
5186
- "minimum",
5187
- "oneOf",
5188
- "pattern",
5189
- "patternProperties",
5190
- "properties",
5191
- "required",
5192
- "title",
5193
- "type"
5194
- ]);
5195
- var MAPS = /* @__PURE__ */ new Set(["properties", "patternProperties"]);
5196
- function schemaKeywords(schema) {
5197
- const out = /* @__PURE__ */ new Set();
5198
- const visit = (node) => {
5199
- if (Array.isArray(node)) return node.forEach(visit);
5200
- if (!isObject(node)) return;
5201
- for (const [k, v] of Object.entries(node)) {
5202
- out.add(k);
5203
- if (MAPS.has(k) && isObject(v)) Object.values(v).forEach(visit);
5204
- else if (k !== "enum" && k !== "const" && k !== "default" && k !== "required") visit(v);
5205
- }
5206
- };
5207
- visit(schema);
5208
- return out;
5209
- }
5210
- function assertKeywords(schema) {
5211
- const unknown = [...schemaKeywords(schema)].filter((k) => !SUPPORTED_KEYWORDS.has(k));
5212
- if (unknown.length) throw new Error(`Sequence schema uses unsupported JSON Schema keyword(s): ${unknown.join(", ")}`);
5213
- }
5214
- var isObject = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
5215
- var obj = (v, at) => {
5216
- if (!isObject(v)) throw new Error(`Sequence schema: expected an object at ${at}`);
5217
- return v;
5218
- };
5219
- var arr = (v, at) => {
5220
- if (!Array.isArray(v)) throw new Error(`Sequence schema: expected an array at ${at}`);
5221
- return v;
5222
- };
5223
- var num = (v, at) => {
5224
- if (typeof v !== "number") throw new Error(`Sequence schema: expected a number at ${at}`);
5225
- return v;
5961
+ var PROBLEMS = {
5962
+ R1: { sentence: "The document has a top-level key the API does not take.", short: "Unknown key" },
5963
+ R2: { sentence: "The Sequence has too many steps or is too large.", short: "Too long" },
5964
+ R3: { sentence: "A step has a missing or repeated id.", short: "Needs an id" },
5965
+ R4: { sentence: "This step is not exactly one kind of step.", short: "Needs setup: pick a kind" },
5966
+ R5: { sentence: "This step has a setting its kind does not take.", short: "Unknown setting" },
5967
+ R6: { sentence: "This step leads to a step that does not exist.", short: "Leads nowhere" },
5968
+ R7: { sentence: "Choose where this step goes on its \u2713 branch.", short: "Needs setup: the \u2713 branch" },
5969
+ R8: { sentence: "These steps loop back on themselves.", short: "Loops back" },
5970
+ R9: { sentence: "Nothing leads to this step.", short: "Not reached" },
5971
+ R10: { sentence: "This path runs off the end.", short: "Needs an ending" },
5972
+ R11: { sentence: "When they reply leads to a step that does not exist.", short: "Leads nowhere" },
5973
+ R12: { sentence: "The delay or timeout is out of range.", short: "Needs setup: a duration" },
5974
+ R13: { sentence: "A setting of this step is missing or not valid.", short: "Needs setup" },
5975
+ R14: { sentence: "Choose what to wait for and for how long.", short: "Needs setup: what to wait for" },
5976
+ R15: { sentence: "The condition is not one Curviate can check.", short: "Needs setup: the condition" },
5977
+ R16: { sentence: "A variable is not one Curviate knows.", short: "Unknown variable" },
5978
+ R17: { sentence: "A variable carries a default or a filter; use a fallback instead.", short: "Variable with a default" },
5979
+ R18: { sentence: "Single braces are not a variable.", short: "Single-brace placeholder" },
5980
+ R19: { sentence: "Add a fallback for Contacts missing a field.", short: "Needs a fallback" },
5981
+ R20: { sentence: "A comment must use a Contact variable.", short: "Needs a Contact variable" },
5982
+ R21: { sentence: "Withdraw after must be between 14 and 180 days.", short: "Needs setup: withdraw after" },
5983
+ R22: { sentence: "The text is over its limit.", short: "Text too long" },
5984
+ R23: { sentence: "A/B needs exactly two variants.", short: "Needs setup: variants" },
5985
+ R24: { sentence: "The event or Tag name is not valid.", short: "Needs setup: a valid name" },
5986
+ R25: { sentence: "Saved values belong on a wait for an incoming event, at most 20.", short: "Needs setup: saved values" },
5987
+ R26: { sentence: "A field path is not valid.", short: "Needs setup: a field path" },
5988
+ R27: { sentence: "This uses a value no step saves.", short: "Value not saved" },
5989
+ R28: { sentence: "At least and at most compare numbers on saved values and custom fields.", short: "Needs setup: a number" },
5990
+ R29: { sentence: "The incoming sample has no plain value at this field.", short: "Not a plain value" },
5991
+ W1: { sentence: "Two paths merge here.", short: "Merge point" },
5992
+ W2: { sentence: "InMail needs a sending Account with Sales Navigator or Recruiter.", short: "No InMail Account" },
5993
+ W3: { sentence: "Basic Accounts send only a few invitation notes a month.", short: "Few notes on Basic" },
5994
+ W4: { sentence: "Read receipts are not available on Recruiter.", short: "No read receipts" },
5995
+ W5: { sentence: "The invitation already follows the person.", short: "Follows twice" },
5996
+ W6: { sentence: "This waits for acceptance with no invitation before it.", short: "No invitation before" },
5997
+ W7: { sentence: "The comment is personalised by name only.", short: "Name-only comment" },
5998
+ W8: { sentence: "A link in the first message gets fewer replies.", short: "Link in first message" },
5999
+ W9: { sentence: "Motion variables are deprecated.", short: "Motion variable" },
6000
+ W10: { sentence: "Retired: no longer reported.", short: "Retired" },
6001
+ W11: { sentence: "A custom field looks like a typo.", short: "Possible typo" },
6002
+ W12: { sentence: "This checks for a seen message before any message is sent.", short: "Nothing seen yet" },
6003
+ W13: { sentence: "This form is deprecated.", short: "Deprecated form" },
6004
+ W14: { sentence: "A saved value is never used.", short: "Unused value" },
6005
+ W16: { sentence: "This reads a Contact field, not the saved value.", short: "Contact field, not the value" }
5226
6006
  };
5227
- var str = (v, at) => {
5228
- if (typeof v !== "string") throw new Error(`Sequence schema: expected a string at ${at}`);
5229
- return v;
5230
- };
5231
- var after = (pattern, prefix) => {
5232
- if (!pattern.startsWith(prefix)) throw new Error(`Sequence schema: pattern ${pattern} does not start with ${prefix}`);
5233
- return "^" + pattern.slice(prefix.length);
5234
- };
5235
- function vocabulary(schema) {
5236
- assertKeywords(schema);
5237
- const root = obj(schema, "$");
5238
- const props = obj(root["properties"], "properties");
5239
- const steps = obj(props["steps"], "properties.steps");
5240
- const alts = arr(obj(steps["items"], "steps.items")["oneOf"], "steps.items.oneOf").map((a, i) => obj(a, `steps.items.oneOf[${i}]`));
5241
- const propsOf = (a) => obj(a["properties"], "step.properties");
5242
- const withProps = (a) => obj(obj(propsOf(a)["with"], "with")["properties"], "with.properties");
5243
- const doAlts = alts.filter((a) => isObject(propsOf(a)["do"]));
5244
- const kindOf = (a) => {
5245
- const req = arr(a["required"], "step.required");
5246
- return str(req[1], "step.required[1]");
5247
- };
5248
- const stepKinds = [...new Set(alts.map(kindOf))];
5249
- const stepKeys = {};
5250
- for (const a of alts) {
5251
- const k = kindOf(a);
5252
- stepKeys[k] = [.../* @__PURE__ */ new Set([...stepKeys[k] ?? [], ...Object.keys(propsOf(a))])];
5253
- }
5254
- const actions = [];
5255
- const actionParams = {};
5256
- const messagingActions = [];
5257
- let variants;
5258
- for (const a of doAlts) {
5259
- const p = propsOf(a);
5260
- const action = str(obj(p["do"], "do")["const"], "do.const");
5261
- actions.push(action);
5262
- actionParams[action] = Object.keys(withProps(a));
5263
- if (isObject(p["variants"])) {
5264
- messagingActions.push(action);
5265
- variants ?? (variants = p["variants"]);
5266
- }
5267
- }
5268
- if (!variants) throw new Error("Sequence schema: no action takes variants");
5269
- const byKind = (k) => propsOf(alts.find((a) => kindOf(a) === k) ?? {});
5270
- const stepId = obj(propsOf(alts[0])["id"], "id");
5271
- const until = arr(obj(byKind("until")["until"], "until")["anyOf"], "until.anyOf").map((x) => obj(x, "until.anyOf[]"));
5272
- const tag = obj(byKind("tag")["tag"], "tag");
5273
- const reacting = doAlts.find((a) => "reaction" in withProps(a));
5274
- if (!reacting) throw new Error("Sequence schema: no action takes a reaction");
5275
- const reactWith = withProps(reacting);
5276
- const preds = arr(obj(byKind("if")["if"], "if")["oneOf"], "if.oneOf").map((x) => obj(x, "if.oneOf[]"));
5277
- const singleKey = (p) => {
5278
- const req = p["required"];
5279
- return Array.isArray(req) && req.length === 1 && typeof req[0] === "string" ? req[0] : null;
5280
- };
5281
- const booleanPredicates = preds.filter((p) => {
5282
- const k = singleKey(p);
5283
- return k !== null && k.startsWith("contact.") && obj(propsOf(p)[k], k)["type"] === "boolean";
5284
- }).map((p) => singleKey(p));
5285
- const fields = preds.find((p) => singleKey(p) === null && isObject(p["properties"]) && Object.keys(propsOf(p)).length > 1);
5286
- if (!fields) throw new Error("Sequence schema: no standard-field predicate");
5287
- const standardFields = Object.keys(propsOf(fields)).map((k) => k.slice("contact.".length));
5288
- const operand = obj(Object.values(propsOf(fields))[0], "field operand");
5289
- const extra = preds.find((p) => isObject(p["patternProperties"]));
5290
- if (!extra) throw new Error("Sequence schema: no contact.extra predicate");
5291
- const extraKey = Object.keys(obj(extra["patternProperties"], "patternProperties"))[0];
5292
- const degree = obj(preds.find((p) => singleKey(p) === "contact.degree")["properties"], "degree")["contact.degree"];
5293
- const degreeEq = obj(obj(arr(obj(degree, "degree")["oneOf"], "degree.oneOf")[0], "degree.eq")["properties"], "degree.eq.properties")["eq"];
5294
- const connections = obj(propsOf(preds.find((p) => singleKey(p) === "contact.connections_gte"))["contact.connections_gte"], "connections_gte");
5295
- return {
5296
- schemaUrl: str(root["$id"], "$id"),
5297
- minSteps: num(steps["minItems"], "steps.minItems"),
5298
- maxSteps: num(steps["maxItems"], "steps.maxItems"),
5299
- stepKinds,
5300
- stepKeys,
5301
- actions,
5302
- actionParams,
5303
- messagingActions,
5304
- minVariants: num(variants["minItems"], "variants.minItems"),
5305
- maxVariants: num(variants["maxItems"], "variants.maxItems"),
5306
- idPattern: after(str(stepId["pattern"], "id.pattern"), "^(?!end$)"),
5307
- maxNoteChars: num(obj(propsOf(alts[0])["note"], "note")["maxLength"], "note.maxLength"),
5308
- durationPattern: str(obj(byKind("wait")["wait"], "wait")["pattern"], "wait.pattern"),
5309
- events: arr(until[0]["enum"], "until.enum").map((e) => str(e, "until.enum[]")),
5310
- eventNamePattern: after(str(until[1]["pattern"], "until.pattern"), "^(event|signal)\\."),
5311
- emitPattern: str(obj(byKind("emit")["emit"], "emit")["pattern"], "emit.pattern"),
5312
- tagPattern: str(tag["pattern"], "tag.pattern"),
5313
- maxTagChars: num(tag["maxLength"], "tag.maxLength"),
5314
- reactions: arr(obj(reactWith["reaction"], "reaction")["enum"], "reaction.enum").map((e) => str(e, "reaction.enum[]")),
5315
- post: str(obj(reactWith["post"], "post")["const"], "post.const"),
5316
- booleanPredicates,
5317
- standardFields,
5318
- fieldOperators: Object.keys(obj(operand["properties"], "operand.properties")),
5319
- extraKeyPattern: after(extraKey, "^contact\\.extra\\."),
5320
- degrees: arr(obj(degreeEq, "degree.eq")["enum"], "degree.enum"),
5321
- connectionsMin: num(connections["minimum"], "connections_gte.minimum")
5322
- };
5323
- }
6007
+ var SEQUENCE_PROBLEM_CODES = Object.keys(PROBLEMS);
5324
6008
 
5325
- // src/sequence/validate.ts
5326
- var issue = (ctx, i) => ctx.issues.push(i);
5327
- var warning = (ctx, i) => ctx.warnings.push(i);
6009
+ // src/shared/sequence/validate.ts
6010
+ function lintSequence(doc) {
6011
+ return run(doc, { kind: "lint" });
6012
+ }
5328
6013
  function jsonPointer(...tokens) {
5329
6014
  return tokens.map((t) => "/" + String(t).replace(/~/g, "~0").replace(/\//g, "~1")).join("");
5330
6015
  }
5331
- function entry(code, path, pointer, rule, token) {
5332
- return { path, rule, code, message: rule, pointer, ...ruleHelp(code, token) };
6016
+ function sequenceEntry(code, path, pointer, rule, token, candidates) {
6017
+ return {
6018
+ path,
6019
+ rule,
6020
+ code,
6021
+ message: rule,
6022
+ pointer,
6023
+ ...ruleHelp(code, token),
6024
+ severity: code.startsWith("W") ? "warning" : "error",
6025
+ ...candidates?.length ? { candidates } : {}
6026
+ };
5333
6027
  }
5334
- function entries(issues) {
5335
- return issues.sort((a, b) => a.order - b.order).map((i) => entry(i.code, i.path, i.pointer, i.rule, i.token));
6028
+ function sequenceSizeError(bytes) {
6029
+ return sequenceEntry("R2", "$", "", `the document is ${bytes} bytes; the limit is ${MAX_SEQUENCE_BYTES}, UTF-8 compact JSON`);
5336
6030
  }
5337
6031
  var UNTIL = `; accepted until ${DEPRECATED_ACCEPTED_UNTIL}`;
6032
+ var ID_RE = new RegExp(ID_PATTERN);
6033
+ var DURATION_RE = new RegExp(DURATION_PATTERN);
6034
+ var EVENT_RE = new RegExp(EVENT_NAME_PATTERN);
6035
+ var EXTRA_KEY_RE = new RegExp(EXTRA_KEY_PATTERN);
6036
+ var TAG_RE = new RegExp(TAG_PATTERN);
6037
+ var SAVE_NAME_RE = new RegExp(SAVE_NAME_PATTERN);
5338
6038
  var VARIABLE_RE = /\{\{([^{}]*)\}\}/g;
5339
6039
  var SINGLE_BRACE_RE = /\{[A-Za-z_][A-Za-z0-9_.]*\}/g;
6040
+ var KIND_LIST = STEP_KINDS.join(", ");
5340
6041
  function keysOf(o) {
5341
6042
  return Object.keys(o).filter((k) => o[k] !== void 0);
5342
6043
  }
5343
6044
  function has(o, k) {
5344
- return Object.prototype.hasOwnProperty.call(o, k) && o[k] !== void 0;
6045
+ return Object.hasOwn(o, k) && o[k] !== void 0;
5345
6046
  }
5346
6047
  function quote(token) {
5347
6048
  return [...token].slice(0, MAX_QUOTED_TOKEN_CHARS).join("");
@@ -5358,31 +6059,32 @@ function deeperThan(v, max) {
5358
6059
  for (let depth = 1; level.length; depth++) {
5359
6060
  const next = [];
5360
6061
  for (const x of level) {
5361
- if (typeof x === "object" && x !== null) for (const child of Object.values(x)) next.push(child);
6062
+ if (typeof x === "object" && x !== null)
6063
+ for (const child of Object.values(x))
6064
+ next.push(child);
5362
6065
  }
5363
- if (!next.length) return false;
5364
- if (depth > max) return true;
6066
+ if (!next.length)
6067
+ return false;
6068
+ if (depth > max)
6069
+ return true;
5365
6070
  level = next;
5366
6071
  }
5367
6072
  return false;
5368
6073
  }
5369
- function durationMinutes(v) {
5370
- const m = /^([1-9][0-9]*)(m|h|d)$/.exec(v);
5371
- if (!m) return 0;
5372
- return Number(m[1]) * { m: 1, h: 60, d: 1440 }[m[2]];
5373
- }
5374
6074
  function durationRule(bounds) {
5375
6075
  return `must be a duration between ${bounds.label}: a number then m, h or d, like 30d`;
5376
6076
  }
5377
6077
  var durationHint = (bounds) => `${bounds.label}, for example ${bounds.example}`;
5378
- function durationOk(ctx, v, bounds) {
5379
- if (typeof v !== "string" || !ctx.re.duration.test(v)) return false;
6078
+ function durationOk(v, bounds) {
6079
+ if (typeof v !== "string" || !DURATION_RE.test(v))
6080
+ return false;
5380
6081
  const m = durationMinutes(v);
5381
6082
  return m >= bounds.min && m <= bounds.max;
5382
6083
  }
5383
- function tagOk(ctx, v) {
5384
- return typeof v === "string" && [...v].length <= ctx.v.maxTagChars && ctx.re.tag.test(v);
6084
+ function tagOk(v) {
6085
+ return typeof v === "string" && [...v].length <= MAX_TAG_CHARS && TAG_RE.test(v);
5385
6086
  }
6087
+ var TAG_RULE = "a Tag name is 1 to 64 characters, without , or ;";
5386
6088
  function variablesIn(text) {
5387
6089
  return [...text.matchAll(VARIABLE_RE)].map((m) => ({ token: m[0], inner: m[1].trim() }));
5388
6090
  }
@@ -5392,44 +6094,34 @@ function hasVariable(text) {
5392
6094
  function staticLength(text) {
5393
6095
  return [...text.replace(VARIABLE_RE, "")].length;
5394
6096
  }
5395
- function longVariable(ctx, inner) {
5396
- if (inner.includes(".")) return inner;
5397
- if (!ctx.re.short.test(inner)) return null;
5398
- if (MY_FIELDS.includes(inner)) return `account.${inner.slice(3)}`;
5399
- if (inner.startsWith("my_")) return null;
5400
- if (ctx.v.standardFields.includes(inner)) return `contact.${inner}`;
5401
- return `contact.extra.${inner}`;
5402
- }
5403
- function isKnownVariable(ctx, inner) {
5404
- const long = longVariable(ctx, inner);
5405
- if (long === null) return false;
5406
- if (long.startsWith("contact.extra.")) return ctx.re.extraKey.test(long.slice("contact.extra.".length));
5407
- if (long.startsWith("contact.")) return ctx.v.standardFields.includes(long.slice(8));
5408
- if (long.startsWith("account.")) return ACCOUNT_FIELDS.includes(long.slice(8));
5409
- return false;
5410
- }
5411
6097
  function distance(a, b, cap) {
5412
- if (Math.abs(a.length - b.length) > cap) return cap + 1;
6098
+ if (Math.abs(a.length - b.length) > cap)
6099
+ return cap + 1;
5413
6100
  let prev = Array.from({ length: b.length + 1 }, (_, j) => j);
5414
6101
  for (let i = 1; i <= a.length; i++) {
5415
6102
  const cur = [i];
5416
- for (let j = 1; j <= b.length; j++) cur[j] = Math.min(prev[j] + 1, cur[j - 1] + 1, prev[j - 1] + (a[i - 1] === b[j - 1] ? 0 : 1));
6103
+ for (let j = 1; j <= b.length; j++)
6104
+ cur[j] = Math.min(prev[j] + 1, cur[j - 1] + 1, prev[j - 1] + (a[i - 1] === b[j - 1] ? 0 : 1));
5417
6105
  prev = cur;
5418
6106
  }
5419
6107
  return prev[b.length];
5420
6108
  }
5421
- function likelyTypoOf(ctx, key) {
6109
+ var NEAR_NAMES = [...STANDARD_FIELDS, ...MY_FIELDS];
6110
+ function likelyTypoOf(key) {
5422
6111
  let best = null;
5423
6112
  let bestD = 3;
5424
- for (const n of [...ctx.v.standardFields, ...MY_FIELDS]) {
6113
+ for (const n of NEAR_NAMES) {
5425
6114
  const d = distance(key, n, 2);
5426
- if (d < bestD) [best, bestD] = [n, d];
6115
+ if (d < bestD)
6116
+ [best, bestD] = [n, d];
5427
6117
  }
5428
6118
  return best;
5429
6119
  }
6120
+ var issue = (ctx, i) => ctx.issues.push(i);
6121
+ var warning = (ctx, i) => ctx.warnings.push(i);
5430
6122
  function checkText(ctx, order, path, pointer, parent, value, limit) {
5431
6123
  if (typeof value !== "string" || value.trim() === "") {
5432
- issue(ctx, { order, code: "R13", path, pointer: value === void 0 ? parent : pointer, rule: "must be non-empty text" });
6124
+ issue(ctx, { order, code: "R13", path, pointer: typeof value === "undefined" ? parent : pointer, rule: "must be non-empty text" });
5433
6125
  return null;
5434
6126
  }
5435
6127
  const at = { order, path, pointer };
@@ -5442,22 +6134,31 @@ function checkText(ctx, order, path, pointer, parent, value, limit) {
5442
6134
  issue(ctx, { ...at, code: "R17", rule: "variable defaults are not supported; add a fallback" });
5443
6135
  continue;
5444
6136
  }
5445
- if (v.inner.startsWith("motion.") && ctx.re.motion.test(v.inner.slice(7))) {
5446
- const key = v.inner.slice(7);
6137
+ const motion = /^motion\.([a-z][a-z0-9_]{0,39})$/.exec(v.inner);
6138
+ if (motion) {
5447
6139
  warning(ctx, {
5448
6140
  ...at,
5449
6141
  code: "W13",
5450
6142
  rule: `${quote(v.token)}: Motion variables were removed${UNTIL}; write the text in place or use a custom field`,
5451
- token: `text in place or a custom field such as {{${key}}}`
6143
+ token: `text in place or a custom field such as {{${motion[1]}}}`
5452
6144
  });
5453
- warning(ctx, { ...at, code: "W9", rule: `${quote(v.token)} is checked against settings.variables by the server only` });
6145
+ if (ctx.mode.kind === "lint") {
6146
+ warning(ctx, { ...at, code: "W9", rule: `${quote(v.token)} is checked against settings.variables by the server only` });
6147
+ } else if (!Object.hasOwn(ctx.mode.variables, motion[1])) {
6148
+ issue(ctx, { ...at, code: "R16", rule: `${quote(v.token)} is not defined in settings.variables` });
6149
+ }
6150
+ continue;
6151
+ }
6152
+ const ev = /^enrollment\.(.*)$/s.exec(v.inner);
6153
+ if (ev) {
6154
+ enrollmentRef(ctx, at, quote(v.token), ev[1], (k) => `{{${k}}}`);
5454
6155
  continue;
5455
6156
  }
5456
6157
  if (!v.inner.includes(".") && v.inner.startsWith("my_") && !MY_FIELDS.includes(v.inner)) {
5457
6158
  issue(ctx, { ...at, code: "R16", rule: `${quote(v.token)}: the my_ prefix is reserved for my_first_name, my_last_name and my_full_name` });
5458
6159
  continue;
5459
6160
  }
5460
- if (!isKnownVariable(ctx, v.inner)) {
6161
+ if (!isKnownVariable(v.inner)) {
5461
6162
  issue(ctx, {
5462
6163
  ...at,
5463
6164
  code: "R16",
@@ -5465,9 +6166,15 @@ function checkText(ctx, order, path, pointer, parent, value, limit) {
5465
6166
  });
5466
6167
  continue;
5467
6168
  }
5468
- if (!v.inner.includes(".") && longVariable(ctx, v.inner).startsWith("contact.extra.")) {
5469
- const near = likelyTypoOf(ctx, v.inner);
5470
- if (near !== null) warning(ctx, { ...at, code: "W11", rule: `${quote(v.token)} is read as a custom field; did you mean ${near}?`, token: near });
6169
+ const long = longVariable(v.inner);
6170
+ if (!v.inner.includes(".") && long.startsWith("contact.extra.") && ctx.values.saved.has(v.inner)) {
6171
+ const want = `{{enrollment.${v.inner}}}`;
6172
+ warning(ctx, { ...at, code: "W16", rule: `${quote(v.token)} is read as a Contact field; did you mean ${want}?`, token: want, candidates: [want] });
6173
+ }
6174
+ if (!v.inner.includes(".") && long.startsWith("contact.extra.")) {
6175
+ const near = likelyTypoOf(v.inner);
6176
+ if (near !== null)
6177
+ warning(ctx, { ...at, code: "W11", rule: `${quote(v.token)} is read as a custom field; did you mean ${near}?`, token: near });
5471
6178
  }
5472
6179
  }
5473
6180
  const rest = value.replace(VARIABLE_RE, " ");
@@ -5479,11 +6186,37 @@ function checkText(ctx, order, path, pointer, parent, value, limit) {
5479
6186
  }
5480
6187
  }
5481
6188
  const len = staticLength(value);
5482
- if (len > limit) issue(ctx, { ...at, code: "R22", rule: `${len} characters; the limit is ${limit}, variables not counted` });
6189
+ if (len > limit) {
6190
+ issue(ctx, { ...at, code: "R22", rule: `${len} characters; the limit is ${limit}, variables not counted` });
6191
+ }
5483
6192
  return value;
5484
6193
  }
6194
+ function enrollmentRef(ctx, at, shown, key, form) {
6195
+ if (key === "id") {
6196
+ issue(ctx, { ...at, code: "R27", rule: `${shown}: enrollment.id is reserved`, token: "{{enrollment_id}}", candidates: ["{{enrollment_id}}"] });
6197
+ return;
6198
+ }
6199
+ ctx.values.used.add(key);
6200
+ if (ctx.values.saved.has(key))
6201
+ return;
6202
+ const candidates = ctx.values.toContact.has(key) ? [form(`contact.extra.${key}`)] : [...ctx.values.saved.keys()].filter((n) => distance(key, n, 2) <= 2).map((n) => form(`enrollment.${n}`));
6203
+ issue(ctx, { ...at, code: "R27", rule: `${shown}: no save of this Sequence writes ${quote(key)}`, ...candidates.length ? { token: candidates[0], candidates } : {} });
6204
+ }
6205
+ function isKnownVariable(inner) {
6206
+ const long = longVariable(inner);
6207
+ if (long === null)
6208
+ return false;
6209
+ if (long.startsWith("contact.extra."))
6210
+ return EXTRA_KEY_RE.test(long.slice("contact.extra.".length));
6211
+ if (long.startsWith("contact."))
6212
+ return STANDARD_FIELDS.includes(long.slice(8));
6213
+ if (long.startsWith("account."))
6214
+ return ACCOUNT_FIELDS.includes(long.slice(8));
6215
+ return false;
6216
+ }
5485
6217
  function checkFallbackText(ctx, order, path, pointer, parent, value, limit, keyword) {
5486
- if (keyword !== void 0 && value === keyword) return;
6218
+ if (keyword !== void 0 && value === keyword)
6219
+ return;
5487
6220
  const at = { order, code: "R19", path, pointer };
5488
6221
  if (typeof value !== "string") {
5489
6222
  issue(ctx, { ...at, pointer: value === void 0 ? parent : pointer, rule: keyword ? `must be text or "${keyword}"` : "must be text" });
@@ -5499,82 +6232,39 @@ function checkFallbackText(ctx, order, path, pointer, parent, value, limit, keyw
5499
6232
  }
5500
6233
  checkText(ctx, order, path, pointer, parent, value, limit);
5501
6234
  }
5502
- function createValidator(v) {
5503
- const re = {
5504
- id: new RegExp(v.idPattern),
5505
- duration: new RegExp(v.durationPattern),
5506
- emit: new RegExp(v.emitPattern),
5507
- event: new RegExp(v.eventNamePattern),
5508
- extraKey: new RegExp(v.extraKeyPattern),
5509
- tag: new RegExp(v.tagPattern),
5510
- motion: new RegExp(MOTION_KEY_PATTERN),
5511
- short: new RegExp(SHORT_VARIABLE_PATTERN)
5512
- };
5513
- return (doc) => run(doc, { v, re, issues: [], warnings: [] });
5514
- }
5515
- function validateSequence(value) {
5516
- const out = verdict(value);
5517
- return { errors: out.errors.slice(0, MAX_ERRORS_LISTED), warnings: out.warnings };
5518
- }
5519
- function verdict(value) {
5520
- shipped ?? (shipped = createValidator(vocabulary(SEQUENCE_SCHEMA_V1)));
5521
- return shipped(value);
5522
- }
5523
- var shipped;
5524
- var UNSTORABLE_RULE = "must be well-formed Unicode and must not contain a NUL (\\u0000) character";
5525
- var MAX_ERRORS_LISTED = 100;
5526
- var UNSTORABLE_RE = /\u0000|[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?<![\uD800-\uDBFF])[\uDC00-\uDFFF]/;
5527
- function firstUnstorable(doc) {
5528
- const stack = [["value", doc, ""]];
5529
- while (stack.length) {
5530
- const [kind, value, path] = stack.pop();
5531
- if (kind === "key") {
5532
- if (UNSTORABLE_RE.test(value)) return path ? `${path} key` : "$ key";
5533
- } else if (typeof value === "string") {
5534
- if (UNSTORABLE_RE.test(value)) return path || "$";
5535
- } else if (Array.isArray(value)) {
5536
- for (let i = value.length - 1; i >= 0; i--) stack.push(["value", value[i], `${path}[${i}]`]);
5537
- } else if (typeof value === "object" && value !== null) {
5538
- const entries2 = Object.entries(value);
5539
- for (let i = entries2.length - 1; i >= 0; i--) {
5540
- const [k, v] = entries2[i];
5541
- stack.push(["value", v, path ? `${path}.${k}` : k], ["key", k, path]);
5542
- }
5543
- }
5544
- }
5545
- return null;
5546
- }
5547
- function run(doc, ctx) {
5548
- const bad = firstUnstorable(doc);
5549
- if (bad !== null) return { errors: [{ path: bad, rule: UNSTORABLE_RULE }], warnings: [] };
5550
- const size = compactBytes(doc);
5551
- if (size > MAX_SEQUENCE_BYTES) {
5552
- return { errors: [entry("R2", "$", "", `the document is ${size} bytes; the limit is ${MAX_SEQUENCE_BYTES}, UTF-8 compact JSON`)], warnings: [] };
5553
- }
5554
- return rules(doc, ctx);
6235
+ function entries(issues) {
6236
+ return issues.sort((a, b) => a.order - b.order).map((i) => sequenceEntry(i.code, i.path, i.pointer, i.rule, i.token, i.candidates));
5555
6237
  }
5556
- function rules(doc, ctx) {
5557
- const { v } = ctx;
6238
+ function run(doc, mode) {
6239
+ const ctx = { mode, issues: [], warnings: [], values: { saved: /* @__PURE__ */ new Map(), toContact: /* @__PURE__ */ new Set(), used: /* @__PURE__ */ new Set() } };
5558
6240
  const DOC2 = -1;
5559
- if (!isObject(doc)) return { errors: [entry("R1", "$", "", "a Sequence document is a JSON object")], warnings: [] };
6241
+ if (!isObject(doc)) {
6242
+ return { errors: [sequenceEntry("R1", "$", "", "a Sequence document is a JSON object")], warnings: [] };
6243
+ }
5560
6244
  for (const k of keysOf(doc)) {
5561
6245
  if (k !== "$schema" && k !== "on_reply" && k !== "steps") {
5562
6246
  issue(ctx, { order: DOC2, code: "R1", path: quote(k), pointer: jsonPointer(k), rule: "unknown key; a Sequence document holds only $schema, on_reply and steps" });
5563
6247
  }
5564
6248
  }
5565
- if (has(doc, "$schema") && doc["$schema"] !== v.schemaUrl) {
5566
- issue(ctx, { order: DOC2, code: "R1", path: "$schema", pointer: "/$schema", rule: `must be ${v.schemaUrl}` });
6249
+ if (has(doc, "$schema") && doc["$schema"] !== SEQUENCE_SCHEMA_URL2) {
6250
+ issue(ctx, { order: DOC2, code: "R1", path: "$schema", pointer: "/$schema", rule: `must be ${SEQUENCE_SCHEMA_URL2}` });
6251
+ }
6252
+ const bytes = compactBytes(doc);
6253
+ if (bytes > MAX_SEQUENCE_BYTES) {
6254
+ const e = sequenceSizeError(bytes);
6255
+ issue(ctx, { order: DOC2, code: e.code, path: e.path, pointer: e.pointer, rule: e.rule });
5567
6256
  }
5568
6257
  const rawSteps = doc["steps"];
5569
6258
  const stepsArr = Array.isArray(rawSteps) ? rawSteps : [];
5570
- if (!Array.isArray(rawSteps) || rawSteps.length < v.minSteps) {
5571
- issue(ctx, { order: DOC2, code: "R2", path: "steps", pointer: has(doc, "steps") ? "/steps" : "", rule: `required: an array of ${v.minSteps} to ${v.maxSteps} steps` });
5572
- } else if (rawSteps.length > v.maxSteps) {
5573
- issue(ctx, { order: DOC2, code: "R2", path: "steps", pointer: "/steps", rule: `${rawSteps.length} steps; the limit is ${v.maxSteps}` });
6259
+ ctx.values = valuesOf(stepsArr);
6260
+ if (!Array.isArray(rawSteps) || rawSteps.length === 0) {
6261
+ issue(ctx, { order: DOC2, code: "R2", path: "steps", pointer: has(doc, "steps") ? "/steps" : "", rule: `required: an array of 1 to ${MAX_STEPS} steps` });
6262
+ } else if (rawSteps.length > MAX_STEPS) {
6263
+ issue(ctx, { order: DOC2, code: "R2", path: "steps", pointer: "/steps", rule: `${rawSteps.length} steps; the limit is ${MAX_STEPS}` });
5574
6264
  }
5575
6265
  const infos = [];
5576
6266
  const firstIndex = /* @__PURE__ */ new Map();
5577
- let structural = !Array.isArray(rawSteps) || rawSteps.length < v.minSteps || rawSteps.length > v.maxSteps;
6267
+ let structural = !Array.isArray(rawSteps) || rawSteps.length === 0 || rawSteps.length > MAX_STEPS;
5578
6268
  stepsArr.forEach((raw, index) => {
5579
6269
  const ptr = jsonPointer("steps", index);
5580
6270
  const info = { index, prefix: `steps[${index}]`, ptr, id: null, kind: null, raw: null };
@@ -5588,10 +6278,10 @@ function rules(doc, ctx) {
5588
6278
  const id = raw["id"];
5589
6279
  const idAt = { order: index, code: "R3", path: `${info.prefix}.id`, pointer: `${ptr}/id` };
5590
6280
  if (!has(raw, "id")) {
5591
- issue(ctx, { ...idAt, pointer: ptr, rule: `required: matching ${v.idPattern}` });
6281
+ issue(ctx, { ...idAt, pointer: ptr, rule: `required: matching ${ID_PATTERN}` });
5592
6282
  structural = true;
5593
- } else if (typeof id !== "string" || !ctx.re.id.test(id)) {
5594
- issue(ctx, { ...idAt, rule: `must match ${v.idPattern}` });
6283
+ } else if (typeof id !== "string" || !ID_RE.test(id)) {
6284
+ issue(ctx, { ...idAt, rule: `must match ${ID_PATTERN}` });
5595
6285
  structural = true;
5596
6286
  } else if (id === END) {
5597
6287
  issue(ctx, { ...idAt, rule: '"end" is reserved' });
@@ -5604,14 +6294,14 @@ function rules(doc, ctx) {
5604
6294
  info.id = id;
5605
6295
  info.prefix = id;
5606
6296
  }
5607
- const kinds = v.stepKinds.filter((k) => has(raw, k));
6297
+ const kinds = STEP_KINDS.filter((k) => has(raw, k));
5608
6298
  if (kinds.length !== 1) {
5609
6299
  issue(ctx, {
5610
6300
  order: index,
5611
6301
  code: "R4",
5612
6302
  path: info.prefix,
5613
6303
  pointer: ptr,
5614
- rule: kinds.length === 0 ? `a step needs exactly one of ${v.stepKinds.join(", ")}` : `a step has exactly one kind; found ${kinds.join(", ")}`
6304
+ rule: kinds.length === 0 ? `a step needs exactly one of ${KIND_LIST}` : `a step has exactly one kind; found ${kinds.join(", ")}`
5615
6305
  });
5616
6306
  structural = true;
5617
6307
  return;
@@ -5633,11 +6323,16 @@ function rules(doc, ctx) {
5633
6323
  }
5634
6324
  const usedIds = new Set(firstIndex.keys());
5635
6325
  for (const info of infos) {
5636
- if (!info.raw || !info.kind) continue;
6326
+ if (!info.raw || !info.kind)
6327
+ continue;
5637
6328
  const { raw, kind, index: order, prefix: p, ptr } = info;
5638
- const keys = v.stepKeys[kind] ?? [];
5639
6329
  for (const k of keysOf(raw)) {
5640
- if (keys.includes(k)) continue;
6330
+ if (STEP_KEYS[kind].includes(k))
6331
+ continue;
6332
+ if (k === "save" || k === "save_to_contact") {
6333
+ issue(ctx, { order, code: "R25", path: `${p}.${k}`, pointer: `${ptr}/${k}`, rule: `${k} is allowed only on an until event.<name> step` });
6334
+ continue;
6335
+ }
5641
6336
  issue(ctx, {
5642
6337
  order,
5643
6338
  code: "R5",
@@ -5648,14 +6343,14 @@ function rules(doc, ctx) {
5648
6343
  }
5649
6344
  if (has(raw, "note")) {
5650
6345
  const n = raw["note"];
5651
- if (typeof n !== "string" || [...n].length > v.maxNoteChars) {
5652
- issue(ctx, { order, code: "R22", path: `${p}.note`, pointer: `${ptr}/note`, rule: `must be text of at most ${v.maxNoteChars} characters` });
6346
+ if (typeof n !== "string" || [...n].length > MAX_STEP_NOTE_CHARS) {
6347
+ issue(ctx, { order, code: "R22", path: `${p}.note`, pointer: `${ptr}/note`, rule: `must be text of at most ${MAX_STEP_NOTE_CHARS} characters` });
5653
6348
  }
5654
6349
  }
5655
- if (has(raw, "event") && keys.includes("event")) {
6350
+ if (has(raw, "event") && STEP_KEYS[kind].includes("event")) {
5656
6351
  const e = raw["event"];
5657
- if (typeof e !== "string" || !ctx.re.event.test(e)) {
5658
- issue(ctx, { order, code: "R24", path: `${p}.event`, pointer: `${ptr}/event`, rule: `must match ${v.eventNamePattern}, like invite_sent` });
6352
+ if (typeof e !== "string" || !EVENT_RE.test(e)) {
6353
+ issue(ctx, { order, code: "R24", path: `${p}.event`, pointer: `${ptr}/event`, rule: `must match ${EVENT_NAME_PATTERN}, like invite_sent` });
5659
6354
  }
5660
6355
  }
5661
6356
  switch (kind) {
@@ -5663,7 +6358,7 @@ function rules(doc, ctx) {
5663
6358
  checkDo(ctx, order, p, ptr, raw, usedIds);
5664
6359
  break;
5665
6360
  case "wait":
5666
- if (!durationOk(ctx, raw["wait"], DURATION_BOUNDS.wait)) {
6361
+ if (!durationOk(raw["wait"], DURATION_BOUNDS.wait)) {
5667
6362
  issue(ctx, { order, code: "R12", path: `${p}.wait`, pointer: `${ptr}/wait`, rule: durationRule(DURATION_BOUNDS.wait), token: durationHint(DURATION_BOUNDS.wait) });
5668
6363
  }
5669
6364
  break;
@@ -5671,17 +6366,18 @@ function rules(doc, ctx) {
5671
6366
  checkUntil(ctx, order, p, ptr, raw);
5672
6367
  break;
5673
6368
  case "if":
5674
- checkPredicate(ctx, raw["if"], `${p}.if`, `${ptr}/if`, order);
6369
+ checkPredicate(raw["if"], `${p}.if`, `${ptr}/if`, false, ctx.issues, order, false, ctx);
5675
6370
  break;
5676
6371
  case "emit": {
5677
6372
  const e = raw["emit"];
5678
- if (typeof e !== "string" || !ctx.re.emit.test(e)) {
5679
- issue(ctx, { order, code: "R24", path: `${p}.emit`, pointer: `${ptr}/emit`, rule: `must match ${v.emitPattern}, like reply.received` });
6373
+ if (typeof e !== "string" || !EVENT_RE.test(e)) {
6374
+ issue(ctx, { order, code: "R24", path: `${p}.emit`, pointer: `${ptr}/emit`, rule: `must match ${EVENT_NAME_PATTERN}, like reply.received` });
5680
6375
  }
5681
6376
  break;
5682
6377
  }
5683
6378
  case "tag":
5684
- if (!tagOk(ctx, raw["tag"])) issue(ctx, { order, code: "R24", path: `${p}.tag`, pointer: `${ptr}/tag`, rule: "a Tag name is 1 to 64 characters, without , or ;" });
6379
+ if (!tagOk(raw["tag"]))
6380
+ issue(ctx, { order, code: "R24", path: `${p}.tag`, pointer: `${ptr}/tag`, rule: TAG_RULE });
5685
6381
  break;
5686
6382
  }
5687
6383
  if ((kind === "until" || kind === "if") && !has(raw, "then")) {
@@ -5689,7 +6385,8 @@ function rules(doc, ctx) {
5689
6385
  structural = true;
5690
6386
  }
5691
6387
  for (const f of ["then", "else", "next"]) {
5692
- if (!has(raw, f) || !keys.includes(f)) continue;
6388
+ if (!has(raw, f) || !STEP_KEYS[kind].includes(f))
6389
+ continue;
5693
6390
  const t = raw[f];
5694
6391
  const at = { order, code: "R6", path: `${p}.${f}`, pointer: `${ptr}/${f}` };
5695
6392
  if (typeof t !== "string") {
@@ -5701,7 +6398,24 @@ function rules(doc, ctx) {
5701
6398
  }
5702
6399
  }
5703
6400
  }
5704
- if (!structural) graph(ctx, infos, firstIndex, onReply);
6401
+ for (const info of infos) {
6402
+ const save = info.kind === "until" && awaitedEventName(info.raw["until"]) !== null ? info.raw["save"] : void 0;
6403
+ if (!isObject(save))
6404
+ continue;
6405
+ for (const name of keysOf(save)) {
6406
+ if (ctx.values.used.has(name) || name === "id" || !SAVE_NAME_RE.test(name))
6407
+ continue;
6408
+ warning(ctx, {
6409
+ order: info.index,
6410
+ code: "W14",
6411
+ path: `${info.prefix}.save.${quote(name)}`,
6412
+ pointer: `${info.ptr}/save${jsonPointer(name)}`,
6413
+ rule: `${quote(name)} is saved but nothing in this Sequence reads it; a webhook template may still send it`
6414
+ });
6415
+ }
6416
+ }
6417
+ if (!structural)
6418
+ graph(ctx, infos, firstIndex, onReply);
5705
6419
  return { errors: entries(ctx.issues), warnings: entries(ctx.warnings) };
5706
6420
  }
5707
6421
  function checkUntil(ctx, order, p, ptr, raw) {
@@ -5710,17 +6424,21 @@ function checkUntil(ctx, order, p, ptr, raw) {
5710
6424
  const custom = typeof ev === "string" ? /^(event|signal)\.(.*)$/s.exec(ev) : null;
5711
6425
  if (custom) {
5712
6426
  const [, ns, name] = custom;
5713
- if (!ctx.re.event.test(name)) {
5714
- issue(ctx, { ...at, rule: `an event name must match ${ctx.v.eventNamePattern}` });
6427
+ if (!EVENT_RE.test(name)) {
6428
+ issue(ctx, { ...at, rule: `an event name must match ${EVENT_NAME_PATTERN}` });
5715
6429
  } else if (ns === "signal") {
5716
6430
  warning(ctx, { ...at, code: "W13", rule: `until signal.${quote(name)} is now until event.${quote(name)}${UNTIL}`, token: `event.${quote(name)}` });
5717
6431
  }
5718
- } else if (!ctx.v.events.includes(ev)) {
5719
- issue(ctx, { ...at, rule: `unknown event; use ${ctx.v.events.join(", ")} or event.<name>` });
6432
+ } else if (!EVENTS.includes(ev)) {
6433
+ issue(ctx, { ...at, rule: "unknown event; use invite.accepted, message.replied, message.read or event.<name>" });
6434
+ }
6435
+ for (const key of ["save", "save_to_contact"]) {
6436
+ if (has(raw, key))
6437
+ checkSave(ctx, order, p, ptr, raw, key, custom && EVENT_RE.test(custom[2]) ? custom[2] : null);
5720
6438
  }
5721
6439
  if (!has(raw, "timeout")) {
5722
6440
  issue(ctx, { order, code: "R14", path: `${p}.timeout`, pointer: ptr, rule: "required on an until step" });
5723
- } else if (!durationOk(ctx, raw["timeout"], DURATION_BOUNDS.timeout)) {
6441
+ } else if (!durationOk(raw["timeout"], DURATION_BOUNDS.timeout)) {
5724
6442
  issue(ctx, {
5725
6443
  order,
5726
6444
  code: "R12",
@@ -5731,20 +6449,75 @@ function checkUntil(ctx, order, p, ptr, raw) {
5731
6449
  });
5732
6450
  }
5733
6451
  }
5734
- function limitOf(a, field) {
5735
- if (a === "message.send") return TEXT_LIMITS.message;
5736
- if (a === "post.comment") return TEXT_LIMITS.comment;
5737
- if (a === "invite.send") return TEXT_LIMITS.invite_note;
5738
- return field === "subject" ? TEXT_LIMITS.inmail_subject : TEXT_LIMITS.inmail_text;
6452
+ var SAVE_CONTACT_FIELDS = STANDARD_FIELDS.filter((f) => f !== "email");
6453
+ function checkSave(ctx, order, p, ptr, raw, key, event) {
6454
+ const v = raw[key];
6455
+ const at = { order, path: `${p}.${key}`, pointer: `${ptr}/${key}` };
6456
+ if (event === null) {
6457
+ issue(ctx, { ...at, code: "R25", rule: `${key} is allowed only on an until event.<name> step` });
6458
+ return;
6459
+ }
6460
+ if (!isObject(v) || keysOf(v).length === 0) {
6461
+ issue(ctx, { ...at, code: "R25", rule: `must be an object of 1 to ${MAX_SAVE_ENTRIES} entries` });
6462
+ return;
6463
+ }
6464
+ const names = keysOf(v);
6465
+ if (names.length > MAX_SAVE_ENTRIES)
6466
+ issue(ctx, { ...at, code: "R25", rule: `${names.length} entries; the limit is ${MAX_SAVE_ENTRIES}` });
6467
+ for (const name of names) {
6468
+ const e = { order, path: `${at.path}.${quote(name)}`, pointer: `${at.pointer}${jsonPointer(name)}` };
6469
+ if (key === "save") {
6470
+ if (name === "id")
6471
+ issue(ctx, { ...e, code: "R25", rule: '"id" is reserved; enrollment_id is always sent' });
6472
+ else if (!SAVE_NAME_RE.test(name))
6473
+ issue(ctx, { ...e, code: "R25", rule: `a value name must match ${SAVE_NAME_PATTERN}` });
6474
+ } else if (SAVE_CONTACT_FIELDS.includes(name)) {
6475
+ issue(ctx, { ...e, code: "R25", rule: `${name} is a standard Contact field; only email can be saved to one` });
6476
+ } else if (name !== "email" && !EXTRA_KEY_RE.test(name)) {
6477
+ issue(ctx, { ...e, code: "R25", rule: `a custom field key must match ${EXTRA_KEY_PATTERN}, or be email` });
6478
+ }
6479
+ const path = typeof v[name] === "string" ? normalizePath(v[name]) : null;
6480
+ if (path === null) {
6481
+ issue(ctx, { ...e, code: "R26", rule: 'not a field path; use dots, items[0] and ["a.b"], at most 256 characters, like lead.score' });
6482
+ continue;
6483
+ }
6484
+ const type = sampleType(ctx, event, path);
6485
+ if (type === "object" || type === "array") {
6486
+ issue(ctx, { ...e, code: "R29", rule: `${quote(path)} is an ${type} in the sample of event ${event}; save a field inside it` });
6487
+ }
6488
+ }
6489
+ }
6490
+ function sampleType(ctx, event, path) {
6491
+ if (ctx.mode.kind !== "server")
6492
+ return null;
6493
+ return ctx.mode.shapes[event]?.find((f) => f.path === path)?.type ?? null;
6494
+ }
6495
+ function valuesOf(steps) {
6496
+ const out = { saved: /* @__PURE__ */ new Map(), toContact: /* @__PURE__ */ new Set(), used: /* @__PURE__ */ new Set() };
6497
+ for (const s of steps) {
6498
+ if (!isObject(s))
6499
+ continue;
6500
+ const m = typeof s["until"] === "string" ? /^(?:event|signal)\.(.+)$/s.exec(s["until"]) : null;
6501
+ if (isObject(s["save"])) {
6502
+ for (const [name, raw] of Object.entries(s["save"])) {
6503
+ const path = typeof raw === "string" ? normalizePath(raw) : null;
6504
+ out.saved.set(name, [...out.saved.get(name) ?? [], ...m && path ? [{ event: m[1], path }] : []]);
6505
+ }
6506
+ }
6507
+ if (isObject(s["save_to_contact"]))
6508
+ for (const k of Object.keys(s["save_to_contact"]))
6509
+ out.toContact.add(k);
6510
+ }
6511
+ return out;
5739
6512
  }
5740
6513
  function checkDo(ctx, order, p, ptr, raw, usedIds) {
5741
- const { v } = ctx;
5742
- const a = raw["do"];
5743
- if (typeof a !== "string" || !v.actions.includes(a)) {
5744
- issue(ctx, { order, code: "R13", path: `${p}.do`, pointer: `${ptr}/do`, rule: `unknown action; use ${v.actions.join(", ")}` });
6514
+ const action = raw["do"];
6515
+ if (!ACTIONS.includes(action)) {
6516
+ issue(ctx, { order, code: "R13", path: `${p}.do`, pointer: `${ptr}/do`, rule: `unknown action; use ${ACTIONS.join(", ")}` });
5745
6517
  return;
5746
6518
  }
5747
- const params = v.actionParams[a] ?? [];
6519
+ const a = action;
6520
+ const params = ACTION_PARAMS[a];
5748
6521
  const wp = `${ptr}/with`;
5749
6522
  const withParent = isObject(raw["with"]) ? wp : ptr;
5750
6523
  let w = {};
@@ -5766,14 +6539,16 @@ function checkDo(ctx, order, p, ptr, raw, usedIds) {
5766
6539
  }
5767
6540
  }
5768
6541
  }
5769
- const messaging = v.messagingActions.includes(a);
6542
+ const messaging = MESSAGING_ACTIONS.includes(a);
5770
6543
  const hasVariants = has(raw, "variants");
5771
6544
  const vptr = `${ptr}/variants`;
5772
- if (hasVariants && !messaging) issue(ctx, { order, code: "R23", path: `${p}.variants`, pointer: vptr, rule: "variants are allowed on messaging steps only" });
5773
- if (has(w, "post") && params.includes("post") && w["post"] !== v.post) {
5774
- issue(ctx, { order, code: "R13", path: `${p}.with.post`, pointer: `${wp}/post`, rule: `only "${v.post}" is supported` });
6545
+ if (hasVariants && !messaging) {
6546
+ issue(ctx, { order, code: "R23", path: `${p}.variants`, pointer: vptr, rule: "variants are allowed on messaging steps only" });
6547
+ }
6548
+ if (has(w, "post") && params.includes("post") && w["post"] !== "latest") {
6549
+ issue(ctx, { order, code: "R13", path: `${p}.with.post`, pointer: `${wp}/post`, rule: 'only "latest" is supported' });
5775
6550
  }
5776
- if (has(w, "newer_than") && params.includes("newer_than") && !durationOk(ctx, w["newer_than"], DURATION_BOUNDS.newer_than)) {
6551
+ if (has(w, "newer_than") && params.includes("newer_than") && !durationOk(w["newer_than"], DURATION_BOUNDS.newer_than)) {
5777
6552
  issue(ctx, {
5778
6553
  order,
5779
6554
  code: "R12",
@@ -5783,25 +6558,27 @@ function checkDo(ctx, order, p, ptr, raw, usedIds) {
5783
6558
  token: durationHint(DURATION_BOUNDS.newer_than)
5784
6559
  });
5785
6560
  }
5786
- if (has(w, "reaction") && params.includes("reaction") && !v.reactions.includes(w["reaction"])) {
5787
- issue(ctx, { order, code: "R13", path: `${p}.with.reaction`, pointer: `${wp}/reaction`, rule: `must be one of ${v.reactions.join(", ")}` });
6561
+ if (has(w, "reaction") && params.includes("reaction") && !REACTIONS.includes(w["reaction"])) {
6562
+ issue(ctx, { order, code: "R13", path: `${p}.with.reaction`, pointer: `${wp}/reaction`, rule: `must be one of ${REACTIONS.join(", ")}` });
5788
6563
  }
5789
- if (has(w, "withdraw_after") && params.includes("withdraw_after") && !durationOk(ctx, w["withdraw_after"], DURATION_BOUNDS.withdraw_after)) {
6564
+ if (has(w, "withdraw_after") && params.includes("withdraw_after") && !durationOk(w["withdraw_after"], DURATION_BOUNDS.withdraw_after)) {
5790
6565
  issue(ctx, { order, code: "R21", path: `${p}.with.withdraw_after`, pointer: `${wp}/withdraw_after`, rule: durationRule(DURATION_BOUNDS.withdraw_after) });
5791
6566
  }
5792
- if (!messaging) return;
6567
+ if (!messaging)
6568
+ return;
5793
6569
  const textKeys = a === "inmail.send" ? ["subject", "text"] : ["text"];
5794
6570
  const withTextKeys = a === "invite.send" ? ["note"] : textKeys;
5795
6571
  const variants = hasVariants ? raw["variants"] : void 0;
5796
6572
  if (hasVariants && withTextKeys.some((k) => has(w, k) && w[k] !== null)) {
5797
6573
  issue(ctx, { order, code: "R23", path: `${p}.variants`, pointer: vptr, rule: `use either variants or with.${withTextKeys.join(" and with.")}, not both` });
5798
6574
  }
6575
+ const limitOf = (field) => a === "message.send" ? TEXT_LIMITS.message : a === "post.comment" ? TEXT_LIMITS.comment : a === "invite.send" ? TEXT_LIMITS.invite_note : field === "subject" ? TEXT_LIMITS.inmail_subject : TEXT_LIMITS.inmail_text;
5799
6576
  const texts = [];
5800
6577
  if (hasVariants) {
5801
6578
  const n = Array.isArray(variants) ? variants.length : null;
5802
- if (n !== null && n > MAX_VARIANTS && n <= v.maxVariants) {
6579
+ if (n !== null && n > MAX_VARIANTS && n <= MAX_VARIANTS_DEPRECATED) {
5803
6580
  warning(ctx, { order, code: "W13", path: `${p}.variants`, pointer: vptr, rule: `${n} variants; A/B takes exactly ${MAX_VARIANTS}${UNTIL}`, token: `${MAX_VARIANTS} variants` });
5804
- } else if (n === null || n < v.minVariants || n > MAX_VARIANTS) {
6581
+ } else if (n === null || n < MIN_VARIANTS || n > MAX_VARIANTS) {
5805
6582
  issue(ctx, {
5806
6583
  order,
5807
6584
  code: "R23",
@@ -5810,17 +6587,17 @@ function checkDo(ctx, order, p, ptr, raw, usedIds) {
5810
6587
  rule: `needs exactly ${MAX_VARIANTS} variants (A/B)${n === null ? "" : `; found ${n}`}`
5811
6588
  });
5812
6589
  }
5813
- (Array.isArray(variants) ? variants : []).forEach((variant, j) => {
6590
+ (Array.isArray(variants) ? variants : []).forEach((v, j) => {
5814
6591
  let vp = `${p}.variants[${j}]`;
5815
6592
  const vptrJ = `${vptr}/${j}`;
5816
- if (!isObject(variant)) {
6593
+ if (!isObject(v)) {
5817
6594
  issue(ctx, { order, code: "R23", path: vp, pointer: vptrJ, rule: "a variant is a JSON object" });
5818
6595
  return;
5819
6596
  }
5820
- const vid = variant["id"];
5821
- const idAt = { order, code: "R23", path: `${vp}.id`, pointer: has(variant, "id") ? `${vptrJ}/id` : vptrJ };
5822
- if (typeof vid !== "string" || !ctx.re.id.test(vid)) {
5823
- issue(ctx, { ...idAt, rule: `must match ${v.idPattern}` });
6597
+ const vid = v["id"];
6598
+ const idAt = { order, code: "R23", path: `${vp}.id`, pointer: has(v, "id") ? `${vptrJ}/id` : vptrJ };
6599
+ if (typeof vid !== "string" || !ID_RE.test(vid)) {
6600
+ issue(ctx, { ...idAt, rule: `must match ${ID_PATTERN}` });
5824
6601
  } else if (vid === END) {
5825
6602
  issue(ctx, { ...idAt, rule: '"end" is reserved' });
5826
6603
  } else if (usedIds.has(vid)) {
@@ -5829,14 +6606,15 @@ function checkDo(ctx, order, p, ptr, raw, usedIds) {
5829
6606
  usedIds.add(vid);
5830
6607
  vp = `${p}.variants.${vid}`;
5831
6608
  }
5832
- for (const k of keysOf(variant)) {
6609
+ for (const k of keysOf(v)) {
5833
6610
  if (k !== "id" && !textKeys.includes(k)) {
5834
6611
  issue(ctx, { order, code: "R23", path: `${vp}.${quote(k)}`, pointer: `${vptrJ}${jsonPointer(k)}`, rule: `unknown key; a variant holds id and ${textKeys.join(" and ")}` });
5835
6612
  }
5836
6613
  }
5837
6614
  for (const field of textKeys) {
5838
- const value = checkText(ctx, order, `${vp}.${field}`, `${vptrJ}/${field}`, vptrJ, variant[field], limitOf(a, field));
5839
- if (value !== null) texts.push({ path: `${vp}.${field}`, pointer: `${vptrJ}/${field}`, value });
6615
+ const value = checkText(ctx, order, `${vp}.${field}`, `${vptrJ}/${field}`, vptrJ, v[field], limitOf(field));
6616
+ if (value !== null)
6617
+ texts.push({ path: `${vp}.${field}`, pointer: `${vptrJ}/${field}`, field, value });
5840
6618
  }
5841
6619
  });
5842
6620
  } else {
@@ -5844,20 +6622,22 @@ function checkDo(ctx, order, p, ptr, raw, usedIds) {
5844
6622
  const fp = `${wp}/${field}`;
5845
6623
  if (a === "invite.send") {
5846
6624
  if (has(w, "note") && w["note"] !== null) {
5847
- const value = checkText(ctx, order, `${p}.with.note`, fp, withParent, w["note"], limitOf(a, field));
5848
- if (value !== null) texts.push({ path: `${p}.with.note`, pointer: fp, value });
6625
+ const value = checkText(ctx, order, `${p}.with.note`, fp, withParent, w["note"], limitOf(field));
6626
+ if (value !== null)
6627
+ texts.push({ path: `${p}.with.note`, pointer: fp, field, value });
5849
6628
  }
5850
6629
  } else if (!has(w, field)) {
5851
6630
  issue(ctx, { order, code: "R13", path: `${p}.with.${field}`, pointer: withParent, rule: "required, or use variants" });
5852
6631
  } else {
5853
- const value = checkText(ctx, order, `${p}.with.${field}`, fp, withParent, w[field], limitOf(a, field));
5854
- if (value !== null) texts.push({ path: `${p}.with.${field}`, pointer: fp, value });
6632
+ const value = checkText(ctx, order, `${p}.with.${field}`, fp, withParent, w[field], limitOf(field));
6633
+ if (value !== null)
6634
+ texts.push({ path: `${p}.with.${field}`, pointer: fp, field, value });
5855
6635
  }
5856
6636
  }
5857
6637
  }
5858
6638
  if (a === "post.comment") {
5859
6639
  for (const t of texts) {
5860
- const contactVars = variablesIn(t.value).filter((x) => isKnownVariable(ctx, x.inner)).map((x) => longVariable(ctx, x.inner)).filter((l) => l.startsWith("contact."));
6640
+ const contactVars = variablesIn(t.value).filter((v) => isKnownVariable(v.inner)).map((v) => longVariable(v.inner)).filter((l) => l.startsWith("contact."));
5861
6641
  if (contactVars.length === 0) {
5862
6642
  issue(ctx, { order, code: "R20", path: t.path, pointer: t.pointer, rule: "a comment must use a contact variable, such as {{contact.extra.comment}}" });
5863
6643
  } else if (contactVars.every((l) => NAME_FIELDS.includes(l.slice(8)))) {
@@ -5874,8 +6654,11 @@ function checkDo(ctx, order, p, ptr, raw, usedIds) {
5874
6654
  const usesVariable = texts.some((t) => hasVariable(t.value));
5875
6655
  if (a === "invite.send") {
5876
6656
  const fp = `${p}.with.note_fallback`;
5877
- if (has(w, "note_fallback")) checkFallbackText(ctx, order, fp, `${wp}/note_fallback`, withParent, w["note_fallback"], TEXT_LIMITS.invite_note, "drop");
5878
- else if (usesVariable) issue(ctx, { order, code: "R19", path: fp, pointer: withParent, rule: 'required because the note uses a variable: text, or "drop"' });
6657
+ if (has(w, "note_fallback")) {
6658
+ checkFallbackText(ctx, order, fp, `${wp}/note_fallback`, withParent, w["note_fallback"], TEXT_LIMITS.invite_note, fallbackKeyword(a) ?? void 0);
6659
+ } else if (usesVariable) {
6660
+ issue(ctx, { order, code: "R19", path: fp, pointer: withParent, rule: 'required because the note uses a variable: text, or "drop"' });
6661
+ }
5879
6662
  } else if (a === "inmail.send") {
5880
6663
  const fp = `${p}.with.fallback`;
5881
6664
  const fptr = `${wp}/fallback`;
@@ -5898,14 +6681,16 @@ function checkDo(ctx, order, p, ptr, raw, usedIds) {
5898
6681
  } else {
5899
6682
  const fp = `${p}.with.fallback`;
5900
6683
  const comment = a === "post.comment";
5901
- if (has(w, "fallback")) checkFallbackText(ctx, order, fp, `${wp}/fallback`, withParent, w["fallback"], limitOf(a, "text"), comment ? "skip" : void 0);
5902
- else if (comment) issue(ctx, { order, code: "R19", path: fp, pointer: withParent, rule: 'required on a comment: text, or "skip"' });
5903
- else if (usesVariable) issue(ctx, { order, code: "R19", path: fp, pointer: withParent, rule: "required because the text uses a variable" });
6684
+ if (has(w, "fallback"))
6685
+ checkFallbackText(ctx, order, fp, `${wp}/fallback`, withParent, w["fallback"], limitOf("text"), fallbackKeyword(a) ?? void 0);
6686
+ else if (comment)
6687
+ issue(ctx, { order, code: "R19", path: fp, pointer: withParent, rule: 'required on a comment: text, or "skip"' });
6688
+ else if (usesVariable)
6689
+ issue(ctx, { order, code: "R19", path: fp, pointer: withParent, rule: "required because the text uses a variable" });
5904
6690
  }
5905
6691
  }
5906
- function checkPredicate(ctx, pred, path, pointer, order, nested = false) {
5907
- const { v } = ctx;
5908
- const push = (rule) => issue(ctx, { order, code: "R15", path, pointer, rule });
6692
+ function checkPredicate(pred, path, pointer, inWhere, out, order = 0, nested = false, ctx) {
6693
+ const push = (rule, code = "R15") => out.push({ order, code, path, pointer, rule });
5909
6694
  if (!nested && deeperThan(pred, MAX_PREDICATE_DEPTH)) {
5910
6695
  push(`a predicate nests at most ${MAX_PREDICATE_DEPTH} levels`);
5911
6696
  return;
@@ -5915,63 +6700,101 @@ function checkPredicate(ctx, pred, path, pointer, order, nested = false) {
5915
6700
  return;
5916
6701
  }
5917
6702
  const key = keysOf(pred)[0];
5918
- const val = pred[key];
6703
+ const v = pred[key];
5919
6704
  if (key === "not") {
5920
- if (nested) push("not cannot wrap another not");
5921
- else checkPredicate(ctx, val, `${path}.not`, `${pointer}/not`, order, true);
6705
+ if (nested)
6706
+ push("not cannot wrap another not");
6707
+ else
6708
+ checkPredicate(v, `${path}.not`, `${pointer}/not`, inWhere, out, order, true, ctx);
5922
6709
  return;
5923
6710
  }
5924
6711
  if (key.startsWith("account.")) {
5925
- if (key !== "account.premium") push(`unknown predicate ${quote(key)}`);
5926
- else if (typeof val !== "boolean") push("account.premium takes true or false");
6712
+ if (inWhere)
6713
+ push("account.* predicates are not allowed in where");
6714
+ else if (key !== "account.premium")
6715
+ push(`unknown predicate ${quote(key)}`);
6716
+ else if (typeof v !== "boolean")
6717
+ push("account.premium takes true or false");
5927
6718
  return;
5928
6719
  }
5929
6720
  if (key.startsWith("message.")) {
5930
- if (key !== "message.seen") push(`unknown predicate ${quote(key)}`);
5931
- else if (typeof val !== "boolean") push("message.seen takes true or false");
6721
+ if (inWhere)
6722
+ push("message.* predicates are not allowed in where");
6723
+ else if (key !== "message.seen")
6724
+ push(`unknown predicate ${quote(key)}`);
6725
+ else if (typeof v !== "boolean")
6726
+ push("message.seen takes true or false");
5932
6727
  return;
5933
6728
  }
5934
- if (v.booleanPredicates.includes(key)) {
5935
- if (typeof val !== "boolean") push(`${key} takes true or false`);
6729
+ if (BOOLEAN_PREDICATES.includes(key)) {
6730
+ if (typeof v !== "boolean")
6731
+ push(`${key} takes true or false`);
5936
6732
  return;
5937
6733
  }
5938
6734
  switch (key) {
5939
6735
  case "contact.degree": {
5940
- const deg = (n) => v.degrees.includes(n);
5941
- const ok2 = isObject(val) && keysOf(val).length === 1 && (has(val, "eq") && deg(val["eq"]) || has(val, "in") && Array.isArray(val["in"]) && val["in"].length > 0 && val["in"].every(deg));
5942
- if (!ok2) push('contact.degree takes {"eq": n} or {"in": [n, ...]}, n being 1, 2 or 3');
6736
+ const deg = (n) => n === 1 || n === 2 || n === 3;
6737
+ const ok2 = isObject(v) && keysOf(v).length === 1 && (has(v, "eq") && deg(v["eq"]) || has(v, "in") && Array.isArray(v["in"]) && v["in"].length > 0 && v["in"].every(deg));
6738
+ if (!ok2)
6739
+ push('contact.degree takes {"eq": n} or {"in": [n, ...]}, n being 1, 2 or 3');
5943
6740
  return;
5944
6741
  }
5945
6742
  case "contact.connections_gte":
5946
- if (!Number.isInteger(val) || val < v.connectionsMin) push("contact.connections_gte takes an integer of 0 or more");
6743
+ if (!Number.isInteger(v) || v < 0)
6744
+ push("contact.connections_gte takes an integer of 0 or more");
5947
6745
  return;
5948
6746
  case "contact.posted_within":
5949
- if (typeof val !== "string" || !ctx.re.duration.test(val)) push("contact.posted_within takes a duration like 14d");
6747
+ if (typeof v !== "string" || !DURATION_RE.test(v))
6748
+ push("contact.posted_within takes a duration like 14d");
5950
6749
  return;
5951
6750
  case "contact.tag":
5952
- if (!tagOk(ctx, val)) push("contact.tag takes a Tag name of 1 to 64 characters, without , or ;");
6751
+ if (!tagOk(v))
6752
+ push("contact.tag takes a Tag name of 1 to 64 characters, without , or ;");
5953
6753
  return;
5954
6754
  }
5955
- const field = key.startsWith("contact.extra.") ? ctx.re.extraKey.test(key.slice(14)) : key.startsWith("contact.") && v.standardFields.includes(key.slice(8));
6755
+ const enrollmentKey = key.startsWith("enrollment.") ? key.slice("enrollment.".length) : null;
6756
+ if (enrollmentKey !== null && inWhere) {
6757
+ push("enrollment.* predicates are not allowed in where");
6758
+ return;
6759
+ }
6760
+ const field = enrollmentKey !== null ? true : key.startsWith("contact.extra.") ? EXTRA_KEY_RE.test(key.slice(14)) : key.startsWith("contact.") && STANDARD_FIELDS.includes(key.slice(8));
5956
6761
  if (!field) {
5957
6762
  push(`unknown predicate ${quote(key)}`);
5958
6763
  return;
5959
6764
  }
5960
6765
  const scalar = (x) => typeof x === "string" || typeof x === "number" || typeof x === "boolean";
5961
- const op = isObject(val) && keysOf(val).length === 1 ? keysOf(val)[0] : null;
5962
- const operand = op === null ? void 0 : val[op];
5963
- const ok = op !== null && v.fieldOperators.includes(op) && (op === "eq" || op === "neq" ? scalar(operand) : op === "in" ? Array.isArray(operand) && operand.length > 0 && operand.every(scalar) : op === "contains" ? typeof operand === "string" : typeof operand === "boolean");
6766
+ const op = isObject(v) && keysOf(v).length === 1 ? keysOf(v)[0] : null;
6767
+ const operand = op === null ? void 0 : v[op];
6768
+ if (enrollmentKey !== null && ctx)
6769
+ enrollmentRef(ctx, { order, path, pointer }, quote(key), enrollmentKey, (k) => k);
6770
+ if (op !== null && NUMBER_OPERATORS.includes(op)) {
6771
+ if (enrollmentKey === null && !key.startsWith("contact.extra."))
6772
+ push(`${op} compares numbers on enrollment.<key> and contact.extra.<key> only`, "R28");
6773
+ else if (typeof operand !== "number" || !Number.isFinite(operand))
6774
+ push(`${op} takes a number, like {"${op}": 50}`, "R28");
6775
+ else if (enrollmentKey !== null && ctx) {
6776
+ for (const sv of ctx.values.saved.get(enrollmentKey) ?? []) {
6777
+ const type = sampleType(ctx, sv.event, sv.path);
6778
+ if (type === "boolean" || type === "object" || type === "array") {
6779
+ push(`${quote(key)} is saved from ${quote(sv.path)}, a ${type} in the sample of event ${sv.event}; ${op} compares numbers`, "R29");
6780
+ break;
6781
+ }
6782
+ }
6783
+ }
6784
+ return;
6785
+ }
6786
+ const ok = op !== null && FIELD_OPERATORS.includes(op) && (op === "eq" || op === "neq" ? scalar(operand) : op === "in" ? Array.isArray(operand) && operand.length > 0 && operand.every(scalar) : op === "contains" ? typeof operand === "string" : typeof operand === "boolean");
5964
6787
  if (!ok) {
5965
6788
  push(`${quote(key)} takes exactly one of {"eq": v}, {"neq": v}, {"in": [v, ...]}, {"contains": "text"}, {"exists": true or false}`);
5966
6789
  }
5967
6790
  }
5968
6791
  function graph(ctx, infos, ids, onReply) {
5969
6792
  const n = infos.length;
5970
- const resolve = (t) => t === END ? END : ids.get(t);
6793
+ const resolve2 = (t) => t === END ? END : ids.get(t);
5971
6794
  const ft = (i) => i + 1 < n ? i + 1 : "off";
5972
6795
  const edges = infos.map((info, i) => {
5973
6796
  const raw = info.raw;
5974
- const explicit = (f) => ({ field: f, to: resolve(raw[f]), fallThrough: false });
6797
+ const explicit = (f) => ({ field: f, to: resolve2(raw[f]), fallThrough: false });
5975
6798
  const implicit = (f) => ({ field: f, to: ft(i), fallThrough: true });
5976
6799
  const pick = (f) => has(raw, f) ? explicit(f) : implicit(f);
5977
6800
  switch (info.kind) {
@@ -6004,12 +6827,17 @@ function graph(ctx, infos, ids, onReply) {
6004
6827
  const queue = [...roots];
6005
6828
  while (queue.length) {
6006
6829
  const i = queue.shift();
6007
- if (seen.has(i)) continue;
6830
+ if (seen.has(i))
6831
+ continue;
6008
6832
  seen.add(i);
6009
- for (const e of edges[i]) if (typeof e.to === "number") queue.push(e.to);
6833
+ for (const e of edges[i])
6834
+ if (typeof e.to === "number")
6835
+ queue.push(e.to);
6010
6836
  }
6011
6837
  infos.forEach((info, i) => {
6012
- if (!seen.has(i)) graphIssues.push({ order: i, code: "R9", path: info.prefix, pointer: info.ptr, rule: "unreachable: no path leads here from the first step or from on_reply" });
6838
+ if (!seen.has(i)) {
6839
+ graphIssues.push({ order: i, code: "R9", path: info.prefix, pointer: info.ptr, rule: "unreachable: no path leads here from the first step or from on_reply" });
6840
+ }
6013
6841
  });
6014
6842
  const state = new Array(n).fill(0);
6015
6843
  const stack = [];
@@ -6017,10 +6845,13 @@ function graph(ctx, infos, ids, onReply) {
6017
6845
  state[i] = 1;
6018
6846
  stack.push(i);
6019
6847
  for (const e of edges[i]) {
6020
- if (typeof e.to !== "number") continue;
6848
+ if (typeof e.to !== "number")
6849
+ continue;
6021
6850
  if (state[e.to] === 1) {
6022
6851
  const rule = `cycle: ${[...stack.slice(stack.indexOf(e.to)), e.to].map((k) => infos[k].prefix).join(" -> ")}`;
6023
- if (!graphIssues.some((x) => x.rule === rule)) graphIssues.push({ order: i, code: "R8", path: `${infos[i].prefix}.${e.field}`, pointer: exitPtr(i, e), rule });
6852
+ if (!graphIssues.some((x) => x.rule === rule)) {
6853
+ graphIssues.push({ order: i, code: "R8", path: `${infos[i].prefix}.${e.field}`, pointer: exitPtr(i, e), rule });
6854
+ }
6024
6855
  } else if (state[e.to] === 0) {
6025
6856
  dfs(e.to);
6026
6857
  }
@@ -6028,9 +6859,12 @@ function graph(ctx, infos, ids, onReply) {
6028
6859
  stack.pop();
6029
6860
  state[i] = 2;
6030
6861
  };
6031
- for (const r of roots) if (state[r] === 0) dfs(r);
6862
+ for (const r of roots)
6863
+ if (state[r] === 0)
6864
+ dfs(r);
6032
6865
  ctx.issues.push(...graphIssues);
6033
- if (graphIssues.length) return;
6866
+ if (graphIssues.length)
6867
+ return;
6034
6868
  const preds = infos.map(() => []);
6035
6869
  edges.forEach((es, i) => es.forEach((e) => typeof e.to === "number" && preds[e.to].push({ from: i, edge: e })));
6036
6870
  const action = (i) => infos[i].raw["do"];
@@ -6053,20 +6887,32 @@ function graph(ctx, infos, ids, onReply) {
6053
6887
  const withoutInvite = new Array(n).fill(false);
6054
6888
  const withoutMessage = new Array(n).fill(false);
6055
6889
  const fromFirst = new Set(topo(n, edges, [0]));
6056
- for (const r of roots) if (r === 0 || !fromFirst.has(r)) withoutInvite[r] = withoutMessage[r] = true;
6890
+ for (const r of roots)
6891
+ if (r === 0 || !fromFirst.has(r))
6892
+ withoutInvite[r] = withoutMessage[r] = true;
6057
6893
  for (const i of order) {
6058
6894
  for (const { from, edge } of preds[i]) {
6059
6895
  const invited = action(from) === "invite.send" && edge.field === "next";
6060
- if (afterInvite[from] || invited) afterInvite[i] = true;
6061
- if (withoutInvite[from] && !invited) withoutInvite[i] = true;
6896
+ if (afterInvite[from] || invited)
6897
+ afterInvite[i] = true;
6898
+ if (withoutInvite[from] && !invited)
6899
+ withoutInvite[i] = true;
6062
6900
  const messaged = (action(from) === "message.send" || action(from) === "inmail.send") && edge.field === "next";
6063
- if (withoutMessage[from] && !messaged) withoutMessage[i] = true;
6901
+ if (withoutMessage[from] && !messaged)
6902
+ withoutMessage[i] = true;
6064
6903
  }
6065
6904
  }
6066
6905
  infos.forEach((info, i) => {
6067
- if (!seen.has(i)) return;
6906
+ if (!seen.has(i))
6907
+ return;
6068
6908
  if (action(i) === "profile.follow" && afterInvite[i]) {
6069
- warning(ctx, { order: i, code: "W5", path: `${info.prefix}.do`, pointer: `${info.ptr}/do`, rule: "profile.follow after invite.send on the same path; an invitation already follows" });
6909
+ warning(ctx, {
6910
+ order: i,
6911
+ code: "W5",
6912
+ path: `${info.prefix}.do`,
6913
+ pointer: `${info.ptr}/do`,
6914
+ rule: "profile.follow after invite.send on the same path; an invitation already follows"
6915
+ });
6070
6916
  }
6071
6917
  const cond = info.raw["if"];
6072
6918
  const seenPred = isObject(cond) && (has(cond, "message.seen") || isObject(cond["not"]) && has(cond["not"], "message.seen"));
@@ -6085,23 +6931,29 @@ function graph(ctx, infos, ids, onReply) {
6085
6931
  });
6086
6932
  const flagged = /* @__PURE__ */ new Set();
6087
6933
  infos.forEach((info, i) => {
6088
- if (info.raw["until"] !== "invite.accepted") return;
6934
+ if (info.raw["until"] !== "invite.accepted")
6935
+ return;
6089
6936
  const start = edges[i][0].to;
6090
6937
  const visited = /* @__PURE__ */ new Set();
6091
6938
  const q = typeof start === "number" ? [start] : [];
6092
6939
  while (q.length) {
6093
6940
  const k = q.shift();
6094
- if (visited.has(k)) continue;
6941
+ if (visited.has(k))
6942
+ continue;
6095
6943
  visited.add(k);
6096
6944
  if (action(k) === "message.send") {
6097
6945
  if (!flagged.has(k)) {
6098
6946
  flagged.add(k);
6099
6947
  const link = messageTexts(infos[k]).find((t) => t.value.includes("http"));
6100
- if (link) warning(ctx, { order: k, code: "W8", path: link.path, pointer: link.pointer, rule: "a link in the first message after the invitation is accepted" });
6948
+ if (link) {
6949
+ warning(ctx, { order: k, code: "W8", path: link.path, pointer: link.pointer, rule: "a link in the first message after the invitation is accepted" });
6950
+ }
6101
6951
  }
6102
6952
  continue;
6103
6953
  }
6104
- for (const e of edges[k]) if (typeof e.to === "number") q.push(e.to);
6954
+ for (const e of edges[k])
6955
+ if (typeof e.to === "number")
6956
+ q.push(e.to);
6105
6957
  }
6106
6958
  });
6107
6959
  }
@@ -6110,30 +6962,78 @@ function messageTexts(info) {
6110
6962
  const w = isObject(raw["with"]) ? raw["with"] : {};
6111
6963
  const out = [];
6112
6964
  if (Array.isArray(raw["variants"])) {
6113
- raw["variants"].forEach((variant, j) => {
6114
- if (isObject(variant) && typeof variant["text"] === "string") {
6115
- const id = typeof variant["id"] === "string" ? `variants.${variant["id"]}` : `variants[${j}]`;
6116
- out.push({ path: `${info.prefix}.${id}.text`, pointer: `${info.ptr}/variants/${j}/text`, value: variant["text"] });
6965
+ raw["variants"].forEach((v, j) => {
6966
+ if (isObject(v) && typeof v["text"] === "string") {
6967
+ const id = typeof v["id"] === "string" ? `variants.${v["id"]}` : `variants[${j}]`;
6968
+ out.push({ path: `${info.prefix}.${id}.text`, pointer: `${info.ptr}/variants/${j}/text`, value: v["text"] });
6117
6969
  }
6118
6970
  });
6119
6971
  }
6120
- if (typeof w["text"] === "string") out.push({ path: `${info.prefix}.with.text`, pointer: `${info.ptr}/with/text`, value: w["text"] });
6121
- if (typeof w["fallback"] === "string") out.push({ path: `${info.prefix}.with.fallback`, pointer: `${info.ptr}/with/fallback`, value: w["fallback"] });
6972
+ if (typeof w["text"] === "string")
6973
+ out.push({ path: `${info.prefix}.with.text`, pointer: `${info.ptr}/with/text`, value: w["text"] });
6974
+ if (typeof w["fallback"] === "string")
6975
+ out.push({ path: `${info.prefix}.with.fallback`, pointer: `${info.ptr}/with/fallback`, value: w["fallback"] });
6122
6976
  return out;
6123
6977
  }
6124
6978
  function topo(n, edges, roots) {
6125
6979
  const out = [];
6126
6980
  const done = new Array(n).fill(false);
6127
6981
  const visit = (i) => {
6128
- if (done[i]) return;
6982
+ if (done[i])
6983
+ return;
6129
6984
  done[i] = true;
6130
- for (const e of edges[i]) if (typeof e.to === "number") visit(e.to);
6985
+ for (const e of edges[i])
6986
+ if (typeof e.to === "number")
6987
+ visit(e.to);
6131
6988
  out.push(i);
6132
6989
  };
6133
6990
  roots.forEach(visit);
6134
6991
  return out.reverse();
6135
6992
  }
6136
6993
 
6994
+ // src/sequence/validate.ts
6995
+ function validateSequence(value) {
6996
+ const out = verdict(value);
6997
+ return { errors: out.errors.slice(0, MAX_ERRORS_LISTED), warnings: out.warnings };
6998
+ }
6999
+ function verdict(value) {
7000
+ const bad = firstUnstorable(value);
7001
+ if (bad !== null) return { errors: [{ path: bad, rule: UNSTORABLE_RULE }], warnings: [] };
7002
+ const size = compactBytes2(value);
7003
+ if (size > MAX_SEQUENCE_BYTES) return { errors: [sequenceSizeError(size)], warnings: [] };
7004
+ return lintSequence(value);
7005
+ }
7006
+ function compactBytes2(v) {
7007
+ try {
7008
+ return new TextEncoder().encode(JSON.stringify(v)).length;
7009
+ } catch {
7010
+ return 0;
7011
+ }
7012
+ }
7013
+ var UNSTORABLE_RULE = "must be well-formed Unicode and must not contain a NUL (\\u0000) character";
7014
+ var MAX_ERRORS_LISTED = 100;
7015
+ var UNSTORABLE_RE = /\u0000|[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?<![\uD800-\uDBFF])[\uDC00-\uDFFF]/;
7016
+ function firstUnstorable(doc) {
7017
+ const stack = [["value", doc, ""]];
7018
+ while (stack.length) {
7019
+ const [kind, value, path] = stack.pop();
7020
+ if (kind === "key") {
7021
+ if (UNSTORABLE_RE.test(value)) return path ? `${path} key` : "$ key";
7022
+ } else if (typeof value === "string") {
7023
+ if (UNSTORABLE_RE.test(value)) return path || "$";
7024
+ } else if (Array.isArray(value)) {
7025
+ for (let i = value.length - 1; i >= 0; i--) stack.push(["value", value[i], `${path}[${i}]`]);
7026
+ } else if (typeof value === "object" && value !== null) {
7027
+ const entries2 = Object.entries(value);
7028
+ for (let i = entries2.length - 1; i >= 0; i--) {
7029
+ const [k, v] = entries2[i];
7030
+ stack.push(["value", v, path ? `${path}.${k}` : k], ["key", k, path]);
7031
+ }
7032
+ }
7033
+ }
7034
+ return null;
7035
+ }
7036
+
6137
7037
  // src/sequence/builder.ts
6138
7038
  var refuse = (errors) => errors[0].rule === UNSTORABLE_RULE ? new CurviateError({
6139
7039
  code: "INVALID_REQUEST",
@@ -6296,6 +7196,26 @@ var MotionsResource = class {
6296
7196
  *
6297
7197
  * Needs Automate: a Team below that Tier is refused with `TIER_REQUIRED`.
6298
7198
  *
7199
+ * Days and hours in `schedule` are read in each sending account's own time
7200
+ * zone. `schedule.timezone` and `settings.stop_on_engagement` are retired:
7201
+ * accepted with warning W13 until 2027-01-25, never stored or returned
7202
+ * (`stop_on_engagement: true` with an acting reply route is a 400).
7203
+ * `settings.acceptance_floor` defaults to `null` (off). With
7204
+ * `settings.accounts_per_contact` above 1 (Several mode) the settings gain
7205
+ * `on_reply_elsewhere` (`stop` or `continue`), `withdraw_on_reply_elsewhere`
7206
+ * and `account_gap` (`1d` to `30d`), served only in that mode;
7207
+ * `accounts_per_contact` is locked once the Motion has started.
7208
+ *
7209
+ * `settings.priority` (`normal`, the default, or `prioritized`) is the
7210
+ * Motion's share of each sending account's daily allowance: a prioritized
7211
+ * Motion gets twice a normal one's share, and a share nobody uses flows to
7212
+ * the others. Curviate paces all Motions on an account against one budget,
7213
+ * so there is no per-Motion start cap. `settings.new_enrollments_per_account_per_day`
7214
+ * and `settings.assign: "even"` are retired: accepted and ignored until
7215
+ * 2027-01-28 with a `SETTING_REMOVED` entry in `notices[]` and
7216
+ * `Deprecation: true`, never returned, then a `400`. A pending Enrollment
7217
+ * waits unassigned (`account_id: null`) until an account with room takes it.
7218
+ *
6299
7219
  * @example
6300
7220
  * const motion = await curviate.motions.create({
6301
7221
  * name: "Q4 webinar follow-up",
@@ -6308,14 +7228,17 @@ var MotionsResource = class {
6308
7228
  return this.ctx.request({ method: "POST", path: "/v1/motions", body: withDocument(body) });
6309
7229
  }
6310
7230
  /**
6311
- * List Motions, most recently updated first. `status` (any of them; default
6312
- * all but `archived`), `account_id`, `q` (name substring). Items carry no
7231
+ * List Motions. `status` (any of them; default all but `archived`),
7232
+ * `account_id` (one or several accounts as a comma list, up to 50: Motions
7233
+ * sending from any of them), `q` (name substring), `sort`: `updated`
7234
+ * (default, latest edit first) or `activity` (latest step record first,
7235
+ * else `updated_at`). Items carry `last_activity_at` (or `null`) but no
6313
7236
  * `sequence`: read it with `get`. Pages with an opaque `cursor` until it is
6314
- * `null`.
7237
+ * `null`; a cursor belongs to its sort.
6315
7238
  * `GET /v1/motions`
6316
7239
  *
6317
7240
  * @example
6318
- * const { items } = await curviate.motions.list({ status: ["active", "paused"] });
7241
+ * const { items } = await curviate.motions.list({ status: ["active", "paused"], account_id: "acc_1,acc_2", sort: "activity" });
6319
7242
  */
6320
7243
  list(params) {
6321
7244
  return this.ctx.request({ method: "GET", path: "/v1/motions", query: commaLists(params, "status") });
@@ -6417,6 +7340,17 @@ var MotionsResource = class {
6417
7340
  * Sequence, rendered messages for `sample` Contacts (1 to 20, default 5) or
6418
7341
  * the `contact_ids` you name (up to 20, not with `sample`), and the budget
6419
7342
  * projection. A read: never reaches LinkedIn, spends nothing.
7343
+ *
7344
+ * `projection.audience` is `{ total, skipped, to_enroll }`, `skipped`
7345
+ * counted per reason (`do_not_contact`, `excluded`, `other_motion`,
7346
+ * `known_chat`, `filtered`; a Contact counts under the first that applies);
7347
+ * `projection.contacts` equals `to_enroll`. `projection.accounts[]` names the
7348
+ * pool that binds each account's pace (`binding`), its
7349
+ * `expected_actions_per_day` (an upper estimate) and `projection.warnings[]`
7350
+ * (`hours_below_share`, `no_sending_account`: no account of the Motion can
7351
+ * send its InMail step). There is no `daily_cap`. In
7352
+ * Several mode `accounts[].contacts` counts Enrollments, not people, and
7353
+ * `projection.fits_before_end_at` is counted per account zone.
6420
7354
  * `GET /v1/motions/{id}/preview`
6421
7355
  *
6422
7356
  * @example
@@ -6482,15 +7416,20 @@ var EnrollmentsResource = class {
6482
7416
  }
6483
7417
  /**
6484
7418
  * List Enrollments, newest first. Filters: `motion_id`, `contact_id`,
6485
- * `account_id`, `status` (any of them), `step_id` (at that Step now),
6486
- * `version`; by Step outcome: `done_step` with `outcome` (how that Step
7419
+ * `account_id` (one or several as a comma list, up to 50), `status` (any of
7420
+ * them), `step_id` (at any of these Steps now, up to 10), `version`; by Step outcome: `done_step` with `outcome` (how that Step
6487
7421
  * ended), `reason` (catalogue codes, any of them), `variant_id`,
6488
7422
  * `relationship_at_enrollment` (any of them). Pages with an opaque `cursor`
6489
7423
  * until it is `null`.
6490
7424
  * `GET /v1/enrollments`
6491
7425
  *
7426
+ * A finished Enrollment's `finish_reason` may be `engaged_elsewhere`: the
7427
+ * Contact replied to, or was messaged from, another account of the Motion
7428
+ * (Several mode).
7429
+ *
6492
7430
  * @example
6493
7431
  * const { items } = await curviate.enrollments.list({ motion_id: "mot_123", done_step: "opener", outcome: "failed" });
7432
+ * const atEither = await curviate.enrollments.list({ motion_id: "mot_123", step_id: ["opener", "bump"] });
6494
7433
  */
6495
7434
  list(params) {
6496
7435
  return this.ctx.request({
@@ -6501,7 +7440,12 @@ var EnrollmentsResource = class {
6501
7440
  }
6502
7441
  /**
6503
7442
  * One Enrollment with its `history` of Steps (metadata only, no rendered
6504
- * text).
7443
+ * text). `history` reaches back 12 months, plus the entries `retry` reads
7444
+ * at any age; `history_truncated: true` says older entries are not listed.
7445
+ * The Enrollment's state has no such limit. `values` are what the
7446
+ * Sequence's `save` maps took from an incoming event (name to value; read
7447
+ * them as `{{enrollment.<name>}}` and `if enrollment.<name>`); they are
7448
+ * empty once it finishes, and after a resume.
6505
7449
  * `GET /v1/enrollments/{id}`
6506
7450
  *
6507
7451
  * @example
@@ -6511,8 +7455,8 @@ var EnrollmentsResource = class {
6511
7455
  return this.ctx.request({ method: "GET", path: apiPath`/v1/enrollments/${enrollmentId}` });
6512
7456
  }
6513
7457
  /**
6514
- * Stop a `pending`, `active` or `paused` Enrollment (`finished`,
6515
- * `stopped`); else `ENROLLMENT_NOT_ACTIVE`.
7458
+ * Stop a `queued`, `active` or `paused` Enrollment (it ends `finished`,
7459
+ * `finish_reason: "stopped"`); else `ENROLLMENT_NOT_ACTIVE`.
6516
7460
  * `POST /v1/enrollments/{id}/stop`
6517
7461
  *
6518
7462
  * @example
@@ -6522,7 +7466,7 @@ var EnrollmentsResource = class {
6522
7466
  return this.ctx.request({ method: "POST", path: apiPath`/v1/enrollments/${enrollmentId}/stop` });
6523
7467
  }
6524
7468
  /**
6525
- * Hold one `pending`, `active` or `paused` Enrollment until you `resume` it
7469
+ * Hold one `queued`, `active` or `paused` Enrollment until you `resume` it
6526
7470
  * (`waiting.kind: "paused_by_user"`); its waits freeze. A Step being sent
6527
7471
  * right now is `STEP_IN_FLIGHT`: retry in a minute. Works on every plan.
6528
7472
  * `POST /v1/enrollments/{id}/pause`
@@ -6687,6 +7631,15 @@ var NetworkConnectionsResource = class {
6687
7631
  * characters), `include_removed` (Connections that left), `limit` (up to
6688
7632
  * 1,000). `await` it for one page; `for await` it for every Connection
6689
7633
  * across pages.
7634
+ *
7635
+ * By default an item is one account and one person (`object: "connection"`),
7636
+ * so someone connected to three of your accounts appears three times.
7637
+ * `group: "person"` lists one item per person (`object: "connection_person"`)
7638
+ * with `accounts`, each of your accounts they are connected to, and `contact`,
7639
+ * their Contact summary (`id`, `list_count`, up to 10 `lists`; `null` when
7640
+ * they are not a Contact); `account_id` then picks which people are
7641
+ * listed. `include_total: true` (only with `group: "person"`) adds `total`,
7642
+ * the number of people that match across all pages. Branch on `object`.
6690
7643
  * `GET /v1/network/connections`
6691
7644
  *
6692
7645
  * @example
@@ -6720,16 +7673,24 @@ var NetworkResource = class {
6720
7673
  /**
6721
7674
  * Start a full re-read of one account's Connections, paced and inside the
6722
7675
  * account's activity window (hours to days for a large network). Once per
6723
- * account per 24 hours; follow it with `syncStatus`.
7676
+ * account per 24 hours; follow it with `syncStatus`. With an `account_id` it
7677
+ * resolves with that account's answer (`object: "network_resync"`). With no
7678
+ * argument (or no `account_id`) it is Resync all: every account you can
7679
+ * operate that can start one now, one after another in a random order, each
7680
+ * 20 to 90 seconds after the previous; it resolves with
7681
+ * `object: "network_resync_list"`, one item per account with its `status`
7682
+ * (`queued`, `cooldown`, `in_progress` or `paused`: answers, not errors) and
7683
+ * `starts_at`. Branch on `object`.
6724
7684
  * `POST /v1/network/resync`
6725
7685
  *
6726
7686
  * Needs Automate: a Team below that Tier is refused with `TIER_REQUIRED`.
6727
7687
  *
6728
7688
  * @example
6729
7689
  * await curviate.network.resync({ account_id: "acc_123" });
7690
+ * const all = await curviate.network.resync(); // Resync all
6730
7691
  */
6731
7692
  resync(body) {
6732
- return this.ctx.request({ method: "POST", path: "/v1/network/resync", body });
7693
+ return this.ctx.request({ method: "POST", path: "/v1/network/resync", body: body ?? {} });
6733
7694
  }
6734
7695
  };
6735
7696
 
@@ -6740,9 +7701,20 @@ var UniboxAccountsResource = class {
6740
7701
  }
6741
7702
  /**
6742
7703
  * For every account you can operate: whether its whole inbox is copied into
6743
- * the Unibox, and how far the chat-list sync is.
7704
+ * the Unibox, and how far the chat-list sync is. A disconnected account is
7705
+ * listed too (`account_status: "disconnected"`), read-only.
6744
7706
  * `GET /v1/unibox/accounts`
6745
7707
  *
7708
+ * Read `sync` before trusting an empty Unibox: `off` and `pending` mean
7709
+ * nothing was listed yet. `sync.next_run_at` is when the next read of the
7710
+ * account happens: once the first listing is `complete`, the next poll
7711
+ * (every 5 to 10 minutes while the Unibox is open in the dashboard,
7712
+ * otherwise 30 to 60, slower at weekends, inside the account's active hours
7713
+ * only); while `paused`, when the pause ends; `null` when none can be
7714
+ * scheduled or full-inbox sync is off. An account without full-inbox sync
7715
+ * that still has Motion chats is polled for them, yet reads `off` with
7716
+ * `next_run_at: null`.
7717
+ *
6746
7718
  * @example
6747
7719
  * const { items } = await curviate.unibox.accounts.list();
6748
7720
  */
@@ -6774,12 +7746,26 @@ var UniboxResource = class {
6774
7746
  /**
6775
7747
  * List the chats of every account you can operate in one inbox, newest
6776
7748
  * activity first. Filters: `account_ids`, `motion_ids`, `tags` (several
6777
- * each), `unread`, `last_message_from`, `type`, `q` (3+ characters).
6778
- * `await` it for one page; `for await` it for every chat across pages.
7749
+ * each), `person`, `unread`, `last_message_from`, `type`, `q` (3+
7750
+ * characters). `await` it for one page; `for await` it for every chat
7751
+ * across pages.
6779
7752
  * `GET /v1/unibox/chats`
6780
7753
  *
7754
+ * `person` is one Contact id (`con_...`) or LinkedIn id (`ACo...`), not a
7755
+ * list; an unknown id matches nothing. `motion_ids` also takes `"none"`:
7756
+ * the chats no Motion links, alone or beside Motion ids. `include_total:
7757
+ * true` adds `total` to the page, the chats that match the filters (not
7758
+ * the page); without it the body has no `total`.
7759
+ *
7760
+ * A chat of a disconnected account is listed (`account_status:
7761
+ * "disconnected"`), read-only. `unread` is LinkedIn's state for the
7762
+ * account, the same for every viewer. `marked_unread` is `{origin, at}`
7763
+ * (`origin`: `unibox`, `api` or `linkedin`) when the chat was made unread
7764
+ * without a newer message from the other person, else `null`.
7765
+ *
6781
7766
  * @example
6782
7767
  * for await (const chat of curviate.unibox.list({ unread: true })) console.log(chat.chat_id);
7768
+ * const { total } = await curviate.unibox.list({ person: "con_123", motion_ids: ["none"], include_total: true });
6783
7769
  */
6784
7770
  list(params) {
6785
7771
  return pageable(
@@ -6811,9 +7797,18 @@ var UniboxResource = class {
6811
7797
  /**
6812
7798
  * Queue a re-read of the newest chats (up to 100) of each account with
6813
7799
  * full-inbox sync on, or of `account_ids`. Runs within minutes. Once per
6814
- * account per 5 minutes; a repeat answers `cooldown` with `next_allowed_at`.
7800
+ * account per 2 minutes; a repeat answers `cooldown` with `next_allowed_at`.
6815
7801
  * `POST /v1/unibox/refresh`
6816
7802
  *
7803
+ * It covers every account the poll covers: full-inbox sync on, or Motion
7804
+ * chats or Motion outreach without it (only those chats are kept). An
7805
+ * account's `status` is `queued`, `cooldown`, `paused` (it runs when the
7806
+ * pause ends) or `not_synced`, which means none of those apply (or the
7807
+ * first full listing is still running), so there is nothing to refresh.
7808
+ * `next_allowed_at` is when the next manual refresh is allowed (while
7809
+ * `paused`, when the pause ends, `null` if unknown). A disconnected account
7810
+ * named here is ignored and claims no cooldown.
7811
+ *
6817
7812
  * Needs Automate: a Team below that Tier is refused with `TIER_REQUIRED`.
6818
7813
  *
6819
7814
  * @example
@@ -6858,27 +7853,57 @@ var AnalyticsMotionsResource = class {
6858
7853
  this.ctx = ctx;
6859
7854
  }
6860
7855
  /**
6861
- * Results of your Motions in total, per Motion and per account:
6862
- * Enrollments by state, invitations, messages, replies, engagement and a
6863
- * daily series.
7856
+ * Results of your Motions and direct actions (API, CLI, MCP, Unibox) in
7857
+ * total, per Motion and per account: Enrollments by state right now (the
7858
+ * window does not apply), invitations and acceptance, messages and reply,
7859
+ * InMails and InMail reply (never mixed with messages), replies, engagement,
7860
+ * a daily series. Each Contact counts once per Account and source. Rates are
7861
+ * outcomes so far (`maturing` is the part sent under 7 days ago;
7862
+ * `low_sample` under 30); `compare: "previous_period"` adds `previous` and
7863
+ * `delta` to every rate. `origin` filters by source: `motion`, `unibox`,
7864
+ * `api`, or `direct` (unibox and api).
7865
+ *
7866
+ * Grammar: `include` picks sections (`performance`, `daily`, `motions`,
7867
+ * `accounts`, `flags`), `fields` keeps dotted paths (`motions.reply.rate`),
7868
+ * `sort` and `limit` order and cut the Motion and account lists (`-key`
7869
+ * is descending), and `flags` says what to read next. Give any of these
7870
+ * without `include` and the body is the summary: `performance`, the top 5
7871
+ * by sent, `flags`. Give none and you still get the full body, with a
7872
+ * `Deprecation` header: it becomes the summary later.
7873
+ *
7874
+ * Window: `from` and `to` reach back up to 3650 days (more is a `400` naming
7875
+ * `from`). Beyond 12 months you get daily counts only:
7876
+ * `window.counts_only_through` is the last day read from the daily counts
7877
+ * (`null` when the whole window has full detail), and
7878
+ * `window.paths_through` says `paths` covers only the days after it, so past
7879
+ * it `paths` does not add up to the totals.
6864
7880
  * `GET /v1/analytics/motions`
6865
7881
  *
6866
7882
  * @example
6867
7883
  * const { totals, motions } = await curviate.analytics.motions.list({ from: "2026-10-01", to: "2026-10-31" });
7884
+ *
7885
+ * @example
7886
+ * const summary = await curviate.analytics.motions.list({ range: "30d", compare: "previous_period", origin: ["motion", "unibox"] });
6868
7887
  */
6869
7888
  list(params) {
6870
- return this.ctx.request({ method: "GET", path: "/v1/analytics/motions", query: params });
7889
+ return this.ctx.request({ method: "GET", path: "/v1/analytics/motions", query: commaLists(params, "account_id", "motion_id", "origin", "include", "fields", "sort") });
6871
7890
  }
6872
7891
  /**
6873
7892
  * One Motion's results with a row per step and per variant (document
6874
- * order, never ranked); `version` picks a Sequence version.
7893
+ * order, never ranked); `version` picks a Sequence version. The window,
7894
+ * `origin`, `include`, `fields`, `sort`, `limit` and `compare` work as on
7895
+ * {@link AnalyticsMotionsResource.list}; here the summary is `performance`,
7896
+ * `accounts` and `flags`, and `steps`, `variants` and `paths` come with
7897
+ * `include`. `paths` splits the Motion by the Contact's relationship to the
7898
+ * account when the Enrollment started (provisional); it covers only the
7899
+ * days after `window.paths_through`.
6875
7900
  * `GET /v1/analytics/motions/{motion_id}`
6876
7901
  *
6877
7902
  * @example
6878
7903
  * const { steps } = await curviate.analytics.motions.get("mot_123");
6879
7904
  */
6880
7905
  get(motionId, params) {
6881
- return this.ctx.request({ method: "GET", path: apiPath`/v1/analytics/motions/${motionId}`, query: params });
7906
+ return this.ctx.request({ method: "GET", path: apiPath`/v1/analytics/motions/${motionId}`, query: commaLists(params, "account_id", "origin", "include", "fields", "sort") });
6882
7907
  }
6883
7908
  };
6884
7909
  var AnalyticsEventsResource = class {
@@ -6887,10 +7912,16 @@ var AnalyticsEventsResource = class {
6887
7912
  }
6888
7913
  /**
6889
7914
  * Recent Motion events, newest first: steps executed, skipped or failed,
6890
- * invitations accepted and replies. Filters: `account_id`, `motion_id`,
6891
- * `kind` (several). Keyset paging: events recorded after your first page
7915
+ * invitations accepted and replies, plus the direct actions (Unibox, API).
7916
+ * Filters: `account_id`, `motion_id`, `contact_id`, `kind` and `origin`
7917
+ * (several). `origin` is `motion`, `unibox`, `api` or `direct` (an alias
7918
+ * for `unibox` and `api`). `contact_id` (`con_...`) keeps one person's
7919
+ * events, their Enrollments' and the direct actions with them; an unknown
7920
+ * id, another team's, or one merged into another Contact throws
7921
+ * `RESOURCE_NOT_FOUND` (404). Keyset paging: events recorded after your first page
6892
7922
  * never shift later pages. `await` it for one page; `for await` it for
6893
- * every event across pages.
7923
+ * every event across pages. A direct action's `person_name` names the
7924
+ * person (they may not be a Contact); `null` for a Motion event.
6894
7925
  * `GET /v1/analytics/events`
6895
7926
  *
6896
7927
  * @example
@@ -6898,7 +7929,7 @@ var AnalyticsEventsResource = class {
6898
7929
  */
6899
7930
  list(params) {
6900
7931
  return pageable(
6901
- (p) => this.ctx.request({ method: "GET", path: "/v1/analytics/events", query: commaLists(p, "kind") }),
7932
+ (p) => this.ctx.request({ method: "GET", path: "/v1/analytics/events", query: commaLists(p, "kind", "origin") }),
6902
7933
  params
6903
7934
  );
6904
7935
  }
@@ -6919,7 +7950,346 @@ var AnalyticsResource = class {
6919
7950
  * const { types } = await curviate.analytics.activity({ account_id: "acc_123" });
6920
7951
  */
6921
7952
  activity(params) {
6922
- return this.ctx.request({ method: "GET", path: "/v1/analytics/activity", query: params });
7953
+ return this.ctx.request({ method: "GET", path: "/v1/analytics/activity", query: commaLists(params, "account_id") });
7954
+ }
7955
+ /**
7956
+ * The Analytics tables as one download, now, any size and no row limit,
7957
+ * never message text: `table` picks `daily`, `accounts`, `motions` and
7958
+ * `enrollments` (default all; several come as one ZIP), `format` is `csv`
7959
+ * (default; UTF-8 with a BOM, formula-guarded), `json` or `ndjson` (one
7960
+ * table only), under the overview's filters (window, `account_id`,
7961
+ * `motion_id`, `origin`). Resolves with the file's bytes. Owners and
7962
+ * Managers. For a file to fetch later, `exports.create`.
7963
+ * `GET /v1/analytics/export`
7964
+ *
7965
+ * @example
7966
+ * const csv = await curviate.analytics.export({ table: "daily", range: "90d" });
7967
+ */
7968
+ async export(params) {
7969
+ return this.ctx.request({ bytes: true, method: "GET", path: "/v1/analytics/export", query: commaLists(params, "account_id", "motion_id", "origin", "table") });
7970
+ }
7971
+ };
7972
+
7973
+ // src/resources/billing.ts
7974
+ var BillingSeatsResource = class {
7975
+ constructor(ctx) {
7976
+ this.ctx = ctx;
7977
+ }
7978
+ /**
7979
+ * Move an unused paid seat of this Team to another of your Teams, at no
7980
+ * charge; it stays on this Team's invoice at its Tier. `seat_id` picks the
7981
+ * seat, or leave it out to lend the oldest unused one; each call lends one
7982
+ * more seat. Trial, occupied, unconfirmed, cancelling and already-lent seats
7983
+ * cannot move (`SEAT_NOT_FOUND`). The borrowing Team connects an Account
7984
+ * into it and the seat counts against that Team's rate limit, not this
7985
+ * one's. While it is lent, a connect naming it or a cancel of it throws
7986
+ * `SEAT_LENT`: move it back first. Only the Owner who pays for both Teams
7987
+ * can lend (`INSUFFICIENT_ROLE`). Resolves with the lent `seat_id` and the
7988
+ * `team_id` it went to.
7989
+ * `POST /v1/billing/seats/lend`
7990
+ *
7991
+ * @example
7992
+ * const { seat_id } = await curviate.billing.seats.lend({ team_id: "tnt_456" });
7993
+ */
7994
+ lend(body) {
7995
+ return this.ctx.request({ method: "POST", path: "/v1/billing/seats/lend", body });
7996
+ }
7997
+ /**
7998
+ * End a seat loan while no Account uses the seat. Call it from either Team:
7999
+ * with the lending Team's seat id, or with the lent seat's id in the Team
8000
+ * that received it. A seat an Account still uses throws `SEAT_NOT_EMPTY`:
8001
+ * disconnect the Account first. The Owner who pays for either Team can move
8002
+ * it back.
8003
+ * `POST /v1/billing/seats/{seat_id}/return`
8004
+ *
8005
+ * @example
8006
+ * await curviate.billing.seats.return("seat_123");
8007
+ */
8008
+ return(seatId) {
8009
+ return this.ctx.request({ method: "POST", path: apiPath`/v1/billing/seats/${seatId}/return` });
8010
+ }
8011
+ };
8012
+ var BillingInvoicesResource = class {
8013
+ constructor(ctx) {
8014
+ this.ctx = ctx;
8015
+ }
8016
+ /**
8017
+ * The Team's issued invoices, newest first, with number, `total`,
8018
+ * `currency`, `status` (`paid`, `open`, `failed` or `void`) and service
8019
+ * period. Draft invoices are never listed and an invoice carries no link:
8020
+ * download its PDF by `number`. `limit` is 1 to 100 (default 20). `await` it
8021
+ * for one page; `for await` it for every invoice across pages. Owners only
8022
+ * (`INSUFFICIENT_ROLE`); `BILLING_PORTAL_UNAVAILABLE` (503) means retry.
8023
+ * `GET /v1/billing/invoices`
8024
+ *
8025
+ * @example
8026
+ * for await (const invoice of curviate.billing.invoices.list()) console.log(invoice.number, invoice.status);
8027
+ */
8028
+ list(params) {
8029
+ return pageable(
8030
+ (p) => this.ctx.request({ method: "GET", path: "/v1/billing/invoices", ...p ? { query: p } : {} }),
8031
+ params
8032
+ );
8033
+ }
8034
+ /**
8035
+ * One invoice as PDF bytes, by its `number` from {@link BillingInvoicesResource.list}.
8036
+ * An unknown number throws `RESOURCE_NOT_FOUND`. Owners only.
8037
+ * `GET /v1/billing/invoices/{number}/pdf`
8038
+ *
8039
+ * @example
8040
+ * const pdf = await curviate.billing.invoices.pdf("INV-0042");
8041
+ * await fs.promises.writeFile("INV-0042.pdf", Buffer.from(pdf));
8042
+ */
8043
+ pdf(number) {
8044
+ return this.ctx.request({ method: "GET", path: apiPath`/v1/billing/invoices/${number}/pdf` });
8045
+ }
8046
+ };
8047
+ var BillingResource = class {
8048
+ constructor(ctx) {
8049
+ this.seats = new BillingSeatsResource(ctx);
8050
+ this.invoices = new BillingInvoicesResource(ctx);
8051
+ }
8052
+ };
8053
+
8054
+ // src/resources/do-not-contact.ts
8055
+ var DoNotContactResource = class {
8056
+ constructor(ctx) {
8057
+ this.ctx = ctx;
8058
+ }
8059
+ /**
8060
+ * The Team's do-not-contact list, newest first. `q` matches the Contact's
8061
+ * name. `await` it for one page; `for await` it for every entry across
8062
+ * pages.
8063
+ * `GET /v1/do-not-contact`
8064
+ *
8065
+ * @example
8066
+ * for await (const entry of curviate.doNotContact.list({ q: "ana" })) console.log(entry.id, entry.reason);
8067
+ */
8068
+ list(params) {
8069
+ return pageable(
8070
+ (p) => this.ctx.request({ method: "GET", path: "/v1/do-not-contact", ...p ? { query: p } : {} }),
8071
+ params
8072
+ );
8073
+ }
8074
+ /**
8075
+ * One entry. `reason` is one of `asked_to_stop`, `existing_customer`,
8076
+ * `competitor`, `partner`, `employee`, `other`, or `null`.
8077
+ * `GET /v1/do-not-contact/{id}`
8078
+ *
8079
+ * @example
8080
+ * const entry = await curviate.doNotContact.get("dnc_123");
8081
+ */
8082
+ get(id) {
8083
+ return this.ctx.request({ method: "GET", path: apiPath`/v1/do-not-contact/${id}` });
8084
+ }
8085
+ /**
8086
+ * Add one person by `contact_id` or `linkedin_url` (someone who is not a
8087
+ * Contact yet becomes one first, from stored data only), with an optional
8088
+ * `reason`. A person already on the list resolves with their existing entry,
8089
+ * unchanged. Any team member may add. There is no bulk route: loop. Needs
8090
+ * Automate: a Team below that Tier is refused with `TIER_REQUIRED`.
8091
+ * `POST /v1/do-not-contact`
8092
+ *
8093
+ * @example
8094
+ * const entry = await curviate.doNotContact.add({ linkedin_url: "https://www.linkedin.com/in/ana/", reason: "asked_to_stop" });
8095
+ */
8096
+ add(body) {
8097
+ return this.ctx.request({ method: "POST", path: "/v1/do-not-contact", body });
8098
+ }
8099
+ /**
8100
+ * Set the entry's `reason` from the fixed list; `null` clears it. Needs
8101
+ * Automate: a Team below that Tier is refused with `TIER_REQUIRED`.
8102
+ * `PATCH /v1/do-not-contact/{id}`
8103
+ *
8104
+ * @example
8105
+ * await curviate.doNotContact.update("dnc_123", { reason: "existing_customer" });
8106
+ */
8107
+ update(id, body) {
8108
+ return this.ctx.request({ method: "PATCH", path: apiPath`/v1/do-not-contact/${id}`, body });
8109
+ }
8110
+ /**
8111
+ * Remove the entry (bodyless, 204): Motions and writes may reach the person
8112
+ * again. Owners and Managers only (`INSUFFICIENT_ROLE`).
8113
+ * `DELETE /v1/do-not-contact/{id}`
8114
+ *
8115
+ * @example
8116
+ * await curviate.doNotContact.remove("dnc_123");
8117
+ */
8118
+ async remove(id) {
8119
+ await this.ctx.request({ method: "DELETE", path: apiPath`/v1/do-not-contact/${id}` });
8120
+ }
8121
+ };
8122
+
8123
+ // src/resources/incoming-events.ts
8124
+ var IncomingEventsResource = class {
8125
+ constructor(ctx) {
8126
+ this.ctx = ctx;
8127
+ }
8128
+ /**
8129
+ * Create an incoming event, switched off, waiting for its sample. Exactly
8130
+ * one of `event_name` (fixed) and `event_name_field` (a path in the post).
8131
+ * The result's `url` is the only time it is shown in full.
8132
+ * `POST /v1/incoming-events`
8133
+ *
8134
+ * @example
8135
+ * const { id, url } = await curviate.incomingEvents.create({ event_name: "reply_received", match: { email: "lead.email" } });
8136
+ */
8137
+ create(body) {
8138
+ return this.ctx.request({ method: "POST", path: "/v1/incoming-events", body });
8139
+ }
8140
+ /**
8141
+ * The Team's incoming events (`url` masked); `event` keeps one event name.
8142
+ * `await` it for one page; `for await` it for every one across pages.
8143
+ * `GET /v1/incoming-events`
8144
+ */
8145
+ list(params) {
8146
+ return pageable(
8147
+ (p) => this.ctx.request({ method: "GET", path: "/v1/incoming-events", ...p ? { query: p } : {} }),
8148
+ params
8149
+ );
8150
+ }
8151
+ /**
8152
+ * One incoming event, `url` masked; `shape` is the captured sample as paths
8153
+ * and types only.
8154
+ * `GET /v1/incoming-events/{id}`
8155
+ */
8156
+ get(id) {
8157
+ return this.ctx.request({ method: "GET", path: apiPath`/v1/incoming-events/${id}` });
8158
+ }
8159
+ /**
8160
+ * Change it; `enabled: true` switches it on once the sample is in. In
8161
+ * `match` and `required_header`, `null` removes an entry.
8162
+ * `PATCH /v1/incoming-events/{id}`
8163
+ *
8164
+ * @example
8165
+ * await curviate.incomingEvents.update("inc_123", { enabled: true });
8166
+ */
8167
+ update(id, body) {
8168
+ return this.ctx.request({ method: "PATCH", path: apiPath`/v1/incoming-events/${id}`, body });
8169
+ }
8170
+ /**
8171
+ * Delete it: its URL answers `410 INCOMING_URL_RETIRED` from then on.
8172
+ * `DELETE /v1/incoming-events/{id}`
8173
+ */
8174
+ async delete(id) {
8175
+ await this.ctx.request({ method: "DELETE", path: apiPath`/v1/incoming-events/${id}` });
8176
+ }
8177
+ /**
8178
+ * Issue a new URL, in full in this result only. The old one keeps working
8179
+ * for 24 hours (`previous_url_valid_until`), then answers `410
8180
+ * INCOMING_URL_RETIRED`. Only the creator.
8181
+ * `POST /v1/incoming-events/{id}/rotate`
8182
+ */
8183
+ rotate(id) {
8184
+ return this.ctx.request({ method: "POST", path: apiPath`/v1/incoming-events/${id}/rotate` });
8185
+ }
8186
+ /**
8187
+ * The next post replaces the sample (paths and types only) and, when
8188
+ * switched on, still moves Enrollments. Only the creator.
8189
+ * `POST /v1/incoming-events/{id}/capture`
8190
+ */
8191
+ capture(id) {
8192
+ return this.ctx.request({ method: "POST", path: apiPath`/v1/incoming-events/${id}/capture` });
8193
+ }
8194
+ /**
8195
+ * The last 20 posts and what each did (`outcome`; accept values you do not
8196
+ * know), kept 30 days. Metadata only, never a value from the post.
8197
+ * `GET /v1/incoming-events/{id}/arrivals`
8198
+ */
8199
+ arrivals(id) {
8200
+ return this.ctx.request({ method: "GET", path: apiPath`/v1/incoming-events/${id}/arrivals` });
8201
+ }
8202
+ };
8203
+
8204
+ // src/resources/tenant.ts
8205
+ var TenantBetaResource = class {
8206
+ constructor(ctx) {
8207
+ this.ctx = ctx;
8208
+ }
8209
+ /**
8210
+ * The persisted decision only, not the effective consent of a given call:
8211
+ * the `X-Curviate-Beta` header overrides it per request, either way. Every
8212
+ * member may read.
8213
+ * `GET /v1/tenant/beta`
8214
+ *
8215
+ * @example
8216
+ * const { beta_enabled } = await curviate.tenant.beta.get();
8217
+ */
8218
+ get() {
8219
+ return this.ctx.request({ method: "GET", path: "/v1/tenant/beta" });
8220
+ }
8221
+ /**
8222
+ * Persist the consent for every account of the Team. Off: beta-gated
8223
+ * operations refuse `BETA_NOT_ENABLED` again from the next call. Owners
8224
+ * only (`INSUFFICIENT_ROLE`).
8225
+ * `PATCH /v1/tenant/beta`
8226
+ *
8227
+ * @example
8228
+ * await curviate.tenant.beta.set({ beta_enabled: true });
8229
+ */
8230
+ set(body) {
8231
+ return this.ctx.request({ method: "PATCH", path: "/v1/tenant/beta", body });
8232
+ }
8233
+ };
8234
+ var TenantResource = class {
8235
+ constructor(ctx) {
8236
+ this.beta = new TenantBetaResource(ctx);
8237
+ }
8238
+ };
8239
+
8240
+ // src/resources/exports.ts
8241
+ var ExportsResource = class {
8242
+ constructor(ctx) {
8243
+ this.ctx = ctx;
8244
+ }
8245
+ /**
8246
+ * Start an export; it runs in the background and this resolves at once with
8247
+ * `status: "queued"`. `entity` is what to export (`analytics`,
8248
+ * `motion_activity`, `account_activity` or `account_metrics`), `format`
8249
+ * `csv`, `json` or `ndjson` (one table only), `tables` picks the Analytics
8250
+ * tables (`daily`, `accounts`, `motions`, `enrollments`; several make one
8251
+ * ZIP), `filters` are the Analytics overview's filters (default: the last
8252
+ * 30 days). Your team has one export at a time: a second is refused with
8253
+ * `EXPORT_IN_PROGRESS`, and `details.export_id` names the running one (it
8254
+ * may be a teammate's, which `get` answers 404). An Owner or Manager only.
8255
+ * `POST /v1/exports`
8256
+ *
8257
+ * Not retry-safe: a retry starts a second export once the first finishes.
8258
+ *
8259
+ * @example
8260
+ * const exp = await curviate.exports.create({ entity: "analytics", format: "csv", tables: ["daily"], filters: { range: "90d" } });
8261
+ */
8262
+ create(body) {
8263
+ return this.ctx.request({ method: "POST", path: "/v1/exports", body });
8264
+ }
8265
+ /**
8266
+ * One export of yours and its `status`: `queued`, `running`, `ready`
8267
+ * (download it), `failed` (read `error`: `EXPORT_TOO_LARGE` means narrow the
8268
+ * filters or tables), `expired` (24 hours after `ready_at`, the file is
8269
+ * deleted) or `removed` (a Contact was deleted, which removes the Team's
8270
+ * exports that hold Contacts). Accept statuses you do not know. There is no
8271
+ * link to the file.
8272
+ * `GET /v1/exports/{export_id}`
8273
+ *
8274
+ * @example
8275
+ * const { status } = await curviate.exports.get("exp_123");
8276
+ */
8277
+ get(exportId) {
8278
+ return this.ctx.request({ method: "GET", path: apiPath`/v1/exports/${exportId}` });
8279
+ }
8280
+ /**
8281
+ * Download a ready export's file as bytes (CSV with a BOM, JSON, NDJSON, or
8282
+ * a ZIP when several tables were asked). A queued, running or failed export
8283
+ * throws `EXPORT_NOT_READY`, an expired one `EXPORT_EXPIRED` and one removed
8284
+ * by a Contact delete `EXPORT_REMOVED` (run it again).
8285
+ * `GET /v1/exports/{export_id}/file`
8286
+ *
8287
+ * @example
8288
+ * const bytes = await curviate.exports.download("exp_123");
8289
+ * await fs.promises.writeFile("export.csv", Buffer.from(bytes));
8290
+ */
8291
+ async download(exportId) {
8292
+ return this.ctx.request({ bytes: true, method: "GET", path: apiPath`/v1/exports/${exportId}/file` });
6923
8293
  }
6924
8294
  };
6925
8295
 
@@ -6980,6 +8350,14 @@ var SalesNavigatorResource = class {
6980
8350
  * base64-encoded file bytes, always sent as JSON, never multipart.
6981
8351
  * `POST /v1/{account_id}/sales-navigator/chats`
6982
8352
  *
8353
+ * When the Account already has a private
8354
+ * conversation with this person that you may not read, this throws
8355
+ * `CHAT_NOT_VISIBLE` (409) and sends nothing. Ask the person who connected
8356
+ * the Account to reply, or to sync the full inbox. It is detected from what
8357
+ * Curviate stored, so a thread older than 90 days that Curviate never
8358
+ * touched is not detected. A person on the do-not-contact list throws
8359
+ * `DO_NOT_CONTACT` (409).
8360
+ *
6983
8361
  * @beta Beta operation. See the namespace note above: while beta-gated it
6984
8362
  * refuses `BETA_NOT_ENABLED` (403) until a human enables beta in Settings,
6985
8363
  * or the request carries the `X-Curviate-Beta` header.
@@ -7170,6 +8548,14 @@ var RecruiterResource = class {
7170
8548
  * are REQUIRED; optional attachments ride the body as base64-encoded
7171
8549
  * objects (no multipart). `POST /v1/{account_id}/recruiter/chats`
7172
8550
  *
8551
+ * When the Account already has a private
8552
+ * conversation with this person that you may not read, this throws
8553
+ * `CHAT_NOT_VISIBLE` (409) and sends nothing. Ask the person who connected
8554
+ * the Account to reply, or to sync the full inbox. It is detected from what
8555
+ * Curviate stored, so a thread older than 90 days that Curviate never
8556
+ * touched is not detected. A person on the do-not-contact list throws
8557
+ * `DO_NOT_CONTACT` (409).
8558
+ *
7173
8559
  * @beta Beta operation. See the namespace note above: while beta-gated it
7174
8560
  * refuses `BETA_NOT_ENABLED` (403) until a human enables beta in Settings,
7175
8561
  * or the request carries the `X-Curviate-Beta` header.
@@ -7789,6 +9175,9 @@ var CompaniesResource = class {
7789
9175
  * the whole request rejects with a 404, not a partial result.
7790
9176
  * Re-inviting an already-invited member is a safe no-op, the same
7791
9177
  * invitation id, never a duplicate.
9178
+ *
9179
+ * A person on the Team's do-not-contact list throws `DO_NOT_CONTACT` (409);
9180
+ * nothing is sent.
7792
9181
  */
7793
9182
  followInvite(identifier, body) {
7794
9183
  return this.ctx.request({
@@ -7919,9 +9308,50 @@ var CompaniesResource = class {
7919
9308
  };
7920
9309
 
7921
9310
  // src/resources/webhooks.ts
9311
+ var WebhookDeliveriesResource = class {
9312
+ constructor(ctx) {
9313
+ this.ctx = ctx;
9314
+ }
9315
+ /**
9316
+ * The webhook's deliveries of the last 30 days, newest first: one item per
9317
+ * delivery (one event rendered once), attempts counted. `failed: true` keeps
9318
+ * those that ended `failed` or `not_retried`. No body is kept. An item's `id`
9319
+ * is the same value as the delivered body's `id`. `await` it for one page;
9320
+ * `for await` it for every delivery across pages.
9321
+ * `GET /v1/webhooks/{id}/deliveries`
9322
+ *
9323
+ * @example
9324
+ * for await (const d of curviate.webhooks.deliveries.list("wh_123", { failed: true })) console.log(d.id, d.status);
9325
+ */
9326
+ list(webhookId, params) {
9327
+ return pageable(
9328
+ (p) => this.ctx.request({
9329
+ method: "GET",
9330
+ path: apiPath`/v1/webhooks/${webhookId}/deliveries`,
9331
+ ...p ? { query: p } : {}
9332
+ }),
9333
+ params
9334
+ );
9335
+ }
9336
+ /**
9337
+ * One delivery: status, reason, attempts, the receiver's latest code and
9338
+ * timing, and `redeliverable_until`.
9339
+ * `GET /v1/webhooks/{id}/deliveries/{delivery_id}`
9340
+ *
9341
+ * @example
9342
+ * const d = await curviate.webhooks.deliveries.get("wh_123", "wdl_456");
9343
+ */
9344
+ get(webhookId, deliveryId) {
9345
+ return this.ctx.request({
9346
+ method: "GET",
9347
+ path: apiPath`/v1/webhooks/${webhookId}/deliveries/${deliveryId}`
9348
+ });
9349
+ }
9350
+ };
7922
9351
  var WebhooksResource = class {
7923
9352
  constructor(ctx) {
7924
9353
  this.ctx = ctx;
9354
+ this.deliveries = new WebhookDeliveriesResource(ctx);
7925
9355
  }
7926
9356
  /**
7927
9357
  * Register a new webhook endpoint to receive real-time events.
@@ -7933,6 +9363,13 @@ var WebhooksResource = class {
7933
9363
  * account of the workspace (reads then return `account_ids: null`). An
7934
9364
  * explicit `[]` is rejected.
7935
9365
  *
9366
+ * `body_template` (a JSON text with `{{variables}}` in its strings) replaces
9367
+ * the standard payload; check it offline first with
9368
+ * `validateWebhookTemplate`. A template that fails is refused with
9369
+ * `WEBHOOK_TEMPLATE_INVALID` (every finding with `line` and `col`); its
9370
+ * warnings come back in `warnings`. `custom_event_names` narrows
9371
+ * `motion.custom_event` to those names (`null`: every one).
9372
+ *
7936
9373
  * @example
7937
9374
  * await curviate.webhooks.create({ source: "account_status", request_url: "https://example.com/hooks" });
7938
9375
  */
@@ -7970,6 +9407,8 @@ var WebhooksResource = class {
7970
9407
  * Return a single webhook owned by the calling tenant.
7971
9408
  * `GET /v1/webhooks/{id}`
7972
9409
  * The plaintext secret is never present on a read, only `secret_prefix`.
9410
+ * `request_url` reads masked (`https://host/•••`). `disabled_reason`,
9411
+ * `failing_since` and `last_delivery_status_code` say how it is delivering.
7973
9412
  */
7974
9413
  get(id) {
7975
9414
  return this.ctx.request({
@@ -7981,7 +9420,15 @@ var WebhooksResource = class {
7981
9420
  * Update a webhook in place. `source` is immutable.
7982
9421
  * `PATCH /v1/webhooks/{id}`
7983
9422
  * `account_ids: null` switches an `account_status` webhook to every current
7984
- * and future account.
9423
+ * and future account. Sending back the masked `request_url` you read keeps
9424
+ * the stored URL. A `headers` entry with a key and no value keeps that
9425
+ * header's stored value; a new key needs a value. Setting `body_template`
9426
+ * lints it (`WEBHOOK_TEMPLATE_INVALID` on an error; warnings in the result's
9427
+ * `warnings`). Never rotates the secret: use {@link rotateSecret}.
9428
+ *
9429
+ * @example
9430
+ * // keep the stored Authorization value, replace the rest
9431
+ * await curviate.webhooks.update("wh_123", { headers: [{ key: "Authorization" }, { key: "X-Env", value: "prod" }] });
7985
9432
  */
7986
9433
  update(id, body) {
7987
9434
  return this.ctx.request({
@@ -8000,6 +9447,77 @@ var WebhooksResource = class {
8000
9447
  path: apiPath`/v1/webhooks/${id}`
8001
9448
  });
8002
9449
  }
9450
+ /**
9451
+ * The variables a body template of this source may use, each with its
9452
+ * scope, type and an example from the synthetic sample. On `motion` it also
9453
+ * lists every Enrollment value your Team's current Sequences save
9454
+ * (`{{enrollment.<key>}}`), with `saved_by`.
9455
+ * `GET /v1/webhooks/variables`
9456
+ *
9457
+ * @example
9458
+ * const { variables } = await curviate.webhooks.variables({ source: "motion" });
9459
+ */
9460
+ variables(params) {
9461
+ return this.ctx.request({
9462
+ method: "GET",
9463
+ path: "/v1/webhooks/variables",
9464
+ query: params
9465
+ });
9466
+ }
9467
+ /**
9468
+ * Queue a synthetic delivery to confirm your endpoint receives and verifies
9469
+ * deliveries: one signed attempt with `Curviate-Test: true`, never changing
9470
+ * the webhook's health. `event` picks the subscribed event it stands for
9471
+ * (default: the first); the delivered `event` is always `webhook.test`, with
9472
+ * `data.test_event` the one you picked. Read the attempt with
9473
+ * `deliveries.get(id, delivery_id)`.
9474
+ * `POST /v1/webhooks/{id}/test`
9475
+ *
9476
+ * @example
9477
+ * const { delivery_id } = await curviate.webhooks.test("wh_123", { event: "enrollment.finished" });
9478
+ */
9479
+ test(id, body) {
9480
+ return this.ctx.request({
9481
+ method: "POST",
9482
+ path: apiPath`/v1/webhooks/${id}/test`,
9483
+ body: body ?? {}
9484
+ });
9485
+ }
9486
+ /**
9487
+ * Send a delivery's original request again, byte for byte (never
9488
+ * re-rendered), freshly signed, as one attempt with no retries. Works until
9489
+ * `redeliverable_until` (receipt + 24 h) on an enabled webhook; a success
9490
+ * cancels its pending retries. Refusals: `NOTHING_TO_REDELIVER` (never
9491
+ * sent), `REDELIVERY_EXPIRED`, `WEBHOOK_DISABLED`, `DELIVERY_IN_FLIGHT`
9492
+ * (retry shortly).
9493
+ * `POST /v1/webhooks/{id}/deliveries/{delivery_id}/redeliver`
9494
+ *
9495
+ * @example
9496
+ * await curviate.webhooks.redeliver("wh_123", "wdl_456");
9497
+ */
9498
+ redeliver(id, deliveryId) {
9499
+ return this.ctx.request({
9500
+ method: "POST",
9501
+ path: apiPath`/v1/webhooks/${id}/deliveries/${deliveryId}/redeliver`
9502
+ });
9503
+ }
9504
+ /**
9505
+ * Issue a new signing secret, returned once: store it now. For 24 hours
9506
+ * (`previous_valid_until`) every delivery carries both signatures
9507
+ * (`v1=<new>,v1=<old>`) and `constructEvent` accepts either secret, so
9508
+ * deploy the new one inside that window. A second rotation before then
9509
+ * drops the oldest secret at once. Only the webhook's creator may rotate.
9510
+ * `POST /v1/webhooks/{id}/rotate-secret`
9511
+ *
9512
+ * @example
9513
+ * const { secret, previous_valid_until } = await curviate.webhooks.rotateSecret("wh_123");
9514
+ */
9515
+ rotateSecret(id) {
9516
+ return this.ctx.request({
9517
+ method: "POST",
9518
+ path: apiPath`/v1/webhooks/${id}/rotate-secret`
9519
+ });
9520
+ }
8003
9521
  };
8004
9522
 
8005
9523
  // src/resources/drafts.ts
@@ -8189,7 +9707,12 @@ function buildRootNamespaces(ctx) {
8189
9707
  tier: new TierResource(ctx),
8190
9708
  network: new NetworkResource(ctx),
8191
9709
  unibox: new UniboxResource(ctx),
8192
- analytics: new AnalyticsResource(ctx)
9710
+ analytics: new AnalyticsResource(ctx),
9711
+ billing: new BillingResource(ctx),
9712
+ doNotContact: new DoNotContactResource(ctx),
9713
+ exports: new ExportsResource(ctx),
9714
+ incomingEvents: new IncomingEventsResource(ctx),
9715
+ tenant: new TenantResource(ctx)
8193
9716
  };
8194
9717
  }
8195
9718
  function buildAccountScopedNamespaces(ctx) {
@@ -8208,7 +9731,7 @@ function buildAccountScopedNamespaces(ctx) {
8208
9731
  }
8209
9732
 
8210
9733
  // src/client.ts
8211
- var Curviate = class {
9734
+ var Curviate = class _Curviate {
8212
9735
  constructor(config) {
8213
9736
  this.config = resolveConfig(config);
8214
9737
  const ctx = createContext(this.config);
@@ -8229,6 +9752,11 @@ var Curviate = class {
8229
9752
  this.network = ns.network;
8230
9753
  this.unibox = ns.unibox;
8231
9754
  this.analytics = ns.analytics;
9755
+ this.billing = ns.billing;
9756
+ this.doNotContact = ns.doNotContact;
9757
+ this.exports = ns.exports;
9758
+ this.incomingEvents = ns.incomingEvents;
9759
+ this.tenant = ns.tenant;
8232
9760
  }
8233
9761
  /**
8234
9762
  * Return an account-scoped accessor that fixes `account_id` on every call,
@@ -8252,6 +9780,25 @@ var Curviate = class {
8252
9780
  const ctx = createContext(this.config, accountId);
8253
9781
  return buildAccountScopedNamespaces(ctx);
8254
9782
  }
9783
+ /**
9784
+ * The same client acting in another Team: every call made through the
9785
+ * returned client sends `X-Curviate-Team: <team>`, overriding the `team`
9786
+ * client option. Use it per call with a key that works in all of your
9787
+ * Teams, which a Team-level operation otherwise refuses with
9788
+ * `TEAM_REQUIRED`. A Team key accepts only its own Team
9789
+ * (`TEAM_ACCESS_DENIED`). No network call; `team` is checked as in the
9790
+ * constructor.
9791
+ *
9792
+ * @param team - a Team id (`tnt_...`); `curviate.teams.list()` lists yours.
9793
+ *
9794
+ * @example
9795
+ * for (const team of (await curviate.teams.list()).items) {
9796
+ * const { items } = await curviate.withTeam(team.id).motions.list({ status: ["active"] });
9797
+ * }
9798
+ */
9799
+ withTeam(team) {
9800
+ return new _Curviate({ ...this.config, team });
9801
+ }
8255
9802
  /**
8256
9803
  * Transparent cursor-pagination iterator.
8257
9804
  *
@@ -8335,16 +9882,16 @@ var WebhookSignatureError = class _WebhookSignatureError extends Error {
8335
9882
  function parseHeader(header) {
8336
9883
  const parts = header.split(",");
8337
9884
  let tStr;
8338
- let v1;
9885
+ const v1 = [];
8339
9886
  for (const part of parts) {
8340
9887
  const eq = part.indexOf("=");
8341
9888
  if (eq === -1) continue;
8342
9889
  const key = part.slice(0, eq).trim();
8343
9890
  const val = part.slice(eq + 1).trim();
8344
9891
  if (key === "t") tStr = val;
8345
- else if (key === "v1") v1 = val;
9892
+ else if (key === "v1") v1.push(val);
8346
9893
  }
8347
- if (tStr === void 0 || v1 === void 0) {
9894
+ if (tStr === void 0 || v1.length === 0) {
8348
9895
  throw new WebhookSignatureError(
8349
9896
  "malformed_header",
8350
9897
  'Webhook signature header must contain both "t=<timestamp>" and "v1=<hmac>".'
@@ -8399,20 +9946,29 @@ async function hmacWebCrypto(secret, payload) {
8399
9946
  return bytesToHex(new Uint8Array(sig));
8400
9947
  }
8401
9948
  function verifyAndParse(computedHex, v1, timestamp, bodyStr, replayWindowSecs) {
8402
- let providedBytes;
8403
- try {
8404
- providedBytes = hexToBytes(v1);
8405
- } catch {
9949
+ const computedBytes = hexToBytes(computedHex);
9950
+ let matched = false;
9951
+ let validHex = false;
9952
+ for (const candidate of v1) {
9953
+ let providedBytes;
9954
+ try {
9955
+ providedBytes = hexToBytes(candidate);
9956
+ } catch {
9957
+ continue;
9958
+ }
9959
+ validHex = true;
9960
+ if (constantTimeEqual(computedBytes, providedBytes)) matched = true;
9961
+ }
9962
+ if (!validHex) {
8406
9963
  throw new WebhookSignatureError(
8407
9964
  "invalid_signature",
8408
9965
  "Webhook signature v1 value is not valid hex."
8409
9966
  );
8410
9967
  }
8411
- const computedBytes = hexToBytes(computedHex);
8412
- if (!constantTimeEqual(computedBytes, providedBytes)) {
9968
+ if (!matched) {
8413
9969
  throw new WebhookSignatureError(
8414
9970
  "invalid_signature",
8415
- "Webhook signature does not match. Verify your signing secret."
9971
+ "Webhook signature does not match any v1. Verify your signing secret."
8416
9972
  );
8417
9973
  }
8418
9974
  const nowSecs = Date.now() / 1e3;
@@ -8457,6 +10013,343 @@ async function constructEvent(rawBody, signatureHeader, secret, opts) {
8457
10013
  const computedHex = await hmacWebCrypto(secret, hmacPayload);
8458
10014
  return verifyAndParse(computedHex, v1, timestamp, bodyStr, replayWindowSecs);
8459
10015
  }
10016
+
10017
+ // src/shared/webhook-template.ts
10018
+ var MAX_TEMPLATE_BYTES = 16384;
10019
+ var MAX_TEMPLATE_REFERENCES = 200;
10020
+ var BODY_TEMPLATES_DOCS_URL = "https://docs.curviate.com/reference/webhooks/body-templates";
10021
+ var RULES = {
10022
+ T1: { title: "Empty template", hint: () => "Send null for the standard payload, or write the JSON body your tool expects." },
10023
+ T2: {
10024
+ title: "Not valid JSON",
10025
+ hint: (t) => t ? `Remove the trailing comma before ${t}.` : "Write the template as JSON: double-quoted keys and strings, commas between members, nothing after the last value."
10026
+ },
10027
+ T3: { title: "Variable never closed", hint: () => "Close the variable with }}, like {{contact.email}}." },
10028
+ T4: { title: "Variable outside a string", hint: (t) => `Put the variable inside a JSON string: "${t}" keeps the value's type.` },
10029
+ T5: { title: "Variable in a key", hint: (t) => `Use a fixed key and put the variable in the value, like ${t}.` },
10030
+ T6: { title: "Unknown variable", hint: (t) => t ? `Use ${t}.` : "Use a variable this source carries: GET /v1/webhooks/variables?source=<source> lists them." },
10031
+ T7: { title: "enrollment.id is reserved", hint: () => "Use {{enrollment_id}} for the Enrollment's id." },
10032
+ T8: { title: "No defaults or filters", hint: () => "Remove the part after |: a template has no defaults or filters, and a missing value drops its key." },
10033
+ T9: { title: "Single-brace placeholder", hint: (t) => `Write the variable with double braces, like ${t}.` },
10034
+ T10: { title: "Template size", hint: () => `Keep the template to ${MAX_TEMPLATE_BYTES.toLocaleString("en-US")} bytes and ${MAX_TEMPLATE_REFERENCES} variables.` },
10035
+ TW1: { title: "Enrollment value no Sequence saves", hint: (t) => `Save ${t} with save on an until event.<name> step of a Sequence; until one does, it is always missing.` }
10036
+ };
10037
+ var WEBHOOK_TEMPLATE_RULE_CODES = Object.keys(RULES);
10038
+ var SCOPES_BY_SOURCE = {
10039
+ motion: ["event", "account", "contact", "motion", "enrollment"],
10040
+ messaging: ["event", "account", "contact", "motion"],
10041
+ user: ["event", "account", "contact", "motion"],
10042
+ account_status: ["event", "account"],
10043
+ post: ["event", "account"]
10044
+ };
10045
+ var ACCOUNT_PARTS = ["first_name", "last_name", "full_name"];
10046
+ var EXTRA_KEY = /^[A-Za-z0-9_]{1,64}$/;
10047
+ var VALUE_KEY = /^[a-z][a-z0-9_]{0,63}$/;
10048
+ var FIXED = [
10049
+ ["event", "event"],
10050
+ ["occurred_at", "event"],
10051
+ ...ACCOUNT_PARTS.map((p) => [`account.${p}`, "account"]),
10052
+ ...STANDARD_FIELDS.map((f) => [`contact.${f}`, "contact"]),
10053
+ ["motion.id", "motion"],
10054
+ ["motion.name", "motion"],
10055
+ ["enrollment_id", "motion"],
10056
+ ["step_id", "enrollment"]
10057
+ ];
10058
+ var SCOPE_OF = new Map(FIXED);
10059
+ var SHORT = new Map([...STANDARD_FIELDS.map((f) => [f, `contact.${f}`]), ...ACCOUNT_PARTS.map((p) => [`my_${p}`, `account.${p}`])]);
10060
+ function resolve(inner) {
10061
+ const raw = inner.trim();
10062
+ if (raw.includes("|"))
10063
+ return { ok: false, code: "T8", rule: `{{${raw.slice(0, raw.indexOf("|")).trim()} | \u2026}} carries a default or a filter` };
10064
+ const name = SHORT.get(raw) ?? raw;
10065
+ if (name === "enrollment.id")
10066
+ return { ok: false, code: "T7", rule: "{{enrollment.id}} is reserved", token: "{{enrollment_id}}", candidates: ["{{enrollment_id}}"] };
10067
+ const fixed = SCOPE_OF.get(name);
10068
+ if (fixed)
10069
+ return { ok: true, name, scope: fixed };
10070
+ if (name.startsWith("contact.extra.") && EXTRA_KEY.test(name.slice(14)))
10071
+ return { ok: true, name, scope: "contact" };
10072
+ if (name.startsWith("enrollment.") && VALUE_KEY.test(name.slice(11)))
10073
+ return { ok: true, name, scope: "enrollment" };
10074
+ return { ok: false, code: "T6", rule: `unknown variable {{${raw}}}` };
10075
+ }
10076
+ var scopeOk = (source, r) => (SCOPES_BY_SOURCE[source] ?? []).includes(r.scope);
10077
+ function candidatesFor(source, raw) {
10078
+ const known = [...FIXED.filter(([n, s]) => scopeOk(source, { name: n, scope: s })).map(([n]) => n), ...[...SHORT].filter(([, l]) => scopeOk(source, { name: l, scope: SCOPE_OF.get(l) })).map(([s]) => s)];
10079
+ return known.map((n) => [n, distance(raw, n, 3)]).filter(([, d]) => d <= 3).sort((a, b) => a[1] - b[1] || (a[0] < b[0] ? -1 : 1)).slice(0, 3).map(([n]) => `{{${n}}}`);
10080
+ }
10081
+ var LITERAL = /-?(?:0|[1-9]\d*)(?:\.\d+)?(?:[eE][+-]?\d+)?|true|false|null/y;
10082
+ var STRUCTURAL = /* @__PURE__ */ new Set(["{", "}", "[", "]", ":", ",", '"']);
10083
+ var what = (c) => c === void 0 ? "the end of the template" : STRUCTURAL.has(c) ? `'${c}'` : "a character that starts no JSON value";
10084
+ function scan(src) {
10085
+ const out = { strings: [], bare: [], error: null };
10086
+ const fail = (at, rule, code = "T2", token) => (out.error = { at, code, rule, ...token ? { token } : {} }, out);
10087
+ const stack = [];
10088
+ let i = 0;
10089
+ let expect = "value";
10090
+ let opened = false;
10091
+ let comma = false;
10092
+ const ws = () => {
10093
+ while (i < src.length && (src[i] === " " || src[i] === " " || src[i] === "\n" || src[i] === "\r"))
10094
+ i++;
10095
+ };
10096
+ const string = () => {
10097
+ let j = i + 1;
10098
+ while (j < src.length) {
10099
+ const c = src.charCodeAt(j);
10100
+ if (c === 34)
10101
+ return j + 1;
10102
+ if (c === 92) {
10103
+ const e = src[j + 1];
10104
+ if (e === "u" ? !/^[0-9a-fA-F]{4}$/.test(src.slice(j + 2, j + 6)) : e === void 0 || !'"\\/bfnrt'.includes(e))
10105
+ return `invalid escape in a string at line ${lineCol(src, j).line}`;
10106
+ j += e === "u" ? 6 : 2;
10107
+ continue;
10108
+ }
10109
+ if (c < 32)
10110
+ return "a control character (a raw line break or tab) inside a string; write it as \\n or \\t";
10111
+ j++;
10112
+ }
10113
+ return "a string is never closed";
10114
+ };
10115
+ for (; ; ) {
10116
+ ws();
10117
+ const c = src[i];
10118
+ if (expect === "next") {
10119
+ const closer = stack.at(-1);
10120
+ if (closer === void 0)
10121
+ return i < src.length ? fail(i, `unexpected ${what(c)} after the JSON value`) : out;
10122
+ if (c === ",") {
10123
+ i++;
10124
+ comma = true;
10125
+ expect = closer === "}" ? "key" : "value";
10126
+ } else if (c === closer) {
10127
+ stack.pop();
10128
+ i++;
10129
+ } else
10130
+ return fail(i, `expected ',' or '${closer}', found ${what(c)}`);
10131
+ continue;
10132
+ }
10133
+ if (opened && c === stack.at(-1)) {
10134
+ stack.pop();
10135
+ i++;
10136
+ opened = false;
10137
+ expect = "next";
10138
+ continue;
10139
+ }
10140
+ if (comma && (c === "}" || c === "]"))
10141
+ return fail(i, `trailing comma before '${c}'`, "T2", `'${c}'`);
10142
+ opened = comma = false;
10143
+ if (c === void 0)
10144
+ return fail(i, "the template ends where a value is expected");
10145
+ if (expect === "key") {
10146
+ if (c !== '"')
10147
+ return fail(i, `expected a double-quoted key, found ${what(c)}`);
10148
+ const end = string();
10149
+ if (typeof end === "string")
10150
+ return fail(i, end);
10151
+ out.strings.push({ start: i, end, key: true });
10152
+ i = end;
10153
+ ws();
10154
+ if (src[i] !== ":")
10155
+ return fail(i, `expected ':' after a key, found ${what(src[i])}`);
10156
+ i++;
10157
+ expect = "value";
10158
+ continue;
10159
+ }
10160
+ if (c === "{" && src[i + 1] === "{") {
10161
+ const close = src.indexOf("}}", i + 2);
10162
+ if (close < 0)
10163
+ return fail(i, "{{ is never closed", "T3");
10164
+ out.bare.push({ start: i, end: close + 2 });
10165
+ i = close + 2;
10166
+ expect = "next";
10167
+ } else if (c === "{" || c === "[") {
10168
+ stack.push(c === "{" ? "}" : "]");
10169
+ i++;
10170
+ opened = true;
10171
+ expect = c === "{" ? "key" : "value";
10172
+ } else if (c === '"') {
10173
+ const end = string();
10174
+ if (typeof end === "string")
10175
+ return fail(i, end);
10176
+ out.strings.push({ start: i, end, key: false });
10177
+ i = end;
10178
+ expect = "next";
10179
+ } else {
10180
+ LITERAL.lastIndex = i;
10181
+ const m = LITERAL.exec(src);
10182
+ if (!m)
10183
+ return fail(i, `unexpected ${what(c)}`);
10184
+ i += m[0].length;
10185
+ expect = "next";
10186
+ }
10187
+ }
10188
+ }
10189
+ function lineCol(src, at) {
10190
+ let line = 1;
10191
+ let start = 0;
10192
+ for (let k = src.indexOf("\n"); k !== -1 && k < at; k = src.indexOf("\n", k + 1)) {
10193
+ line++;
10194
+ start = k + 1;
10195
+ }
10196
+ return { line, col: at - start + 1 };
10197
+ }
10198
+ function lintWebhookTemplate(template, opts) {
10199
+ const found = [];
10200
+ const finish = () => {
10201
+ const all = found.map((f, n) => ({ f, n })).sort((a, b) => a.f.at - b.f.at || a.n - b.n).map(({ f }) => entry(template, f));
10202
+ return { errors: all.filter((e) => e.severity === "error"), warnings: all.filter((e) => e.severity === "warning") };
10203
+ };
10204
+ if (template.trim() === "") {
10205
+ found.push({ at: 0, code: "T1", rule: "the template is empty; send null for the standard payload" });
10206
+ return finish();
10207
+ }
10208
+ const bytes = new TextEncoder().encode(template).length;
10209
+ if (bytes > MAX_TEMPLATE_BYTES) {
10210
+ found.push({ at: 0, code: "T10", rule: `the template is ${bytes} bytes; the limit is ${MAX_TEMPLATE_BYTES}` });
10211
+ return finish();
10212
+ }
10213
+ const s = scan(template);
10214
+ let refs = 0;
10215
+ const counted = (at) => {
10216
+ refs += 1;
10217
+ if (refs === MAX_TEMPLATE_REFERENCES + 1)
10218
+ found.push({ at, code: "T10", rule: `the template has more than ${MAX_TEMPLATE_REFERENCES} variables` });
10219
+ };
10220
+ const judge = (at, inner) => {
10221
+ const r = resolve(inner);
10222
+ if (!r.ok) {
10223
+ const raw = inner.trim();
10224
+ const dotless = r.code === "T6" && !raw.includes(".") && /^[A-Za-z0-9_]+$/.test(raw);
10225
+ const extra = dotless && (SCOPES_BY_SOURCE[opts.source] ?? []).includes("contact") ? `{{contact.extra.${raw}}}` : void 0;
10226
+ const near = r.code === "T6" ? candidatesFor(opts.source, raw) : r.candidates ?? [];
10227
+ const candidates = extra ? [extra, ...near.filter((c) => c !== extra)] : near;
10228
+ found.push({ at, code: r.code, rule: r.rule, ...r.token ?? extra ?? candidates[0] ? { token: r.token ?? extra ?? candidates[0] } : {}, ...candidates.length ? { candidates } : {} });
10229
+ return;
10230
+ }
10231
+ if (!scopeOk(opts.source, r)) {
10232
+ found.push({ at, code: "T6", rule: `{{${inner.trim()}}} is not carried by ${opts.source} events` });
10233
+ return;
10234
+ }
10235
+ const key = r.name.startsWith("enrollment.") ? r.name.slice(11) : null;
10236
+ if (key !== null && opts.saved && !opts.saved.has(key))
10237
+ found.push({ at, code: "TW1", rule: `{{${r.name}}}: no current Sequence of this Team saves ${key}`, token: key });
10238
+ };
10239
+ for (const str of s.strings) {
10240
+ const raw = template.slice(str.start + 1, str.end - 1);
10241
+ const base = str.start + 1;
10242
+ for (let from = 0; ; ) {
10243
+ const k = raw.indexOf("{{", from);
10244
+ if (k < 0)
10245
+ break;
10246
+ if (raw[k - 1] === "\\" && raw[k - 2] === "\\") {
10247
+ from = k + 2;
10248
+ continue;
10249
+ }
10250
+ const close = raw.indexOf("}}", k + 2);
10251
+ if (close < 0) {
10252
+ found.push({ at: base + k, code: "T3", rule: "{{ is never closed" });
10253
+ break;
10254
+ }
10255
+ const inner = raw.slice(k + 2, close);
10256
+ counted(base + k);
10257
+ if (str.key) {
10258
+ const leaf = (resolve(inner).ok ? SHORT.get(inner.trim()) ?? inner.trim() : inner.trim()).split(".").pop() || "value";
10259
+ found.push({ at: base + k, code: "T5", rule: `{{${inner.trim()}}} is in an object key`, token: `"${leaf}": "{{${inner.trim()}}}"` });
10260
+ } else
10261
+ judge(base + k, inner);
10262
+ from = close + 2;
10263
+ }
10264
+ if (!str.key) {
10265
+ for (const m of raw.matchAll(/\{([A-Za-z_][A-Za-z0-9_.]*)\}/g)) {
10266
+ const k = m.index;
10267
+ if (raw[k - 1] === "{" || raw[k + m[0].length] === "}")
10268
+ continue;
10269
+ found.push({ at: base + k, code: "T9", rule: `{${m[1]}} is a single-brace placeholder`, token: `{{${m[1]}}}` });
10270
+ }
10271
+ }
10272
+ }
10273
+ for (const b of s.bare) {
10274
+ const inner = template.slice(b.start + 2, b.end - 2).trim();
10275
+ counted(b.start);
10276
+ found.push({ at: b.start, code: "T4", rule: `{{${inner}}} is outside a JSON string`, token: `{{${inner}}}` });
10277
+ }
10278
+ if (s.error)
10279
+ found.push({ at: s.error.at, code: s.error.code, rule: s.error.code === "T3" ? s.error.rule : `not valid JSON: ${s.error.rule}`, ...s.error.token ? { token: s.error.token } : {} });
10280
+ return finish();
10281
+ }
10282
+ function entry(src, f) {
10283
+ const { line, col } = lineCol(src, f.at);
10284
+ return {
10285
+ path: "body_template",
10286
+ rule: f.rule,
10287
+ code: f.code,
10288
+ message: f.rule,
10289
+ pointer: "/body_template",
10290
+ line,
10291
+ col,
10292
+ hint: RULES[f.code].hint(f.token ?? ""),
10293
+ docs_url: `${BODY_TEMPLATES_DOCS_URL}#${f.code.toLowerCase()}`,
10294
+ severity: f.code === "TW1" ? "warning" : "error",
10295
+ ...f.candidates?.length ? { candidates: f.candidates } : {}
10296
+ };
10297
+ }
10298
+ var isObj2 = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
10299
+ var own = (o, k) => isObj2(o) && Object.hasOwn(o, k) ? o[k] : void 0;
10300
+ function valueOf(name, ctx) {
10301
+ const [ns, a, ...rest] = name.split(".");
10302
+ let v;
10303
+ if (a === void 0)
10304
+ v = own(ctx, ns);
10305
+ else if (ns === "contact" && a === "extra")
10306
+ v = own(own(ctx.contact, "extra"), rest.join("."));
10307
+ else if (rest.length === 0)
10308
+ v = own(ns === "contact" ? ctx.contact : ns === "account" ? ctx.account : ns === "motion" ? ctx.motion : ns === "enrollment" ? ctx.enrollment : void 0, a);
10309
+ if (typeof v === "string")
10310
+ return v.trim() === "" ? void 0 : v;
10311
+ if (typeof v === "number")
10312
+ return Number.isFinite(v) ? v : void 0;
10313
+ return typeof v === "boolean" ? v : void 0;
10314
+ }
10315
+ var DROP = /* @__PURE__ */ Symbol("drop");
10316
+ var WHOLE = /^\{\{([^{}]*)\}\}$/;
10317
+ function renderWebhookTemplate(template, ctx) {
10318
+ const look = (inner) => {
10319
+ const r = resolve(inner);
10320
+ return r.ok ? valueOf(r.name, ctx) : void 0;
10321
+ };
10322
+ const sub = (v) => {
10323
+ if (typeof v === "string") {
10324
+ const whole = WHOLE.exec(v);
10325
+ if (whole)
10326
+ return look(whole[1]) ?? DROP;
10327
+ return v.replace(/\\\{\{|\{\{([^{}]*)\}\}/g, (m, inner) => inner === void 0 ? "{{" : String(look(inner) ?? ""));
10328
+ }
10329
+ if (Array.isArray(v))
10330
+ return v.map(sub).filter((x) => x !== DROP);
10331
+ if (isObj2(v)) {
10332
+ const o = {};
10333
+ for (const [k, x] of Object.entries(v)) {
10334
+ const r = sub(x);
10335
+ if (r !== DROP)
10336
+ o[k] = r;
10337
+ }
10338
+ return o;
10339
+ }
10340
+ return v;
10341
+ };
10342
+ const out = sub(JSON.parse(template));
10343
+ return JSON.stringify(out === DROP ? null : out);
10344
+ }
10345
+
10346
+ // src/templates.ts
10347
+ function validateWebhookTemplate(template, opts) {
10348
+ return lintWebhookTemplate(template, { source: opts.source });
10349
+ }
10350
+ function renderWebhookTemplate2(template, context) {
10351
+ return renderWebhookTemplate(template, context);
10352
+ }
8460
10353
  export {
8461
10354
  Curviate,
8462
10355
  CurviateError,
@@ -8468,6 +10361,8 @@ export {
8468
10361
  constructEvent,
8469
10362
  isCurviateError,
8470
10363
  newStepId,
10364
+ renderWebhookTemplate2 as renderWebhookTemplate,
8471
10365
  step,
8472
- validateSequence
10366
+ validateSequence,
10367
+ validateWebhookTemplate
8473
10368
  };