@curviate/sdk 0.18.1 → 0.19.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
@@ -9,6 +9,68 @@ Versioning: semantic — minor for additive changes, patch for bug fixes; no sta
9
9
 
10
10
  ## [Unreleased]
11
11
 
12
+ ## [0.19.0] - 2026-08-03
13
+
14
+ ### Fixed
15
+
16
+ - **`constructEvent` now parses real webhook deliveries. It previously rejected
17
+ every single one.** The Curviate platform sends the event name in `event`;
18
+ the SDK required a `type` field that no delivery has ever contained, so
19
+ verification threw `WebhookSignatureError` on 100% of genuine webhooks. The
20
+ HMAC path was never at fault (a tampered body was correctly rejected as
21
+ `invalid_signature`); the parse step was. If webhook verification has never
22
+ worked for you, this is why, and this release fixes it.
23
+
24
+ - **A body problem no longer reports a header problem.** A payload whose
25
+ signature verifies but whose body cannot be parsed used to fail with
26
+ `reason: "malformed_header"`, pointing at the two things that had just been
27
+ proven correct: the header and the signing secret. Integrators read that and
28
+ rotated a secret that was never wrong. Those cases now report the new
29
+ `reason: "malformed_payload"`, and the message no longer mentions the header,
30
+ the signature, or the secret.
31
+
32
+ ### Changed
33
+
34
+ - **BREAKING (type-level), and it breaks no working code.** The `CurviateEvent`
35
+ discriminant is now `event`, not `type`:
36
+
37
+ ```diff
38
+ - if (event.type === "message.received") { ... }
39
+ + if (event.event === "message.received") { ... }
40
+ ```
41
+
42
+ Read the label carefully before deferring the upgrade: **there is no working
43
+ consumer of `event.type` to break.** `constructEvent` has never once returned
44
+ successfully for a real delivery, so no `event.type` branch has ever executed
45
+ in production. Any handler you have written behind it is unreachable code
46
+ today. `event.type` is now a compile error precisely so `tsc` points you at
47
+ every site that needs the one-word edit; nothing changes at runtime for code
48
+ that was previously working, because there was none.
49
+
50
+ `WebhookSignatureError["reason"]` gains the `"malformed_payload"` member. An
51
+ exhaustive `switch` over `reason` will need the new arm.
52
+
53
+ ### Added
54
+
55
+ - **`CurviateEventEnvelope`**: the per-delivery metadata now carried on every
56
+ event. `event.id` is the `wdl_…` delivery id (de-duplicate retries on it),
57
+ `event.webhook_id` is the registration, `event.delivered_at` is the attempt
58
+ timestamp. These are typed optional and are **not** required by the parser:
59
+ requiring an envelope field is what caused this defect, so nothing beyond the
60
+ discriminant can fail a verification again.
61
+
62
+ - Forward compatibility: a payload carrying `type` instead of `event` is
63
+ accepted and normalized onto `event`, so a future platform-side change to the
64
+ envelope cannot break verification a second time. `type` is never required.
65
+
66
+ - `test/webhooks.dispatcherEnvelope.test.ts` and
67
+ `test/fixtures/webhook-delivery.capture.json`: the regression test replays
68
+ bytes and a signature header **captured off the wire from the real dispatch
69
+ worker**, rather than constructing its own payload. The pre-existing suite was
70
+ green throughout this defect because it built its own `{ type: … }` fixtures;
71
+ a suite that constructs its own input cannot detect an emitter/parser
72
+ divergence, and that is what let this ship.
73
+
12
74
  ## [0.18.1] — 2026-07-18
13
75
 
14
76
  ### Changed
package/README.md CHANGED
@@ -1,9 +1,9 @@
1
1
  # @curviate/sdk
2
2
 
