@curviate/sdk 0.44.0 → 0.45.0-next.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/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.0";
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,43 @@ 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.
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.
740
843
  *
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.
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. `get` carries every row. Reading the list
855
+ * never calls LinkedIn.
745
856
  *
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.
857
+ * Each item also carries `external_id` (your own id for the account's end
858
+ * user, set on `auth.intent` or `update`) and `metadata`, both `null` when
859
+ * unset.
748
860
  *
749
861
  * @param params - optional `limit` (1-250), `cursor` (from a prior page), and
750
862
  * `external_id` (exact match: only that end user's accounts).
@@ -794,25 +906,41 @@ var AccountsResource = class {
794
906
  * `quotas[]` REPLACED its earlier `quota_name` / `remaining` / `total` /
795
907
  * `reset_time` / `recommended_throttle_hint` shape. Each row now reports
796
908
  * `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`:
909
+ * `over_default`, `halt` and the window fields. **Compare `used` against
910
+ * `effective_ceiling`, not `ceiling`**: the effective one is the ceiling
911
+ * with the account's warm-up ramp applied, and on a new account the two
912
+ * differ by roughly a factor of seven. Limits are always enforced: at the
913
+ * effective ceiling the action is refused with `BUDGET_EXHAUSTED`; to send
914
+ * more, raise the limit. Two rows are not counters and say so in `kind`:
803
915
  * `total_actions` (`tally`) and `pending_invites` (`gauge`, whose `used` is
804
916
  * `null` until something has observed it).
805
917
  *
806
918
  * The per-minute REQUEST ceiling is a different thing and is deliberately
807
919
  * 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.
920
+ * LinkedIn account, and it is answered with `429 RATE_LIMIT_ACCOUNT`.
921
+ *
922
+ * `activity_window` says which time zone the account's active hours are
923
+ * read in. Nobody sets it: it follows where the account connects from.
924
+ * `effective_timezone` is that IANA zone, or `null` while it is unknown;
925
+ * `source` says where it came from (`country`, `region`, `detected` or
926
+ * `none`); `zone_state` is `known`, `pinned` (a check agreed on one zone,
927
+ * kept until the next location change) or `unknown`, when the account acts
928
+ * only in the hours every zone in `fallback_timezones` shares (an empty list
929
+ * means the window is closed). A live write outside the window is refused
930
+ * with `BUDGET_EXHAUSTED` (`safetyReason: "activity_window"`); reads are
931
+ * never affected.
932
+ *
933
+ * `warm_up` is the account's ramp: `enabled` (the switch, default `true`;
934
+ * set it with `PATCH /v1/{account_id}/safety-policy` and
935
+ * `warm_up: { enabled }`), `dormant` (no action for more than 30 days),
936
+ * `state` and the `triggers` that apply now (`connect`, `dormant`,
937
+ * `post_restriction`). Off skips the connect and quiet ramps; recovery after
938
+ * a LinkedIn restriction still applies.
939
+ *
940
+ * `connection_location` carries `checked_at` (the last location check, or
941
+ * `null`), `detected_country` (where your own proxy was found to connect
942
+ * from, or `null`) and `flags` (`country_mismatch`, `zone_moved`). It never
943
+ * carries an IP.
816
944
  *
817
945
  * `account_states[]` lists platform conditions the account is in right now,
818
946
  * additive to `status` and independent of it: `recruiter_session_evicted`,
@@ -853,11 +981,20 @@ var AccountsResource = class {
853
981
  * proxy can only take a `country` together with `proxy: null`. The
854
982
  * `proxy.password`, if given, is never returned.
855
983
  *
984
+ * `save_connections_as_contacts` (Automate; on by default) saves the
985
+ * account's Connections as Contacts: each person becomes one Contact in your
986
+ * team, an existing Contact is only filled in, do-not-contact people are
987
+ * skipped. `true` also saves the Connections already synced; `false` keeps
988
+ * the Contacts saved so far. Without Automate it throws `TIER_REQUIRED`, and
989
+ * `get` omits the field. `save_connections_paused_reason` reads
990
+ * `"contact_limit_reached"` while your team holds its 100,000 Contacts.
991
+ *
856
992
  * Returns the account (as `get` returns it, without `quotas` and
857
993
  * `event_log`). After a location change its `connection_location` is re-read,
858
994
  * so it shows where the account really connects from now. A change that did
859
995
  * not land where asked throws `CONNECTION_LOCATION_UNAVAILABLE`, whose
860
- * `connectionLocation` carries the actual location.
996
+ * `connectionLocation` carries the actual location. The account's time zone
997
+ * follows its connection location: there is no zone to set.
861
998
  *
862
999
  * @example
863
1000
  * await curviate.accounts.update("acc_123", { external_id: "usr_42", metadata: { plan: "pro" } });
@@ -865,6 +1002,9 @@ var AccountsResource = class {
865
1002
  * @example
866
1003
  * const account = await curviate.accounts.update("acc_123", { country: "DE" });
867
1004
  * console.log(account.connection_location?.country); // "DE"
1005
+ *
1006
+ * @example
1007
+ * await curviate.accounts.update("acc_123", { save_connections_as_contacts: false });
868
1008
  */
869
1009
  update(accountId, body) {
870
1010
  return this.ctx.request({
@@ -957,6 +1097,96 @@ var AccountsResource = class {
957
1097
  path: apiPath`/v1/billing/seats/${seatId}/cancel/revert`
958
1098
  });
959
1099
  }
1100
+ /**
1101
+ * The account's own numbers, one snapshot per day, oldest first: 1st-degree
1102
+ * `connections`, `followers`, `profile_viewers_90d`,
1103
+ * `search_appearances_week`, `post_impressions_7d`, and `ssi` (Sales
1104
+ * Navigator accounts only, else `null`). Read once a day inside the
1105
+ * account's activity window and stored: this makes no LinkedIn call. `from`
1106
+ * and `to` are the account's local days (default the last 90 days; at most
1107
+ * 366 days across); a day with no snapshot has no item. One response holds
1108
+ * the whole range (`cursor` is always `null`).
1109
+ * `GET /v1/accounts/{account_id}/metrics`
1110
+ *
1111
+ * @example
1112
+ * const { items } = await curviate.accounts.metrics("acc_123", { from: "2026-08-01", to: "2026-10-28" });
1113
+ */
1114
+ metrics(accountId, params) {
1115
+ return this.ctx.request({
1116
+ method: "GET",
1117
+ path: apiPath`/v1/accounts/${accountId}/metrics`,
1118
+ ...params !== void 0 ? { query: params } : {}
1119
+ });
1120
+ }
1121
+ /**
1122
+ * One account at a glance, with no LinkedIn call: `profile`, its daily
1123
+ * `metrics` snapshot (`null` before the first one, or when you may not
1124
+ * operate the account), `limits` (its three outreach rows now),
1125
+ * `activity` (its latest 10 actions and their outcomes, metadata only), and
1126
+ * its results in the window (`performance`, `daily`, its `motions`) as the
1127
+ * team overview reads them scoped to it. `profile`, `metrics`, `limits` and
1128
+ * `activity` do not follow the window. The window, `motion_id`, `origin`,
1129
+ * `include`, `fields`, `sort`, `limit` and `compare` work as on
1130
+ * `analytics.motions.list`; with none of `include` and `fields` the body is
1131
+ * trimmed to a summary. `flags` includes `account_attention` and
1132
+ * `limit_reached` for this account.
1133
+ * `GET /v1/accounts/{account_id}/overview`
1134
+ *
1135
+ * @example
1136
+ * const { performance, flags } = await curviate.accounts.overview("acc_123", { range: "30d" });
1137
+ */
1138
+ overview(accountId, params) {
1139
+ return this.ctx.request({
1140
+ method: "GET",
1141
+ path: apiPath`/v1/accounts/${accountId}/overview`,
1142
+ query: commaLists(params, "motion_id", "origin", "include", "fields", "sort")
1143
+ });
1144
+ }
1145
+ /**
1146
+ * Read an account's safety policy: every configurable field for every
1147
+ * budget row, and where each value was inherited from. Also carries the
1148
+ * derived `warm_up` (`enabled`, `dormant`, `state`, `factor`, `triggers`)
1149
+ * and `activity_window` (the zone the hours are read in, from where the
1150
+ * account connects). Limits are always enforced.
1151
+ * `GET /v1/{account_id}/safety-policy`
1152
+ *
1153
+ * @example
1154
+ * const { warm_up } = await curviate.accounts.getSafetyPolicy("acc_123");
1155
+ */
1156
+ getSafetyPolicy(accountId) {
1157
+ return this.ctx.request({
1158
+ method: "GET",
1159
+ path: apiPath`/v1/${accountId}/safety-policy`
1160
+ });
1161
+ }
1162
+ /**
1163
+ * Update an account's safety policy. Send only the fields you are changing;
1164
+ * `null` clears an override back to the Team or Curviate default. Limits are
1165
+ * always enforced: to send more, raise the limit. Returns the policy as
1166
+ * `getSafetyPolicy` reads it.
1167
+ *
1168
+ * `warm_up: { enabled }` turns the account's warm-up ramp on or off (default
1169
+ * on; off skips the connect and quiet ramps, recovery after a LinkedIn
1170
+ * restriction still applies). `active_hours` sets the account's own days and
1171
+ * local hours; a row's own `activity_window_start` / `activity_window_end`
1172
+ * win over it. Read-only fields (`activity_window`, a row's
1173
+ * `effective_ceiling`, `over_default`, `warm_up_state` ...) are accepted and
1174
+ * ignored, so a read document can be sent back unchanged. A time zone is not
1175
+ * a setting: `timezone` and a row's `activity_window_timezone` other than
1176
+ * `null` are `400 INVALID_REQUEST`; change the account's `country` or own
1177
+ * `proxy` instead.
1178
+ * `PATCH /v1/{account_id}/safety-policy`
1179
+ *
1180
+ * @example
1181
+ * await curviate.accounts.updateSafetyPolicy("acc_123", { warm_up: { enabled: false } });
1182
+ */
1183
+ updateSafetyPolicy(accountId, body) {
1184
+ return this.ctx.request({
1185
+ method: "PATCH",
1186
+ path: apiPath`/v1/${accountId}/safety-policy`,
1187
+ body
1188
+ });
1189
+ }
960
1190
  };
961
1191
 
962
1192
  // src/resources/auth.ts
@@ -973,7 +1203,36 @@ var AuthResource = class {
973
1203
  * success (200 re-authenticated in place / 201 new), or a checkpoint
974
1204
  * challenge (202) when LinkedIn requires verification; resolve it with
975
1205
  * {@link solveCheckpoint} (code) or {@link pollCheckpoint} (mobile-app
976
- * approval).
1206
+ * approval, or `checking`: LinkedIn is checking the sign-in, nothing to
1207
+ * answer, keep polling).
1208
+ *
1209
+ * **Checkpoints.** `challenge_type` may be `checking` (poll it, no action)
1210
+ * or `phone_register` (LinkedIn wants a phone number: solve it with
1211
+ * `(+CC)number`, then answer the code check that follows). On `otp` and
1212
+ * `otp_or_mobile_app_approval`, `channel` says where LinkedIn sent the code
1213
+ * (`email`, `sms`, `whatsapp`, or `null` when not known). `expires_at` is
1214
+ * when Curviate stops accepting an answer; a chained checkpoint keeps the
1215
+ * sign-in's original `expires_at`. After it, solve, poll, resend and request
1216
+ * answer `409 CHECKPOINT_EXPIRED`: start the connection again.
1217
+ *
1218
+ * **Refusals.** A cookie connect LinkedIn rejects, for any reason, is
1219
+ * `401 LINKEDIN_COOKIE_INVALID`. `403 LINKEDIN_SIGN_IN_REQUIRED`: LinkedIn
1220
+ * wants the person to sign in on linkedin.com first; sign in there, then
1221
+ * retry, or connect with cookies from a browser where it is signed in. An
1222
+ * intent that times out before any checkpoint is
1223
+ * `503 LINKEDIN_SERVICE_UNAVAILABLE`; the platform's account cap is
1224
+ * `503 PLATFORM_ERROR` with `retryLikelyToSucceed: false`.
1225
+ *
1226
+ * `user_languages` (cookie connects only; a credentials connect ignores
1227
+ * it): the language preferences of the browser the cookies came from, most
1228
+ * preferred first, at most 5 BCP 47 tags (`en-US`), each at most 35
1229
+ * characters.
1230
+ *
1231
+ * `recruiter_contract_id`: the Sales Navigator or Recruiter contract the
1232
+ * account sends from, for a LinkedIn account that holds several. It is
1233
+ * selected once the Account is connected; the answer then carries
1234
+ * `contract_selected`, and `contract_selected: false` means connected, not
1235
+ * selected (select it again with a reconnect).
977
1236
  *
978
1237
  * For `auth_method: "cookie"`, `user_agent` is **required**; connecting by
979
1238
  * session cookie without one is rejected with `INVALID_REQUEST`. It stays
@@ -1021,7 +1280,14 @@ var AuthResource = class {
1021
1280
  * to keep the account's configured one. The account's `connection_location`
1022
1281
  * says where it connects from; when a connect re-attached an account you
1023
1282
  * already had and moved it, `connection_location.previous_country` names
1024
- * where it was.
1283
+ * where it was. The account's time zone follows its connection location;
1284
+ * `timezone` is deprecated and ignored.
1285
+ *
1286
+ * `warm_up: false` connects a NEW account with warm-up off (no connect ramp,
1287
+ * no ramp after a quiet spell; recovery after a LinkedIn restriction still
1288
+ * applies). Default `true`. Sent with `account_id` it is
1289
+ * `400 INVALID_REQUEST`: change an existing account's switch on its safety
1290
+ * policy instead.
1025
1291
  *
1026
1292
  * @example
1027
1293
  * const result = await curviate.auth.intent({
@@ -1040,12 +1306,21 @@ var AuthResource = class {
1040
1306
  });
1041
1307
  }
1042
1308
  /**
1043
- * Solve a checkpoint challenge by submitting an OTP / 2FA code (or the
1044
- * special value `TRY_ANOTHER_WAY` to switch challenge method).
1309
+ * Solve a checkpoint challenge by submitting an OTP / 2FA code, a phone
1310
+ * number for `phone_register` (`(+CC)number`; `code` accepts `( ) +`), or
1311
+ * the special value `TRY_ANOTHER_WAY`.
1312
+ *
1313
+ * `TRY_ANOTHER_WAY` asks LinkedIn for another verification method and
1314
+ * counts no attempt; the answer is the method LinkedIn offers instead (202),
1315
+ * with the same `expires_at`. It is `400 INVALID_REQUEST` on
1316
+ * `contract_selection`, `challenge_selection`, `phone_register` and
1317
+ * `checking`.
1045
1318
  *
1046
- * Returns the connected account (201) or, when LinkedIn chains a second
1319
+ * Returns the connected account (201, with `contract_selected` when
1320
+ * `recruiter_contract_id` was sent) or, when LinkedIn chains a second
1047
1321
  * challenge (another checkpoint, 202); resolve that one with a further
1048
- * `solveCheckpoint` call for the same `accountId`.
1322
+ * `solveCheckpoint` call for the same `accountId`. Past `expires_at` it is
1323
+ * `409 CHECKPOINT_EXPIRED` (409 on every path now).
1049
1324
  *
1050
1325
  * @param accountId - the (provisional) `account_id` from the 202 response.
1051
1326
  * @param body - the verification `code`.
@@ -1091,11 +1366,14 @@ var AuthResource = class {
1091
1366
  });
1092
1367
  }
1093
1368
  /**
1094
- * Poll for mobile-app approval of a pending checkpoint challenge until it
1095
- * leaves `pending`.
1369
+ * Poll an action-based checkpoint (`mobile_app_approval`,
1370
+ * `otp_or_mobile_app_approval`, `checking`) until it leaves `pending`.
1096
1371
  *
1097
- * On `status: "expired"`, the response carries `challenge_type`
1098
- * (`"mobile_app_approval"`) and a human-readable `recovery_hint`.
1372
+ * A transient error on Curviate's side answers `pending`: keep polling.
1373
+ * Only a refusal answers `failed`, and a terminal refusal throws its own
1374
+ * code. Past `expires_at` (also after the expiry sweep) the poll throws
1375
+ * `409 CHECKPOINT_EXPIRED`; there is no `status: "expired"` answer. Start
1376
+ * the connection again.
1099
1377
  *
1100
1378
  * @param accountId - the (provisional) `account_id` from the 202 response.
1101
1379
  */
@@ -1143,6 +1421,15 @@ var MessagingResource = class {
1143
1421
  * Company pages are reply-only and cannot start a conversation this way.
1144
1422
  * Reply to an existing one instead, with `sendMessage()` using a
1145
1423
  * `COMPANY_` chat id from `inboxes.listChats()`.
1424
+ *
1425
+ * When the Account already has a private
1426
+ * conversation with this person that you may not read, this throws
1427
+ * `CHAT_NOT_VISIBLE` (409) and sends nothing. Ask the person who connected
1428
+ * the Account to reply, or to sync the full inbox. It is detected from what
1429
+ * Curviate stored, so a thread older than 90 days that Curviate never
1430
+ * touched is not detected. A person on the do-not-contact list throws
1431
+ * `DO_NOT_CONTACT` (409). A send marks the chat read, and a disconnected
1432
+ * Account throws `REAUTH_REQUIRED` (409).
1146
1433
  */
1147
1434
  startChat(body) {
1148
1435
  return this.ctx.request({
@@ -1169,6 +1456,10 @@ var MessagingResource = class {
1169
1456
  /**
1170
1457
  * Mark a chat read or unread. `PATCH /v1/{account_id}/chats/{chat_id}`
1171
1458
  * Body: `{read}`.
1459
+ *
1460
+ * `read: false` makes the Unibox chat's `marked_unread` read
1461
+ * `{origin: "api", at}`. A later send through the API marks the chat read
1462
+ * again. A disconnected Account throws `REAUTH_REQUIRED` (409).
1172
1463
  */
1173
1464
  markChatRead(chatId, body) {
1174
1465
  return this.ctx.request({
@@ -1188,6 +1479,18 @@ var MessagingResource = class {
1188
1479
  * first message rather than from the chat. Ordinary (non-InMail) messages
1189
1480
  * return `specifics: {}`.
1190
1481
  *
1482
+ * A message the connected Account sent carries `origin`: `motion`, `unibox`,
1483
+ * `api` (any API key or MCP grant) or `linkedin` (sent on LinkedIn itself).
1484
+ * It is derived by Curviate, omitted when unknown, absent on a received
1485
+ * message and never on a chat's `last_message`. `is_deleted` is `true` for a
1486
+ * deleted message. For an API key or MCP grant, a message's `quoted` is
1487
+ * omitted when the quoted message is from before the Motion chat's visible
1488
+ * start, the same line the message list stops at.
1489
+ *
1490
+ * On a disconnected Account the page comes from Curviate's stored copy
1491
+ * alone: `coverage.complete` is `false` when the stored history is not
1492
+ * whole, and an attachment reads `unavailable: true`.
1493
+ *
1191
1494
  * A CHAT READ CARRIES NO SUBJECT AT ALL, including on its embedded
1192
1495
  * `last_message`: the preview is deliberately narrower than a message read, so
1193
1496
  * that a chat read returns the same fields whether it was answered from
@@ -1222,6 +1525,13 @@ var MessagingResource = class {
1222
1525
  * `{ kind: "personal" }`. Never infer the acting identity from a
1223
1526
  * message's `sender` field, only from `sent_as`.
1224
1527
  *
1528
+ *
1529
+ * A person on the Team's do-not-contact list throws `DO_NOT_CONTACT` (409);
1530
+ * nothing is sent.
1531
+ *
1532
+ * Marks the chat read (its `unread` turns `false`, `marked_unread` clears,
1533
+ * and `chat.updated` reports it). A disconnected Account throws
1534
+ * `REAUTH_REQUIRED` (409); nothing is sent.
1225
1535
  * @example
1226
1536
  * const acc = curviate.account("acc_YOUR_ACCOUNT_ID");
1227
1537
  * const result = await acc.messaging.sendMessage("COMPANY_83734124_2-YTQ3ODU3Njgt", {
@@ -1294,7 +1604,18 @@ var MessagingResource = class {
1294
1604
  path: apiPath`/v1/{account_id}/chats/${chatId}/messages/${messageId}/attachments/${attachmentId}`
1295
1605
  });
1296
1606
  }
1297
- /** Send an InMail. `POST /v1/{account_id}/messages/inmail` */
1607
+ /**
1608
+ * Send an InMail. `POST /v1/{account_id}/messages/inmail`
1609
+ *
1610
+ * When the Account already has a private
1611
+ * conversation with this person that you may not read, this throws
1612
+ * `CHAT_NOT_VISIBLE` (409) and sends nothing. Ask the person who connected
1613
+ * the Account to reply, or to sync the full inbox. It is detected from what
1614
+ * Curviate stored, so a thread older than 90 days that Curviate never
1615
+ * touched is not detected. A person on the do-not-contact list throws
1616
+ * `DO_NOT_CONTACT` (409). A send marks the chat read, and a disconnected
1617
+ * Account throws `REAUTH_REQUIRED` (409).
1618
+ */
1298
1619
  sendInMail(body) {
1299
1620
  return this.ctx.request({
1300
1621
  method: "POST",
@@ -1399,6 +1720,9 @@ var UsersResource = class {
1399
1720
  * Follow a user (bodyless). `POST /v1/{account_id}/users/{user_id}/follow`
1400
1721
  * If the target's profile is private, sends a connect request instead and
1401
1722
  * returns `connect_request_sent`.
1723
+ *
1724
+ * A person on the Team's do-not-contact list throws `DO_NOT_CONTACT` (409);
1725
+ * nothing is sent.
1402
1726
  */
1403
1727
  follow(userId) {
1404
1728
  return this.ctx.request({
@@ -1434,6 +1758,9 @@ var UsersResource = class {
1434
1758
  * `POST /v1/{account_id}/users/{user_id}/endorse-skill`
1435
1759
  * Body key is `endorsement_id` (was `skill_endorsement_id` on `profiles.endorse`).
1436
1760
  * Obtain it from `GET /v1/{account_id}/users/{user_id}?linkedin_sections=linkedin_skills`.
1761
+ *
1762
+ * A person on the Team's do-not-contact list throws `DO_NOT_CONTACT` (409);
1763
+ * nothing is sent.
1437
1764
  */
1438
1765
  endorseSkill(userId, body) {
1439
1766
  return this.ctx.request({
@@ -1449,7 +1776,12 @@ var InvitesResource = class {
1449
1776
  constructor(ctx) {
1450
1777
  this.ctx = ctx;
1451
1778
  }
1452
- /** Send a connect-request. `POST /v1/{account_id}/invites` */
1779
+ /**
1780
+ * Send a connect-request. `POST /v1/{account_id}/invites`
1781
+ *
1782
+ * A person on the Team's do-not-contact list throws `DO_NOT_CONTACT` (409);
1783
+ * nothing is sent.
1784
+ */
1453
1785
  send(body) {
1454
1786
  return this.ctx.request({
1455
1787
  method: "POST",
@@ -1745,7 +2077,12 @@ var PostsResource = class {
1745
2077
  ...params ? { query: params } : {}
1746
2078
  });
1747
2079
  }
1748
- /** React to a post. `POST /v1/{account_id}/posts/{post_id}/reactions` */
2080
+ /**
2081
+ * React to a post. `POST /v1/{account_id}/posts/{post_id}/reactions`
2082
+ *
2083
+ * A person on the Team's do-not-contact list throws `DO_NOT_CONTACT` (409);
2084
+ * nothing is sent.
2085
+ */
1749
2086
  react(postId, body) {
1750
2087
  return this.ctx.request({
1751
2088
  method: "POST",
@@ -1851,6 +2188,9 @@ var CommentsResource = class {
1851
2188
  * Publish a comment on a post. `POST /v1/{account_id}/posts/{post_id}/comments`
1852
2189
  * Was `posts.comment`. Attachments (at most one) travel as a base64
1853
2190
  * object, never `FormData`.
2191
+ *
2192
+ * A person on the Team's do-not-contact list throws `DO_NOT_CONTACT` (409);
2193
+ * nothing is sent.
1854
2194
  */
1855
2195
  create(postId, body) {
1856
2196
  return this.ctx.request({
@@ -1884,6 +2224,9 @@ var CommentsResource = class {
1884
2224
  * Reply to a comment. Shares its path with `edit`; POST creates a reply
1885
2225
  * under the target comment, PATCH edits the target comment's own text.
1886
2226
  * `POST /v1/{account_id}/posts/{post_id}/comments/{comment_id}`
2227
+ *
2228
+ * A person on the Team's do-not-contact list throws `DO_NOT_CONTACT` (409);
2229
+ * nothing is sent.
1887
2230
  */
1888
2231
  reply(postId, commentId, body) {
1889
2232
  return this.ctx.request({
@@ -1916,6 +2259,9 @@ var CommentsResource = class {
1916
2259
  }
1917
2260
  /**
1918
2261
  * React to a comment. `POST /v1/{account_id}/posts/{post_id}/comments/{comment_id}/reactions`
2262
+ *
2263
+ * A person on the Team's do-not-contact list throws `DO_NOT_CONTACT` (409);
2264
+ * nothing is sent.
1919
2265
  */
1920
2266
  addReaction(postId, commentId, body) {
1921
2267
  return this.ctx.request({
@@ -2344,6 +2690,9 @@ var ConnectRequestsResource = class {
2344
2690
  * A reconnect that signs in to a different LinkedIn account fails with
2345
2691
  * `LINKEDIN_ACCOUNT_MISMATCH` and leaves the account unchanged.
2346
2692
  *
2693
+ * `warm_up: false` connects the new account with warm-up off (default
2694
+ * `true`); with `account_id` it is `400 INVALID_REQUEST`.
2695
+ *
2347
2696
  * @example
2348
2697
  * const req = await curviate.connectRequests.create();
2349
2698
  * console.log(`Open this link to connect your LinkedIn account: ${req.url}`);
@@ -2392,11 +2741,23 @@ var TeamsResource = class {
2392
2741
  this.ctx = ctx;
2393
2742
  }
2394
2743
  /**
2395
- * List the teams you belong to, with your role in each.
2744
+ * List the teams you belong to, with your role in each. `is_current` marks
2745
+ * the team this credential acts in; `logo_url` is the team's public PNG or
2746
+ * `null`. Pages with an opaque `cursor` until it is `null`.
2747
+ *
2748
+ * `include: "counts"` adds to each row `tier` (or the Tier of a seat lent to
2749
+ * it; `null` with no subscription), `trial: { days_left }` while a trial is
2750
+ * all the team has, `accounts` and `active_motions` (what you would read in
2751
+ * that team), and `seats: { used, total, lent_out, lent_in }` on rows where
2752
+ * you are an Owner. Which rows get counts depends on the credential: a
2753
+ * dashboard session and an All-Teams key get them on every row; a Team API
2754
+ * key, or a token for one Team, only on its own row (the other rows come
2755
+ * back without them).
2396
2756
  * `GET /v1/teams`
2397
2757
  *
2398
2758
  * @example
2399
2759
  * const { items } = await curviate.teams.list();
2760
+ * const withCounts = await curviate.teams.list({ include: "counts" });
2400
2761
  */
2401
2762
  list(params) {
2402
2763
  return this.ctx.request({
@@ -2406,8 +2767,27 @@ var TeamsResource = class {
2406
2767
  });
2407
2768
  }
2408
2769
  /**
2409
- * The team this credential acts in: its name, your role, and the Owner who
2410
- * receives invoices.
2770
+ * Create a team with you as its Owner and the Owner who receives its
2771
+ * invoices. It has no subscription and no seat yet. Any role may call it,
2772
+ * and two identical calls create two teams. The result is the
2773
+ * {@link TeamsResource.list} item shape plus `next_step`, which depends on
2774
+ * the credential: an All-Teams key gets `{ team_header }`, the header line
2775
+ * to send (`X-Curviate-Team: <team_id>`, usable at once; with this SDK pass
2776
+ * the `team_id` to `curviate.withTeam(team_id)`); any other credential gets
2777
+ * `{ command, url }` (the CLI command that sets up a key in the new team,
2778
+ * and the dashboard page with your teams).
2779
+ * `POST /v1/teams`
2780
+ *
2781
+ * @example
2782
+ * const { team_id, next_step } = await curviate.teams.create({ name: "Agency client A" });
2783
+ * if (next_step?.command) console.log(next_step.command); // set up a key for the new team
2784
+ */
2785
+ create(body) {
2786
+ return this.ctx.request({ method: "POST", path: "/v1/teams", body });
2787
+ }
2788
+ /**
2789
+ * The team this credential acts in: its name, your role, its `logo_url`, and
2790
+ * the Owner who receives invoices.
2411
2791
  * `GET /v1/teams/current`
2412
2792
  *
2413
2793
  * @example
@@ -2417,8 +2797,9 @@ var TeamsResource = class {
2417
2797
  return this.ctx.request({ method: "GET", path: "/v1/teams/current" });
2418
2798
  }
2419
2799
  /**
2420
- * Rename the current team, or set `billing_owner_user_id` (an Owner of this
2421
- * team, whose email receives invoices). Owners only.
2800
+ * Rename the current team (Owners and Managers), or set
2801
+ * `billing_owner_user_id` (an Owner of this team, whose email receives
2802
+ * invoices; Owners only).
2422
2803
  * `PATCH /v1/teams/current`
2423
2804
  *
2424
2805
  * @example
@@ -2432,7 +2813,37 @@ var TeamsResource = class {
2432
2813
  });
2433
2814
  }
2434
2815
  /**
2435
- * List the current team's members and their roles.
2816
+ * Set the current team's logo from a PNG of at most 1 MB. The file's own
2817
+ * bytes are checked, whatever its content type: a file that is not a PNG
2818
+ * throws `UNSUPPORTED_MEDIA_TYPE` (415) and a larger one `PAYLOAD_TOO_LARGE`
2819
+ * (413). Replaces the previous logo and resolves with the team, whose
2820
+ * `logo_url` is the new public PNG URL. Owners and Managers.
2821
+ * `PUT /v1/teams/current/logo`
2822
+ *
2823
+ * @example
2824
+ * await curviate.teams.setLogo(await fs.promises.readFile("logo.png"));
2825
+ */
2826
+ setLogo(png) {
2827
+ return this.ctx.request({
2828
+ method: "PUT",
2829
+ path: "/v1/teams/current/logo",
2830
+ rawBody: { data: png, contentType: "image/png" }
2831
+ });
2832
+ }
2833
+ /**
2834
+ * Remove the current team's logo; the team resolves with `logo_url: null`.
2835
+ * Owners and Managers.
2836
+ * `DELETE /v1/teams/current/logo`
2837
+ *
2838
+ * @example
2839
+ * await curviate.teams.removeLogo();
2840
+ */
2841
+ removeLogo() {
2842
+ return this.ctx.request({ method: "DELETE", path: "/v1/teams/current/logo" });
2843
+ }
2844
+ /**
2845
+ * List the current team's members and their roles. A member's `name` is
2846
+ * their name, else their email (`null` only when neither is known).
2436
2847
  * `GET /v1/teams/current/members`
2437
2848
  *
2438
2849
  * @example
@@ -2463,7 +2874,7 @@ var TeamsResource = class {
2463
2874
  });
2464
2875
  }
2465
2876
  /**
2466
- * Remove a member (Owners only), or pass `"me"` to leave the team yourself.
2877
+ * Remove a member (an Owner removes anyone, a Manager removes Members), or pass `"me"` to leave the team yourself.
2467
2878
  * Their API key and connectors for this team stop working at once. Same
2468
2879
  * refusals as {@link TeamsResource.updateMember}.
2469
2880
  * `DELETE /v1/teams/current/members/{user_id}`
@@ -2481,7 +2892,8 @@ var TeamsResource = class {
2481
2892
  * Invite 1 to 20 people by email, each with a role (`member` by default).
2482
2893
  * Each gets a link valid for 7 days. All or nothing: if any row is refused
2483
2894
  * (`ALREADY_MEMBER`, `INVITE_ALREADY_PENDING`, `INVITE_LIMIT_REACHED`),
2484
- * nothing is created or sent and the error names the row. Owners only.
2895
+ * nothing is created or sent and the error names the row. Owners and Managers
2896
+ * (a Manager invites Members only).
2485
2897
  * `POST /v1/teams/current/invites`
2486
2898
  *
2487
2899
  * @example
@@ -2495,7 +2907,7 @@ var TeamsResource = class {
2495
2907
  });
2496
2908
  }
2497
2909
  /**
2498
- * List the current team's invites, optionally by `status`.
2910
+ * List the current team's invites, optionally by `status`. Owners and Managers.
2499
2911
  * `GET /v1/teams/current/invites`
2500
2912
  *
2501
2913
  * @example
@@ -2510,7 +2922,8 @@ var TeamsResource = class {
2510
2922
  }
2511
2923
  /**
2512
2924
  * 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`.
2925
+ * An accepted or revoked one is refused with `INVITE_NOT_PENDING`. Owners,
2926
+ * and Managers for Member invites.
2514
2927
  * `POST /v1/teams/current/invites/{invite_id}/resend`
2515
2928
  *
2516
2929
  * @example
@@ -2523,7 +2936,8 @@ var TeamsResource = class {
2523
2936
  });
2524
2937
  }
2525
2938
  /**
2526
- * Revoke an invite; its link stops working.
2939
+ * Revoke an invite; its link stops working. Owners, and Managers for Member
2940
+ * invites.
2527
2941
  * `DELETE /v1/teams/current/invites/{invite_id}`
2528
2942
  *
2529
2943
  * @example
@@ -2537,17 +2951,6 @@ var TeamsResource = class {
2537
2951
  }
2538
2952
  };
2539
2953
 
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
2954
  // src/resources/contacts.ts
2552
2955
  var ContactImportsResource = class {
2553
2956
  constructor(ctx) {
@@ -2632,23 +3035,33 @@ var ContactsResource = class {
2632
3035
  this.imports = new ContactImportsResource(ctx);
2633
3036
  }
2634
3037
  /**
2635
- * List Contacts, newest first (`order: "asc"` for oldest). Filters combine:
2636
- * `list_id`, `tag` (comma list, any of them), `import_id`, `linkedin_url`,
3038
+ * List Contacts, newest first (`order: "asc"` for oldest). Filters combine
3039
+ * with AND: `list_id` (up to 20 Contact lists, as an array or a comma list:
3040
+ * Contacts in any of them, or in every one with `list_match: "all"`; one id
3041
+ * is one list), `not_list_id` (Contacts in none of those lists), `has_email`
3042
+ * (`true`: only Contacts with an email address, `false`: only those
3043
+ * without), `tag` (comma list, any of them), `import_id`, `linkedin_url`,
2637
3044
  * `q` (substring over names, headline, job title, company, email,
2638
3045
  * 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`.
3046
+ * that account, or of any of up to 50, as an array or a comma list), `motion_id` + `enrollment_status` (any of the statuses
3047
+ * given). A list id that is not your team's throws `RESOURCE_NOT_FOUND`
3048
+ * naming the key. Pages with an opaque `cursor` until it is `null`.
3049
+ * `include_total: true` adds `total` to the page, the number of Contacts the
3050
+ * filters match (with `limit: 1` it is the count before a bulk call or a new
3051
+ * list); it is left out unless asked for.
2642
3052
  * `GET /v1/contacts`
2643
3053
  *
2644
3054
  * @example
2645
3055
  * const { items, cursor } = await curviate.contacts.list({ tag: "warm", limit: 100 });
3056
+ *
3057
+ * @example
3058
+ * const { total } = await curviate.contacts.list({ list_id: ["cl_1", "cl_2"], list_match: "all", has_email: true, include_total: true, limit: 1 });
2646
3059
  */
2647
3060
  list(params) {
2648
3061
  return this.ctx.request({
2649
3062
  method: "GET",
2650
3063
  path: "/v1/contacts",
2651
- query: commaLists(params, "enrollment_status")
3064
+ query: commaLists(params, "enrollment_status", "list_id", "not_list_id", "account_id")
2652
3065
  });
2653
3066
  }
2654
3067
  /**
@@ -2721,12 +3134,22 @@ var ContactsResource = class {
2721
3134
  });
2722
3135
  }
2723
3136
  /**
2724
- * Delete up to 1,000 Contacts. Ids that are not your team's come back in
2725
- * `not_found`. Owners and Managers only.
3137
+ * Delete up to 1,000 Contacts by `contact_ids`, or every Contact a `filter`
3138
+ * matches (up to 10,000). Send exactly one of the two. `filter` takes the
3139
+ * {@link ContactsResource.list} filters as JSON, comma lists as arrays
3140
+ * (`list_id`, `not_list_id`, `list_match`, `has_email`, `tag`, ...); a
3141
+ * filter matching more than 10,000 Contacts throws `INVALID_REQUEST`
3142
+ * naming the count and deletes nothing, so count first with
3143
+ * `list({ ...filter, include_total: true, limit: 1 })`. Ids that are not your
3144
+ * team's come back in `not_found` (always empty with a filter). Owners and
3145
+ * Managers only.
2726
3146
  * `POST /v1/contacts/delete`
2727
3147
  *
2728
3148
  * @example
2729
3149
  * const { deleted, not_found } = await curviate.contacts.deleteMany({ contact_ids: ["con_1", "con_2"] });
3150
+ *
3151
+ * @example
3152
+ * await curviate.contacts.deleteMany({ filter: { list_id: ["cl_1"], has_email: false } });
2730
3153
  */
2731
3154
  deleteMany(body) {
2732
3155
  return this.ctx.request({
@@ -2738,13 +3161,17 @@ var ContactsResource = class {
2738
3161
  /**
2739
3162
  * Add or remove Tags and list memberships on up to 1,000 Contacts at once:
2740
3163
  * `add_tags`, `remove_tags`, `add_list_id` or `add_list_name` (created if
2741
- * new), `remove_list_id`.
3164
+ * new), `remove_list_id`. Name the Contacts with exactly one of
3165
+ * `contact_ids` or `filter`: every Contact the {@link ContactsResource.list}
3166
+ * filters match (comma lists as arrays), up to 10,000. A filter over the
3167
+ * cap throws `INVALID_REQUEST` naming the count and changes nothing.
2742
3168
  * `POST /v1/contacts/update`
2743
3169
  *
2744
3170
  * Needs Automate: a Team below that Tier is refused with `TIER_REQUIRED`.
2745
3171
  *
2746
3172
  * @example
2747
3173
  * await curviate.contacts.updateMany({ contact_ids: ["con_1", "con_2"], add_tags: ["vip"], add_list_name: "Q4" });
3174
+ * await curviate.contacts.updateMany({ filter: { tag: ["warm"], has_email: true }, add_list_name: "Warm with email" });
2748
3175
  */
2749
3176
  updateMany(body) {
2750
3177
  return this.ctx.request({
@@ -2778,7 +3205,7 @@ var ContactsResource = class {
2778
3205
  return this.ctx.request({
2779
3206
  method: "GET",
2780
3207
  path: "/v1/contacts/export",
2781
- query: commaLists(params, "enrollment_status")
3208
+ query: commaLists(params, "enrollment_status", "list_id", "not_list_id", "account_id")
2782
3209
  });
2783
3210
  }
2784
3211
  };
@@ -2789,13 +3216,20 @@ var ContactListsResource = class {
2789
3216
  this.ctx = ctx;
2790
3217
  }
2791
3218
  /**
2792
- * Create an empty Contact list.
3219
+ * Create a static Contact list: empty, or with `from`, a filter as in
3220
+ * `contacts.updateMany({ filter })`, holding every Contact it matches right
3221
+ * now (up to 10,000; more throws `INVALID_REQUEST` naming the count and
3222
+ * creates nothing). `from.list_id` of two lists combines them (a Contact in
3223
+ * both counts once), `list_match: "all"` keeps those in both and
3224
+ * `not_list_id` leaves some out. The lists named in `from` are not changed,
3225
+ * and `contact_count` is how many Contacts matched.
2793
3226
  * `POST /v1/contact-lists`
2794
3227
  *
2795
3228
  * Needs Automate: a Team below that Tier is refused with `TIER_REQUIRED`.
2796
3229
  *
2797
3230
  * @example
2798
3231
  * const list = await curviate.contactLists.create({ name: "Q4 webinar" });
3232
+ * const both = await curviate.contactLists.create({ name: "Both", from: { list_id: ["cl_1", "cl_2"], list_match: "all" } });
2799
3233
  */
2800
3234
  create(body) {
2801
3235
  return this.ctx.request({
@@ -2922,7 +3356,7 @@ var TagsResource = class {
2922
3356
 
2923
3357
  // src/sequence/order.ts
2924
3358
  var DOC = ["$schema", "on_reply", "steps"];
2925
- var STEP = ["id", "do", "wait", "until", "if", "emit", "tag", "timeout", "variants", "with", "event", "then", "else", "next", "note"];
3359
+ var STEP = ["id", "do", "wait", "until", "if", "emit", "tag", "timeout", "save", "save_to_contact", "variants", "with", "event", "then", "else", "next", "note"];
2926
3360
  var WITH = ["post", "newer_than", "reaction", "subject", "text", "note", "note_fallback", "fallback", "withdraw_after"];
2927
3361
  var VARIANT = ["id", "subject", "text"];
2928
3362
  var FALLBACK = ["subject", "text"];
@@ -2957,7 +3391,7 @@ var schema_v1_default = {
2957
3391
  $schema: "https://json-schema.org/draft/2020-12/schema",
2958
3392
  $id: "https://docs.curviate.com/schemas/sequence/v1.json",
2959
3393
  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.',
3394
+ 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
3395
  type: "object",
2962
3396
  additionalProperties: false,
2963
3397
  required: [
@@ -2972,7 +3406,7 @@ var schema_v1_default = {
2972
3406
  on_reply: {
2973
3407
  type: "string",
2974
3408
  pattern: "^(end|[a-z][a-z0-9_]{0,39})$",
2975
- description: "Where a reply interrupts to. Used only while stop_on_engagement is false."
3409
+ description: "Where a reply interrupts to."
2976
3410
  },
2977
3411
  steps: {
2978
3412
  type: "array",
@@ -3700,6 +4134,52 @@ var schema_v1_default = {
3700
4134
  pattern: "^[1-9][0-9]*(m|h|d)$",
3701
4135
  description: "Give up waiting after about this long (1h to 180d; a small random spread is added, never subtracted)."
3702
4136
  },
4137
+ save: {
4138
+ type: "object",
4139
+ minProperties: 1,
4140
+ maxProperties: 20,
4141
+ propertyNames: {
4142
+ pattern: "^[a-z][a-z0-9_]{0,63}$",
4143
+ not: {
4144
+ const: "id"
4145
+ }
4146
+ },
4147
+ additionalProperties: {
4148
+ type: "string",
4149
+ minLength: 1,
4150
+ maxLength: 256,
4151
+ description: 'A field path into the incoming payload: dots, items[0] and ["a.b"].'
4152
+ },
4153
+ 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."
4154
+ },
4155
+ save_to_contact: {
4156
+ type: "object",
4157
+ minProperties: 1,
4158
+ maxProperties: 20,
4159
+ propertyNames: {
4160
+ pattern: "^(email|[A-Za-z0-9_]{1,64})$",
4161
+ not: {
4162
+ enum: [
4163
+ "full_name",
4164
+ "first_name",
4165
+ "last_name",
4166
+ "headline",
4167
+ "job_title",
4168
+ "company",
4169
+ "location",
4170
+ "linkedin_url",
4171
+ "about"
4172
+ ]
4173
+ }
4174
+ },
4175
+ additionalProperties: {
4176
+ type: "string",
4177
+ minLength: 1,
4178
+ maxLength: 256,
4179
+ description: 'A field path into the incoming payload: dots, items[0] and ["a.b"].'
4180
+ },
4181
+ 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."
4182
+ },
3703
4183
  then: {
3704
4184
  type: "string",
3705
4185
  pattern: "^(end|[a-z][a-z0-9_]{0,39})$",
@@ -4325,12 +4805,73 @@ var schema_v1_default = {
4325
4805
  },
4326
4806
  exists: {
4327
4807
  type: "boolean"
4808
+ },
4809
+ gte: {
4810
+ type: "number"
4811
+ },
4812
+ lte: {
4813
+ type: "number"
4328
4814
  }
4329
4815
  },
4330
- description: "Exactly one operator. Strings compare trimmed, case-insensitive."
4816
+ description: "Exactly one operator. Strings compare trimmed, case-insensitive; gte and lte compare a number, or text that reads as one."
4331
4817
  }
4332
4818
  }
4333
4819
  },
4820
+ {
4821
+ type: "object",
4822
+ minProperties: 1,
4823
+ maxProperties: 1,
4824
+ additionalProperties: false,
4825
+ patternProperties: {
4826
+ "^enrollment\\.(?!id$)[a-z][a-z0-9_]{0,63}$": {
4827
+ type: "object",
4828
+ minProperties: 1,
4829
+ maxProperties: 1,
4830
+ additionalProperties: false,
4831
+ properties: {
4832
+ eq: {
4833
+ type: [
4834
+ "string",
4835
+ "number",
4836
+ "boolean"
4837
+ ]
4838
+ },
4839
+ neq: {
4840
+ type: [
4841
+ "string",
4842
+ "number",
4843
+ "boolean"
4844
+ ]
4845
+ },
4846
+ in: {
4847
+ type: "array",
4848
+ minItems: 1,
4849
+ items: {
4850
+ type: [
4851
+ "string",
4852
+ "number",
4853
+ "boolean"
4854
+ ]
4855
+ }
4856
+ },
4857
+ contains: {
4858
+ type: "string"
4859
+ },
4860
+ exists: {
4861
+ type: "boolean"
4862
+ },
4863
+ gte: {
4864
+ type: "number"
4865
+ },
4866
+ lte: {
4867
+ type: "number"
4868
+ }
4869
+ },
4870
+ description: "Exactly one operator. Strings compare trimmed, case-insensitive; gte and lte compare a number, or text that reads as one."
4871
+ }
4872
+ },
4873
+ description: "if only: an Enrollment value a save of this Sequence writes. Unset is false, except exists:false."
4874
+ },
4334
4875
  {
4335
4876
  type: "object",
4336
4877
  additionalProperties: false,
@@ -4956,12 +5497,73 @@ var schema_v1_default = {
4956
5497
  },
4957
5498
  exists: {
4958
5499
  type: "boolean"
5500
+ },
5501
+ gte: {
5502
+ type: "number"
5503
+ },
5504
+ lte: {
5505
+ type: "number"
4959
5506
  }
4960
5507
  },
4961
- description: "Exactly one operator. Strings compare trimmed, case-insensitive."
5508
+ description: "Exactly one operator. Strings compare trimmed, case-insensitive; gte and lte compare a number, or text that reads as one."
4962
5509
  }
4963
5510
  }
4964
5511
  },
5512
+ {
5513
+ type: "object",
5514
+ minProperties: 1,
5515
+ maxProperties: 1,
5516
+ additionalProperties: false,
5517
+ patternProperties: {
5518
+ "^enrollment\\.(?!id$)[a-z][a-z0-9_]{0,63}$": {
5519
+ type: "object",
5520
+ minProperties: 1,
5521
+ maxProperties: 1,
5522
+ additionalProperties: false,
5523
+ properties: {
5524
+ eq: {
5525
+ type: [
5526
+ "string",
5527
+ "number",
5528
+ "boolean"
5529
+ ]
5530
+ },
5531
+ neq: {
5532
+ type: [
5533
+ "string",
5534
+ "number",
5535
+ "boolean"
5536
+ ]
5537
+ },
5538
+ in: {
5539
+ type: "array",
5540
+ minItems: 1,
5541
+ items: {
5542
+ type: [
5543
+ "string",
5544
+ "number",
5545
+ "boolean"
5546
+ ]
5547
+ }
5548
+ },
5549
+ contains: {
5550
+ type: "string"
5551
+ },
5552
+ exists: {
5553
+ type: "boolean"
5554
+ },
5555
+ gte: {
5556
+ type: "number"
5557
+ },
5558
+ lte: {
5559
+ type: "number"
5560
+ }
5561
+ },
5562
+ description: "Exactly one operator. Strings compare trimmed, case-insensitive; gte and lte compare a number, or text that reads as one."
5563
+ }
5564
+ },
5565
+ description: "if only: an Enrollment value a save of this Sequence writes. Unset is false, except exists:false."
5566
+ },
4965
5567
  {
4966
5568
  type: "object",
4967
5569
  additionalProperties: false,
@@ -5026,7 +5628,7 @@ var schema_v1_default = {
5026
5628
  emit: {
5027
5629
  type: "string",
5028
5630
  pattern: "^[a-z][a-z0-9_.]{0,63}$",
5029
- description: "Legacy: a custom event name. New documents put event on the step that matters."
5631
+ description: "Send event: the custom event name this step sends (motion.custom_event)."
5030
5632
  },
5031
5633
  next: {
5032
5634
  type: "string",
@@ -5094,16 +5696,86 @@ function deepFreeze(value) {
5094
5696
  }
5095
5697
  var SEQUENCE_SCHEMA_V1 = deepFreeze(schema_v1_default);
5096
5698
 
5097
- // src/sequence/constants.ts
5699
+ // src/shared/sequence/limits.ts
5700
+ var SEQUENCE_SCHEMA_URL2 = "https://docs.curviate.com/schemas/sequence/v1.json";
5701
+ var MAX_STEPS = 100;
5702
+ var MIN_VARIANTS = 2;
5703
+ var MAX_VARIANTS = 2;
5704
+ var DEPRECATED_ACCEPTED_UNTIL = "2027-01-19";
5705
+ var MAX_VARIANTS_DEPRECATED = 5;
5706
+ var MAX_SEQUENCE_BYTES = 256 * 1024;
5707
+ var MAX_STEP_NOTE_CHARS = 500;
5708
+ var MAX_TAG_CHARS = 64;
5709
+ var MAX_PREDICATE_DEPTH = 10;
5710
+ var MAX_VARIABLE_REFS_PER_TEXT = 50;
5711
+ var MAX_QUOTED_TOKEN_CHARS = 64;
5712
+ var ID_PATTERN = "^[a-z][a-z0-9_]{0,39}$";
5713
+ var EVENT_NAME_PATTERN = "^[a-z][a-z0-9_.]{0,63}$";
5714
+ var EXTRA_KEY_PATTERN = "^[A-Za-z0-9_]{1,64}$";
5715
+ var SAVE_NAME_PATTERN = "^[a-z][a-z0-9_]{0,63}$";
5716
+ var MAX_SAVE_ENTRIES = 20;
5717
+ var DURATION_PATTERN = "^[1-9][0-9]*(m|h|d)$";
5718
+ var TAG_PATTERN = "^[^,;]*[^,;\\s][^,;]*$";
5098
5719
  var END = "end";
5720
+ var M = 1;
5099
5721
  var H = 60;
5100
- var D = 24 * H;
5722
+ var D = 24 * 60;
5101
5723
  var DURATION_BOUNDS = {
5102
- wait: { min: 1, max: 365 * D, label: "1m and 365d", example: "1d" },
5724
+ wait: { min: 1 * M, max: 365 * D, label: "1m and 365d", example: "1d" },
5103
5725
  timeout: { min: 1 * H, max: 180 * D, label: "1h and 180d", example: "21d" },
5104
5726
  withdraw_after: { min: 14 * D, max: 180 * D, label: "14d and 180d", example: "21d" },
5105
5727
  newer_than: { min: 1 * H, max: 365 * D, label: "1h and 365d", example: "30d" }
5106
5728
  };
5729
+ var STEP_KINDS = ["do", "wait", "until", "if", "emit", "tag"];
5730
+ var ACTIONS = [
5731
+ "profile.view",
5732
+ "profile.follow",
5733
+ "profile.unfollow",
5734
+ "invite.send",
5735
+ "invite.withdraw",
5736
+ "message.send",
5737
+ "inmail.send",
5738
+ "post.react",
5739
+ "post.comment",
5740
+ "skill.endorse"
5741
+ ];
5742
+ var ACTION_PARAMS = {
5743
+ "profile.view": [],
5744
+ "profile.follow": [],
5745
+ "profile.unfollow": [],
5746
+ "invite.send": ["note", "note_fallback", "withdraw_after"],
5747
+ "invite.withdraw": [],
5748
+ "message.send": ["text", "fallback"],
5749
+ "inmail.send": ["subject", "text", "fallback"],
5750
+ "post.react": ["post", "newer_than", "reaction"],
5751
+ "post.comment": ["post", "newer_than", "text", "fallback"],
5752
+ "skill.endorse": []
5753
+ };
5754
+ var MESSAGING_ACTIONS = ["invite.send", "message.send", "inmail.send", "post.comment"];
5755
+ var REACTIONS = ["like", "celebrate", "support", "love", "insightful", "funny"];
5756
+ var EVENTS = ["invite.accepted", "message.replied", "message.read"];
5757
+ var STANDARD_FIELDS = [
5758
+ "full_name",
5759
+ "first_name",
5760
+ "last_name",
5761
+ "headline",
5762
+ "job_title",
5763
+ "company",
5764
+ "email",
5765
+ "location",
5766
+ "linkedin_url",
5767
+ "about"
5768
+ ];
5769
+ var NAME_FIELDS = ["full_name", "first_name", "last_name"];
5770
+ var ACCOUNT_FIELDS = ["first_name", "last_name", "full_name"];
5771
+ var BOOLEAN_PREDICATES = [
5772
+ "contact.connected",
5773
+ "contact.open_profile",
5774
+ "contact.premium",
5775
+ "contact.has_photo"
5776
+ ];
5777
+ var FIELD_OPERATORS = ["eq", "neq", "in", "contains", "exists"];
5778
+ var NUMBER_OPERATORS = ["gte", "lte"];
5107
5779
  var TEXT_LIMITS = {
5108
5780
  message: 8e3,
5109
5781
  invite_note: 300,
@@ -5111,22 +5783,104 @@ var TEXT_LIMITS = {
5111
5783
  inmail_text: 1900,
5112
5784
  comment: 1250
5113
5785
  };
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";
5786
+ var STEP_KEYS = {
5787
+ do: ["id", "do", "with", "variants", "event", "next", "else", "note"],
5788
+ wait: ["id", "wait", "next", "note"],
5789
+ until: ["id", "until", "timeout", "save", "save_to_contact", "then", "else", "note"],
5790
+ if: ["id", "if", "then", "else", "note"],
5791
+ emit: ["id", "emit", "next", "note"],
5792
+ tag: ["id", "tag", "event", "next", "note"]
5793
+ };
5123
5794
  var SHORT_VARIABLE_PATTERN = "^[a-z][a-z0-9_]{0,63}$";
5795
+ var SHORT_RE = new RegExp(SHORT_VARIABLE_PATTERN);
5124
5796
  var MY_FIELDS = ["my_first_name", "my_last_name", "my_full_name"];
5797
+ function longVariable(inner) {
5798
+ if (inner.includes("."))
5799
+ return inner;
5800
+ if (!SHORT_RE.test(inner))
5801
+ return null;
5802
+ if (MY_FIELDS.includes(inner))
5803
+ return `account.${inner.slice(3)}`;
5804
+ if (inner.startsWith("my_"))
5805
+ return null;
5806
+ if (STANDARD_FIELDS.includes(inner))
5807
+ return `contact.${inner}`;
5808
+ return `contact.extra.${inner}`;
5809
+ }
5810
+ var isObject = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
5811
+ var fallbackKeyword = (action) => action === "invite.send" ? "drop" : action === "post.comment" ? "skip" : null;
5125
5812
 
5126
- // src/sequence/rules.ts
5127
- var SEQUENCE_RULES_DOCS_URL = "https://docs.curviate.com/reference/sequence-rules";
5128
- var HINTS = {
5129
- R1: () => "Keep only $schema, on_reply and steps at the top level; $schema, when present, is https://docs.curviate.com/schemas/sequence/v1.json.",
5813
+ // src/shared/sequence/paths.ts
5814
+ function durationMinutes(v) {
5815
+ const m = typeof v === "string" ? /^([1-9][0-9]*)(m|h|d)$/.exec(v) : null;
5816
+ if (!m)
5817
+ return 0;
5818
+ return Number(m[1]) * { m: 1, h: 60, d: 1440 }[m[2]];
5819
+ }
5820
+
5821
+ // src/shared/field-paths.ts
5822
+ var PATH_MAX = 256;
5823
+ var PLAIN = /^[A-Za-z0-9_$-]+$/;
5824
+ function parsePath(input) {
5825
+ const s = input.trim();
5826
+ const out = [];
5827
+ let i = 0;
5828
+ const key = () => {
5829
+ const m = /^[A-Za-z0-9_$-]+/.exec(s.slice(i));
5830
+ if (!m)
5831
+ return false;
5832
+ out.push(m[0]);
5833
+ i += m[0].length;
5834
+ return true;
5835
+ };
5836
+ if (s === "" || s[0] !== "[" && !key())
5837
+ return null;
5838
+ while (i < s.length) {
5839
+ if (s[i] === ".") {
5840
+ i += 1;
5841
+ if (!key())
5842
+ return null;
5843
+ } else if (s[i] === "[") {
5844
+ const idx = /^\[(\d{1,6})\]/.exec(s.slice(i));
5845
+ const str = /^\[("(?:[^"\\]|\\.)*")\]/.exec(s.slice(i));
5846
+ if (idx) {
5847
+ out.push(Number(idx[1]));
5848
+ i += idx[0].length;
5849
+ } else if (str) {
5850
+ try {
5851
+ out.push(JSON.parse(str[1]));
5852
+ } catch {
5853
+ return null;
5854
+ }
5855
+ i += str[0].length;
5856
+ } else
5857
+ return null;
5858
+ } else
5859
+ return null;
5860
+ }
5861
+ return out;
5862
+ }
5863
+ function formatPath(segs) {
5864
+ return segs.map((g, n) => typeof g === "number" ? `[${g}]` : PLAIN.test(g) ? n === 0 ? g : `.${g}` : `[${JSON.stringify(g)}]`).join("");
5865
+ }
5866
+ function normalizePath(input) {
5867
+ const segs = parsePath(input);
5868
+ if (segs === null)
5869
+ return null;
5870
+ const p = formatPath(segs);
5871
+ return p.length > PATH_MAX ? null : p;
5872
+ }
5873
+
5874
+ // src/shared/sequence/references.ts
5875
+ function awaitedEventName(until) {
5876
+ const m = typeof until === "string" ? /^(?:event|signal)\.(.+)$/s.exec(until) : null;
5877
+ return m ? m[1] : null;
5878
+ }
5879
+
5880
+ // src/shared/sequence/rules.ts
5881
+ var SEQUENCE_RULES_DOCS_URL = "https://docs.curviate.com/reference/sequence-rules";
5882
+ var HINTS = {
5883
+ R1: () => "Keep only $schema, on_reply and steps at the top level; $schema, when present, is https://docs.curviate.com/schemas/sequence/v1.json.",
5130
5884
  R2: () => "Keep the document to 1 to 100 steps and 256 KiB; split a longer flow into two Motions.",
5131
5885
  R3: () => "Give each step a unique id: lowercase letters, digits and _, starting with a letter, at most 40 characters, not end.",
5132
5886
  R4: () => "Give the step exactly one of do, wait, until, if, emit or tag.",
@@ -5145,203 +5899,124 @@ var HINTS = {
5145
5899
  R17: () => "Remove the part after | and add a variable-free fallback on the step instead.",
5146
5900
  R18: () => "Write a variable with double braces, like {{first_name}}, or remove the braces.",
5147
5901
  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.",
5902
+ R20: () => "Use a Contact variable in the comment, for example a custom field {{comment}} your AI writes per Contact.",
5149
5903
  R21: () => "Set withdraw_after between 14d and 180d, for example 21d.",
5150
5904
  R22: () => "Shorten the text to the limit (variables are not counted) or use fewer variables.",
5151
5905
  R23: () => "Give the step exactly 2 variants (A/B) with ids unique in the document, or one text in with.",
5152
5906
  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 ;.",
5907
+ 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.",
5908
+ R26: () => 'Write the path with dots, items[0] and ["a.b"], like lead.score, at most 256 characters.',
5909
+ 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>}}.",
5910
+ R28: () => 'Use gte or lte with a number, like {"gte": 50}, on enrollment.<key> or contact.extra.<key>.',
5911
+ R29: () => "Save a field inside the object or list, or compare that value with eq, neq or exists instead.",
5153
5912
  W1: () => "If the merge is intended, keep it; otherwise give the jumping step its own continuation.",
5913
+ W2: () => "Add a sending Account with Sales Navigator or Recruiter, or remove the InMail step.",
5914
+ W3: () => 'Set note_fallback to "drop" so invitations go on without a note once the noted ones are used up.',
5915
+ W4: () => "Wait for message.replied instead, or add a sending Account that does not use Recruiter.",
5154
5916
  W5: () => "Remove the follow step: the invitation already follows the person.",
5155
5917
  W6: () => "Send an invitation on this path before waiting for its acceptance.",
5156
5918
  W7: () => "Write the comment per Contact, for example in a custom field {{comment}}.",
5157
5919
  W8: () => "Keep the link for a later message.",
5158
5920
  W9: () => "Motion variables are deprecated: write the text in place, or use a custom field.",
5921
+ W10: () => "Retired: never emitted. The Sequence's on_reply decides what a reply does.",
5159
5922
  W11: (t) => `Did you mean ${t}?`,
5160
5923
  W12: () => "Send a message on this path before checking whether it was seen.",
5161
- W13: (t) => `Use ${t} instead.`
5924
+ W13: (t) => `Use ${t} instead.`,
5925
+ W14: () => "Use the value in a message or an if step, or remove it from save if no webhook template sends it.",
5926
+ W16: (t) => `Use ${t} to read the saved value.`
5162
5927
  };
5928
+ var SEQUENCE_RULE_CODES = Object.keys(HINTS);
5163
5929
  function ruleHelp(code, token = "") {
5164
5930
  const hint = HINTS[code];
5165
- if (!hint) throw new Error(`no hint for ${code}`);
5931
+ if (!hint)
5932
+ throw new Error(`no hint for ${code}`);
5166
5933
  return { hint: hint(token), docs_url: `${SEQUENCE_RULES_DOCS_URL}#${code.toLowerCase()}` };
5167
5934
  }
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;
5935
+ var PROBLEMS = {
5936
+ R1: { sentence: "The document has a top-level key the API does not take.", short: "Unknown key" },
5937
+ R2: { sentence: "The Sequence has too many steps or is too large.", short: "Too long" },
5938
+ R3: { sentence: "A step has a missing or repeated id.", short: "Needs an id" },
5939
+ R4: { sentence: "This step is not exactly one kind of step.", short: "Needs setup: pick a kind" },
5940
+ R5: { sentence: "This step has a setting its kind does not take.", short: "Unknown setting" },
5941
+ R6: { sentence: "This step leads to a step that does not exist.", short: "Leads nowhere" },
5942
+ R7: { sentence: "Choose where this step goes on its \u2713 branch.", short: "Needs setup: the \u2713 branch" },
5943
+ R8: { sentence: "These steps loop back on themselves.", short: "Loops back" },
5944
+ R9: { sentence: "Nothing leads to this step.", short: "Not reached" },
5945
+ R10: { sentence: "This path runs off the end.", short: "Needs an ending" },
5946
+ R11: { sentence: "When they reply leads to a step that does not exist.", short: "Leads nowhere" },
5947
+ R12: { sentence: "The delay or timeout is out of range.", short: "Needs setup: a duration" },
5948
+ R13: { sentence: "A setting of this step is missing or not valid.", short: "Needs setup" },
5949
+ R14: { sentence: "Choose what to wait for and for how long.", short: "Needs setup: what to wait for" },
5950
+ R15: { sentence: "The condition is not one Curviate can check.", short: "Needs setup: the condition" },
5951
+ R16: { sentence: "A variable is not one Curviate knows.", short: "Unknown variable" },
5952
+ R17: { sentence: "A variable carries a default or a filter; use a fallback instead.", short: "Variable with a default" },
5953
+ R18: { sentence: "Single braces are not a variable.", short: "Single-brace placeholder" },
5954
+ R19: { sentence: "Add a fallback for Contacts missing a field.", short: "Needs a fallback" },
5955
+ R20: { sentence: "A comment must use a Contact variable.", short: "Needs a Contact variable" },
5956
+ R21: { sentence: "Withdraw after must be between 14 and 180 days.", short: "Needs setup: withdraw after" },
5957
+ R22: { sentence: "The text is over its limit.", short: "Text too long" },
5958
+ R23: { sentence: "A/B needs exactly two variants.", short: "Needs setup: variants" },
5959
+ R24: { sentence: "The event or Tag name is not valid.", short: "Needs setup: a valid name" },
5960
+ R25: { sentence: "Saved values belong on a wait for an incoming event, at most 20.", short: "Needs setup: saved values" },
5961
+ R26: { sentence: "A field path is not valid.", short: "Needs setup: a field path" },
5962
+ R27: { sentence: "This uses a value no step saves.", short: "Value not saved" },
5963
+ R28: { sentence: "At least and at most compare numbers on saved values and custom fields.", short: "Needs setup: a number" },
5964
+ R29: { sentence: "The incoming sample has no plain value at this field.", short: "Not a plain value" },
5965
+ W1: { sentence: "Two paths merge here.", short: "Merge point" },
5966
+ W2: { sentence: "InMail needs a sending Account with Sales Navigator or Recruiter.", short: "No InMail Account" },
5967
+ W3: { sentence: "Basic Accounts send only a few invitation notes a month.", short: "Few notes on Basic" },
5968
+ W4: { sentence: "Read receipts are not available on Recruiter.", short: "No read receipts" },
5969
+ W5: { sentence: "The invitation already follows the person.", short: "Follows twice" },
5970
+ W6: { sentence: "This waits for acceptance with no invitation before it.", short: "No invitation before" },
5971
+ W7: { sentence: "The comment is personalised by name only.", short: "Name-only comment" },
5972
+ W8: { sentence: "A link in the first message gets fewer replies.", short: "Link in first message" },
5973
+ W9: { sentence: "Motion variables are deprecated.", short: "Motion variable" },
5974
+ W10: { sentence: "Retired: no longer reported.", short: "Retired" },
5975
+ W11: { sentence: "A custom field looks like a typo.", short: "Possible typo" },
5976
+ W12: { sentence: "This checks for a seen message before any message is sent.", short: "Nothing seen yet" },
5977
+ W13: { sentence: "This form is deprecated.", short: "Deprecated form" },
5978
+ W14: { sentence: "A saved value is never used.", short: "Unused value" },
5979
+ W16: { sentence: "This reads a Contact field, not the saved value.", short: "Contact field, not the value" }
5226
5980
  };
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
- }
5981
+ var SEQUENCE_PROBLEM_CODES = Object.keys(PROBLEMS);
5324
5982
 
5325
- // src/sequence/validate.ts
5326
- var issue = (ctx, i) => ctx.issues.push(i);
5327
- var warning = (ctx, i) => ctx.warnings.push(i);
5983
+ // src/shared/sequence/validate.ts
5984
+ function lintSequence(doc) {
5985
+ return run(doc, { kind: "lint" });
5986
+ }
5328
5987
  function jsonPointer(...tokens) {
5329
5988
  return tokens.map((t) => "/" + String(t).replace(/~/g, "~0").replace(/\//g, "~1")).join("");
5330
5989
  }
5331
- function entry(code, path, pointer, rule, token) {
5332
- return { path, rule, code, message: rule, pointer, ...ruleHelp(code, token) };
5990
+ function sequenceEntry(code, path, pointer, rule, token, candidates) {
5991
+ return {
5992
+ path,
5993
+ rule,
5994
+ code,
5995
+ message: rule,
5996
+ pointer,
5997
+ ...ruleHelp(code, token),
5998
+ severity: code.startsWith("W") ? "warning" : "error",
5999
+ ...candidates?.length ? { candidates } : {}
6000
+ };
5333
6001
  }
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));
6002
+ function sequenceSizeError(bytes) {
6003
+ return sequenceEntry("R2", "$", "", `the document is ${bytes} bytes; the limit is ${MAX_SEQUENCE_BYTES}, UTF-8 compact JSON`);
5336
6004
  }
5337
6005
  var UNTIL = `; accepted until ${DEPRECATED_ACCEPTED_UNTIL}`;
6006
+ var ID_RE = new RegExp(ID_PATTERN);
6007
+ var DURATION_RE = new RegExp(DURATION_PATTERN);
6008
+ var EVENT_RE = new RegExp(EVENT_NAME_PATTERN);
6009
+ var EXTRA_KEY_RE = new RegExp(EXTRA_KEY_PATTERN);
6010
+ var TAG_RE = new RegExp(TAG_PATTERN);
6011
+ var SAVE_NAME_RE = new RegExp(SAVE_NAME_PATTERN);
5338
6012
  var VARIABLE_RE = /\{\{([^{}]*)\}\}/g;
5339
6013
  var SINGLE_BRACE_RE = /\{[A-Za-z_][A-Za-z0-9_.]*\}/g;
6014
+ var KIND_LIST = STEP_KINDS.join(", ");
5340
6015
  function keysOf(o) {
5341
6016
  return Object.keys(o).filter((k) => o[k] !== void 0);
5342
6017
  }
5343
6018
  function has(o, k) {
5344
- return Object.prototype.hasOwnProperty.call(o, k) && o[k] !== void 0;
6019
+ return Object.hasOwn(o, k) && o[k] !== void 0;
5345
6020
  }
5346
6021
  function quote(token) {
5347
6022
  return [...token].slice(0, MAX_QUOTED_TOKEN_CHARS).join("");
@@ -5358,31 +6033,32 @@ function deeperThan(v, max) {
5358
6033
  for (let depth = 1; level.length; depth++) {
5359
6034
  const next = [];
5360
6035
  for (const x of level) {
5361
- if (typeof x === "object" && x !== null) for (const child of Object.values(x)) next.push(child);
6036
+ if (typeof x === "object" && x !== null)
6037
+ for (const child of Object.values(x))
6038
+ next.push(child);
5362
6039
  }
5363
- if (!next.length) return false;
5364
- if (depth > max) return true;
6040
+ if (!next.length)
6041
+ return false;
6042
+ if (depth > max)
6043
+ return true;
5365
6044
  level = next;
5366
6045
  }
5367
6046
  return false;
5368
6047
  }
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
6048
  function durationRule(bounds) {
5375
6049
  return `must be a duration between ${bounds.label}: a number then m, h or d, like 30d`;
5376
6050
  }
5377
6051
  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;
6052
+ function durationOk(v, bounds) {
6053
+ if (typeof v !== "string" || !DURATION_RE.test(v))
6054
+ return false;
5380
6055
  const m = durationMinutes(v);
5381
6056
  return m >= bounds.min && m <= bounds.max;
5382
6057
  }
5383
- function tagOk(ctx, v) {
5384
- return typeof v === "string" && [...v].length <= ctx.v.maxTagChars && ctx.re.tag.test(v);
6058
+ function tagOk(v) {
6059
+ return typeof v === "string" && [...v].length <= MAX_TAG_CHARS && TAG_RE.test(v);
5385
6060
  }
6061
+ var TAG_RULE = "a Tag name is 1 to 64 characters, without , or ;";
5386
6062
  function variablesIn(text) {
5387
6063
  return [...text.matchAll(VARIABLE_RE)].map((m) => ({ token: m[0], inner: m[1].trim() }));
5388
6064
  }
@@ -5392,44 +6068,34 @@ function hasVariable(text) {
5392
6068
  function staticLength(text) {
5393
6069
  return [...text.replace(VARIABLE_RE, "")].length;
5394
6070
  }
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
6071
  function distance(a, b, cap) {
5412
- if (Math.abs(a.length - b.length) > cap) return cap + 1;
6072
+ if (Math.abs(a.length - b.length) > cap)
6073
+ return cap + 1;
5413
6074
  let prev = Array.from({ length: b.length + 1 }, (_, j) => j);
5414
6075
  for (let i = 1; i <= a.length; i++) {
5415
6076
  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));
6077
+ for (let j = 1; j <= b.length; j++)
6078
+ cur[j] = Math.min(prev[j] + 1, cur[j - 1] + 1, prev[j - 1] + (a[i - 1] === b[j - 1] ? 0 : 1));
5417
6079
  prev = cur;
5418
6080
  }
5419
6081
  return prev[b.length];
5420
6082
  }
5421
- function likelyTypoOf(ctx, key) {
6083
+ var NEAR_NAMES = [...STANDARD_FIELDS, ...MY_FIELDS];
6084
+ function likelyTypoOf(key) {
5422
6085
  let best = null;
5423
6086
  let bestD = 3;
5424
- for (const n of [...ctx.v.standardFields, ...MY_FIELDS]) {
6087
+ for (const n of NEAR_NAMES) {
5425
6088
  const d = distance(key, n, 2);
5426
- if (d < bestD) [best, bestD] = [n, d];
6089
+ if (d < bestD)
6090
+ [best, bestD] = [n, d];
5427
6091
  }
5428
6092
  return best;
5429
6093
  }
6094
+ var issue = (ctx, i) => ctx.issues.push(i);
6095
+ var warning = (ctx, i) => ctx.warnings.push(i);
5430
6096
  function checkText(ctx, order, path, pointer, parent, value, limit) {
5431
6097
  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" });
6098
+ issue(ctx, { order, code: "R13", path, pointer: typeof value === "undefined" ? parent : pointer, rule: "must be non-empty text" });
5433
6099
  return null;
5434
6100
  }
5435
6101
  const at = { order, path, pointer };
@@ -5442,22 +6108,31 @@ function checkText(ctx, order, path, pointer, parent, value, limit) {
5442
6108
  issue(ctx, { ...at, code: "R17", rule: "variable defaults are not supported; add a fallback" });
5443
6109
  continue;
5444
6110
  }
5445
- if (v.inner.startsWith("motion.") && ctx.re.motion.test(v.inner.slice(7))) {
5446
- const key = v.inner.slice(7);
6111
+ const motion = /^motion\.([a-z][a-z0-9_]{0,39})$/.exec(v.inner);
6112
+ if (motion) {
5447
6113
  warning(ctx, {
5448
6114
  ...at,
5449
6115
  code: "W13",
5450
6116
  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}}}`
6117
+ token: `text in place or a custom field such as {{${motion[1]}}}`
5452
6118
  });
5453
- warning(ctx, { ...at, code: "W9", rule: `${quote(v.token)} is checked against settings.variables by the server only` });
6119
+ if (ctx.mode.kind === "lint") {
6120
+ warning(ctx, { ...at, code: "W9", rule: `${quote(v.token)} is checked against settings.variables by the server only` });
6121
+ } else if (!Object.hasOwn(ctx.mode.variables, motion[1])) {
6122
+ issue(ctx, { ...at, code: "R16", rule: `${quote(v.token)} is not defined in settings.variables` });
6123
+ }
6124
+ continue;
6125
+ }
6126
+ const ev = /^enrollment\.(.*)$/s.exec(v.inner);
6127
+ if (ev) {
6128
+ enrollmentRef(ctx, at, quote(v.token), ev[1], (k) => `{{${k}}}`);
5454
6129
  continue;
5455
6130
  }
5456
6131
  if (!v.inner.includes(".") && v.inner.startsWith("my_") && !MY_FIELDS.includes(v.inner)) {
5457
6132
  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
6133
  continue;
5459
6134
  }
5460
- if (!isKnownVariable(ctx, v.inner)) {
6135
+ if (!isKnownVariable(v.inner)) {
5461
6136
  issue(ctx, {
5462
6137
  ...at,
5463
6138
  code: "R16",
@@ -5465,9 +6140,15 @@ function checkText(ctx, order, path, pointer, parent, value, limit) {
5465
6140
  });
5466
6141
  continue;
5467
6142
  }
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 });
6143
+ const long = longVariable(v.inner);
6144
+ if (!v.inner.includes(".") && long.startsWith("contact.extra.") && ctx.values.saved.has(v.inner)) {
6145
+ const want = `{{enrollment.${v.inner}}}`;
6146
+ warning(ctx, { ...at, code: "W16", rule: `${quote(v.token)} is read as a Contact field; did you mean ${want}?`, token: want, candidates: [want] });
6147
+ }
6148
+ if (!v.inner.includes(".") && long.startsWith("contact.extra.")) {
6149
+ const near = likelyTypoOf(v.inner);
6150
+ if (near !== null)
6151
+ warning(ctx, { ...at, code: "W11", rule: `${quote(v.token)} is read as a custom field; did you mean ${near}?`, token: near });
5471
6152
  }
5472
6153
  }
5473
6154
  const rest = value.replace(VARIABLE_RE, " ");
@@ -5479,11 +6160,37 @@ function checkText(ctx, order, path, pointer, parent, value, limit) {
5479
6160
  }
5480
6161
  }
5481
6162
  const len = staticLength(value);
5482
- if (len > limit) issue(ctx, { ...at, code: "R22", rule: `${len} characters; the limit is ${limit}, variables not counted` });
6163
+ if (len > limit) {
6164
+ issue(ctx, { ...at, code: "R22", rule: `${len} characters; the limit is ${limit}, variables not counted` });
6165
+ }
5483
6166
  return value;
5484
6167
  }
6168
+ function enrollmentRef(ctx, at, shown, key, form) {
6169
+ if (key === "id") {
6170
+ issue(ctx, { ...at, code: "R27", rule: `${shown}: enrollment.id is reserved`, token: "{{enrollment_id}}", candidates: ["{{enrollment_id}}"] });
6171
+ return;
6172
+ }
6173
+ ctx.values.used.add(key);
6174
+ if (ctx.values.saved.has(key))
6175
+ return;
6176
+ 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}`));
6177
+ issue(ctx, { ...at, code: "R27", rule: `${shown}: no save of this Sequence writes ${quote(key)}`, ...candidates.length ? { token: candidates[0], candidates } : {} });
6178
+ }
6179
+ function isKnownVariable(inner) {
6180
+ const long = longVariable(inner);
6181
+ if (long === null)
6182
+ return false;
6183
+ if (long.startsWith("contact.extra."))
6184
+ return EXTRA_KEY_RE.test(long.slice("contact.extra.".length));
6185
+ if (long.startsWith("contact."))
6186
+ return STANDARD_FIELDS.includes(long.slice(8));
6187
+ if (long.startsWith("account."))
6188
+ return ACCOUNT_FIELDS.includes(long.slice(8));
6189
+ return false;
6190
+ }
5485
6191
  function checkFallbackText(ctx, order, path, pointer, parent, value, limit, keyword) {
5486
- if (keyword !== void 0 && value === keyword) return;
6192
+ if (keyword !== void 0 && value === keyword)
6193
+ return;
5487
6194
  const at = { order, code: "R19", path, pointer };
5488
6195
  if (typeof value !== "string") {
5489
6196
  issue(ctx, { ...at, pointer: value === void 0 ? parent : pointer, rule: keyword ? `must be text or "${keyword}"` : "must be text" });
@@ -5499,82 +6206,39 @@ function checkFallbackText(ctx, order, path, pointer, parent, value, limit, keyw
5499
6206
  }
5500
6207
  checkText(ctx, order, path, pointer, parent, value, limit);
5501
6208
  }
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);
6209
+ function entries(issues) {
6210
+ 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
6211
  }
5556
- function rules(doc, ctx) {
5557
- const { v } = ctx;
6212
+ function run(doc, mode) {
6213
+ const ctx = { mode, issues: [], warnings: [], values: { saved: /* @__PURE__ */ new Map(), toContact: /* @__PURE__ */ new Set(), used: /* @__PURE__ */ new Set() } };
5558
6214
  const DOC2 = -1;
5559
- if (!isObject(doc)) return { errors: [entry("R1", "$", "", "a Sequence document is a JSON object")], warnings: [] };
6215
+ if (!isObject(doc)) {
6216
+ return { errors: [sequenceEntry("R1", "$", "", "a Sequence document is a JSON object")], warnings: [] };
6217
+ }
5560
6218
  for (const k of keysOf(doc)) {
5561
6219
  if (k !== "$schema" && k !== "on_reply" && k !== "steps") {
5562
6220
  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
6221
  }
5564
6222
  }
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}` });
6223
+ if (has(doc, "$schema") && doc["$schema"] !== SEQUENCE_SCHEMA_URL2) {
6224
+ issue(ctx, { order: DOC2, code: "R1", path: "$schema", pointer: "/$schema", rule: `must be ${SEQUENCE_SCHEMA_URL2}` });
6225
+ }
6226
+ const bytes = compactBytes(doc);
6227
+ if (bytes > MAX_SEQUENCE_BYTES) {
6228
+ const e = sequenceSizeError(bytes);
6229
+ issue(ctx, { order: DOC2, code: e.code, path: e.path, pointer: e.pointer, rule: e.rule });
5567
6230
  }
5568
6231
  const rawSteps = doc["steps"];
5569
6232
  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}` });
6233
+ ctx.values = valuesOf(stepsArr);
6234
+ if (!Array.isArray(rawSteps) || rawSteps.length === 0) {
6235
+ issue(ctx, { order: DOC2, code: "R2", path: "steps", pointer: has(doc, "steps") ? "/steps" : "", rule: `required: an array of 1 to ${MAX_STEPS} steps` });
6236
+ } else if (rawSteps.length > MAX_STEPS) {
6237
+ issue(ctx, { order: DOC2, code: "R2", path: "steps", pointer: "/steps", rule: `${rawSteps.length} steps; the limit is ${MAX_STEPS}` });
5574
6238
  }
5575
6239
  const infos = [];
5576
6240
  const firstIndex = /* @__PURE__ */ new Map();
5577
- let structural = !Array.isArray(rawSteps) || rawSteps.length < v.minSteps || rawSteps.length > v.maxSteps;
6241
+ let structural = !Array.isArray(rawSteps) || rawSteps.length === 0 || rawSteps.length > MAX_STEPS;
5578
6242
  stepsArr.forEach((raw, index) => {
5579
6243
  const ptr = jsonPointer("steps", index);
5580
6244
  const info = { index, prefix: `steps[${index}]`, ptr, id: null, kind: null, raw: null };
@@ -5588,10 +6252,10 @@ function rules(doc, ctx) {
5588
6252
  const id = raw["id"];
5589
6253
  const idAt = { order: index, code: "R3", path: `${info.prefix}.id`, pointer: `${ptr}/id` };
5590
6254
  if (!has(raw, "id")) {
5591
- issue(ctx, { ...idAt, pointer: ptr, rule: `required: matching ${v.idPattern}` });
6255
+ issue(ctx, { ...idAt, pointer: ptr, rule: `required: matching ${ID_PATTERN}` });
5592
6256
  structural = true;
5593
- } else if (typeof id !== "string" || !ctx.re.id.test(id)) {
5594
- issue(ctx, { ...idAt, rule: `must match ${v.idPattern}` });
6257
+ } else if (typeof id !== "string" || !ID_RE.test(id)) {
6258
+ issue(ctx, { ...idAt, rule: `must match ${ID_PATTERN}` });
5595
6259
  structural = true;
5596
6260
  } else if (id === END) {
5597
6261
  issue(ctx, { ...idAt, rule: '"end" is reserved' });
@@ -5604,14 +6268,14 @@ function rules(doc, ctx) {
5604
6268
  info.id = id;
5605
6269
  info.prefix = id;
5606
6270
  }
5607
- const kinds = v.stepKinds.filter((k) => has(raw, k));
6271
+ const kinds = STEP_KINDS.filter((k) => has(raw, k));
5608
6272
  if (kinds.length !== 1) {
5609
6273
  issue(ctx, {
5610
6274
  order: index,
5611
6275
  code: "R4",
5612
6276
  path: info.prefix,
5613
6277
  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(", ")}`
6278
+ rule: kinds.length === 0 ? `a step needs exactly one of ${KIND_LIST}` : `a step has exactly one kind; found ${kinds.join(", ")}`
5615
6279
  });
5616
6280
  structural = true;
5617
6281
  return;
@@ -5633,11 +6297,16 @@ function rules(doc, ctx) {
5633
6297
  }
5634
6298
  const usedIds = new Set(firstIndex.keys());
5635
6299
  for (const info of infos) {
5636
- if (!info.raw || !info.kind) continue;
6300
+ if (!info.raw || !info.kind)
6301
+ continue;
5637
6302
  const { raw, kind, index: order, prefix: p, ptr } = info;
5638
- const keys = v.stepKeys[kind] ?? [];
5639
6303
  for (const k of keysOf(raw)) {
5640
- if (keys.includes(k)) continue;
6304
+ if (STEP_KEYS[kind].includes(k))
6305
+ continue;
6306
+ if (k === "save" || k === "save_to_contact") {
6307
+ issue(ctx, { order, code: "R25", path: `${p}.${k}`, pointer: `${ptr}/${k}`, rule: `${k} is allowed only on an until event.<name> step` });
6308
+ continue;
6309
+ }
5641
6310
  issue(ctx, {
5642
6311
  order,
5643
6312
  code: "R5",
@@ -5648,14 +6317,14 @@ function rules(doc, ctx) {
5648
6317
  }
5649
6318
  if (has(raw, "note")) {
5650
6319
  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` });
6320
+ if (typeof n !== "string" || [...n].length > MAX_STEP_NOTE_CHARS) {
6321
+ issue(ctx, { order, code: "R22", path: `${p}.note`, pointer: `${ptr}/note`, rule: `must be text of at most ${MAX_STEP_NOTE_CHARS} characters` });
5653
6322
  }
5654
6323
  }
5655
- if (has(raw, "event") && keys.includes("event")) {
6324
+ if (has(raw, "event") && STEP_KEYS[kind].includes("event")) {
5656
6325
  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` });
6326
+ if (typeof e !== "string" || !EVENT_RE.test(e)) {
6327
+ issue(ctx, { order, code: "R24", path: `${p}.event`, pointer: `${ptr}/event`, rule: `must match ${EVENT_NAME_PATTERN}, like invite_sent` });
5659
6328
  }
5660
6329
  }
5661
6330
  switch (kind) {
@@ -5663,7 +6332,7 @@ function rules(doc, ctx) {
5663
6332
  checkDo(ctx, order, p, ptr, raw, usedIds);
5664
6333
  break;
5665
6334
  case "wait":
5666
- if (!durationOk(ctx, raw["wait"], DURATION_BOUNDS.wait)) {
6335
+ if (!durationOk(raw["wait"], DURATION_BOUNDS.wait)) {
5667
6336
  issue(ctx, { order, code: "R12", path: `${p}.wait`, pointer: `${ptr}/wait`, rule: durationRule(DURATION_BOUNDS.wait), token: durationHint(DURATION_BOUNDS.wait) });
5668
6337
  }
5669
6338
  break;
@@ -5671,17 +6340,18 @@ function rules(doc, ctx) {
5671
6340
  checkUntil(ctx, order, p, ptr, raw);
5672
6341
  break;
5673
6342
  case "if":
5674
- checkPredicate(ctx, raw["if"], `${p}.if`, `${ptr}/if`, order);
6343
+ checkPredicate(raw["if"], `${p}.if`, `${ptr}/if`, false, ctx.issues, order, false, ctx);
5675
6344
  break;
5676
6345
  case "emit": {
5677
6346
  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` });
6347
+ if (typeof e !== "string" || !EVENT_RE.test(e)) {
6348
+ issue(ctx, { order, code: "R24", path: `${p}.emit`, pointer: `${ptr}/emit`, rule: `must match ${EVENT_NAME_PATTERN}, like reply.received` });
5680
6349
  }
5681
6350
  break;
5682
6351
  }
5683
6352
  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 ;" });
6353
+ if (!tagOk(raw["tag"]))
6354
+ issue(ctx, { order, code: "R24", path: `${p}.tag`, pointer: `${ptr}/tag`, rule: TAG_RULE });
5685
6355
  break;
5686
6356
  }
5687
6357
  if ((kind === "until" || kind === "if") && !has(raw, "then")) {
@@ -5689,7 +6359,8 @@ function rules(doc, ctx) {
5689
6359
  structural = true;
5690
6360
  }
5691
6361
  for (const f of ["then", "else", "next"]) {
5692
- if (!has(raw, f) || !keys.includes(f)) continue;
6362
+ if (!has(raw, f) || !STEP_KEYS[kind].includes(f))
6363
+ continue;
5693
6364
  const t = raw[f];
5694
6365
  const at = { order, code: "R6", path: `${p}.${f}`, pointer: `${ptr}/${f}` };
5695
6366
  if (typeof t !== "string") {
@@ -5701,7 +6372,24 @@ function rules(doc, ctx) {
5701
6372
  }
5702
6373
  }
5703
6374
  }
5704
- if (!structural) graph(ctx, infos, firstIndex, onReply);
6375
+ for (const info of infos) {
6376
+ const save = info.kind === "until" && awaitedEventName(info.raw["until"]) !== null ? info.raw["save"] : void 0;
6377
+ if (!isObject(save))
6378
+ continue;
6379
+ for (const name of keysOf(save)) {
6380
+ if (ctx.values.used.has(name) || name === "id" || !SAVE_NAME_RE.test(name))
6381
+ continue;
6382
+ warning(ctx, {
6383
+ order: info.index,
6384
+ code: "W14",
6385
+ path: `${info.prefix}.save.${quote(name)}`,
6386
+ pointer: `${info.ptr}/save${jsonPointer(name)}`,
6387
+ rule: `${quote(name)} is saved but nothing in this Sequence reads it; a webhook template may still send it`
6388
+ });
6389
+ }
6390
+ }
6391
+ if (!structural)
6392
+ graph(ctx, infos, firstIndex, onReply);
5705
6393
  return { errors: entries(ctx.issues), warnings: entries(ctx.warnings) };
5706
6394
  }
5707
6395
  function checkUntil(ctx, order, p, ptr, raw) {
@@ -5710,17 +6398,21 @@ function checkUntil(ctx, order, p, ptr, raw) {
5710
6398
  const custom = typeof ev === "string" ? /^(event|signal)\.(.*)$/s.exec(ev) : null;
5711
6399
  if (custom) {
5712
6400
  const [, ns, name] = custom;
5713
- if (!ctx.re.event.test(name)) {
5714
- issue(ctx, { ...at, rule: `an event name must match ${ctx.v.eventNamePattern}` });
6401
+ if (!EVENT_RE.test(name)) {
6402
+ issue(ctx, { ...at, rule: `an event name must match ${EVENT_NAME_PATTERN}` });
5715
6403
  } else if (ns === "signal") {
5716
6404
  warning(ctx, { ...at, code: "W13", rule: `until signal.${quote(name)} is now until event.${quote(name)}${UNTIL}`, token: `event.${quote(name)}` });
5717
6405
  }
5718
- } else if (!ctx.v.events.includes(ev)) {
5719
- issue(ctx, { ...at, rule: `unknown event; use ${ctx.v.events.join(", ")} or event.<name>` });
6406
+ } else if (!EVENTS.includes(ev)) {
6407
+ issue(ctx, { ...at, rule: "unknown event; use invite.accepted, message.replied, message.read or event.<name>" });
6408
+ }
6409
+ for (const key of ["save", "save_to_contact"]) {
6410
+ if (has(raw, key))
6411
+ checkSave(ctx, order, p, ptr, raw, key, custom && EVENT_RE.test(custom[2]) ? custom[2] : null);
5720
6412
  }
5721
6413
  if (!has(raw, "timeout")) {
5722
6414
  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)) {
6415
+ } else if (!durationOk(raw["timeout"], DURATION_BOUNDS.timeout)) {
5724
6416
  issue(ctx, {
5725
6417
  order,
5726
6418
  code: "R12",
@@ -5731,20 +6423,75 @@ function checkUntil(ctx, order, p, ptr, raw) {
5731
6423
  });
5732
6424
  }
5733
6425
  }
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;
6426
+ var SAVE_CONTACT_FIELDS = STANDARD_FIELDS.filter((f) => f !== "email");
6427
+ function checkSave(ctx, order, p, ptr, raw, key, event) {
6428
+ const v = raw[key];
6429
+ const at = { order, path: `${p}.${key}`, pointer: `${ptr}/${key}` };
6430
+ if (event === null) {
6431
+ issue(ctx, { ...at, code: "R25", rule: `${key} is allowed only on an until event.<name> step` });
6432
+ return;
6433
+ }
6434
+ if (!isObject(v) || keysOf(v).length === 0) {
6435
+ issue(ctx, { ...at, code: "R25", rule: `must be an object of 1 to ${MAX_SAVE_ENTRIES} entries` });
6436
+ return;
6437
+ }
6438
+ const names = keysOf(v);
6439
+ if (names.length > MAX_SAVE_ENTRIES)
6440
+ issue(ctx, { ...at, code: "R25", rule: `${names.length} entries; the limit is ${MAX_SAVE_ENTRIES}` });
6441
+ for (const name of names) {
6442
+ const e = { order, path: `${at.path}.${quote(name)}`, pointer: `${at.pointer}${jsonPointer(name)}` };
6443
+ if (key === "save") {
6444
+ if (name === "id")
6445
+ issue(ctx, { ...e, code: "R25", rule: '"id" is reserved; enrollment_id is always sent' });
6446
+ else if (!SAVE_NAME_RE.test(name))
6447
+ issue(ctx, { ...e, code: "R25", rule: `a value name must match ${SAVE_NAME_PATTERN}` });
6448
+ } else if (SAVE_CONTACT_FIELDS.includes(name)) {
6449
+ issue(ctx, { ...e, code: "R25", rule: `${name} is a standard Contact field; only email can be saved to one` });
6450
+ } else if (name !== "email" && !EXTRA_KEY_RE.test(name)) {
6451
+ issue(ctx, { ...e, code: "R25", rule: `a custom field key must match ${EXTRA_KEY_PATTERN}, or be email` });
6452
+ }
6453
+ const path = typeof v[name] === "string" ? normalizePath(v[name]) : null;
6454
+ if (path === null) {
6455
+ issue(ctx, { ...e, code: "R26", rule: 'not a field path; use dots, items[0] and ["a.b"], at most 256 characters, like lead.score' });
6456
+ continue;
6457
+ }
6458
+ const type = sampleType(ctx, event, path);
6459
+ if (type === "object" || type === "array") {
6460
+ issue(ctx, { ...e, code: "R29", rule: `${quote(path)} is an ${type} in the sample of event ${event}; save a field inside it` });
6461
+ }
6462
+ }
6463
+ }
6464
+ function sampleType(ctx, event, path) {
6465
+ if (ctx.mode.kind !== "server")
6466
+ return null;
6467
+ return ctx.mode.shapes[event]?.find((f) => f.path === path)?.type ?? null;
6468
+ }
6469
+ function valuesOf(steps) {
6470
+ const out = { saved: /* @__PURE__ */ new Map(), toContact: /* @__PURE__ */ new Set(), used: /* @__PURE__ */ new Set() };
6471
+ for (const s of steps) {
6472
+ if (!isObject(s))
6473
+ continue;
6474
+ const m = typeof s["until"] === "string" ? /^(?:event|signal)\.(.+)$/s.exec(s["until"]) : null;
6475
+ if (isObject(s["save"])) {
6476
+ for (const [name, raw] of Object.entries(s["save"])) {
6477
+ const path = typeof raw === "string" ? normalizePath(raw) : null;
6478
+ out.saved.set(name, [...out.saved.get(name) ?? [], ...m && path ? [{ event: m[1], path }] : []]);
6479
+ }
6480
+ }
6481
+ if (isObject(s["save_to_contact"]))
6482
+ for (const k of Object.keys(s["save_to_contact"]))
6483
+ out.toContact.add(k);
6484
+ }
6485
+ return out;
5739
6486
  }
5740
6487
  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(", ")}` });
6488
+ const action = raw["do"];
6489
+ if (!ACTIONS.includes(action)) {
6490
+ issue(ctx, { order, code: "R13", path: `${p}.do`, pointer: `${ptr}/do`, rule: `unknown action; use ${ACTIONS.join(", ")}` });
5745
6491
  return;
5746
6492
  }
5747
- const params = v.actionParams[a] ?? [];
6493
+ const a = action;
6494
+ const params = ACTION_PARAMS[a];
5748
6495
  const wp = `${ptr}/with`;
5749
6496
  const withParent = isObject(raw["with"]) ? wp : ptr;
5750
6497
  let w = {};
@@ -5766,14 +6513,16 @@ function checkDo(ctx, order, p, ptr, raw, usedIds) {
5766
6513
  }
5767
6514
  }
5768
6515
  }
5769
- const messaging = v.messagingActions.includes(a);
6516
+ const messaging = MESSAGING_ACTIONS.includes(a);
5770
6517
  const hasVariants = has(raw, "variants");
5771
6518
  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` });
6519
+ if (hasVariants && !messaging) {
6520
+ issue(ctx, { order, code: "R23", path: `${p}.variants`, pointer: vptr, rule: "variants are allowed on messaging steps only" });
5775
6521
  }
5776
- if (has(w, "newer_than") && params.includes("newer_than") && !durationOk(ctx, w["newer_than"], DURATION_BOUNDS.newer_than)) {
6522
+ if (has(w, "post") && params.includes("post") && w["post"] !== "latest") {
6523
+ issue(ctx, { order, code: "R13", path: `${p}.with.post`, pointer: `${wp}/post`, rule: 'only "latest" is supported' });
6524
+ }
6525
+ if (has(w, "newer_than") && params.includes("newer_than") && !durationOk(w["newer_than"], DURATION_BOUNDS.newer_than)) {
5777
6526
  issue(ctx, {
5778
6527
  order,
5779
6528
  code: "R12",
@@ -5783,25 +6532,27 @@ function checkDo(ctx, order, p, ptr, raw, usedIds) {
5783
6532
  token: durationHint(DURATION_BOUNDS.newer_than)
5784
6533
  });
5785
6534
  }
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(", ")}` });
6535
+ if (has(w, "reaction") && params.includes("reaction") && !REACTIONS.includes(w["reaction"])) {
6536
+ issue(ctx, { order, code: "R13", path: `${p}.with.reaction`, pointer: `${wp}/reaction`, rule: `must be one of ${REACTIONS.join(", ")}` });
5788
6537
  }
5789
- if (has(w, "withdraw_after") && params.includes("withdraw_after") && !durationOk(ctx, w["withdraw_after"], DURATION_BOUNDS.withdraw_after)) {
6538
+ if (has(w, "withdraw_after") && params.includes("withdraw_after") && !durationOk(w["withdraw_after"], DURATION_BOUNDS.withdraw_after)) {
5790
6539
  issue(ctx, { order, code: "R21", path: `${p}.with.withdraw_after`, pointer: `${wp}/withdraw_after`, rule: durationRule(DURATION_BOUNDS.withdraw_after) });
5791
6540
  }
5792
- if (!messaging) return;
6541
+ if (!messaging)
6542
+ return;
5793
6543
  const textKeys = a === "inmail.send" ? ["subject", "text"] : ["text"];
5794
6544
  const withTextKeys = a === "invite.send" ? ["note"] : textKeys;
5795
6545
  const variants = hasVariants ? raw["variants"] : void 0;
5796
6546
  if (hasVariants && withTextKeys.some((k) => has(w, k) && w[k] !== null)) {
5797
6547
  issue(ctx, { order, code: "R23", path: `${p}.variants`, pointer: vptr, rule: `use either variants or with.${withTextKeys.join(" and with.")}, not both` });
5798
6548
  }
6549
+ 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
6550
  const texts = [];
5800
6551
  if (hasVariants) {
5801
6552
  const n = Array.isArray(variants) ? variants.length : null;
5802
- if (n !== null && n > MAX_VARIANTS && n <= v.maxVariants) {
6553
+ if (n !== null && n > MAX_VARIANTS && n <= MAX_VARIANTS_DEPRECATED) {
5803
6554
  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) {
6555
+ } else if (n === null || n < MIN_VARIANTS || n > MAX_VARIANTS) {
5805
6556
  issue(ctx, {
5806
6557
  order,
5807
6558
  code: "R23",
@@ -5810,17 +6561,17 @@ function checkDo(ctx, order, p, ptr, raw, usedIds) {
5810
6561
  rule: `needs exactly ${MAX_VARIANTS} variants (A/B)${n === null ? "" : `; found ${n}`}`
5811
6562
  });
5812
6563
  }
5813
- (Array.isArray(variants) ? variants : []).forEach((variant, j) => {
6564
+ (Array.isArray(variants) ? variants : []).forEach((v, j) => {
5814
6565
  let vp = `${p}.variants[${j}]`;
5815
6566
  const vptrJ = `${vptr}/${j}`;
5816
- if (!isObject(variant)) {
6567
+ if (!isObject(v)) {
5817
6568
  issue(ctx, { order, code: "R23", path: vp, pointer: vptrJ, rule: "a variant is a JSON object" });
5818
6569
  return;
5819
6570
  }
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}` });
6571
+ const vid = v["id"];
6572
+ const idAt = { order, code: "R23", path: `${vp}.id`, pointer: has(v, "id") ? `${vptrJ}/id` : vptrJ };
6573
+ if (typeof vid !== "string" || !ID_RE.test(vid)) {
6574
+ issue(ctx, { ...idAt, rule: `must match ${ID_PATTERN}` });
5824
6575
  } else if (vid === END) {
5825
6576
  issue(ctx, { ...idAt, rule: '"end" is reserved' });
5826
6577
  } else if (usedIds.has(vid)) {
@@ -5829,14 +6580,15 @@ function checkDo(ctx, order, p, ptr, raw, usedIds) {
5829
6580
  usedIds.add(vid);
5830
6581
  vp = `${p}.variants.${vid}`;
5831
6582
  }
5832
- for (const k of keysOf(variant)) {
6583
+ for (const k of keysOf(v)) {
5833
6584
  if (k !== "id" && !textKeys.includes(k)) {
5834
6585
  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
6586
  }
5836
6587
  }
5837
6588
  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 });
6589
+ const value = checkText(ctx, order, `${vp}.${field}`, `${vptrJ}/${field}`, vptrJ, v[field], limitOf(field));
6590
+ if (value !== null)
6591
+ texts.push({ path: `${vp}.${field}`, pointer: `${vptrJ}/${field}`, field, value });
5840
6592
  }
5841
6593
  });
5842
6594
  } else {
@@ -5844,20 +6596,22 @@ function checkDo(ctx, order, p, ptr, raw, usedIds) {
5844
6596
  const fp = `${wp}/${field}`;
5845
6597
  if (a === "invite.send") {
5846
6598
  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 });
6599
+ const value = checkText(ctx, order, `${p}.with.note`, fp, withParent, w["note"], limitOf(field));
6600
+ if (value !== null)
6601
+ texts.push({ path: `${p}.with.note`, pointer: fp, field, value });
5849
6602
  }
5850
6603
  } else if (!has(w, field)) {
5851
6604
  issue(ctx, { order, code: "R13", path: `${p}.with.${field}`, pointer: withParent, rule: "required, or use variants" });
5852
6605
  } 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 });
6606
+ const value = checkText(ctx, order, `${p}.with.${field}`, fp, withParent, w[field], limitOf(field));
6607
+ if (value !== null)
6608
+ texts.push({ path: `${p}.with.${field}`, pointer: fp, field, value });
5855
6609
  }
5856
6610
  }
5857
6611
  }
5858
6612
  if (a === "post.comment") {
5859
6613
  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."));
6614
+ const contactVars = variablesIn(t.value).filter((v) => isKnownVariable(v.inner)).map((v) => longVariable(v.inner)).filter((l) => l.startsWith("contact."));
5861
6615
  if (contactVars.length === 0) {
5862
6616
  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
6617
  } else if (contactVars.every((l) => NAME_FIELDS.includes(l.slice(8)))) {
@@ -5874,8 +6628,11 @@ function checkDo(ctx, order, p, ptr, raw, usedIds) {
5874
6628
  const usesVariable = texts.some((t) => hasVariable(t.value));
5875
6629
  if (a === "invite.send") {
5876
6630
  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"' });
6631
+ if (has(w, "note_fallback")) {
6632
+ checkFallbackText(ctx, order, fp, `${wp}/note_fallback`, withParent, w["note_fallback"], TEXT_LIMITS.invite_note, fallbackKeyword(a) ?? void 0);
6633
+ } else if (usesVariable) {
6634
+ issue(ctx, { order, code: "R19", path: fp, pointer: withParent, rule: 'required because the note uses a variable: text, or "drop"' });
6635
+ }
5879
6636
  } else if (a === "inmail.send") {
5880
6637
  const fp = `${p}.with.fallback`;
5881
6638
  const fptr = `${wp}/fallback`;
@@ -5898,14 +6655,16 @@ function checkDo(ctx, order, p, ptr, raw, usedIds) {
5898
6655
  } else {
5899
6656
  const fp = `${p}.with.fallback`;
5900
6657
  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" });
6658
+ if (has(w, "fallback"))
6659
+ checkFallbackText(ctx, order, fp, `${wp}/fallback`, withParent, w["fallback"], limitOf("text"), fallbackKeyword(a) ?? void 0);
6660
+ else if (comment)
6661
+ issue(ctx, { order, code: "R19", path: fp, pointer: withParent, rule: 'required on a comment: text, or "skip"' });
6662
+ else if (usesVariable)
6663
+ issue(ctx, { order, code: "R19", path: fp, pointer: withParent, rule: "required because the text uses a variable" });
5904
6664
  }
5905
6665
  }
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 });
6666
+ function checkPredicate(pred, path, pointer, inWhere, out, order = 0, nested = false, ctx) {
6667
+ const push = (rule, code = "R15") => out.push({ order, code, path, pointer, rule });
5909
6668
  if (!nested && deeperThan(pred, MAX_PREDICATE_DEPTH)) {
5910
6669
  push(`a predicate nests at most ${MAX_PREDICATE_DEPTH} levels`);
5911
6670
  return;
@@ -5915,63 +6674,101 @@ function checkPredicate(ctx, pred, path, pointer, order, nested = false) {
5915
6674
  return;
5916
6675
  }
5917
6676
  const key = keysOf(pred)[0];
5918
- const val = pred[key];
6677
+ const v = pred[key];
5919
6678
  if (key === "not") {
5920
- if (nested) push("not cannot wrap another not");
5921
- else checkPredicate(ctx, val, `${path}.not`, `${pointer}/not`, order, true);
6679
+ if (nested)
6680
+ push("not cannot wrap another not");
6681
+ else
6682
+ checkPredicate(v, `${path}.not`, `${pointer}/not`, inWhere, out, order, true, ctx);
5922
6683
  return;
5923
6684
  }
5924
6685
  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");
6686
+ if (inWhere)
6687
+ push("account.* predicates are not allowed in where");
6688
+ else if (key !== "account.premium")
6689
+ push(`unknown predicate ${quote(key)}`);
6690
+ else if (typeof v !== "boolean")
6691
+ push("account.premium takes true or false");
5927
6692
  return;
5928
6693
  }
5929
6694
  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");
6695
+ if (inWhere)
6696
+ push("message.* predicates are not allowed in where");
6697
+ else if (key !== "message.seen")
6698
+ push(`unknown predicate ${quote(key)}`);
6699
+ else if (typeof v !== "boolean")
6700
+ push("message.seen takes true or false");
5932
6701
  return;
5933
6702
  }
5934
- if (v.booleanPredicates.includes(key)) {
5935
- if (typeof val !== "boolean") push(`${key} takes true or false`);
6703
+ if (BOOLEAN_PREDICATES.includes(key)) {
6704
+ if (typeof v !== "boolean")
6705
+ push(`${key} takes true or false`);
5936
6706
  return;
5937
6707
  }
5938
6708
  switch (key) {
5939
6709
  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');
6710
+ const deg = (n) => n === 1 || n === 2 || n === 3;
6711
+ 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));
6712
+ if (!ok2)
6713
+ push('contact.degree takes {"eq": n} or {"in": [n, ...]}, n being 1, 2 or 3');
5943
6714
  return;
5944
6715
  }
5945
6716
  case "contact.connections_gte":
5946
- if (!Number.isInteger(val) || val < v.connectionsMin) push("contact.connections_gte takes an integer of 0 or more");
6717
+ if (!Number.isInteger(v) || v < 0)
6718
+ push("contact.connections_gte takes an integer of 0 or more");
5947
6719
  return;
5948
6720
  case "contact.posted_within":
5949
- if (typeof val !== "string" || !ctx.re.duration.test(val)) push("contact.posted_within takes a duration like 14d");
6721
+ if (typeof v !== "string" || !DURATION_RE.test(v))
6722
+ push("contact.posted_within takes a duration like 14d");
5950
6723
  return;
5951
6724
  case "contact.tag":
5952
- if (!tagOk(ctx, val)) push("contact.tag takes a Tag name of 1 to 64 characters, without , or ;");
6725
+ if (!tagOk(v))
6726
+ push("contact.tag takes a Tag name of 1 to 64 characters, without , or ;");
5953
6727
  return;
5954
6728
  }
5955
- const field = key.startsWith("contact.extra.") ? ctx.re.extraKey.test(key.slice(14)) : key.startsWith("contact.") && v.standardFields.includes(key.slice(8));
6729
+ const enrollmentKey = key.startsWith("enrollment.") ? key.slice("enrollment.".length) : null;
6730
+ if (enrollmentKey !== null && inWhere) {
6731
+ push("enrollment.* predicates are not allowed in where");
6732
+ return;
6733
+ }
6734
+ 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
6735
  if (!field) {
5957
6736
  push(`unknown predicate ${quote(key)}`);
5958
6737
  return;
5959
6738
  }
5960
6739
  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");
6740
+ const op = isObject(v) && keysOf(v).length === 1 ? keysOf(v)[0] : null;
6741
+ const operand = op === null ? void 0 : v[op];
6742
+ if (enrollmentKey !== null && ctx)
6743
+ enrollmentRef(ctx, { order, path, pointer }, quote(key), enrollmentKey, (k) => k);
6744
+ if (op !== null && NUMBER_OPERATORS.includes(op)) {
6745
+ if (enrollmentKey === null && !key.startsWith("contact.extra."))
6746
+ push(`${op} compares numbers on enrollment.<key> and contact.extra.<key> only`, "R28");
6747
+ else if (typeof operand !== "number" || !Number.isFinite(operand))
6748
+ push(`${op} takes a number, like {"${op}": 50}`, "R28");
6749
+ else if (enrollmentKey !== null && ctx) {
6750
+ for (const sv of ctx.values.saved.get(enrollmentKey) ?? []) {
6751
+ const type = sampleType(ctx, sv.event, sv.path);
6752
+ if (type === "boolean" || type === "object" || type === "array") {
6753
+ push(`${quote(key)} is saved from ${quote(sv.path)}, a ${type} in the sample of event ${sv.event}; ${op} compares numbers`, "R29");
6754
+ break;
6755
+ }
6756
+ }
6757
+ }
6758
+ return;
6759
+ }
6760
+ 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
6761
  if (!ok) {
5965
6762
  push(`${quote(key)} takes exactly one of {"eq": v}, {"neq": v}, {"in": [v, ...]}, {"contains": "text"}, {"exists": true or false}`);
5966
6763
  }
5967
6764
  }
5968
6765
  function graph(ctx, infos, ids, onReply) {
5969
6766
  const n = infos.length;
5970
- const resolve = (t) => t === END ? END : ids.get(t);
6767
+ const resolve2 = (t) => t === END ? END : ids.get(t);
5971
6768
  const ft = (i) => i + 1 < n ? i + 1 : "off";
5972
6769
  const edges = infos.map((info, i) => {
5973
6770
  const raw = info.raw;
5974
- const explicit = (f) => ({ field: f, to: resolve(raw[f]), fallThrough: false });
6771
+ const explicit = (f) => ({ field: f, to: resolve2(raw[f]), fallThrough: false });
5975
6772
  const implicit = (f) => ({ field: f, to: ft(i), fallThrough: true });
5976
6773
  const pick = (f) => has(raw, f) ? explicit(f) : implicit(f);
5977
6774
  switch (info.kind) {
@@ -6004,12 +6801,17 @@ function graph(ctx, infos, ids, onReply) {
6004
6801
  const queue = [...roots];
6005
6802
  while (queue.length) {
6006
6803
  const i = queue.shift();
6007
- if (seen.has(i)) continue;
6804
+ if (seen.has(i))
6805
+ continue;
6008
6806
  seen.add(i);
6009
- for (const e of edges[i]) if (typeof e.to === "number") queue.push(e.to);
6807
+ for (const e of edges[i])
6808
+ if (typeof e.to === "number")
6809
+ queue.push(e.to);
6010
6810
  }
6011
6811
  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" });
6812
+ if (!seen.has(i)) {
6813
+ 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" });
6814
+ }
6013
6815
  });
6014
6816
  const state = new Array(n).fill(0);
6015
6817
  const stack = [];
@@ -6017,10 +6819,13 @@ function graph(ctx, infos, ids, onReply) {
6017
6819
  state[i] = 1;
6018
6820
  stack.push(i);
6019
6821
  for (const e of edges[i]) {
6020
- if (typeof e.to !== "number") continue;
6822
+ if (typeof e.to !== "number")
6823
+ continue;
6021
6824
  if (state[e.to] === 1) {
6022
6825
  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 });
6826
+ if (!graphIssues.some((x) => x.rule === rule)) {
6827
+ graphIssues.push({ order: i, code: "R8", path: `${infos[i].prefix}.${e.field}`, pointer: exitPtr(i, e), rule });
6828
+ }
6024
6829
  } else if (state[e.to] === 0) {
6025
6830
  dfs(e.to);
6026
6831
  }
@@ -6028,9 +6833,12 @@ function graph(ctx, infos, ids, onReply) {
6028
6833
  stack.pop();
6029
6834
  state[i] = 2;
6030
6835
  };
6031
- for (const r of roots) if (state[r] === 0) dfs(r);
6836
+ for (const r of roots)
6837
+ if (state[r] === 0)
6838
+ dfs(r);
6032
6839
  ctx.issues.push(...graphIssues);
6033
- if (graphIssues.length) return;
6840
+ if (graphIssues.length)
6841
+ return;
6034
6842
  const preds = infos.map(() => []);
6035
6843
  edges.forEach((es, i) => es.forEach((e) => typeof e.to === "number" && preds[e.to].push({ from: i, edge: e })));
6036
6844
  const action = (i) => infos[i].raw["do"];
@@ -6053,20 +6861,32 @@ function graph(ctx, infos, ids, onReply) {
6053
6861
  const withoutInvite = new Array(n).fill(false);
6054
6862
  const withoutMessage = new Array(n).fill(false);
6055
6863
  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;
6864
+ for (const r of roots)
6865
+ if (r === 0 || !fromFirst.has(r))
6866
+ withoutInvite[r] = withoutMessage[r] = true;
6057
6867
  for (const i of order) {
6058
6868
  for (const { from, edge } of preds[i]) {
6059
6869
  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;
6870
+ if (afterInvite[from] || invited)
6871
+ afterInvite[i] = true;
6872
+ if (withoutInvite[from] && !invited)
6873
+ withoutInvite[i] = true;
6062
6874
  const messaged = (action(from) === "message.send" || action(from) === "inmail.send") && edge.field === "next";
6063
- if (withoutMessage[from] && !messaged) withoutMessage[i] = true;
6875
+ if (withoutMessage[from] && !messaged)
6876
+ withoutMessage[i] = true;
6064
6877
  }
6065
6878
  }
6066
6879
  infos.forEach((info, i) => {
6067
- if (!seen.has(i)) return;
6880
+ if (!seen.has(i))
6881
+ return;
6068
6882
  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" });
6883
+ warning(ctx, {
6884
+ order: i,
6885
+ code: "W5",
6886
+ path: `${info.prefix}.do`,
6887
+ pointer: `${info.ptr}/do`,
6888
+ rule: "profile.follow after invite.send on the same path; an invitation already follows"
6889
+ });
6070
6890
  }
6071
6891
  const cond = info.raw["if"];
6072
6892
  const seenPred = isObject(cond) && (has(cond, "message.seen") || isObject(cond["not"]) && has(cond["not"], "message.seen"));
@@ -6085,23 +6905,29 @@ function graph(ctx, infos, ids, onReply) {
6085
6905
  });
6086
6906
  const flagged = /* @__PURE__ */ new Set();
6087
6907
  infos.forEach((info, i) => {
6088
- if (info.raw["until"] !== "invite.accepted") return;
6908
+ if (info.raw["until"] !== "invite.accepted")
6909
+ return;
6089
6910
  const start = edges[i][0].to;
6090
6911
  const visited = /* @__PURE__ */ new Set();
6091
6912
  const q = typeof start === "number" ? [start] : [];
6092
6913
  while (q.length) {
6093
6914
  const k = q.shift();
6094
- if (visited.has(k)) continue;
6915
+ if (visited.has(k))
6916
+ continue;
6095
6917
  visited.add(k);
6096
6918
  if (action(k) === "message.send") {
6097
6919
  if (!flagged.has(k)) {
6098
6920
  flagged.add(k);
6099
6921
  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" });
6922
+ if (link) {
6923
+ warning(ctx, { order: k, code: "W8", path: link.path, pointer: link.pointer, rule: "a link in the first message after the invitation is accepted" });
6924
+ }
6101
6925
  }
6102
6926
  continue;
6103
6927
  }
6104
- for (const e of edges[k]) if (typeof e.to === "number") q.push(e.to);
6928
+ for (const e of edges[k])
6929
+ if (typeof e.to === "number")
6930
+ q.push(e.to);
6105
6931
  }
6106
6932
  });
6107
6933
  }
@@ -6110,30 +6936,78 @@ function messageTexts(info) {
6110
6936
  const w = isObject(raw["with"]) ? raw["with"] : {};
6111
6937
  const out = [];
6112
6938
  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"] });
6939
+ raw["variants"].forEach((v, j) => {
6940
+ if (isObject(v) && typeof v["text"] === "string") {
6941
+ const id = typeof v["id"] === "string" ? `variants.${v["id"]}` : `variants[${j}]`;
6942
+ out.push({ path: `${info.prefix}.${id}.text`, pointer: `${info.ptr}/variants/${j}/text`, value: v["text"] });
6117
6943
  }
6118
6944
  });
6119
6945
  }
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"] });
6946
+ if (typeof w["text"] === "string")
6947
+ out.push({ path: `${info.prefix}.with.text`, pointer: `${info.ptr}/with/text`, value: w["text"] });
6948
+ if (typeof w["fallback"] === "string")
6949
+ out.push({ path: `${info.prefix}.with.fallback`, pointer: `${info.ptr}/with/fallback`, value: w["fallback"] });
6122
6950
  return out;
6123
6951
  }
6124
6952
  function topo(n, edges, roots) {
6125
6953
  const out = [];
6126
6954
  const done = new Array(n).fill(false);
6127
6955
  const visit = (i) => {
6128
- if (done[i]) return;
6956
+ if (done[i])
6957
+ return;
6129
6958
  done[i] = true;
6130
- for (const e of edges[i]) if (typeof e.to === "number") visit(e.to);
6959
+ for (const e of edges[i])
6960
+ if (typeof e.to === "number")
6961
+ visit(e.to);
6131
6962
  out.push(i);
6132
6963
  };
6133
6964
  roots.forEach(visit);
6134
6965
  return out.reverse();
6135
6966
  }
6136
6967
 
6968
+ // src/sequence/validate.ts
6969
+ function validateSequence(value) {
6970
+ const out = verdict(value);
6971
+ return { errors: out.errors.slice(0, MAX_ERRORS_LISTED), warnings: out.warnings };
6972
+ }
6973
+ function verdict(value) {
6974
+ const bad = firstUnstorable(value);
6975
+ if (bad !== null) return { errors: [{ path: bad, rule: UNSTORABLE_RULE }], warnings: [] };
6976
+ const size = compactBytes2(value);
6977
+ if (size > MAX_SEQUENCE_BYTES) return { errors: [sequenceSizeError(size)], warnings: [] };
6978
+ return lintSequence(value);
6979
+ }
6980
+ function compactBytes2(v) {
6981
+ try {
6982
+ return new TextEncoder().encode(JSON.stringify(v)).length;
6983
+ } catch {
6984
+ return 0;
6985
+ }
6986
+ }
6987
+ var UNSTORABLE_RULE = "must be well-formed Unicode and must not contain a NUL (\\u0000) character";
6988
+ var MAX_ERRORS_LISTED = 100;
6989
+ var UNSTORABLE_RE = /\u0000|[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?<![\uD800-\uDBFF])[\uDC00-\uDFFF]/;
6990
+ function firstUnstorable(doc) {
6991
+ const stack = [["value", doc, ""]];
6992
+ while (stack.length) {
6993
+ const [kind, value, path] = stack.pop();
6994
+ if (kind === "key") {
6995
+ if (UNSTORABLE_RE.test(value)) return path ? `${path} key` : "$ key";
6996
+ } else if (typeof value === "string") {
6997
+ if (UNSTORABLE_RE.test(value)) return path || "$";
6998
+ } else if (Array.isArray(value)) {
6999
+ for (let i = value.length - 1; i >= 0; i--) stack.push(["value", value[i], `${path}[${i}]`]);
7000
+ } else if (typeof value === "object" && value !== null) {
7001
+ const entries2 = Object.entries(value);
7002
+ for (let i = entries2.length - 1; i >= 0; i--) {
7003
+ const [k, v] = entries2[i];
7004
+ stack.push(["value", v, path ? `${path}.${k}` : k], ["key", k, path]);
7005
+ }
7006
+ }
7007
+ }
7008
+ return null;
7009
+ }
7010
+
6137
7011
  // src/sequence/builder.ts
6138
7012
  var refuse = (errors) => errors[0].rule === UNSTORABLE_RULE ? new CurviateError({
6139
7013
  code: "INVALID_REQUEST",
@@ -6296,6 +7170,16 @@ var MotionsResource = class {
6296
7170
  *
6297
7171
  * Needs Automate: a Team below that Tier is refused with `TIER_REQUIRED`.
6298
7172
  *
7173
+ * Days and hours in `schedule` are read in each sending account's own time
7174
+ * zone. `schedule.timezone` and `settings.stop_on_engagement` are retired:
7175
+ * accepted with warning W13 until 2027-01-25, never stored or returned
7176
+ * (`stop_on_engagement: true` with an acting reply route is a 400).
7177
+ * `settings.acceptance_floor` defaults to `null` (off). With
7178
+ * `settings.accounts_per_contact` above 1 (Several mode) the settings gain
7179
+ * `on_reply_elsewhere` (`stop` or `continue`), `withdraw_on_reply_elsewhere`
7180
+ * and `account_gap` (`1d` to `30d`), served only in that mode;
7181
+ * `accounts_per_contact` is locked once the Motion has started.
7182
+ *
6299
7183
  * @example
6300
7184
  * const motion = await curviate.motions.create({
6301
7185
  * name: "Q4 webinar follow-up",
@@ -6308,14 +7192,17 @@ var MotionsResource = class {
6308
7192
  return this.ctx.request({ method: "POST", path: "/v1/motions", body: withDocument(body) });
6309
7193
  }
6310
7194
  /**
6311
- * List Motions, most recently updated first. `status` (any of them; default
6312
- * all but `archived`), `account_id`, `q` (name substring). Items carry no
7195
+ * List Motions. `status` (any of them; default all but `archived`),
7196
+ * `account_id` (one or several accounts as a comma list, up to 50: Motions
7197
+ * sending from any of them), `q` (name substring), `sort`: `updated`
7198
+ * (default, latest edit first) or `activity` (latest step record first,
7199
+ * else `updated_at`). Items carry `last_activity_at` (or `null`) but no
6313
7200
  * `sequence`: read it with `get`. Pages with an opaque `cursor` until it is
6314
- * `null`.
7201
+ * `null`; a cursor belongs to its sort.
6315
7202
  * `GET /v1/motions`
6316
7203
  *
6317
7204
  * @example
6318
- * const { items } = await curviate.motions.list({ status: ["active", "paused"] });
7205
+ * const { items } = await curviate.motions.list({ status: ["active", "paused"], account_id: "acc_1,acc_2", sort: "activity" });
6319
7206
  */
6320
7207
  list(params) {
6321
7208
  return this.ctx.request({ method: "GET", path: "/v1/motions", query: commaLists(params, "status") });
@@ -6417,6 +7304,13 @@ var MotionsResource = class {
6417
7304
  * Sequence, rendered messages for `sample` Contacts (1 to 20, default 5) or
6418
7305
  * the `contact_ids` you name (up to 20, not with `sample`), and the budget
6419
7306
  * projection. A read: never reaches LinkedIn, spends nothing.
7307
+ *
7308
+ * `projection.audience` is `{ total, skipped, to_enroll }`, `skipped`
7309
+ * counted per reason (`do_not_contact`, `excluded`, `other_motion`,
7310
+ * `known_chat`, `filtered`; a Contact counts under the first that applies);
7311
+ * `projection.contacts` equals `to_enroll`. In
7312
+ * Several mode `accounts[].contacts` counts Enrollments, not people, and
7313
+ * `projection.fits_before_end_at` is counted per account zone.
6420
7314
  * `GET /v1/motions/{id}/preview`
6421
7315
  *
6422
7316
  * @example
@@ -6482,15 +7376,20 @@ var EnrollmentsResource = class {
6482
7376
  }
6483
7377
  /**
6484
7378
  * 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
7379
+ * `account_id` (one or several as a comma list, up to 50), `status` (any of
7380
+ * them), `step_id` (at any of these Steps now, up to 10), `version`; by Step outcome: `done_step` with `outcome` (how that Step
6487
7381
  * ended), `reason` (catalogue codes, any of them), `variant_id`,
6488
7382
  * `relationship_at_enrollment` (any of them). Pages with an opaque `cursor`
6489
7383
  * until it is `null`.
6490
7384
  * `GET /v1/enrollments`
6491
7385
  *
7386
+ * A finished Enrollment's `finish_reason` may be `engaged_elsewhere`: the
7387
+ * Contact replied to, or was messaged from, another account of the Motion
7388
+ * (Several mode).
7389
+ *
6492
7390
  * @example
6493
7391
  * const { items } = await curviate.enrollments.list({ motion_id: "mot_123", done_step: "opener", outcome: "failed" });
7392
+ * const atEither = await curviate.enrollments.list({ motion_id: "mot_123", step_id: ["opener", "bump"] });
6494
7393
  */
6495
7394
  list(params) {
6496
7395
  return this.ctx.request({
@@ -6501,7 +7400,12 @@ var EnrollmentsResource = class {
6501
7400
  }
6502
7401
  /**
6503
7402
  * One Enrollment with its `history` of Steps (metadata only, no rendered
6504
- * text).
7403
+ * text). `history` reaches back 12 months, plus the entries `retry` reads
7404
+ * at any age; `history_truncated: true` says older entries are not listed.
7405
+ * The Enrollment's state has no such limit. `values` are what the
7406
+ * Sequence's `save` maps took from an incoming event (name to value; read
7407
+ * them as `{{enrollment.<name>}}` and `if enrollment.<name>`); they are
7408
+ * empty once it finishes, and after a resume.
6505
7409
  * `GET /v1/enrollments/{id}`
6506
7410
  *
6507
7411
  * @example
@@ -6511,8 +7415,8 @@ var EnrollmentsResource = class {
6511
7415
  return this.ctx.request({ method: "GET", path: apiPath`/v1/enrollments/${enrollmentId}` });
6512
7416
  }
6513
7417
  /**
6514
- * Stop a `pending`, `active` or `paused` Enrollment (`finished`,
6515
- * `stopped`); else `ENROLLMENT_NOT_ACTIVE`.
7418
+ * Stop a `queued`, `active` or `paused` Enrollment (it ends `finished`,
7419
+ * `finish_reason: "stopped"`); else `ENROLLMENT_NOT_ACTIVE`.
6516
7420
  * `POST /v1/enrollments/{id}/stop`
6517
7421
  *
6518
7422
  * @example
@@ -6522,7 +7426,7 @@ var EnrollmentsResource = class {
6522
7426
  return this.ctx.request({ method: "POST", path: apiPath`/v1/enrollments/${enrollmentId}/stop` });
6523
7427
  }
6524
7428
  /**
6525
- * Hold one `pending`, `active` or `paused` Enrollment until you `resume` it
7429
+ * Hold one `queued`, `active` or `paused` Enrollment until you `resume` it
6526
7430
  * (`waiting.kind: "paused_by_user"`); its waits freeze. A Step being sent
6527
7431
  * right now is `STEP_IN_FLIGHT`: retry in a minute. Works on every plan.
6528
7432
  * `POST /v1/enrollments/{id}/pause`
@@ -6687,6 +7591,15 @@ var NetworkConnectionsResource = class {
6687
7591
  * characters), `include_removed` (Connections that left), `limit` (up to
6688
7592
  * 1,000). `await` it for one page; `for await` it for every Connection
6689
7593
  * across pages.
7594
+ *
7595
+ * By default an item is one account and one person (`object: "connection"`),
7596
+ * so someone connected to three of your accounts appears three times.
7597
+ * `group: "person"` lists one item per person (`object: "connection_person"`)
7598
+ * with `accounts`, each of your accounts they are connected to, and `contact`,
7599
+ * their Contact summary (`id`, `list_count`, up to 10 `lists`; `null` when
7600
+ * they are not a Contact); `account_id` then picks which people are
7601
+ * listed. `include_total: true` (only with `group: "person"`) adds `total`,
7602
+ * the number of people that match across all pages. Branch on `object`.
6690
7603
  * `GET /v1/network/connections`
6691
7604
  *
6692
7605
  * @example
@@ -6720,16 +7633,24 @@ var NetworkResource = class {
6720
7633
  /**
6721
7634
  * Start a full re-read of one account's Connections, paced and inside the
6722
7635
  * account's activity window (hours to days for a large network). Once per
6723
- * account per 24 hours; follow it with `syncStatus`.
7636
+ * account per 24 hours; follow it with `syncStatus`. With an `account_id` it
7637
+ * resolves with that account's answer (`object: "network_resync"`). With no
7638
+ * argument (or no `account_id`) it is Resync all: every account you can
7639
+ * operate that can start one now, one after another in a random order, each
7640
+ * 20 to 90 seconds after the previous; it resolves with
7641
+ * `object: "network_resync_list"`, one item per account with its `status`
7642
+ * (`queued`, `cooldown`, `in_progress` or `paused`: answers, not errors) and
7643
+ * `starts_at`. Branch on `object`.
6724
7644
  * `POST /v1/network/resync`
6725
7645
  *
6726
7646
  * Needs Automate: a Team below that Tier is refused with `TIER_REQUIRED`.
6727
7647
  *
6728
7648
  * @example
6729
7649
  * await curviate.network.resync({ account_id: "acc_123" });
7650
+ * const all = await curviate.network.resync(); // Resync all
6730
7651
  */
6731
7652
  resync(body) {
6732
- return this.ctx.request({ method: "POST", path: "/v1/network/resync", body });
7653
+ return this.ctx.request({ method: "POST", path: "/v1/network/resync", body: body ?? {} });
6733
7654
  }
6734
7655
  };
6735
7656
 
@@ -6740,9 +7661,20 @@ var UniboxAccountsResource = class {
6740
7661
  }
6741
7662
  /**
6742
7663
  * For every account you can operate: whether its whole inbox is copied into
6743
- * the Unibox, and how far the chat-list sync is.
7664
+ * the Unibox, and how far the chat-list sync is. A disconnected account is
7665
+ * listed too (`account_status: "disconnected"`), read-only.
6744
7666
  * `GET /v1/unibox/accounts`
6745
7667
  *
7668
+ * Read `sync` before trusting an empty Unibox: `off` and `pending` mean
7669
+ * nothing was listed yet. `sync.next_run_at` is when the next read of the
7670
+ * account happens: once the first listing is `complete`, the next poll
7671
+ * (every 5 to 10 minutes while the Unibox is open in the dashboard,
7672
+ * otherwise 30 to 60, slower at weekends, inside the account's active hours
7673
+ * only); while `paused`, when the pause ends; `null` when none can be
7674
+ * scheduled or full-inbox sync is off. An account without full-inbox sync
7675
+ * that still has Motion chats is polled for them, yet reads `off` with
7676
+ * `next_run_at: null`.
7677
+ *
6746
7678
  * @example
6747
7679
  * const { items } = await curviate.unibox.accounts.list();
6748
7680
  */
@@ -6774,12 +7706,26 @@ var UniboxResource = class {
6774
7706
  /**
6775
7707
  * List the chats of every account you can operate in one inbox, newest
6776
7708
  * 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.
7709
+ * each), `person`, `unread`, `last_message_from`, `type`, `q` (3+
7710
+ * characters). `await` it for one page; `for await` it for every chat
7711
+ * across pages.
6779
7712
  * `GET /v1/unibox/chats`
6780
7713
  *
7714
+ * `person` is one Contact id (`con_...`) or LinkedIn id (`ACo...`), not a
7715
+ * list; an unknown id matches nothing. `motion_ids` also takes `"none"`:
7716
+ * the chats no Motion links, alone or beside Motion ids. `include_total:
7717
+ * true` adds `total` to the page, the chats that match the filters (not
7718
+ * the page); without it the body has no `total`.
7719
+ *
7720
+ * A chat of a disconnected account is listed (`account_status:
7721
+ * "disconnected"`), read-only. `unread` is LinkedIn's state for the
7722
+ * account, the same for every viewer. `marked_unread` is `{origin, at}`
7723
+ * (`origin`: `unibox`, `api` or `linkedin`) when the chat was made unread
7724
+ * without a newer message from the other person, else `null`.
7725
+ *
6781
7726
  * @example
6782
7727
  * for await (const chat of curviate.unibox.list({ unread: true })) console.log(chat.chat_id);
7728
+ * const { total } = await curviate.unibox.list({ person: "con_123", motion_ids: ["none"], include_total: true });
6783
7729
  */
6784
7730
  list(params) {
6785
7731
  return pageable(
@@ -6811,9 +7757,18 @@ var UniboxResource = class {
6811
7757
  /**
6812
7758
  * Queue a re-read of the newest chats (up to 100) of each account with
6813
7759
  * 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`.
7760
+ * account per 2 minutes; a repeat answers `cooldown` with `next_allowed_at`.
6815
7761
  * `POST /v1/unibox/refresh`
6816
7762
  *
7763
+ * It covers every account the poll covers: full-inbox sync on, or Motion
7764
+ * chats or Motion outreach without it (only those chats are kept). An
7765
+ * account's `status` is `queued`, `cooldown`, `paused` (it runs when the
7766
+ * pause ends) or `not_synced`, which means none of those apply (or the
7767
+ * first full listing is still running), so there is nothing to refresh.
7768
+ * `next_allowed_at` is when the next manual refresh is allowed (while
7769
+ * `paused`, when the pause ends, `null` if unknown). A disconnected account
7770
+ * named here is ignored and claims no cooldown.
7771
+ *
6817
7772
  * Needs Automate: a Team below that Tier is refused with `TIER_REQUIRED`.
6818
7773
  *
6819
7774
  * @example
@@ -6858,27 +7813,57 @@ var AnalyticsMotionsResource = class {
6858
7813
  this.ctx = ctx;
6859
7814
  }
6860
7815
  /**
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.
7816
+ * Results of your Motions and direct actions (API, CLI, MCP, Unibox) in
7817
+ * total, per Motion and per account: Enrollments by state right now (the
7818
+ * window does not apply), invitations and acceptance, messages and reply,
7819
+ * InMails and InMail reply (never mixed with messages), replies, engagement,
7820
+ * a daily series. Each Contact counts once per Account and source. Rates are
7821
+ * outcomes so far (`maturing` is the part sent under 7 days ago;
7822
+ * `low_sample` under 30); `compare: "previous_period"` adds `previous` and
7823
+ * `delta` to every rate. `origin` filters by source: `motion`, `unibox`,
7824
+ * `api`, or `direct` (unibox and api).
7825
+ *
7826
+ * Grammar: `include` picks sections (`performance`, `daily`, `motions`,
7827
+ * `accounts`, `flags`), `fields` keeps dotted paths (`motions.reply.rate`),
7828
+ * `sort` and `limit` order and cut the Motion and account lists (`-key`
7829
+ * is descending), and `flags` says what to read next. Give any of these
7830
+ * without `include` and the body is the summary: `performance`, the top 5
7831
+ * by sent, `flags`. Give none and you still get the full body, with a
7832
+ * `Deprecation` header: it becomes the summary later.
7833
+ *
7834
+ * Window: `from` and `to` reach back up to 3650 days (more is a `400` naming
7835
+ * `from`). Beyond 12 months you get daily counts only:
7836
+ * `window.counts_only_through` is the last day read from the daily counts
7837
+ * (`null` when the whole window has full detail), and
7838
+ * `window.paths_through` says `paths` covers only the days after it, so past
7839
+ * it `paths` does not add up to the totals.
6864
7840
  * `GET /v1/analytics/motions`
6865
7841
  *
6866
7842
  * @example
6867
7843
  * const { totals, motions } = await curviate.analytics.motions.list({ from: "2026-10-01", to: "2026-10-31" });
7844
+ *
7845
+ * @example
7846
+ * const summary = await curviate.analytics.motions.list({ range: "30d", compare: "previous_period", origin: ["motion", "unibox"] });
6868
7847
  */
6869
7848
  list(params) {
6870
- return this.ctx.request({ method: "GET", path: "/v1/analytics/motions", query: params });
7849
+ return this.ctx.request({ method: "GET", path: "/v1/analytics/motions", query: commaLists(params, "account_id", "motion_id", "origin", "include", "fields", "sort") });
6871
7850
  }
6872
7851
  /**
6873
7852
  * One Motion's results with a row per step and per variant (document
6874
- * order, never ranked); `version` picks a Sequence version.
7853
+ * order, never ranked); `version` picks a Sequence version. The window,
7854
+ * `origin`, `include`, `fields`, `sort`, `limit` and `compare` work as on
7855
+ * {@link AnalyticsMotionsResource.list}; here the summary is `performance`,
7856
+ * `accounts` and `flags`, and `steps`, `variants` and `paths` come with
7857
+ * `include`. `paths` splits the Motion by the Contact's relationship to the
7858
+ * account when the Enrollment started (provisional); it covers only the
7859
+ * days after `window.paths_through`.
6875
7860
  * `GET /v1/analytics/motions/{motion_id}`
6876
7861
  *
6877
7862
  * @example
6878
7863
  * const { steps } = await curviate.analytics.motions.get("mot_123");
6879
7864
  */
6880
7865
  get(motionId, params) {
6881
- return this.ctx.request({ method: "GET", path: apiPath`/v1/analytics/motions/${motionId}`, query: params });
7866
+ return this.ctx.request({ method: "GET", path: apiPath`/v1/analytics/motions/${motionId}`, query: commaLists(params, "account_id", "origin", "include", "fields", "sort") });
6882
7867
  }
6883
7868
  };
6884
7869
  var AnalyticsEventsResource = class {
@@ -6887,10 +7872,16 @@ var AnalyticsEventsResource = class {
6887
7872
  }
6888
7873
  /**
6889
7874
  * 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
7875
+ * invitations accepted and replies, plus the direct actions (Unibox, API).
7876
+ * Filters: `account_id`, `motion_id`, `contact_id`, `kind` and `origin`
7877
+ * (several). `origin` is `motion`, `unibox`, `api` or `direct` (an alias
7878
+ * for `unibox` and `api`). `contact_id` (`con_...`) keeps one person's
7879
+ * events, their Enrollments' and the direct actions with them; an unknown
7880
+ * id, another team's, or one merged into another Contact throws
7881
+ * `RESOURCE_NOT_FOUND` (404). Keyset paging: events recorded after your first page
6892
7882
  * never shift later pages. `await` it for one page; `for await` it for
6893
- * every event across pages.
7883
+ * every event across pages. A direct action's `person_name` names the
7884
+ * person (they may not be a Contact); `null` for a Motion event.
6894
7885
  * `GET /v1/analytics/events`
6895
7886
  *
6896
7887
  * @example
@@ -6898,7 +7889,7 @@ var AnalyticsEventsResource = class {
6898
7889
  */
6899
7890
  list(params) {
6900
7891
  return pageable(
6901
- (p) => this.ctx.request({ method: "GET", path: "/v1/analytics/events", query: commaLists(p, "kind") }),
7892
+ (p) => this.ctx.request({ method: "GET", path: "/v1/analytics/events", query: commaLists(p, "kind", "origin") }),
6902
7893
  params
6903
7894
  );
6904
7895
  }
@@ -6919,7 +7910,346 @@ var AnalyticsResource = class {
6919
7910
  * const { types } = await curviate.analytics.activity({ account_id: "acc_123" });
6920
7911
  */
6921
7912
  activity(params) {
6922
- return this.ctx.request({ method: "GET", path: "/v1/analytics/activity", query: params });
7913
+ return this.ctx.request({ method: "GET", path: "/v1/analytics/activity", query: commaLists(params, "account_id") });
7914
+ }
7915
+ /**
7916
+ * The Analytics tables as one download, now, any size and no row limit,
7917
+ * never message text: `table` picks `daily`, `accounts`, `motions` and
7918
+ * `enrollments` (default all; several come as one ZIP), `format` is `csv`
7919
+ * (default; UTF-8 with a BOM, formula-guarded), `json` or `ndjson` (one
7920
+ * table only), under the overview's filters (window, `account_id`,
7921
+ * `motion_id`, `origin`). Resolves with the file's bytes. Owners and
7922
+ * Managers. For a file to fetch later, `exports.create`.
7923
+ * `GET /v1/analytics/export`
7924
+ *
7925
+ * @example
7926
+ * const csv = await curviate.analytics.export({ table: "daily", range: "90d" });
7927
+ */
7928
+ async export(params) {
7929
+ return this.ctx.request({ bytes: true, method: "GET", path: "/v1/analytics/export", query: commaLists(params, "account_id", "motion_id", "origin", "table") });
7930
+ }
7931
+ };
7932
+
7933
+ // src/resources/billing.ts
7934
+ var BillingSeatsResource = class {
7935
+ constructor(ctx) {
7936
+ this.ctx = ctx;
7937
+ }
7938
+ /**
7939
+ * Move an unused paid seat of this Team to another of your Teams, at no
7940
+ * charge; it stays on this Team's invoice at its Tier. `seat_id` picks the
7941
+ * seat, or leave it out to lend the oldest unused one; each call lends one
7942
+ * more seat. Trial, occupied, unconfirmed, cancelling and already-lent seats
7943
+ * cannot move (`SEAT_NOT_FOUND`). The borrowing Team connects an Account
7944
+ * into it and the seat counts against that Team's rate limit, not this
7945
+ * one's. While it is lent, a connect naming it or a cancel of it throws
7946
+ * `SEAT_LENT`: move it back first. Only the Owner who pays for both Teams
7947
+ * can lend (`INSUFFICIENT_ROLE`). Resolves with the lent `seat_id` and the
7948
+ * `team_id` it went to.
7949
+ * `POST /v1/billing/seats/lend`
7950
+ *
7951
+ * @example
7952
+ * const { seat_id } = await curviate.billing.seats.lend({ team_id: "tnt_456" });
7953
+ */
7954
+ lend(body) {
7955
+ return this.ctx.request({ method: "POST", path: "/v1/billing/seats/lend", body });
7956
+ }
7957
+ /**
7958
+ * End a seat loan while no Account uses the seat. Call it from either Team:
7959
+ * with the lending Team's seat id, or with the lent seat's id in the Team
7960
+ * that received it. A seat an Account still uses throws `SEAT_NOT_EMPTY`:
7961
+ * disconnect the Account first. The Owner who pays for either Team can move
7962
+ * it back.
7963
+ * `POST /v1/billing/seats/{seat_id}/return`
7964
+ *
7965
+ * @example
7966
+ * await curviate.billing.seats.return("seat_123");
7967
+ */
7968
+ return(seatId) {
7969
+ return this.ctx.request({ method: "POST", path: apiPath`/v1/billing/seats/${seatId}/return` });
7970
+ }
7971
+ };
7972
+ var BillingInvoicesResource = class {
7973
+ constructor(ctx) {
7974
+ this.ctx = ctx;
7975
+ }
7976
+ /**
7977
+ * The Team's issued invoices, newest first, with number, `total`,
7978
+ * `currency`, `status` (`paid`, `open`, `failed` or `void`) and service
7979
+ * period. Draft invoices are never listed and an invoice carries no link:
7980
+ * download its PDF by `number`. `limit` is 1 to 100 (default 20). `await` it
7981
+ * for one page; `for await` it for every invoice across pages. Owners only
7982
+ * (`INSUFFICIENT_ROLE`); `BILLING_PORTAL_UNAVAILABLE` (503) means retry.
7983
+ * `GET /v1/billing/invoices`
7984
+ *
7985
+ * @example
7986
+ * for await (const invoice of curviate.billing.invoices.list()) console.log(invoice.number, invoice.status);
7987
+ */
7988
+ list(params) {
7989
+ return pageable(
7990
+ (p) => this.ctx.request({ method: "GET", path: "/v1/billing/invoices", ...p ? { query: p } : {} }),
7991
+ params
7992
+ );
7993
+ }
7994
+ /**
7995
+ * One invoice as PDF bytes, by its `number` from {@link BillingInvoicesResource.list}.
7996
+ * An unknown number throws `RESOURCE_NOT_FOUND`. Owners only.
7997
+ * `GET /v1/billing/invoices/{number}/pdf`
7998
+ *
7999
+ * @example
8000
+ * const pdf = await curviate.billing.invoices.pdf("INV-0042");
8001
+ * await fs.promises.writeFile("INV-0042.pdf", Buffer.from(pdf));
8002
+ */
8003
+ pdf(number) {
8004
+ return this.ctx.request({ method: "GET", path: apiPath`/v1/billing/invoices/${number}/pdf` });
8005
+ }
8006
+ };
8007
+ var BillingResource = class {
8008
+ constructor(ctx) {
8009
+ this.seats = new BillingSeatsResource(ctx);
8010
+ this.invoices = new BillingInvoicesResource(ctx);
8011
+ }
8012
+ };
8013
+
8014
+ // src/resources/do-not-contact.ts
8015
+ var DoNotContactResource = class {
8016
+ constructor(ctx) {
8017
+ this.ctx = ctx;
8018
+ }
8019
+ /**
8020
+ * The Team's do-not-contact list, newest first. `q` matches the Contact's
8021
+ * name. `await` it for one page; `for await` it for every entry across
8022
+ * pages.
8023
+ * `GET /v1/do-not-contact`
8024
+ *
8025
+ * @example
8026
+ * for await (const entry of curviate.doNotContact.list({ q: "ana" })) console.log(entry.id, entry.reason);
8027
+ */
8028
+ list(params) {
8029
+ return pageable(
8030
+ (p) => this.ctx.request({ method: "GET", path: "/v1/do-not-contact", ...p ? { query: p } : {} }),
8031
+ params
8032
+ );
8033
+ }
8034
+ /**
8035
+ * One entry. `reason` is one of `asked_to_stop`, `existing_customer`,
8036
+ * `competitor`, `partner`, `employee`, `other`, or `null`.
8037
+ * `GET /v1/do-not-contact/{id}`
8038
+ *
8039
+ * @example
8040
+ * const entry = await curviate.doNotContact.get("dnc_123");
8041
+ */
8042
+ get(id) {
8043
+ return this.ctx.request({ method: "GET", path: apiPath`/v1/do-not-contact/${id}` });
8044
+ }
8045
+ /**
8046
+ * Add one person by `contact_id` or `linkedin_url` (someone who is not a
8047
+ * Contact yet becomes one first, from stored data only), with an optional
8048
+ * `reason`. A person already on the list resolves with their existing entry,
8049
+ * unchanged. Any team member may add. There is no bulk route: loop. Needs
8050
+ * Automate: a Team below that Tier is refused with `TIER_REQUIRED`.
8051
+ * `POST /v1/do-not-contact`
8052
+ *
8053
+ * @example
8054
+ * const entry = await curviate.doNotContact.add({ linkedin_url: "https://www.linkedin.com/in/ana/", reason: "asked_to_stop" });
8055
+ */
8056
+ add(body) {
8057
+ return this.ctx.request({ method: "POST", path: "/v1/do-not-contact", body });
8058
+ }
8059
+ /**
8060
+ * Set the entry's `reason` from the fixed list; `null` clears it. Needs
8061
+ * Automate: a Team below that Tier is refused with `TIER_REQUIRED`.
8062
+ * `PATCH /v1/do-not-contact/{id}`
8063
+ *
8064
+ * @example
8065
+ * await curviate.doNotContact.update("dnc_123", { reason: "existing_customer" });
8066
+ */
8067
+ update(id, body) {
8068
+ return this.ctx.request({ method: "PATCH", path: apiPath`/v1/do-not-contact/${id}`, body });
8069
+ }
8070
+ /**
8071
+ * Remove the entry (bodyless, 204): Motions and writes may reach the person
8072
+ * again. Owners and Managers only (`INSUFFICIENT_ROLE`).
8073
+ * `DELETE /v1/do-not-contact/{id}`
8074
+ *
8075
+ * @example
8076
+ * await curviate.doNotContact.remove("dnc_123");
8077
+ */
8078
+ async remove(id) {
8079
+ await this.ctx.request({ method: "DELETE", path: apiPath`/v1/do-not-contact/${id}` });
8080
+ }
8081
+ };
8082
+
8083
+ // src/resources/incoming-events.ts
8084
+ var IncomingEventsResource = class {
8085
+ constructor(ctx) {
8086
+ this.ctx = ctx;
8087
+ }
8088
+ /**
8089
+ * Create an incoming event, switched off, waiting for its sample. Exactly
8090
+ * one of `event_name` (fixed) and `event_name_field` (a path in the post).
8091
+ * The result's `url` is the only time it is shown in full.
8092
+ * `POST /v1/incoming-events`
8093
+ *
8094
+ * @example
8095
+ * const { id, url } = await curviate.incomingEvents.create({ event_name: "reply_received", match: { email: "lead.email" } });
8096
+ */
8097
+ create(body) {
8098
+ return this.ctx.request({ method: "POST", path: "/v1/incoming-events", body });
8099
+ }
8100
+ /**
8101
+ * The Team's incoming events (`url` masked); `event` keeps one event name.
8102
+ * `await` it for one page; `for await` it for every one across pages.
8103
+ * `GET /v1/incoming-events`
8104
+ */
8105
+ list(params) {
8106
+ return pageable(
8107
+ (p) => this.ctx.request({ method: "GET", path: "/v1/incoming-events", ...p ? { query: p } : {} }),
8108
+ params
8109
+ );
8110
+ }
8111
+ /**
8112
+ * One incoming event, `url` masked; `shape` is the captured sample as paths
8113
+ * and types only.
8114
+ * `GET /v1/incoming-events/{id}`
8115
+ */
8116
+ get(id) {
8117
+ return this.ctx.request({ method: "GET", path: apiPath`/v1/incoming-events/${id}` });
8118
+ }
8119
+ /**
8120
+ * Change it; `enabled: true` switches it on once the sample is in. In
8121
+ * `match` and `required_header`, `null` removes an entry.
8122
+ * `PATCH /v1/incoming-events/{id}`
8123
+ *
8124
+ * @example
8125
+ * await curviate.incomingEvents.update("inc_123", { enabled: true });
8126
+ */
8127
+ update(id, body) {
8128
+ return this.ctx.request({ method: "PATCH", path: apiPath`/v1/incoming-events/${id}`, body });
8129
+ }
8130
+ /**
8131
+ * Delete it: its URL answers `410 INCOMING_URL_RETIRED` from then on.
8132
+ * `DELETE /v1/incoming-events/{id}`
8133
+ */
8134
+ async delete(id) {
8135
+ await this.ctx.request({ method: "DELETE", path: apiPath`/v1/incoming-events/${id}` });
8136
+ }
8137
+ /**
8138
+ * Issue a new URL, in full in this result only. The old one keeps working
8139
+ * for 24 hours (`previous_url_valid_until`), then answers `410
8140
+ * INCOMING_URL_RETIRED`. Only the creator.
8141
+ * `POST /v1/incoming-events/{id}/rotate`
8142
+ */
8143
+ rotate(id) {
8144
+ return this.ctx.request({ method: "POST", path: apiPath`/v1/incoming-events/${id}/rotate` });
8145
+ }
8146
+ /**
8147
+ * The next post replaces the sample (paths and types only) and, when
8148
+ * switched on, still moves Enrollments. Only the creator.
8149
+ * `POST /v1/incoming-events/{id}/capture`
8150
+ */
8151
+ capture(id) {
8152
+ return this.ctx.request({ method: "POST", path: apiPath`/v1/incoming-events/${id}/capture` });
8153
+ }
8154
+ /**
8155
+ * The last 20 posts and what each did (`outcome`; accept values you do not
8156
+ * know), kept 30 days. Metadata only, never a value from the post.
8157
+ * `GET /v1/incoming-events/{id}/arrivals`
8158
+ */
8159
+ arrivals(id) {
8160
+ return this.ctx.request({ method: "GET", path: apiPath`/v1/incoming-events/${id}/arrivals` });
8161
+ }
8162
+ };
8163
+
8164
+ // src/resources/tenant.ts
8165
+ var TenantBetaResource = class {
8166
+ constructor(ctx) {
8167
+ this.ctx = ctx;
8168
+ }
8169
+ /**
8170
+ * The persisted decision only, not the effective consent of a given call:
8171
+ * the `X-Curviate-Beta` header overrides it per request, either way. Every
8172
+ * member may read.
8173
+ * `GET /v1/tenant/beta`
8174
+ *
8175
+ * @example
8176
+ * const { beta_enabled } = await curviate.tenant.beta.get();
8177
+ */
8178
+ get() {
8179
+ return this.ctx.request({ method: "GET", path: "/v1/tenant/beta" });
8180
+ }
8181
+ /**
8182
+ * Persist the consent for every account of the Team. Off: beta-gated
8183
+ * operations refuse `BETA_NOT_ENABLED` again from the next call. Owners
8184
+ * only (`INSUFFICIENT_ROLE`).
8185
+ * `PATCH /v1/tenant/beta`
8186
+ *
8187
+ * @example
8188
+ * await curviate.tenant.beta.set({ beta_enabled: true });
8189
+ */
8190
+ set(body) {
8191
+ return this.ctx.request({ method: "PATCH", path: "/v1/tenant/beta", body });
8192
+ }
8193
+ };
8194
+ var TenantResource = class {
8195
+ constructor(ctx) {
8196
+ this.beta = new TenantBetaResource(ctx);
8197
+ }
8198
+ };
8199
+
8200
+ // src/resources/exports.ts
8201
+ var ExportsResource = class {
8202
+ constructor(ctx) {
8203
+ this.ctx = ctx;
8204
+ }
8205
+ /**
8206
+ * Start an export; it runs in the background and this resolves at once with
8207
+ * `status: "queued"`. `entity` is what to export (`analytics`,
8208
+ * `motion_activity`, `account_activity` or `account_metrics`), `format`
8209
+ * `csv`, `json` or `ndjson` (one table only), `tables` picks the Analytics
8210
+ * tables (`daily`, `accounts`, `motions`, `enrollments`; several make one
8211
+ * ZIP), `filters` are the Analytics overview's filters (default: the last
8212
+ * 30 days). Your team has one export at a time: a second is refused with
8213
+ * `EXPORT_IN_PROGRESS`, and `details.export_id` names the running one (it
8214
+ * may be a teammate's, which `get` answers 404). An Owner or Manager only.
8215
+ * `POST /v1/exports`
8216
+ *
8217
+ * Not retry-safe: a retry starts a second export once the first finishes.
8218
+ *
8219
+ * @example
8220
+ * const exp = await curviate.exports.create({ entity: "analytics", format: "csv", tables: ["daily"], filters: { range: "90d" } });
8221
+ */
8222
+ create(body) {
8223
+ return this.ctx.request({ method: "POST", path: "/v1/exports", body });
8224
+ }
8225
+ /**
8226
+ * One export of yours and its `status`: `queued`, `running`, `ready`
8227
+ * (download it), `failed` (read `error`: `EXPORT_TOO_LARGE` means narrow the
8228
+ * filters or tables), `expired` (24 hours after `ready_at`, the file is
8229
+ * deleted) or `removed` (a Contact was deleted, which removes the Team's
8230
+ * exports that hold Contacts). Accept statuses you do not know. There is no
8231
+ * link to the file.
8232
+ * `GET /v1/exports/{export_id}`
8233
+ *
8234
+ * @example
8235
+ * const { status } = await curviate.exports.get("exp_123");
8236
+ */
8237
+ get(exportId) {
8238
+ return this.ctx.request({ method: "GET", path: apiPath`/v1/exports/${exportId}` });
8239
+ }
8240
+ /**
8241
+ * Download a ready export's file as bytes (CSV with a BOM, JSON, NDJSON, or
8242
+ * a ZIP when several tables were asked). A queued, running or failed export
8243
+ * throws `EXPORT_NOT_READY`, an expired one `EXPORT_EXPIRED` and one removed
8244
+ * by a Contact delete `EXPORT_REMOVED` (run it again).
8245
+ * `GET /v1/exports/{export_id}/file`
8246
+ *
8247
+ * @example
8248
+ * const bytes = await curviate.exports.download("exp_123");
8249
+ * await fs.promises.writeFile("export.csv", Buffer.from(bytes));
8250
+ */
8251
+ async download(exportId) {
8252
+ return this.ctx.request({ bytes: true, method: "GET", path: apiPath`/v1/exports/${exportId}/file` });
6923
8253
  }
6924
8254
  };
6925
8255
 
@@ -6980,6 +8310,14 @@ var SalesNavigatorResource = class {
6980
8310
  * base64-encoded file bytes, always sent as JSON, never multipart.
6981
8311
  * `POST /v1/{account_id}/sales-navigator/chats`
6982
8312
  *
8313
+ * When the Account already has a private
8314
+ * conversation with this person that you may not read, this throws
8315
+ * `CHAT_NOT_VISIBLE` (409) and sends nothing. Ask the person who connected
8316
+ * the Account to reply, or to sync the full inbox. It is detected from what
8317
+ * Curviate stored, so a thread older than 90 days that Curviate never
8318
+ * touched is not detected. A person on the do-not-contact list throws
8319
+ * `DO_NOT_CONTACT` (409).
8320
+ *
6983
8321
  * @beta Beta operation. See the namespace note above: while beta-gated it
6984
8322
  * refuses `BETA_NOT_ENABLED` (403) until a human enables beta in Settings,
6985
8323
  * or the request carries the `X-Curviate-Beta` header.
@@ -7170,6 +8508,14 @@ var RecruiterResource = class {
7170
8508
  * are REQUIRED; optional attachments ride the body as base64-encoded
7171
8509
  * objects (no multipart). `POST /v1/{account_id}/recruiter/chats`
7172
8510
  *
8511
+ * When the Account already has a private
8512
+ * conversation with this person that you may not read, this throws
8513
+ * `CHAT_NOT_VISIBLE` (409) and sends nothing. Ask the person who connected
8514
+ * the Account to reply, or to sync the full inbox. It is detected from what
8515
+ * Curviate stored, so a thread older than 90 days that Curviate never
8516
+ * touched is not detected. A person on the do-not-contact list throws
8517
+ * `DO_NOT_CONTACT` (409).
8518
+ *
7173
8519
  * @beta Beta operation. See the namespace note above: while beta-gated it
7174
8520
  * refuses `BETA_NOT_ENABLED` (403) until a human enables beta in Settings,
7175
8521
  * or the request carries the `X-Curviate-Beta` header.
@@ -7789,6 +9135,9 @@ var CompaniesResource = class {
7789
9135
  * the whole request rejects with a 404, not a partial result.
7790
9136
  * Re-inviting an already-invited member is a safe no-op, the same
7791
9137
  * invitation id, never a duplicate.
9138
+ *
9139
+ * A person on the Team's do-not-contact list throws `DO_NOT_CONTACT` (409);
9140
+ * nothing is sent.
7792
9141
  */
7793
9142
  followInvite(identifier, body) {
7794
9143
  return this.ctx.request({
@@ -7919,9 +9268,50 @@ var CompaniesResource = class {
7919
9268
  };
7920
9269
 
7921
9270
  // src/resources/webhooks.ts
9271
+ var WebhookDeliveriesResource = class {
9272
+ constructor(ctx) {
9273
+ this.ctx = ctx;
9274
+ }
9275
+ /**
9276
+ * The webhook's deliveries of the last 30 days, newest first: one item per
9277
+ * delivery (one event rendered once), attempts counted. `failed: true` keeps
9278
+ * those that ended `failed` or `not_retried`. No body is kept. An item's `id`
9279
+ * is the same value as the delivered body's `id`. `await` it for one page;
9280
+ * `for await` it for every delivery across pages.
9281
+ * `GET /v1/webhooks/{id}/deliveries`
9282
+ *
9283
+ * @example
9284
+ * for await (const d of curviate.webhooks.deliveries.list("wh_123", { failed: true })) console.log(d.id, d.status);
9285
+ */
9286
+ list(webhookId, params) {
9287
+ return pageable(
9288
+ (p) => this.ctx.request({
9289
+ method: "GET",
9290
+ path: apiPath`/v1/webhooks/${webhookId}/deliveries`,
9291
+ ...p ? { query: p } : {}
9292
+ }),
9293
+ params
9294
+ );
9295
+ }
9296
+ /**
9297
+ * One delivery: status, reason, attempts, the receiver's latest code and
9298
+ * timing, and `redeliverable_until`.
9299
+ * `GET /v1/webhooks/{id}/deliveries/{delivery_id}`
9300
+ *
9301
+ * @example
9302
+ * const d = await curviate.webhooks.deliveries.get("wh_123", "wdl_456");
9303
+ */
9304
+ get(webhookId, deliveryId) {
9305
+ return this.ctx.request({
9306
+ method: "GET",
9307
+ path: apiPath`/v1/webhooks/${webhookId}/deliveries/${deliveryId}`
9308
+ });
9309
+ }
9310
+ };
7922
9311
  var WebhooksResource = class {
7923
9312
  constructor(ctx) {
7924
9313
  this.ctx = ctx;
9314
+ this.deliveries = new WebhookDeliveriesResource(ctx);
7925
9315
  }
7926
9316
  /**
7927
9317
  * Register a new webhook endpoint to receive real-time events.
@@ -7933,6 +9323,13 @@ var WebhooksResource = class {
7933
9323
  * account of the workspace (reads then return `account_ids: null`). An
7934
9324
  * explicit `[]` is rejected.
7935
9325
  *
9326
+ * `body_template` (a JSON text with `{{variables}}` in its strings) replaces
9327
+ * the standard payload; check it offline first with
9328
+ * `validateWebhookTemplate`. A template that fails is refused with
9329
+ * `WEBHOOK_TEMPLATE_INVALID` (every finding with `line` and `col`); its
9330
+ * warnings come back in `warnings`. `custom_event_names` narrows
9331
+ * `motion.custom_event` to those names (`null`: every one).
9332
+ *
7936
9333
  * @example
7937
9334
  * await curviate.webhooks.create({ source: "account_status", request_url: "https://example.com/hooks" });
7938
9335
  */
@@ -7970,6 +9367,8 @@ var WebhooksResource = class {
7970
9367
  * Return a single webhook owned by the calling tenant.
7971
9368
  * `GET /v1/webhooks/{id}`
7972
9369
  * The plaintext secret is never present on a read, only `secret_prefix`.
9370
+ * `request_url` reads masked (`https://host/•••`). `disabled_reason`,
9371
+ * `failing_since` and `last_delivery_status_code` say how it is delivering.
7973
9372
  */
7974
9373
  get(id) {
7975
9374
  return this.ctx.request({
@@ -7981,7 +9380,15 @@ var WebhooksResource = class {
7981
9380
  * Update a webhook in place. `source` is immutable.
7982
9381
  * `PATCH /v1/webhooks/{id}`
7983
9382
  * `account_ids: null` switches an `account_status` webhook to every current
7984
- * and future account.
9383
+ * and future account. Sending back the masked `request_url` you read keeps
9384
+ * the stored URL. A `headers` entry with a key and no value keeps that
9385
+ * header's stored value; a new key needs a value. Setting `body_template`
9386
+ * lints it (`WEBHOOK_TEMPLATE_INVALID` on an error; warnings in the result's
9387
+ * `warnings`). Never rotates the secret: use {@link rotateSecret}.
9388
+ *
9389
+ * @example
9390
+ * // keep the stored Authorization value, replace the rest
9391
+ * await curviate.webhooks.update("wh_123", { headers: [{ key: "Authorization" }, { key: "X-Env", value: "prod" }] });
7985
9392
  */
7986
9393
  update(id, body) {
7987
9394
  return this.ctx.request({
@@ -8000,6 +9407,77 @@ var WebhooksResource = class {
8000
9407
  path: apiPath`/v1/webhooks/${id}`
8001
9408
  });
8002
9409
  }
9410
+ /**
9411
+ * The variables a body template of this source may use, each with its
9412
+ * scope, type and an example from the synthetic sample. On `motion` it also
9413
+ * lists every Enrollment value your Team's current Sequences save
9414
+ * (`{{enrollment.<key>}}`), with `saved_by`.
9415
+ * `GET /v1/webhooks/variables`
9416
+ *
9417
+ * @example
9418
+ * const { variables } = await curviate.webhooks.variables({ source: "motion" });
9419
+ */
9420
+ variables(params) {
9421
+ return this.ctx.request({
9422
+ method: "GET",
9423
+ path: "/v1/webhooks/variables",
9424
+ query: params
9425
+ });
9426
+ }
9427
+ /**
9428
+ * Queue a synthetic delivery to confirm your endpoint receives and verifies
9429
+ * deliveries: one signed attempt with `Curviate-Test: true`, never changing
9430
+ * the webhook's health. `event` picks the subscribed event it stands for
9431
+ * (default: the first); the delivered `event` is always `webhook.test`, with
9432
+ * `data.test_event` the one you picked. Read the attempt with
9433
+ * `deliveries.get(id, delivery_id)`.
9434
+ * `POST /v1/webhooks/{id}/test`
9435
+ *
9436
+ * @example
9437
+ * const { delivery_id } = await curviate.webhooks.test("wh_123", { event: "enrollment.finished" });
9438
+ */
9439
+ test(id, body) {
9440
+ return this.ctx.request({
9441
+ method: "POST",
9442
+ path: apiPath`/v1/webhooks/${id}/test`,
9443
+ body: body ?? {}
9444
+ });
9445
+ }
9446
+ /**
9447
+ * Send a delivery's original request again, byte for byte (never
9448
+ * re-rendered), freshly signed, as one attempt with no retries. Works until
9449
+ * `redeliverable_until` (receipt + 24 h) on an enabled webhook; a success
9450
+ * cancels its pending retries. Refusals: `NOTHING_TO_REDELIVER` (never
9451
+ * sent), `REDELIVERY_EXPIRED`, `WEBHOOK_DISABLED`, `DELIVERY_IN_FLIGHT`
9452
+ * (retry shortly).
9453
+ * `POST /v1/webhooks/{id}/deliveries/{delivery_id}/redeliver`
9454
+ *
9455
+ * @example
9456
+ * await curviate.webhooks.redeliver("wh_123", "wdl_456");
9457
+ */
9458
+ redeliver(id, deliveryId) {
9459
+ return this.ctx.request({
9460
+ method: "POST",
9461
+ path: apiPath`/v1/webhooks/${id}/deliveries/${deliveryId}/redeliver`
9462
+ });
9463
+ }
9464
+ /**
9465
+ * Issue a new signing secret, returned once: store it now. For 24 hours
9466
+ * (`previous_valid_until`) every delivery carries both signatures
9467
+ * (`v1=<new>,v1=<old>`) and `constructEvent` accepts either secret, so
9468
+ * deploy the new one inside that window. A second rotation before then
9469
+ * drops the oldest secret at once. Only the webhook's creator may rotate.
9470
+ * `POST /v1/webhooks/{id}/rotate-secret`
9471
+ *
9472
+ * @example
9473
+ * const { secret, previous_valid_until } = await curviate.webhooks.rotateSecret("wh_123");
9474
+ */
9475
+ rotateSecret(id) {
9476
+ return this.ctx.request({
9477
+ method: "POST",
9478
+ path: apiPath`/v1/webhooks/${id}/rotate-secret`
9479
+ });
9480
+ }
8003
9481
  };
8004
9482
 
8005
9483
  // src/resources/drafts.ts
@@ -8189,7 +9667,12 @@ function buildRootNamespaces(ctx) {
8189
9667
  tier: new TierResource(ctx),
8190
9668
  network: new NetworkResource(ctx),
8191
9669
  unibox: new UniboxResource(ctx),
8192
- analytics: new AnalyticsResource(ctx)
9670
+ analytics: new AnalyticsResource(ctx),
9671
+ billing: new BillingResource(ctx),
9672
+ doNotContact: new DoNotContactResource(ctx),
9673
+ exports: new ExportsResource(ctx),
9674
+ incomingEvents: new IncomingEventsResource(ctx),
9675
+ tenant: new TenantResource(ctx)
8193
9676
  };
8194
9677
  }
8195
9678
  function buildAccountScopedNamespaces(ctx) {
@@ -8208,7 +9691,7 @@ function buildAccountScopedNamespaces(ctx) {
8208
9691
  }
8209
9692
 
8210
9693
  // src/client.ts
8211
- var Curviate = class {
9694
+ var Curviate = class _Curviate {
8212
9695
  constructor(config) {
8213
9696
  this.config = resolveConfig(config);
8214
9697
  const ctx = createContext(this.config);
@@ -8229,6 +9712,11 @@ var Curviate = class {
8229
9712
  this.network = ns.network;
8230
9713
  this.unibox = ns.unibox;
8231
9714
  this.analytics = ns.analytics;
9715
+ this.billing = ns.billing;
9716
+ this.doNotContact = ns.doNotContact;
9717
+ this.exports = ns.exports;
9718
+ this.incomingEvents = ns.incomingEvents;
9719
+ this.tenant = ns.tenant;
8232
9720
  }
8233
9721
  /**
8234
9722
  * Return an account-scoped accessor that fixes `account_id` on every call,
@@ -8252,6 +9740,25 @@ var Curviate = class {
8252
9740
  const ctx = createContext(this.config, accountId);
8253
9741
  return buildAccountScopedNamespaces(ctx);
8254
9742
  }
9743
+ /**
9744
+ * The same client acting in another Team: every call made through the
9745
+ * returned client sends `X-Curviate-Team: <team>`, overriding the `team`
9746
+ * client option. Use it per call with a key that works in all of your
9747
+ * Teams, which a Team-level operation otherwise refuses with
9748
+ * `TEAM_REQUIRED`. A Team key accepts only its own Team
9749
+ * (`TEAM_ACCESS_DENIED`). No network call; `team` is checked as in the
9750
+ * constructor.
9751
+ *
9752
+ * @param team - a Team id (`tnt_...`); `curviate.teams.list()` lists yours.
9753
+ *
9754
+ * @example
9755
+ * for (const team of (await curviate.teams.list()).items) {
9756
+ * const { items } = await curviate.withTeam(team.id).motions.list({ status: ["active"] });
9757
+ * }
9758
+ */
9759
+ withTeam(team) {
9760
+ return new _Curviate({ ...this.config, team });
9761
+ }
8255
9762
  /**
8256
9763
  * Transparent cursor-pagination iterator.
8257
9764
  *
@@ -8335,16 +9842,16 @@ var WebhookSignatureError = class _WebhookSignatureError extends Error {
8335
9842
  function parseHeader(header) {
8336
9843
  const parts = header.split(",");
8337
9844
  let tStr;
8338
- let v1;
9845
+ const v1 = [];
8339
9846
  for (const part of parts) {
8340
9847
  const eq = part.indexOf("=");
8341
9848
  if (eq === -1) continue;
8342
9849
  const key = part.slice(0, eq).trim();
8343
9850
  const val = part.slice(eq + 1).trim();
8344
9851
  if (key === "t") tStr = val;
8345
- else if (key === "v1") v1 = val;
9852
+ else if (key === "v1") v1.push(val);
8346
9853
  }
8347
- if (tStr === void 0 || v1 === void 0) {
9854
+ if (tStr === void 0 || v1.length === 0) {
8348
9855
  throw new WebhookSignatureError(
8349
9856
  "malformed_header",
8350
9857
  'Webhook signature header must contain both "t=<timestamp>" and "v1=<hmac>".'
@@ -8399,20 +9906,29 @@ async function hmacWebCrypto(secret, payload) {
8399
9906
  return bytesToHex(new Uint8Array(sig));
8400
9907
  }
8401
9908
  function verifyAndParse(computedHex, v1, timestamp, bodyStr, replayWindowSecs) {
8402
- let providedBytes;
8403
- try {
8404
- providedBytes = hexToBytes(v1);
8405
- } catch {
9909
+ const computedBytes = hexToBytes(computedHex);
9910
+ let matched = false;
9911
+ let validHex = false;
9912
+ for (const candidate of v1) {
9913
+ let providedBytes;
9914
+ try {
9915
+ providedBytes = hexToBytes(candidate);
9916
+ } catch {
9917
+ continue;
9918
+ }
9919
+ validHex = true;
9920
+ if (constantTimeEqual(computedBytes, providedBytes)) matched = true;
9921
+ }
9922
+ if (!validHex) {
8406
9923
  throw new WebhookSignatureError(
8407
9924
  "invalid_signature",
8408
9925
  "Webhook signature v1 value is not valid hex."
8409
9926
  );
8410
9927
  }
8411
- const computedBytes = hexToBytes(computedHex);
8412
- if (!constantTimeEqual(computedBytes, providedBytes)) {
9928
+ if (!matched) {
8413
9929
  throw new WebhookSignatureError(
8414
9930
  "invalid_signature",
8415
- "Webhook signature does not match. Verify your signing secret."
9931
+ "Webhook signature does not match any v1. Verify your signing secret."
8416
9932
  );
8417
9933
  }
8418
9934
  const nowSecs = Date.now() / 1e3;
@@ -8457,6 +9973,343 @@ async function constructEvent(rawBody, signatureHeader, secret, opts) {
8457
9973
  const computedHex = await hmacWebCrypto(secret, hmacPayload);
8458
9974
  return verifyAndParse(computedHex, v1, timestamp, bodyStr, replayWindowSecs);
8459
9975
  }
9976
+
9977
+ // src/shared/webhook-template.ts
9978
+ var MAX_TEMPLATE_BYTES = 16384;
9979
+ var MAX_TEMPLATE_REFERENCES = 200;
9980
+ var BODY_TEMPLATES_DOCS_URL = "https://docs.curviate.com/reference/webhooks/body-templates";
9981
+ var RULES = {
9982
+ T1: { title: "Empty template", hint: () => "Send null for the standard payload, or write the JSON body your tool expects." },
9983
+ T2: {
9984
+ title: "Not valid JSON",
9985
+ 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."
9986
+ },
9987
+ T3: { title: "Variable never closed", hint: () => "Close the variable with }}, like {{contact.email}}." },
9988
+ T4: { title: "Variable outside a string", hint: (t) => `Put the variable inside a JSON string: "${t}" keeps the value's type.` },
9989
+ T5: { title: "Variable in a key", hint: (t) => `Use a fixed key and put the variable in the value, like ${t}.` },
9990
+ T6: { title: "Unknown variable", hint: (t) => t ? `Use ${t}.` : "Use a variable this source carries: GET /v1/webhooks/variables?source=<source> lists them." },
9991
+ T7: { title: "enrollment.id is reserved", hint: () => "Use {{enrollment_id}} for the Enrollment's id." },
9992
+ 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." },
9993
+ T9: { title: "Single-brace placeholder", hint: (t) => `Write the variable with double braces, like ${t}.` },
9994
+ T10: { title: "Template size", hint: () => `Keep the template to ${MAX_TEMPLATE_BYTES.toLocaleString("en-US")} bytes and ${MAX_TEMPLATE_REFERENCES} variables.` },
9995
+ 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.` }
9996
+ };
9997
+ var WEBHOOK_TEMPLATE_RULE_CODES = Object.keys(RULES);
9998
+ var SCOPES_BY_SOURCE = {
9999
+ motion: ["event", "account", "contact", "motion", "enrollment"],
10000
+ messaging: ["event", "account", "contact", "motion"],
10001
+ user: ["event", "account", "contact", "motion"],
10002
+ account_status: ["event", "account"],
10003
+ post: ["event", "account"]
10004
+ };
10005
+ var ACCOUNT_PARTS = ["first_name", "last_name", "full_name"];
10006
+ var EXTRA_KEY = /^[A-Za-z0-9_]{1,64}$/;
10007
+ var VALUE_KEY = /^[a-z][a-z0-9_]{0,63}$/;
10008
+ var FIXED = [
10009
+ ["event", "event"],
10010
+ ["occurred_at", "event"],
10011
+ ...ACCOUNT_PARTS.map((p) => [`account.${p}`, "account"]),
10012
+ ...STANDARD_FIELDS.map((f) => [`contact.${f}`, "contact"]),
10013
+ ["motion.id", "motion"],
10014
+ ["motion.name", "motion"],
10015
+ ["enrollment_id", "motion"],
10016
+ ["step_id", "enrollment"]
10017
+ ];
10018
+ var SCOPE_OF = new Map(FIXED);
10019
+ var SHORT = new Map([...STANDARD_FIELDS.map((f) => [f, `contact.${f}`]), ...ACCOUNT_PARTS.map((p) => [`my_${p}`, `account.${p}`])]);
10020
+ function resolve(inner) {
10021
+ const raw = inner.trim();
10022
+ if (raw.includes("|"))
10023
+ return { ok: false, code: "T8", rule: `{{${raw.slice(0, raw.indexOf("|")).trim()} | \u2026}} carries a default or a filter` };
10024
+ const name = SHORT.get(raw) ?? raw;
10025
+ if (name === "enrollment.id")
10026
+ return { ok: false, code: "T7", rule: "{{enrollment.id}} is reserved", token: "{{enrollment_id}}", candidates: ["{{enrollment_id}}"] };
10027
+ const fixed = SCOPE_OF.get(name);
10028
+ if (fixed)
10029
+ return { ok: true, name, scope: fixed };
10030
+ if (name.startsWith("contact.extra.") && EXTRA_KEY.test(name.slice(14)))
10031
+ return { ok: true, name, scope: "contact" };
10032
+ if (name.startsWith("enrollment.") && VALUE_KEY.test(name.slice(11)))
10033
+ return { ok: true, name, scope: "enrollment" };
10034
+ return { ok: false, code: "T6", rule: `unknown variable {{${raw}}}` };
10035
+ }
10036
+ var scopeOk = (source, r) => (SCOPES_BY_SOURCE[source] ?? []).includes(r.scope);
10037
+ function candidatesFor(source, raw) {
10038
+ 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)];
10039
+ 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}}}`);
10040
+ }
10041
+ var LITERAL = /-?(?:0|[1-9]\d*)(?:\.\d+)?(?:[eE][+-]?\d+)?|true|false|null/y;
10042
+ var STRUCTURAL = /* @__PURE__ */ new Set(["{", "}", "[", "]", ":", ",", '"']);
10043
+ var what = (c) => c === void 0 ? "the end of the template" : STRUCTURAL.has(c) ? `'${c}'` : "a character that starts no JSON value";
10044
+ function scan(src) {
10045
+ const out = { strings: [], bare: [], error: null };
10046
+ const fail = (at, rule, code = "T2", token) => (out.error = { at, code, rule, ...token ? { token } : {} }, out);
10047
+ const stack = [];
10048
+ let i = 0;
10049
+ let expect = "value";
10050
+ let opened = false;
10051
+ let comma = false;
10052
+ const ws = () => {
10053
+ while (i < src.length && (src[i] === " " || src[i] === " " || src[i] === "\n" || src[i] === "\r"))
10054
+ i++;
10055
+ };
10056
+ const string = () => {
10057
+ let j = i + 1;
10058
+ while (j < src.length) {
10059
+ const c = src.charCodeAt(j);
10060
+ if (c === 34)
10061
+ return j + 1;
10062
+ if (c === 92) {
10063
+ const e = src[j + 1];
10064
+ if (e === "u" ? !/^[0-9a-fA-F]{4}$/.test(src.slice(j + 2, j + 6)) : e === void 0 || !'"\\/bfnrt'.includes(e))
10065
+ return `invalid escape in a string at line ${lineCol(src, j).line}`;
10066
+ j += e === "u" ? 6 : 2;
10067
+ continue;
10068
+ }
10069
+ if (c < 32)
10070
+ return "a control character (a raw line break or tab) inside a string; write it as \\n or \\t";
10071
+ j++;
10072
+ }
10073
+ return "a string is never closed";
10074
+ };
10075
+ for (; ; ) {
10076
+ ws();
10077
+ const c = src[i];
10078
+ if (expect === "next") {
10079
+ const closer = stack.at(-1);
10080
+ if (closer === void 0)
10081
+ return i < src.length ? fail(i, `unexpected ${what(c)} after the JSON value`) : out;
10082
+ if (c === ",") {
10083
+ i++;
10084
+ comma = true;
10085
+ expect = closer === "}" ? "key" : "value";
10086
+ } else if (c === closer) {
10087
+ stack.pop();
10088
+ i++;
10089
+ } else
10090
+ return fail(i, `expected ',' or '${closer}', found ${what(c)}`);
10091
+ continue;
10092
+ }
10093
+ if (opened && c === stack.at(-1)) {
10094
+ stack.pop();
10095
+ i++;
10096
+ opened = false;
10097
+ expect = "next";
10098
+ continue;
10099
+ }
10100
+ if (comma && (c === "}" || c === "]"))
10101
+ return fail(i, `trailing comma before '${c}'`, "T2", `'${c}'`);
10102
+ opened = comma = false;
10103
+ if (c === void 0)
10104
+ return fail(i, "the template ends where a value is expected");
10105
+ if (expect === "key") {
10106
+ if (c !== '"')
10107
+ return fail(i, `expected a double-quoted key, found ${what(c)}`);
10108
+ const end = string();
10109
+ if (typeof end === "string")
10110
+ return fail(i, end);
10111
+ out.strings.push({ start: i, end, key: true });
10112
+ i = end;
10113
+ ws();
10114
+ if (src[i] !== ":")
10115
+ return fail(i, `expected ':' after a key, found ${what(src[i])}`);
10116
+ i++;
10117
+ expect = "value";
10118
+ continue;
10119
+ }
10120
+ if (c === "{" && src[i + 1] === "{") {
10121
+ const close = src.indexOf("}}", i + 2);
10122
+ if (close < 0)
10123
+ return fail(i, "{{ is never closed", "T3");
10124
+ out.bare.push({ start: i, end: close + 2 });
10125
+ i = close + 2;
10126
+ expect = "next";
10127
+ } else if (c === "{" || c === "[") {
10128
+ stack.push(c === "{" ? "}" : "]");
10129
+ i++;
10130
+ opened = true;
10131
+ expect = c === "{" ? "key" : "value";
10132
+ } else if (c === '"') {
10133
+ const end = string();
10134
+ if (typeof end === "string")
10135
+ return fail(i, end);
10136
+ out.strings.push({ start: i, end, key: false });
10137
+ i = end;
10138
+ expect = "next";
10139
+ } else {
10140
+ LITERAL.lastIndex = i;
10141
+ const m = LITERAL.exec(src);
10142
+ if (!m)
10143
+ return fail(i, `unexpected ${what(c)}`);
10144
+ i += m[0].length;
10145
+ expect = "next";
10146
+ }
10147
+ }
10148
+ }
10149
+ function lineCol(src, at) {
10150
+ let line = 1;
10151
+ let start = 0;
10152
+ for (let k = src.indexOf("\n"); k !== -1 && k < at; k = src.indexOf("\n", k + 1)) {
10153
+ line++;
10154
+ start = k + 1;
10155
+ }
10156
+ return { line, col: at - start + 1 };
10157
+ }
10158
+ function lintWebhookTemplate(template, opts) {
10159
+ const found = [];
10160
+ const finish = () => {
10161
+ 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));
10162
+ return { errors: all.filter((e) => e.severity === "error"), warnings: all.filter((e) => e.severity === "warning") };
10163
+ };
10164
+ if (template.trim() === "") {
10165
+ found.push({ at: 0, code: "T1", rule: "the template is empty; send null for the standard payload" });
10166
+ return finish();
10167
+ }
10168
+ const bytes = new TextEncoder().encode(template).length;
10169
+ if (bytes > MAX_TEMPLATE_BYTES) {
10170
+ found.push({ at: 0, code: "T10", rule: `the template is ${bytes} bytes; the limit is ${MAX_TEMPLATE_BYTES}` });
10171
+ return finish();
10172
+ }
10173
+ const s = scan(template);
10174
+ let refs = 0;
10175
+ const counted = (at) => {
10176
+ refs += 1;
10177
+ if (refs === MAX_TEMPLATE_REFERENCES + 1)
10178
+ found.push({ at, code: "T10", rule: `the template has more than ${MAX_TEMPLATE_REFERENCES} variables` });
10179
+ };
10180
+ const judge = (at, inner) => {
10181
+ const r = resolve(inner);
10182
+ if (!r.ok) {
10183
+ const raw = inner.trim();
10184
+ const dotless = r.code === "T6" && !raw.includes(".") && /^[A-Za-z0-9_]+$/.test(raw);
10185
+ const extra = dotless && (SCOPES_BY_SOURCE[opts.source] ?? []).includes("contact") ? `{{contact.extra.${raw}}}` : void 0;
10186
+ const near = r.code === "T6" ? candidatesFor(opts.source, raw) : r.candidates ?? [];
10187
+ const candidates = extra ? [extra, ...near.filter((c) => c !== extra)] : near;
10188
+ found.push({ at, code: r.code, rule: r.rule, ...r.token ?? extra ?? candidates[0] ? { token: r.token ?? extra ?? candidates[0] } : {}, ...candidates.length ? { candidates } : {} });
10189
+ return;
10190
+ }
10191
+ if (!scopeOk(opts.source, r)) {
10192
+ found.push({ at, code: "T6", rule: `{{${inner.trim()}}} is not carried by ${opts.source} events` });
10193
+ return;
10194
+ }
10195
+ const key = r.name.startsWith("enrollment.") ? r.name.slice(11) : null;
10196
+ if (key !== null && opts.saved && !opts.saved.has(key))
10197
+ found.push({ at, code: "TW1", rule: `{{${r.name}}}: no current Sequence of this Team saves ${key}`, token: key });
10198
+ };
10199
+ for (const str of s.strings) {
10200
+ const raw = template.slice(str.start + 1, str.end - 1);
10201
+ const base = str.start + 1;
10202
+ for (let from = 0; ; ) {
10203
+ const k = raw.indexOf("{{", from);
10204
+ if (k < 0)
10205
+ break;
10206
+ if (raw[k - 1] === "\\" && raw[k - 2] === "\\") {
10207
+ from = k + 2;
10208
+ continue;
10209
+ }
10210
+ const close = raw.indexOf("}}", k + 2);
10211
+ if (close < 0) {
10212
+ found.push({ at: base + k, code: "T3", rule: "{{ is never closed" });
10213
+ break;
10214
+ }
10215
+ const inner = raw.slice(k + 2, close);
10216
+ counted(base + k);
10217
+ if (str.key) {
10218
+ const leaf = (resolve(inner).ok ? SHORT.get(inner.trim()) ?? inner.trim() : inner.trim()).split(".").pop() || "value";
10219
+ found.push({ at: base + k, code: "T5", rule: `{{${inner.trim()}}} is in an object key`, token: `"${leaf}": "{{${inner.trim()}}}"` });
10220
+ } else
10221
+ judge(base + k, inner);
10222
+ from = close + 2;
10223
+ }
10224
+ if (!str.key) {
10225
+ for (const m of raw.matchAll(/\{([A-Za-z_][A-Za-z0-9_.]*)\}/g)) {
10226
+ const k = m.index;
10227
+ if (raw[k - 1] === "{" || raw[k + m[0].length] === "}")
10228
+ continue;
10229
+ found.push({ at: base + k, code: "T9", rule: `{${m[1]}} is a single-brace placeholder`, token: `{{${m[1]}}}` });
10230
+ }
10231
+ }
10232
+ }
10233
+ for (const b of s.bare) {
10234
+ const inner = template.slice(b.start + 2, b.end - 2).trim();
10235
+ counted(b.start);
10236
+ found.push({ at: b.start, code: "T4", rule: `{{${inner}}} is outside a JSON string`, token: `{{${inner}}}` });
10237
+ }
10238
+ if (s.error)
10239
+ 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 } : {} });
10240
+ return finish();
10241
+ }
10242
+ function entry(src, f) {
10243
+ const { line, col } = lineCol(src, f.at);
10244
+ return {
10245
+ path: "body_template",
10246
+ rule: f.rule,
10247
+ code: f.code,
10248
+ message: f.rule,
10249
+ pointer: "/body_template",
10250
+ line,
10251
+ col,
10252
+ hint: RULES[f.code].hint(f.token ?? ""),
10253
+ docs_url: `${BODY_TEMPLATES_DOCS_URL}#${f.code.toLowerCase()}`,
10254
+ severity: f.code === "TW1" ? "warning" : "error",
10255
+ ...f.candidates?.length ? { candidates: f.candidates } : {}
10256
+ };
10257
+ }
10258
+ var isObj2 = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
10259
+ var own = (o, k) => isObj2(o) && Object.hasOwn(o, k) ? o[k] : void 0;
10260
+ function valueOf(name, ctx) {
10261
+ const [ns, a, ...rest] = name.split(".");
10262
+ let v;
10263
+ if (a === void 0)
10264
+ v = own(ctx, ns);
10265
+ else if (ns === "contact" && a === "extra")
10266
+ v = own(own(ctx.contact, "extra"), rest.join("."));
10267
+ else if (rest.length === 0)
10268
+ v = own(ns === "contact" ? ctx.contact : ns === "account" ? ctx.account : ns === "motion" ? ctx.motion : ns === "enrollment" ? ctx.enrollment : void 0, a);
10269
+ if (typeof v === "string")
10270
+ return v.trim() === "" ? void 0 : v;
10271
+ if (typeof v === "number")
10272
+ return Number.isFinite(v) ? v : void 0;
10273
+ return typeof v === "boolean" ? v : void 0;
10274
+ }
10275
+ var DROP = /* @__PURE__ */ Symbol("drop");
10276
+ var WHOLE = /^\{\{([^{}]*)\}\}$/;
10277
+ function renderWebhookTemplate(template, ctx) {
10278
+ const look = (inner) => {
10279
+ const r = resolve(inner);
10280
+ return r.ok ? valueOf(r.name, ctx) : void 0;
10281
+ };
10282
+ const sub = (v) => {
10283
+ if (typeof v === "string") {
10284
+ const whole = WHOLE.exec(v);
10285
+ if (whole)
10286
+ return look(whole[1]) ?? DROP;
10287
+ return v.replace(/\\\{\{|\{\{([^{}]*)\}\}/g, (m, inner) => inner === void 0 ? "{{" : String(look(inner) ?? ""));
10288
+ }
10289
+ if (Array.isArray(v))
10290
+ return v.map(sub).filter((x) => x !== DROP);
10291
+ if (isObj2(v)) {
10292
+ const o = {};
10293
+ for (const [k, x] of Object.entries(v)) {
10294
+ const r = sub(x);
10295
+ if (r !== DROP)
10296
+ o[k] = r;
10297
+ }
10298
+ return o;
10299
+ }
10300
+ return v;
10301
+ };
10302
+ const out = sub(JSON.parse(template));
10303
+ return JSON.stringify(out === DROP ? null : out);
10304
+ }
10305
+
10306
+ // src/templates.ts
10307
+ function validateWebhookTemplate(template, opts) {
10308
+ return lintWebhookTemplate(template, { source: opts.source });
10309
+ }
10310
+ function renderWebhookTemplate2(template, context) {
10311
+ return renderWebhookTemplate(template, context);
10312
+ }
8460
10313
  export {
8461
10314
  Curviate,
8462
10315
  CurviateError,
@@ -8468,6 +10321,8 @@ export {
8468
10321
  constructEvent,
8469
10322
  isCurviateError,
8470
10323
  newStepId,
10324
+ renderWebhookTemplate2 as renderWebhookTemplate,
8471
10325
  step,
8472
- validateSequence
10326
+ validateSequence,
10327
+ validateWebhookTemplate
8473
10328
  };