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