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