@curviate/sdk 0.25.0 → 0.27.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,6 +7,167 @@ Versioning: semantic. Minor for additive changes, patch for bug fixes; no stabil
7
7
 
8
8
  ---
9
9
 
10
+ ## [0.27.0] - 2026-09-06
11
+
12
+ Fixture regenerated against the deployed production document
13
+ (`https://api.curviate.com`, `server_git_sha 2a86542f...`, 125 paths).
14
+ **BREAKING**: the `message.delivered` webhook event is gone from the catalogue,
15
+ from `CurviateEvent`, and from the events a subscription can be created with.
16
+ Everything else here is additive or description-only. Pre-1.0 a breaking change
17
+ ships as a minor, so a caret range on `0.26.x` will not pick this up.
18
+
19
+ ### BREAKING
20
+
21
+ - **`message.delivered` is removed from the webhook event catalogue.** The
22
+ platform never emitted it, so no `event.event === 'message.delivered'` branch
23
+ has ever run: the event was subscribable and undeliverable. Two surfaces lose
24
+ the member.
25
+ - `CurviateEvent` (hand-written, `src/webhooks.ts`) drops its
26
+ `{ event: "message.delivered"; data: MessagePayload }` arm, going from 25
27
+ arms to 24. A `switch` on `event.event` that still lists it stops
28
+ compiling, and that arm was dead code in every build it ever shipped in.
29
+ - The generated create-events enum, `events` on the `messaging` variant of
30
+ the `POST /v1/webhooks` request body
31
+ (`operations["postV1Webhooks"]["requestBody"]`), goes from 8 members to 7.
32
+ Passing `"message.delivered"` to a create call is now a type error, and the
33
+ server rejects the value.
34
+
35
+ The two are pinned to each other by an exact two-way type equality in
36
+ `test/webhooks.constructEvent.test.ts`, so they cannot drift apart: that pin
37
+ is what failed first on this regeneration.
38
+
39
+ `GET /v1/webhooks/events` now reports 27 events, messaging 7, where it
40
+ reported 28 and 8.
41
+
42
+ **The read path is untyped and deliberately unaffected.** `events` on a
43
+ webhook returned by `GET /v1/webhooks` or `GET /v1/webhooks/{id}` is
44
+ `string[]`, not the create enum. A subscription created before 2026-09-06 may
45
+ still echo `message.delivered` in its stored `events` until the server-side
46
+ cleanup migration lands; it was never delivered. Reading such a subscription
47
+ back therefore neither breaks a build nor throws, and no migration is
48
+ required of a consumer that only lists webhooks.
49
+
50
+ **The replacement is a read, not an event**: `is_delivered` on the message
51
+ resource.
52
+
53
+ ### Regenerated types
54
+
55
+ - **BREAKING for an exhaustive `switch` - `notices[].code` gains
56
+ `EXPANSION_WITHHELD_CEILING`.** On the same ten chat and message reads that
57
+ accept `expand=public_identifier`, alongside `EXPANSION_LIMIT_REACHED` and
58
+ `EXPANSION_WITHHELD_ACTIVITY_WINDOW`. The expansion was withheld because the
59
+ account is at an account-safety ceiling, not because the page ran out of
60
+ lookup budget and not because the account is outside its activity window.
61
+ Same consequence for the reader as its two siblings, `public_identifier` is
62
+ `null` for a reason that is not "no public profile", and a different remedy,
63
+ which is why it is a separate code rather than a reuse. Additive on the wire:
64
+ only a `switch` with a `never`-typed default arm stops compiling.
65
+ - Description-only, no type change:
66
+ - `SafetyWarning` now states that `hint.message` is the one field whose text
67
+ differs from the `BUDGET_EXHAUSTED` refusal. On a warn-posture ceiling
68
+ breach it says the action went through and how far past the ceiling the row
69
+ now stands, rather than telling the caller what was refused. The shape is
70
+ unchanged, so no branch moves; the string is longer and more specific.
71
+ - The `502` response documents that `retry_likely_to_succeed` is what
72
+ separates its two cases: a temporary upstream error is worth retrying, an
73
+ upstream response that could not be interpreted is not, and the latter
74
+ carries `retry_hint: {"kind": "never"}`. No new field, and the SDK's own
75
+ retry set is unchanged in this release.
76
+ - The `notices` description on the ten expansion reads now lists the ceiling
77
+ case as a third reason a person can go unresolved.
78
+
79
+ ## [0.26.0] - 2026-09-06
80
+
81
+ Fixture regenerated against the deployed production document
82
+ (`https://api.curviate.com`, `server_git_sha bd795601...`, 125 paths).
83
+ **BREAKING at the type level** for a consumer that reads `row` off an error
84
+ or a safety warning as a `string`: it is now `string | null` on both. Three
85
+ response enums also widen, which reds an exhaustive `switch` with a `never`
86
+ default. Everything else in this release is additive.
87
+
88
+ ### Added
89
+
90
+ - **`NOT_STORED` in the error taxonomy.** The `422` a read gets when
91
+ `mode=cache_only` (or `refill` on a resource that cannot be filled) finds
92
+ nothing in Curviate's store, so the API refuses rather than reaching
93
+ LinkedIn. It was missing from `ERROR_CODES`, so it decoded to `INTERNAL`:
94
+ a caller could not branch on it, and, the harmful half, `INTERNAL` is
95
+ retryable, so a `cache_only` miss was retried on a GET against an answer
96
+ that cannot change until the caller picks another mode. It is now decoded to
97
+ itself and is deliberately absent from the retryable set. Not the same as
98
+ `RESOURCE_NOT_FOUND`: the resource may exist and this API simply holds no
99
+ copy, so the fix is another mode, not another id. `cache_only` is the mode
100
+ that raises it on these reads, and `refill` or `auto` is the remedy.
101
+
102
+ **This reds `@curviate/cli`'s exit-code exhaustiveness guard, by design.**
103
+ That guard iterates the exported `ERROR_CODES` array rather than a copy of
104
+ it, which is the whole reason the array is exported, so a code added here
105
+ with no exit-code mapping is supposed to fail it. Map `NOT_STORED` there when
106
+ bumping the pin.
107
+ - **`messaging.getChat(chatId, params?)` takes the retrieval query.**
108
+ `GET /v1/{account_id}/chats/{chat_id}` declares `expand`, `mode` and
109
+ `max_age`; the method sent no query at all, so a chat read was the one
110
+ store-servable read with no retrieval ladder. `ChatGetQuery` is derived from
111
+ the generated `paths` types, like every other query type here.
112
+ `users.get()` and `messaging.listMessages()` already carried the pair and
113
+ are unchanged; the README now documents all three.
114
+
115
+ ### Regenerated types
116
+
117
+ - **BREAKING - `row` is nullable on both `Error` and `SafetyWarning`.**
118
+ `Error.row` goes `string | undefined` to `string | null | undefined`, and
119
+ `SafetyWarning.row` - which is REQUIRED, so it is the one that will actually
120
+ break a build - goes `string` to `string | null`. `null` says the breach
121
+ names no row: an `activity_window` refusal on an action that spends no
122
+ budget of its own has an hour to answer for and no counter. Read
123
+ `hint.parameter` in that case; it names `posture`. A consumer that assigned
124
+ `.row` to a `string` or passed it somewhere non-null now has a type error,
125
+ and that error is correct - the runtime value was always going to be `null`
126
+ once the server started sending it. The SDK's own `CurviateError.budgetRow`
127
+ is UNCHANGED (`string | undefined`): `transport.ts` maps the wire field with
128
+ a `typeof === "string"` guard, so a `null` row arrives as an absent
129
+ `budgetRow`, which is what "no row to name" already meant on that surface.
130
+ - **BREAKING for an exhaustive `switch` - three response enums widen.** Each
131
+ gains a member, so a `switch` with a `never`-typed default arm stops
132
+ compiling. None of them removes a member, so a `switch` with a real default
133
+ keeps working:
134
+ - `GET /v1/{account_id}/safety-events` `items[].reason` gains
135
+ `activity_window` alongside `budget_exhausted` and `rate_limited`.
136
+ - `GET /v1/{account_id}/safety-events` `items[].budget_row` gains `"*"`, the
137
+ wildcard for an event that is not scoped to one row.
138
+ - `notices[].code` on the ten chat and message reads that accept
139
+ `expand=public_identifier` gains `EXPANSION_WITHHELD_ACTIVITY_WINDOW`
140
+ alongside `EXPANSION_LIMIT_REACHED`: the expansion was refused because the
141
+ account is outside its activity window, not because the page ran out of
142
+ lookup budget. Same consequence for the reader either way -
143
+ `public_identifier` is `null` for reasons that are not "no public profile"
144
+ - but a different fix, so it is a separate code. The `notices` key itself
145
+ is not new.
146
+ - **Additive - `interface_ceiling` and `effective_interface_ceiling` on every
147
+ budget row.** Optional, `number | null`. They appear on `quotas[]` from
148
+ `GET /v1/accounts/{account_id}` and on the policy rows from
149
+ `GET`/`PATCH /v1/{account_id}/safety-policy`. What a call through the
150
+ matching ELEVATED interface (`/sales-navigator/...`, `/recruiter/...`) is
151
+ held to on that row; `null` wherever the row and limit profile have no such
152
+ figure, which is every row on `basic` and `premium`. `ceiling`,
153
+ `effective_ceiling`, `band` and `over_default` all remain the STANDARD
154
+ figure, so a row can report `band: "over"` while an elevated call still
155
+ succeeds. Read `effective_interface_ceiling`, not `interface_ceiling`, for
156
+ the same reason `effective_ceiling` and not `ceiling` answers that on the
157
+ standard side. On the `PATCH` request body both are typed `unknown` and
158
+ documented READ-ONLY: sent back they are accepted and ignored, so a read can
159
+ round-trip unedited.
160
+ - **Additive - `activity_window` on the `PATCH /v1/{account_id}/safety-policy`
161
+ request body**, typed `unknown` and documented NOT ACCEPTED. Send the
162
+ settable subfields instead: top-level `timezone`, and per row
163
+ `activity_window_start`, `activity_window_end`, `activity_window_timezone`,
164
+ `activity_window_applies_to`.
165
+ - Description-only, no type change: `reset_at` on `Error` and `SafetyWarning`
166
+ now states that it is the window roll on a `ceiling` refusal and the next
167
+ window open on an `activity_window` one, and restates the two cases where no
168
+ clock frees the account (the `pending_invites` gauge, and InMail credits).
169
+ The chat-search `q` parameter documents that a 1-2 character term is slower.
170
+
10
171
  ## [0.25.0] - 2026-09-05
11
172
 
12
173
  **BREAKING** for a consumer that pinned the old type declarations: `quotas[]`
package/README.md CHANGED
@@ -122,6 +122,13 @@ try {
122
122
  case "ACCOUNT_NOT_FOUND":
123
123
  console.error("Account does not exist for this tenant.");
124
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;
125
132
  default:
126
133
  if (err.retryLikelyToSucceed) {
127
134
  // Safe to retry: server-side transient error
@@ -137,6 +144,67 @@ exported `ErrorCode` type is the complete set, so `tsc` tells you when a `switch
137
144
 
138
145
  ---
139
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
+
140
208
  ## Cursor pagination
141
209
 
142
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: