@curviate/sdk 0.1.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 +141 -0
- package/LICENSE +21 -0
- package/README.md +207 -0
- package/dist/index.d.ts +15997 -0
- package/dist/index.js +1504 -0
- package/package.json +62 -0
- package/src/generated/types.ts +14843 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,1504 @@
|
|
|
1
|
+
// src/errors.ts
|
|
2
|
+
var CurviateError = class _CurviateError extends Error {
|
|
3
|
+
constructor(init) {
|
|
4
|
+
super(init.message);
|
|
5
|
+
this.name = "CurviateError";
|
|
6
|
+
this.code = init.code;
|
|
7
|
+
this.httpStatus = init.httpStatus;
|
|
8
|
+
this.retryHint = init.retryHint ?? null;
|
|
9
|
+
this.userFixable = init.userFixable;
|
|
10
|
+
this.retryLikelyToSucceed = init.retryLikelyToSucceed;
|
|
11
|
+
this.requiredTier = init.requiredTier;
|
|
12
|
+
this.retryAfterMs = init.retryAfterMs;
|
|
13
|
+
Object.setPrototypeOf(this, _CurviateError.prototype);
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Explicit, credential-safe serialization. Only the documented structured
|
|
17
|
+
* fields are emitted — never a credential, auth header, or any reference to
|
|
18
|
+
* the client. `JSON.stringify(error)` calls this automatically.
|
|
19
|
+
*/
|
|
20
|
+
toJSON() {
|
|
21
|
+
const json = {
|
|
22
|
+
name: "CurviateError",
|
|
23
|
+
code: this.code,
|
|
24
|
+
message: this.message,
|
|
25
|
+
retryHint: this.retryHint,
|
|
26
|
+
userFixable: this.userFixable,
|
|
27
|
+
retryLikelyToSucceed: this.retryLikelyToSucceed
|
|
28
|
+
};
|
|
29
|
+
if (this.httpStatus !== void 0) json.httpStatus = this.httpStatus;
|
|
30
|
+
if (this.requiredTier !== void 0) json.requiredTier = this.requiredTier;
|
|
31
|
+
if (this.retryAfterMs !== void 0) json.retryAfterMs = this.retryAfterMs;
|
|
32
|
+
return json;
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
function isCurviateError(err) {
|
|
36
|
+
return err instanceof CurviateError;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// src/config.ts
|
|
40
|
+
var DEFAULT_BASE_URL = "https://api.curviate.com";
|
|
41
|
+
var DEFAULT_TIMEOUT_MS = 3e4;
|
|
42
|
+
var DEFAULT_MAX_RETRIES = 3;
|
|
43
|
+
function resolveConfig(config) {
|
|
44
|
+
if (typeof config?.apiKey !== "string" || config.apiKey.length === 0) {
|
|
45
|
+
throw new CurviateError({
|
|
46
|
+
code: "INVALID_REQUEST",
|
|
47
|
+
message: "An apiKey is required to construct a Curviate client.",
|
|
48
|
+
userFixable: true,
|
|
49
|
+
retryLikelyToSucceed: false
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
return Object.freeze({
|
|
53
|
+
apiKey: config.apiKey,
|
|
54
|
+
baseUrl: config.baseUrl ?? DEFAULT_BASE_URL,
|
|
55
|
+
timeout: config.timeout ?? DEFAULT_TIMEOUT_MS,
|
|
56
|
+
maxRetries: config.maxRetries ?? DEFAULT_MAX_RETRIES,
|
|
57
|
+
fetch: config.fetch
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// src/transport.ts
|
|
62
|
+
var BASE_DELAY_MS = 500;
|
|
63
|
+
var MAX_DELAY_MS = 3e4;
|
|
64
|
+
var RETRYABLE_CODES = /* @__PURE__ */ new Set([
|
|
65
|
+
"INTERNAL",
|
|
66
|
+
"PLATFORM_ERROR",
|
|
67
|
+
"PLATFORM_RATE_LIMIT",
|
|
68
|
+
"RATE_LIMIT_ACCOUNT",
|
|
69
|
+
"RATE_LIMIT_TENANT"
|
|
70
|
+
]);
|
|
71
|
+
var WRITE_METHODS = /* @__PURE__ */ new Set(["POST", "PATCH", "PUT", "DELETE"]);
|
|
72
|
+
var KNOWN_CODES = /* @__PURE__ */ new Set([
|
|
73
|
+
"UNAUTHORIZED",
|
|
74
|
+
"INVALID_REQUEST",
|
|
75
|
+
"UNSUPPORTED_MEDIA_TYPE",
|
|
76
|
+
"PAYLOAD_TOO_LARGE",
|
|
77
|
+
"ACCOUNT_NOT_FOUND",
|
|
78
|
+
"ACCOUNT_RESTRICTED",
|
|
79
|
+
"RESOURCE_NOT_FOUND",
|
|
80
|
+
"TIER_NOT_ACTIVE",
|
|
81
|
+
"LINKEDIN_FEATURE_NOT_SUBSCRIBED",
|
|
82
|
+
"RATE_LIMIT_ACCOUNT",
|
|
83
|
+
"RATE_LIMIT_TENANT",
|
|
84
|
+
"PLATFORM_RATE_LIMIT",
|
|
85
|
+
"PLATFORM_ERROR",
|
|
86
|
+
"PLATFORM_NOT_IMPLEMENTED",
|
|
87
|
+
"CHECKPOINT_NOT_FOUND",
|
|
88
|
+
"CHECKPOINT_EXPIRED",
|
|
89
|
+
"CHECKPOINT_INVALID_CODE",
|
|
90
|
+
"CHECKPOINT_MAX_ATTEMPTS",
|
|
91
|
+
"CHECKPOINT_ALREADY_RESOLVED",
|
|
92
|
+
"CHECKPOINT_UNSUPPORTED",
|
|
93
|
+
"CONNECTION_IN_PROGRESS",
|
|
94
|
+
"LINKEDIN_AUTH_FAILED",
|
|
95
|
+
"LINKEDIN_RATE_LIMITED",
|
|
96
|
+
"LINKEDIN_COOKIE_INVALID",
|
|
97
|
+
"LINKEDIN_SERVICE_UNAVAILABLE",
|
|
98
|
+
"MESSAGE_WINDOW_EXPIRED",
|
|
99
|
+
"RECIPIENT_UNREACHABLE",
|
|
100
|
+
"PAYMENT_REQUIRED",
|
|
101
|
+
"PAYMENT_FAILED",
|
|
102
|
+
"SUBSCRIPTION_BUSY",
|
|
103
|
+
"SUBSCRIPTION_NOT_FOUND",
|
|
104
|
+
"SEAT_NOT_FOUND",
|
|
105
|
+
"SEAT_CANCELLED",
|
|
106
|
+
"INTERNAL"
|
|
107
|
+
]);
|
|
108
|
+
var REQUIRED_TIERS = /* @__PURE__ */ new Set([
|
|
109
|
+
"core",
|
|
110
|
+
"sn",
|
|
111
|
+
"sales_nav",
|
|
112
|
+
"recruiter"
|
|
113
|
+
]);
|
|
114
|
+
function defaultSleep(ms) {
|
|
115
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
116
|
+
}
|
|
117
|
+
function parseRetryAfterMs(header) {
|
|
118
|
+
if (header == null) return void 0;
|
|
119
|
+
const seconds = Number(header);
|
|
120
|
+
if (Number.isFinite(seconds) && seconds >= 0) return seconds * 1e3;
|
|
121
|
+
return 3e4;
|
|
122
|
+
}
|
|
123
|
+
function backoffDelay(n, jitter) {
|
|
124
|
+
const exp = Math.min(BASE_DELAY_MS * 2 ** (n - 1), MAX_DELAY_MS);
|
|
125
|
+
return exp + jitter;
|
|
126
|
+
}
|
|
127
|
+
function toErrorCode(code) {
|
|
128
|
+
return code && KNOWN_CODES.has(code) ? code : "INTERNAL";
|
|
129
|
+
}
|
|
130
|
+
function toRetryHint(hint) {
|
|
131
|
+
if (!hint || typeof hint !== "object") return null;
|
|
132
|
+
const kind = hint.kind;
|
|
133
|
+
if (kind !== "delay" && kind !== "backoff" && kind !== "never") return null;
|
|
134
|
+
const out = { kind };
|
|
135
|
+
if (typeof hint.delay_ms === "number") out.delayMs = hint.delay_ms;
|
|
136
|
+
return out;
|
|
137
|
+
}
|
|
138
|
+
async function errorFromResponse(res) {
|
|
139
|
+
const retryAfterMs = parseRetryAfterMs(res.headers.get("Retry-After"));
|
|
140
|
+
let env;
|
|
141
|
+
try {
|
|
142
|
+
env = await res.clone().json();
|
|
143
|
+
} catch {
|
|
144
|
+
env = void 0;
|
|
145
|
+
}
|
|
146
|
+
const requiredTier = env?.required_tier && REQUIRED_TIERS.has(env.required_tier) ? env.required_tier : void 0;
|
|
147
|
+
return new CurviateError({
|
|
148
|
+
code: toErrorCode(env?.code),
|
|
149
|
+
message: env?.message ?? `Request failed with status ${res.status}.`,
|
|
150
|
+
httpStatus: res.status,
|
|
151
|
+
retryHint: toRetryHint(env?.retry_hint),
|
|
152
|
+
userFixable: env?.user_fixable ?? false,
|
|
153
|
+
retryLikelyToSucceed: env?.retry_likely_to_succeed ?? false,
|
|
154
|
+
...requiredTier !== void 0 ? { requiredTier } : {},
|
|
155
|
+
...retryAfterMs !== void 0 ? { retryAfterMs } : {}
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
function retryDelay(err, attempt, jitter) {
|
|
159
|
+
if (err.retryAfterMs !== void 0) return err.retryAfterMs;
|
|
160
|
+
if (err.retryHint?.kind === "delay" && err.retryHint.delayMs !== void 0) {
|
|
161
|
+
return err.retryHint.delayMs;
|
|
162
|
+
}
|
|
163
|
+
return backoffDelay(attempt, jitter);
|
|
164
|
+
}
|
|
165
|
+
function buildInit(method, opts, signal) {
|
|
166
|
+
const headers = {
|
|
167
|
+
authorization: `Bearer ${opts.apiKey}`
|
|
168
|
+
};
|
|
169
|
+
const init = { method, headers, signal };
|
|
170
|
+
if (method === "GET" || method === "HEAD" || opts.body === void 0) {
|
|
171
|
+
return init;
|
|
172
|
+
}
|
|
173
|
+
if (opts.body instanceof FormData) {
|
|
174
|
+
init.body = opts.body;
|
|
175
|
+
return init;
|
|
176
|
+
}
|
|
177
|
+
headers["content-type"] = "application/json";
|
|
178
|
+
init.body = JSON.stringify(opts.body);
|
|
179
|
+
return init;
|
|
180
|
+
}
|
|
181
|
+
function buildUrl(path, opts) {
|
|
182
|
+
const url = new URL(path, opts.baseUrl);
|
|
183
|
+
if (opts.query) {
|
|
184
|
+
for (const [key, value] of Object.entries(opts.query)) {
|
|
185
|
+
if (value !== void 0 && value !== null) url.searchParams.set(key, String(value));
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
return url.toString();
|
|
189
|
+
}
|
|
190
|
+
async function parseSuccess(res) {
|
|
191
|
+
const ct = res.headers.get("content-type") ?? "";
|
|
192
|
+
if (ct.includes("application/json")) {
|
|
193
|
+
return await res.json();
|
|
194
|
+
}
|
|
195
|
+
return await res.arrayBuffer();
|
|
196
|
+
}
|
|
197
|
+
async function execute(method, path, opts) {
|
|
198
|
+
const doFetch = opts.fetch ?? globalThis.fetch;
|
|
199
|
+
const sleep = opts._sleepFn ?? defaultSleep;
|
|
200
|
+
const jitterFn = opts._jitterFn ?? (() => Math.random() * 200);
|
|
201
|
+
const url = buildUrl(path, opts);
|
|
202
|
+
const isWrite = WRITE_METHODS.has(method);
|
|
203
|
+
let attempt = 0;
|
|
204
|
+
for (; ; ) {
|
|
205
|
+
const controller = new AbortController();
|
|
206
|
+
const timer = setTimeout(() => controller.abort(), opts.timeout);
|
|
207
|
+
let res;
|
|
208
|
+
try {
|
|
209
|
+
res = await doFetch(url, buildInit(method, opts, controller.signal));
|
|
210
|
+
} catch (cause) {
|
|
211
|
+
clearTimeout(timer);
|
|
212
|
+
const aborted = controller.signal.aborted;
|
|
213
|
+
const err2 = new CurviateError({
|
|
214
|
+
code: "INTERNAL",
|
|
215
|
+
message: aborted ? "Request timed out." : "Network error.",
|
|
216
|
+
userFixable: false,
|
|
217
|
+
retryLikelyToSucceed: true
|
|
218
|
+
});
|
|
219
|
+
if (!isWrite && attempt < opts.maxRetries) {
|
|
220
|
+
await sleep(backoffDelay(attempt + 1, jitterFn()));
|
|
221
|
+
attempt += 1;
|
|
222
|
+
continue;
|
|
223
|
+
}
|
|
224
|
+
throw err2;
|
|
225
|
+
}
|
|
226
|
+
clearTimeout(timer);
|
|
227
|
+
if (res.ok) {
|
|
228
|
+
return parseSuccess(res);
|
|
229
|
+
}
|
|
230
|
+
const err = await errorFromResponse(res);
|
|
231
|
+
const retryable = !isWrite && RETRYABLE_CODES.has(err.code) && attempt < opts.maxRetries;
|
|
232
|
+
if (retryable) {
|
|
233
|
+
await sleep(retryDelay(err, attempt + 1, jitterFn()));
|
|
234
|
+
attempt += 1;
|
|
235
|
+
continue;
|
|
236
|
+
}
|
|
237
|
+
if (isWrite && err.retryAfterMs !== void 0) {
|
|
238
|
+
await sleep(err.retryAfterMs);
|
|
239
|
+
}
|
|
240
|
+
throw err;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// src/internal/context.ts
|
|
245
|
+
function createContext(config, accountId) {
|
|
246
|
+
const request = (args) => {
|
|
247
|
+
const query = accountId !== void 0 ? { account_id: accountId, ...args.query } : args.query;
|
|
248
|
+
return execute(args.method, args.path, {
|
|
249
|
+
apiKey: config.apiKey,
|
|
250
|
+
baseUrl: config.baseUrl,
|
|
251
|
+
timeout: config.timeout,
|
|
252
|
+
maxRetries: config.maxRetries,
|
|
253
|
+
...config.fetch ? { fetch: config.fetch } : {},
|
|
254
|
+
...query !== void 0 ? { query } : {},
|
|
255
|
+
...args.body !== void 0 ? { body: args.body } : {}
|
|
256
|
+
});
|
|
257
|
+
};
|
|
258
|
+
return { request, accountId };
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// src/internal/paginate.ts
|
|
262
|
+
async function* paginate(fn, params) {
|
|
263
|
+
let cursor = void 0;
|
|
264
|
+
for (; ; ) {
|
|
265
|
+
const callParams = cursor != null ? { ...params, cursor } : { ...params };
|
|
266
|
+
const page = await fn(callParams);
|
|
267
|
+
const items = page.items ?? page.data ?? [];
|
|
268
|
+
for (const item of items) {
|
|
269
|
+
yield item;
|
|
270
|
+
}
|
|
271
|
+
cursor = page.cursor;
|
|
272
|
+
if (cursor == null) break;
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// src/resources/accounts.ts
|
|
277
|
+
var AccountsResource = class {
|
|
278
|
+
constructor(ctx) {
|
|
279
|
+
this.ctx = ctx;
|
|
280
|
+
}
|
|
281
|
+
/**
|
|
282
|
+
* List the tenant's connected LinkedIn accounts, cursor-paginated.
|
|
283
|
+
*
|
|
284
|
+
* @param params - optional `limit` (1–250) and `cursor` (from a prior page).
|
|
285
|
+
* @returns a page of accounts and the next-page `cursor` (null when exhausted).
|
|
286
|
+
*
|
|
287
|
+
* @example
|
|
288
|
+
* const page = await curviate.accounts.list({ limit: 50 });
|
|
289
|
+
* for (const acc of page.items ?? []) console.log(acc.account_id);
|
|
290
|
+
*/
|
|
291
|
+
list(params) {
|
|
292
|
+
return this.ctx.request({
|
|
293
|
+
method: "GET",
|
|
294
|
+
path: "/v1/accounts",
|
|
295
|
+
...params !== void 0 ? { query: params } : {}
|
|
296
|
+
});
|
|
297
|
+
}
|
|
298
|
+
/**
|
|
299
|
+
* Connect a LinkedIn account to an empty seat.
|
|
300
|
+
*
|
|
301
|
+
* Returns an account (201) or a checkpoint challenge (202) when LinkedIn
|
|
302
|
+
* requires verification. Callers discriminate on `result.object`.
|
|
303
|
+
*/
|
|
304
|
+
link(body) {
|
|
305
|
+
return this.ctx.request({
|
|
306
|
+
method: "POST",
|
|
307
|
+
path: "/v1/accounts/link",
|
|
308
|
+
body
|
|
309
|
+
});
|
|
310
|
+
}
|
|
311
|
+
/**
|
|
312
|
+
* Submit an OTP or 2FA code to resolve a checkpoint challenge.
|
|
313
|
+
*/
|
|
314
|
+
submitCheckpoint(body) {
|
|
315
|
+
return this.ctx.request({
|
|
316
|
+
method: "POST",
|
|
317
|
+
path: "/v1/accounts/checkpoints/submit",
|
|
318
|
+
body
|
|
319
|
+
});
|
|
320
|
+
}
|
|
321
|
+
/**
|
|
322
|
+
* Poll for mobile-app approval of a pending checkpoint challenge.
|
|
323
|
+
*/
|
|
324
|
+
pollCheckpoint(body) {
|
|
325
|
+
return this.ctx.request({
|
|
326
|
+
method: "POST",
|
|
327
|
+
path: "/v1/accounts/checkpoints/poll",
|
|
328
|
+
body
|
|
329
|
+
});
|
|
330
|
+
}
|
|
331
|
+
/**
|
|
332
|
+
* Generate a one-time hosted connection link the end user opens to authorize a
|
|
333
|
+
* LinkedIn connection without credentials transiting the API or LLM context.
|
|
334
|
+
*/
|
|
335
|
+
createConnectLink(body) {
|
|
336
|
+
return this.ctx.request({
|
|
337
|
+
method: "POST",
|
|
338
|
+
path: "/v1/accounts/connect-link",
|
|
339
|
+
body
|
|
340
|
+
});
|
|
341
|
+
}
|
|
342
|
+
/**
|
|
343
|
+
* Return metadata and current state for one connected account, including the
|
|
344
|
+
* central `quotas[]` view for all tracked quota families.
|
|
345
|
+
*/
|
|
346
|
+
get(accountId) {
|
|
347
|
+
return this.ctx.request({
|
|
348
|
+
method: "GET",
|
|
349
|
+
path: `/v1/accounts/${accountId}`
|
|
350
|
+
});
|
|
351
|
+
}
|
|
352
|
+
/**
|
|
353
|
+
* Re-authorize a disconnected account in place (same account_id, same seat).
|
|
354
|
+
*/
|
|
355
|
+
reconnect(accountId, body) {
|
|
356
|
+
return this.ctx.request({
|
|
357
|
+
method: "POST",
|
|
358
|
+
path: `/v1/accounts/${accountId}/reconnect`,
|
|
359
|
+
body
|
|
360
|
+
});
|
|
361
|
+
}
|
|
362
|
+
/**
|
|
363
|
+
* Refresh frozen account sources (re-sync conversations, connection lists, etc.).
|
|
364
|
+
*/
|
|
365
|
+
refresh(accountId) {
|
|
366
|
+
return this.ctx.request({
|
|
367
|
+
method: "POST",
|
|
368
|
+
path: `/v1/accounts/${accountId}/refresh`
|
|
369
|
+
});
|
|
370
|
+
}
|
|
371
|
+
/**
|
|
372
|
+
* Update managed-proxy configuration for an account.
|
|
373
|
+
*/
|
|
374
|
+
update(accountId, body) {
|
|
375
|
+
return this.ctx.request({
|
|
376
|
+
method: "PATCH",
|
|
377
|
+
path: `/v1/accounts/${accountId}`,
|
|
378
|
+
body
|
|
379
|
+
});
|
|
380
|
+
}
|
|
381
|
+
/**
|
|
382
|
+
* Hard-disconnect a LinkedIn account; releases the attached seat.
|
|
383
|
+
*/
|
|
384
|
+
disconnect(accountId) {
|
|
385
|
+
return this.ctx.request({
|
|
386
|
+
method: "DELETE",
|
|
387
|
+
path: `/v1/accounts/${accountId}`
|
|
388
|
+
});
|
|
389
|
+
}
|
|
390
|
+
};
|
|
391
|
+
|
|
392
|
+
// src/resources/messaging.ts
|
|
393
|
+
function buildFormData(scalars, attachments) {
|
|
394
|
+
const form = new FormData();
|
|
395
|
+
for (const [key, value] of Object.entries(scalars)) {
|
|
396
|
+
if (value !== void 0 && value !== null) {
|
|
397
|
+
form.append(key, String(value));
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
for (const attachment of attachments) {
|
|
401
|
+
if (attachment instanceof File) {
|
|
402
|
+
form.append("attachments", attachment);
|
|
403
|
+
} else {
|
|
404
|
+
form.append("attachments", new Blob([attachment]));
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
return form;
|
|
408
|
+
}
|
|
409
|
+
var MessagingResource = class {
|
|
410
|
+
constructor(ctx) {
|
|
411
|
+
this.ctx = ctx;
|
|
412
|
+
}
|
|
413
|
+
/** List paginated chats for the account. `GET /v1/chats` */
|
|
414
|
+
listChats(params) {
|
|
415
|
+
return this.ctx.request({
|
|
416
|
+
method: "GET",
|
|
417
|
+
path: "/v1/chats",
|
|
418
|
+
...params ? { query: params } : {}
|
|
419
|
+
});
|
|
420
|
+
}
|
|
421
|
+
/**
|
|
422
|
+
* Start a new chat. Accepts optional `attachments[]` — when present, the
|
|
423
|
+
* request is sent as `multipart/form-data`. `POST /v1/chats`
|
|
424
|
+
*/
|
|
425
|
+
startChat(body) {
|
|
426
|
+
const { attachments, ...scalars } = body;
|
|
427
|
+
if (attachments && attachments.length > 0) {
|
|
428
|
+
return this.ctx.request({
|
|
429
|
+
method: "POST",
|
|
430
|
+
path: "/v1/chats",
|
|
431
|
+
body: buildFormData(scalars, attachments)
|
|
432
|
+
});
|
|
433
|
+
}
|
|
434
|
+
return this.ctx.request({
|
|
435
|
+
method: "POST",
|
|
436
|
+
path: "/v1/chats",
|
|
437
|
+
body: scalars
|
|
438
|
+
});
|
|
439
|
+
}
|
|
440
|
+
/** Get details of a single chat. `GET /v1/chats/{chat_id}` */
|
|
441
|
+
getChat(chatId) {
|
|
442
|
+
return this.ctx.request({
|
|
443
|
+
method: "GET",
|
|
444
|
+
path: `/v1/chats/${chatId}`
|
|
445
|
+
});
|
|
446
|
+
}
|
|
447
|
+
/** List messages in a chat, cursor-paginated. `GET /v1/chats/{chat_id}/messages` */
|
|
448
|
+
listMessages(chatId, params) {
|
|
449
|
+
return this.ctx.request({
|
|
450
|
+
method: "GET",
|
|
451
|
+
path: `/v1/chats/${chatId}/messages`,
|
|
452
|
+
...params ? { query: params } : {}
|
|
453
|
+
});
|
|
454
|
+
}
|
|
455
|
+
/**
|
|
456
|
+
* Send a message in a chat. Multipart when `attachments[]` are provided.
|
|
457
|
+
* `POST /v1/chats/{chat_id}/messages`
|
|
458
|
+
*/
|
|
459
|
+
sendMessage(chatId, body) {
|
|
460
|
+
const { attachments, ...scalars } = body;
|
|
461
|
+
if (attachments && attachments.length > 0) {
|
|
462
|
+
return this.ctx.request({
|
|
463
|
+
method: "POST",
|
|
464
|
+
path: `/v1/chats/${chatId}/messages`,
|
|
465
|
+
body: buildFormData(scalars, attachments)
|
|
466
|
+
});
|
|
467
|
+
}
|
|
468
|
+
return this.ctx.request({
|
|
469
|
+
method: "POST",
|
|
470
|
+
path: `/v1/chats/${chatId}/messages`,
|
|
471
|
+
body: scalars
|
|
472
|
+
});
|
|
473
|
+
}
|
|
474
|
+
/** Trigger a re-sync of a specific chat's message history. `GET /v1/chats/{chat_id}/sync` */
|
|
475
|
+
syncChat(chatId) {
|
|
476
|
+
return this.ctx.request({
|
|
477
|
+
method: "GET",
|
|
478
|
+
path: `/v1/chats/${chatId}/sync`
|
|
479
|
+
});
|
|
480
|
+
}
|
|
481
|
+
/** Get a single message by ID. `GET /v1/messages/{message_id}` */
|
|
482
|
+
getMessage(messageId) {
|
|
483
|
+
return this.ctx.request({
|
|
484
|
+
method: "GET",
|
|
485
|
+
path: `/v1/messages/${messageId}`
|
|
486
|
+
});
|
|
487
|
+
}
|
|
488
|
+
/** Edit a message within the ~60-minute edit window. `PATCH /v1/messages/{message_id}` */
|
|
489
|
+
editMessage(messageId, body) {
|
|
490
|
+
return this.ctx.request({
|
|
491
|
+
method: "PATCH",
|
|
492
|
+
path: `/v1/messages/${messageId}`,
|
|
493
|
+
body
|
|
494
|
+
});
|
|
495
|
+
}
|
|
496
|
+
/** Delete a message within the delete window. `DELETE /v1/messages/{message_id}` */
|
|
497
|
+
deleteMessage(messageId) {
|
|
498
|
+
return this.ctx.request({
|
|
499
|
+
method: "DELETE",
|
|
500
|
+
path: `/v1/messages/${messageId}`
|
|
501
|
+
});
|
|
502
|
+
}
|
|
503
|
+
/**
|
|
504
|
+
* Download a message attachment. Returns raw binary.
|
|
505
|
+
* `GET /v1/messages/{message_id}/attachments/{attachment_id}`
|
|
506
|
+
* Returns `ArrayBuffer` — binary response; the SDK does not cache or store it.
|
|
507
|
+
*/
|
|
508
|
+
getAttachment(messageId, attachmentId) {
|
|
509
|
+
return this.ctx.request({
|
|
510
|
+
method: "GET",
|
|
511
|
+
path: `/v1/messages/${messageId}/attachments/${attachmentId}`
|
|
512
|
+
});
|
|
513
|
+
}
|
|
514
|
+
/** Add a reaction to a message. `POST /v1/messages/{message_id}/reactions` */
|
|
515
|
+
addReaction(messageId, body) {
|
|
516
|
+
return this.ctx.request({
|
|
517
|
+
method: "POST",
|
|
518
|
+
path: `/v1/messages/${messageId}/reactions`,
|
|
519
|
+
body
|
|
520
|
+
});
|
|
521
|
+
}
|
|
522
|
+
/** Send an InMail. `POST /v1/messages/inmail` */
|
|
523
|
+
sendInMail(body) {
|
|
524
|
+
return this.ctx.request({
|
|
525
|
+
method: "POST",
|
|
526
|
+
path: "/v1/messages/inmail",
|
|
527
|
+
body
|
|
528
|
+
});
|
|
529
|
+
}
|
|
530
|
+
/** Get the account's InMail credit balance. `GET /v1/messaging/inmail-balance` */
|
|
531
|
+
getInMailBalance(params) {
|
|
532
|
+
return this.ctx.request({
|
|
533
|
+
method: "GET",
|
|
534
|
+
path: "/v1/messaging/inmail-balance",
|
|
535
|
+
...params ? { query: params } : {}
|
|
536
|
+
});
|
|
537
|
+
}
|
|
538
|
+
/** Re-sync account message history. `GET /v1/messages/sync` */
|
|
539
|
+
syncMessages(params) {
|
|
540
|
+
return this.ctx.request({
|
|
541
|
+
method: "GET",
|
|
542
|
+
path: "/v1/messages/sync",
|
|
543
|
+
...params ? { query: params } : {}
|
|
544
|
+
});
|
|
545
|
+
}
|
|
546
|
+
};
|
|
547
|
+
|
|
548
|
+
// src/resources/profiles.ts
|
|
549
|
+
var ProfilesResource = class {
|
|
550
|
+
constructor(ctx) {
|
|
551
|
+
this.ctx = ctx;
|
|
552
|
+
}
|
|
553
|
+
/** Get the account's own LinkedIn profile. `GET /v1/profiles/me` */
|
|
554
|
+
getMe() {
|
|
555
|
+
return this.ctx.request({
|
|
556
|
+
method: "GET",
|
|
557
|
+
path: "/v1/profiles/me"
|
|
558
|
+
});
|
|
559
|
+
}
|
|
560
|
+
/**
|
|
561
|
+
* Get a LinkedIn member profile by id or public handle.
|
|
562
|
+
* `GET /v1/profiles/{profile_id}`
|
|
563
|
+
* Pass `{ notify: true }` to signal a profile view.
|
|
564
|
+
* The `account_id` is injected by the account-scoped context.
|
|
565
|
+
*/
|
|
566
|
+
get(profileId, params) {
|
|
567
|
+
return this.ctx.request({
|
|
568
|
+
method: "GET",
|
|
569
|
+
path: `/v1/profiles/${profileId}`,
|
|
570
|
+
...params ? { query: params } : {}
|
|
571
|
+
});
|
|
572
|
+
}
|
|
573
|
+
/** List the account's 1st-degree connections. `GET /v1/profiles/relations` */
|
|
574
|
+
listConnections(params) {
|
|
575
|
+
return this.ctx.request({
|
|
576
|
+
method: "GET",
|
|
577
|
+
path: "/v1/profiles/relations",
|
|
578
|
+
...params ? { query: params } : {}
|
|
579
|
+
});
|
|
580
|
+
}
|
|
581
|
+
/** List a profile's followers. `GET /v1/profiles/{profile_id}/followers` */
|
|
582
|
+
listFollowers(profileId, params) {
|
|
583
|
+
return this.ctx.request({
|
|
584
|
+
method: "GET",
|
|
585
|
+
path: `/v1/profiles/${profileId}/followers`,
|
|
586
|
+
...params ? { query: params } : {}
|
|
587
|
+
});
|
|
588
|
+
}
|
|
589
|
+
/**
|
|
590
|
+
* List a profile's posts. `GET /v1/profiles/{profile_id}/posts`
|
|
591
|
+
* Pass `{ is_company: true }` for company posts.
|
|
592
|
+
*/
|
|
593
|
+
listPosts(profileId, params) {
|
|
594
|
+
return this.ctx.request({
|
|
595
|
+
method: "GET",
|
|
596
|
+
path: `/v1/profiles/${profileId}/posts`,
|
|
597
|
+
...params ? { query: params } : {}
|
|
598
|
+
});
|
|
599
|
+
}
|
|
600
|
+
/** List a profile's comments. `GET /v1/profiles/{profile_id}/comments` */
|
|
601
|
+
listComments(profileId, params) {
|
|
602
|
+
return this.ctx.request({
|
|
603
|
+
method: "GET",
|
|
604
|
+
path: `/v1/profiles/${profileId}/comments`,
|
|
605
|
+
...params ? { query: params } : {}
|
|
606
|
+
});
|
|
607
|
+
}
|
|
608
|
+
/** List a profile's reactions. `GET /v1/profiles/{profile_id}/reactions` */
|
|
609
|
+
listReactions(profileId, params) {
|
|
610
|
+
return this.ctx.request({
|
|
611
|
+
method: "GET",
|
|
612
|
+
path: `/v1/profiles/${profileId}/reactions`,
|
|
613
|
+
...params ? { query: params } : {}
|
|
614
|
+
});
|
|
615
|
+
}
|
|
616
|
+
/** Get a company profile. `GET /v1/profiles/companies/{company_id}` */
|
|
617
|
+
getCompany(companyId) {
|
|
618
|
+
return this.ctx.request({
|
|
619
|
+
method: "GET",
|
|
620
|
+
path: `/v1/profiles/companies/${companyId}`
|
|
621
|
+
});
|
|
622
|
+
}
|
|
623
|
+
/** Endorse a skill on a profile. `POST /v1/profiles/{profile_id}/endorse` */
|
|
624
|
+
endorse(profileId, body) {
|
|
625
|
+
return this.ctx.request({
|
|
626
|
+
method: "POST",
|
|
627
|
+
path: `/v1/profiles/${profileId}/endorse`,
|
|
628
|
+
body
|
|
629
|
+
});
|
|
630
|
+
}
|
|
631
|
+
};
|
|
632
|
+
|
|
633
|
+
// src/resources/invites.ts
|
|
634
|
+
var InvitesResource = class {
|
|
635
|
+
constructor(ctx) {
|
|
636
|
+
this.ctx = ctx;
|
|
637
|
+
}
|
|
638
|
+
/** Send a connection invitation. `POST /v1/invites` */
|
|
639
|
+
send(body) {
|
|
640
|
+
return this.ctx.request({
|
|
641
|
+
method: "POST",
|
|
642
|
+
path: "/v1/invites",
|
|
643
|
+
body
|
|
644
|
+
});
|
|
645
|
+
}
|
|
646
|
+
/** List sent invitations. `GET /v1/invites/sent` */
|
|
647
|
+
listSent(params) {
|
|
648
|
+
return this.ctx.request({
|
|
649
|
+
method: "GET",
|
|
650
|
+
path: "/v1/invites/sent",
|
|
651
|
+
...params ? { query: params } : {}
|
|
652
|
+
});
|
|
653
|
+
}
|
|
654
|
+
/** List received invitations. `GET /v1/invites/received` */
|
|
655
|
+
listReceived(params) {
|
|
656
|
+
return this.ctx.request({
|
|
657
|
+
method: "GET",
|
|
658
|
+
path: "/v1/invites/received",
|
|
659
|
+
...params ? { query: params } : {}
|
|
660
|
+
});
|
|
661
|
+
}
|
|
662
|
+
/**
|
|
663
|
+
* Accept or decline a received invitation.
|
|
664
|
+
* `POST /v1/invites/received/{invitation_id}`
|
|
665
|
+
*/
|
|
666
|
+
respond(invitationId, body) {
|
|
667
|
+
return this.ctx.request({
|
|
668
|
+
method: "POST",
|
|
669
|
+
path: `/v1/invites/received/${invitationId}`,
|
|
670
|
+
body
|
|
671
|
+
});
|
|
672
|
+
}
|
|
673
|
+
/** Cancel a sent invitation. `DELETE /v1/invites/{invitation_id}` */
|
|
674
|
+
cancel(invitationId) {
|
|
675
|
+
return this.ctx.request({
|
|
676
|
+
method: "DELETE",
|
|
677
|
+
path: `/v1/invites/${invitationId}`
|
|
678
|
+
});
|
|
679
|
+
}
|
|
680
|
+
};
|
|
681
|
+
|
|
682
|
+
// src/resources/search.ts
|
|
683
|
+
var SearchResource = class {
|
|
684
|
+
constructor(ctx) {
|
|
685
|
+
this.ctx = ctx;
|
|
686
|
+
}
|
|
687
|
+
/**
|
|
688
|
+
* Resolve human-readable terms to opaque filter IDs for structured search.
|
|
689
|
+
* `GET /v1/search/parameters`
|
|
690
|
+
*/
|
|
691
|
+
getParameters(query) {
|
|
692
|
+
return this.ctx.request({
|
|
693
|
+
method: "GET",
|
|
694
|
+
path: "/v1/search/parameters",
|
|
695
|
+
query
|
|
696
|
+
});
|
|
697
|
+
}
|
|
698
|
+
/**
|
|
699
|
+
* Search LinkedIn people with structured filters or a pasted URL.
|
|
700
|
+
* `POST /v1/search/people`
|
|
701
|
+
* Cursor passed via query param (not body) — matches the OpenAPI spec.
|
|
702
|
+
*/
|
|
703
|
+
people(body) {
|
|
704
|
+
const { cursor, limit, ...rest } = body;
|
|
705
|
+
return this.ctx.request({
|
|
706
|
+
method: "POST",
|
|
707
|
+
path: "/v1/search/people",
|
|
708
|
+
body: rest,
|
|
709
|
+
...cursor !== void 0 || limit !== void 0 ? { query: { ...cursor !== void 0 ? { cursor } : {}, ...limit !== void 0 ? { limit } : {} } } : {}
|
|
710
|
+
});
|
|
711
|
+
}
|
|
712
|
+
/**
|
|
713
|
+
* Search LinkedIn companies.
|
|
714
|
+
* `POST /v1/search/companies`
|
|
715
|
+
*/
|
|
716
|
+
companies(body) {
|
|
717
|
+
const { cursor, limit, ...rest } = body;
|
|
718
|
+
return this.ctx.request({
|
|
719
|
+
method: "POST",
|
|
720
|
+
path: "/v1/search/companies",
|
|
721
|
+
body: rest,
|
|
722
|
+
...cursor !== void 0 || limit !== void 0 ? { query: { ...cursor !== void 0 ? { cursor } : {}, ...limit !== void 0 ? { limit } : {} } } : {}
|
|
723
|
+
});
|
|
724
|
+
}
|
|
725
|
+
/**
|
|
726
|
+
* Search LinkedIn posts.
|
|
727
|
+
* `POST /v1/search/posts`
|
|
728
|
+
*/
|
|
729
|
+
posts(body) {
|
|
730
|
+
const { cursor, limit, ...rest } = body;
|
|
731
|
+
return this.ctx.request({
|
|
732
|
+
method: "POST",
|
|
733
|
+
path: "/v1/search/posts",
|
|
734
|
+
body: rest,
|
|
735
|
+
...cursor !== void 0 || limit !== void 0 ? { query: { ...cursor !== void 0 ? { cursor } : {}, ...limit !== void 0 ? { limit } : {} } } : {}
|
|
736
|
+
});
|
|
737
|
+
}
|
|
738
|
+
/**
|
|
739
|
+
* Search LinkedIn jobs.
|
|
740
|
+
* `POST /v1/search/jobs`
|
|
741
|
+
*/
|
|
742
|
+
jobs(body) {
|
|
743
|
+
const { cursor, limit, ...rest } = body;
|
|
744
|
+
return this.ctx.request({
|
|
745
|
+
method: "POST",
|
|
746
|
+
path: "/v1/search/jobs",
|
|
747
|
+
body: rest,
|
|
748
|
+
...cursor !== void 0 || limit !== void 0 ? { query: { ...cursor !== void 0 ? { cursor } : {}, ...limit !== void 0 ? { limit } : {} } } : {}
|
|
749
|
+
});
|
|
750
|
+
}
|
|
751
|
+
};
|
|
752
|
+
|
|
753
|
+
// src/resources/posts.ts
|
|
754
|
+
function buildFormData2(scalars, attachments) {
|
|
755
|
+
const form = new FormData();
|
|
756
|
+
for (const [key, value] of Object.entries(scalars)) {
|
|
757
|
+
if (value !== void 0 && value !== null) {
|
|
758
|
+
form.append(key, String(value));
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
for (const attachment of attachments ?? []) {
|
|
762
|
+
if (attachment instanceof File) {
|
|
763
|
+
form.append("attachments", attachment);
|
|
764
|
+
} else {
|
|
765
|
+
form.append("attachments", new Blob([attachment]));
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
return form;
|
|
769
|
+
}
|
|
770
|
+
var PostsResource = class {
|
|
771
|
+
constructor(ctx) {
|
|
772
|
+
this.ctx = ctx;
|
|
773
|
+
}
|
|
774
|
+
/** List the account's own posts. `GET /v1/posts` */
|
|
775
|
+
list(params) {
|
|
776
|
+
return this.ctx.request({
|
|
777
|
+
method: "GET",
|
|
778
|
+
path: "/v1/posts",
|
|
779
|
+
...params ? { query: params } : {}
|
|
780
|
+
});
|
|
781
|
+
}
|
|
782
|
+
/**
|
|
783
|
+
* Create a new post. Always sent as multipart/form-data (the API only accepts
|
|
784
|
+
* multipart for this endpoint). `POST /v1/posts`
|
|
785
|
+
*/
|
|
786
|
+
create(body) {
|
|
787
|
+
const { attachments, video_thumbnail, ...scalars } = body;
|
|
788
|
+
const form = buildFormData2(scalars, attachments);
|
|
789
|
+
if (video_thumbnail) {
|
|
790
|
+
if (video_thumbnail instanceof File) {
|
|
791
|
+
form.append("video_thumbnail", video_thumbnail);
|
|
792
|
+
} else {
|
|
793
|
+
form.append("video_thumbnail", new Blob([video_thumbnail]));
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
return this.ctx.request({
|
|
797
|
+
method: "POST",
|
|
798
|
+
path: "/v1/posts",
|
|
799
|
+
body: form
|
|
800
|
+
});
|
|
801
|
+
}
|
|
802
|
+
/** Get a single post. `GET /v1/posts/{post_id}` */
|
|
803
|
+
get(postId) {
|
|
804
|
+
return this.ctx.request({
|
|
805
|
+
method: "GET",
|
|
806
|
+
path: `/v1/posts/${postId}`
|
|
807
|
+
});
|
|
808
|
+
}
|
|
809
|
+
/** List comments on a post. `GET /v1/posts/{post_id}/comments` */
|
|
810
|
+
listComments(postId, params) {
|
|
811
|
+
return this.ctx.request({
|
|
812
|
+
method: "GET",
|
|
813
|
+
path: `/v1/posts/${postId}/comments`,
|
|
814
|
+
...params ? { query: params } : {}
|
|
815
|
+
});
|
|
816
|
+
}
|
|
817
|
+
/**
|
|
818
|
+
* Comment on a post. Always sent as multipart/form-data. `POST /v1/posts/{post_id}/comments`
|
|
819
|
+
*/
|
|
820
|
+
comment(postId, body) {
|
|
821
|
+
const { attachments, ...scalars } = body;
|
|
822
|
+
return this.ctx.request({
|
|
823
|
+
method: "POST",
|
|
824
|
+
path: `/v1/posts/${postId}/comments`,
|
|
825
|
+
body: buildFormData2(scalars, attachments)
|
|
826
|
+
});
|
|
827
|
+
}
|
|
828
|
+
/** List reactions on a post. `GET /v1/posts/{post_id}/reactions` */
|
|
829
|
+
listReactions(postId, params) {
|
|
830
|
+
return this.ctx.request({
|
|
831
|
+
method: "GET",
|
|
832
|
+
path: `/v1/posts/${postId}/reactions`,
|
|
833
|
+
...params ? { query: params } : {}
|
|
834
|
+
});
|
|
835
|
+
}
|
|
836
|
+
/** React to a post. `POST /v1/posts/{post_id}/reactions` */
|
|
837
|
+
react(postId, body) {
|
|
838
|
+
return this.ctx.request({
|
|
839
|
+
method: "POST",
|
|
840
|
+
path: `/v1/posts/${postId}/reactions`,
|
|
841
|
+
body
|
|
842
|
+
});
|
|
843
|
+
}
|
|
844
|
+
};
|
|
845
|
+
|
|
846
|
+
// src/resources/sales-navigator.ts
|
|
847
|
+
function buildFormData3(scalars, attachments, voice_message, video_message) {
|
|
848
|
+
const form = new FormData();
|
|
849
|
+
for (const [key, value] of Object.entries(scalars)) {
|
|
850
|
+
if (value !== void 0 && value !== null) {
|
|
851
|
+
if (Array.isArray(value)) {
|
|
852
|
+
for (const item of value) {
|
|
853
|
+
form.append(key, String(item));
|
|
854
|
+
}
|
|
855
|
+
} else {
|
|
856
|
+
form.append(key, String(value));
|
|
857
|
+
}
|
|
858
|
+
}
|
|
859
|
+
}
|
|
860
|
+
for (const attachment of attachments ?? []) {
|
|
861
|
+
if (attachment instanceof File) {
|
|
862
|
+
form.append("attachments", attachment);
|
|
863
|
+
} else {
|
|
864
|
+
form.append("attachments", new Blob([attachment]));
|
|
865
|
+
}
|
|
866
|
+
}
|
|
867
|
+
if (voice_message) {
|
|
868
|
+
if (voice_message instanceof File) {
|
|
869
|
+
form.append("voice_message", voice_message);
|
|
870
|
+
} else {
|
|
871
|
+
form.append("voice_message", new Blob([voice_message]));
|
|
872
|
+
}
|
|
873
|
+
}
|
|
874
|
+
if (video_message) {
|
|
875
|
+
if (video_message instanceof File) {
|
|
876
|
+
form.append("video_message", video_message);
|
|
877
|
+
} else {
|
|
878
|
+
form.append("video_message", new Blob([video_message]));
|
|
879
|
+
}
|
|
880
|
+
}
|
|
881
|
+
return form;
|
|
882
|
+
}
|
|
883
|
+
var SalesNavigatorResource = class {
|
|
884
|
+
constructor(ctx) {
|
|
885
|
+
this.ctx = ctx;
|
|
886
|
+
}
|
|
887
|
+
/**
|
|
888
|
+
* Search LinkedIn members using the full Sales Navigator filter set.
|
|
889
|
+
* `POST /v1/sales-navigator/search/people`
|
|
890
|
+
* Requires tier `sn`. Returns `TIER_NOT_ACTIVE` (403) when the seat lacks it.
|
|
891
|
+
*/
|
|
892
|
+
searchPeople(body, params) {
|
|
893
|
+
return this.ctx.request({
|
|
894
|
+
method: "POST",
|
|
895
|
+
path: "/v1/sales-navigator/search/people",
|
|
896
|
+
body,
|
|
897
|
+
...params ? { query: params } : {}
|
|
898
|
+
});
|
|
899
|
+
}
|
|
900
|
+
/**
|
|
901
|
+
* Search LinkedIn companies using the full Sales Navigator company filter set.
|
|
902
|
+
* `POST /v1/sales-navigator/search/companies`
|
|
903
|
+
*/
|
|
904
|
+
searchCompanies(body, params) {
|
|
905
|
+
return this.ctx.request({
|
|
906
|
+
method: "POST",
|
|
907
|
+
path: "/v1/sales-navigator/search/companies",
|
|
908
|
+
body,
|
|
909
|
+
...params ? { query: params } : {}
|
|
910
|
+
});
|
|
911
|
+
}
|
|
912
|
+
/**
|
|
913
|
+
* Resolve human-readable terms to opaque Sales Navigator filter IDs.
|
|
914
|
+
* `GET /v1/sales-navigator/search/parameters`
|
|
915
|
+
*/
|
|
916
|
+
getParameters(params) {
|
|
917
|
+
return this.ctx.request({
|
|
918
|
+
method: "GET",
|
|
919
|
+
path: "/v1/sales-navigator/search/parameters",
|
|
920
|
+
query: params
|
|
921
|
+
});
|
|
922
|
+
}
|
|
923
|
+
/**
|
|
924
|
+
* Start a new Sales Navigator chat. Accepts optional `attachments[]`,
|
|
925
|
+
* `voice_message`, and `video_message` — when present, the request is sent
|
|
926
|
+
* as `multipart/form-data`. `POST /v1/sales-navigator/chats`
|
|
927
|
+
*/
|
|
928
|
+
startChat(body) {
|
|
929
|
+
const { attachments, voice_message, video_message, ...scalars } = body;
|
|
930
|
+
const hasFiles = attachments && attachments.length > 0 || voice_message || video_message;
|
|
931
|
+
if (hasFiles) {
|
|
932
|
+
return this.ctx.request({
|
|
933
|
+
method: "POST",
|
|
934
|
+
path: "/v1/sales-navigator/chats",
|
|
935
|
+
body: buildFormData3(
|
|
936
|
+
scalars,
|
|
937
|
+
attachments,
|
|
938
|
+
voice_message,
|
|
939
|
+
video_message
|
|
940
|
+
)
|
|
941
|
+
});
|
|
942
|
+
}
|
|
943
|
+
return this.ctx.request({
|
|
944
|
+
method: "POST",
|
|
945
|
+
path: "/v1/sales-navigator/chats",
|
|
946
|
+
body: scalars
|
|
947
|
+
});
|
|
948
|
+
}
|
|
949
|
+
/**
|
|
950
|
+
* Retrieve a LinkedIn profile with Sales Navigator enrichment.
|
|
951
|
+
* `GET /v1/sales-navigator/profiles/{identifier}`
|
|
952
|
+
*/
|
|
953
|
+
getProfile(identifier, params) {
|
|
954
|
+
return this.ctx.request({
|
|
955
|
+
method: "GET",
|
|
956
|
+
path: `/v1/sales-navigator/profiles/${identifier}`,
|
|
957
|
+
// cast needed: linkedin_sections is string[] but transport encodes arrays as repeated params
|
|
958
|
+
...params ? { query: params } : {}
|
|
959
|
+
});
|
|
960
|
+
}
|
|
961
|
+
/**
|
|
962
|
+
* Save a Sales Navigator member as a lead. `POST /v1/sales-navigator/leads/{user_id}`
|
|
963
|
+
*/
|
|
964
|
+
saveLead(userId, body) {
|
|
965
|
+
return this.ctx.request({
|
|
966
|
+
method: "POST",
|
|
967
|
+
path: `/v1/sales-navigator/leads/${userId}`,
|
|
968
|
+
body
|
|
969
|
+
});
|
|
970
|
+
}
|
|
971
|
+
/**
|
|
972
|
+
* Trigger a re-sync of the account's Sales Navigator message history.
|
|
973
|
+
* `GET /v1/sales-navigator/messages/sync`
|
|
974
|
+
*/
|
|
975
|
+
syncMessages(params) {
|
|
976
|
+
return this.ctx.request({
|
|
977
|
+
method: "GET",
|
|
978
|
+
path: "/v1/sales-navigator/messages/sync",
|
|
979
|
+
query: params
|
|
980
|
+
});
|
|
981
|
+
}
|
|
982
|
+
};
|
|
983
|
+
|
|
984
|
+
// src/resources/recruiter.ts
|
|
985
|
+
function buildFormData4(scalars, attachments, voice_message, video_message) {
|
|
986
|
+
const form = new FormData();
|
|
987
|
+
for (const [key, value] of Object.entries(scalars)) {
|
|
988
|
+
if (value !== void 0 && value !== null) {
|
|
989
|
+
if (Array.isArray(value)) {
|
|
990
|
+
for (const item of value) {
|
|
991
|
+
form.append(key, String(item));
|
|
992
|
+
}
|
|
993
|
+
} else {
|
|
994
|
+
form.append(key, String(value));
|
|
995
|
+
}
|
|
996
|
+
}
|
|
997
|
+
}
|
|
998
|
+
for (const attachment of attachments ?? []) {
|
|
999
|
+
if (attachment instanceof File) {
|
|
1000
|
+
form.append("attachments", attachment);
|
|
1001
|
+
} else {
|
|
1002
|
+
form.append("attachments", new Blob([attachment]));
|
|
1003
|
+
}
|
|
1004
|
+
}
|
|
1005
|
+
if (voice_message) {
|
|
1006
|
+
if (voice_message instanceof File) {
|
|
1007
|
+
form.append("voice_message", voice_message);
|
|
1008
|
+
} else {
|
|
1009
|
+
form.append("voice_message", new Blob([voice_message]));
|
|
1010
|
+
}
|
|
1011
|
+
}
|
|
1012
|
+
if (video_message) {
|
|
1013
|
+
if (video_message instanceof File) {
|
|
1014
|
+
form.append("video_message", video_message);
|
|
1015
|
+
} else {
|
|
1016
|
+
form.append("video_message", new Blob([video_message]));
|
|
1017
|
+
}
|
|
1018
|
+
}
|
|
1019
|
+
return form;
|
|
1020
|
+
}
|
|
1021
|
+
var RecruiterResource = class {
|
|
1022
|
+
constructor(ctx) {
|
|
1023
|
+
this.ctx = ctx;
|
|
1024
|
+
}
|
|
1025
|
+
/**
|
|
1026
|
+
* Trigger a re-sync of the account's Recruiter message history.
|
|
1027
|
+
* `GET /v1/recruiter/messages/sync`
|
|
1028
|
+
*/
|
|
1029
|
+
syncMessages(params) {
|
|
1030
|
+
return this.ctx.request({
|
|
1031
|
+
method: "GET",
|
|
1032
|
+
path: "/v1/recruiter/messages/sync",
|
|
1033
|
+
query: params
|
|
1034
|
+
});
|
|
1035
|
+
}
|
|
1036
|
+
/**
|
|
1037
|
+
* Start a Recruiter chat (InMail). Accepts optional `attachments[]`,
|
|
1038
|
+
* `voice_message`, and `video_message` — when present, the request is sent
|
|
1039
|
+
* as `multipart/form-data`. `POST /v1/recruiter/chats`
|
|
1040
|
+
*/
|
|
1041
|
+
startChat(body) {
|
|
1042
|
+
const { attachments, voice_message, video_message, ...scalars } = body;
|
|
1043
|
+
const hasFiles = attachments && attachments.length > 0 || voice_message || video_message;
|
|
1044
|
+
if (hasFiles) {
|
|
1045
|
+
return this.ctx.request({
|
|
1046
|
+
method: "POST",
|
|
1047
|
+
path: "/v1/recruiter/chats",
|
|
1048
|
+
body: buildFormData4(
|
|
1049
|
+
scalars,
|
|
1050
|
+
attachments,
|
|
1051
|
+
voice_message,
|
|
1052
|
+
video_message
|
|
1053
|
+
)
|
|
1054
|
+
});
|
|
1055
|
+
}
|
|
1056
|
+
return this.ctx.request({
|
|
1057
|
+
method: "POST",
|
|
1058
|
+
path: "/v1/recruiter/chats",
|
|
1059
|
+
body: scalars
|
|
1060
|
+
});
|
|
1061
|
+
}
|
|
1062
|
+
/**
|
|
1063
|
+
* Retrieve a LinkedIn profile with Recruiter enrichment.
|
|
1064
|
+
* `GET /v1/recruiter/profiles/{identifier}`
|
|
1065
|
+
*/
|
|
1066
|
+
getProfile(identifier, params) {
|
|
1067
|
+
return this.ctx.request({
|
|
1068
|
+
method: "GET",
|
|
1069
|
+
path: `/v1/recruiter/profiles/${identifier}`,
|
|
1070
|
+
// cast needed: linkedin_sections is string[] but transport encodes arrays as repeated params
|
|
1071
|
+
...params ? { query: params } : {}
|
|
1072
|
+
});
|
|
1073
|
+
}
|
|
1074
|
+
/**
|
|
1075
|
+
* Search LinkedIn members using Recruiter filters.
|
|
1076
|
+
* `POST /v1/recruiter/search/people`
|
|
1077
|
+
*/
|
|
1078
|
+
searchPeople(body, params) {
|
|
1079
|
+
return this.ctx.request({
|
|
1080
|
+
method: "POST",
|
|
1081
|
+
path: "/v1/recruiter/search/people",
|
|
1082
|
+
body,
|
|
1083
|
+
...params ? { query: params } : {}
|
|
1084
|
+
});
|
|
1085
|
+
}
|
|
1086
|
+
/**
|
|
1087
|
+
* Resolve human-readable terms to opaque Recruiter filter IDs.
|
|
1088
|
+
* `GET /v1/recruiter/search/parameters`
|
|
1089
|
+
*/
|
|
1090
|
+
getParameters(params) {
|
|
1091
|
+
return this.ctx.request({
|
|
1092
|
+
method: "GET",
|
|
1093
|
+
path: "/v1/recruiter/search/parameters",
|
|
1094
|
+
query: params
|
|
1095
|
+
});
|
|
1096
|
+
}
|
|
1097
|
+
/**
|
|
1098
|
+
* List Recruiter hiring projects. `GET /v1/recruiter/projects`
|
|
1099
|
+
*/
|
|
1100
|
+
listProjects(params) {
|
|
1101
|
+
return this.ctx.request({
|
|
1102
|
+
method: "GET",
|
|
1103
|
+
path: "/v1/recruiter/projects",
|
|
1104
|
+
...params ? { query: params } : {}
|
|
1105
|
+
});
|
|
1106
|
+
}
|
|
1107
|
+
/**
|
|
1108
|
+
* Get a single Recruiter hiring project. `GET /v1/recruiter/projects/{project_id}`
|
|
1109
|
+
*/
|
|
1110
|
+
getProject(projectId) {
|
|
1111
|
+
return this.ctx.request({
|
|
1112
|
+
method: "GET",
|
|
1113
|
+
path: `/v1/recruiter/projects/${projectId}`
|
|
1114
|
+
});
|
|
1115
|
+
}
|
|
1116
|
+
/**
|
|
1117
|
+
* Add a member to a hiring project pipeline as a candidate.
|
|
1118
|
+
* `POST /v1/recruiter/projects/candidates/{user_id}`
|
|
1119
|
+
*/
|
|
1120
|
+
addCandidate(userId, body) {
|
|
1121
|
+
return this.ctx.request({
|
|
1122
|
+
method: "POST",
|
|
1123
|
+
path: `/v1/recruiter/projects/candidates/${userId}`,
|
|
1124
|
+
body
|
|
1125
|
+
});
|
|
1126
|
+
}
|
|
1127
|
+
/**
|
|
1128
|
+
* Add a member to a hiring project pipeline as an applicant.
|
|
1129
|
+
* `POST /v1/recruiter/projects/applicants/{user_id}`
|
|
1130
|
+
*/
|
|
1131
|
+
addApplicant(userId, body) {
|
|
1132
|
+
return this.ctx.request({
|
|
1133
|
+
method: "POST",
|
|
1134
|
+
path: `/v1/recruiter/projects/applicants/${userId}`,
|
|
1135
|
+
body
|
|
1136
|
+
});
|
|
1137
|
+
}
|
|
1138
|
+
/**
|
|
1139
|
+
* Reject an applicant from a hiring project.
|
|
1140
|
+
* `POST /v1/recruiter/projects/applicants/{user_id}/reject`
|
|
1141
|
+
*/
|
|
1142
|
+
rejectApplicant(userId, body) {
|
|
1143
|
+
return this.ctx.request({
|
|
1144
|
+
method: "POST",
|
|
1145
|
+
path: `/v1/recruiter/projects/applicants/${userId}/reject`,
|
|
1146
|
+
body
|
|
1147
|
+
});
|
|
1148
|
+
}
|
|
1149
|
+
/**
|
|
1150
|
+
* List Recruiter job postings. `GET /v1/recruiter/jobs`
|
|
1151
|
+
*/
|
|
1152
|
+
listJobs(params) {
|
|
1153
|
+
return this.ctx.request({
|
|
1154
|
+
method: "GET",
|
|
1155
|
+
path: "/v1/recruiter/jobs",
|
|
1156
|
+
...params ? { query: params } : {}
|
|
1157
|
+
});
|
|
1158
|
+
}
|
|
1159
|
+
/**
|
|
1160
|
+
* Create a Recruiter job posting draft. `POST /v1/recruiter/jobs`
|
|
1161
|
+
*/
|
|
1162
|
+
createJob(body) {
|
|
1163
|
+
return this.ctx.request({
|
|
1164
|
+
method: "POST",
|
|
1165
|
+
path: "/v1/recruiter/jobs",
|
|
1166
|
+
body
|
|
1167
|
+
});
|
|
1168
|
+
}
|
|
1169
|
+
/**
|
|
1170
|
+
* Publish a Recruiter job posting draft.
|
|
1171
|
+
* `POST /v1/recruiter/jobs/{job_id}/publish`
|
|
1172
|
+
*/
|
|
1173
|
+
publishJob(jobId, body) {
|
|
1174
|
+
return this.ctx.request({
|
|
1175
|
+
method: "POST",
|
|
1176
|
+
path: `/v1/recruiter/jobs/${jobId}/publish`,
|
|
1177
|
+
body
|
|
1178
|
+
});
|
|
1179
|
+
}
|
|
1180
|
+
/**
|
|
1181
|
+
* Solve a publish verification checkpoint.
|
|
1182
|
+
* `POST /v1/recruiter/jobs/{job_id}/checkpoint`
|
|
1183
|
+
*/
|
|
1184
|
+
solveJobCheckpoint(jobId, body) {
|
|
1185
|
+
return this.ctx.request({
|
|
1186
|
+
method: "POST",
|
|
1187
|
+
path: `/v1/recruiter/jobs/${jobId}/checkpoint`,
|
|
1188
|
+
body
|
|
1189
|
+
});
|
|
1190
|
+
}
|
|
1191
|
+
/**
|
|
1192
|
+
* List applicants for a job posting.
|
|
1193
|
+
* `GET /v1/recruiter/jobs/{job_id}/applicants`
|
|
1194
|
+
*/
|
|
1195
|
+
listApplicants(jobId, params) {
|
|
1196
|
+
return this.ctx.request({
|
|
1197
|
+
method: "GET",
|
|
1198
|
+
path: `/v1/recruiter/jobs/${jobId}/applicants`,
|
|
1199
|
+
// cast needed: include_degree / exclude_degree can be string[] but transport encodes arrays as repeated params
|
|
1200
|
+
...params ? { query: params } : {}
|
|
1201
|
+
});
|
|
1202
|
+
}
|
|
1203
|
+
/**
|
|
1204
|
+
* Get one job applicant. `GET /v1/recruiter/jobs/applicants/{applicant_id}`
|
|
1205
|
+
*/
|
|
1206
|
+
getApplicant(applicantId) {
|
|
1207
|
+
return this.ctx.request({
|
|
1208
|
+
method: "GET",
|
|
1209
|
+
path: `/v1/recruiter/jobs/applicants/${applicantId}`
|
|
1210
|
+
});
|
|
1211
|
+
}
|
|
1212
|
+
/**
|
|
1213
|
+
* Download an applicant's resume as raw binary.
|
|
1214
|
+
* `GET /v1/recruiter/jobs/applicants/{applicant_id}/resume`
|
|
1215
|
+
* Returns `ArrayBuffer` — binary response; the SDK does not cache or store it.
|
|
1216
|
+
*/
|
|
1217
|
+
downloadResume(applicantId) {
|
|
1218
|
+
return this.ctx.request({
|
|
1219
|
+
method: "GET",
|
|
1220
|
+
path: `/v1/recruiter/jobs/applicants/${applicantId}/resume`
|
|
1221
|
+
});
|
|
1222
|
+
}
|
|
1223
|
+
};
|
|
1224
|
+
|
|
1225
|
+
// src/resources/webhooks.ts
|
|
1226
|
+
var WebhooksResource = class {
|
|
1227
|
+
constructor(ctx) {
|
|
1228
|
+
this.ctx = ctx;
|
|
1229
|
+
}
|
|
1230
|
+
/**
|
|
1231
|
+
* Register a new webhook endpoint to receive real-time events.
|
|
1232
|
+
* `POST /v1/webhooks`
|
|
1233
|
+
* The HMAC signing secret is returned exactly once in the 201 response.
|
|
1234
|
+
* `account_ids` is required (each webhook is scoped to specific accounts).
|
|
1235
|
+
*/
|
|
1236
|
+
create(body) {
|
|
1237
|
+
return this.ctx.request({
|
|
1238
|
+
method: "POST",
|
|
1239
|
+
path: "/v1/webhooks",
|
|
1240
|
+
body
|
|
1241
|
+
});
|
|
1242
|
+
}
|
|
1243
|
+
/**
|
|
1244
|
+
* List the tenant's registered webhooks, cursor-paginated.
|
|
1245
|
+
* `GET /v1/webhooks`
|
|
1246
|
+
*/
|
|
1247
|
+
list(params) {
|
|
1248
|
+
return this.ctx.request({
|
|
1249
|
+
method: "GET",
|
|
1250
|
+
path: "/v1/webhooks",
|
|
1251
|
+
...params ? { query: params } : {}
|
|
1252
|
+
});
|
|
1253
|
+
}
|
|
1254
|
+
/**
|
|
1255
|
+
* Return the complete canonical webhook event catalogue (21 events, grouped by source).
|
|
1256
|
+
* `GET /v1/webhooks/events`
|
|
1257
|
+
*/
|
|
1258
|
+
listEvents() {
|
|
1259
|
+
return this.ctx.request({
|
|
1260
|
+
method: "GET",
|
|
1261
|
+
path: "/v1/webhooks/events"
|
|
1262
|
+
});
|
|
1263
|
+
}
|
|
1264
|
+
/**
|
|
1265
|
+
* Update a webhook in place. `source` is immutable.
|
|
1266
|
+
* `PATCH /v1/webhooks/{id}`
|
|
1267
|
+
*/
|
|
1268
|
+
update(id, body) {
|
|
1269
|
+
return this.ctx.request({
|
|
1270
|
+
method: "PATCH",
|
|
1271
|
+
path: `/v1/webhooks/${id}`,
|
|
1272
|
+
body
|
|
1273
|
+
});
|
|
1274
|
+
}
|
|
1275
|
+
/**
|
|
1276
|
+
* Permanently remove a webhook subscription.
|
|
1277
|
+
* `DELETE /v1/webhooks/{id}`
|
|
1278
|
+
*/
|
|
1279
|
+
delete(id) {
|
|
1280
|
+
return this.ctx.request({
|
|
1281
|
+
method: "DELETE",
|
|
1282
|
+
path: `/v1/webhooks/${id}`
|
|
1283
|
+
});
|
|
1284
|
+
}
|
|
1285
|
+
/**
|
|
1286
|
+
* Return the set of changes since the last known version for a connected account.
|
|
1287
|
+
* Enables event-driven state sync without polling the account endpoint.
|
|
1288
|
+
* `GET /v1/accounts/{id}/state-diff`
|
|
1289
|
+
*/
|
|
1290
|
+
getStateDiff(accountId, params) {
|
|
1291
|
+
return this.ctx.request({
|
|
1292
|
+
method: "GET",
|
|
1293
|
+
path: `/v1/accounts/${accountId}/state-diff`,
|
|
1294
|
+
...params ? { query: params } : {}
|
|
1295
|
+
});
|
|
1296
|
+
}
|
|
1297
|
+
};
|
|
1298
|
+
|
|
1299
|
+
// src/resources/index.ts
|
|
1300
|
+
function buildNamespaces(ctx) {
|
|
1301
|
+
return {
|
|
1302
|
+
accounts: new AccountsResource(ctx),
|
|
1303
|
+
messaging: new MessagingResource(ctx),
|
|
1304
|
+
profiles: new ProfilesResource(ctx),
|
|
1305
|
+
invites: new InvitesResource(ctx),
|
|
1306
|
+
search: new SearchResource(ctx),
|
|
1307
|
+
posts: new PostsResource(ctx),
|
|
1308
|
+
salesNavigator: new SalesNavigatorResource(ctx),
|
|
1309
|
+
recruiter: new RecruiterResource(ctx),
|
|
1310
|
+
webhooks: new WebhooksResource(ctx)
|
|
1311
|
+
};
|
|
1312
|
+
}
|
|
1313
|
+
function buildAccountScopedNamespaces(ctx) {
|
|
1314
|
+
const { accounts: _accounts, ...rest } = buildNamespaces(ctx);
|
|
1315
|
+
void _accounts;
|
|
1316
|
+
return rest;
|
|
1317
|
+
}
|
|
1318
|
+
|
|
1319
|
+
// src/client.ts
|
|
1320
|
+
var Curviate = class {
|
|
1321
|
+
constructor(config) {
|
|
1322
|
+
this.config = resolveConfig(config);
|
|
1323
|
+
const ctx = createContext(this.config);
|
|
1324
|
+
const ns = buildNamespaces(ctx);
|
|
1325
|
+
this.accounts = ns.accounts;
|
|
1326
|
+
this.messaging = ns.messaging;
|
|
1327
|
+
this.profiles = ns.profiles;
|
|
1328
|
+
this.invites = ns.invites;
|
|
1329
|
+
this.search = ns.search;
|
|
1330
|
+
this.posts = ns.posts;
|
|
1331
|
+
this.salesNavigator = ns.salesNavigator;
|
|
1332
|
+
this.recruiter = ns.recruiter;
|
|
1333
|
+
this.webhooks = ns.webhooks;
|
|
1334
|
+
}
|
|
1335
|
+
/**
|
|
1336
|
+
* Return an account-scoped accessor that fixes `account_id` on every call,
|
|
1337
|
+
* so callers need not repeat it per method.
|
|
1338
|
+
*
|
|
1339
|
+
* @param accountId - a Curviate account id (`acc_…`). Empty throws
|
|
1340
|
+
* `CurviateError({ code: 'INVALID_REQUEST' })` synchronously, no network call.
|
|
1341
|
+
*
|
|
1342
|
+
* @example
|
|
1343
|
+
* await curviate.account("acc_123").messaging.listChats();
|
|
1344
|
+
*/
|
|
1345
|
+
account(accountId) {
|
|
1346
|
+
if (typeof accountId !== "string" || accountId.length === 0) {
|
|
1347
|
+
throw new CurviateError({
|
|
1348
|
+
code: "INVALID_REQUEST",
|
|
1349
|
+
message: "account(accountId) requires a non-empty account id.",
|
|
1350
|
+
userFixable: true,
|
|
1351
|
+
retryLikelyToSucceed: false
|
|
1352
|
+
});
|
|
1353
|
+
}
|
|
1354
|
+
const ctx = createContext(this.config, accountId);
|
|
1355
|
+
return buildAccountScopedNamespaces(ctx);
|
|
1356
|
+
}
|
|
1357
|
+
/**
|
|
1358
|
+
* Transparent cursor-pagination iterator.
|
|
1359
|
+
*
|
|
1360
|
+
* Calls `fn` repeatedly, injecting the `cursor` from each response into the
|
|
1361
|
+
* next call, yielding individual items until `cursor` is null.
|
|
1362
|
+
*
|
|
1363
|
+
* @param fn - a bound resource method (e.g. `profiles.listConnections.bind(profiles)`)
|
|
1364
|
+
* @param params - the initial params (minus cursor)
|
|
1365
|
+
*
|
|
1366
|
+
* @example
|
|
1367
|
+
* for await (const profile of curviate.paginate(
|
|
1368
|
+
* curviate.account('acc_123').profiles.listConnections,
|
|
1369
|
+
* { limit: 50 }
|
|
1370
|
+
* )) {
|
|
1371
|
+
* console.log(profile);
|
|
1372
|
+
* }
|
|
1373
|
+
*/
|
|
1374
|
+
paginate(fn, params) {
|
|
1375
|
+
return paginate(fn, params);
|
|
1376
|
+
}
|
|
1377
|
+
};
|
|
1378
|
+
|
|
1379
|
+
// src/webhooks.ts
|
|
1380
|
+
var WebhookSignatureError = class _WebhookSignatureError extends Error {
|
|
1381
|
+
constructor(reason, message) {
|
|
1382
|
+
super(message);
|
|
1383
|
+
this.name = "WebhookSignatureError";
|
|
1384
|
+
this.reason = reason;
|
|
1385
|
+
Object.setPrototypeOf(this, _WebhookSignatureError.prototype);
|
|
1386
|
+
}
|
|
1387
|
+
};
|
|
1388
|
+
function parseHeader(header) {
|
|
1389
|
+
const parts = header.split(",");
|
|
1390
|
+
let tStr;
|
|
1391
|
+
let v1;
|
|
1392
|
+
for (const part of parts) {
|
|
1393
|
+
const eq = part.indexOf("=");
|
|
1394
|
+
if (eq === -1) continue;
|
|
1395
|
+
const key = part.slice(0, eq).trim();
|
|
1396
|
+
const val = part.slice(eq + 1).trim();
|
|
1397
|
+
if (key === "t") tStr = val;
|
|
1398
|
+
else if (key === "v1") v1 = val;
|
|
1399
|
+
}
|
|
1400
|
+
if (tStr === void 0 || v1 === void 0) {
|
|
1401
|
+
throw new WebhookSignatureError(
|
|
1402
|
+
"malformed_header",
|
|
1403
|
+
'Webhook signature header must contain both "t=<timestamp>" and "v1=<hmac>".'
|
|
1404
|
+
);
|
|
1405
|
+
}
|
|
1406
|
+
const timestamp = Number(tStr);
|
|
1407
|
+
if (!Number.isFinite(timestamp) || isNaN(timestamp)) {
|
|
1408
|
+
throw new WebhookSignatureError(
|
|
1409
|
+
"malformed_header",
|
|
1410
|
+
"Webhook signature header timestamp is not a valid number."
|
|
1411
|
+
);
|
|
1412
|
+
}
|
|
1413
|
+
return { timestamp, v1 };
|
|
1414
|
+
}
|
|
1415
|
+
function hexToBytes(hex) {
|
|
1416
|
+
if (hex.length % 2 !== 0) {
|
|
1417
|
+
throw new WebhookSignatureError("malformed_header", "HMAC hex has odd length.");
|
|
1418
|
+
}
|
|
1419
|
+
const bytes = new Uint8Array(hex.length / 2);
|
|
1420
|
+
for (let i = 0; i < bytes.length; i++) {
|
|
1421
|
+
const byte = parseInt(hex.slice(i * 2, i * 2 + 2), 16);
|
|
1422
|
+
if (isNaN(byte)) {
|
|
1423
|
+
throw new WebhookSignatureError("malformed_header", "HMAC hex contains non-hex character.");
|
|
1424
|
+
}
|
|
1425
|
+
bytes[i] = byte;
|
|
1426
|
+
}
|
|
1427
|
+
return bytes;
|
|
1428
|
+
}
|
|
1429
|
+
function bytesToHex(bytes) {
|
|
1430
|
+
return Array.from(bytes).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
1431
|
+
}
|
|
1432
|
+
function constantTimeEqual(a, b) {
|
|
1433
|
+
const len = Math.max(a.length, b.length);
|
|
1434
|
+
let diff = a.length ^ b.length;
|
|
1435
|
+
for (let i = 0; i < len; i++) {
|
|
1436
|
+
const ai = i < a.length ? a[i] : 0;
|
|
1437
|
+
const bi = i < b.length ? b[i] : 0;
|
|
1438
|
+
diff |= ai ^ bi;
|
|
1439
|
+
}
|
|
1440
|
+
return diff === 0;
|
|
1441
|
+
}
|
|
1442
|
+
async function hmacWebCrypto(secret, payload) {
|
|
1443
|
+
const enc = new TextEncoder();
|
|
1444
|
+
const keyMaterial = await globalThis.crypto.subtle.importKey(
|
|
1445
|
+
"raw",
|
|
1446
|
+
enc.encode(secret),
|
|
1447
|
+
{ name: "HMAC", hash: "SHA-256" },
|
|
1448
|
+
false,
|
|
1449
|
+
["sign"]
|
|
1450
|
+
);
|
|
1451
|
+
const sig = await globalThis.crypto.subtle.sign("HMAC", keyMaterial, enc.encode(payload));
|
|
1452
|
+
return bytesToHex(new Uint8Array(sig));
|
|
1453
|
+
}
|
|
1454
|
+
function verifyAndParse(computedHex, v1, timestamp, bodyStr, replayWindowSecs) {
|
|
1455
|
+
let providedBytes;
|
|
1456
|
+
try {
|
|
1457
|
+
providedBytes = hexToBytes(v1);
|
|
1458
|
+
} catch {
|
|
1459
|
+
throw new WebhookSignatureError(
|
|
1460
|
+
"invalid_signature",
|
|
1461
|
+
"Webhook signature v1 value is not valid hex."
|
|
1462
|
+
);
|
|
1463
|
+
}
|
|
1464
|
+
const computedBytes = hexToBytes(computedHex);
|
|
1465
|
+
if (!constantTimeEqual(computedBytes, providedBytes)) {
|
|
1466
|
+
throw new WebhookSignatureError(
|
|
1467
|
+
"invalid_signature",
|
|
1468
|
+
"Webhook signature does not match. Verify your signing secret."
|
|
1469
|
+
);
|
|
1470
|
+
}
|
|
1471
|
+
const nowSecs = Date.now() / 1e3;
|
|
1472
|
+
const ageSecs = Math.abs(nowSecs - timestamp);
|
|
1473
|
+
if (ageSecs > replayWindowSecs) {
|
|
1474
|
+
throw new WebhookSignatureError(
|
|
1475
|
+
"replay_detected",
|
|
1476
|
+
`Webhook event is outside the replay window (${Math.floor(ageSecs)}s ago/ahead, window is ${replayWindowSecs}s).`
|
|
1477
|
+
);
|
|
1478
|
+
}
|
|
1479
|
+
let parsed;
|
|
1480
|
+
try {
|
|
1481
|
+
parsed = JSON.parse(bodyStr);
|
|
1482
|
+
} catch {
|
|
1483
|
+
throw new WebhookSignatureError("malformed_header", "Webhook body is not valid JSON.");
|
|
1484
|
+
}
|
|
1485
|
+
if (typeof parsed !== "object" || parsed === null || typeof parsed["type"] !== "string") {
|
|
1486
|
+
throw new WebhookSignatureError("malformed_header", 'Webhook payload missing "type" field.');
|
|
1487
|
+
}
|
|
1488
|
+
return parsed;
|
|
1489
|
+
}
|
|
1490
|
+
async function constructEvent(rawBody, signatureHeader, secret, opts) {
|
|
1491
|
+
const replayWindowSecs = opts?.replayWindowSecs ?? 300;
|
|
1492
|
+
const { timestamp, v1 } = parseHeader(signatureHeader);
|
|
1493
|
+
const bodyStr = typeof rawBody === "string" ? rawBody : rawBody.toString("utf8");
|
|
1494
|
+
const hmacPayload = `${timestamp}.${bodyStr}`;
|
|
1495
|
+
const computedHex = await hmacWebCrypto(secret, hmacPayload);
|
|
1496
|
+
return verifyAndParse(computedHex, v1, timestamp, bodyStr, replayWindowSecs);
|
|
1497
|
+
}
|
|
1498
|
+
export {
|
|
1499
|
+
Curviate,
|
|
1500
|
+
CurviateError,
|
|
1501
|
+
WebhookSignatureError,
|
|
1502
|
+
constructEvent,
|
|
1503
|
+
isCurviateError
|
|
1504
|
+
};
|