@datacline/langos-sdk-node 0.2.0-alpha.1
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 +63 -0
- package/LICENSE +21 -0
- package/README.md +190 -0
- package/dist/index.cjs +774 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +569 -0
- package/dist/index.d.ts +569 -0
- package/dist/index.mjs +758 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +64 -0
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,758 @@
|
|
|
1
|
+
import { createHmac, timingSafeEqual, randomUUID } from 'crypto';
|
|
2
|
+
|
|
3
|
+
// src/core/errors.ts
|
|
4
|
+
var LangosError = class extends Error {
|
|
5
|
+
constructor(message) {
|
|
6
|
+
super(message);
|
|
7
|
+
this.name = "LangosError";
|
|
8
|
+
Object.setPrototypeOf(this, new.target.prototype);
|
|
9
|
+
}
|
|
10
|
+
};
|
|
11
|
+
var LangosAPIError = class _LangosAPIError extends LangosError {
|
|
12
|
+
status;
|
|
13
|
+
code;
|
|
14
|
+
requestId;
|
|
15
|
+
headers;
|
|
16
|
+
body;
|
|
17
|
+
errors;
|
|
18
|
+
constructor(status, body, headers, fallbackMessage) {
|
|
19
|
+
super(body?.detail || body?.title || fallbackMessage);
|
|
20
|
+
this.name = "LangosAPIError";
|
|
21
|
+
this.status = status;
|
|
22
|
+
this.code = body?.code;
|
|
23
|
+
this.requestId = body?.request_id || headers.get("x-request-id") || void 0;
|
|
24
|
+
this.headers = headers;
|
|
25
|
+
this.body = body;
|
|
26
|
+
this.errors = body?.errors;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Typed accessor for 402 `quota_exceeded` errors. Returns the structured
|
|
30
|
+
* details payload (sessions used/limit, reset timestamp, upgrade URL, plan
|
|
31
|
+
* tier) when the error matches; otherwise `null`.
|
|
32
|
+
*
|
|
33
|
+
* Use this instead of reaching into `err.body` to render upgrade prompts.
|
|
34
|
+
*/
|
|
35
|
+
get quotaDetails() {
|
|
36
|
+
if (this.status !== 402 || this.code !== "quota_exceeded" || !this.body) return null;
|
|
37
|
+
const b = this.body;
|
|
38
|
+
if (typeof b.sessions_used !== "number" || typeof b.sessions_limit !== "number") return null;
|
|
39
|
+
return {
|
|
40
|
+
sessions_used: b.sessions_used,
|
|
41
|
+
sessions_limit: b.sessions_limit,
|
|
42
|
+
reset_at: typeof b.reset_at === "string" ? b.reset_at : "",
|
|
43
|
+
upgrade_url: typeof b.upgrade_url === "string" ? b.upgrade_url : null,
|
|
44
|
+
plan_tier: typeof b.plan_tier === "string" ? b.plan_tier : ""
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Typed accessor for 403 `feature_not_available` errors. Returns the gated
|
|
49
|
+
* feature key and an upgrade URL; otherwise `null`.
|
|
50
|
+
*/
|
|
51
|
+
get featureDetails() {
|
|
52
|
+
if (this.status !== 403 || this.code !== "feature_not_available" || !this.body) return null;
|
|
53
|
+
const b = this.body;
|
|
54
|
+
if (typeof b.feature !== "string") return null;
|
|
55
|
+
return {
|
|
56
|
+
feature: b.feature,
|
|
57
|
+
upgrade_url: typeof b.upgrade_url === "string" ? b.upgrade_url : null
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
static from(status, body, headers) {
|
|
61
|
+
const parsed = body && typeof body === "object" ? body : void 0;
|
|
62
|
+
switch (status) {
|
|
63
|
+
case 400:
|
|
64
|
+
return new LangosBadRequestError(status, parsed, headers, "Bad Request");
|
|
65
|
+
case 401:
|
|
66
|
+
return new LangosAuthenticationError(status, parsed, headers, "Unauthorized");
|
|
67
|
+
case 403:
|
|
68
|
+
return new LangosForbiddenError(status, parsed, headers, "Forbidden");
|
|
69
|
+
case 404:
|
|
70
|
+
return new LangosNotFoundError(status, parsed, headers, "Not Found");
|
|
71
|
+
case 409:
|
|
72
|
+
return new LangosConflictError(status, parsed, headers, "Conflict");
|
|
73
|
+
case 422:
|
|
74
|
+
return new LangosBadRequestError(status, parsed, headers, "Unprocessable Entity");
|
|
75
|
+
case 429: {
|
|
76
|
+
const retryAfterRaw = headers.get("retry-after");
|
|
77
|
+
const retryAfter = retryAfterRaw ? parseInt(retryAfterRaw, 10) : void 0;
|
|
78
|
+
return new LangosRateLimitError(
|
|
79
|
+
status,
|
|
80
|
+
parsed,
|
|
81
|
+
headers,
|
|
82
|
+
"Rate limit exceeded",
|
|
83
|
+
Number.isFinite(retryAfter) ? retryAfter : void 0
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
default:
|
|
87
|
+
if (status >= 500) {
|
|
88
|
+
return new LangosServerError(status, parsed, headers, "Server Error");
|
|
89
|
+
}
|
|
90
|
+
return new _LangosAPIError(status, parsed, headers, `HTTP ${status}`);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
};
|
|
94
|
+
function tagged(Cls, tag) {
|
|
95
|
+
return class extends Cls {
|
|
96
|
+
constructor(...args) {
|
|
97
|
+
super(...args);
|
|
98
|
+
this.name = tag;
|
|
99
|
+
}
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
var LangosAuthenticationError = tagged(LangosAPIError, "LangosAuthenticationError");
|
|
103
|
+
var LangosForbiddenError = tagged(LangosAPIError, "LangosForbiddenError");
|
|
104
|
+
var LangosNotFoundError = tagged(LangosAPIError, "LangosNotFoundError");
|
|
105
|
+
var LangosBadRequestError = tagged(LangosAPIError, "LangosBadRequestError");
|
|
106
|
+
var LangosConflictError = tagged(LangosAPIError, "LangosConflictError");
|
|
107
|
+
var LangosServerError = tagged(LangosAPIError, "LangosServerError");
|
|
108
|
+
var LangosRateLimitError = class extends LangosAPIError {
|
|
109
|
+
retryAfter;
|
|
110
|
+
constructor(status, body, headers, fallback, retryAfter) {
|
|
111
|
+
super(status, body, headers, fallback);
|
|
112
|
+
this.name = "LangosRateLimitError";
|
|
113
|
+
this.retryAfter = retryAfter;
|
|
114
|
+
}
|
|
115
|
+
};
|
|
116
|
+
var LangosConnectionError = class extends LangosError {
|
|
117
|
+
cause;
|
|
118
|
+
constructor(message, cause) {
|
|
119
|
+
super(message);
|
|
120
|
+
this.name = "LangosConnectionError";
|
|
121
|
+
this.cause = cause;
|
|
122
|
+
}
|
|
123
|
+
};
|
|
124
|
+
var LangosTimeoutError = class extends LangosError {
|
|
125
|
+
timeoutMs;
|
|
126
|
+
constructor(timeoutMs) {
|
|
127
|
+
super(`Request timed out after ${timeoutMs}ms`);
|
|
128
|
+
this.name = "LangosTimeoutError";
|
|
129
|
+
this.timeoutMs = timeoutMs;
|
|
130
|
+
}
|
|
131
|
+
};
|
|
132
|
+
var LangosSignatureVerificationError = class extends LangosError {
|
|
133
|
+
constructor(reason) {
|
|
134
|
+
super(`Webhook signature verification failed: ${reason}`);
|
|
135
|
+
this.name = "LangosSignatureVerificationError";
|
|
136
|
+
}
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
// src/core/retry.ts
|
|
140
|
+
var INITIAL_MS = 500;
|
|
141
|
+
var MAX_MS = 8e3;
|
|
142
|
+
function shouldRetry(status, isNetworkError) {
|
|
143
|
+
if (isNetworkError) return true;
|
|
144
|
+
if (status === null) return false;
|
|
145
|
+
if (status === 408 || status === 429) return true;
|
|
146
|
+
if (status >= 500 && status !== 501) return true;
|
|
147
|
+
return false;
|
|
148
|
+
}
|
|
149
|
+
function backoffDelay(attempt, retryAfterSeconds) {
|
|
150
|
+
if (retryAfterSeconds && retryAfterSeconds > 0) {
|
|
151
|
+
return Math.min(retryAfterSeconds * 1e3, MAX_MS * 4);
|
|
152
|
+
}
|
|
153
|
+
const base = Math.min(INITIAL_MS * Math.pow(2, attempt), MAX_MS);
|
|
154
|
+
return Math.floor(base * (0.5 + Math.random() * 0.5));
|
|
155
|
+
}
|
|
156
|
+
function sleep(ms, signal) {
|
|
157
|
+
return new Promise((resolve, reject) => {
|
|
158
|
+
setTimeout(resolve, ms);
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
var UNSAFE = /* @__PURE__ */ new Set(["POST", "PATCH", "DELETE", "PUT"]);
|
|
162
|
+
function shouldAddIdempotencyKey(method, hasUserKey) {
|
|
163
|
+
if (hasUserKey) return false;
|
|
164
|
+
return UNSAFE.has(method.toUpperCase());
|
|
165
|
+
}
|
|
166
|
+
function newIdempotencyKey() {
|
|
167
|
+
return randomUUID();
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// src/core/version.ts
|
|
171
|
+
var version = "0.2.0-alpha.1";
|
|
172
|
+
|
|
173
|
+
// src/core/request.ts
|
|
174
|
+
async function makeRequest(cfg, req) {
|
|
175
|
+
const url = buildUrl(cfg.baseUrl, req.path, req.query);
|
|
176
|
+
const headers = buildHeaders(cfg, req);
|
|
177
|
+
const userMaxRetries = req.options?.maxRetries ?? cfg.maxRetries;
|
|
178
|
+
const userTimeout = req.options?.timeout ?? cfg.timeout;
|
|
179
|
+
const userSignal = req.options?.signal;
|
|
180
|
+
const bodyJson = req.body !== void 0 ? JSON.stringify(req.body) : void 0;
|
|
181
|
+
let attempt = 0;
|
|
182
|
+
while (true) {
|
|
183
|
+
const ac = new AbortController();
|
|
184
|
+
const onUserAbort = () => ac.abort();
|
|
185
|
+
if (userSignal) {
|
|
186
|
+
if (userSignal.aborted) ac.abort();
|
|
187
|
+
else userSignal.addEventListener("abort", onUserAbort, { once: true });
|
|
188
|
+
}
|
|
189
|
+
const timer = setTimeout(() => ac.abort(), userTimeout);
|
|
190
|
+
let response = null;
|
|
191
|
+
let networkError = null;
|
|
192
|
+
cfg.logger.debug({ method: req.method, url, attempt }, "langos request");
|
|
193
|
+
try {
|
|
194
|
+
response = await cfg.fetchImpl(url, {
|
|
195
|
+
method: req.method,
|
|
196
|
+
headers,
|
|
197
|
+
body: bodyJson,
|
|
198
|
+
signal: ac.signal
|
|
199
|
+
});
|
|
200
|
+
} catch (err) {
|
|
201
|
+
networkError = err;
|
|
202
|
+
} finally {
|
|
203
|
+
clearTimeout(timer);
|
|
204
|
+
if (userSignal) userSignal.removeEventListener("abort", onUserAbort);
|
|
205
|
+
}
|
|
206
|
+
if (networkError) {
|
|
207
|
+
const isTimeout = ac.signal.aborted && !userSignal?.aborted && networkError?.name === "AbortError";
|
|
208
|
+
if (userSignal?.aborted) throw networkError;
|
|
209
|
+
if (attempt < userMaxRetries && shouldRetry(null, true)) {
|
|
210
|
+
const delay = backoffDelay(attempt);
|
|
211
|
+
cfg.logger.debug({ delay, err: String(networkError) }, "langos retry (network)");
|
|
212
|
+
await sleep(delay);
|
|
213
|
+
attempt++;
|
|
214
|
+
continue;
|
|
215
|
+
}
|
|
216
|
+
if (isTimeout) throw new LangosTimeoutError(userTimeout);
|
|
217
|
+
throw new LangosConnectionError(
|
|
218
|
+
`Network error: ${networkError?.message || String(networkError)}`,
|
|
219
|
+
networkError
|
|
220
|
+
);
|
|
221
|
+
}
|
|
222
|
+
const status = response.status;
|
|
223
|
+
if (status >= 400) {
|
|
224
|
+
const retryAfterRaw = response.headers.get("retry-after");
|
|
225
|
+
const retryAfter = retryAfterRaw ? parseInt(retryAfterRaw, 10) : void 0;
|
|
226
|
+
const body = await safeReadJson(response);
|
|
227
|
+
if (attempt < userMaxRetries && shouldRetry(status, false)) {
|
|
228
|
+
const delay = backoffDelay(
|
|
229
|
+
attempt,
|
|
230
|
+
Number.isFinite(retryAfter) ? retryAfter : void 0
|
|
231
|
+
);
|
|
232
|
+
cfg.logger.debug({ delay, status }, "langos retry (status)");
|
|
233
|
+
await sleep(delay);
|
|
234
|
+
attempt++;
|
|
235
|
+
LangosAPIError.from(status, body, response.headers);
|
|
236
|
+
continue;
|
|
237
|
+
}
|
|
238
|
+
throw LangosAPIError.from(status, body, response.headers);
|
|
239
|
+
}
|
|
240
|
+
if (status === 204) return void 0;
|
|
241
|
+
const data = await safeReadJson(response);
|
|
242
|
+
return data;
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
function buildUrl(baseUrl, path, query) {
|
|
246
|
+
const trimmed = baseUrl.replace(/\/$/, "");
|
|
247
|
+
const url = new URL(`${trimmed}${path.startsWith("/") ? path : `/${path}`}`);
|
|
248
|
+
if (query) {
|
|
249
|
+
for (const [k, v] of Object.entries(query)) {
|
|
250
|
+
if (v !== void 0 && v !== null && v !== "") url.searchParams.set(k, String(v));
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
return url.toString();
|
|
254
|
+
}
|
|
255
|
+
function buildHeaders(cfg, req) {
|
|
256
|
+
const headers = new Headers();
|
|
257
|
+
headers.set("Authorization", `Bearer ${cfg.apiKey}`);
|
|
258
|
+
headers.set("Accept", "application/json");
|
|
259
|
+
if (req.body !== void 0) headers.set("Content-Type", "application/json");
|
|
260
|
+
headers.set("User-Agent", buildUserAgent(cfg));
|
|
261
|
+
if (cfg.telemetry !== false) {
|
|
262
|
+
headers.set(
|
|
263
|
+
"X-Langos-Client-Telemetry",
|
|
264
|
+
JSON.stringify({
|
|
265
|
+
lang: "node",
|
|
266
|
+
sdkVersion: version,
|
|
267
|
+
runtime: typeof process !== "undefined" ? `${process.platform}/${process.version}` : "unknown"
|
|
268
|
+
})
|
|
269
|
+
);
|
|
270
|
+
}
|
|
271
|
+
if (req.options?.idempotencyKey || shouldAddIdempotencyKey(req.method, !!req.options?.idempotencyKey)) {
|
|
272
|
+
headers.set("Idempotency-Key", req.options?.idempotencyKey ?? newIdempotencyKey());
|
|
273
|
+
}
|
|
274
|
+
for (const [k, v] of Object.entries(req.options?.headers ?? {})) {
|
|
275
|
+
headers.set(k, v);
|
|
276
|
+
}
|
|
277
|
+
return headers;
|
|
278
|
+
}
|
|
279
|
+
function buildUserAgent(cfg) {
|
|
280
|
+
const base = `langos-node/${version}`;
|
|
281
|
+
return cfg.appName ? `${base} ${cfg.appName}` : base;
|
|
282
|
+
}
|
|
283
|
+
async function safeReadJson(response) {
|
|
284
|
+
const text = await response.text();
|
|
285
|
+
if (!text) return void 0;
|
|
286
|
+
try {
|
|
287
|
+
return JSON.parse(text);
|
|
288
|
+
} catch {
|
|
289
|
+
return text;
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// src/resources/base.ts
|
|
294
|
+
var APIResource = class {
|
|
295
|
+
cfg;
|
|
296
|
+
constructor(cfg) {
|
|
297
|
+
this.cfg = cfg;
|
|
298
|
+
}
|
|
299
|
+
get(path, query, options) {
|
|
300
|
+
return makeRequest(this.cfg, { method: "GET", path, query, options });
|
|
301
|
+
}
|
|
302
|
+
post(path, body, options) {
|
|
303
|
+
return makeRequest(this.cfg, { method: "POST", path, body, options });
|
|
304
|
+
}
|
|
305
|
+
delete(path, options) {
|
|
306
|
+
return makeRequest(this.cfg, { method: "DELETE", path, options });
|
|
307
|
+
}
|
|
308
|
+
patch(path, body, options) {
|
|
309
|
+
return makeRequest(this.cfg, { method: "PATCH", path, body, options });
|
|
310
|
+
}
|
|
311
|
+
};
|
|
312
|
+
|
|
313
|
+
// src/core/transform.ts
|
|
314
|
+
function assessmentFromWire(w) {
|
|
315
|
+
return {
|
|
316
|
+
id: String(w.id),
|
|
317
|
+
object: "assessment",
|
|
318
|
+
name: String(w.name),
|
|
319
|
+
description: w.description ?? null,
|
|
320
|
+
challengeCount: Number(w.challenge_count ?? 0),
|
|
321
|
+
createdAt: String(w.created_at),
|
|
322
|
+
updatedAt: String(w.updated_at)
|
|
323
|
+
};
|
|
324
|
+
}
|
|
325
|
+
function candidateFromWire(w) {
|
|
326
|
+
return {
|
|
327
|
+
id: String(w.id),
|
|
328
|
+
object: "candidate",
|
|
329
|
+
email: String(w.email),
|
|
330
|
+
name: w.name ?? null,
|
|
331
|
+
assessmentId: String(w.assessment_id),
|
|
332
|
+
externalId: w.external_id ?? null,
|
|
333
|
+
status: w.status,
|
|
334
|
+
invitationUrl: w.invitation_url ?? null,
|
|
335
|
+
invitedAt: w.invited_at ?? null,
|
|
336
|
+
startedAt: w.started_at ?? null,
|
|
337
|
+
completedAt: w.completed_at ?? null,
|
|
338
|
+
cancelledAt: w.cancelled_at ?? null,
|
|
339
|
+
expiresAt: w.expires_at ?? null,
|
|
340
|
+
latestSessionId: w.latest_session_id ?? null,
|
|
341
|
+
score: w.score === null || w.score === void 0 ? null : Number(w.score),
|
|
342
|
+
metadata: w.metadata ?? null,
|
|
343
|
+
createdAt: String(w.created_at),
|
|
344
|
+
updatedAt: String(w.updated_at)
|
|
345
|
+
};
|
|
346
|
+
}
|
|
347
|
+
function candidateCreateToWire(p) {
|
|
348
|
+
const expires = p.expiresAt instanceof Date ? p.expiresAt.toISOString() : p.expiresAt ?? null;
|
|
349
|
+
return {
|
|
350
|
+
email: p.email,
|
|
351
|
+
assessment_id: p.assessmentId,
|
|
352
|
+
name: p.name ?? null,
|
|
353
|
+
external_id: p.externalId ?? null,
|
|
354
|
+
expires_at: expires,
|
|
355
|
+
metadata: p.metadata ?? null
|
|
356
|
+
};
|
|
357
|
+
}
|
|
358
|
+
function submissionFromWire(w) {
|
|
359
|
+
if (!w) return null;
|
|
360
|
+
return {
|
|
361
|
+
language: w.language ?? null,
|
|
362
|
+
finalCode: w.final_code ?? null,
|
|
363
|
+
diff: w.diff ?? null,
|
|
364
|
+
commitSha: w.commit_sha ?? null,
|
|
365
|
+
previewUrl: w.preview_url ?? null
|
|
366
|
+
};
|
|
367
|
+
}
|
|
368
|
+
function feedbackFromWire(w) {
|
|
369
|
+
if (!w) return null;
|
|
370
|
+
return {
|
|
371
|
+
notes: w.notes ?? null,
|
|
372
|
+
problemSolvingScore: w.problem_solving_score === null || w.problem_solving_score === void 0 ? null : Number(w.problem_solving_score),
|
|
373
|
+
codeQualityScore: w.code_quality_score === null || w.code_quality_score === void 0 ? null : Number(w.code_quality_score)
|
|
374
|
+
};
|
|
375
|
+
}
|
|
376
|
+
function analyticsFromWire(w) {
|
|
377
|
+
const a = w || {};
|
|
378
|
+
return {
|
|
379
|
+
editorEventCount: Number(a.editor_event_count ?? 0),
|
|
380
|
+
runEventCount: Number(a.run_event_count ?? 0),
|
|
381
|
+
testEventCount: Number(a.test_event_count ?? 0),
|
|
382
|
+
aiEventCount: Number(a.ai_event_count ?? 0),
|
|
383
|
+
pasteEventCount: Number(a.paste_event_count ?? 0),
|
|
384
|
+
activeSeconds: Number(a.active_seconds ?? 0),
|
|
385
|
+
idleSeconds: Number(a.idle_seconds ?? 0),
|
|
386
|
+
codingSeconds: Number(a.coding_seconds ?? 0)
|
|
387
|
+
};
|
|
388
|
+
}
|
|
389
|
+
function sessionFromWire(w) {
|
|
390
|
+
return {
|
|
391
|
+
id: String(w.id),
|
|
392
|
+
object: "session",
|
|
393
|
+
candidateId: String(w.candidate_id),
|
|
394
|
+
assessmentId: String(w.assessment_id),
|
|
395
|
+
status: w.status,
|
|
396
|
+
attempt: Number(w.attempt ?? 1),
|
|
397
|
+
startedAt: w.started_at ?? null,
|
|
398
|
+
completedAt: w.completed_at ?? null,
|
|
399
|
+
durationSeconds: w.duration_seconds ?? null,
|
|
400
|
+
score: w.score === null || w.score === void 0 ? null : Number(w.score),
|
|
401
|
+
passed: typeof w.passed === "boolean" ? w.passed : null,
|
|
402
|
+
reportUrl: w.report_url ?? null,
|
|
403
|
+
submission: submissionFromWire(w.submission),
|
|
404
|
+
analytics: analyticsFromWire(w.analytics),
|
|
405
|
+
insights: insightsFromWire(w.insights),
|
|
406
|
+
feedback: feedbackFromWire(w.feedback),
|
|
407
|
+
createdAt: String(w.created_at),
|
|
408
|
+
updatedAt: String(w.updated_at)
|
|
409
|
+
};
|
|
410
|
+
}
|
|
411
|
+
var PLAN_TIERS = ["free", "starter", "mid_tier", "growth", "custom"];
|
|
412
|
+
function planTierFromWire(raw) {
|
|
413
|
+
return typeof raw === "string" && PLAN_TIERS.includes(raw) ? raw : "free";
|
|
414
|
+
}
|
|
415
|
+
function accountFromWire(w) {
|
|
416
|
+
return {
|
|
417
|
+
id: String(w.id),
|
|
418
|
+
object: "account",
|
|
419
|
+
name: String(w.name),
|
|
420
|
+
slug: String(w.slug ?? ""),
|
|
421
|
+
planTier: planTierFromWire(w.plan_tier),
|
|
422
|
+
billingCycle: String(w.billing_cycle),
|
|
423
|
+
status: String(w.status),
|
|
424
|
+
sessionsUsed: Number(w.sessions_used ?? 0),
|
|
425
|
+
sessionsLimit: w.sessions_limit === null || w.sessions_limit === void 0 ? null : Number(w.sessions_limit),
|
|
426
|
+
sessionsRemaining: w.sessions_remaining === null || w.sessions_remaining === void 0 ? null : Number(w.sessions_remaining),
|
|
427
|
+
trialEndsAt: w.trial_ends_at ?? null,
|
|
428
|
+
features: {
|
|
429
|
+
webIdeEnabled: !!w.features?.web_ide_enabled,
|
|
430
|
+
replayEnabled: !!w.features?.replay_enabled,
|
|
431
|
+
aiAssistanceEnabled: !!w.features?.ai_assistance_enabled
|
|
432
|
+
},
|
|
433
|
+
planCaps: w.plan_caps ? {
|
|
434
|
+
label: String(w.plan_caps.label ?? ""),
|
|
435
|
+
priceMonthly: w.plan_caps.price_monthly === null || w.plan_caps.price_monthly === void 0 ? null : Number(w.plan_caps.price_monthly),
|
|
436
|
+
billing: "monthly",
|
|
437
|
+
currency: "USD",
|
|
438
|
+
sessionLimit: w.plan_caps.session_limit === null || w.plan_caps.session_limit === void 0 ? null : Number(w.plan_caps.session_limit),
|
|
439
|
+
readyMadeChallenges: w.plan_caps.ready_made_challenges === null || w.plan_caps.ready_made_challenges === void 0 ? null : Number(w.plan_caps.ready_made_challenges),
|
|
440
|
+
customChallengesMax: w.plan_caps.custom_challenges_max === null || w.plan_caps.custom_challenges_max === void 0 ? null : Number(w.plan_caps.custom_challenges_max),
|
|
441
|
+
features: w.plan_caps.features ?? {},
|
|
442
|
+
support: String(w.plan_caps.support ?? ""),
|
|
443
|
+
popular: w.plan_caps.popular === true,
|
|
444
|
+
contactSales: w.plan_caps.contact_sales === true
|
|
445
|
+
} : null,
|
|
446
|
+
integration: {
|
|
447
|
+
provider: String(w.integration?.provider ?? ""),
|
|
448
|
+
apiKeyPrefix: String(w.integration?.api_key_prefix ?? ""),
|
|
449
|
+
scopes: Array.isArray(w.integration?.scopes) ? w.integration.scopes.map(String) : [],
|
|
450
|
+
rateLimitPerMinute: w.integration?.rate_limit_per_minute === null || w.integration?.rate_limit_per_minute === void 0 ? null : Number(w.integration.rate_limit_per_minute),
|
|
451
|
+
webhookUrl: w.integration?.webhook_url ?? null
|
|
452
|
+
}
|
|
453
|
+
};
|
|
454
|
+
}
|
|
455
|
+
function webhookEndpointFromWire(w) {
|
|
456
|
+
return {
|
|
457
|
+
object: "webhook_endpoint",
|
|
458
|
+
webhookUrl: w.webhook_url ?? null,
|
|
459
|
+
signingSecret: w.signing_secret ?? null
|
|
460
|
+
};
|
|
461
|
+
}
|
|
462
|
+
function webhookEndpointParamsToWire(p) {
|
|
463
|
+
const out = {};
|
|
464
|
+
if (p.webhookUrl !== void 0) out.webhook_url = p.webhookUrl;
|
|
465
|
+
if (p.rotateSigningSecret !== void 0) out.rotate_signing_secret = p.rotateSigningSecret;
|
|
466
|
+
return out;
|
|
467
|
+
}
|
|
468
|
+
function insightsFromWire(w) {
|
|
469
|
+
if (!w) return null;
|
|
470
|
+
return {
|
|
471
|
+
aiUsagePercent: w.ai_usage_percent === null || w.ai_usage_percent === void 0 ? null : Number(w.ai_usage_percent),
|
|
472
|
+
testPassRate: w.test_pass_rate === null || w.test_pass_rate === void 0 ? null : Number(w.test_pass_rate),
|
|
473
|
+
codeQuality: w.code_quality ?? null,
|
|
474
|
+
...passthroughExtras(w, ["ai_usage_percent", "test_pass_rate", "code_quality"])
|
|
475
|
+
};
|
|
476
|
+
}
|
|
477
|
+
function passthroughExtras(o, known) {
|
|
478
|
+
const out = {};
|
|
479
|
+
for (const [k, v] of Object.entries(o)) {
|
|
480
|
+
if (!known.includes(k)) out[k] = v;
|
|
481
|
+
}
|
|
482
|
+
return out;
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
// src/resources/account.ts
|
|
486
|
+
var AccountResource = class extends APIResource {
|
|
487
|
+
/**
|
|
488
|
+
* Read the calling integration's account.
|
|
489
|
+
*
|
|
490
|
+
* Includes plan, quota, feature flags, and integration metadata
|
|
491
|
+
* (key prefix, scopes, rate limit, webhook URL).
|
|
492
|
+
*
|
|
493
|
+
* @returns {Promise<Account>}
|
|
494
|
+
*/
|
|
495
|
+
async retrieve(options) {
|
|
496
|
+
const w = await this.get("/account", void 0, options);
|
|
497
|
+
return accountFromWire(w);
|
|
498
|
+
}
|
|
499
|
+
/**
|
|
500
|
+
* Set or clear the outbound webhook URL, and optionally rotate the signing
|
|
501
|
+
* secret.
|
|
502
|
+
*
|
|
503
|
+
* When `rotateSigningSecret: true`, the response includes the new secret in
|
|
504
|
+
* `signingSecret`. Store it — the secret cannot be re-fetched. Subsequent
|
|
505
|
+
* reads return `signingSecret: null`.
|
|
506
|
+
*
|
|
507
|
+
* @param {WebhookEndpointParams} params
|
|
508
|
+
*/
|
|
509
|
+
async setWebhookEndpoint(params, options) {
|
|
510
|
+
const body = webhookEndpointParamsToWire(params);
|
|
511
|
+
const w = await this.patch("/account/webhook-endpoint", body, options);
|
|
512
|
+
return webhookEndpointFromWire(w);
|
|
513
|
+
}
|
|
514
|
+
};
|
|
515
|
+
|
|
516
|
+
// src/core/pagination.ts
|
|
517
|
+
async function fetchPage(fetcher, mapItem, initialCursor) {
|
|
518
|
+
const wire = await fetcher(initialCursor);
|
|
519
|
+
return wrapPage(wire, mapItem, fetcher);
|
|
520
|
+
}
|
|
521
|
+
function wrapPage(wire, mapItem, fetcher) {
|
|
522
|
+
const data = wire.data.map(mapItem);
|
|
523
|
+
const page = {
|
|
524
|
+
data,
|
|
525
|
+
hasMore: !!wire.has_more,
|
|
526
|
+
nextCursor: wire.next_cursor ?? null,
|
|
527
|
+
async getNextPage() {
|
|
528
|
+
if (!wire.has_more || !wire.next_cursor) return null;
|
|
529
|
+
const next = await fetcher(wire.next_cursor);
|
|
530
|
+
return wrapPage(next, mapItem, fetcher);
|
|
531
|
+
}
|
|
532
|
+
};
|
|
533
|
+
const iter = {
|
|
534
|
+
async *[Symbol.asyncIterator]() {
|
|
535
|
+
let current = page;
|
|
536
|
+
while (current) {
|
|
537
|
+
for (const item of current.data) yield item;
|
|
538
|
+
current = await current.getNextPage();
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
};
|
|
542
|
+
return Object.assign({}, page, iter, {
|
|
543
|
+
[Symbol.asyncIterator]: iter[Symbol.asyncIterator].bind(iter)
|
|
544
|
+
});
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
// src/resources/assessments.ts
|
|
548
|
+
var AssessmentsResource = class extends APIResource {
|
|
549
|
+
list(params = {}, options) {
|
|
550
|
+
return fetchPage(
|
|
551
|
+
(cursor) => this.get(
|
|
552
|
+
"/assessments",
|
|
553
|
+
{ limit: params.limit, cursor: cursor ?? params.cursor },
|
|
554
|
+
options
|
|
555
|
+
),
|
|
556
|
+
assessmentFromWire
|
|
557
|
+
);
|
|
558
|
+
}
|
|
559
|
+
async retrieve(id, options) {
|
|
560
|
+
const w = await this.get(`/assessments/${encodeURIComponent(id)}`, void 0, options);
|
|
561
|
+
return assessmentFromWire(w);
|
|
562
|
+
}
|
|
563
|
+
};
|
|
564
|
+
|
|
565
|
+
// src/resources/candidates.ts
|
|
566
|
+
var CandidatesResource = class extends APIResource {
|
|
567
|
+
list(params = {}, options) {
|
|
568
|
+
return fetchPage(
|
|
569
|
+
(cursor) => this.get("/candidates", {
|
|
570
|
+
limit: params.limit,
|
|
571
|
+
cursor: cursor ?? params.cursor,
|
|
572
|
+
status: params.status,
|
|
573
|
+
assessment_id: params.assessmentId
|
|
574
|
+
}, options),
|
|
575
|
+
candidateFromWire
|
|
576
|
+
);
|
|
577
|
+
}
|
|
578
|
+
async retrieve(id, options) {
|
|
579
|
+
const w = await this.get(`/candidates/${encodeURIComponent(id)}`, void 0, options);
|
|
580
|
+
return candidateFromWire(w);
|
|
581
|
+
}
|
|
582
|
+
async create(params, options) {
|
|
583
|
+
const w = await this.post("/candidates", candidateCreateToWire(params), options);
|
|
584
|
+
return candidateFromWire(w);
|
|
585
|
+
}
|
|
586
|
+
/** Idempotent — calling twice on a cancelled candidate returns the same record. */
|
|
587
|
+
async cancel(id, options) {
|
|
588
|
+
const w = await this.delete(`/candidates/${encodeURIComponent(id)}`, options);
|
|
589
|
+
return candidateFromWire(w);
|
|
590
|
+
}
|
|
591
|
+
};
|
|
592
|
+
|
|
593
|
+
// src/resources/sessions.ts
|
|
594
|
+
var SessionsResource = class extends APIResource {
|
|
595
|
+
async retrieve(id, options) {
|
|
596
|
+
const w = await this.get(`/sessions/${encodeURIComponent(id)}`, void 0, options);
|
|
597
|
+
return sessionFromWire(w);
|
|
598
|
+
}
|
|
599
|
+
listForCandidate(candidateId, params = {}, options) {
|
|
600
|
+
return fetchPage(
|
|
601
|
+
(cursor) => this.get(
|
|
602
|
+
`/candidates/${encodeURIComponent(candidateId)}/sessions`,
|
|
603
|
+
{ limit: params.limit, cursor: cursor ?? params.cursor },
|
|
604
|
+
options
|
|
605
|
+
),
|
|
606
|
+
sessionFromWire
|
|
607
|
+
);
|
|
608
|
+
}
|
|
609
|
+
};
|
|
610
|
+
var DEFAULT_TOLERANCE_SECONDS = 300;
|
|
611
|
+
var MIN_SECRET_LENGTH = 16;
|
|
612
|
+
var MAX_TIMESTAMP_SECONDS = 2 ** 32;
|
|
613
|
+
var Webhooks = {
|
|
614
|
+
constructEvent(payload, signatureHeader, secret, tolerance = DEFAULT_TOLERANCE_SECONDS) {
|
|
615
|
+
if (typeof secret !== "string" || secret.length < MIN_SECRET_LENGTH) {
|
|
616
|
+
throw new LangosSignatureVerificationError(
|
|
617
|
+
`Signing secret missing or too short (need at least ${MIN_SECRET_LENGTH} chars). Set the secret returned by \`client.account.rotateSigningSecret()\`.`
|
|
618
|
+
);
|
|
619
|
+
}
|
|
620
|
+
if (!signatureHeader) {
|
|
621
|
+
throw new LangosSignatureVerificationError("Missing Langos-Signature header");
|
|
622
|
+
}
|
|
623
|
+
const sigHeader = Array.isArray(signatureHeader) ? signatureHeader[0] : signatureHeader;
|
|
624
|
+
const parsed = parseSignatureHeader(sigHeader);
|
|
625
|
+
if (parsed.timestamp === null || parsed.signatures.length === 0) {
|
|
626
|
+
throw new LangosSignatureVerificationError(
|
|
627
|
+
"Malformed signature header (expected t=...,v1=...)"
|
|
628
|
+
);
|
|
629
|
+
}
|
|
630
|
+
const nowSec = Math.floor(Date.now() / 1e3);
|
|
631
|
+
if (Math.abs(nowSec - parsed.timestamp) > tolerance) {
|
|
632
|
+
throw new LangosSignatureVerificationError(
|
|
633
|
+
`Timestamp outside the tolerance window (${tolerance}s)`
|
|
634
|
+
);
|
|
635
|
+
}
|
|
636
|
+
const rawBody = typeof payload === "string" ? payload : payload.toString("utf-8");
|
|
637
|
+
const expected = computeSignature(secret, parsed.timestamp, rawBody);
|
|
638
|
+
const ok = parsed.signatures.some((s) => safeEquals(s, expected));
|
|
639
|
+
if (!ok) {
|
|
640
|
+
throw new LangosSignatureVerificationError("No signatures matched");
|
|
641
|
+
}
|
|
642
|
+
let event;
|
|
643
|
+
try {
|
|
644
|
+
event = JSON.parse(rawBody);
|
|
645
|
+
} catch {
|
|
646
|
+
throw new LangosSignatureVerificationError("Payload is not valid JSON");
|
|
647
|
+
}
|
|
648
|
+
return event;
|
|
649
|
+
}
|
|
650
|
+
};
|
|
651
|
+
function parseSignatureHeader(header) {
|
|
652
|
+
let timestamp = null;
|
|
653
|
+
const signatures = [];
|
|
654
|
+
for (const part of header.split(",")) {
|
|
655
|
+
const [k, v] = part.trim().split("=");
|
|
656
|
+
if (k === "t" && v) {
|
|
657
|
+
const n = parseInt(v, 10);
|
|
658
|
+
if (Number.isFinite(n) && n > 0 && n < MAX_TIMESTAMP_SECONDS) {
|
|
659
|
+
timestamp = n;
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
if (k === "v1" && v) signatures.push(v);
|
|
663
|
+
}
|
|
664
|
+
return { timestamp, signatures };
|
|
665
|
+
}
|
|
666
|
+
function computeSignature(secret, timestamp, body) {
|
|
667
|
+
return createHmac("sha256", secret).update(`${timestamp}.${body}`).digest("hex");
|
|
668
|
+
}
|
|
669
|
+
function safeEquals(a, b) {
|
|
670
|
+
const ab = Buffer.from(a, "hex");
|
|
671
|
+
const bb = Buffer.from(b, "hex");
|
|
672
|
+
if (ab.length !== bb.length || ab.length === 0) return false;
|
|
673
|
+
try {
|
|
674
|
+
return timingSafeEqual(ab, bb);
|
|
675
|
+
} catch {
|
|
676
|
+
return false;
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
// src/core/logger.ts
|
|
681
|
+
var noopLogger = {
|
|
682
|
+
debug() {
|
|
683
|
+
},
|
|
684
|
+
info() {
|
|
685
|
+
},
|
|
686
|
+
warn() {
|
|
687
|
+
},
|
|
688
|
+
error() {
|
|
689
|
+
}
|
|
690
|
+
};
|
|
691
|
+
|
|
692
|
+
// src/client.ts
|
|
693
|
+
var DEFAULT_BASE_URL = "https://app.langos.io/api/v1";
|
|
694
|
+
var DEFAULT_TIMEOUT_MS = 3e4;
|
|
695
|
+
var DEFAULT_MAX_RETRIES = 2;
|
|
696
|
+
var Langos = class {
|
|
697
|
+
account;
|
|
698
|
+
assessments;
|
|
699
|
+
candidates;
|
|
700
|
+
sessions;
|
|
701
|
+
/**
|
|
702
|
+
* Webhook signature helpers (`Langos.webhooks.constructEvent`). Static-style:
|
|
703
|
+
* does not need the client instance, since signing is independent of API
|
|
704
|
+
* calls. Exposed both as a class member and as `Langos.webhooks` for parity
|
|
705
|
+
* with Stripe's `stripe.webhooks` style.
|
|
706
|
+
*/
|
|
707
|
+
webhooks = Webhooks;
|
|
708
|
+
static webhooks = Webhooks;
|
|
709
|
+
cfg;
|
|
710
|
+
constructor(options) {
|
|
711
|
+
if (!options.apiKey) {
|
|
712
|
+
throw new Error(
|
|
713
|
+
"Langos: missing apiKey. Pass via constructor or set LANGOS_API_KEY environment variable and read it."
|
|
714
|
+
);
|
|
715
|
+
}
|
|
716
|
+
const fetchImpl = options.fetch ?? globalThis.fetch;
|
|
717
|
+
if (!fetchImpl) {
|
|
718
|
+
throw new Error(
|
|
719
|
+
"Langos: no fetch implementation found. Use Node 18+ or pass `fetch` via the options."
|
|
720
|
+
);
|
|
721
|
+
}
|
|
722
|
+
if (options.appName !== void 0) {
|
|
723
|
+
assertHeaderSafe("appName", options.appName);
|
|
724
|
+
}
|
|
725
|
+
if (options.apiKey !== void 0) {
|
|
726
|
+
assertHeaderSafe("apiKey", options.apiKey);
|
|
727
|
+
}
|
|
728
|
+
this.cfg = {
|
|
729
|
+
apiKey: options.apiKey,
|
|
730
|
+
baseUrl: (options.baseUrl ?? DEFAULT_BASE_URL).replace(/\/$/, ""),
|
|
731
|
+
timeout: options.timeout ?? DEFAULT_TIMEOUT_MS,
|
|
732
|
+
maxRetries: options.maxRetries ?? DEFAULT_MAX_RETRIES,
|
|
733
|
+
fetchImpl,
|
|
734
|
+
logger: options.logger ?? noopLogger,
|
|
735
|
+
appName: options.appName,
|
|
736
|
+
telemetry: options.telemetry !== false
|
|
737
|
+
};
|
|
738
|
+
this.account = new AccountResource(this.cfg);
|
|
739
|
+
this.assessments = new AssessmentsResource(this.cfg);
|
|
740
|
+
this.candidates = new CandidatesResource(this.cfg);
|
|
741
|
+
this.sessions = new SessionsResource(this.cfg);
|
|
742
|
+
}
|
|
743
|
+
};
|
|
744
|
+
var HEADER_UNSAFE = /[\r\n\0\x00-\x1f\x7f]/;
|
|
745
|
+
function assertHeaderSafe(field, value) {
|
|
746
|
+
if (typeof value !== "string") {
|
|
747
|
+
throw new TypeError(`Langos: ${field} must be a string`);
|
|
748
|
+
}
|
|
749
|
+
if (HEADER_UNSAFE.test(value)) {
|
|
750
|
+
throw new TypeError(
|
|
751
|
+
`Langos: ${field} contains control characters (\\r, \\n, \\0, or other C0). These are not allowed in HTTP headers and would enable header injection.`
|
|
752
|
+
);
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
export { Langos, LangosAPIError, LangosAuthenticationError, LangosBadRequestError, LangosConflictError, LangosConnectionError, LangosError, LangosForbiddenError, LangosNotFoundError, LangosRateLimitError, LangosServerError, LangosSignatureVerificationError, LangosTimeoutError, Webhooks, version };
|
|
757
|
+
//# sourceMappingURL=index.mjs.map
|
|
758
|
+
//# sourceMappingURL=index.mjs.map
|