3
- The official TypeScript SDK for the [Curviate API](https://docs.curviate.com) — agent-native
3
+ The official TypeScript SDK for the [Curviate API](https://docs.curviate.com). Agent-native
4
4
  LinkedIn infrastructure for AI engineers and agent builders.
5
5
 
6
- > **Status:** `0.16.0` — pre-1.0. Full v2 API parity; the surface is public but not yet stability-promised.
6
+ > **Status:** pre-1.0. Full v2 API parity; the surface is public but not yet stability-promised.
7
7
 
8
8
  ---
9
9
 
@@ -17,7 +17,7 @@ Requires Node 18+. Works in Cloudflare Workers and Vercel Edge.
17
17
 
18
18
  ---
19
19
 
20
- ## Auth — construct the client
20
+ ## Auth: construct the client
21
21
 
22
22
  Get your API key from the Curviate dashboard. Store it as an environment variable.
23
23
 
@@ -37,7 +37,7 @@ const curviate = new Curviate({
37
37
 
38
38
  ## Account-scoped accessor
39
39
 
40
- Every LinkedIn operation (messages, member profiles, invites, posts) is tied to a **managed account** — a LinkedIn session you have connected via the connect flow (`curviate.auth.intent()`). The `curviate.account(id)` accessor fixes the `account_id` on every call so you do not have to thread it manually:
40
+ Every LinkedIn operation (messages, member profiles, invites, posts) is tied to a **managed account**, a LinkedIn session you have connected via the connect flow (`curviate.auth.intent()`). The `curviate.account(id)` accessor fixes the `account_id` on every call so you do not have to thread it manually:
41
41
 
42
42
  ```ts
43
43
  // Root-level: tenant-wide operations (accounts, auth, webhooks)
@@ -56,7 +56,7 @@ const profile = await acc.users.get("some-user-id"); // someone else's
56
56
 
57
57
  ## First end-to-end call
58
58
 
59
- This snippet lists your connected accounts, picks the first one, and sends a message — something an agent might do to automate outreach:
59
+ This snippet lists your connected accounts, picks the first one, and sends a message, something an agent might do to automate outreach:
60
60
 
61
61
  ```ts
62
62
  import { Curviate, isCurviateError } from "@curviate/sdk";
@@ -78,7 +78,7 @@ async function sendFirstMessage() {
78
78
  // 3. Send a message to the first chat
79
79
  const chat = chats[0]!;
80
80
  await acc.messaging.sendMessage(chat.id ?? "", {
81
- text: "Hi — following up from our conversation.",
81
+ text: "Hi, following up from our conversation.",
82
82
  });
83
83
 
84
84
  console.log("Message sent.");
@@ -114,7 +114,7 @@ try {
114
114
  break;
115
115
  default:
116
116
  if (err.retryLikelyToSucceed) {
117
- // Safe to retry — server-side transient error
117
+ // Safe to retry: server-side transient error
118
118
  await retry();
119
119
  }
120
120
  }
@@ -127,7 +127,7 @@ All 34 error codes are documented in the [API reference](https://docs.curviate.c
127
127
 
128
128
  ## Cursor pagination
129
129
 
130
- Resources that return lists support cursor pagination. `curviate.paginate()` is an async iterator that follows the `cursor` field automatically — pull items one at a time without managing cursors:
130
+ 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:
131
131
 
132
132
  ```ts
133
133
  // Iterate over every chat across all pages
@@ -163,27 +163,28 @@ app.post("/webhook", express.raw({ type: "application/json" }), async (req, res)
163
163
  event = await constructEvent(req.body, sig, secret);
164
164
  } catch (err) {
165
165
  if (err instanceof WebhookSignatureError) {
166
- console.warn("Bad webhook:", err.reason); // 'invalid_signature' | 'replay_detected' | 'malformed_header'
166
+ // 'invalid_signature' | 'replay_detected' | 'malformed_header' | 'malformed_payload'
167
+ console.warn("Bad webhook:", err.reason);
167
168
  return res.sendStatus(400);
168
169
  }
169
170
  throw err;
170
171
  }
171
172
 
172
- switch (event.type) {
173
+ switch (event.event) {
173
174
  case "message.received":
174
- // event.data is MessagePayload — account_id, message_id, etc.
175
+ // event.data is MessagePayload: account_id, message_id, and so on.
175
176
  handleNewMessage(event.data);
176
177
  break;
177
178
  case "account.connected":
178
179
  handleAccountConnected(event.data);
179
180
  break;
180
- // 22 more event types — all in the CurviateEvent union
181
+ // 22 more event types, all in the CurviateEvent union.
181
182
  }
182
183
 
183
184
  res.sendStatus(200);
184
185
  });
185
186
 
186
- // Hono / Vercel Edge — always await (Web Crypto is async)
187
+ // Hono / Vercel Edge. Always await; Web Crypto is async.
187
188
  app.post("/webhook", async (c) => {
188
189
  const rawBody = await c.req.text();
189
190
  const event = await constructEvent(rawBody, c.req.header("curviate-signature")!, secret);
@@ -191,16 +192,28 @@ app.post("/webhook", async (c) => {
191
192
  });
192
193
  ```
193
194
 
194
- `constructEvent` always returns a `Promise<CurviateEvent>` — always `await` it.
195
+ `constructEvent` always returns a `Promise<CurviateEvent>`. Always `await` it.
195
196
 
196
- `WebhookSignatureError` is NOT a `CurviateError` — narrow with `instanceof WebhookSignatureError`.
197
+ Each event also carries its delivery metadata: `event.id` (a `wdl_` delivery id you
198
+ can de-duplicate retries on), `event.webhook_id`, and `event.delivered_at`.
199
+
200
+ `WebhookSignatureError` is NOT a `CurviateError`. Narrow with `instanceof WebhookSignatureError`.
201
+ Its `reason` tells you where to look: `malformed_header` means the signature header could not be
202
+ read, `invalid_signature` means the HMAC did not match (check the secret, and check that you passed
203
+ the raw request bytes), `replay_detected` means the event is outside the replay window, and
204
+ `malformed_payload` means the signature was **valid** but the body was not a Curviate event, so
205
+ your secret and header are both fine.
206
+
207
+ > **Upgrading from 0.18.x or earlier?** The event discriminant is `event.event`, not `event.type`.
208
+ > See the 0.19.0 entry in [CHANGELOG.md](./CHANGELOG.md); the change is a one-word edit and it
209
+ > breaks no working code, because `constructEvent` never returned successfully before 0.19.0.
197
210
 
198
211
  ---
199
212
 
200
213
  ## Links
201
214
 
202
215
  - API reference: https://docs.curviate.com
203
- - Issues: https://github.com/curviate/sdk/issues
216
+ - Issues: https://github.com/curviate/curviate-sdk/issues
204
217
  - Changelog: [CHANGELOG.md](./CHANGELOG.md)
205
218
 
206
219
  ## License
package/dist/index.d.ts CHANGED
@@ -29211,6 +29211,20 @@ declare function isCurviateError(err: unknown): err is CurviateError;
29211
29211
  * Extends `Error`, NOT `CurviateError` — callers can narrow with
29212
29212
  * `instanceof WebhookSignatureError` independently of CurviateError.
29213
29213
  *
29214
+ * The `reason` codes split by where the problem actually is, so the code you
29215
+ * read points at the thing you need to look at:
29216
+ *
29217
+ * - `malformed_header` - the `Curviate-Signature` header itself is unusable
29218
+ * (missing `t=` or `v1=`, non-numeric timestamp). Check how you are reading
29219
+ * the header.
29220
+ * - `invalid_signature` - the header parsed, but the HMAC does not match.
29221
+ * Check the signing secret, and check that you passed the raw request bytes
29222
+ * rather than a re-serialized object.
29223
+ * - `replay_detected` - signature valid, timestamp outside the replay window.
29224
+ * - `malformed_payload` - **signature valid**, but the body is not a Curviate
29225
+ * event. Nothing is wrong with your secret or your header; look at what is
29226
+ * POSTing to the endpoint.
29227
+ *
29214
29228
  * @example
29215
29229
  * try {
29216
29230
  * const event = await constructEvent(rawBody, header, secret);
@@ -29223,7 +29237,7 @@ declare function isCurviateError(err: unknown): err is CurviateError;
29223
29237
  declare class WebhookSignatureError extends Error {
29224
29238
  readonly name = "WebhookSignatureError";
29225
29239
  /** Structured reason code for the verification failure. */
29226
- readonly reason: "invalid_signature" | "replay_detected" | "malformed_header";
29240
+ readonly reason: "invalid_signature" | "replay_detected" | "malformed_header" | "malformed_payload";
29227
29241
  constructor(reason: WebhookSignatureError["reason"], message: string);
29228
29242
  }
29229
29243
  /**
@@ -29248,9 +29262,34 @@ interface AccountPayload {
29248
29262
  account_id: string;
29249
29263
  [key: string]: unknown;
29250
29264
  }
29265
+ /**
29266
+ * Delivery metadata that accompanies every Curviate webhook event.
29267
+ *
29268
+ * These fields are present on every delivery the platform sends, but
29269
+ * `constructEvent` does **not** require them: the parser insists only on the
29270
+ * discriminant. That is deliberate. Requiring an envelope field is exactly how
29271
+ * verification broke before (the parser demanded a field the emitter did not
29272
+ * send, and rejected every genuine delivery), so anything the union does not
29273
+ * strictly need to narrow is optional here and cannot fail a verification.
29274
+ */
29275
+ interface CurviateEventEnvelope {
29276
+ /** Unique id for this delivery attempt, e.g. `wdl_01J...`. Use it to de-duplicate retries. */
29277
+ id?: string;
29278
+ /** Id of the webhook registration this delivery belongs to, e.g. `wh_01J...`. */
29279
+ webhook_id?: string;
29280
+ /** ISO-8601 timestamp of the delivery attempt. */
29281
+ delivered_at?: string;
29282
+ }
29251
29283
  /**
29252
29284
  * The complete discriminated union of all 24 canonical, create-subscribable
29253
- * Curviate webhook events. The `type` field is the discriminant.
29285
+ * Curviate webhook events. The **`event`** field is the discriminant, matching
29286
+ * the field the platform actually sends on the wire.
29287
+ *
29288
+ * > Migrating from 0.18.x and earlier: the discriminant was `type`, a field no
29289
+ * > Curviate delivery has ever contained, so `constructEvent` threw on every
29290
+ * > real webhook and no `event.type` branch could ever run. Switch your checks
29291
+ * > to `event.event === '...'`. `event.type` is now a compile error, which is
29292
+ * > intentional: it points at every site that needs the one-word change.
29254
29293
  *
29255
29294
  * Re-keyed for the v2 catalogue (was 19). Renamed/removed vs. the prior set:
29256
29295
  * `account.stopped`, `account.sync_started`, `account.sync_complete`,
@@ -29265,83 +29304,83 @@ interface AccountPayload {
29265
29304
  *
29266
29305
  * @example
29267
29306
  * const event = await constructEvent(rawBody, header, secret);
29268
- * if (event.type === 'message.received') {
29307
+ * if (event.event === 'message.received') {
29269
29308
  * // event.data is MessagePayload
29270
29309
  * }
29271
29310
  */
29272
- type CurviateEvent = {
29273
- type: "message.received";
29311
+ type CurviateEvent = CurviateEventEnvelope & ({
29312
+ event: "message.received";
29274
29313
  data: MessagePayload;
29275
29314
  } | {
29276
- type: "message.delivered";
29315
+ event: "message.delivered";
29277
29316
  data: MessagePayload;
29278
29317
  } | {
29279
- type: "message.read";
29318
+ event: "message.read";
29280
29319
  data: MessagePayload;
29281
29320
  } | {
29282
- type: "message.edited";
29321
+ event: "message.edited";
29283
29322
  data: MessagePayload;
29284
29323
  } | {
29285
- type: "message.deleted";
29324
+ event: "message.deleted";
29286
29325
  data: MessagePayload;
29287
29326
  } | {
29288
- type: "message.reaction";
29327
+ event: "message.reaction";
29289
29328
  data: MessagePayload;
29290
29329
  } | {
29291
- type: "chat.updated";
29330
+ event: "chat.updated";
29292
29331
  data: MessagePayload;
29293
29332
  } | {
29294
- type: "chat.deleted";
29333
+ event: "chat.deleted";
29295
29334
  data: MessagePayload;
29296
29335
  } | {
29297
- type: "connection.accepted";
29336
+ event: "connection.accepted";
29298
29337
  data: ConnectionPayload;
29299
29338
  } | {
29300
- type: "connection.new";
29339
+ event: "connection.new";
29301
29340
  data: ConnectionPayload;
29302
29341
  } | {
29303
- type: "account.created";
29342
+ event: "account.created";
29304
29343
  data: AccountPayload;
29305
29344
  } | {
29306
- type: "account.connected";
29345
+ event: "account.connected";
29307
29346
  data: AccountPayload;
29308
29347
  } | {
29309
- type: "account.synced";
29348
+ event: "account.synced";
29310
29349
  data: AccountPayload;
29311
29350
  } | {
29312
- type: "account.reconnected";
29351
+ event: "account.reconnected";
29313
29352
  data: AccountPayload;
29314
29353
  } | {
29315
- type: "account.reconnect_needed";
29354
+ event: "account.reconnect_needed";
29316
29355
  data: AccountPayload;
29317
29356
  } | {
29318
- type: "account.creation_failed";
29357
+ event: "account.creation_failed";
29319
29358
  data: AccountPayload;
29320
29359
  } | {
29321
- type: "account.disconnected";
29360
+ event: "account.disconnected";
29322
29361
  data: AccountPayload;
29323
29362
  } | {
29324
- type: "account.error";
29363
+ event: "account.error";
29325
29364
  data: AccountPayload;
29326
29365
  } | {
29327
- type: "account.paused";
29366
+ event: "account.paused";
29328
29367
  data: AccountPayload;
29329
29368
  } | {
29330
- type: "account.connecting";
29369
+ event: "account.connecting";
29331
29370
  data: AccountPayload;
29332
29371
  } | {
29333
- type: "account.permission_revoked";
29372
+ event: "account.permission_revoked";
29334
29373
  data: AccountPayload;
29335
29374
  } | {
29336
- type: "account.initial_sync.running";
29375
+ event: "account.initial_sync.running";
29337
29376
  data: AccountPayload;
29338
29377
  } | {
29339
- type: "account.initial_sync.completed";
29378
+ event: "account.initial_sync.completed";
29340
29379
  data: AccountPayload;
29341
29380
  } | {
29342
- type: "account.initial_sync.failed";
29381
+ event: "account.initial_sync.failed";
29343
29382
  data: AccountPayload;
29344
- };
29383
+ });
29345
29384
  /**
29346
29385
  * Options for {@link constructEvent}.
29347
29386
  */
@@ -29386,7 +29425,7 @@ interface ConstructEventOptions {
29386
29425
  * }
29387
29426
  * throw err;
29388
29427
  * }
29389
- * if (event.type === 'message.received') { ... }
29428
+ * if (event.event === 'message.received') { ... }
29390
29429
  * res.sendStatus(200);
29391
29430
  * });
29392
29431
  *
@@ -29400,4 +29439,4 @@ interface ConstructEventOptions {
29400
29439
  */
29401
29440
  declare function constructEvent(rawBody: string | Buffer, signatureHeader: string, secret: string, opts?: ConstructEventOptions): Promise<CurviateEvent>;
29402
29441
 
29403
- export { type AccountListPage, type AccountListParams, type AccountPayload, type AccountScopedNamespaces, type ConnectionPayload, type ConstructEventOptions, Curviate, type CurviateConfig, CurviateError, type CurviateErrorInit, type CurviateErrorJSON, type CurviateEvent, type ErrorCode, type MessagePayload, type RequiredTier, type ResolvedConfig, type ResourceNamespaces, type RetryHint, WebhookSignatureError, type components, constructEvent, isCurviateError, type operations, type paths };
29442
+ export { type AccountListPage, type AccountListParams, type AccountPayload, type AccountScopedNamespaces, type ConnectionPayload, type ConstructEventOptions, Curviate, type CurviateConfig, CurviateError, type CurviateErrorInit, type CurviateErrorJSON, type CurviateEvent, type CurviateEventEnvelope, type ErrorCode, type MessagePayload, type RequiredTier, type ResolvedConfig, type ResourceNamespaces, type RetryHint, WebhookSignatureError, type components, constructEvent, isCurviateError, type operations, type paths };
package/dist/index.js CHANGED
@@ -2684,12 +2684,27 @@ function verifyAndParse(computedHex, v1, timestamp, bodyStr, replayWindowSecs) {
2684
2684
  try {
2685
2685
  parsed = JSON.parse(bodyStr);
2686
2686
  } catch {
2687
- throw new WebhookSignatureError("malformed_header", "Webhook body is not valid JSON.");
2687
+ throw new WebhookSignatureError(
2688
+ "malformed_payload",
2689
+ "Webhook body is not valid JSON. Verification already succeeded, so the problem is the request body, not your webhook configuration."
2690
+ );
2688
2691
  }
2689
- if (typeof parsed !== "object" || parsed === null || typeof parsed["type"] !== "string") {
2690
- throw new WebhookSignatureError("malformed_header", 'Webhook payload missing "type" field.');
2692
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
2693
+ throw new WebhookSignatureError(
2694
+ "malformed_payload",
2695
+ "Webhook body is not a JSON object. Verification already succeeded, so the problem is the request body, not your webhook configuration."
2696
+ );
2697
+ }
2698
+ const record = parsed;
2699
+ const discriminant = typeof record["event"] === "string" ? record["event"] : typeof record["type"] === "string" ? record["type"] : void 0;
2700
+ if (discriminant === void 0) {
2701
+ throw new WebhookSignatureError(
2702
+ "malformed_payload",
2703
+ 'Webhook body has no "event" field, so it cannot be resolved to a Curviate event. Verification already succeeded, so the problem is the request body, not your webhook configuration.'
2704
+ );
2691
2705
  }
2692
- return parsed;
2706
+ record["event"] = discriminant;
2707
+ return record;
2693
2708
  }
2694
2709
  async function constructEvent(rawBody, signatureHeader, secret, opts) {
2695
2710
  const replayWindowSecs = opts?.replayWindowSecs ?? 300;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@curviate/sdk",
3
- "version": "0.18.1",
3
+ "version": "0.19.0",
4
4
  "private": false,
5
5
  "description": "TypeScript SDK for the Curviate API.",
6
6
  "license": "MIT",
@@ -8,10 +8,10 @@
8
8
  "homepage": "https://docs.curviate.com",
9
9
  "repository": {
10
10
  "type": "git",
11
- "url": "git+https://github.com/curviate/sdk.git"
11
+ "url": "git+https://github.com/curviate/curviate-sdk.git"
12
12
  },
13
13
  "bugs": {
14
- "url": "https://github.com/curviate/sdk/issues"
14
+ "url": "https://github.com/curviate/curviate-sdk/issues"
15
15
  },
16
16
  "publishConfig": {
17
17
  "access": "public"