@signaliz/sdk 1.0.61 → 1.0.62

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.
@@ -48,9 +48,13 @@ function publicRetryDetails(body) {
48
48
  ...body?.credits_used !== void 0 ? { credits_used: body.credits_used } : {},
49
49
  ...body?.credits_charged !== void 0 ? { credits_charged: body.credits_charged } : {},
50
50
  ...body?.job_id !== void 0 ? { job_id: body.job_id } : {},
51
+ ...body?.idempotency_key !== void 0 ? { idempotency_key: body.idempotency_key } : {},
51
52
  ...body?.verification_run_id !== void 0 ? { verification_run_id: body.verification_run_id } : {},
52
53
  ...body?.run_id !== void 0 ? { run_id: body.run_id } : {},
53
54
  ...body?.suggested_action !== void 0 ? { suggested_action: body.suggested_action } : {},
55
+ ...body?.do_not_auto_resubmit !== void 0 ? { do_not_auto_resubmit: body.do_not_auto_resubmit } : {},
56
+ ...body?.results_cleaned !== void 0 ? { results_cleaned: body.results_cleaned } : {},
57
+ ...body?.results_expired !== void 0 ? { results_expired: body.results_expired } : {},
54
58
  ...body?.signal_run_id !== void 0 ? { signal_run_id: body.signal_run_id } : {},
55
59
  ...body?.company_signal_run_id !== void 0 ? { company_signal_run_id: body.company_signal_run_id } : {},
56
60
  ...body?.resume_context_persisted !== void 0 ? { resume_context_persisted: body.resume_context_persisted } : {}
@@ -66,8 +70,9 @@ function mergePublicErrorDetails(body, explicitDetails) {
66
70
  }
67
71
  function mapErrorType(code, status) {
68
72
  if (code === "RATE_LIMITED" || status === 429) return "rate_limited";
69
- if (code === "VALIDATION_ERROR" || code === "SIGNAL_RUN_COMPANY_MISMATCH" || status === 400) return "validation";
73
+ if (code === "PAYLOAD_TOO_LARGE" || code === "VALIDATION_ERROR" || code === "SIGNAL_RUN_COMPANY_MISMATCH" || status === 400 || status === 413) return "validation";
70
74
  if (code === "NOT_FOUND" || status === 404) return "not_found";
75
+ if (code === "BATCH_RESULTS_EXPIRED" || status === 410) return "not_found";
71
76
  if (code === "NO_SUPPORTED_SIGNAL") return "not_found";
72
77
  if (code === "AUTH_EXPIRED" || status === 401) return "auth_expired";
73
78
  if (code === "INSUFFICIENT_CREDITS" || status === 402) return "insufficient_credits";
@@ -78,7 +83,7 @@ function mapErrorType(code, status) {
78
83
 
79
84
  // src/client.ts
80
85
  var DEFAULT_BASE_URL = "https://api.signaliz.com/functions/v1";
81
- var DEFAULT_TIMEOUT = 12e4;
86
+ var DEFAULT_TIMEOUT = 13e4;
82
87
  var DEFAULT_MAX_RETRIES = 3;
83
88
  var HttpClient = class {
84
89
  constructor(config) {
@@ -92,9 +97,9 @@ var HttpClient = class {
92
97
  throw new Error("Signaliz: provide either apiKey or clientId + clientSecret");
93
98
  }
94
99
  }
95
- async request(functionName, body, method = "POST") {
100
+ async request(functionName, body, method = "POST", options = {}) {
96
101
  let lastError;
97
- const idempotencyKey = method === "POST" ? requestIdempotencyKey(body) : void 0;
102
+ const idempotencyKey = method === "POST" ? requestIdempotencyKey(body, options.automaticIdempotency !== false) : void 0;
98
103
  for (let attempt = 0; attempt <= this.maxRetries; attempt++) {
99
104
  let timer;
100
105
  try {
@@ -162,8 +167,8 @@ var HttpClient = class {
162
167
  throw lastError || new Error("Request failed after retries");
163
168
  }
164
169
  /** Convenience wrapper for POST-based edge function calls */
165
- async post(functionName, body) {
166
- return this.request(functionName, body, "POST");
170
+ async post(functionName, body, options = {}) {
171
+ return this.request(functionName, body, "POST", options);
167
172
  }
168
173
  /** Convenience wrapper for GET-based edge function calls with query params */
169
174
  async get(functionName, query = {}) {
@@ -280,10 +285,12 @@ var HttpClient = class {
280
285
  return this.accessTokenPromise;
281
286
  }
282
287
  };
283
- function requestIdempotencyKey(body) {
288
+ function requestIdempotencyKey(body, automaticIdempotency) {
284
289
  const requested = typeof body.idempotency_key === "string" ? body.idempotency_key.trim() : "";
290
+ if (requested) return requested;
291
+ if (!automaticIdempotency) return void 0;
285
292
  const nonce = globalThis.crypto?.randomUUID?.() || `${Date.now()}-${Math.random().toString(16).slice(2)}`;
286
- return requested || `sdk_${nonce}`;
293
+ return `sdk_${nonce}`;
287
294
  }
288
295
  function sleep(ms) {
289
296
  return new Promise((r) => setTimeout(r, ms));
@@ -385,7 +392,7 @@ function mapMcpErrorType(error) {
385
392
  function mapErrorTypeFromCode(code) {
386
393
  if (!code) return "unknown";
387
394
  if (code === "RATE_LIMITED") return "rate_limited";
388
- if (code === "INVALID_ARGUMENT" || code === "VALIDATION_ERROR" || code === "VALIDATION_001" || code === "SIGNAL_RUN_COMPANY_MISMATCH" || code === "APPROVAL_REQUIRED" || code === "WEBHOOK_APPROVAL_REQUIRED" || code === "TARGET_LIMIT_EXCEEDED") return "validation";
395
+ if (code === "INVALID_ARGUMENT" || code === "PAYLOAD_TOO_LARGE" || code === "VALIDATION_ERROR" || code === "VALIDATION_001" || code === "SIGNAL_RUN_COMPANY_MISMATCH" || code === "APPROVAL_REQUIRED" || code === "WEBHOOK_APPROVAL_REQUIRED" || code === "TARGET_LIMIT_EXCEEDED") return "validation";
389
396
  if (code === "AUTH_REQUIRED" || code === "AUTH_001") return "auth_expired";
390
397
  if (code === "QUOTA_EXCEEDED") return "insufficient_credits";
391
398
  if (code === "BUILD_NOT_READY" || code === "ARTIFACT_NOT_READY") return "not_found";
@@ -393,6 +400,166 @@ function mapErrorTypeFromCode(code) {
393
400
  return "unknown";
394
401
  }
395
402
 
403
+ // src/email-validation.ts
404
+ function isValidEmailString(value) {
405
+ if (typeof value !== "string") return false;
406
+ const email = value.trim();
407
+ if (!email || email.length > 254) return false;
408
+ const separator = email.indexOf("@");
409
+ if (separator <= 0 || separator !== email.lastIndexOf("@")) return false;
410
+ const local = email.slice(0, separator);
411
+ const domain = email.slice(separator + 1).toLowerCase();
412
+ if (local.length > 64 || domain.length > 253 || local.startsWith(".") || local.endsWith(".") || local.includes("..") || domain.startsWith(".") || domain.endsWith(".") || domain.includes("..")) return false;
413
+ if (!/^[A-Za-z0-9.!#$%&'*+/=?^_`{|}~-]+$/.test(local)) return false;
414
+ const labels = domain.split(".");
415
+ if (labels.length < 2) return false;
416
+ return labels.every(
417
+ (label) => label.length > 0 && label.length <= 63 && /^[a-z0-9-]+$/.test(label) && !label.startsWith("-") && !label.endsWith("-")
418
+ );
419
+ }
420
+
421
+ // src/core-product-input-bounds.ts
422
+ var LARGE_BATCH_MAX_ENCODED_BYTES = 8 * 1024 * 1024;
423
+ var DOMAIN_MAX_UTF8_BYTES = 253;
424
+ var NAME_MAX_UTF8_BYTES = 500;
425
+ var TITLE_MAX_UTF8_BYTES = 500;
426
+ var ID_MAX_UTF8_BYTES = 200;
427
+ var RESEARCH_PROMPT_MAX_UTF8_BYTES = 2e3;
428
+ var CAMPAIGN_OFFER_MAX_UTF8_BYTES = 4e3;
429
+ var SIGNAL_TYPE_MAX_UTF8_BYTES = 100;
430
+ var UTF8_ENCODER = new TextEncoder();
431
+ function byteLength(value) {
432
+ return UTF8_ENCODER.encode(value).byteLength;
433
+ }
434
+ function payloadTooLarge(message, details) {
435
+ throw new SignalizError({
436
+ code: "PAYLOAD_TOO_LARGE",
437
+ message,
438
+ errorType: "validation",
439
+ details: {
440
+ ...details,
441
+ retry_eligible: false,
442
+ credits_used: 0,
443
+ credits_charged: 0
444
+ }
445
+ });
446
+ }
447
+ function assertStringBytes(value, field, maxBytes, index) {
448
+ if (typeof value !== "string") return;
449
+ const encodedBytes = byteLength(value);
450
+ if (encodedBytes <= maxBytes) return;
451
+ payloadTooLarge(
452
+ `${field} is ${encodedBytes} UTF-8 bytes; maximum is ${maxBytes} bytes`,
453
+ {
454
+ field,
455
+ ...index !== void 0 ? { index } : {},
456
+ encoded_bytes: encodedBytes,
457
+ max_bytes: maxBytes
458
+ }
459
+ );
460
+ }
461
+ function assertCompanySignalRows(body) {
462
+ const requests = Array.isArray(body.requests) ? body.requests : [];
463
+ assertStringBytes(
464
+ body.research_prompt,
465
+ "research_prompt",
466
+ RESEARCH_PROMPT_MAX_UTF8_BYTES
467
+ );
468
+ if (Array.isArray(body.signal_types)) {
469
+ body.signal_types.forEach(
470
+ (signalType, index) => assertStringBytes(
471
+ signalType,
472
+ `signal_types[${index}]`,
473
+ SIGNAL_TYPE_MAX_UTF8_BYTES,
474
+ index
475
+ )
476
+ );
477
+ }
478
+ requests.forEach((value, index) => {
479
+ if (!value || typeof value !== "object" || Array.isArray(value)) return;
480
+ const request = value;
481
+ assertStringBytes(
482
+ request.company_domain,
483
+ `requests[${index}].company_domain`,
484
+ DOMAIN_MAX_UTF8_BYTES,
485
+ index
486
+ );
487
+ assertStringBytes(
488
+ request.company_name,
489
+ `requests[${index}].company_name`,
490
+ NAME_MAX_UTF8_BYTES,
491
+ index
492
+ );
493
+ });
494
+ }
495
+ function assertSignalCopyRows(body) {
496
+ const requests = Array.isArray(body.requests) ? body.requests : [];
497
+ requests.forEach((value, index) => {
498
+ if (!value || typeof value !== "object" || Array.isArray(value)) return;
499
+ const request = value;
500
+ assertStringBytes(
501
+ request.company_domain,
502
+ `requests[${index}].company_domain`,
503
+ DOMAIN_MAX_UTF8_BYTES,
504
+ index
505
+ );
506
+ assertStringBytes(
507
+ request.person_name,
508
+ `requests[${index}].person_name`,
509
+ NAME_MAX_UTF8_BYTES,
510
+ index
511
+ );
512
+ assertStringBytes(
513
+ request.title,
514
+ `requests[${index}].title`,
515
+ TITLE_MAX_UTF8_BYTES,
516
+ index
517
+ );
518
+ assertStringBytes(
519
+ request.campaign_offer,
520
+ `requests[${index}].campaign_offer`,
521
+ CAMPAIGN_OFFER_MAX_UTF8_BYTES,
522
+ index
523
+ );
524
+ assertStringBytes(
525
+ request.research_prompt,
526
+ `requests[${index}].research_prompt`,
527
+ RESEARCH_PROMPT_MAX_UTF8_BYTES,
528
+ index
529
+ );
530
+ assertStringBytes(
531
+ request.signal_run_id,
532
+ `requests[${index}].signal_run_id`,
533
+ ID_MAX_UTF8_BYTES,
534
+ index
535
+ );
536
+ });
537
+ }
538
+ function assertLargeCoreProductInputBounds(path, body) {
539
+ const requests = Array.isArray(body.requests) ? body.requests : [];
540
+ if (requests.length <= 25) return;
541
+ assertStringBytes(body.idempotency_key, "idempotency_key", ID_MAX_UTF8_BYTES);
542
+ if (path === "api/v1/company-signals") {
543
+ assertCompanySignalRows(body);
544
+ } else {
545
+ assertSignalCopyRows(body);
546
+ }
547
+ const encoded = JSON.stringify(body);
548
+ if (typeof encoded !== "string") {
549
+ throw new TypeError("Core product batch input must be JSON serializable.");
550
+ }
551
+ const encodedBytes = byteLength(encoded);
552
+ if (encodedBytes <= LARGE_BATCH_MAX_ENCODED_BYTES) return;
553
+ payloadTooLarge(
554
+ `${path === "api/v1/company-signals" ? "Company Signal" : "Signal Copy"} batch request is ${encodedBytes} UTF-8 bytes after JSON encoding; maximum is ${LARGE_BATCH_MAX_ENCODED_BYTES} bytes`,
555
+ {
556
+ field: "requests",
557
+ encoded_bytes: encodedBytes,
558
+ max_bytes: LARGE_BATCH_MAX_ENCODED_BYTES
559
+ }
560
+ );
561
+ }
562
+
396
563
  // src/index.ts
397
564
  var MAX_ROW_RATE_LIMIT_RETRIES = 2;
398
565
  var Signaliz = class {
@@ -426,9 +593,25 @@ var Signaliz = class {
426
593
  options
427
594
  );
428
595
  }
596
+ async resumeFindEmailBatch(jobId, options) {
597
+ const startedAt = Date.now();
598
+ const { rows, total } = await this.readRecoverableCoreBatchJob(
599
+ "api/v1/find-email",
600
+ jobId,
601
+ "Find Email",
602
+ options
603
+ );
604
+ return finalizeCoreBatch(
605
+ total,
606
+ startedAt,
607
+ recoverableRowsToRound(rows, "Find Email"),
608
+ (item) => normalizeFindEmailResult(item.raw),
609
+ options
610
+ );
611
+ }
429
612
  async verifyEmail(email, options) {
430
613
  const normalizedEmail = typeof email === "string" ? email.trim() : "";
431
- if (options?.dryRun !== true && !options?.verificationRunId && normalizedEmail && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(normalizedEmail)) {
614
+ if (options?.dryRun !== true && !options?.verificationRunId && normalizedEmail && !isValidEmailString(normalizedEmail)) {
432
615
  const error = `Invalid email format: "${normalizedEmail}"`;
433
616
  return normalizeVerifyEmailResult(normalizedEmail, {
434
617
  success: false,
@@ -478,19 +661,20 @@ var Signaliz = class {
478
661
  }
479
662
  async verifyEmails(emails, options) {
480
663
  validateBatchSize(emails);
664
+ const requests = emails.map((email) => verifyEmailBatchRequestBody(email, options));
481
665
  if (options?.dryRun === true) {
482
666
  return this.runCoreProductDryRun(
483
667
  "api/v1/verify-email",
484
- emails.map((email) => ({ email, skip_cache: options.skipCache })),
668
+ requests,
485
669
  options.idempotencyKey
486
670
  );
487
671
  }
488
672
  const startedAt = Date.now();
489
673
  const round = await this.runCoreBatchRound(
490
674
  "api/v1/verify-email",
491
- emails.map((email, index) => ({
675
+ requests.map((request, index) => ({
492
676
  index,
493
- request: { email, skip_cache: options?.skipCache }
677
+ request
494
678
  })),
495
679
  options
496
680
  );
@@ -498,7 +682,23 @@ var Signaliz = class {
498
682
  emails.length,
499
683
  startedAt,
500
684
  round,
501
- (item) => normalizeVerifyEmailResult(emails[item.index], item.raw),
685
+ (item) => normalizeVerifyEmailResult(String(requests[item.index].email ?? ""), item.raw),
686
+ options
687
+ );
688
+ }
689
+ async resumeVerifyEmailBatch(jobId, options) {
690
+ const startedAt = Date.now();
691
+ const { rows, total } = await this.readRecoverableCoreBatchJob(
692
+ "api/v1/verify-email",
693
+ jobId,
694
+ "Verify Email",
695
+ options
696
+ );
697
+ return finalizeCoreBatch(
698
+ total,
699
+ startedAt,
700
+ recoverableRowsToRound(rows, "Verify Email"),
701
+ (item) => normalizeVerifyEmailResult(String(item.raw?.email || ""), item.raw),
502
702
  options
503
703
  );
504
704
  }
@@ -514,25 +714,92 @@ var Signaliz = class {
514
714
  validateSignalDiscoveryParams(params);
515
715
  const data = await this.client.post(
516
716
  "api/v1/signals",
517
- signalDiscoveryRequestBody(params)
717
+ signalDiscoveryRequestBody(params),
718
+ // Signals Everything owns a normalized, workspace-scoped automatic key
719
+ // at the API boundary so SDK, CLI, MCP, and direct REST calls coalesce.
720
+ { automaticIdempotency: false }
518
721
  );
519
722
  return normalizeSignalDiscoveryResult(params, data);
520
723
  }
521
724
  async enrichCompanies(companies, options) {
522
725
  validateBatchSize(companies);
523
726
  companies.forEach((params) => validateCoreProductMaxWaitMs(params.maxWaitMs));
727
+ const requests = companies.map(companySignalRequestBody);
728
+ if (companies.length > 25 && requests.some((request) => request.include_candidate_evidence === true)) {
729
+ throw new RangeError(
730
+ "includeCandidateEvidence is not supported in Company Signals batches larger than 25 because rejected evidence diagnostics are intentionally excluded from durable scale storage; split diagnostic requests into batches of at most 25."
731
+ );
732
+ }
733
+ const largeBatch = companySignalLargeBatchSubmission(requests);
734
+ if (companies.length > 25 && !largeBatch) {
735
+ throw new RangeError(
736
+ "Company Signals batches larger than 25 require homogeneous new-enrichment rows with company identity only and uniform controls; split recovery or mixed-control rows into batches of at most 25."
737
+ );
738
+ }
524
739
  if (options?.dryRun === true) {
525
740
  return this.runCoreProductDryRun(
526
741
  "api/v1/company-signals",
527
- companies.map(companySignalRequestBody),
528
- options.idempotencyKey
742
+ largeBatch?.requests || requests,
743
+ options.idempotencyKey,
744
+ largeBatch?.submissionFields
529
745
  );
530
746
  }
531
747
  const startedAt = Date.now();
532
- const initialTasks = companies.map((params, index) => ({
533
- index,
534
- request: companySignalRequestBody(params)
535
- }));
748
+ if (options?.waitForResult === false) {
749
+ if (!largeBatch) {
750
+ throw new RangeError(
751
+ "Company Signals --no-wait requires more than 25 homogeneous new-enrichment rows; recovery reads and mixed controls must use batches of at most 25."
752
+ );
753
+ }
754
+ return this.submitRecoverableCoreBatchJob(
755
+ "api/v1/company-signals",
756
+ largeBatch.requests,
757
+ "Company Signals",
758
+ options.idempotencyKey,
759
+ companies.map((_, index) => index),
760
+ largeBatch.submissionFields
761
+ );
762
+ }
763
+ if (largeBatch) {
764
+ const job = await this.submitRecoverableCoreBatchJob(
765
+ "api/v1/company-signals",
766
+ largeBatch.requests,
767
+ "Company Signals",
768
+ options?.idempotencyKey,
769
+ companies.map((_, index) => index),
770
+ largeBatch.submissionFields
771
+ );
772
+ const { rows, total } = await this.readRecoverableCoreBatchJob(
773
+ "api/v1/company-signals",
774
+ job.jobId,
775
+ "Company Signals",
776
+ {
777
+ pageSize: 25,
778
+ maxWaitMs: options?.maxWaitMs,
779
+ pollIntervalMs: options?.pollIntervalMs,
780
+ idempotencyKey: job.idempotencyKey,
781
+ compactDuplicates: options?.compactDuplicates === true
782
+ }
783
+ );
784
+ if (total !== companies.length) {
785
+ throw new Error(`Company Signals batch returned ${total} results for ${companies.length} requests`);
786
+ }
787
+ const batch = finalizeCoreBatch(
788
+ companies.length,
789
+ startedAt,
790
+ recoverableRowsToRound(rows, "Company Signals"),
791
+ (item) => normalizeCompanySignalResult(companies[item.index], item.raw)
792
+ );
793
+ return compactKnownDuplicateBatch(
794
+ batch,
795
+ dedupeExactBatchItems(
796
+ requests,
797
+ (request) => coreProductBatchRequestKey("api/v1/company-signals", request)
798
+ ).sourceIndexByInput,
799
+ options?.compactDuplicates === true
800
+ );
801
+ }
802
+ const initialTasks = requests.map((request, index) => ({ index, request }));
536
803
  const round = await this.runCoreBatchRound("api/v1/company-signals", initialTasks, options);
537
804
  const results = round.map((item) => {
538
805
  if (!item.success || !item.raw) {
@@ -555,6 +822,22 @@ var Signaliz = class {
555
822
  results: compactedResults
556
823
  };
557
824
  }
825
+ async resumeCompanySignalBatch(jobId, options) {
826
+ const startedAt = Date.now();
827
+ const { rows, total } = await this.readRecoverableCoreBatchJob(
828
+ "api/v1/company-signals",
829
+ jobId,
830
+ "Company Signals",
831
+ options
832
+ );
833
+ return finalizeRecoveredCoreBatch(
834
+ total,
835
+ startedAt,
836
+ recoverableRowsToRound(rows, "Company Signals"),
837
+ (item) => normalizeCompanySignalResult(companySignalParamsFromRecoveredRow(item.raw), item.raw),
838
+ options
839
+ );
840
+ }
558
841
  async signalToCopy(params) {
559
842
  validateSignalToCopyParams(params);
560
843
  const request = signalToCopyRequestBody(params);
@@ -566,6 +849,7 @@ var Signaliz = class {
566
849
  async createSignalCopyBatch(requests, options) {
567
850
  validateBatchSize(requests);
568
851
  requests.forEach(validateSignalToCopyParams);
852
+ validateLargeAvailableDataSignalCopyBatch(requests);
569
853
  if (options?.dryRun === true) {
570
854
  return this.runCoreProductDryRun(
571
855
  "api/v1/signal-to-copy",
@@ -574,6 +858,59 @@ var Signaliz = class {
574
858
  );
575
859
  }
576
860
  const startedAt = Date.now();
861
+ if (requests.length > 25) {
862
+ if (options?.waitForResult === false) {
863
+ return this.submitRecoverableCoreBatchJob(
864
+ "api/v1/signal-to-copy",
865
+ requests.map(availableDataSignalCopyRequestBody),
866
+ "Signal to Copy",
867
+ options.idempotencyKey,
868
+ requests.map((_, index) => index)
869
+ );
870
+ }
871
+ const job = await this.submitRecoverableCoreBatchJob(
872
+ "api/v1/signal-to-copy",
873
+ requests.map(availableDataSignalCopyRequestBody),
874
+ "Signal to Copy",
875
+ options?.idempotencyKey,
876
+ requests.map((_, index) => index)
877
+ );
878
+ const { rows, total } = await this.readRecoverableCoreBatchJob(
879
+ "api/v1/signal-to-copy",
880
+ job.jobId,
881
+ "Signal to Copy",
882
+ {
883
+ pageSize: 100,
884
+ maxWaitMs: options?.maxWaitMs,
885
+ pollIntervalMs: options?.pollIntervalMs,
886
+ idempotencyKey: job.idempotencyKey,
887
+ compactDuplicates: options?.compactDuplicates === true
888
+ }
889
+ );
890
+ if (total !== requests.length) {
891
+ throw new Error(`Signal to Copy batch returned ${total} results for ${requests.length} requests`);
892
+ }
893
+ const results2 = recoverableSignalCopyRows(rows);
894
+ const succeeded2 = results2.filter((item) => item.success).length;
895
+ const batch = {
896
+ total: requests.length,
897
+ succeeded: succeeded2,
898
+ failed: requests.length - succeeded2,
899
+ durationMs: Date.now() - startedAt,
900
+ results: results2
901
+ };
902
+ return compactKnownDuplicateBatch(
903
+ batch,
904
+ dedupeExactBatchItems(
905
+ requests,
906
+ (params) => coreProductBatchRequestKey("api/v1/signal-to-copy", signalToCopyRequestBody(params))
907
+ ).sourceIndexByInput,
908
+ options?.compactDuplicates === true
909
+ );
910
+ }
911
+ if (options?.waitForResult === false) {
912
+ throw new RangeError("Signal to Copy --no-wait requires a durable batch of more than 25 rows.");
913
+ }
577
914
  const deduplicated = dedupeExactBatchItems(
578
915
  requests,
579
916
  (params) => coreProductBatchRequestKey("api/v1/signal-to-copy", signalToCopyRequestBody(params))
@@ -598,12 +935,36 @@ var Signaliz = class {
598
935
  results: compactedResults
599
936
  };
600
937
  }
601
- async runCoreProductDryRun(path, requests, idempotencyKey) {
602
- const data = await this.client.post(path, compact({
938
+ async resumeSignalCopyBatch(jobId, options) {
939
+ const startedAt = Date.now();
940
+ const { rows, total } = await this.readRecoverableCoreBatchJob(
941
+ "api/v1/signal-to-copy",
942
+ jobId,
943
+ "Signal to Copy",
944
+ options
945
+ );
946
+ const results = recoverableSignalCopyRows(rows);
947
+ const compactedResults = compactExactDuplicateResults(results, options?.compactDuplicates === true);
948
+ const succeeded = compactedResults.filter((item) => item.success).length;
949
+ return {
950
+ total,
951
+ succeeded,
952
+ failed: total - succeeded,
953
+ durationMs: Date.now() - startedAt,
954
+ results: compactedResults
955
+ };
956
+ }
957
+ async runCoreProductDryRun(path, requests, idempotencyKey, defaults = {}) {
958
+ const requestBody = compact({
603
959
  requests,
960
+ ...defaults,
604
961
  dry_run: true,
605
962
  idempotency_key: idempotencyKey
606
- }));
963
+ });
964
+ if (path === "api/v1/company-signals" || path === "api/v1/signal-to-copy") {
965
+ assertLargeCoreProductInputBounds(path, requestBody);
966
+ }
967
+ const data = await this.client.post(path, requestBody);
607
968
  if (!isCoreProductDryRun(data)) {
608
969
  throw new Error(`${path} dry run returned a live-result contract`);
609
970
  }
@@ -649,76 +1010,209 @@ var Signaliz = class {
649
1010
  }
650
1011
  return uniqueResults;
651
1012
  }
652
- async runRecoverableCoreBatchJob(path, requests, label, pageSize = 500, maxWaitMs = 20 * 6e4, idempotencyKey, idempotencyItemIndices) {
653
- const startedAt = Date.now();
1013
+ async runRecoverableCoreBatchJob(path, requests, label, pageSize = 500, maxWaitMs = 20 * 6e4, idempotencyKey, idempotencyItemIndices, submissionFields = {}) {
1014
+ const job = await this.submitRecoverableCoreBatchJob(
1015
+ path,
1016
+ requests,
1017
+ label,
1018
+ idempotencyKey,
1019
+ idempotencyItemIndices,
1020
+ submissionFields
1021
+ );
1022
+ const recovered = await this.readRecoverableCoreBatchJob(path, job.jobId, label, {
1023
+ pageSize,
1024
+ maxWaitMs,
1025
+ idempotencyKey: job.idempotencyKey
1026
+ });
1027
+ if (recovered.total !== requests.length) {
1028
+ throw new Error(`${label} batch returned ${recovered.total} results for ${requests.length} requests`);
1029
+ }
1030
+ return recovered.rows;
1031
+ }
1032
+ async submitRecoverableCoreBatchJob(path, requests, label, idempotencyKey, idempotencyItemIndices, submissionFields = {}) {
654
1033
  const submissionIdempotencyKey = idempotencyKey?.trim() || newBatchIdempotencyKey(label);
1034
+ const requestBody = {
1035
+ ...submissionFields,
1036
+ requests,
1037
+ idempotency_key: submissionIdempotencyKey,
1038
+ batch_item_indices: idempotencyItemIndices
1039
+ };
1040
+ if (path === "api/v1/company-signals" || path === "api/v1/signal-to-copy") {
1041
+ assertLargeCoreProductInputBounds(path, requestBody);
1042
+ }
655
1043
  let submission;
656
1044
  try {
657
- submission = await this.client.post(path, {
658
- requests,
659
- idempotency_key: submissionIdempotencyKey,
660
- batch_item_indices: idempotencyItemIndices
661
- });
1045
+ submission = await this.client.post(path, requestBody);
662
1046
  } catch (error) {
663
1047
  if (error instanceof SignalizError && !error.isRetryable) throw error;
664
1048
  const message = error instanceof Error ? error.message : String(error);
665
- throw new Error(
666
- `${label} batch submission was not confirmed. Retry with idempotency key "${submissionIdempotencyKey}" to recover the same job. ${message}`
667
- );
1049
+ throw new SignalizError({
1050
+ code: "BATCH_SUBMISSION_UNCONFIRMED",
1051
+ message: `${label} batch submission was not confirmed. Retry with idempotency key "${submissionIdempotencyKey}" to recover the same job. ${message}`,
1052
+ errorType: "provider_error",
1053
+ details: {
1054
+ idempotency_key: submissionIdempotencyKey,
1055
+ suggested_action: "retry_with_idempotency_key",
1056
+ retry_eligible: true
1057
+ }
1058
+ });
668
1059
  }
669
1060
  const jobId = String(submission.job_id || "").trim();
670
- if (!jobId) throw new Error(`${label} batch did not return a recoverable job_id`);
671
- let status = await this.client.post(path, {
672
- job_id: jobId,
673
- page: 1,
674
- page_size: pageSize
675
- });
676
- while (!["completed", "partial", "failed"].includes(String(status.status || "").toLowerCase())) {
677
- if (Date.now() - startedAt >= maxWaitMs) {
678
- throw new Error(`${label} batch ${jobId} did not complete within ${maxWaitMs}ms`);
679
- }
680
- const retryAfterMs = Number(status.next_poll_after_seconds || 2) * 1e3;
681
- await sleep2(Math.min(Math.max(250, retryAfterMs), maxWaitMs - (Date.now() - startedAt)));
682
- status = await this.client.post(path, {
1061
+ if (!jobId) {
1062
+ throw new SignalizError({
1063
+ code: "INVALID_JOB_RESPONSE",
1064
+ message: `${label} batch did not return a recoverable job_id`,
1065
+ errorType: "provider_error",
1066
+ details: {
1067
+ idempotency_key: submissionIdempotencyKey,
1068
+ suggested_action: "retry_with_idempotency_key",
1069
+ retry_eligible: true
1070
+ }
1071
+ });
1072
+ }
1073
+ const submissionStatus = String(submission.status || "").toLowerCase();
1074
+ const status = ["queued", "processing", "completed", "partial", "failed"].includes(submissionStatus) ? submissionStatus : "queued";
1075
+ return {
1076
+ success: true,
1077
+ status,
1078
+ capability: path === "api/v1/company-signals" ? "company_signals" : "signal_to_copy",
1079
+ jobId,
1080
+ idempotencyKey: String(submission.idempotency_key || submissionIdempotencyKey),
1081
+ total: Math.max(0, Number(submission.total ?? submission.items_total ?? requests.length)),
1082
+ ...Number.isFinite(Number(submission.next_poll_after_seconds)) ? {
1083
+ nextPollAfterSeconds: Number(submission.next_poll_after_seconds)
1084
+ } : {}
1085
+ };
1086
+ }
1087
+ async readRecoverableCoreBatchJob(path, jobIdInput, label, options = {}) {
1088
+ const jobId = String(jobIdInput || "").trim();
1089
+ if (!jobId) throw new RangeError(`${label} jobId is required`);
1090
+ if (options.pageSize !== void 0 && (!Number.isFinite(options.pageSize) || options.pageSize <= 0)) {
1091
+ throw new RangeError("pageSize must be a positive finite number");
1092
+ }
1093
+ if (options.maxWaitMs !== void 0 && (!Number.isFinite(options.maxWaitMs) || options.maxWaitMs <= 0)) {
1094
+ throw new RangeError("maxWaitMs must be a positive finite number");
1095
+ }
1096
+ if (options.pollIntervalMs !== void 0 && (!Number.isFinite(options.pollIntervalMs) || options.pollIntervalMs <= 0)) {
1097
+ throw new RangeError("pollIntervalMs must be a positive finite number");
1098
+ }
1099
+ const maximumPageSize = path === "api/v1/signal-to-copy" ? 100 : 25;
1100
+ const pageSize = Math.min(maximumPageSize, Math.max(1, Math.trunc(options.pageSize ?? maximumPageSize)));
1101
+ const emailJob = path === "api/v1/find-email" || path === "api/v1/verify-email";
1102
+ const maxWaitMs = options.maxWaitMs ?? (emailJob ? 20 * 6e4 : void 0);
1103
+ const startedAt = Date.now();
1104
+ let observedIdempotencyKey = String(options.idempotencyKey || "").trim();
1105
+ try {
1106
+ let status = await this.client.post(path, {
683
1107
  job_id: jobId,
684
1108
  page: 1,
685
- page_size: pageSize
1109
+ page_size: pageSize,
1110
+ compact_duplicates: options.compactDuplicates === true
686
1111
  });
687
- }
688
- const totalPages = Math.max(1, Number(status.total_pages || 1));
689
- const pages = [Array.isArray(status.results) ? status.results : []];
690
- if (totalPages > 1) {
691
- const pageNumbers = Array.from({ length: totalPages - 1 }, (_, index) => index + 2);
692
- const remainingPages = await runBatch(
693
- pageNumbers,
694
- async (page) => await this.client.post(path, {
695
- job_id: jobId,
696
- page,
697
- page_size: pageSize
698
- }),
699
- { concurrency: Math.min(10, pageNumbers.length) }
1112
+ observedIdempotencyKey = String(status.idempotency_key || observedIdempotencyKey).trim();
1113
+ assertRecoverableBatchResultsRetained(
1114
+ status,
1115
+ label,
1116
+ jobId,
1117
+ observedIdempotencyKey
700
1118
  );
701
- for (const page of remainingPages.results) {
702
- if (!page.success || !page.data) {
703
- throw new Error(`${label} batch result page ${page.index + 2} failed: ${page.error || "unknown error"}`);
1119
+ while (!["completed", "partial", "failed"].includes(String(status.status || "").toLowerCase())) {
1120
+ if (maxWaitMs !== void 0 && Date.now() - startedAt >= maxWaitMs) {
1121
+ throw new SignalizError({
1122
+ code: "TIMEOUT",
1123
+ message: `${label} batch ${jobId} did not complete within ${maxWaitMs}ms`,
1124
+ errorType: "timeout",
1125
+ retryAfter: Number(status.next_poll_after_seconds || 2),
1126
+ details: {
1127
+ job_id: jobId,
1128
+ ...observedIdempotencyKey ? { idempotency_key: observedIdempotencyKey } : {},
1129
+ suggested_action: "check_job_status",
1130
+ retry_eligible: true
1131
+ }
1132
+ });
704
1133
  }
705
- pages.push(Array.isArray(page.data.results) ? page.data.results : []);
1134
+ const retryAfterMs = options.pollIntervalMs ?? Number(status.next_poll_after_seconds || 2) * 1e3;
1135
+ const boundedDelay = maxWaitMs === void 0 ? Math.max(250, retryAfterMs) : Math.min(Math.max(250, retryAfterMs), maxWaitMs - (Date.now() - startedAt));
1136
+ await sleep2(boundedDelay);
1137
+ status = await this.client.post(path, {
1138
+ job_id: jobId,
1139
+ page: 1,
1140
+ page_size: pageSize,
1141
+ compact_duplicates: options.compactDuplicates === true
1142
+ });
1143
+ observedIdempotencyKey = String(status.idempotency_key || observedIdempotencyKey).trim();
1144
+ assertRecoverableBatchResultsRetained(
1145
+ status,
1146
+ label,
1147
+ jobId,
1148
+ observedIdempotencyKey
1149
+ );
706
1150
  }
707
- }
708
- const rows = pages.flat();
709
- if (rows.length !== requests.length) {
710
- throw new Error(`${label} batch returned ${rows.length} results for ${requests.length} requests`);
711
- }
712
- const seen = /* @__PURE__ */ new Set();
713
- for (const row of rows) {
714
- const index = Number(row.index);
715
- if (!Number.isInteger(index) || index < 0 || index >= requests.length || seen.has(index)) {
716
- throw new Error(`${label} batch returned an invalid result index`);
1151
+ const totalPages = Math.max(1, Number(status.total_pages || 1));
1152
+ const pages = [Array.isArray(status.results) ? status.results : []];
1153
+ if (totalPages > 1) {
1154
+ const pageNumbers = Array.from({ length: totalPages - 1 }, (_, index) => index + 2);
1155
+ const remainingPages = await runBatch(
1156
+ pageNumbers,
1157
+ async (page) => await this.client.post(path, {
1158
+ job_id: jobId,
1159
+ page,
1160
+ page_size: pageSize,
1161
+ compact_duplicates: options.compactDuplicates === true
1162
+ }),
1163
+ { concurrency: Math.min(10, pageNumbers.length) }
1164
+ );
1165
+ for (const page of remainingPages.results) {
1166
+ if (!page.success || !page.data) {
1167
+ throw new Error(`${label} batch result page ${page.index + 2} failed: ${page.error || "unknown error"}`);
1168
+ }
1169
+ observedIdempotencyKey = String(
1170
+ page.data.idempotency_key || observedIdempotencyKey
1171
+ ).trim();
1172
+ assertRecoverableBatchResultsRetained(
1173
+ page.data,
1174
+ label,
1175
+ jobId,
1176
+ observedIdempotencyKey
1177
+ );
1178
+ pages.push(Array.isArray(page.data.results) ? page.data.results : []);
1179
+ }
1180
+ }
1181
+ const rows = pages.flat();
1182
+ const total = Math.max(0, Number(status.total_results ?? status.total ?? status.items_total ?? rows.length));
1183
+ if (rows.length !== total) {
1184
+ throw new Error(`${label} batch returned ${rows.length} results for ${total} job rows`);
1185
+ }
1186
+ const seen = /* @__PURE__ */ new Set();
1187
+ for (const row of rows) {
1188
+ const index = Number(row.index);
1189
+ if (!Number.isInteger(index) || index < 0 || seen.has(index)) {
1190
+ throw new Error(`${label} batch returned an invalid result index`);
1191
+ }
1192
+ seen.add(index);
717
1193
  }
718
- seen.add(index);
1194
+ if (seen.size !== total) throw new Error(`${label} batch returned incomplete indexed results`);
1195
+ return { rows, total };
1196
+ } catch (error) {
1197
+ if (error instanceof SignalizError && String(error.details?.job_id || "") === jobId) {
1198
+ throw error;
1199
+ }
1200
+ const signalizError = error instanceof SignalizError ? error : null;
1201
+ const message = error instanceof Error ? error.message : String(error);
1202
+ throw new SignalizError({
1203
+ code: signalizError?.code || "BATCH_RECOVERY_FAILED",
1204
+ message: `${label} batch ${jobId} could not be fully retrieved. Resume the existing job instead of resubmitting it. ${message}`,
1205
+ errorType: signalizError?.errorType || "provider_error",
1206
+ retryAfter: signalizError?.retryAfter,
1207
+ details: {
1208
+ ...signalizError?.details || {},
1209
+ job_id: jobId,
1210
+ ...observedIdempotencyKey ? { idempotency_key: observedIdempotencyKey } : {},
1211
+ suggested_action: "check_job_status",
1212
+ retry_eligible: true
1213
+ }
1214
+ });
719
1215
  }
720
- if (seen.size !== requests.length) throw new Error(`${label} batch returned incomplete indexed results`);
721
- return rows;
722
1216
  }
723
1217
  async runSignalCopyBatchChunk(requests, concurrency, rowRateLimitAttempt = 0, idempotencyKey, idempotencyItemIndices = requests.map((_, index) => index)) {
724
1218
  const results = new Array(requests.length);
@@ -783,11 +1277,11 @@ var Signaliz = class {
783
1277
  (task) => coreProductBatchRequestKey(path, task.request)
784
1278
  );
785
1279
  const uniqueTasks = deduplicated.uniqueItems;
786
- const recoverableBatch = path === "api/v1/find-email" ? { label: "Find Email", pageSize: 500, maxWaitMs: 20 * 6e4 } : path === "api/v1/verify-email" ? { label: "Verify Email", pageSize: 500, maxWaitMs: 20 * 6e4 } : void 0;
1280
+ const recoverableBatch = path === "api/v1/find-email" ? { label: "Find Email", pageSize: 25, maxWaitMs: 20 * 6e4 } : path === "api/v1/verify-email" ? { label: "Verify Email", pageSize: 25, maxWaitMs: 20 * 6e4 } : void 0;
787
1281
  const containsFindRecoveryReads = path === "api/v1/find-email" && uniqueTasks.some(
788
1282
  (task) => typeof task.request.run_id === "string" && task.request.run_id.trim()
789
1283
  );
790
- if (path !== "api/v1/company-signals" && recoverableBatch && uniqueTasks.length > 25 && !containsFindRecoveryReads) {
1284
+ if (recoverableBatch && uniqueTasks.length > 25 && !containsFindRecoveryReads) {
791
1285
  const rows = await this.runRecoverableCoreBatchJob(
792
1286
  path,
793
1287
  uniqueTasks.map((task) => task.request),
@@ -916,7 +1410,7 @@ function batchItemFailure(index, error, raw) {
916
1410
  const retryStrategy = raw?.retry_strategy ?? raw?.retryStrategy;
917
1411
  const creditsUsed = raw?.credits_used ?? raw?.creditsUsed;
918
1412
  const creditsCharged = raw?.credits_charged ?? raw?.creditsCharged;
919
- const runId = raw?.run_id ?? raw?.runId;
1413
+ const runId = [raw?.run_id, raw?.runId, raw?.signal_run_id, raw?.signalRunId].find((value) => typeof value === "string" && value.trim());
920
1414
  const verificationRunId = raw?.verification_run_id ?? raw?.verificationRunId;
921
1415
  const jobId = raw?.job_id ?? raw?.jobId;
922
1416
  const suggestedAction = raw?.suggested_action ?? raw?.suggestedAction;
@@ -939,6 +1433,42 @@ function batchItemFailure(index, error, raw) {
939
1433
  function recoverableJobRowFailed(row) {
940
1434
  return coreProductResponseFailed(row) || ["failed", "skipped"].includes(String(row._status || "").toLowerCase());
941
1435
  }
1436
+ function recoverableRowsToRound(rows, label) {
1437
+ return rows.map((raw) => {
1438
+ const index = Number(raw.index);
1439
+ const failed = recoverableJobRowFailed(raw);
1440
+ return {
1441
+ index,
1442
+ success: !failed,
1443
+ raw,
1444
+ error: failed ? raw.error || raw._error || raw.message || raw.failure_reason || `${label} item failed` : void 0
1445
+ };
1446
+ });
1447
+ }
1448
+ function recoverableSignalCopyRows(rows) {
1449
+ return rows.map((raw) => {
1450
+ const index = Number(raw.index);
1451
+ const duplicateOf = recoveredDuplicateOf(raw);
1452
+ if (duplicateOf !== void 0) return { index, success: true, duplicateOf };
1453
+ if (recoverableJobRowFailed(raw)) {
1454
+ return batchItemFailure(
1455
+ index,
1456
+ raw.error || raw._error || raw.message || "Signal to Copy item failed",
1457
+ raw
1458
+ );
1459
+ }
1460
+ try {
1461
+ assertTerminalSignalResponse(raw, "Signal to Copy");
1462
+ return { index, success: true, data: normalizeSignalToCopyResult(raw) };
1463
+ } catch (error) {
1464
+ return batchItemFailure(index, error instanceof Error ? error.message : String(error), raw);
1465
+ }
1466
+ });
1467
+ }
1468
+ function recoveredDuplicateOf(raw) {
1469
+ const value = Number(raw.duplicate_of ?? raw.duplicateOf ?? raw._duplicate_of);
1470
+ return Number.isInteger(value) && value >= 0 ? value : void 0;
1471
+ }
942
1472
  function newBatchIdempotencyKey(label) {
943
1473
  const nonce = globalThis.crypto?.randomUUID?.() || `${Date.now()}-${Math.random().toString(16).slice(2)}`;
944
1474
  return `signaliz-${label.toLowerCase().replace(/[^a-z0-9]+/g, "-")}-${nonce}`;
@@ -960,6 +1490,16 @@ function findEmailRequestBody(params) {
960
1490
  idempotency_key: params.idempotencyKey
961
1491
  });
962
1492
  }
1493
+ function verifyEmailBatchRequestBody(input, options) {
1494
+ if (typeof input === "string") {
1495
+ return compact({ email: input, skip_cache: options?.skipCache });
1496
+ }
1497
+ return compact({
1498
+ email: input.email,
1499
+ skip_cache: input.skipCache ?? options?.skipCache,
1500
+ idempotency_key: input.idempotencyKey
1501
+ });
1502
+ }
963
1503
  function isCoreProductDryRun(data) {
964
1504
  return data.dry_run === true && data.status === "planned";
965
1505
  }
@@ -994,12 +1534,17 @@ function normalizeFindEmailResult(data) {
994
1534
  const email = failed ? null : rawEmail;
995
1535
  const found = Boolean(email) && data.found !== false;
996
1536
  const verificationStatus = data.verification_status ?? (data.is_valid === true ? "valid" : data.status ?? "unknown");
1537
+ const deliverabilityStatus = data.deliverability_status ?? verificationStatus;
1538
+ const verificationVerdict = data.verification_verdict ?? verificationStatus;
997
1539
  const rawFreshness = String(data.verification_freshness ?? "").toLowerCase();
998
1540
  const verificationFreshness = rawFreshness === "fresh" || rawFreshness === "stale" ? rawFreshness : "unknown";
999
1541
  const needsReverification = data.needs_reverification === true || verificationFreshness === "stale";
1000
1542
  const explicitlyNotSendSafe = data.verified_for_sending === false || needsReverification;
1001
1543
  const verified = !explicitlyNotSendSafe && (data.is_valid === true || data.isValid === true || data.email_verified === true || data.verified_for_sending === true || ["valid", "deliverable"].includes(String(verificationStatus).toLowerCase()));
1002
1544
  const verificationAgeDays = typeof data.verification_age_days === "number" && Number.isFinite(data.verification_age_days) ? data.verification_age_days : null;
1545
+ const providerUsed = data.provider_used ?? data.provider ?? data.email_source ?? data.source ?? "unknown";
1546
+ const verificationSource = data.verification_source ?? providerUsed;
1547
+ const billingMetadata = data.billing_metadata && typeof data.billing_metadata === "object" && !Array.isArray(data.billing_metadata) ? data.billing_metadata : void 0;
1003
1548
  return {
1004
1549
  success: processing || !failed && found,
1005
1550
  found,
@@ -1008,61 +1553,86 @@ function normalizeFindEmailResult(data) {
1008
1553
  lastName: data.last_name,
1009
1554
  confidence: failed ? 0 : data.confidence ?? data.confidence_score ?? (verified ? 1 : 0),
1010
1555
  verificationStatus: failed ? "error" : verificationStatus,
1556
+ deliverabilityStatus: failed ? "error" : deliverabilityStatus,
1557
+ verificationVerdict: failed ? "error" : verificationVerdict,
1011
1558
  isVerified: !failed && verified,
1012
1559
  isVerifiedForSending: !failed && !needsReverification && data.verified_for_sending === true,
1560
+ isDeliverable: !failed && !needsReverification && data.verified_for_sending === true,
1013
1561
  isCatchAll: data.email_is_catch_all === true || data.is_catch_all === true,
1014
1562
  verificationFreshness,
1015
1563
  verificationAgeDays,
1016
1564
  verifiedAt: typeof data.verified_at === "string" ? data.verified_at : null,
1017
1565
  needsReverification: failed || needsReverification,
1018
- providerUsed: data.provider_used ?? data.provider ?? data.email_source ?? data.source ?? "unknown",
1019
- creditsUsed: data.credits_used,
1020
- estimatedExternalCostUsd: data.estimated_external_cost_usd,
1021
- billingMetadata: data.billing_metadata,
1022
- resultReturnedFrom: data.result_returned_from,
1023
- cacheHit: data.cache_hit,
1566
+ providerUsed,
1567
+ verificationSource,
1568
+ verificationObservedAt: typeof data.verification_observed_at === "string" ? data.verification_observed_at : void 0,
1569
+ failureReason: data.failure_reason ?? (failed || !found ? "no_verified_email_found" : "none"),
1570
+ creditsUsed: data.credits_used ?? billingMetadata?.credits_used,
1571
+ estimatedExternalCostUsd: data.estimated_external_cost_usd ?? billingMetadata?.estimated_external_cost_usd,
1572
+ billingMetadata,
1573
+ historicalBillingMetadata: data.historical_billing_metadata,
1574
+ resultReturnedFrom: data.result_returned_from ?? billingMetadata?.result_returned_from,
1575
+ cacheHit: typeof data.cache_hit === "boolean" ? data.cache_hit : billingMetadata?.cache_hit,
1024
1576
  negativeCacheHit: data.negative_cache_hit,
1025
1577
  negativeCacheTtlSeconds: data.negative_cache_ttl_seconds,
1026
1578
  cacheTier: data.cache_tier,
1027
- freshEnrichmentUsed: data.fresh_enrichment_used,
1028
- liveProviderCalled: data.live_provider_called,
1029
- openrouterCalled: data.openrouter_called,
1030
- byoOpenrouterUsed: data.byo_openrouter_used,
1579
+ freshEnrichmentUsed: typeof data.fresh_enrichment_used === "boolean" ? data.fresh_enrichment_used : billingMetadata?.fresh_enrichment_used,
1580
+ liveProviderCalled: typeof data.live_provider_called === "boolean" ? data.live_provider_called : billingMetadata?.live_provider_called,
1581
+ openrouterCalled: typeof data.openrouter_called === "boolean" ? data.openrouter_called : billingMetadata?.openrouter_called,
1582
+ byoOpenrouterUsed: typeof data.byo_openrouter_used === "boolean" ? data.byo_openrouter_used : billingMetadata?.byo_openrouter_used,
1031
1583
  error: processing || found ? void 0 : coreProductErrorMessage(data) ?? "No verified email found",
1032
1584
  errorCode: processing || found ? void 0 : coreProductErrorCode(data) ?? "EMAIL_NOT_FOUND",
1033
1585
  status: processing ? "processing" : "completed",
1034
1586
  runId: data.run_id,
1035
1587
  nextPollAfterSeconds: data.next_poll_after_seconds,
1036
1588
  suggestedAction: data.suggested_action,
1589
+ cachePublicationStatus: data.cache_publication_status,
1590
+ cachePublicationRetryEligible: data.cache_publication_retry_eligible,
1037
1591
  raw: data
1038
1592
  };
1039
1593
  }
1040
1594
  function normalizeVerifyEmailResult(email, data) {
1041
1595
  const failed = coreProductResponseFailed(data);
1042
- const verificationStatus = String(data.verification_status ?? "").toLowerCase();
1043
- const verified = !failed && (data.email_verified === true || ["valid", "deliverable"].includes(verificationStatus));
1044
1596
  const malformed = data.is_malformed === true || data.verification_verdict === "malformed" || data.verification_status === "malformed" || data.deliverability_status === "malformed";
1597
+ const verificationStatus = String(
1598
+ failed ? malformed ? "malformed" : "error" : data.verification_status ?? data.verification_verdict ?? data.deliverability_status ?? "unknown"
1599
+ ).toLowerCase();
1600
+ const deliverabilityStatus = String(data.deliverability_status ?? verificationStatus).toLowerCase();
1601
+ const verified = !failed && (data.email_verified === true || ["valid", "deliverable"].includes(verificationStatus));
1602
+ const isCatchAll = data.email_is_catch_all ?? data.is_catch_all ?? data.catch_all ?? data.diagnostics?.is_catch_all ?? false;
1603
+ const verifiedForSending = !failed && data.verified_for_sending === true;
1604
+ const verificationRunId = data.verification_run_id ?? data.run_id;
1045
1605
  return {
1046
1606
  success: !failed,
1047
1607
  error: failed ? coreProductErrorMessage(data) : void 0,
1048
1608
  errorCode: failed ? coreProductErrorCode(data) : void 0,
1049
1609
  email: data.email ?? email,
1050
1610
  status: !failed && String(data.status || "").toLowerCase() === "processing" ? "processing" : "completed",
1051
- verificationRunId: data.verification_run_id ?? data.run_id,
1611
+ verificationRunId,
1052
1612
  retryAfterMs: data.retry_after_ms,
1053
1613
  nextPollAfterSeconds: data.next_poll_after_seconds,
1054
1614
  isValid: failed ? false : data.is_valid ?? data.valid ?? verified,
1055
1615
  isDeliverable: failed ? false : data.is_deliverable ?? data.deliverable ?? verified,
1056
1616
  isMalformed: malformed,
1057
- isCatchAll: data.email_is_catch_all ?? data.is_catch_all ?? data.catch_all ?? data.diagnostics?.is_catch_all ?? false,
1058
- verifiedForSending: !failed && data.verified_for_sending === true,
1617
+ isCatchAll,
1618
+ verifiedForSending,
1619
+ verificationStatus,
1620
+ deliverabilityStatus,
1059
1621
  verificationVerdict: failed ? malformed ? "malformed" : "error" : data.verification_verdict ?? data.verification_status ?? "unknown",
1622
+ isRoleAccount: data.is_role_account === true || data.email_is_role_account === true || data.role === true,
1623
+ quality: typeof data.quality === "string" ? data.quality : null,
1624
+ recommendation: typeof data.recommendation === "string" ? data.recommendation : verifiedForSending ? "send" : ["unknown", "verifier_error"].includes(verificationStatus) ? "manual_review" : "do_not_send",
1625
+ smtpStatus: data.smtp_status ?? (isCatchAll ? "accept_all" : verifiedForSending ? "accepted" : "not_checked"),
1626
+ providerStatus: data.provider_status ?? verificationStatus,
1627
+ failureReason: data.failure_reason ?? (verifiedForSending ? "none" : isCatchAll ? "catch_all_domain_not_send_safe" : "verification_not_send_safe"),
1060
1628
  confidenceScore: failed ? 0 : data.confidence_score ?? data.confidence ?? data.diagnostics?.confidence_score ?? (verified ? 1 : 0),
1061
1629
  provider: data.provider,
1062
1630
  verificationSource: data.verification_source,
1631
+ verificationObservedAt: typeof data.verification_observed_at === "string" ? data.verification_observed_at : void 0,
1063
1632
  billingReplayed: data.billing_replayed,
1064
1633
  creditsUsed: data.credits_used,
1065
1634
  billingMetadata: data.billing_metadata,
1635
+ historicalBillingMetadata: data.historical_billing_metadata,
1066
1636
  resultReturnedFrom: data.result_returned_from,
1067
1637
  cacheHit: data.cache_hit,
1068
1638
  negativeCacheHit: data.negative_cache_hit,
@@ -1072,6 +1642,11 @@ function normalizeVerifyEmailResult(email, data) {
1072
1642
  liveProviderCalled: data.live_provider_called,
1073
1643
  openrouterCalled: data.openrouter_called,
1074
1644
  byoOpenrouterUsed: data.byo_openrouter_used,
1645
+ estimatedExternalCostUsd: data.estimated_external_cost_usd,
1646
+ runId: data.run_id ?? verificationRunId,
1647
+ cachePublicationStatus: data.cache_publication_status,
1648
+ cachePublicationRetryEligible: data.cache_publication_retry_eligible,
1649
+ suggestedAction: data.suggested_action,
1075
1650
  raw: data
1076
1651
  };
1077
1652
  }
@@ -1099,6 +1674,54 @@ function companySignalRequestBody(params) {
1099
1674
  idempotency_key: params.idempotencyKey
1100
1675
  });
1101
1676
  }
1677
+ function companySignalParamsFromRecoveredRow(row) {
1678
+ return {
1679
+ companyDomain: row.company?.domain ?? row.company_domain,
1680
+ companyName: row.company?.name ?? row.company_name,
1681
+ signalRunId: row.signal_run_id ?? row.run_id
1682
+ };
1683
+ }
1684
+ var COMPANY_SIGNAL_LARGE_BATCH_CONTROL_FIELDS = [
1685
+ "research_prompt",
1686
+ "signal_types",
1687
+ "target_signal_count",
1688
+ "lookback_days",
1689
+ "online",
1690
+ "enable_deep_search",
1691
+ "search_mode",
1692
+ "include_summary",
1693
+ "enable_outreach_intelligence",
1694
+ "enable_predictive_intelligence",
1695
+ "skip_cache",
1696
+ "include_candidate_evidence"
1697
+ ];
1698
+ function companySignalLargeBatchSubmission(requests) {
1699
+ if (requests.length <= 25) return null;
1700
+ if (requests.some(
1701
+ (request) => typeof request.signal_run_id === "string" && request.signal_run_id.trim() || typeof request.idempotency_key === "string" && request.idempotency_key.trim()
1702
+ )) {
1703
+ return null;
1704
+ }
1705
+ const submissionFields = Object.fromEntries(
1706
+ COMPANY_SIGNAL_LARGE_BATCH_CONTROL_FIELDS.filter((field) => requests[0]?.[field] !== void 0).map((field) => [field, requests[0][field]])
1707
+ );
1708
+ const controlKey = JSON.stringify(submissionFields);
1709
+ for (const request of requests) {
1710
+ const requestControls = Object.fromEntries(
1711
+ COMPANY_SIGNAL_LARGE_BATCH_CONTROL_FIELDS.filter((field) => request[field] !== void 0).map((field) => [field, request[field]])
1712
+ );
1713
+ if (JSON.stringify(requestControls) !== controlKey) return null;
1714
+ const hasIdentity = typeof request.company_domain === "string" && request.company_domain.trim() || typeof request.company_name === "string" && request.company_name.trim();
1715
+ if (!hasIdentity) return null;
1716
+ }
1717
+ return {
1718
+ requests: requests.map((request) => compact({
1719
+ company_domain: request.company_domain,
1720
+ company_name: request.company_name
1721
+ })),
1722
+ submissionFields
1723
+ };
1724
+ }
1102
1725
  function normalizeCompanySignalResult(params, data) {
1103
1726
  const failed = coreProductResponseFailed(data);
1104
1727
  const rawSignals = failed ? [] : data.signals ?? data.signal_feed ?? [];
@@ -1349,6 +1972,9 @@ function dedupeExactBatchItems(items, keyForItem) {
1349
1972
  function normalizedBatchText(value) {
1350
1973
  return typeof value === "string" ? value.trim().toLowerCase().replace(/\s+/g, " ") : "";
1351
1974
  }
1975
+ function normalizedBatchIdempotencyKey(value) {
1976
+ return typeof value === "string" ? value.trim() : "";
1977
+ }
1352
1978
  function normalizedBatchDomain(value) {
1353
1979
  return normalizedBatchText(value).replace(/^https?:\/\//, "").replace(/^www\./, "").split(/[/?#]/, 1)[0].replace(/\.+$/, "");
1354
1980
  }
@@ -1372,7 +1998,8 @@ function coreProductBatchRequestKey(path, request) {
1372
1998
  normalizedBatchDomain(request.company_domain),
1373
1999
  normalizedBatchLinkedIn(request.linkedin_url),
1374
2000
  normalizedBatchText(request.company_name),
1375
- request.skip_cache === true || request.bypass_cache === true
2001
+ request.skip_cache === true || request.bypass_cache === true,
2002
+ normalizedBatchIdempotencyKey(request.idempotency_key)
1376
2003
  ]);
1377
2004
  }
1378
2005
  if (path === "api/v1/verify-email") {
@@ -1380,7 +2007,8 @@ function coreProductBatchRequestKey(path, request) {
1380
2007
  normalizedBatchText(request.email),
1381
2008
  request.skip_cache === true || request.bypass_cache === true,
1382
2009
  request.skip_catch_all_verification === true,
1383
- request.skip_esp_check === true
2010
+ request.skip_esp_check === true,
2011
+ normalizedBatchIdempotencyKey(request.idempotency_key)
1384
2012
  ]);
1385
2013
  }
1386
2014
  if (path === "api/v1/company-signals") {
@@ -1402,7 +2030,8 @@ function coreProductBatchRequestKey(path, request) {
1402
2030
  normalizedBatchText(request.search_mode || "balanced"),
1403
2031
  request.enable_outreach_intelligence === true,
1404
2032
  request.enable_predictive_intelligence === true,
1405
- request.skip_cache === true || request.bypass_cache === true
2033
+ request.skip_cache === true || request.bypass_cache === true,
2034
+ normalizedBatchIdempotencyKey(request.idempotency_key)
1406
2035
  ]);
1407
2036
  }
1408
2037
  return JSON.stringify([
@@ -1411,9 +2040,11 @@ function coreProductBatchRequestKey(path, request) {
1411
2040
  typeof request.title === "string" ? request.title.trim() : "",
1412
2041
  typeof request.campaign_offer === "string" ? request.campaign_offer.trim() : "",
1413
2042
  typeof request.research_prompt === "string" ? request.research_prompt.trim() : "",
2043
+ Number(request.lookback_days) || null,
1414
2044
  typeof request.signal_run_id === "string" ? request.signal_run_id.trim() : "",
1415
2045
  request.skip_cache === true,
1416
- request.enable_deep_search === true
2046
+ request.enable_deep_search === true,
2047
+ normalizedBatchIdempotencyKey(request.idempotency_key)
1417
2048
  ]);
1418
2049
  }
1419
2050
  function finalizeCoreBatch(total, startedAt, round, normalize, options) {
@@ -1451,6 +2082,37 @@ function finalizeCoreBatch(total, startedAt, round, normalize, options) {
1451
2082
  results: compactedResults
1452
2083
  };
1453
2084
  }
2085
+ function finalizeRecoveredCoreBatch(total, startedAt, round, normalize, options) {
2086
+ const results = round.map((item) => {
2087
+ const duplicateOf = item.raw ? recoveredDuplicateOf(item.raw) : void 0;
2088
+ if (duplicateOf !== void 0) return { index: item.index, success: true, duplicateOf };
2089
+ if (!item.success || !item.raw) {
2090
+ return batchItemFailure(
2091
+ item.index,
2092
+ item.error || "Signaliz batch item failed",
2093
+ item.raw ?? item
2094
+ );
2095
+ }
2096
+ try {
2097
+ return { index: item.index, success: true, data: normalize(item) };
2098
+ } catch (error) {
2099
+ return {
2100
+ index: item.index,
2101
+ success: false,
2102
+ error: error instanceof Error ? error.message : String(error)
2103
+ };
2104
+ }
2105
+ });
2106
+ const compactedResults = compactExactDuplicateResults(results, options?.compactDuplicates === true);
2107
+ const succeeded = compactedResults.filter((item) => item.success).length;
2108
+ return {
2109
+ total,
2110
+ succeeded,
2111
+ failed: total - succeeded,
2112
+ durationMs: Date.now() - startedAt,
2113
+ results: compactedResults
2114
+ };
2115
+ }
1454
2116
  function compactExactDuplicateResults(results, enabled) {
1455
2117
  if (!enabled) return results;
1456
2118
  const sourceIndexByRaw = /* @__PURE__ */ new WeakMap();
@@ -1466,6 +2128,19 @@ function compactExactDuplicateResults(results, enabled) {
1466
2128
  return item;
1467
2129
  });
1468
2130
  }
2131
+ function compactKnownDuplicateBatch(batch, sourceIndexByInput, enabled) {
2132
+ if (!enabled) return batch;
2133
+ const canonicalInputByUniqueIndex = /* @__PURE__ */ new Map();
2134
+ sourceIndexByInput.forEach((uniqueIndex, inputIndex) => {
2135
+ if (!canonicalInputByUniqueIndex.has(uniqueIndex)) canonicalInputByUniqueIndex.set(uniqueIndex, inputIndex);
2136
+ });
2137
+ const results = batch.results.map((item, index) => {
2138
+ const canonicalIndex = canonicalInputByUniqueIndex.get(sourceIndexByInput[index]) ?? index;
2139
+ if (canonicalIndex === index || !item.success || !batch.results[canonicalIndex]?.success) return item;
2140
+ return { index, success: true, duplicateOf: canonicalIndex };
2141
+ });
2142
+ return { ...batch, results };
2143
+ }
1469
2144
  function signalToCopyRequestBody(params) {
1470
2145
  return compact({
1471
2146
  company_domain: params.companyDomain,
@@ -1482,6 +2157,28 @@ function signalToCopyRequestBody(params) {
1482
2157
  idempotency_key: params.idempotencyKey
1483
2158
  });
1484
2159
  }
2160
+ function validateLargeAvailableDataSignalCopyBatch(requests) {
2161
+ if (requests.length <= 25) return;
2162
+ const invalidIndex = requests.findIndex(
2163
+ (request) => request.skipCache === true || request.enableDeepSearch === true || Boolean(request.idempotencyKey?.trim()) || request.maxWaitMs !== void 0 || request.pollIntervalMs !== void 0 || request.waitForResult === false || !request.companyDomain?.trim() || !request.personName?.trim() || !request.title?.trim() || !request.campaignOffer?.trim()
2164
+ );
2165
+ if (invalidIndex >= 0) {
2166
+ throw new RangeError(
2167
+ `Signal to Copy batches larger than 25 are available-data-only; row ${invalidIndex} requests fresh/deep/synchronous controls or lacks required recipient fields. Split fresh requests into batches of at most 25.`
2168
+ );
2169
+ }
2170
+ }
2171
+ function availableDataSignalCopyRequestBody(params) {
2172
+ return compact({
2173
+ company_domain: params.companyDomain,
2174
+ person_name: params.personName,
2175
+ title: params.title,
2176
+ campaign_offer: params.campaignOffer,
2177
+ research_prompt: params.researchPrompt,
2178
+ lookback_days: params.lookbackDays,
2179
+ signal_run_id: params.signalRunId
2180
+ });
2181
+ }
1485
2182
  function validateSignalToCopyParams(params) {
1486
2183
  validateCoreProductMaxWaitMs(params.maxWaitMs);
1487
2184
  if (params.lookbackDays !== void 0 && (!Number.isInteger(params.lookbackDays) || params.lookbackDays < 1 || params.lookbackDays > 365)) {
@@ -1615,6 +2312,21 @@ async function waitForNextSignalPoll(data, override, startedAt, maxWaitMs) {
1615
2312
  function sleep2(ms) {
1616
2313
  return new Promise((resolve) => setTimeout(resolve, ms));
1617
2314
  }
2315
+ function assertRecoverableBatchResultsRetained(status, label, jobId, idempotencyKey) {
2316
+ if (status.results_expired !== true && status.results_cleaned !== true) return;
2317
+ throw new SignalizError({
2318
+ code: "BATCH_RESULTS_EXPIRED",
2319
+ message: `${label} batch ${jobId} completed, but its retained result pages have expired and cannot be recovered. Do not automatically resubmit provider work.`,
2320
+ errorType: "not_found",
2321
+ details: {
2322
+ job_id: jobId,
2323
+ ...idempotencyKey ? { idempotency_key: idempotencyKey } : {},
2324
+ suggested_action: "review_before_resubmitting",
2325
+ retry_eligible: false,
2326
+ results_expired: true
2327
+ }
2328
+ });
2329
+ }
1618
2330
  async function runBatch(items, execute, options) {
1619
2331
  validateBatchSize(items);
1620
2332
  const concurrency = validatedBatchConcurrency(options);