@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.
@@ -79,9 +79,13 @@ function publicRetryDetails(body) {
79
79
  ...body?.credits_used !== void 0 ? { credits_used: body.credits_used } : {},
80
80
  ...body?.credits_charged !== void 0 ? { credits_charged: body.credits_charged } : {},
81
81
  ...body?.job_id !== void 0 ? { job_id: body.job_id } : {},
82
+ ...body?.idempotency_key !== void 0 ? { idempotency_key: body.idempotency_key } : {},
82
83
  ...body?.verification_run_id !== void 0 ? { verification_run_id: body.verification_run_id } : {},
83
84
  ...body?.run_id !== void 0 ? { run_id: body.run_id } : {},
84
85
  ...body?.suggested_action !== void 0 ? { suggested_action: body.suggested_action } : {},
86
+ ...body?.do_not_auto_resubmit !== void 0 ? { do_not_auto_resubmit: body.do_not_auto_resubmit } : {},
87
+ ...body?.results_cleaned !== void 0 ? { results_cleaned: body.results_cleaned } : {},
88
+ ...body?.results_expired !== void 0 ? { results_expired: body.results_expired } : {},
85
89
  ...body?.signal_run_id !== void 0 ? { signal_run_id: body.signal_run_id } : {},
86
90
  ...body?.company_signal_run_id !== void 0 ? { company_signal_run_id: body.company_signal_run_id } : {},
87
91
  ...body?.resume_context_persisted !== void 0 ? { resume_context_persisted: body.resume_context_persisted } : {}
@@ -97,8 +101,9 @@ function mergePublicErrorDetails(body, explicitDetails) {
97
101
  }
98
102
  function mapErrorType(code, status) {
99
103
  if (code === "RATE_LIMITED" || status === 429) return "rate_limited";
100
- if (code === "VALIDATION_ERROR" || code === "SIGNAL_RUN_COMPANY_MISMATCH" || status === 400) return "validation";
104
+ if (code === "PAYLOAD_TOO_LARGE" || code === "VALIDATION_ERROR" || code === "SIGNAL_RUN_COMPANY_MISMATCH" || status === 400 || status === 413) return "validation";
101
105
  if (code === "NOT_FOUND" || status === 404) return "not_found";
106
+ if (code === "BATCH_RESULTS_EXPIRED" || status === 410) return "not_found";
102
107
  if (code === "NO_SUPPORTED_SIGNAL") return "not_found";
103
108
  if (code === "AUTH_EXPIRED" || status === 401) return "auth_expired";
104
109
  if (code === "INSUFFICIENT_CREDITS" || status === 402) return "insufficient_credits";
@@ -109,7 +114,7 @@ function mapErrorType(code, status) {
109
114
 
110
115
  // src/client.ts
111
116
  var DEFAULT_BASE_URL = "https://api.signaliz.com/functions/v1";
112
- var DEFAULT_TIMEOUT = 12e4;
117
+ var DEFAULT_TIMEOUT = 13e4;
113
118
  var DEFAULT_MAX_RETRIES = 3;
114
119
  var HttpClient = class {
115
120
  constructor(config) {
@@ -123,9 +128,9 @@ var HttpClient = class {
123
128
  throw new Error("Signaliz: provide either apiKey or clientId + clientSecret");
124
129
  }
125
130
  }
126
- async request(functionName, body, method = "POST") {
131
+ async request(functionName, body, method = "POST", options = {}) {
127
132
  let lastError;
128
- const idempotencyKey = method === "POST" ? requestIdempotencyKey(body) : void 0;
133
+ const idempotencyKey = method === "POST" ? requestIdempotencyKey(body, options.automaticIdempotency !== false) : void 0;
129
134
  for (let attempt = 0; attempt <= this.maxRetries; attempt++) {
130
135
  let timer;
131
136
  try {
@@ -193,8 +198,8 @@ var HttpClient = class {
193
198
  throw lastError || new Error("Request failed after retries");
194
199
  }
195
200
  /** Convenience wrapper for POST-based edge function calls */
196
- async post(functionName, body) {
197
- return this.request(functionName, body, "POST");
201
+ async post(functionName, body, options = {}) {
202
+ return this.request(functionName, body, "POST", options);
198
203
  }
199
204
  /** Convenience wrapper for GET-based edge function calls with query params */
200
205
  async get(functionName, query = {}) {
@@ -311,10 +316,12 @@ var HttpClient = class {
311
316
  return this.accessTokenPromise;
312
317
  }
313
318
  };
314
- function requestIdempotencyKey(body) {
319
+ function requestIdempotencyKey(body, automaticIdempotency) {
315
320
  const requested = typeof body.idempotency_key === "string" ? body.idempotency_key.trim() : "";
321
+ if (requested) return requested;
322
+ if (!automaticIdempotency) return void 0;
316
323
  const nonce = globalThis.crypto?.randomUUID?.() || `${Date.now()}-${Math.random().toString(16).slice(2)}`;
317
- return requested || `sdk_${nonce}`;
324
+ return `sdk_${nonce}`;
318
325
  }
319
326
  function sleep(ms) {
320
327
  return new Promise((r) => setTimeout(r, ms));
@@ -416,7 +423,7 @@ function mapMcpErrorType(error) {
416
423
  function mapErrorTypeFromCode(code) {
417
424
  if (!code) return "unknown";
418
425
  if (code === "RATE_LIMITED") return "rate_limited";
419
- 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";
426
+ 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";
420
427
  if (code === "AUTH_REQUIRED" || code === "AUTH_001") return "auth_expired";
421
428
  if (code === "QUOTA_EXCEEDED") return "insufficient_credits";
422
429
  if (code === "BUILD_NOT_READY" || code === "ARTIFACT_NOT_READY") return "not_found";
@@ -424,6 +431,166 @@ function mapErrorTypeFromCode(code) {
424
431
  return "unknown";
425
432
  }
426
433
 
434
+ // src/email-validation.ts
435
+ function isValidEmailString(value) {
436
+ if (typeof value !== "string") return false;
437
+ const email = value.trim();
438
+ if (!email || email.length > 254) return false;
439
+ const separator = email.indexOf("@");
440
+ if (separator <= 0 || separator !== email.lastIndexOf("@")) return false;
441
+ const local = email.slice(0, separator);
442
+ const domain = email.slice(separator + 1).toLowerCase();
443
+ if (local.length > 64 || domain.length > 253 || local.startsWith(".") || local.endsWith(".") || local.includes("..") || domain.startsWith(".") || domain.endsWith(".") || domain.includes("..")) return false;
444
+ if (!/^[A-Za-z0-9.!#$%&'*+/=?^_`{|}~-]+$/.test(local)) return false;
445
+ const labels = domain.split(".");
446
+ if (labels.length < 2) return false;
447
+ return labels.every(
448
+ (label) => label.length > 0 && label.length <= 63 && /^[a-z0-9-]+$/.test(label) && !label.startsWith("-") && !label.endsWith("-")
449
+ );
450
+ }
451
+
452
+ // src/core-product-input-bounds.ts
453
+ var LARGE_BATCH_MAX_ENCODED_BYTES = 8 * 1024 * 1024;
454
+ var DOMAIN_MAX_UTF8_BYTES = 253;
455
+ var NAME_MAX_UTF8_BYTES = 500;
456
+ var TITLE_MAX_UTF8_BYTES = 500;
457
+ var ID_MAX_UTF8_BYTES = 200;
458
+ var RESEARCH_PROMPT_MAX_UTF8_BYTES = 2e3;
459
+ var CAMPAIGN_OFFER_MAX_UTF8_BYTES = 4e3;
460
+ var SIGNAL_TYPE_MAX_UTF8_BYTES = 100;
461
+ var UTF8_ENCODER = new TextEncoder();
462
+ function byteLength(value) {
463
+ return UTF8_ENCODER.encode(value).byteLength;
464
+ }
465
+ function payloadTooLarge(message, details) {
466
+ throw new SignalizError({
467
+ code: "PAYLOAD_TOO_LARGE",
468
+ message,
469
+ errorType: "validation",
470
+ details: {
471
+ ...details,
472
+ retry_eligible: false,
473
+ credits_used: 0,
474
+ credits_charged: 0
475
+ }
476
+ });
477
+ }
478
+ function assertStringBytes(value, field, maxBytes, index) {
479
+ if (typeof value !== "string") return;
480
+ const encodedBytes = byteLength(value);
481
+ if (encodedBytes <= maxBytes) return;
482
+ payloadTooLarge(
483
+ `${field} is ${encodedBytes} UTF-8 bytes; maximum is ${maxBytes} bytes`,
484
+ {
485
+ field,
486
+ ...index !== void 0 ? { index } : {},
487
+ encoded_bytes: encodedBytes,
488
+ max_bytes: maxBytes
489
+ }
490
+ );
491
+ }
492
+ function assertCompanySignalRows(body) {
493
+ const requests = Array.isArray(body.requests) ? body.requests : [];
494
+ assertStringBytes(
495
+ body.research_prompt,
496
+ "research_prompt",
497
+ RESEARCH_PROMPT_MAX_UTF8_BYTES
498
+ );
499
+ if (Array.isArray(body.signal_types)) {
500
+ body.signal_types.forEach(
501
+ (signalType, index) => assertStringBytes(
502
+ signalType,
503
+ `signal_types[${index}]`,
504
+ SIGNAL_TYPE_MAX_UTF8_BYTES,
505
+ index
506
+ )
507
+ );
508
+ }
509
+ requests.forEach((value, index) => {
510
+ if (!value || typeof value !== "object" || Array.isArray(value)) return;
511
+ const request = value;
512
+ assertStringBytes(
513
+ request.company_domain,
514
+ `requests[${index}].company_domain`,
515
+ DOMAIN_MAX_UTF8_BYTES,
516
+ index
517
+ );
518
+ assertStringBytes(
519
+ request.company_name,
520
+ `requests[${index}].company_name`,
521
+ NAME_MAX_UTF8_BYTES,
522
+ index
523
+ );
524
+ });
525
+ }
526
+ function assertSignalCopyRows(body) {
527
+ const requests = Array.isArray(body.requests) ? body.requests : [];
528
+ requests.forEach((value, index) => {
529
+ if (!value || typeof value !== "object" || Array.isArray(value)) return;
530
+ const request = value;
531
+ assertStringBytes(
532
+ request.company_domain,
533
+ `requests[${index}].company_domain`,
534
+ DOMAIN_MAX_UTF8_BYTES,
535
+ index
536
+ );
537
+ assertStringBytes(
538
+ request.person_name,
539
+ `requests[${index}].person_name`,
540
+ NAME_MAX_UTF8_BYTES,
541
+ index
542
+ );
543
+ assertStringBytes(
544
+ request.title,
545
+ `requests[${index}].title`,
546
+ TITLE_MAX_UTF8_BYTES,
547
+ index
548
+ );
549
+ assertStringBytes(
550
+ request.campaign_offer,
551
+ `requests[${index}].campaign_offer`,
552
+ CAMPAIGN_OFFER_MAX_UTF8_BYTES,
553
+ index
554
+ );
555
+ assertStringBytes(
556
+ request.research_prompt,
557
+ `requests[${index}].research_prompt`,
558
+ RESEARCH_PROMPT_MAX_UTF8_BYTES,
559
+ index
560
+ );
561
+ assertStringBytes(
562
+ request.signal_run_id,
563
+ `requests[${index}].signal_run_id`,
564
+ ID_MAX_UTF8_BYTES,
565
+ index
566
+ );
567
+ });
568
+ }
569
+ function assertLargeCoreProductInputBounds(path2, body) {
570
+ const requests = Array.isArray(body.requests) ? body.requests : [];
571
+ if (requests.length <= 25) return;
572
+ assertStringBytes(body.idempotency_key, "idempotency_key", ID_MAX_UTF8_BYTES);
573
+ if (path2 === "api/v1/company-signals") {
574
+ assertCompanySignalRows(body);
575
+ } else {
576
+ assertSignalCopyRows(body);
577
+ }
578
+ const encoded = JSON.stringify(body);
579
+ if (typeof encoded !== "string") {
580
+ throw new TypeError("Core product batch input must be JSON serializable.");
581
+ }
582
+ const encodedBytes = byteLength(encoded);
583
+ if (encodedBytes <= LARGE_BATCH_MAX_ENCODED_BYTES) return;
584
+ payloadTooLarge(
585
+ `${path2 === "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`,
586
+ {
587
+ field: "requests",
588
+ encoded_bytes: encodedBytes,
589
+ max_bytes: LARGE_BATCH_MAX_ENCODED_BYTES
590
+ }
591
+ );
592
+ }
593
+
427
594
  // src/index.ts
428
595
  var MAX_ROW_RATE_LIMIT_RETRIES = 2;
429
596
  var Signaliz = class {
@@ -457,9 +624,25 @@ var Signaliz = class {
457
624
  options
458
625
  );
459
626
  }
627
+ async resumeFindEmailBatch(jobId, options) {
628
+ const startedAt = Date.now();
629
+ const { rows, total } = await this.readRecoverableCoreBatchJob(
630
+ "api/v1/find-email",
631
+ jobId,
632
+ "Find Email",
633
+ options
634
+ );
635
+ return finalizeCoreBatch(
636
+ total,
637
+ startedAt,
638
+ recoverableRowsToRound(rows, "Find Email"),
639
+ (item) => normalizeFindEmailResult(item.raw),
640
+ options
641
+ );
642
+ }
460
643
  async verifyEmail(email, options) {
461
644
  const normalizedEmail = typeof email === "string" ? email.trim() : "";
462
- if (options?.dryRun !== true && !options?.verificationRunId && normalizedEmail && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(normalizedEmail)) {
645
+ if (options?.dryRun !== true && !options?.verificationRunId && normalizedEmail && !isValidEmailString(normalizedEmail)) {
463
646
  const error = `Invalid email format: "${normalizedEmail}"`;
464
647
  return normalizeVerifyEmailResult(normalizedEmail, {
465
648
  success: false,
@@ -509,19 +692,20 @@ var Signaliz = class {
509
692
  }
510
693
  async verifyEmails(emails, options) {
511
694
  validateBatchSize(emails);
695
+ const requests = emails.map((email) => verifyEmailBatchRequestBody(email, options));
512
696
  if (options?.dryRun === true) {
513
697
  return this.runCoreProductDryRun(
514
698
  "api/v1/verify-email",
515
- emails.map((email) => ({ email, skip_cache: options.skipCache })),
699
+ requests,
516
700
  options.idempotencyKey
517
701
  );
518
702
  }
519
703
  const startedAt = Date.now();
520
704
  const round = await this.runCoreBatchRound(
521
705
  "api/v1/verify-email",
522
- emails.map((email, index) => ({
706
+ requests.map((request, index) => ({
523
707
  index,
524
- request: { email, skip_cache: options?.skipCache }
708
+ request
525
709
  })),
526
710
  options
527
711
  );
@@ -529,7 +713,23 @@ var Signaliz = class {
529
713
  emails.length,
530
714
  startedAt,
531
715
  round,
532
- (item) => normalizeVerifyEmailResult(emails[item.index], item.raw),
716
+ (item) => normalizeVerifyEmailResult(String(requests[item.index].email ?? ""), item.raw),
717
+ options
718
+ );
719
+ }
720
+ async resumeVerifyEmailBatch(jobId, options) {
721
+ const startedAt = Date.now();
722
+ const { rows, total } = await this.readRecoverableCoreBatchJob(
723
+ "api/v1/verify-email",
724
+ jobId,
725
+ "Verify Email",
726
+ options
727
+ );
728
+ return finalizeCoreBatch(
729
+ total,
730
+ startedAt,
731
+ recoverableRowsToRound(rows, "Verify Email"),
732
+ (item) => normalizeVerifyEmailResult(String(item.raw?.email || ""), item.raw),
533
733
  options
534
734
  );
535
735
  }
@@ -545,25 +745,92 @@ var Signaliz = class {
545
745
  validateSignalDiscoveryParams(params);
546
746
  const data = await this.client.post(
547
747
  "api/v1/signals",
548
- signalDiscoveryRequestBody(params)
748
+ signalDiscoveryRequestBody(params),
749
+ // Signals Everything owns a normalized, workspace-scoped automatic key
750
+ // at the API boundary so SDK, CLI, MCP, and direct REST calls coalesce.
751
+ { automaticIdempotency: false }
549
752
  );
550
753
  return normalizeSignalDiscoveryResult(params, data);
551
754
  }
552
755
  async enrichCompanies(companies, options) {
553
756
  validateBatchSize(companies);
554
757
  companies.forEach((params) => validateCoreProductMaxWaitMs(params.maxWaitMs));
758
+ const requests = companies.map(companySignalRequestBody);
759
+ if (companies.length > 25 && requests.some((request) => request.include_candidate_evidence === true)) {
760
+ throw new RangeError(
761
+ "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."
762
+ );
763
+ }
764
+ const largeBatch = companySignalLargeBatchSubmission(requests);
765
+ if (companies.length > 25 && !largeBatch) {
766
+ throw new RangeError(
767
+ "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."
768
+ );
769
+ }
555
770
  if (options?.dryRun === true) {
556
771
  return this.runCoreProductDryRun(
557
772
  "api/v1/company-signals",
558
- companies.map(companySignalRequestBody),
559
- options.idempotencyKey
773
+ largeBatch?.requests || requests,
774
+ options.idempotencyKey,
775
+ largeBatch?.submissionFields
560
776
  );
561
777
  }
562
778
  const startedAt = Date.now();
563
- const initialTasks = companies.map((params, index) => ({
564
- index,
565
- request: companySignalRequestBody(params)
566
- }));
779
+ if (options?.waitForResult === false) {
780
+ if (!largeBatch) {
781
+ throw new RangeError(
782
+ "Company Signals --no-wait requires more than 25 homogeneous new-enrichment rows; recovery reads and mixed controls must use batches of at most 25."
783
+ );
784
+ }
785
+ return this.submitRecoverableCoreBatchJob(
786
+ "api/v1/company-signals",
787
+ largeBatch.requests,
788
+ "Company Signals",
789
+ options.idempotencyKey,
790
+ companies.map((_, index) => index),
791
+ largeBatch.submissionFields
792
+ );
793
+ }
794
+ if (largeBatch) {
795
+ const job = await this.submitRecoverableCoreBatchJob(
796
+ "api/v1/company-signals",
797
+ largeBatch.requests,
798
+ "Company Signals",
799
+ options?.idempotencyKey,
800
+ companies.map((_, index) => index),
801
+ largeBatch.submissionFields
802
+ );
803
+ const { rows, total } = await this.readRecoverableCoreBatchJob(
804
+ "api/v1/company-signals",
805
+ job.jobId,
806
+ "Company Signals",
807
+ {
808
+ pageSize: 25,
809
+ maxWaitMs: options?.maxWaitMs,
810
+ pollIntervalMs: options?.pollIntervalMs,
811
+ idempotencyKey: job.idempotencyKey,
812
+ compactDuplicates: options?.compactDuplicates === true
813
+ }
814
+ );
815
+ if (total !== companies.length) {
816
+ throw new Error(`Company Signals batch returned ${total} results for ${companies.length} requests`);
817
+ }
818
+ const batch = finalizeCoreBatch(
819
+ companies.length,
820
+ startedAt,
821
+ recoverableRowsToRound(rows, "Company Signals"),
822
+ (item) => normalizeCompanySignalResult(companies[item.index], item.raw)
823
+ );
824
+ return compactKnownDuplicateBatch(
825
+ batch,
826
+ dedupeExactBatchItems(
827
+ requests,
828
+ (request) => coreProductBatchRequestKey("api/v1/company-signals", request)
829
+ ).sourceIndexByInput,
830
+ options?.compactDuplicates === true
831
+ );
832
+ }
833
+ const initialTasks = requests.map((request, index) => ({ index, request }));
567
834
  const round = await this.runCoreBatchRound("api/v1/company-signals", initialTasks, options);
568
835
  const results = round.map((item) => {
569
836
  if (!item.success || !item.raw) {
@@ -586,6 +853,22 @@ var Signaliz = class {
586
853
  results: compactedResults
587
854
  };
588
855
  }
856
+ async resumeCompanySignalBatch(jobId, options) {
857
+ const startedAt = Date.now();
858
+ const { rows, total } = await this.readRecoverableCoreBatchJob(
859
+ "api/v1/company-signals",
860
+ jobId,
861
+ "Company Signals",
862
+ options
863
+ );
864
+ return finalizeRecoveredCoreBatch(
865
+ total,
866
+ startedAt,
867
+ recoverableRowsToRound(rows, "Company Signals"),
868
+ (item) => normalizeCompanySignalResult(companySignalParamsFromRecoveredRow(item.raw), item.raw),
869
+ options
870
+ );
871
+ }
589
872
  async signalToCopy(params) {
590
873
  validateSignalToCopyParams(params);
591
874
  const request = signalToCopyRequestBody(params);
@@ -597,6 +880,7 @@ var Signaliz = class {
597
880
  async createSignalCopyBatch(requests, options) {
598
881
  validateBatchSize(requests);
599
882
  requests.forEach(validateSignalToCopyParams);
883
+ validateLargeAvailableDataSignalCopyBatch(requests);
600
884
  if (options?.dryRun === true) {
601
885
  return this.runCoreProductDryRun(
602
886
  "api/v1/signal-to-copy",
@@ -605,6 +889,59 @@ var Signaliz = class {
605
889
  );
606
890
  }
607
891
  const startedAt = Date.now();
892
+ if (requests.length > 25) {
893
+ if (options?.waitForResult === false) {
894
+ return this.submitRecoverableCoreBatchJob(
895
+ "api/v1/signal-to-copy",
896
+ requests.map(availableDataSignalCopyRequestBody),
897
+ "Signal to Copy",
898
+ options.idempotencyKey,
899
+ requests.map((_, index) => index)
900
+ );
901
+ }
902
+ const job = await this.submitRecoverableCoreBatchJob(
903
+ "api/v1/signal-to-copy",
904
+ requests.map(availableDataSignalCopyRequestBody),
905
+ "Signal to Copy",
906
+ options?.idempotencyKey,
907
+ requests.map((_, index) => index)
908
+ );
909
+ const { rows, total } = await this.readRecoverableCoreBatchJob(
910
+ "api/v1/signal-to-copy",
911
+ job.jobId,
912
+ "Signal to Copy",
913
+ {
914
+ pageSize: 100,
915
+ maxWaitMs: options?.maxWaitMs,
916
+ pollIntervalMs: options?.pollIntervalMs,
917
+ idempotencyKey: job.idempotencyKey,
918
+ compactDuplicates: options?.compactDuplicates === true
919
+ }
920
+ );
921
+ if (total !== requests.length) {
922
+ throw new Error(`Signal to Copy batch returned ${total} results for ${requests.length} requests`);
923
+ }
924
+ const results2 = recoverableSignalCopyRows(rows);
925
+ const succeeded2 = results2.filter((item) => item.success).length;
926
+ const batch = {
927
+ total: requests.length,
928
+ succeeded: succeeded2,
929
+ failed: requests.length - succeeded2,
930
+ durationMs: Date.now() - startedAt,
931
+ results: results2
932
+ };
933
+ return compactKnownDuplicateBatch(
934
+ batch,
935
+ dedupeExactBatchItems(
936
+ requests,
937
+ (params) => coreProductBatchRequestKey("api/v1/signal-to-copy", signalToCopyRequestBody(params))
938
+ ).sourceIndexByInput,
939
+ options?.compactDuplicates === true
940
+ );
941
+ }
942
+ if (options?.waitForResult === false) {
943
+ throw new RangeError("Signal to Copy --no-wait requires a durable batch of more than 25 rows.");
944
+ }
608
945
  const deduplicated = dedupeExactBatchItems(
609
946
  requests,
610
947
  (params) => coreProductBatchRequestKey("api/v1/signal-to-copy", signalToCopyRequestBody(params))
@@ -629,12 +966,36 @@ var Signaliz = class {
629
966
  results: compactedResults
630
967
  };
631
968
  }
632
- async runCoreProductDryRun(path2, requests, idempotencyKey) {
633
- const data = await this.client.post(path2, compact({
969
+ async resumeSignalCopyBatch(jobId, options) {
970
+ const startedAt = Date.now();
971
+ const { rows, total } = await this.readRecoverableCoreBatchJob(
972
+ "api/v1/signal-to-copy",
973
+ jobId,
974
+ "Signal to Copy",
975
+ options
976
+ );
977
+ const results = recoverableSignalCopyRows(rows);
978
+ const compactedResults = compactExactDuplicateResults(results, options?.compactDuplicates === true);
979
+ const succeeded = compactedResults.filter((item) => item.success).length;
980
+ return {
981
+ total,
982
+ succeeded,
983
+ failed: total - succeeded,
984
+ durationMs: Date.now() - startedAt,
985
+ results: compactedResults
986
+ };
987
+ }
988
+ async runCoreProductDryRun(path2, requests, idempotencyKey, defaults = {}) {
989
+ const requestBody = compact({
634
990
  requests,
991
+ ...defaults,
635
992
  dry_run: true,
636
993
  idempotency_key: idempotencyKey
637
- }));
994
+ });
995
+ if (path2 === "api/v1/company-signals" || path2 === "api/v1/signal-to-copy") {
996
+ assertLargeCoreProductInputBounds(path2, requestBody);
997
+ }
998
+ const data = await this.client.post(path2, requestBody);
638
999
  if (!isCoreProductDryRun(data)) {
639
1000
  throw new Error(`${path2} dry run returned a live-result contract`);
640
1001
  }
@@ -680,76 +1041,209 @@ var Signaliz = class {
680
1041
  }
681
1042
  return uniqueResults;
682
1043
  }
683
- async runRecoverableCoreBatchJob(path2, requests, label, pageSize = 500, maxWaitMs = 20 * 6e4, idempotencyKey, idempotencyItemIndices) {
684
- const startedAt = Date.now();
1044
+ async runRecoverableCoreBatchJob(path2, requests, label, pageSize = 500, maxWaitMs = 20 * 6e4, idempotencyKey, idempotencyItemIndices, submissionFields = {}) {
1045
+ const job = await this.submitRecoverableCoreBatchJob(
1046
+ path2,
1047
+ requests,
1048
+ label,
1049
+ idempotencyKey,
1050
+ idempotencyItemIndices,
1051
+ submissionFields
1052
+ );
1053
+ const recovered = await this.readRecoverableCoreBatchJob(path2, job.jobId, label, {
1054
+ pageSize,
1055
+ maxWaitMs,
1056
+ idempotencyKey: job.idempotencyKey
1057
+ });
1058
+ if (recovered.total !== requests.length) {
1059
+ throw new Error(`${label} batch returned ${recovered.total} results for ${requests.length} requests`);
1060
+ }
1061
+ return recovered.rows;
1062
+ }
1063
+ async submitRecoverableCoreBatchJob(path2, requests, label, idempotencyKey, idempotencyItemIndices, submissionFields = {}) {
685
1064
  const submissionIdempotencyKey = idempotencyKey?.trim() || newBatchIdempotencyKey(label);
1065
+ const requestBody = {
1066
+ ...submissionFields,
1067
+ requests,
1068
+ idempotency_key: submissionIdempotencyKey,
1069
+ batch_item_indices: idempotencyItemIndices
1070
+ };
1071
+ if (path2 === "api/v1/company-signals" || path2 === "api/v1/signal-to-copy") {
1072
+ assertLargeCoreProductInputBounds(path2, requestBody);
1073
+ }
686
1074
  let submission;
687
1075
  try {
688
- submission = await this.client.post(path2, {
689
- requests,
690
- idempotency_key: submissionIdempotencyKey,
691
- batch_item_indices: idempotencyItemIndices
692
- });
1076
+ submission = await this.client.post(path2, requestBody);
693
1077
  } catch (error) {
694
1078
  if (error instanceof SignalizError && !error.isRetryable) throw error;
695
1079
  const message = error instanceof Error ? error.message : String(error);
696
- throw new Error(
697
- `${label} batch submission was not confirmed. Retry with idempotency key "${submissionIdempotencyKey}" to recover the same job. ${message}`
698
- );
1080
+ throw new SignalizError({
1081
+ code: "BATCH_SUBMISSION_UNCONFIRMED",
1082
+ message: `${label} batch submission was not confirmed. Retry with idempotency key "${submissionIdempotencyKey}" to recover the same job. ${message}`,
1083
+ errorType: "provider_error",
1084
+ details: {
1085
+ idempotency_key: submissionIdempotencyKey,
1086
+ suggested_action: "retry_with_idempotency_key",
1087
+ retry_eligible: true
1088
+ }
1089
+ });
699
1090
  }
700
1091
  const jobId = String(submission.job_id || "").trim();
701
- if (!jobId) throw new Error(`${label} batch did not return a recoverable job_id`);
702
- let status = await this.client.post(path2, {
703
- job_id: jobId,
704
- page: 1,
705
- page_size: pageSize
706
- });
707
- while (!["completed", "partial", "failed"].includes(String(status.status || "").toLowerCase())) {
708
- if (Date.now() - startedAt >= maxWaitMs) {
709
- throw new Error(`${label} batch ${jobId} did not complete within ${maxWaitMs}ms`);
710
- }
711
- const retryAfterMs = Number(status.next_poll_after_seconds || 2) * 1e3;
712
- await sleep2(Math.min(Math.max(250, retryAfterMs), maxWaitMs - (Date.now() - startedAt)));
713
- status = await this.client.post(path2, {
1092
+ if (!jobId) {
1093
+ throw new SignalizError({
1094
+ code: "INVALID_JOB_RESPONSE",
1095
+ message: `${label} batch did not return a recoverable job_id`,
1096
+ errorType: "provider_error",
1097
+ details: {
1098
+ idempotency_key: submissionIdempotencyKey,
1099
+ suggested_action: "retry_with_idempotency_key",
1100
+ retry_eligible: true
1101
+ }
1102
+ });
1103
+ }
1104
+ const submissionStatus = String(submission.status || "").toLowerCase();
1105
+ const status = ["queued", "processing", "completed", "partial", "failed"].includes(submissionStatus) ? submissionStatus : "queued";
1106
+ return {
1107
+ success: true,
1108
+ status,
1109
+ capability: path2 === "api/v1/company-signals" ? "company_signals" : "signal_to_copy",
1110
+ jobId,
1111
+ idempotencyKey: String(submission.idempotency_key || submissionIdempotencyKey),
1112
+ total: Math.max(0, Number(submission.total ?? submission.items_total ?? requests.length)),
1113
+ ...Number.isFinite(Number(submission.next_poll_after_seconds)) ? {
1114
+ nextPollAfterSeconds: Number(submission.next_poll_after_seconds)
1115
+ } : {}
1116
+ };
1117
+ }
1118
+ async readRecoverableCoreBatchJob(path2, jobIdInput, label, options = {}) {
1119
+ const jobId = String(jobIdInput || "").trim();
1120
+ if (!jobId) throw new RangeError(`${label} jobId is required`);
1121
+ if (options.pageSize !== void 0 && (!Number.isFinite(options.pageSize) || options.pageSize <= 0)) {
1122
+ throw new RangeError("pageSize must be a positive finite number");
1123
+ }
1124
+ if (options.maxWaitMs !== void 0 && (!Number.isFinite(options.maxWaitMs) || options.maxWaitMs <= 0)) {
1125
+ throw new RangeError("maxWaitMs must be a positive finite number");
1126
+ }
1127
+ if (options.pollIntervalMs !== void 0 && (!Number.isFinite(options.pollIntervalMs) || options.pollIntervalMs <= 0)) {
1128
+ throw new RangeError("pollIntervalMs must be a positive finite number");
1129
+ }
1130
+ const maximumPageSize = path2 === "api/v1/signal-to-copy" ? 100 : 25;
1131
+ const pageSize = Math.min(maximumPageSize, Math.max(1, Math.trunc(options.pageSize ?? maximumPageSize)));
1132
+ const emailJob = path2 === "api/v1/find-email" || path2 === "api/v1/verify-email";
1133
+ const maxWaitMs = options.maxWaitMs ?? (emailJob ? 20 * 6e4 : void 0);
1134
+ const startedAt = Date.now();
1135
+ let observedIdempotencyKey = String(options.idempotencyKey || "").trim();
1136
+ try {
1137
+ let status = await this.client.post(path2, {
714
1138
  job_id: jobId,
715
1139
  page: 1,
716
- page_size: pageSize
1140
+ page_size: pageSize,
1141
+ compact_duplicates: options.compactDuplicates === true
717
1142
  });
718
- }
719
- const totalPages = Math.max(1, Number(status.total_pages || 1));
720
- const pages = [Array.isArray(status.results) ? status.results : []];
721
- if (totalPages > 1) {
722
- const pageNumbers = Array.from({ length: totalPages - 1 }, (_, index) => index + 2);
723
- const remainingPages = await runBatch(
724
- pageNumbers,
725
- async (page) => await this.client.post(path2, {
726
- job_id: jobId,
727
- page,
728
- page_size: pageSize
729
- }),
730
- { concurrency: Math.min(10, pageNumbers.length) }
1143
+ observedIdempotencyKey = String(status.idempotency_key || observedIdempotencyKey).trim();
1144
+ assertRecoverableBatchResultsRetained(
1145
+ status,
1146
+ label,
1147
+ jobId,
1148
+ observedIdempotencyKey
731
1149
  );
732
- for (const page of remainingPages.results) {
733
- if (!page.success || !page.data) {
734
- throw new Error(`${label} batch result page ${page.index + 2} failed: ${page.error || "unknown error"}`);
1150
+ while (!["completed", "partial", "failed"].includes(String(status.status || "").toLowerCase())) {
1151
+ if (maxWaitMs !== void 0 && Date.now() - startedAt >= maxWaitMs) {
1152
+ throw new SignalizError({
1153
+ code: "TIMEOUT",
1154
+ message: `${label} batch ${jobId} did not complete within ${maxWaitMs}ms`,
1155
+ errorType: "timeout",
1156
+ retryAfter: Number(status.next_poll_after_seconds || 2),
1157
+ details: {
1158
+ job_id: jobId,
1159
+ ...observedIdempotencyKey ? { idempotency_key: observedIdempotencyKey } : {},
1160
+ suggested_action: "check_job_status",
1161
+ retry_eligible: true
1162
+ }
1163
+ });
735
1164
  }
736
- pages.push(Array.isArray(page.data.results) ? page.data.results : []);
1165
+ const retryAfterMs = options.pollIntervalMs ?? Number(status.next_poll_after_seconds || 2) * 1e3;
1166
+ const boundedDelay = maxWaitMs === void 0 ? Math.max(250, retryAfterMs) : Math.min(Math.max(250, retryAfterMs), maxWaitMs - (Date.now() - startedAt));
1167
+ await sleep2(boundedDelay);
1168
+ status = await this.client.post(path2, {
1169
+ job_id: jobId,
1170
+ page: 1,
1171
+ page_size: pageSize,
1172
+ compact_duplicates: options.compactDuplicates === true
1173
+ });
1174
+ observedIdempotencyKey = String(status.idempotency_key || observedIdempotencyKey).trim();
1175
+ assertRecoverableBatchResultsRetained(
1176
+ status,
1177
+ label,
1178
+ jobId,
1179
+ observedIdempotencyKey
1180
+ );
737
1181
  }
738
- }
739
- const rows = pages.flat();
740
- if (rows.length !== requests.length) {
741
- throw new Error(`${label} batch returned ${rows.length} results for ${requests.length} requests`);
742
- }
743
- const seen = /* @__PURE__ */ new Set();
744
- for (const row of rows) {
745
- const index = Number(row.index);
746
- if (!Number.isInteger(index) || index < 0 || index >= requests.length || seen.has(index)) {
747
- throw new Error(`${label} batch returned an invalid result index`);
1182
+ const totalPages = Math.max(1, Number(status.total_pages || 1));
1183
+ const pages = [Array.isArray(status.results) ? status.results : []];
1184
+ if (totalPages > 1) {
1185
+ const pageNumbers = Array.from({ length: totalPages - 1 }, (_, index) => index + 2);
1186
+ const remainingPages = await runBatch(
1187
+ pageNumbers,
1188
+ async (page) => await this.client.post(path2, {
1189
+ job_id: jobId,
1190
+ page,
1191
+ page_size: pageSize,
1192
+ compact_duplicates: options.compactDuplicates === true
1193
+ }),
1194
+ { concurrency: Math.min(10, pageNumbers.length) }
1195
+ );
1196
+ for (const page of remainingPages.results) {
1197
+ if (!page.success || !page.data) {
1198
+ throw new Error(`${label} batch result page ${page.index + 2} failed: ${page.error || "unknown error"}`);
1199
+ }
1200
+ observedIdempotencyKey = String(
1201
+ page.data.idempotency_key || observedIdempotencyKey
1202
+ ).trim();
1203
+ assertRecoverableBatchResultsRetained(
1204
+ page.data,
1205
+ label,
1206
+ jobId,
1207
+ observedIdempotencyKey
1208
+ );
1209
+ pages.push(Array.isArray(page.data.results) ? page.data.results : []);
1210
+ }
1211
+ }
1212
+ const rows = pages.flat();
1213
+ const total = Math.max(0, Number(status.total_results ?? status.total ?? status.items_total ?? rows.length));
1214
+ if (rows.length !== total) {
1215
+ throw new Error(`${label} batch returned ${rows.length} results for ${total} job rows`);
1216
+ }
1217
+ const seen = /* @__PURE__ */ new Set();
1218
+ for (const row of rows) {
1219
+ const index = Number(row.index);
1220
+ if (!Number.isInteger(index) || index < 0 || seen.has(index)) {
1221
+ throw new Error(`${label} batch returned an invalid result index`);
1222
+ }
1223
+ seen.add(index);
1224
+ }
1225
+ if (seen.size !== total) throw new Error(`${label} batch returned incomplete indexed results`);
1226
+ return { rows, total };
1227
+ } catch (error) {
1228
+ if (error instanceof SignalizError && String(error.details?.job_id || "") === jobId) {
1229
+ throw error;
748
1230
  }
749
- seen.add(index);
1231
+ const signalizError = error instanceof SignalizError ? error : null;
1232
+ const message = error instanceof Error ? error.message : String(error);
1233
+ throw new SignalizError({
1234
+ code: signalizError?.code || "BATCH_RECOVERY_FAILED",
1235
+ message: `${label} batch ${jobId} could not be fully retrieved. Resume the existing job instead of resubmitting it. ${message}`,
1236
+ errorType: signalizError?.errorType || "provider_error",
1237
+ retryAfter: signalizError?.retryAfter,
1238
+ details: {
1239
+ ...signalizError?.details || {},
1240
+ job_id: jobId,
1241
+ ...observedIdempotencyKey ? { idempotency_key: observedIdempotencyKey } : {},
1242
+ suggested_action: "check_job_status",
1243
+ retry_eligible: true
1244
+ }
1245
+ });
750
1246
  }
751
- if (seen.size !== requests.length) throw new Error(`${label} batch returned incomplete indexed results`);
752
- return rows;
753
1247
  }
754
1248
  async runSignalCopyBatchChunk(requests, concurrency, rowRateLimitAttempt = 0, idempotencyKey, idempotencyItemIndices = requests.map((_, index) => index)) {
755
1249
  const results = new Array(requests.length);
@@ -814,11 +1308,11 @@ var Signaliz = class {
814
1308
  (task) => coreProductBatchRequestKey(path2, task.request)
815
1309
  );
816
1310
  const uniqueTasks = deduplicated.uniqueItems;
817
- const recoverableBatch = path2 === "api/v1/find-email" ? { label: "Find Email", pageSize: 500, maxWaitMs: 20 * 6e4 } : path2 === "api/v1/verify-email" ? { label: "Verify Email", pageSize: 500, maxWaitMs: 20 * 6e4 } : void 0;
1311
+ const recoverableBatch = path2 === "api/v1/find-email" ? { label: "Find Email", pageSize: 25, maxWaitMs: 20 * 6e4 } : path2 === "api/v1/verify-email" ? { label: "Verify Email", pageSize: 25, maxWaitMs: 20 * 6e4 } : void 0;
818
1312
  const containsFindRecoveryReads = path2 === "api/v1/find-email" && uniqueTasks.some(
819
1313
  (task) => typeof task.request.run_id === "string" && task.request.run_id.trim()
820
1314
  );
821
- if (path2 !== "api/v1/company-signals" && recoverableBatch && uniqueTasks.length > 25 && !containsFindRecoveryReads) {
1315
+ if (recoverableBatch && uniqueTasks.length > 25 && !containsFindRecoveryReads) {
822
1316
  const rows = await this.runRecoverableCoreBatchJob(
823
1317
  path2,
824
1318
  uniqueTasks.map((task) => task.request),
@@ -947,7 +1441,7 @@ function batchItemFailure(index, error, raw) {
947
1441
  const retryStrategy = raw?.retry_strategy ?? raw?.retryStrategy;
948
1442
  const creditsUsed = raw?.credits_used ?? raw?.creditsUsed;
949
1443
  const creditsCharged = raw?.credits_charged ?? raw?.creditsCharged;
950
- const runId = raw?.run_id ?? raw?.runId;
1444
+ const runId = [raw?.run_id, raw?.runId, raw?.signal_run_id, raw?.signalRunId].find((value) => typeof value === "string" && value.trim());
951
1445
  const verificationRunId = raw?.verification_run_id ?? raw?.verificationRunId;
952
1446
  const jobId = raw?.job_id ?? raw?.jobId;
953
1447
  const suggestedAction = raw?.suggested_action ?? raw?.suggestedAction;
@@ -970,6 +1464,42 @@ function batchItemFailure(index, error, raw) {
970
1464
  function recoverableJobRowFailed(row) {
971
1465
  return coreProductResponseFailed(row) || ["failed", "skipped"].includes(String(row._status || "").toLowerCase());
972
1466
  }
1467
+ function recoverableRowsToRound(rows, label) {
1468
+ return rows.map((raw) => {
1469
+ const index = Number(raw.index);
1470
+ const failed = recoverableJobRowFailed(raw);
1471
+ return {
1472
+ index,
1473
+ success: !failed,
1474
+ raw,
1475
+ error: failed ? raw.error || raw._error || raw.message || raw.failure_reason || `${label} item failed` : void 0
1476
+ };
1477
+ });
1478
+ }
1479
+ function recoverableSignalCopyRows(rows) {
1480
+ return rows.map((raw) => {
1481
+ const index = Number(raw.index);
1482
+ const duplicateOf = recoveredDuplicateOf(raw);
1483
+ if (duplicateOf !== void 0) return { index, success: true, duplicateOf };
1484
+ if (recoverableJobRowFailed(raw)) {
1485
+ return batchItemFailure(
1486
+ index,
1487
+ raw.error || raw._error || raw.message || "Signal to Copy item failed",
1488
+ raw
1489
+ );
1490
+ }
1491
+ try {
1492
+ assertTerminalSignalResponse(raw, "Signal to Copy");
1493
+ return { index, success: true, data: normalizeSignalToCopyResult(raw) };
1494
+ } catch (error) {
1495
+ return batchItemFailure(index, error instanceof Error ? error.message : String(error), raw);
1496
+ }
1497
+ });
1498
+ }
1499
+ function recoveredDuplicateOf(raw) {
1500
+ const value = Number(raw.duplicate_of ?? raw.duplicateOf ?? raw._duplicate_of);
1501
+ return Number.isInteger(value) && value >= 0 ? value : void 0;
1502
+ }
973
1503
  function newBatchIdempotencyKey(label) {
974
1504
  const nonce = globalThis.crypto?.randomUUID?.() || `${Date.now()}-${Math.random().toString(16).slice(2)}`;
975
1505
  return `signaliz-${label.toLowerCase().replace(/[^a-z0-9]+/g, "-")}-${nonce}`;
@@ -991,6 +1521,16 @@ function findEmailRequestBody(params) {
991
1521
  idempotency_key: params.idempotencyKey
992
1522
  });
993
1523
  }
1524
+ function verifyEmailBatchRequestBody(input, options) {
1525
+ if (typeof input === "string") {
1526
+ return compact({ email: input, skip_cache: options?.skipCache });
1527
+ }
1528
+ return compact({
1529
+ email: input.email,
1530
+ skip_cache: input.skipCache ?? options?.skipCache,
1531
+ idempotency_key: input.idempotencyKey
1532
+ });
1533
+ }
994
1534
  function isCoreProductDryRun(data) {
995
1535
  return data.dry_run === true && data.status === "planned";
996
1536
  }
@@ -1025,12 +1565,17 @@ function normalizeFindEmailResult(data) {
1025
1565
  const email = failed ? null : rawEmail;
1026
1566
  const found = Boolean(email) && data.found !== false;
1027
1567
  const verificationStatus = data.verification_status ?? (data.is_valid === true ? "valid" : data.status ?? "unknown");
1568
+ const deliverabilityStatus = data.deliverability_status ?? verificationStatus;
1569
+ const verificationVerdict = data.verification_verdict ?? verificationStatus;
1028
1570
  const rawFreshness = String(data.verification_freshness ?? "").toLowerCase();
1029
1571
  const verificationFreshness = rawFreshness === "fresh" || rawFreshness === "stale" ? rawFreshness : "unknown";
1030
1572
  const needsReverification = data.needs_reverification === true || verificationFreshness === "stale";
1031
1573
  const explicitlyNotSendSafe = data.verified_for_sending === false || needsReverification;
1032
1574
  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()));
1033
1575
  const verificationAgeDays = typeof data.verification_age_days === "number" && Number.isFinite(data.verification_age_days) ? data.verification_age_days : null;
1576
+ const providerUsed = data.provider_used ?? data.provider ?? data.email_source ?? data.source ?? "unknown";
1577
+ const verificationSource = data.verification_source ?? providerUsed;
1578
+ const billingMetadata = data.billing_metadata && typeof data.billing_metadata === "object" && !Array.isArray(data.billing_metadata) ? data.billing_metadata : void 0;
1034
1579
  return {
1035
1580
  success: processing || !failed && found,
1036
1581
  found,
@@ -1039,61 +1584,86 @@ function normalizeFindEmailResult(data) {
1039
1584
  lastName: data.last_name,
1040
1585
  confidence: failed ? 0 : data.confidence ?? data.confidence_score ?? (verified ? 1 : 0),
1041
1586
  verificationStatus: failed ? "error" : verificationStatus,
1587
+ deliverabilityStatus: failed ? "error" : deliverabilityStatus,
1588
+ verificationVerdict: failed ? "error" : verificationVerdict,
1042
1589
  isVerified: !failed && verified,
1043
1590
  isVerifiedForSending: !failed && !needsReverification && data.verified_for_sending === true,
1591
+ isDeliverable: !failed && !needsReverification && data.verified_for_sending === true,
1044
1592
  isCatchAll: data.email_is_catch_all === true || data.is_catch_all === true,
1045
1593
  verificationFreshness,
1046
1594
  verificationAgeDays,
1047
1595
  verifiedAt: typeof data.verified_at === "string" ? data.verified_at : null,
1048
1596
  needsReverification: failed || needsReverification,
1049
- providerUsed: data.provider_used ?? data.provider ?? data.email_source ?? data.source ?? "unknown",
1050
- creditsUsed: data.credits_used,
1051
- estimatedExternalCostUsd: data.estimated_external_cost_usd,
1052
- billingMetadata: data.billing_metadata,
1053
- resultReturnedFrom: data.result_returned_from,
1054
- cacheHit: data.cache_hit,
1597
+ providerUsed,
1598
+ verificationSource,
1599
+ verificationObservedAt: typeof data.verification_observed_at === "string" ? data.verification_observed_at : void 0,
1600
+ failureReason: data.failure_reason ?? (failed || !found ? "no_verified_email_found" : "none"),
1601
+ creditsUsed: data.credits_used ?? billingMetadata?.credits_used,
1602
+ estimatedExternalCostUsd: data.estimated_external_cost_usd ?? billingMetadata?.estimated_external_cost_usd,
1603
+ billingMetadata,
1604
+ historicalBillingMetadata: data.historical_billing_metadata,
1605
+ resultReturnedFrom: data.result_returned_from ?? billingMetadata?.result_returned_from,
1606
+ cacheHit: typeof data.cache_hit === "boolean" ? data.cache_hit : billingMetadata?.cache_hit,
1055
1607
  negativeCacheHit: data.negative_cache_hit,
1056
1608
  negativeCacheTtlSeconds: data.negative_cache_ttl_seconds,
1057
1609
  cacheTier: data.cache_tier,
1058
- freshEnrichmentUsed: data.fresh_enrichment_used,
1059
- liveProviderCalled: data.live_provider_called,
1060
- openrouterCalled: data.openrouter_called,
1061
- byoOpenrouterUsed: data.byo_openrouter_used,
1610
+ freshEnrichmentUsed: typeof data.fresh_enrichment_used === "boolean" ? data.fresh_enrichment_used : billingMetadata?.fresh_enrichment_used,
1611
+ liveProviderCalled: typeof data.live_provider_called === "boolean" ? data.live_provider_called : billingMetadata?.live_provider_called,
1612
+ openrouterCalled: typeof data.openrouter_called === "boolean" ? data.openrouter_called : billingMetadata?.openrouter_called,
1613
+ byoOpenrouterUsed: typeof data.byo_openrouter_used === "boolean" ? data.byo_openrouter_used : billingMetadata?.byo_openrouter_used,
1062
1614
  error: processing || found ? void 0 : coreProductErrorMessage(data) ?? "No verified email found",
1063
1615
  errorCode: processing || found ? void 0 : coreProductErrorCode(data) ?? "EMAIL_NOT_FOUND",
1064
1616
  status: processing ? "processing" : "completed",
1065
1617
  runId: data.run_id,
1066
1618
  nextPollAfterSeconds: data.next_poll_after_seconds,
1067
1619
  suggestedAction: data.suggested_action,
1620
+ cachePublicationStatus: data.cache_publication_status,
1621
+ cachePublicationRetryEligible: data.cache_publication_retry_eligible,
1068
1622
  raw: data
1069
1623
  };
1070
1624
  }
1071
1625
  function normalizeVerifyEmailResult(email, data) {
1072
1626
  const failed = coreProductResponseFailed(data);
1073
- const verificationStatus = String(data.verification_status ?? "").toLowerCase();
1074
- const verified = !failed && (data.email_verified === true || ["valid", "deliverable"].includes(verificationStatus));
1075
1627
  const malformed = data.is_malformed === true || data.verification_verdict === "malformed" || data.verification_status === "malformed" || data.deliverability_status === "malformed";
1628
+ const verificationStatus = String(
1629
+ failed ? malformed ? "malformed" : "error" : data.verification_status ?? data.verification_verdict ?? data.deliverability_status ?? "unknown"
1630
+ ).toLowerCase();
1631
+ const deliverabilityStatus = String(data.deliverability_status ?? verificationStatus).toLowerCase();
1632
+ const verified = !failed && (data.email_verified === true || ["valid", "deliverable"].includes(verificationStatus));
1633
+ const isCatchAll = data.email_is_catch_all ?? data.is_catch_all ?? data.catch_all ?? data.diagnostics?.is_catch_all ?? false;
1634
+ const verifiedForSending = !failed && data.verified_for_sending === true;
1635
+ const verificationRunId = data.verification_run_id ?? data.run_id;
1076
1636
  return {
1077
1637
  success: !failed,
1078
1638
  error: failed ? coreProductErrorMessage(data) : void 0,
1079
1639
  errorCode: failed ? coreProductErrorCode(data) : void 0,
1080
1640
  email: data.email ?? email,
1081
1641
  status: !failed && String(data.status || "").toLowerCase() === "processing" ? "processing" : "completed",
1082
- verificationRunId: data.verification_run_id ?? data.run_id,
1642
+ verificationRunId,
1083
1643
  retryAfterMs: data.retry_after_ms,
1084
1644
  nextPollAfterSeconds: data.next_poll_after_seconds,
1085
1645
  isValid: failed ? false : data.is_valid ?? data.valid ?? verified,
1086
1646
  isDeliverable: failed ? false : data.is_deliverable ?? data.deliverable ?? verified,
1087
1647
  isMalformed: malformed,
1088
- isCatchAll: data.email_is_catch_all ?? data.is_catch_all ?? data.catch_all ?? data.diagnostics?.is_catch_all ?? false,
1089
- verifiedForSending: !failed && data.verified_for_sending === true,
1648
+ isCatchAll,
1649
+ verifiedForSending,
1650
+ verificationStatus,
1651
+ deliverabilityStatus,
1090
1652
  verificationVerdict: failed ? malformed ? "malformed" : "error" : data.verification_verdict ?? data.verification_status ?? "unknown",
1653
+ isRoleAccount: data.is_role_account === true || data.email_is_role_account === true || data.role === true,
1654
+ quality: typeof data.quality === "string" ? data.quality : null,
1655
+ recommendation: typeof data.recommendation === "string" ? data.recommendation : verifiedForSending ? "send" : ["unknown", "verifier_error"].includes(verificationStatus) ? "manual_review" : "do_not_send",
1656
+ smtpStatus: data.smtp_status ?? (isCatchAll ? "accept_all" : verifiedForSending ? "accepted" : "not_checked"),
1657
+ providerStatus: data.provider_status ?? verificationStatus,
1658
+ failureReason: data.failure_reason ?? (verifiedForSending ? "none" : isCatchAll ? "catch_all_domain_not_send_safe" : "verification_not_send_safe"),
1091
1659
  confidenceScore: failed ? 0 : data.confidence_score ?? data.confidence ?? data.diagnostics?.confidence_score ?? (verified ? 1 : 0),
1092
1660
  provider: data.provider,
1093
1661
  verificationSource: data.verification_source,
1662
+ verificationObservedAt: typeof data.verification_observed_at === "string" ? data.verification_observed_at : void 0,
1094
1663
  billingReplayed: data.billing_replayed,
1095
1664
  creditsUsed: data.credits_used,
1096
1665
  billingMetadata: data.billing_metadata,
1666
+ historicalBillingMetadata: data.historical_billing_metadata,
1097
1667
  resultReturnedFrom: data.result_returned_from,
1098
1668
  cacheHit: data.cache_hit,
1099
1669
  negativeCacheHit: data.negative_cache_hit,
@@ -1103,6 +1673,11 @@ function normalizeVerifyEmailResult(email, data) {
1103
1673
  liveProviderCalled: data.live_provider_called,
1104
1674
  openrouterCalled: data.openrouter_called,
1105
1675
  byoOpenrouterUsed: data.byo_openrouter_used,
1676
+ estimatedExternalCostUsd: data.estimated_external_cost_usd,
1677
+ runId: data.run_id ?? verificationRunId,
1678
+ cachePublicationStatus: data.cache_publication_status,
1679
+ cachePublicationRetryEligible: data.cache_publication_retry_eligible,
1680
+ suggestedAction: data.suggested_action,
1106
1681
  raw: data
1107
1682
  };
1108
1683
  }
@@ -1130,6 +1705,54 @@ function companySignalRequestBody(params) {
1130
1705
  idempotency_key: params.idempotencyKey
1131
1706
  });
1132
1707
  }
1708
+ function companySignalParamsFromRecoveredRow(row) {
1709
+ return {
1710
+ companyDomain: row.company?.domain ?? row.company_domain,
1711
+ companyName: row.company?.name ?? row.company_name,
1712
+ signalRunId: row.signal_run_id ?? row.run_id
1713
+ };
1714
+ }
1715
+ var COMPANY_SIGNAL_LARGE_BATCH_CONTROL_FIELDS = [
1716
+ "research_prompt",
1717
+ "signal_types",
1718
+ "target_signal_count",
1719
+ "lookback_days",
1720
+ "online",
1721
+ "enable_deep_search",
1722
+ "search_mode",
1723
+ "include_summary",
1724
+ "enable_outreach_intelligence",
1725
+ "enable_predictive_intelligence",
1726
+ "skip_cache",
1727
+ "include_candidate_evidence"
1728
+ ];
1729
+ function companySignalLargeBatchSubmission(requests) {
1730
+ if (requests.length <= 25) return null;
1731
+ if (requests.some(
1732
+ (request) => typeof request.signal_run_id === "string" && request.signal_run_id.trim() || typeof request.idempotency_key === "string" && request.idempotency_key.trim()
1733
+ )) {
1734
+ return null;
1735
+ }
1736
+ const submissionFields = Object.fromEntries(
1737
+ COMPANY_SIGNAL_LARGE_BATCH_CONTROL_FIELDS.filter((field) => requests[0]?.[field] !== void 0).map((field) => [field, requests[0][field]])
1738
+ );
1739
+ const controlKey = JSON.stringify(submissionFields);
1740
+ for (const request of requests) {
1741
+ const requestControls = Object.fromEntries(
1742
+ COMPANY_SIGNAL_LARGE_BATCH_CONTROL_FIELDS.filter((field) => request[field] !== void 0).map((field) => [field, request[field]])
1743
+ );
1744
+ if (JSON.stringify(requestControls) !== controlKey) return null;
1745
+ const hasIdentity = typeof request.company_domain === "string" && request.company_domain.trim() || typeof request.company_name === "string" && request.company_name.trim();
1746
+ if (!hasIdentity) return null;
1747
+ }
1748
+ return {
1749
+ requests: requests.map((request) => compact({
1750
+ company_domain: request.company_domain,
1751
+ company_name: request.company_name
1752
+ })),
1753
+ submissionFields
1754
+ };
1755
+ }
1133
1756
  function normalizeCompanySignalResult(params, data) {
1134
1757
  const failed = coreProductResponseFailed(data);
1135
1758
  const rawSignals = failed ? [] : data.signals ?? data.signal_feed ?? [];
@@ -1380,6 +2003,9 @@ function dedupeExactBatchItems(items, keyForItem) {
1380
2003
  function normalizedBatchText(value) {
1381
2004
  return typeof value === "string" ? value.trim().toLowerCase().replace(/\s+/g, " ") : "";
1382
2005
  }
2006
+ function normalizedBatchIdempotencyKey(value) {
2007
+ return typeof value === "string" ? value.trim() : "";
2008
+ }
1383
2009
  function normalizedBatchDomain(value) {
1384
2010
  return normalizedBatchText(value).replace(/^https?:\/\//, "").replace(/^www\./, "").split(/[/?#]/, 1)[0].replace(/\.+$/, "");
1385
2011
  }
@@ -1403,7 +2029,8 @@ function coreProductBatchRequestKey(path2, request) {
1403
2029
  normalizedBatchDomain(request.company_domain),
1404
2030
  normalizedBatchLinkedIn(request.linkedin_url),
1405
2031
  normalizedBatchText(request.company_name),
1406
- request.skip_cache === true || request.bypass_cache === true
2032
+ request.skip_cache === true || request.bypass_cache === true,
2033
+ normalizedBatchIdempotencyKey(request.idempotency_key)
1407
2034
  ]);
1408
2035
  }
1409
2036
  if (path2 === "api/v1/verify-email") {
@@ -1411,7 +2038,8 @@ function coreProductBatchRequestKey(path2, request) {
1411
2038
  normalizedBatchText(request.email),
1412
2039
  request.skip_cache === true || request.bypass_cache === true,
1413
2040
  request.skip_catch_all_verification === true,
1414
- request.skip_esp_check === true
2041
+ request.skip_esp_check === true,
2042
+ normalizedBatchIdempotencyKey(request.idempotency_key)
1415
2043
  ]);
1416
2044
  }
1417
2045
  if (path2 === "api/v1/company-signals") {
@@ -1433,7 +2061,8 @@ function coreProductBatchRequestKey(path2, request) {
1433
2061
  normalizedBatchText(request.search_mode || "balanced"),
1434
2062
  request.enable_outreach_intelligence === true,
1435
2063
  request.enable_predictive_intelligence === true,
1436
- request.skip_cache === true || request.bypass_cache === true
2064
+ request.skip_cache === true || request.bypass_cache === true,
2065
+ normalizedBatchIdempotencyKey(request.idempotency_key)
1437
2066
  ]);
1438
2067
  }
1439
2068
  return JSON.stringify([
@@ -1442,9 +2071,11 @@ function coreProductBatchRequestKey(path2, request) {
1442
2071
  typeof request.title === "string" ? request.title.trim() : "",
1443
2072
  typeof request.campaign_offer === "string" ? request.campaign_offer.trim() : "",
1444
2073
  typeof request.research_prompt === "string" ? request.research_prompt.trim() : "",
2074
+ Number(request.lookback_days) || null,
1445
2075
  typeof request.signal_run_id === "string" ? request.signal_run_id.trim() : "",
1446
2076
  request.skip_cache === true,
1447
- request.enable_deep_search === true
2077
+ request.enable_deep_search === true,
2078
+ normalizedBatchIdempotencyKey(request.idempotency_key)
1448
2079
  ]);
1449
2080
  }
1450
2081
  function finalizeCoreBatch(total, startedAt, round, normalize, options) {
@@ -1482,6 +2113,37 @@ function finalizeCoreBatch(total, startedAt, round, normalize, options) {
1482
2113
  results: compactedResults
1483
2114
  };
1484
2115
  }
2116
+ function finalizeRecoveredCoreBatch(total, startedAt, round, normalize, options) {
2117
+ const results = round.map((item) => {
2118
+ const duplicateOf = item.raw ? recoveredDuplicateOf(item.raw) : void 0;
2119
+ if (duplicateOf !== void 0) return { index: item.index, success: true, duplicateOf };
2120
+ if (!item.success || !item.raw) {
2121
+ return batchItemFailure(
2122
+ item.index,
2123
+ item.error || "Signaliz batch item failed",
2124
+ item.raw ?? item
2125
+ );
2126
+ }
2127
+ try {
2128
+ return { index: item.index, success: true, data: normalize(item) };
2129
+ } catch (error) {
2130
+ return {
2131
+ index: item.index,
2132
+ success: false,
2133
+ error: error instanceof Error ? error.message : String(error)
2134
+ };
2135
+ }
2136
+ });
2137
+ const compactedResults = compactExactDuplicateResults(results, options?.compactDuplicates === true);
2138
+ const succeeded = compactedResults.filter((item) => item.success).length;
2139
+ return {
2140
+ total,
2141
+ succeeded,
2142
+ failed: total - succeeded,
2143
+ durationMs: Date.now() - startedAt,
2144
+ results: compactedResults
2145
+ };
2146
+ }
1485
2147
  function compactExactDuplicateResults(results, enabled) {
1486
2148
  if (!enabled) return results;
1487
2149
  const sourceIndexByRaw = /* @__PURE__ */ new WeakMap();
@@ -1497,6 +2159,19 @@ function compactExactDuplicateResults(results, enabled) {
1497
2159
  return item;
1498
2160
  });
1499
2161
  }
2162
+ function compactKnownDuplicateBatch(batch, sourceIndexByInput, enabled) {
2163
+ if (!enabled) return batch;
2164
+ const canonicalInputByUniqueIndex = /* @__PURE__ */ new Map();
2165
+ sourceIndexByInput.forEach((uniqueIndex, inputIndex) => {
2166
+ if (!canonicalInputByUniqueIndex.has(uniqueIndex)) canonicalInputByUniqueIndex.set(uniqueIndex, inputIndex);
2167
+ });
2168
+ const results = batch.results.map((item, index) => {
2169
+ const canonicalIndex = canonicalInputByUniqueIndex.get(sourceIndexByInput[index]) ?? index;
2170
+ if (canonicalIndex === index || !item.success || !batch.results[canonicalIndex]?.success) return item;
2171
+ return { index, success: true, duplicateOf: canonicalIndex };
2172
+ });
2173
+ return { ...batch, results };
2174
+ }
1500
2175
  function signalToCopyRequestBody(params) {
1501
2176
  return compact({
1502
2177
  company_domain: params.companyDomain,
@@ -1513,6 +2188,28 @@ function signalToCopyRequestBody(params) {
1513
2188
  idempotency_key: params.idempotencyKey
1514
2189
  });
1515
2190
  }
2191
+ function validateLargeAvailableDataSignalCopyBatch(requests) {
2192
+ if (requests.length <= 25) return;
2193
+ const invalidIndex = requests.findIndex(
2194
+ (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()
2195
+ );
2196
+ if (invalidIndex >= 0) {
2197
+ throw new RangeError(
2198
+ `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.`
2199
+ );
2200
+ }
2201
+ }
2202
+ function availableDataSignalCopyRequestBody(params) {
2203
+ return compact({
2204
+ company_domain: params.companyDomain,
2205
+ person_name: params.personName,
2206
+ title: params.title,
2207
+ campaign_offer: params.campaignOffer,
2208
+ research_prompt: params.researchPrompt,
2209
+ lookback_days: params.lookbackDays,
2210
+ signal_run_id: params.signalRunId
2211
+ });
2212
+ }
1516
2213
  function validateSignalToCopyParams(params) {
1517
2214
  validateCoreProductMaxWaitMs(params.maxWaitMs);
1518
2215
  if (params.lookbackDays !== void 0 && (!Number.isInteger(params.lookbackDays) || params.lookbackDays < 1 || params.lookbackDays > 365)) {
@@ -1646,6 +2343,21 @@ async function waitForNextSignalPoll(data, override, startedAt, maxWaitMs) {
1646
2343
  function sleep2(ms) {
1647
2344
  return new Promise((resolve) => setTimeout(resolve, ms));
1648
2345
  }
2346
+ function assertRecoverableBatchResultsRetained(status, label, jobId, idempotencyKey) {
2347
+ if (status.results_expired !== true && status.results_cleaned !== true) return;
2348
+ throw new SignalizError({
2349
+ code: "BATCH_RESULTS_EXPIRED",
2350
+ message: `${label} batch ${jobId} completed, but its retained result pages have expired and cannot be recovered. Do not automatically resubmit provider work.`,
2351
+ errorType: "not_found",
2352
+ details: {
2353
+ job_id: jobId,
2354
+ ...idempotencyKey ? { idempotency_key: idempotencyKey } : {},
2355
+ suggested_action: "review_before_resubmitting",
2356
+ retry_eligible: false,
2357
+ results_expired: true
2358
+ }
2359
+ });
2360
+ }
1649
2361
  async function runBatch(items, execute, options) {
1650
2362
  validateBatchSize(items);
1651
2363
  const concurrency = validatedBatchConcurrency(options);