@curviate/sdk 0.24.3 → 0.25.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -31,6 +31,14 @@ var ERROR_CODES = [
31
31
  // response headers). Distinct from the account/tenant/platform trio above.
32
32
  // Always retry-safe (retry_likely_to_succeed: true); see RETRYABLE_CODES.
33
33
  "RATE_LIMITED",
34
+ // Curviate's OWN account-safety ceiling, not a request-rate limit and not
35
+ // LinkedIn refusing. A 429 that never reached LinkedIn and spent nothing:
36
+ // the budget row named by `budgetRow` is at the ceiling configured on
37
+ // `PATCH /v1/{account_id}/safety-policy`, or the account is outside its
38
+ // activity window. Deliberately absent from RETRYABLE_CODES: a monthly
39
+ // row's reset can be weeks out, so backing off is the wrong recovery.
40
+ // Read `resetAt`, `safetyReason` and `safetyHint` instead.
41
+ "BUDGET_EXHAUSTED",
34
42
  // Platform errors
35
43
  "PLATFORM_ERROR",
36
44
  "PLATFORM_NOT_IMPLEMENTED",
@@ -60,6 +68,14 @@ var ERROR_CODES = [
60
68
  "REAUTH_REQUIRED",
61
69
  // LinkedIn-specific connect errors
62
70
  "LINKEDIN_AUTH_FAILED",
71
+ // LinkedIn allows only one session at a time for some accounts, so a person
72
+ // signing in elsewhere breaks this one. A 401 like LINKEDIN_AUTH_FAILED and a
73
+ // separate code because the remedy differs and the auth one prescribes the
74
+ // wrong remedy: reconnecting does nothing while the other session is open.
75
+ // user_fixable, never retryable: a person has to close the other session.
76
+ // While it lasts, `account_states` on the account resource carries
77
+ // `recruiter_session_evicted`.
78
+ "LINKEDIN_SESSION_EVICTED",
63
79
  "LINKEDIN_RATE_LIMITED",
64
80
  "LINKEDIN_COOKIE_INVALID",
65
81
  "LINKEDIN_SERVICE_UNAVAILABLE",
@@ -94,6 +110,12 @@ var CurviateError = class _CurviateError extends Error {
94
110
  this.retryLikelyToSucceed = init.retryLikelyToSucceed;
95
111
  this.requiredTier = init.requiredTier;
96
112
  this.retryAfterMs = init.retryAfterMs;
113
+ this.budgetRow = init.budgetRow;
114
+ this.retryAfterSeconds = init.retryAfterSeconds;
115
+ this.resetAt = init.resetAt;
116
+ this.safetyHint = init.safetyHint;
117
+ this.safetyReason = init.safetyReason;
118
+ this.blocked = init.blocked;
97
119
  Object.setPrototypeOf(this, _CurviateError.prototype);
98
120
  }
99
121
  /**
@@ -113,6 +135,12 @@ var CurviateError = class _CurviateError extends Error {
113
135
  if (this.httpStatus !== void 0) json.httpStatus = this.httpStatus;
114
136
  if (this.requiredTier !== void 0) json.requiredTier = this.requiredTier;
115
137
  if (this.retryAfterMs !== void 0) json.retryAfterMs = this.retryAfterMs;
138
+ if (this.budgetRow !== void 0) json.budgetRow = this.budgetRow;
139
+ if (this.retryAfterSeconds !== void 0) json.retryAfterSeconds = this.retryAfterSeconds;
140
+ if (this.resetAt !== void 0) json.resetAt = this.resetAt;
141
+ if (this.safetyHint !== void 0) json.safetyHint = this.safetyHint;
142
+ if (this.safetyReason !== void 0) json.safetyReason = this.safetyReason;
143
+ if (this.blocked !== void 0) json.blocked = this.blocked;
116
144
  return json;
117
145
  }
118
146
  };
@@ -152,6 +180,11 @@ var RETRYABLE_CODES = /* @__PURE__ */ new Set([
152
180
  "RATE_LIMIT_ACCOUNT",
153
181
  "RATE_LIMIT_TENANT",
154
182
  "RATE_LIMITED"
183
+ // BUDGET_EXHAUSTED is deliberately NOT here, even though it is a 429. It is
184
+ // Curviate's own account-safety ceiling rather than a request-rate limit:
185
+ // nothing reached LinkedIn, nothing was spent, and a monthly row's reset can
186
+ // be weeks out, so a backoff loop burns retries against a wall that will not
187
+ // move. The recovery is `resetAt` / `safetyHint`, not a sleep.
155
188
  ]);
156
189
  var WRITE_METHODS = /* @__PURE__ */ new Set(["POST", "PATCH", "PUT", "DELETE"]);
157
190
  function defaultSleep(ms) {
@@ -170,6 +203,15 @@ function backoffDelay(n, jitter) {
170
203
  function toErrorCode(code) {
171
204
  return code && KNOWN_ERROR_CODES.has(code) ? code : "INTERNAL";
172
205
  }
206
+ function toSafetyHint(hint) {
207
+ if (!hint || typeof hint !== "object") return void 0;
208
+ const { parameter, message } = hint;
209
+ if (typeof parameter !== "string" || typeof message !== "string") return void 0;
210
+ return { parameter, message };
211
+ }
212
+ function toSafetyReason(reason) {
213
+ return reason === "ceiling" || reason === "activity_window" ? reason : void 0;
214
+ }
173
215
  function toRetryHint(hint) {
174
216
  if (!hint || typeof hint !== "object") return null;
175
217
  const kind = hint.kind;
@@ -187,6 +229,8 @@ async function errorFromResponse(res) {
187
229
  env = void 0;
188
230
  }
189
231
  const requiredTier = env?.required_tier && KNOWN_REQUIRED_TIERS.has(env.required_tier) ? env.required_tier : void 0;
232
+ const safetyHint = toSafetyHint(env?.hint);
233
+ const safetyReason = toSafetyReason(env?.reason);
190
234
  return new CurviateError({
191
235
  code: toErrorCode(env?.code),
192
236
  message: env?.message ?? `Request failed with status ${res.status}.`,
@@ -195,7 +239,23 @@ async function errorFromResponse(res) {
195
239
  userFixable: env?.user_fixable ?? false,
196
240
  retryLikelyToSucceed: env?.retry_likely_to_succeed ?? false,
197
241
  ...requiredTier !== void 0 ? { requiredTier } : {},
198
- ...retryAfterMs !== void 0 ? { retryAfterMs } : {}
242
+ ...retryAfterMs !== void 0 ? { retryAfterMs } : {},
243
+ ...typeof env?.row === "string" ? { budgetRow: env.row } : {},
244
+ // `retry_after` is DELIBERATELY NOT folded into `retryAfterMs`. Everything
245
+ // that reads `retryAfterMs` SLEEPS it, on reads and on writes; a paused
246
+ // budget row can be paused for an hour, and turning that into an hour-long
247
+ // sleep inside a client call would be a hang, not a backoff. It stays on
248
+ // `retryAfterSeconds` for the caller to decide about, next to `budgetRow`,
249
+ // which is the field that says what to do instead: switch work.
250
+ ...typeof env?.retry_after === "number" && Number.isFinite(env.retry_after) ? { retryAfterSeconds: env.retry_after } : {},
251
+ // `reset_at` is null-BEARING: null means "no clock frees this" (the
252
+ // pending_invites gauge, or an InMail credit exhaustion), which is a
253
+ // different fact from the field being absent, so both survive the round
254
+ // trip.
255
+ ...typeof env?.reset_at === "string" || env?.reset_at === null ? { resetAt: env.reset_at } : {},
256
+ ...safetyHint !== void 0 ? { safetyHint } : {},
257
+ ...safetyReason !== void 0 ? { safetyReason } : {},
258
+ ...typeof env?.blocked === "boolean" ? { blocked: env.blocked } : {}
199
259
  });
200
260
  }
201
261
  function retryDelay(err, attempt, jitter) {
@@ -278,7 +338,8 @@ async function execute(method, path, opts) {
278
338
  return parseSuccess(res);
279
339
  }
280
340
  const err = await errorFromResponse(res);
281
- const retryable = !isWrite && RETRYABLE_CODES.has(err.code) && attempt < opts.maxRetries;
341
+ const namesBudgetRow = err.budgetRow !== void 0;
342
+ const retryable = !isWrite && !namesBudgetRow && RETRYABLE_CODES.has(err.code) && attempt < opts.maxRetries;
282
343
  if (retryable) {
283
344
  await sleep(retryDelay(err, attempt + 1, jitterFn()));
284
345
  attempt += 1;
@@ -388,8 +449,31 @@ var AccountsResource = class {
388
449
  }
389
450
  /**
390
451
  * Return metadata and current state for one connected account, including the
391
- * central `quotas[]` view for all tracked quota families and `seat_id` (the
392
- * seat this account occupies, `null` for an admin seatless account).
452
+ * central `quotas[]` account-safety view, `account_states[]` and `seat_id`
453
+ * (the seat this account occupies, `null` for an admin seatless account).
454
+ *
455
+ * `quotas[]` REPLACED its earlier `quota_name` / `remaining` / `total` /
456
+ * `reset_time` / `recommended_throttle_hint` shape. Each row now reports
457
+ * `budget_row`, `kind`, `used`, `ceiling`, `effective_ceiling`, `band`,
458
+ * `posture`, `over_default`, `halt` and the window fields. **Compare `used`
459
+ * against `effective_ceiling`, not `ceiling`**: the effective one is the
460
+ * ceiling with the account's warm-up ramp applied, and on a new account the
461
+ * two differ by roughly a factor of seven. `posture` says what happens at the
462
+ * ceiling and defaults to `warn`, so nothing here refuses until it is
463
+ * configured to. Two rows are not counters and say so in `kind`:
464
+ * `total_actions` (`tally`) and `pending_invites` (`gauge`, whose `used` is
465
+ * `null` until something has observed it).
466
+ *
467
+ * The per-minute REQUEST ceiling is a different thing and is deliberately
468
+ * NOT in this array: it protects Curviate's own servers rather than the
469
+ * LinkedIn account, and it is always enforced with `429` whatever the
470
+ * posture.
471
+ *
472
+ * `account_states[]` lists platform conditions the account is in right now,
473
+ * additive to `status` and independent of it: `recruiter_session_evicted`,
474
+ * `commercial_use_limited`, `profile_view_limited`. The last two are read on
475
+ * calls that SUCCEEDED, so check them before concluding thin results are
476
+ * wrong. Empty for an unaffected account.
393
477
  *
394
478
  * This is a stale-while-revalidate read; it always returns immediately
395
479
  * from the cached row (never blocks on a live substrate call). The cached
@@ -698,6 +782,16 @@ var MessagingResource = class {
698
782
  * Free-text search the account's own inbox; matches participant names
699
783
  * and message content. `GET /v1/{account_id}/chats/search`
700
784
  *
785
+ * SERVED FROM CURVIATE'S STORE, not from LinkedIn: it costs no platform call
786
+ * and draws down no account-safety budget, and a chat that has never been
787
+ * retrieved for this account cannot match. Read `coverage` on the response to
788
+ * see how complete the searched corpus is before treating an empty result as
789
+ * "no such conversation".
790
+ *
791
+ * A result's `type` is `1to1`, `group` or `channel`. It used to be documented
792
+ * as never a channel; that is no longer true, so a caller switching on `type`
793
+ * needs the third arm.
794
+ *
701
795
  * @param params - `query` (required free-text term) plus `limit` and an
702
796
  * opaque `cursor` for pagination.
703
797
  */
@@ -2769,6 +2863,7 @@ async function constructEvent(rawBody, signatureHeader, secret, opts) {
2769
2863
  export {
2770
2864
  Curviate,
2771
2865
  CurviateError,
2866
+ ERROR_CODES,
2772
2867
  WebhookSignatureError,
2773
2868
  constructEvent,
2774
2869
  isCurviateError
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@curviate/sdk",
3
- "version": "0.24.3",
3
+ "version": "0.25.0",
4
4
  "private": false,
5
5
  "description": "TypeScript SDK for the Curviate API.",
6
6
  "license": "MIT",
@@ -38,6 +38,7 @@
38
38
  "scripts": {
39
39
  "build": "tsup",
40
40
  "prepack": "node scripts/check-clean.mjs && node scripts/check-copy.mjs && tsup && node scripts/check-clean.mjs --dist",
41
+ "check:cli-notice": "node scripts/check-cli-notice.mjs",
41
42
  "test": "vitest run",
42
43
  "test:watch": "vitest",
43
44
  "typecheck": "tsc --noEmit",
@@ -58,6 +59,7 @@
58
59
  "@types/node": "^22.15.21",
59
60
  "msw": "^2.7.0",
60
61
  "openapi-typescript": "7.6.1",
62
+ "semver": "^7.8.5",
61
63
  "tsup": "^8.3.5",
62
64
  "typescript": "^5.7.2",
63
65
  "vitest": "^3.2.2"