@curviate/sdk 0.24.3 → 0.26.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 CHANGED
@@ -7,7 +7,180 @@ Versioning: semantic. Minor for additive changes, patch for bug fixes; no stabil
7
7
 
8
8
  ---
9
9
 
10
- ## [Unreleased]
10
+ ## [0.26.0] - 2026-09-06
11
+
12
+ Fixture regenerated against the deployed production document
13
+ (`https://api.curviate.com`, `server_git_sha bd795601...`, 125 paths).
14
+ **BREAKING at the type level** for a consumer that reads `row` off an error
15
+ or a safety warning as a `string`: it is now `string | null` on both. Three
16
+ response enums also widen, which reds an exhaustive `switch` with a `never`
17
+ default. Everything else in this release is additive.
18
+
19
+ ### Added
20
+
21
+ - **`NOT_STORED` in the error taxonomy.** The `422` a read gets when
22
+ `mode=cache_only` (or `refill` on a resource that cannot be filled) finds
23
+ nothing in Curviate's store, so the API refuses rather than reaching
24
+ LinkedIn. It was missing from `ERROR_CODES`, so it decoded to `INTERNAL`:
25
+ a caller could not branch on it, and, the harmful half, `INTERNAL` is
26
+ retryable, so a `cache_only` miss was retried on a GET against an answer
27
+ that cannot change until the caller picks another mode. It is now decoded to
28
+ itself and is deliberately absent from the retryable set. Not the same as
29
+ `RESOURCE_NOT_FOUND`: the resource may exist and this API simply holds no
30
+ copy, so the fix is another mode, not another id. `cache_only` is the mode
31
+ that raises it on these reads, and `refill` or `auto` is the remedy.
32
+
33
+ **This reds `@curviate/cli`'s exit-code exhaustiveness guard, by design.**
34
+ That guard iterates the exported `ERROR_CODES` array rather than a copy of
35
+ it, which is the whole reason the array is exported, so a code added here
36
+ with no exit-code mapping is supposed to fail it. Map `NOT_STORED` there when
37
+ bumping the pin.
38
+ - **`messaging.getChat(chatId, params?)` takes the retrieval query.**
39
+ `GET /v1/{account_id}/chats/{chat_id}` declares `expand`, `mode` and
40
+ `max_age`; the method sent no query at all, so a chat read was the one
41
+ store-servable read with no retrieval ladder. `ChatGetQuery` is derived from
42
+ the generated `paths` types, like every other query type here.
43
+ `users.get()` and `messaging.listMessages()` already carried the pair and
44
+ are unchanged; the README now documents all three.
45
+
46
+ ### Regenerated types
47
+
48
+ - **BREAKING - `row` is nullable on both `Error` and `SafetyWarning`.**
49
+ `Error.row` goes `string | undefined` to `string | null | undefined`, and
50
+ `SafetyWarning.row` - which is REQUIRED, so it is the one that will actually
51
+ break a build - goes `string` to `string | null`. `null` says the breach
52
+ names no row: an `activity_window` refusal on an action that spends no
53
+ budget of its own has an hour to answer for and no counter. Read
54
+ `hint.parameter` in that case; it names `posture`. A consumer that assigned
55
+ `.row` to a `string` or passed it somewhere non-null now has a type error,
56
+ and that error is correct - the runtime value was always going to be `null`
57
+ once the server started sending it. The SDK's own `CurviateError.budgetRow`
58
+ is UNCHANGED (`string | undefined`): `transport.ts` maps the wire field with
59
+ a `typeof === "string"` guard, so a `null` row arrives as an absent
60
+ `budgetRow`, which is what "no row to name" already meant on that surface.
61
+ - **BREAKING for an exhaustive `switch` - three response enums widen.** Each
62
+ gains a member, so a `switch` with a `never`-typed default arm stops
63
+ compiling. None of them removes a member, so a `switch` with a real default
64
+ keeps working:
65
+ - `GET /v1/{account_id}/safety-events` `items[].reason` gains
66
+ `activity_window` alongside `budget_exhausted` and `rate_limited`.
67
+ - `GET /v1/{account_id}/safety-events` `items[].budget_row` gains `"*"`, the
68
+ wildcard for an event that is not scoped to one row.
69
+ - `notices[].code` on the ten chat and message reads that accept
70
+ `expand=public_identifier` gains `EXPANSION_WITHHELD_ACTIVITY_WINDOW`
71
+ alongside `EXPANSION_LIMIT_REACHED`: the expansion was refused because the
72
+ account is outside its activity window, not because the page ran out of
73
+ lookup budget. Same consequence for the reader either way -
74
+ `public_identifier` is `null` for reasons that are not "no public profile"
75
+ - but a different fix, so it is a separate code. The `notices` key itself
76
+ is not new.
77
+ - **Additive - `interface_ceiling` and `effective_interface_ceiling` on every
78
+ budget row.** Optional, `number | null`. They appear on `quotas[]` from
79
+ `GET /v1/accounts/{account_id}` and on the policy rows from
80
+ `GET`/`PATCH /v1/{account_id}/safety-policy`. What a call through the
81
+ matching ELEVATED interface (`/sales-navigator/...`, `/recruiter/...`) is
82
+ held to on that row; `null` wherever the row and limit profile have no such
83
+ figure, which is every row on `basic` and `premium`. `ceiling`,
84
+ `effective_ceiling`, `band` and `over_default` all remain the STANDARD
85
+ figure, so a row can report `band: "over"` while an elevated call still
86
+ succeeds. Read `effective_interface_ceiling`, not `interface_ceiling`, for
87
+ the same reason `effective_ceiling` and not `ceiling` answers that on the
88
+ standard side. On the `PATCH` request body both are typed `unknown` and
89
+ documented READ-ONLY: sent back they are accepted and ignored, so a read can
90
+ round-trip unedited.
91
+ - **Additive - `activity_window` on the `PATCH /v1/{account_id}/safety-policy`
92
+ request body**, typed `unknown` and documented NOT ACCEPTED. Send the
93
+ settable subfields instead: top-level `timezone`, and per row
94
+ `activity_window_start`, `activity_window_end`, `activity_window_timezone`,
95
+ `activity_window_applies_to`.
96
+ - Description-only, no type change: `reset_at` on `Error` and `SafetyWarning`
97
+ now states that it is the window roll on a `ceiling` refusal and the next
98
+ window open on an `activity_window` one, and restates the two cases where no
99
+ clock frees the account (the `pending_invites` gauge, and InMail credits).
100
+ The chat-search `q` parameter documents that a 1-2 character term is slower.
101
+
102
+ ## [0.25.0] - 2026-09-05
103
+
104
+ **BREAKING** for a consumer that pinned the old type declarations: `quotas[]`
105
+ on `GET /v1/accounts/{account_id}` is a different array, and `message_id` on
106
+ `startChat`, `sendMessage`, `sendInMail` and the company chat reply is now
107
+ `string | string[] | null` where it was `string`. Both are detailed under
108
+ "Regenerated types" below. Pre-1.0 a breaking change ships as a minor, so a
109
+ caret range on `0.24.x` will not pick this up.
110
+
111
+ ### Added
112
+
113
+ - **`BUDGET_EXHAUSTED`, the account-safety refusal, and its payload.** A `429`
114
+ that is Curviate's own ceiling rather than a request-rate limit: nothing
115
+ reached LinkedIn and nothing was spent. `CurviateError` gains `budgetRow`
116
+ (wire `row`), `resetAt` (`reset_at`, and `null` in the two cases no clock
117
+ frees: the `pending_invites` gauge and an InMail credit exhaustion),
118
+ `safetyHint` (`hint`, the settable parameter
119
+ on `PATCH /v1/{account_id}/safety-policy`), `safetyReason` (`reason`:
120
+ `ceiling` or `activity_window`) and `blocked`. All six ride `toJSON()`.
121
+ The code is deliberately NOT retryable: a monthly row's reset can be weeks
122
+ out, so a backoff loop burns retries against a wall that will not move.
123
+ On a successful response the same payload arrives under `safety_warning`
124
+ with `blocked: false`, typed on the generated response types.
125
+ - **`budgetRow` and `retryAfterSeconds` on a paused-row `PLATFORM_RATE_LIMIT`.**
126
+ Same wire `row`, different meaning: LinkedIn refused a recent call on that
127
+ row, so the next one is refused locally until the pause lifts. The pause is
128
+ scoped to `(account, row)`, so the recovery is to switch work rather than
129
+ back off across the account. `retry_after` is deliberately NOT folded into
130
+ `retryAfterMs`, which everything sleeps.
131
+ - **A response carrying `row` is never auto-retried, on any method.**
132
+ - **`LINKEDIN_SESSION_EVICTED`.** A 401 split from `LINKEDIN_AUTH_FAILED`
133
+ because the remedies differ: reconnecting does nothing while the other
134
+ session is open. Not retryable.
135
+
136
+ ### Changed
137
+
138
+ - **Fixture regenerated against the deployed production document**
139
+ (`https://api.curviate.com`, `git_sha 33b42521...`), and the generated
140
+ types with it. Two shape changes are BREAKING for a consumer that pinned
141
+ the old declarations, and one enum widened. See "Regenerated types" below.
142
+
143
+ ### Regenerated types
144
+
145
+ - **BREAKING - `quotas[]` on `GET /v1/accounts/{account_id}` is a different
146
+ array.** `quota_name`, `remaining`, `total`, `reset_time` and
147
+ `recommended_throttle_hint` are gone. Each row now carries `budget_row`,
148
+ `kind` (`debit` / `tally` / `gauge`), `window`, `window_start`, `resets_at`,
149
+ `used`, `ceiling`, `effective_ceiling`, `band`, `posture`, `over_default`,
150
+ `halt` and, on the gauge, `observed_at`. **Compare `used` against
151
+ `effective_ceiling`**, which is `ceiling` with the account's warm-up ramp
152
+ applied; on a new account the two differ by roughly a factor of seven.
153
+ `used` is `null` only on the gauge, and only before anything has observed
154
+ it. The per-minute REQUEST ceiling is deliberately not in this array.
155
+ - **BREAKING - `message_id` is `string | string[] | null`** on `startChat`,
156
+ `sendMessage`, `sendInMail` and the company chat reply, where it was
157
+ `string`. `null` when no message was actually sent, an array when the
158
+ message was delivered as several, one per attachment. Confirm a send with
159
+ `chat_id`, which is always present.
160
+ - Chat-search results: `type` gains `channel`. The generated comment used to
161
+ say search results are never channels; that is no longer true, so an
162
+ exhaustive `switch` over `type` needs a third arm.
163
+ - Additive: `GET`/`PATCH /v1/{account_id}/safety-policy`,
164
+ `GET /v1/{account_id}/safety-events`, the four retained-event operations
165
+ under `/v1/{account_id}/events`, a `SafetyWarning` component reachable as
166
+ `safety_warning` on 149 success responses, six new optional fields on the
167
+ shared `Error` component, `coverage` on chat search, `mode` and `max_age`
168
+ query params on the store-served reads, `account_states[]` on the account
169
+ resource, and `observed_at` / `source` / `withdrawn` becoming REQUIRED on
170
+ four entity reads (a response gaining guarantees, not losing them).
171
+
172
+ - **`postpublish` now notices a stale CLI pin.** After every `npm publish`,
173
+ `scripts/check-cli-notice.mjs` compares the version just published
174
+ against what `@curviate/cli` actually declares for `@curviate/sdk` on the
175
+ registry (never a dist-tag lookup, since `npm view @curviate/sdk version`
176
+ returns `latest` and this package's own RC/0.x releases route to `--tag
177
+ next`). Exits non-zero, loudly, when the CLI has not picked up the
178
+ release yet. It cannot undo the publish and does not try to; it replaces
179
+ the prior manual runbook step, which could not fail by construction
180
+ (the SDK-CLI drift-notice defect).
181
+ - `check:clean` now flags a committed npm auth token (`npm_`-prefixed
182
+ shape), and `.npmrc`/`*.tgz` are gitignored. Same gap as the sibling
183
+ `cli` package's copy of this scanner (security-auditor F3).
11
184
 
12
185
  ## [0.24.3] - 2026-08-27
13
186
 
package/README.md CHANGED
@@ -109,9 +109,26 @@ try {
109
109
  // err.retryAfterMs tells you exactly how long to wait
110
110
  await sleep(err.retryAfterMs ?? 5_000);
111
111
  break;
112
+ case "BUDGET_EXHAUSTED":
113
+ // Curviate's OWN account-safety ceiling, not a request-rate limit:
114
+ // nothing reached LinkedIn and nothing was spent, so backing off is the
115
+ // wrong move. Wait until err.resetAt, or raise the setting the hint
116
+ // names on PATCH /v1/{account_id}/safety-policy.
117
+ // resetAt is null in the two cases no clock frees: the pending_invites
118
+ // backlog, and an InMail credit pool LinkedIn regrants on its own schedule.
119
+ console.warn(`${err.budgetRow} is spent until ${err.resetAt ?? "no fixed time"}`);
120
+ console.warn(`change ${err.safetyHint?.parameter} to lift it`);
121
+ break;
112
122
  case "ACCOUNT_NOT_FOUND":
113
123
  console.error("Account does not exist for this tenant.");
114
124
  break;
125
+ case "NOT_STORED":
126
+ // A cache_only read the store could not answer, so nothing was fetched.
127
+ // The id may be perfectly good, so do not go looking for it again:
128
+ // re-read with mode "refill" or "auto", and on a narrowed listing drop
129
+ // the narrowing parameters. See "Retrieval modes" below.
130
+ console.warn("Nothing stored for that resource under this mode.");
131
+ break;
115
132
  default:
116
133
  if (err.retryLikelyToSucceed) {
117
134
  // Safe to retry: server-side transient error
@@ -127,6 +144,67 @@ exported `ErrorCode` type is the complete set, so `tsc` tells you when a `switch
127
144
 
128
145
  ---
129
146
 
147
+ ## Retrieval modes
148
+
149
+ Some reads can be answered from Curviate's own store instead of a live LinkedIn
150
+ fetch. Those reads take the same two query parameters, `mode` and `max_age`:
151
+
152
+ - `users.get()`, including `users.get("me")` (`GET /v1/{account_id}/users/{user_id}`)
153
+ - `messaging.getChat()` (`GET /v1/{account_id}/chats/{chat_id}`)
154
+ - `messaging.listMessages()` (`GET /v1/{account_id}/chats/{chat_id}/messages`)
155
+
156
+ No other read accepts them, so do not pass them elsewhere. These two keys are
157
+ refused rather than ignored: sending either to a read that does not declare it
158
+ is a `400`, and so is sending either one twice. A read that accepted
159
+ `mode=cache_only` and then called LinkedIn anyway would break the one guarantee
160
+ that parameter makes, so neither case is resolved quietly.
161
+
162
+ | `mode` | What the read does |
163
+ | --- | --- |
164
+ | `auto` (default) | serves a stored copy while it is inside the resource's freshness threshold, otherwise fetches |
165
+ | `live` | always fetches |
166
+ | `refill` | serves a stored copy at any age, and fetches once when this read has none |
167
+ | `cache_only` | never fetches, and throws `NOT_STORED` when the store cannot answer |
168
+
169
+ `max_age` is the mechanism the first three are presets over: the oldest stored
170
+ copy, in seconds, the read will accept. It overrides them in both directions,
171
+ and `max_age: 0` is the same as `mode: "live"`. It cannot be combined with
172
+ `cache_only`, whose guarantee is not a freshness threshold; that pair is
173
+ rejected with `INVALID_REQUEST` rather than one of the two being quietly
174
+ dropped.
175
+
176
+ ```ts
177
+ // Serve whatever is stored, at any age; reach LinkedIn only if nothing is.
178
+ const chat = await acc.messaging.getChat("chat_1", { mode: "refill" });
179
+
180
+ // Never reach LinkedIn. Throws NOT_STORED when the store holds nothing.
181
+ const profile = await acc.users.get("me", { mode: "cache_only" });
182
+
183
+ // Accept a stored copy up to five minutes old, fetch otherwise.
184
+ const page = await acc.messaging.listMessages("chat_1", { max_age: 300 });
185
+ ```
186
+
187
+ Every one of these responses carries fields that say what you are holding:
188
+
189
+ - `source` is `"store"` or `"live"`, and `observed_at` is when the data was seen
190
+ on LinkedIn. A stored answer can carry less than a live one, because some
191
+ fields are dropped before anything is written, so `source: "store"` is how you
192
+ know to ask again with `mode: "live"` when a field you need is missing.
193
+ - `withdrawn` is always present, never inferred from a missing field. `true`
194
+ means LinkedIn has said the resource is gone, such as a removed profile, and
195
+ the answer you are holding is the copy Curviate still has: do not act on it.
196
+ `withdrawn_at` rides along when it is `true`. This is the field to branch on
197
+ before messaging or acting on anything served from the store, and `refill`,
198
+ which serves a copy at any age, is the mode most likely to hand you one.
199
+
200
+ `NOT_STORED` under `cache_only` does not always mean the resource is unknown.
201
+ On `listMessages` it also fires when the request narrows the page in a way the
202
+ stored copy cannot reproduce, so a fully stored chat can refuse a narrowed
203
+ `cache_only` read. Re-read without the narrowing parameters, or with a mode that
204
+ may fetch.
205
+
206
+ ---
207
+
130
208
  ## Cursor pagination
131
209
 
132
210
  Resources that return lists support cursor pagination. `curviate.paginate()` is an async iterator that follows the `cursor` field automatically, so you pull items one at a time without managing cursors: