n8n-nodes-lenz 0.0.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.
@@ -0,0 +1,1099 @@
1
+ "use strict";
2
+ var __getOwnPropNames = Object.getOwnPropertyNames;
3
+ var __commonJS = (cb, mod) => function __require() {
4
+ try {
5
+ return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
6
+ } catch (e) {
7
+ throw mod = 0, e;
8
+ }
9
+ };
10
+
11
+ // node_modules/lenz-io/dist/index.cjs
12
+ var require_dist = __commonJS({
13
+ "node_modules/lenz-io/dist/index.cjs"(exports2, module2) {
14
+ "use strict";
15
+ var __defProp = Object.defineProperty;
16
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
17
+ var __getOwnPropNames2 = Object.getOwnPropertyNames;
18
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
19
+ var __export = (target, all) => {
20
+ for (var name in all)
21
+ __defProp(target, name, { get: all[name], enumerable: true });
22
+ };
23
+ var __copyProps = (to, from, except, desc) => {
24
+ if (from && typeof from === "object" || typeof from === "function") {
25
+ for (let key of __getOwnPropNames2(from))
26
+ if (!__hasOwnProp.call(to, key) && key !== except)
27
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
28
+ }
29
+ return to;
30
+ };
31
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
32
+ var index_exports = {};
33
+ __export(index_exports, {
34
+ API_VERSION: () => API_VERSION,
35
+ DEFAULT_BASE_URL: () => DEFAULT_BASE_URL,
36
+ DEFAULT_REPLAY_WINDOW_SECONDS: () => DEFAULT_REPLAY_WINDOW_SECONDS,
37
+ Lenz: () => Lenz2,
38
+ LenzAPIError: () => LenzAPIError,
39
+ LenzAuthError: () => LenzAuthError,
40
+ LenzError: () => LenzError,
41
+ LenzNeedsInputError: () => LenzNeedsInputError,
42
+ LenzPipelineError: () => LenzPipelineError,
43
+ LenzQuotaExceededError: () => LenzQuotaExceededError,
44
+ LenzRateLimitError: () => LenzRateLimitError,
45
+ LenzTimeoutError: () => LenzTimeoutError,
46
+ LenzValidationError: () => LenzValidationError,
47
+ LenzWebhookSignatureError: () => LenzWebhookSignatureError,
48
+ LenzWebhooks: () => LenzWebhooks,
49
+ SIGNATURE_HEADER: () => SIGNATURE_HEADER,
50
+ VERSION: () => VERSION,
51
+ mapResponseToError: () => mapResponseToError,
52
+ verifySignature: () => verifySignature
53
+ });
54
+ module2.exports = __toCommonJS(index_exports);
55
+ var import_node_crypto = require("crypto");
56
+ var LenzError = class extends Error {
57
+ cause_;
58
+ fix;
59
+ docUrl;
60
+ requestId;
61
+ statusCode;
62
+ body;
63
+ constructor(ctx = {}) {
64
+ super(ctx.message || new.target.name);
65
+ this.name = new.target.name;
66
+ this.cause_ = ctx.cause ?? "";
67
+ this.fix = ctx.fix ?? "";
68
+ this.docUrl = ctx.docUrl ?? "";
69
+ this.requestId = ctx.requestId ?? "";
70
+ this.statusCode = ctx.statusCode ?? 0;
71
+ this.body = ctx.body ?? null;
72
+ }
73
+ toString() {
74
+ const lines = [this.message || this.name];
75
+ if (this.cause_) lines.push(` Cause: ${this.cause_}`);
76
+ if (this.fix) lines.push(` Fix: ${this.fix}`);
77
+ if (this.docUrl) lines.push(` Docs: ${this.docUrl}`);
78
+ if (this.requestId) lines.push(` Request ID: ${this.requestId}`);
79
+ return lines.join("\n");
80
+ }
81
+ };
82
+ var LenzAuthError = class extends LenzError {
83
+ };
84
+ var LenzQuotaExceededError = class extends LenzError {
85
+ creditsRemaining = 0;
86
+ };
87
+ var LenzValidationError = class extends LenzError {
88
+ errors = [];
89
+ };
90
+ var LenzRateLimitError = class extends LenzError {
91
+ retryAfter = 0;
92
+ };
93
+ var LenzAPIError = class extends LenzError {
94
+ };
95
+ var LenzTimeoutError = class extends LenzError {
96
+ taskId = "";
97
+ };
98
+ var LenzNeedsInputError = class extends LenzError {
99
+ taskId = "";
100
+ kind = "";
101
+ payload = {};
102
+ };
103
+ var LenzPipelineError = class extends LenzError {
104
+ taskId = "";
105
+ failureReason = "";
106
+ };
107
+ var LenzWebhookSignatureError = class extends LenzError {
108
+ };
109
+ var DOCS_BASE = "https://lenz.io/docs";
110
+ var STATUS_MAP = {
111
+ 401: { cls: LenzAuthError, message: "Unauthorized", docUrl: `${DOCS_BASE}/auth` },
112
+ 403: { cls: LenzAuthError, message: "Forbidden", docUrl: `${DOCS_BASE}/auth` },
113
+ 402: {
114
+ cls: LenzQuotaExceededError,
115
+ message: "Payment required",
116
+ docUrl: `${DOCS_BASE}/billing`
117
+ },
118
+ 422: {
119
+ cls: LenzValidationError,
120
+ message: "Validation failed",
121
+ docUrl: `${DOCS_BASE}/errors/validation`
122
+ },
123
+ 429: {
124
+ cls: LenzRateLimitError,
125
+ message: "Rate limit exceeded",
126
+ docUrl: `${DOCS_BASE}/rate-limits`
127
+ }
128
+ };
129
+ var FIX_HINTS = {
130
+ 401: "Generate a new key at https://lenz.io/api-integration.",
131
+ 403: "This key doesn't have access to that resource.",
132
+ 402: "Upgrade your plan or wait for the period reset.",
133
+ 422: "Check the request body against the OpenAPI spec.",
134
+ 429: "Wait Retry-After seconds and retry."
135
+ };
136
+ function parseBody(raw) {
137
+ if (!raw) return {};
138
+ try {
139
+ const parsed = JSON.parse(raw);
140
+ return parsed && typeof parsed === "object" ? parsed : {};
141
+ } catch {
142
+ return {};
143
+ }
144
+ }
145
+ function getHeader(headers, name) {
146
+ return headers[name] ?? headers[name.toLowerCase()] ?? headers[name.toUpperCase()] ?? "";
147
+ }
148
+ function mapResponseToError(statusCode, body, headers = {}) {
149
+ const parsed = parseBody(body);
150
+ const requestId = getHeader(headers, "X-Request-ID");
151
+ let entry;
152
+ if (statusCode in STATUS_MAP) {
153
+ entry = STATUS_MAP[statusCode];
154
+ } else if (statusCode >= 500 && statusCode < 600) {
155
+ entry = { cls: LenzAPIError, message: "Server error", docUrl: `${DOCS_BASE}/errors` };
156
+ } else {
157
+ entry = { cls: LenzError, message: `HTTP ${statusCode}`, docUrl: `${DOCS_BASE}/errors` };
158
+ }
159
+ const detailRaw = parsed["detail"];
160
+ const detail = typeof detailRaw === "string" ? detailRaw : Array.isArray(detailRaw) ? "Validation failed" : entry.message;
161
+ const err = new entry.cls({
162
+ message: detail,
163
+ cause: detail,
164
+ fix: FIX_HINTS[statusCode] ?? "Retry; if the error persists, file an issue with the Request ID.",
165
+ docUrl: entry.docUrl,
166
+ requestId,
167
+ statusCode,
168
+ body: parsed
169
+ });
170
+ if (err instanceof LenzQuotaExceededError) {
171
+ err.creditsRemaining = Number(parsed["credits_remaining"] ?? 0);
172
+ } else if (err instanceof LenzValidationError) {
173
+ if (Array.isArray(parsed["detail"])) {
174
+ err.errors = parsed["detail"];
175
+ } else if (Array.isArray(parsed["errors"])) {
176
+ err.errors = parsed["errors"];
177
+ }
178
+ } else if (err instanceof LenzRateLimitError) {
179
+ const ra = getHeader(headers, "Retry-After") || String(parsed["retry_after"] ?? "");
180
+ const parsedRa = Number(ra);
181
+ err.retryAfter = Number.isFinite(parsedRa) ? parsedRa : 0;
182
+ }
183
+ return err;
184
+ }
185
+ var VERSION = "2.3.0";
186
+ var API_VERSION = "2026-05-13";
187
+ var DEFAULT_BASE_URL = "https://lenz.io/api/v1";
188
+ var DEFAULT_TIMEOUT_MS = 3e4;
189
+ var DEFAULT_MAX_RETRIES = 3;
190
+ var RETRY_BACKOFF_MS = [1e3, 2e3, 4e3];
191
+ var POLL_BACKOFF_MS = [2e3, 4e3, 8e3];
192
+ var POLL_BACKOFF_CAP_MS = 1e4;
193
+ function sleep(ms) {
194
+ return new Promise((res) => setTimeout(res, ms));
195
+ }
196
+ function retrySleepMs(attempt) {
197
+ return RETRY_BACKOFF_MS[Math.min(attempt, RETRY_BACKOFF_MS.length - 1)] ?? 4e3;
198
+ }
199
+ function pollSleepMs(idx, remainingMs) {
200
+ const base = POLL_BACKOFF_MS[Math.min(idx, POLL_BACKOFF_MS.length - 1)] ?? POLL_BACKOFF_CAP_MS;
201
+ return Math.min(base, POLL_BACKOFF_CAP_MS, Math.max(0, remainingMs));
202
+ }
203
+ var VerificationsNamespace = class {
204
+ constructor(client) {
205
+ this.client = client;
206
+ }
207
+ client;
208
+ list({ page = 1 } = {}) {
209
+ return this.client.request({
210
+ method: "GET",
211
+ path: "/verifications",
212
+ query: { page }
213
+ });
214
+ }
215
+ /**
216
+ * Fetch a single verification. Accepts anon callers — any non-hidden
217
+ * public claim resolves without an API key (the old `library.get`
218
+ * endpoint merged into this one).
219
+ */
220
+ get(verificationId) {
221
+ return this.client.request({
222
+ method: "GET",
223
+ path: `/verifications/${verificationId}`,
224
+ authRequired: false,
225
+ authOptional: true
226
+ // send the key if we have one → owner sees private rows
227
+ });
228
+ }
229
+ async delete(verificationId) {
230
+ try {
231
+ await this.client.request({
232
+ method: "DELETE",
233
+ path: `/verifications/${verificationId}`
234
+ });
235
+ return true;
236
+ } catch (exc) {
237
+ if (exc instanceof LenzError && exc.statusCode === 404) return true;
238
+ throw exc;
239
+ }
240
+ }
241
+ /**
242
+ * Public verifications semantically related to this one (pgvector ANN).
243
+ * Server clamps `limit` to 10. Excludes the verification itself and
244
+ * editorially-hidden claims. Accessible for any verification the caller
245
+ * owns or any public library item.
246
+ */
247
+ related(verificationId, { limit = 5 } = {}) {
248
+ return this.client.request({
249
+ method: "GET",
250
+ path: `/verifications/${verificationId}/related`,
251
+ query: { limit }
252
+ });
253
+ }
254
+ };
255
+ var AskNamespace = class {
256
+ constructor(client) {
257
+ this.client = client;
258
+ }
259
+ client;
260
+ history(verificationId) {
261
+ return this.client.request({
262
+ method: "GET",
263
+ path: `/ask/${verificationId}`
264
+ });
265
+ }
266
+ send(verificationId, input) {
267
+ const body = { message: input.message };
268
+ if (input.language) body.language = input.language;
269
+ return this.client.request({
270
+ method: "POST",
271
+ path: `/ask/${verificationId}`,
272
+ json: body
273
+ });
274
+ }
275
+ async reset(verificationId) {
276
+ await this.client.request({
277
+ method: "DELETE",
278
+ path: `/ask/${verificationId}`
279
+ });
280
+ return true;
281
+ }
282
+ };
283
+ var LibraryNamespace = class {
284
+ constructor(client) {
285
+ this.client = client;
286
+ }
287
+ client;
288
+ list(input = {}) {
289
+ return this.client.request({
290
+ method: "GET",
291
+ path: "/library",
292
+ query: {
293
+ page: input.page ?? 1,
294
+ sort: input.sort ?? "recent",
295
+ search: input.search,
296
+ domain: input.domain,
297
+ entity: input.entity
298
+ },
299
+ authRequired: false
300
+ });
301
+ }
302
+ };
303
+ var Lenz2 = class {
304
+ apiKey;
305
+ baseUrl;
306
+ timeoutMs;
307
+ maxRetries;
308
+ fetchImpl;
309
+ verifications;
310
+ ask;
311
+ library;
312
+ constructor(opts = {}) {
313
+ this.apiKey = opts.apiKey ?? process.env["LENZ_API_KEY"] ?? "";
314
+ this.baseUrl = (opts.baseUrl ?? process.env["LENZ_BASE_URL"] ?? DEFAULT_BASE_URL).replace(
315
+ /\/$/,
316
+ ""
317
+ );
318
+ this.timeoutMs = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS;
319
+ this.maxRetries = opts.maxRetries ?? DEFAULT_MAX_RETRIES;
320
+ this.fetchImpl = opts.fetch ?? globalThis.fetch.bind(globalThis);
321
+ this.verifications = new VerificationsNamespace(this);
322
+ this.ask = new AskNamespace(this);
323
+ this.library = new LibraryNamespace(this);
324
+ }
325
+ // ── Marquee verbs ──
326
+ async verify(input) {
327
+ return this.submit(input);
328
+ }
329
+ async verifyBatch(input) {
330
+ const body = {
331
+ // Per-item shape passes through verbatim — `VerifyBatchItem` allows
332
+ // any subset including a per-item `language` override.
333
+ claims: input.claims.map((c) => {
334
+ const item = {
335
+ text: c.text,
336
+ source_url: c.source_url ?? "",
337
+ webhook_url: c.webhook_url ?? ""
338
+ };
339
+ if (c.language) item.language = c.language;
340
+ return item;
341
+ })
342
+ };
343
+ if (input.webhookUrl) body["webhook_url"] = input.webhookUrl;
344
+ if (input.language) body["language"] = input.language;
345
+ const headers = {};
346
+ if (input.idempotencyKey) headers["Idempotency-Key"] = input.idempotencyKey;
347
+ return this.request({
348
+ method: "POST",
349
+ path: "/verify/batch",
350
+ json: body,
351
+ headers
352
+ });
353
+ }
354
+ async extract(input) {
355
+ const body = { text: input.text };
356
+ if (input.language) body.language = input.language;
357
+ return this.request({
358
+ method: "POST",
359
+ path: "/extract",
360
+ json: body
361
+ });
362
+ }
363
+ /**
364
+ * Fast 3-model panel verdict on each identified claim in the input.
365
+ * Sync, ~5-10s. Returns one entry per atomic_claim. For deeper analysis
366
+ * (citations, full audit trail), escalate low-confidence claims to
367
+ * `verifyAndWait`; the two endpoints share a result cache server-side.
368
+ *
369
+ * Pass `language: "es"` (or any of the 12 supported codes) to receive
370
+ * the claim text in that language. Verdict labels stay English.
371
+ */
372
+ async assess(input) {
373
+ const body = { text: input.text };
374
+ if (input.language) body.language = input.language;
375
+ return this.request({
376
+ method: "POST",
377
+ path: "/assess",
378
+ json: body
379
+ });
380
+ }
381
+ /**
382
+ * Resolve a needs-input interrupt by selecting one or more claims.
383
+ *
384
+ * Each selected claim fans out into its own pipeline; the returned
385
+ * `BatchAccepted` carries one `items` entry (each with its own `task_id`)
386
+ * per claim. Poll each via `getStatus` / `wait`. Every text must match a
387
+ * claim offered in the prior interrupt — the server rejects anything else.
388
+ */
389
+ async select(taskId, input) {
390
+ if (!input.texts || input.texts.length === 0) {
391
+ throw new Error("select requires a non-empty texts array");
392
+ }
393
+ return this.request({
394
+ method: "POST",
395
+ path: `/verify/${taskId}/select`,
396
+ json: { texts: input.texts }
397
+ });
398
+ }
399
+ async getStatus(taskId) {
400
+ return this.request({
401
+ method: "GET",
402
+ path: `/verify/status/${taskId}`
403
+ });
404
+ }
405
+ async usage() {
406
+ return this.request({ method: "GET", path: "/me/usage" });
407
+ }
408
+ // ── Headline ergonomic ──
409
+ /**
410
+ * Submit + poll until the pipeline terminates. Returns the completed
411
+ * Verification, or throws LenzNeedsInputError / LenzPipelineError /
412
+ * LenzTimeoutError. By default sends an auto-generated Idempotency-Key
413
+ * so a network retry on submit doesn't spawn a duplicate task.
414
+ */
415
+ async verifyAndWait(input) {
416
+ const timeoutMs = input.timeoutMs ?? 12e4;
417
+ const idempotencyKey = input.idempotencyKey ?? (input.idempotency !== false ? (0, import_node_crypto.randomUUID)().replace(/-/g, "") : void 0);
418
+ const accepted = await this.submit({ ...input, idempotencyKey });
419
+ console.info(`[lenz-io] Submitted task: ${accepted.task_id}`);
420
+ return this.wait(accepted, { timeoutMs });
421
+ }
422
+ /**
423
+ * Block on an already-submitted task until it terminates, then return its
424
+ * `Verification`. `task` is a `task_id` string OR the `TaskAccepted` returned
425
+ * by `verify` / `select` — so `client.wait(await client.verify({claim}))`
426
+ * reads naturally. Throws for an empty id, `LenzNeedsInputError` /
427
+ * `LenzPipelineError` on terminal non-success, and `LenzTimeoutError` on
428
+ * deadline.
429
+ */
430
+ async wait(task, opts = {}) {
431
+ const taskId = typeof task === "string" ? task : task.task_id;
432
+ if (!taskId) {
433
+ throw new Error("wait() requires a non-empty task_id (got an empty TaskAccepted.task_id).");
434
+ }
435
+ const timeoutMs = opts.timeoutMs ?? 12e4;
436
+ const { terminal, timedOut } = await this._pollToTerminal([taskId], timeoutMs);
437
+ if (timedOut.has(taskId)) {
438
+ const err = new LenzTimeoutError({
439
+ message: `wait timed out after ${timeoutMs}ms`,
440
+ cause: "Pipeline still running server-side.",
441
+ fix: `Resume via client.getStatus('${taskId}') later.`,
442
+ docUrl: "https://lenz.io/docs/verify#timeout"
443
+ });
444
+ err.taskId = taskId;
445
+ throw err;
446
+ }
447
+ return this._verificationFromTerminal(terminal.get(taskId), taskId);
448
+ }
449
+ /**
450
+ * Submit a batch and poll every item to a terminal state. Returns one
451
+ * `BatchItemResult` per task the batch accepted, in input order. Never throws
452
+ * on a per-item outcome — a claim that fails, pauses, or times out becomes a
453
+ * `BatchItemResult` with the matching `status`. (Transport/auth errors on the
454
+ * initial submit still throw.)
455
+ */
456
+ async verifyBatchAndWait(input) {
457
+ const timeoutMs = input.timeoutMs ?? 18e4;
458
+ const accepted = await this.verifyBatch(input);
459
+ const ids = accepted.items.map((it) => it.task_id).filter((id) => Boolean(id));
460
+ const { terminal, timedOut } = await this._pollToTerminal(ids, timeoutMs);
461
+ return accepted.items.map((it) => {
462
+ const status = terminal.get(it.task_id);
463
+ if (!it.task_id || timedOut.has(it.task_id) || !status) {
464
+ return { task_id: it.task_id, claim_text: it.claim_text, status: "timeout" };
465
+ }
466
+ if (status.status === "completed" && status.result) {
467
+ return {
468
+ task_id: it.task_id,
469
+ claim_text: it.claim_text,
470
+ status: "completed",
471
+ verification: status.result,
472
+ status_detail: status
473
+ };
474
+ }
475
+ if (status.status === "needs_input") {
476
+ return {
477
+ task_id: it.task_id,
478
+ claim_text: it.claim_text,
479
+ status: "needs_input",
480
+ status_detail: status
481
+ };
482
+ }
483
+ return {
484
+ task_id: it.task_id,
485
+ claim_text: it.claim_text,
486
+ status: "failed",
487
+ status_detail: status
488
+ };
489
+ });
490
+ }
491
+ // ── poll engine (shared by wait + verifyBatchAndWait) ──
492
+ /**
493
+ * Round-robin poll `taskIds` until each reaches a terminal state or the
494
+ * deadline elapses. Returns `{terminal, timedOut}`; a timed-out task has no
495
+ * `TaskStatus` (`"timeout"` is client-side, never a wire status).
496
+ *
497
+ * Each round polls every still-pending id once (via `Promise.allSettled`, so
498
+ * one poll's transport failure doesn't abort the batch — that id stays
499
+ * pending and retries next round) BEFORE the deadline check, preserving the
500
+ * legacy `verifyAndWait` behavior of polling once more after sleeping the
501
+ * remaining time. Timeout is therefore approximate. Backoff reuses the
502
+ * existing 2/4/8/8…ms sequence; the 10s cap is currently unreachable and kept
503
+ * only to preserve identical timing.
504
+ */
505
+ async _pollToTerminal(taskIds, timeoutMs) {
506
+ let pending = [...taskIds];
507
+ const terminal = /* @__PURE__ */ new Map();
508
+ const timedOut = /* @__PURE__ */ new Set();
509
+ const deadline = Date.now() + timeoutMs;
510
+ let backoffIdx = 0;
511
+ while (pending.length > 0) {
512
+ const settled = await Promise.allSettled(pending.map((id) => this.getStatus(id)));
513
+ const stillPending = [];
514
+ settled.forEach((res, i) => {
515
+ const id = pending[i];
516
+ if (res.status === "fulfilled") {
517
+ const s = res.value;
518
+ if (s.status === "completed" || s.status === "needs_input" || s.status === "failed") {
519
+ terminal.set(id, s);
520
+ } else {
521
+ stillPending.push(id);
522
+ }
523
+ } else {
524
+ stillPending.push(id);
525
+ }
526
+ });
527
+ pending = stillPending;
528
+ if (pending.length === 0) break;
529
+ const remaining = deadline - Date.now();
530
+ if (remaining <= 0) {
531
+ pending.forEach((id) => timedOut.add(id));
532
+ break;
533
+ }
534
+ await sleep(pollSleepMs(backoffIdx, remaining));
535
+ backoffIdx += 1;
536
+ }
537
+ return { terminal, timedOut };
538
+ }
539
+ /**
540
+ * Map a terminal `TaskStatus` to a `Verification` or throw the matching typed
541
+ * error. Shared by `wait` (and thus `verifyAndWait`).
542
+ */
543
+ _verificationFromTerminal(status, taskId) {
544
+ if (status.status === "completed") {
545
+ if (!status.result) {
546
+ throw new LenzPipelineError({
547
+ message: "Pipeline completed but the result is empty.",
548
+ cause: "Server reported status=completed without a result block.",
549
+ fix: "File an issue at https://github.com/lenzhq/lenz-io-node/issues with the Request ID.",
550
+ docUrl: "https://lenz.io/docs/errors"
551
+ });
552
+ }
553
+ return status.result;
554
+ }
555
+ if (status.status === "needs_input") {
556
+ const err2 = new LenzNeedsInputError({
557
+ message: `Pipeline paused: ${status.reason ?? "needs input"}`,
558
+ cause: "The verification needs caller input to proceed.",
559
+ fix: "Inspect the payload, then call client.select(taskId, { texts: [...] }) with the chosen claim(s).",
560
+ docUrl: "https://lenz.io/docs/verify#needs-input"
561
+ });
562
+ err2.taskId = taskId;
563
+ err2.kind = status.reason ?? "";
564
+ err2.payload = status;
565
+ throw err2;
566
+ }
567
+ const detail = status.error || status.failure_detail || status.failure_reason || "unknown";
568
+ const err = new LenzPipelineError({
569
+ message: `Pipeline failed: ${detail}`,
570
+ cause: detail,
571
+ fix: "Retry with a different claim, or check status.error for the diagnostic.",
572
+ docUrl: "https://lenz.io/docs/errors"
573
+ });
574
+ err.taskId = taskId;
575
+ err.failureReason = status.failure_reason ?? "";
576
+ throw err;
577
+ }
578
+ // ── internal helpers ──
579
+ async submit(input) {
580
+ const body = {
581
+ text: input.claim,
582
+ source_url: input.sourceUrl ?? "",
583
+ webhook_url: input.webhookUrl ?? ""
584
+ };
585
+ if (input.language) body.language = input.language;
586
+ const headers = {};
587
+ if (input.idempotencyKey) headers["Idempotency-Key"] = input.idempotencyKey;
588
+ return this.request({
589
+ method: "POST",
590
+ path: "/verify",
591
+ json: body,
592
+ headers
593
+ });
594
+ }
595
+ /** Internal: dispatch an HTTP call with auth + retry. Public so the
596
+ * namespace classes can use it; not part of the documented surface. */
597
+ async request(opts) {
598
+ const authRequired = opts.authRequired !== false;
599
+ if (authRequired && !this.apiKey) {
600
+ throw new LenzAuthError({
601
+ message: "API key required",
602
+ cause: "This method requires authentication; no API key was provided.",
603
+ fix: "Pass apiKey to new Lenz(), set LENZ_API_KEY env var, or get one at https://lenz.io/api-integration. Library endpoints work without a key.",
604
+ docUrl: "https://lenz.io/docs/auth"
605
+ });
606
+ }
607
+ const url = new URL(`${this.baseUrl}${opts.path}`);
608
+ if (opts.query) {
609
+ for (const [k, v] of Object.entries(opts.query)) {
610
+ if (v !== void 0 && v !== "" && v !== null) {
611
+ url.searchParams.set(k, String(v));
612
+ }
613
+ }
614
+ }
615
+ const headers = {
616
+ "User-Agent": `lenz-io-node/${VERSION}`,
617
+ "X-Lenz-API-Version": API_VERSION,
618
+ Accept: "application/json",
619
+ ...opts.headers ?? {}
620
+ };
621
+ if (this.apiKey && (authRequired || opts.authOptional)) {
622
+ headers["Authorization"] = `Bearer ${this.apiKey}`;
623
+ }
624
+ if (opts.json !== void 0) {
625
+ headers["Content-Type"] = "application/json";
626
+ }
627
+ let lastErr = void 0;
628
+ for (let attempt = 0; attempt <= this.maxRetries; attempt++) {
629
+ const controller = new AbortController();
630
+ const timer = setTimeout(() => controller.abort(), this.timeoutMs);
631
+ let response;
632
+ try {
633
+ response = await this.fetchImpl(url.toString(), {
634
+ method: opts.method,
635
+ headers,
636
+ body: opts.json !== void 0 ? JSON.stringify(opts.json) : void 0,
637
+ signal: controller.signal
638
+ });
639
+ } catch (exc) {
640
+ lastErr = exc;
641
+ clearTimeout(timer);
642
+ if (attempt >= this.maxRetries) {
643
+ throw new LenzAPIError({
644
+ message: `${opts.method} ${opts.path} failed after ${attempt + 1} attempts: ${String(exc)}`,
645
+ cause: String(exc),
646
+ fix: "Check your network connection; verify baseUrl is reachable.",
647
+ docUrl: "https://lenz.io/docs/errors"
648
+ });
649
+ }
650
+ await sleep(retrySleepMs(attempt));
651
+ continue;
652
+ }
653
+ clearTimeout(timer);
654
+ if (response.status < 400) {
655
+ if (response.status === 204 || response.headers.get("content-length") === "0") {
656
+ return {};
657
+ }
658
+ return await response.json();
659
+ }
660
+ if (attempt < this.maxRetries && (response.status >= 500 || response.status === 429)) {
661
+ const ra = response.headers.get("Retry-After");
662
+ let waitMs = retrySleepMs(attempt);
663
+ if (ra) {
664
+ const parsed = Number(ra);
665
+ if (Number.isFinite(parsed)) waitMs = parsed * 1e3;
666
+ }
667
+ await sleep(waitMs);
668
+ continue;
669
+ }
670
+ const rawBody = await response.text();
671
+ const respHeaders = {};
672
+ response.headers.forEach((v, k) => {
673
+ respHeaders[k] = v;
674
+ });
675
+ throw mapResponseToError(response.status, rawBody, respHeaders);
676
+ }
677
+ if (lastErr) {
678
+ throw new LenzAPIError({
679
+ message: String(lastErr),
680
+ cause: String(lastErr)
681
+ });
682
+ }
683
+ throw new LenzAPIError({ message: `${opts.method} ${opts.path} failed without diagnostic` });
684
+ }
685
+ };
686
+ var import_node_crypto2 = require("crypto");
687
+ var import_node_buffer = require("buffer");
688
+ var SIGNATURE_HEADER = "X-Lenz-Signature";
689
+ var SIGNATURE_PREFIX = "sha256=";
690
+ var DEFAULT_REPLAY_WINDOW_SECONDS = 300;
691
+ function toBuffer(body) {
692
+ if (import_node_buffer.Buffer.isBuffer(body)) return body;
693
+ if (body instanceof Uint8Array) return import_node_buffer.Buffer.from(body);
694
+ return import_node_buffer.Buffer.from(body, "utf-8");
695
+ }
696
+ function sign(body, secret) {
697
+ const mac = (0, import_node_crypto2.createHmac)("sha256", secret).update(body).digest("hex");
698
+ return `${SIGNATURE_PREFIX}${mac}`;
699
+ }
700
+ function verifySignature(rawBody, signature, secret) {
701
+ if (!signature) {
702
+ throw new LenzWebhookSignatureError({
703
+ message: "Missing webhook signature",
704
+ cause: `No ${SIGNATURE_HEADER} header on the request.`,
705
+ fix: "Inspect the webhook delivery in /api-integration to confirm the secret is set.",
706
+ docUrl: "https://lenz.io/docs/webhooks"
707
+ });
708
+ }
709
+ const buf = toBuffer(rawBody);
710
+ const expected = sign(buf, secret);
711
+ const a = import_node_buffer.Buffer.from(expected);
712
+ const b = import_node_buffer.Buffer.from(signature);
713
+ if (a.length !== b.length || !(0, import_node_crypto2.timingSafeEqual)(a, b)) {
714
+ throw new LenzWebhookSignatureError({
715
+ message: "Webhook signature mismatch",
716
+ cause: "HMAC of the raw body using your secret does not match X-Lenz-Signature.",
717
+ fix: "Verify the secret in /api-integration matches the one you configured here.",
718
+ docUrl: "https://lenz.io/docs/webhooks"
719
+ });
720
+ }
721
+ return true;
722
+ }
723
+ function buildEvent(payload) {
724
+ const event = String(payload["event"] ?? "");
725
+ const base = {
726
+ event,
727
+ taskId: String(payload["task_id"] ?? ""),
728
+ attempt: Number(payload["attempt"] ?? 1) || 1,
729
+ deliveredAt: String(payload["delivered_at"] ?? ""),
730
+ verificationId: payload["verification_id"] ?? null,
731
+ batchId: payload["batch_id"] ?? null,
732
+ status: String(payload["status"] ?? ""),
733
+ raw: payload
734
+ };
735
+ if (event === "verification.completed") {
736
+ return {
737
+ ...base,
738
+ event: "verification.completed",
739
+ result: payload["result"] ?? {}
740
+ };
741
+ }
742
+ if (event === "verification.failed") {
743
+ return {
744
+ ...base,
745
+ event: "verification.failed",
746
+ error: String(payload["error"] ?? "")
747
+ };
748
+ }
749
+ if (event === "verification.needs_input") {
750
+ return {
751
+ ...base,
752
+ event: "verification.needs_input",
753
+ needsInput: payload["needs_input"] ?? {}
754
+ };
755
+ }
756
+ return base;
757
+ }
758
+ var LenzWebhooks = class {
759
+ secret;
760
+ replayWindow;
761
+ constructor(opts) {
762
+ if (!opts.secret) {
763
+ throw new Error("LenzWebhooks requires a non-empty secret. Get it from /api-integration.");
764
+ }
765
+ this.secret = opts.secret;
766
+ this.replayWindow = opts.replayWindowSeconds ?? DEFAULT_REPLAY_WINDOW_SECONDS;
767
+ }
768
+ parse(rawBody, headers) {
769
+ const sig = this.lookupHeader(headers, SIGNATURE_HEADER);
770
+ verifySignature(rawBody, sig, this.secret);
771
+ const text = toBuffer(rawBody).toString("utf-8");
772
+ let payload;
773
+ try {
774
+ payload = JSON.parse(text);
775
+ } catch (exc) {
776
+ throw new LenzWebhookSignatureError({
777
+ message: "Webhook body is not valid JSON",
778
+ cause: String(exc),
779
+ fix: "The signature verified but the body is malformed. Check your reverse proxy isn't rewriting payloads.",
780
+ docUrl: "https://lenz.io/docs/webhooks"
781
+ });
782
+ }
783
+ if (typeof payload !== "object" || payload === null || Array.isArray(payload)) {
784
+ throw new LenzWebhookSignatureError({
785
+ message: "Webhook body must be a JSON object",
786
+ cause: `Got ${typeof payload}.`,
787
+ fix: "Confirm the request comes from Lenz; an upstream proxy may be wrapping the body.",
788
+ docUrl: "https://lenz.io/docs/webhooks"
789
+ });
790
+ }
791
+ const obj = payload;
792
+ this.checkReplay(obj);
793
+ return buildEvent(obj);
794
+ }
795
+ // ── helpers ──
796
+ lookupHeader(headers, name) {
797
+ if (typeof headers.get === "function") {
798
+ const v = headers.get(name);
799
+ return v ? String(v) : "";
800
+ }
801
+ const h = headers;
802
+ return h[name] ?? h[name.toLowerCase()] ?? h[name.toUpperCase()] ?? "";
803
+ }
804
+ checkReplay(payload) {
805
+ const raw = payload["delivered_at"];
806
+ if (!raw) return;
807
+ const ts = new Date(String(raw));
808
+ if (Number.isNaN(ts.getTime())) return;
809
+ const ageSec = (Date.now() - ts.getTime()) / 1e3;
810
+ if (ageSec > this.replayWindow) {
811
+ throw new LenzWebhookSignatureError({
812
+ message: "Webhook delivered_at is outside the replay window",
813
+ cause: `Payload is ${Math.round(ageSec)}s old; window is ${this.replayWindow}s.`,
814
+ fix: "Confirm your server clock is in sync; raise replayWindowSeconds if you intentionally batch deliveries.",
815
+ docUrl: "https://lenz.io/docs/webhooks"
816
+ });
817
+ }
818
+ }
819
+ };
820
+ }
821
+ });
822
+
823
+ // dist/nodes/Lenz/Lenz.node.js
824
+ Object.defineProperty(exports, "__esModule", { value: true });
825
+ exports.Lenz = void 0;
826
+ var n8n_workflow_1 = require("n8n-workflow");
827
+ var lenz_io_1 = require_dist();
828
+ function isPassingVerdict(verdict) {
829
+ return verdict === "True" || verdict === "Mostly True";
830
+ }
831
+ var Lenz = class {
832
+ constructor() {
833
+ this.description = {
834
+ displayName: "Lenz",
835
+ name: "lenz",
836
+ icon: { light: "file:lenz.svg", dark: "file:lenz.dark.svg" },
837
+ group: ["transform"],
838
+ version: [1],
839
+ subtitle: '={{$parameter["operation"]}}',
840
+ description: "Fact-check claims and catch AI hallucinations with sourced, audit-grade verdicts",
841
+ defaults: {
842
+ name: "Lenz"
843
+ },
844
+ inputs: [n8n_workflow_1.NodeConnectionTypes.Main],
845
+ outputs: [n8n_workflow_1.NodeConnectionTypes.Main],
846
+ usableAsTool: true,
847
+ credentials: [
848
+ {
849
+ name: "lenzApi",
850
+ required: true
851
+ }
852
+ ],
853
+ properties: [
854
+ {
855
+ displayName: "Operation",
856
+ name: "operation",
857
+ type: "options",
858
+ noDataExpression: true,
859
+ options: [
860
+ {
861
+ name: "Ask Follow-Up",
862
+ value: "ask",
863
+ description: "Ask a grounded follow-up question about a completed Verify (Deep) result",
864
+ action: "Ask a follow up question"
865
+ },
866
+ {
867
+ name: "Assess (Fast)",
868
+ value: "assess",
869
+ description: "Fast 3-model panel verdict (~5-10s), one entry per claim found in the text",
870
+ action: "Quickly assess text for factual claims"
871
+ },
872
+ {
873
+ name: "Check Usage",
874
+ value: "usage",
875
+ description: "Check remaining quota for the current API key",
876
+ action: "Check usage and quota"
877
+ },
878
+ {
879
+ name: "Extract Claims",
880
+ value: "extract",
881
+ description: "Pull verifiable claims out of text (free)",
882
+ action: "Extract claims from text"
883
+ },
884
+ {
885
+ name: "Verify (Deep)",
886
+ value: "verify",
887
+ description: "Full 8-model pipeline with sourced citations (~90s). Reserve for high-stakes claims.",
888
+ action: "Deeply verify a claim"
889
+ }
890
+ ],
891
+ default: "verify"
892
+ },
893
+ {
894
+ displayName: "Claim",
895
+ name: "claim",
896
+ type: "string",
897
+ typeOptions: { rows: 3 },
898
+ default: "",
899
+ required: true,
900
+ displayOptions: {
901
+ show: { operation: ["verify"] }
902
+ },
903
+ description: "The claim to investigate in depth. Reserve for high-stakes statements that warrant a thorough, sourced check."
904
+ },
905
+ {
906
+ displayName: "Text",
907
+ name: "text",
908
+ type: "string",
909
+ typeOptions: { rows: 3 },
910
+ default: "",
911
+ required: true,
912
+ displayOptions: {
913
+ show: { operation: ["assess", "extract"] }
914
+ },
915
+ description: "The text to check. If it contains several claims, each is handled separately."
916
+ },
917
+ {
918
+ displayName: "Verification ID",
919
+ name: "verificationId",
920
+ type: "string",
921
+ default: "",
922
+ required: true,
923
+ displayOptions: {
924
+ show: { operation: ["ask"] }
925
+ },
926
+ description: "The verification_id from a completed Verify (Deep) result (not a task_id)"
927
+ },
928
+ {
929
+ displayName: "Question",
930
+ name: "question",
931
+ type: "string",
932
+ default: "",
933
+ required: true,
934
+ displayOptions: {
935
+ show: { operation: ["ask"] }
936
+ },
937
+ description: "The follow-up question, answered from the verification full research and evidence"
938
+ },
939
+ {
940
+ displayName: "Language",
941
+ name: "language",
942
+ type: "string",
943
+ default: "",
944
+ placeholder: "Es",
945
+ description: "Optional ISO 639-1 response language code. Defaults to English.",
946
+ displayOptions: {
947
+ show: { operation: ["verify", "assess", "extract", "ask"] }
948
+ }
949
+ }
950
+ ]
951
+ };
952
+ }
953
+ async execute() {
954
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j;
955
+ const items = this.getInputData();
956
+ const returnData = [];
957
+ const credentials = await this.getCredentials("lenzApi");
958
+ const client = new lenz_io_1.Lenz({ apiKey: credentials.apiKey });
959
+ for (let itemIndex = 0; itemIndex < items.length; itemIndex++) {
960
+ try {
961
+ const operation = this.getNodeParameter("operation", itemIndex);
962
+ const language = this.getNodeParameter("language", itemIndex, "") || void 0;
963
+ let responseData;
964
+ if (operation === "verify") {
965
+ const claim = this.getNodeParameter("claim", itemIndex).trim();
966
+ if (!claim) {
967
+ returnData.push({
968
+ json: { skipped: true, reason: "empty_input" },
969
+ pairedItem: { item: itemIndex }
970
+ });
971
+ continue;
972
+ }
973
+ try {
974
+ const result = await client.verifyAndWait({ claim, language });
975
+ responseData = {
976
+ status: "completed",
977
+ passed: isPassingVerdict(result.verdict),
978
+ verdict: (_a = result.verdict) !== null && _a !== void 0 ? _a : null,
979
+ confidence: (_b = result.confidence) !== null && _b !== void 0 ? _b : null,
980
+ lenz_score: (_c = result.lenz_score) !== null && _c !== void 0 ? _c : null,
981
+ executive_summary: (_d = result.executive_summary) !== null && _d !== void 0 ? _d : "",
982
+ citations: ((_e = result.sources) !== null && _e !== void 0 ? _e : []).filter((s) => s.url).map((s) => {
983
+ var _a2, _b2;
984
+ return { title: (_a2 = s.title) !== null && _a2 !== void 0 ? _a2 : "", url: (_b2 = s.url) !== null && _b2 !== void 0 ? _b2 : "" };
985
+ }),
986
+ verification_id: (_f = result.verification_id) !== null && _f !== void 0 ? _f : null
987
+ };
988
+ } catch (err) {
989
+ if (err instanceof lenz_io_1.LenzNeedsInputError) {
990
+ responseData = {
991
+ status: "needs_input",
992
+ reason: err.kind,
993
+ task_id: err.taskId,
994
+ message: "This claim is ambiguous or contains multiple sub-claims. Rephrase it to be more specific and re-run."
995
+ };
996
+ } else if (err instanceof lenz_io_1.LenzTimeoutError) {
997
+ responseData = {
998
+ status: "timeout",
999
+ task_id: err.taskId,
1000
+ message: "The verification did not complete in time (task_id: " + err.taskId + "). It may still be running server-side."
1001
+ };
1002
+ } else if (err instanceof lenz_io_1.LenzPipelineError) {
1003
+ responseData = {
1004
+ status: "failed",
1005
+ task_id: err.taskId,
1006
+ message: err.message
1007
+ };
1008
+ } else if (err instanceof lenz_io_1.LenzError) {
1009
+ throw new n8n_workflow_1.NodeApiError(this.getNode(), { message: err.message, description: err.toString() }, {
1010
+ itemIndex,
1011
+ httpCode: err.statusCode ? String(err.statusCode) : void 0
1012
+ });
1013
+ } else {
1014
+ throw new n8n_workflow_1.NodeApiError(this.getNode(), { message: err.message }, { itemIndex });
1015
+ }
1016
+ }
1017
+ } else if (operation === "assess") {
1018
+ const text = this.getNodeParameter("text", itemIndex).trim();
1019
+ if (!text) {
1020
+ returnData.push({
1021
+ json: { skipped: true, reason: "empty_input" },
1022
+ pairedItem: { item: itemIndex }
1023
+ });
1024
+ continue;
1025
+ }
1026
+ const result = await client.assess({ text, language });
1027
+ if (!result.claims.length) {
1028
+ responseData = {
1029
+ status: result.error_code === "ambiguous" ? "ambiguous" : "no_claim",
1030
+ message: (_g = result.error) !== null && _g !== void 0 ? _g : "No verifiable factual claim was detected.",
1031
+ candidate_claims: (_h = result.candidate_claims) !== null && _h !== void 0 ? _h : []
1032
+ };
1033
+ } else {
1034
+ responseData = {
1035
+ status: "ok",
1036
+ claims: result.claims.map((c) => {
1037
+ var _a2, _b2, _c2, _d2;
1038
+ return {
1039
+ claim: (_a2 = c.claim) !== null && _a2 !== void 0 ? _a2 : "",
1040
+ verdict: (_b2 = c.verdict) !== null && _b2 !== void 0 ? _b2 : null,
1041
+ confidence: (_c2 = c.confidence) !== null && _c2 !== void 0 ? _c2 : null,
1042
+ passed: isPassingVerdict(c.verdict),
1043
+ verification_url: (_d2 = c.verification_url) !== null && _d2 !== void 0 ? _d2 : null
1044
+ };
1045
+ })
1046
+ };
1047
+ }
1048
+ } else if (operation === "extract") {
1049
+ const text = this.getNodeParameter("text", itemIndex).trim();
1050
+ if (!text) {
1051
+ returnData.push({
1052
+ json: { skipped: true, reason: "empty_input" },
1053
+ pairedItem: { item: itemIndex }
1054
+ });
1055
+ continue;
1056
+ }
1057
+ responseData = await client.extract({ text, language });
1058
+ } else if (operation === "ask") {
1059
+ const verificationId = this.getNodeParameter("verificationId", itemIndex);
1060
+ const question = this.getNodeParameter("question", itemIndex);
1061
+ const reply = await client.ask.send(verificationId, { message: question, language });
1062
+ responseData = {
1063
+ answer: (_j = reply.content) !== null && _j !== void 0 ? _j : ""
1064
+ };
1065
+ } else if (operation === "usage") {
1066
+ responseData = await client.usage();
1067
+ } else {
1068
+ throw new n8n_workflow_1.NodeOperationError(this.getNode(), "Unknown operation: " + operation, {
1069
+ itemIndex
1070
+ });
1071
+ }
1072
+ returnData.push({
1073
+ json: responseData,
1074
+ pairedItem: { item: itemIndex }
1075
+ });
1076
+ } catch (error) {
1077
+ if (this.continueOnFail()) {
1078
+ returnData.push({
1079
+ json: { error: error.message },
1080
+ pairedItem: { item: itemIndex }
1081
+ });
1082
+ continue;
1083
+ }
1084
+ if (error instanceof n8n_workflow_1.NodeOperationError) {
1085
+ throw error;
1086
+ }
1087
+ if (error instanceof lenz_io_1.LenzError) {
1088
+ throw new n8n_workflow_1.NodeApiError(this.getNode(), { message: error.message, description: error.toString() }, {
1089
+ itemIndex,
1090
+ httpCode: error.statusCode ? String(error.statusCode) : void 0
1091
+ });
1092
+ }
1093
+ throw new n8n_workflow_1.NodeApiError(this.getNode(), { message: error.message }, { itemIndex });
1094
+ }
1095
+ }
1096
+ return [returnData];
1097
+ }
1098
+ };
1099
+ exports.Lenz = Lenz;