@signaliz/sdk 1.0.61 → 1.0.63

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,206 @@ 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, pollIntervalMs, idempotencyKey, idempotencyItemIndices, submissionFields = {}) {
1045
+ validateCoreBatchPollingOptions({ maxWaitMs, pollIntervalMs });
1046
+ const job = await this.submitRecoverableCoreBatchJob(
1047
+ path2,
1048
+ requests,
1049
+ label,
1050
+ idempotencyKey,
1051
+ idempotencyItemIndices,
1052
+ submissionFields
1053
+ );
1054
+ const recovered = await this.readRecoverableCoreBatchJob(path2, job.jobId, label, {
1055
+ pageSize,
1056
+ maxWaitMs,
1057
+ pollIntervalMs,
1058
+ idempotencyKey: job.idempotencyKey
1059
+ });
1060
+ if (recovered.total !== requests.length) {
1061
+ throw new Error(`${label} batch returned ${recovered.total} results for ${requests.length} requests`);
1062
+ }
1063
+ return recovered.rows;
1064
+ }
1065
+ async submitRecoverableCoreBatchJob(path2, requests, label, idempotencyKey, idempotencyItemIndices, submissionFields = {}) {
685
1066
  const submissionIdempotencyKey = idempotencyKey?.trim() || newBatchIdempotencyKey(label);
1067
+ const requestBody = {
1068
+ ...submissionFields,
1069
+ requests,
1070
+ idempotency_key: submissionIdempotencyKey,
1071
+ batch_item_indices: idempotencyItemIndices
1072
+ };
1073
+ if (path2 === "api/v1/company-signals" || path2 === "api/v1/signal-to-copy") {
1074
+ assertLargeCoreProductInputBounds(path2, requestBody);
1075
+ }
686
1076
  let submission;
687
1077
  try {
688
- submission = await this.client.post(path2, {
689
- requests,
690
- idempotency_key: submissionIdempotencyKey,
691
- batch_item_indices: idempotencyItemIndices
692
- });
1078
+ submission = await this.client.post(path2, requestBody);
693
1079
  } catch (error) {
694
1080
  if (error instanceof SignalizError && !error.isRetryable) throw error;
695
1081
  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
- );
1082
+ throw new SignalizError({
1083
+ code: "BATCH_SUBMISSION_UNCONFIRMED",
1084
+ message: `${label} batch submission was not confirmed. Retry with idempotency key "${submissionIdempotencyKey}" to recover the same job. ${message}`,
1085
+ errorType: "provider_error",
1086
+ details: {
1087
+ idempotency_key: submissionIdempotencyKey,
1088
+ suggested_action: "retry_with_idempotency_key",
1089
+ retry_eligible: true
1090
+ }
1091
+ });
699
1092
  }
700
1093
  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, {
1094
+ if (!jobId) {
1095
+ throw new SignalizError({
1096
+ code: "INVALID_JOB_RESPONSE",
1097
+ message: `${label} batch did not return a recoverable job_id`,
1098
+ errorType: "provider_error",
1099
+ details: {
1100
+ idempotency_key: submissionIdempotencyKey,
1101
+ suggested_action: "retry_with_idempotency_key",
1102
+ retry_eligible: true
1103
+ }
1104
+ });
1105
+ }
1106
+ const submissionStatus = String(submission.status || "").toLowerCase();
1107
+ const status = ["queued", "processing", "completed", "partial", "failed"].includes(submissionStatus) ? submissionStatus : "queued";
1108
+ return {
1109
+ success: true,
1110
+ status,
1111
+ capability: path2 === "api/v1/company-signals" ? "company_signals" : "signal_to_copy",
1112
+ jobId,
1113
+ idempotencyKey: String(submission.idempotency_key || submissionIdempotencyKey),
1114
+ total: Math.max(0, Number(submission.total ?? submission.items_total ?? requests.length)),
1115
+ ...Number.isFinite(Number(submission.next_poll_after_seconds)) ? {
1116
+ nextPollAfterSeconds: Number(submission.next_poll_after_seconds)
1117
+ } : {}
1118
+ };
1119
+ }
1120
+ async readRecoverableCoreBatchJob(path2, jobIdInput, label, options = {}) {
1121
+ const jobId = String(jobIdInput || "").trim();
1122
+ if (!jobId) throw new RangeError(`${label} jobId is required`);
1123
+ if (options.pageSize !== void 0 && (!Number.isFinite(options.pageSize) || options.pageSize <= 0)) {
1124
+ throw new RangeError("pageSize must be a positive finite number");
1125
+ }
1126
+ validateCoreBatchPollingOptions(options);
1127
+ const maximumPageSize = path2 === "api/v1/signal-to-copy" ? 100 : 25;
1128
+ const pageSize = Math.min(maximumPageSize, Math.max(1, Math.trunc(options.pageSize ?? maximumPageSize)));
1129
+ const emailJob = path2 === "api/v1/find-email" || path2 === "api/v1/verify-email";
1130
+ const maxWaitMs = options.maxWaitMs ?? (emailJob ? 20 * 6e4 : void 0);
1131
+ const startedAt = Date.now();
1132
+ let observedIdempotencyKey = String(options.idempotencyKey || "").trim();
1133
+ try {
1134
+ let status = await this.client.post(path2, {
714
1135
  job_id: jobId,
715
1136
  page: 1,
716
- page_size: pageSize
1137
+ page_size: pageSize,
1138
+ compact_duplicates: options.compactDuplicates === true
717
1139
  });
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) }
1140
+ observedIdempotencyKey = String(status.idempotency_key || observedIdempotencyKey).trim();
1141
+ assertRecoverableBatchResultsRetained(
1142
+ status,
1143
+ label,
1144
+ jobId,
1145
+ observedIdempotencyKey
731
1146
  );
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"}`);
1147
+ while (!["completed", "partial", "failed"].includes(String(status.status || "").toLowerCase())) {
1148
+ if (maxWaitMs !== void 0 && Date.now() - startedAt >= maxWaitMs) {
1149
+ throw new SignalizError({
1150
+ code: "TIMEOUT",
1151
+ message: `${label} batch ${jobId} did not complete within ${maxWaitMs}ms`,
1152
+ errorType: "timeout",
1153
+ retryAfter: Number(status.next_poll_after_seconds || 2),
1154
+ details: {
1155
+ job_id: jobId,
1156
+ ...observedIdempotencyKey ? { idempotency_key: observedIdempotencyKey } : {},
1157
+ suggested_action: "check_job_status",
1158
+ retry_eligible: true
1159
+ }
1160
+ });
735
1161
  }
736
- pages.push(Array.isArray(page.data.results) ? page.data.results : []);
1162
+ const retryAfterMs = options.pollIntervalMs ?? Number(status.next_poll_after_seconds || 2) * 1e3;
1163
+ const boundedDelay = maxWaitMs === void 0 ? Math.max(250, retryAfterMs) : Math.min(Math.max(250, retryAfterMs), maxWaitMs - (Date.now() - startedAt));
1164
+ await sleep2(boundedDelay);
1165
+ status = await this.client.post(path2, {
1166
+ job_id: jobId,
1167
+ page: 1,
1168
+ page_size: pageSize,
1169
+ compact_duplicates: options.compactDuplicates === true
1170
+ });
1171
+ observedIdempotencyKey = String(status.idempotency_key || observedIdempotencyKey).trim();
1172
+ assertRecoverableBatchResultsRetained(
1173
+ status,
1174
+ label,
1175
+ jobId,
1176
+ observedIdempotencyKey
1177
+ );
737
1178
  }
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`);
1179
+ const totalPages = Math.max(1, Number(status.total_pages || 1));
1180
+ const pages = [Array.isArray(status.results) ? status.results : []];
1181
+ if (totalPages > 1) {
1182
+ const pageNumbers = Array.from({ length: totalPages - 1 }, (_, index) => index + 2);
1183
+ const remainingPages = await runBatch(
1184
+ pageNumbers,
1185
+ async (page) => await this.client.post(path2, {
1186
+ job_id: jobId,
1187
+ page,
1188
+ page_size: pageSize,
1189
+ compact_duplicates: options.compactDuplicates === true
1190
+ }),
1191
+ { concurrency: Math.min(10, pageNumbers.length) }
1192
+ );
1193
+ for (const page of remainingPages.results) {
1194
+ if (!page.success || !page.data) {
1195
+ throw new Error(`${label} batch result page ${page.index + 2} failed: ${page.error || "unknown error"}`);
1196
+ }
1197
+ observedIdempotencyKey = String(
1198
+ page.data.idempotency_key || observedIdempotencyKey
1199
+ ).trim();
1200
+ assertRecoverableBatchResultsRetained(
1201
+ page.data,
1202
+ label,
1203
+ jobId,
1204
+ observedIdempotencyKey
1205
+ );
1206
+ pages.push(Array.isArray(page.data.results) ? page.data.results : []);
1207
+ }
1208
+ }
1209
+ const rows = pages.flat();
1210
+ const total = Math.max(0, Number(status.total_results ?? status.total ?? status.items_total ?? rows.length));
1211
+ if (rows.length !== total) {
1212
+ throw new Error(`${label} batch returned ${rows.length} results for ${total} job rows`);
1213
+ }
1214
+ const seen = /* @__PURE__ */ new Set();
1215
+ for (const row of rows) {
1216
+ const index = Number(row.index);
1217
+ if (!Number.isInteger(index) || index < 0 || seen.has(index)) {
1218
+ throw new Error(`${label} batch returned an invalid result index`);
1219
+ }
1220
+ seen.add(index);
1221
+ }
1222
+ if (seen.size !== total) throw new Error(`${label} batch returned incomplete indexed results`);
1223
+ return { rows, total };
1224
+ } catch (error) {
1225
+ if (error instanceof SignalizError && String(error.details?.job_id || "") === jobId) {
1226
+ throw error;
748
1227
  }
749
- seen.add(index);
1228
+ const signalizError = error instanceof SignalizError ? error : null;
1229
+ const message = error instanceof Error ? error.message : String(error);
1230
+ throw new SignalizError({
1231
+ code: signalizError?.code || "BATCH_RECOVERY_FAILED",
1232
+ message: `${label} batch ${jobId} could not be fully retrieved. Resume the existing job instead of resubmitting it. ${message}`,
1233
+ errorType: signalizError?.errorType || "provider_error",
1234
+ retryAfter: signalizError?.retryAfter,
1235
+ details: {
1236
+ ...signalizError?.details || {},
1237
+ job_id: jobId,
1238
+ ...observedIdempotencyKey ? { idempotency_key: observedIdempotencyKey } : {},
1239
+ suggested_action: "check_job_status",
1240
+ retry_eligible: true
1241
+ }
1242
+ });
750
1243
  }
751
- if (seen.size !== requests.length) throw new Error(`${label} batch returned incomplete indexed results`);
752
- return rows;
753
1244
  }
754
1245
  async runSignalCopyBatchChunk(requests, concurrency, rowRateLimitAttempt = 0, idempotencyKey, idempotencyItemIndices = requests.map((_, index) => index)) {
755
1246
  const results = new Array(requests.length);
@@ -814,17 +1305,18 @@ var Signaliz = class {
814
1305
  (task) => coreProductBatchRequestKey(path2, task.request)
815
1306
  );
816
1307
  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;
1308
+ 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
1309
  const containsFindRecoveryReads = path2 === "api/v1/find-email" && uniqueTasks.some(
819
1310
  (task) => typeof task.request.run_id === "string" && task.request.run_id.trim()
820
1311
  );
821
- if (path2 !== "api/v1/company-signals" && recoverableBatch && uniqueTasks.length > 25 && !containsFindRecoveryReads) {
1312
+ if (recoverableBatch && uniqueTasks.length > 25 && !containsFindRecoveryReads) {
822
1313
  const rows = await this.runRecoverableCoreBatchJob(
823
1314
  path2,
824
1315
  uniqueTasks.map((task) => task.request),
825
1316
  recoverableBatch.label,
826
1317
  recoverableBatch.pageSize,
827
- recoverableBatch.maxWaitMs,
1318
+ options?.maxWaitMs ?? recoverableBatch.maxWaitMs,
1319
+ options?.pollIntervalMs,
828
1320
  options?.idempotencyKey,
829
1321
  uniqueTasks.map((task) => task.index)
830
1322
  );
@@ -947,7 +1439,7 @@ function batchItemFailure(index, error, raw) {
947
1439
  const retryStrategy = raw?.retry_strategy ?? raw?.retryStrategy;
948
1440
  const creditsUsed = raw?.credits_used ?? raw?.creditsUsed;
949
1441
  const creditsCharged = raw?.credits_charged ?? raw?.creditsCharged;
950
- const runId = raw?.run_id ?? raw?.runId;
1442
+ const runId = [raw?.run_id, raw?.runId, raw?.signal_run_id, raw?.signalRunId].find((value) => typeof value === "string" && value.trim());
951
1443
  const verificationRunId = raw?.verification_run_id ?? raw?.verificationRunId;
952
1444
  const jobId = raw?.job_id ?? raw?.jobId;
953
1445
  const suggestedAction = raw?.suggested_action ?? raw?.suggestedAction;
@@ -970,6 +1462,42 @@ function batchItemFailure(index, error, raw) {
970
1462
  function recoverableJobRowFailed(row) {
971
1463
  return coreProductResponseFailed(row) || ["failed", "skipped"].includes(String(row._status || "").toLowerCase());
972
1464
  }
1465
+ function recoverableRowsToRound(rows, label) {
1466
+ return rows.map((raw) => {
1467
+ const index = Number(raw.index);
1468
+ const failed = recoverableJobRowFailed(raw);
1469
+ return {
1470
+ index,
1471
+ success: !failed,
1472
+ raw,
1473
+ error: failed ? raw.error || raw._error || raw.message || raw.failure_reason || `${label} item failed` : void 0
1474
+ };
1475
+ });
1476
+ }
1477
+ function recoverableSignalCopyRows(rows) {
1478
+ return rows.map((raw) => {
1479
+ const index = Number(raw.index);
1480
+ const duplicateOf = recoveredDuplicateOf(raw);
1481
+ if (duplicateOf !== void 0) return { index, success: true, duplicateOf };
1482
+ if (recoverableJobRowFailed(raw)) {
1483
+ return batchItemFailure(
1484
+ index,
1485
+ raw.error || raw._error || raw.message || "Signal to Copy item failed",
1486
+ raw
1487
+ );
1488
+ }
1489
+ try {
1490
+ assertTerminalSignalResponse(raw, "Signal to Copy");
1491
+ return { index, success: true, data: normalizeSignalToCopyResult(raw) };
1492
+ } catch (error) {
1493
+ return batchItemFailure(index, error instanceof Error ? error.message : String(error), raw);
1494
+ }
1495
+ });
1496
+ }
1497
+ function recoveredDuplicateOf(raw) {
1498
+ const value = Number(raw.duplicate_of ?? raw.duplicateOf ?? raw._duplicate_of);
1499
+ return Number.isInteger(value) && value >= 0 ? value : void 0;
1500
+ }
973
1501
  function newBatchIdempotencyKey(label) {
974
1502
  const nonce = globalThis.crypto?.randomUUID?.() || `${Date.now()}-${Math.random().toString(16).slice(2)}`;
975
1503
  return `signaliz-${label.toLowerCase().replace(/[^a-z0-9]+/g, "-")}-${nonce}`;
@@ -991,6 +1519,16 @@ function findEmailRequestBody(params) {
991
1519
  idempotency_key: params.idempotencyKey
992
1520
  });
993
1521
  }
1522
+ function verifyEmailBatchRequestBody(input, options) {
1523
+ if (typeof input === "string") {
1524
+ return compact({ email: input, skip_cache: options?.skipCache });
1525
+ }
1526
+ return compact({
1527
+ email: input.email,
1528
+ skip_cache: input.skipCache ?? options?.skipCache,
1529
+ idempotency_key: input.idempotencyKey
1530
+ });
1531
+ }
994
1532
  function isCoreProductDryRun(data) {
995
1533
  return data.dry_run === true && data.status === "planned";
996
1534
  }
@@ -1025,12 +1563,17 @@ function normalizeFindEmailResult(data) {
1025
1563
  const email = failed ? null : rawEmail;
1026
1564
  const found = Boolean(email) && data.found !== false;
1027
1565
  const verificationStatus = data.verification_status ?? (data.is_valid === true ? "valid" : data.status ?? "unknown");
1566
+ const deliverabilityStatus = data.deliverability_status ?? verificationStatus;
1567
+ const verificationVerdict = data.verification_verdict ?? verificationStatus;
1028
1568
  const rawFreshness = String(data.verification_freshness ?? "").toLowerCase();
1029
1569
  const verificationFreshness = rawFreshness === "fresh" || rawFreshness === "stale" ? rawFreshness : "unknown";
1030
1570
  const needsReverification = data.needs_reverification === true || verificationFreshness === "stale";
1031
1571
  const explicitlyNotSendSafe = data.verified_for_sending === false || needsReverification;
1032
1572
  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
1573
  const verificationAgeDays = typeof data.verification_age_days === "number" && Number.isFinite(data.verification_age_days) ? data.verification_age_days : null;
1574
+ const providerUsed = data.provider_used ?? data.provider ?? data.email_source ?? data.source ?? "unknown";
1575
+ const verificationSource = data.verification_source ?? providerUsed;
1576
+ const billingMetadata = data.billing_metadata && typeof data.billing_metadata === "object" && !Array.isArray(data.billing_metadata) ? data.billing_metadata : void 0;
1034
1577
  return {
1035
1578
  success: processing || !failed && found,
1036
1579
  found,
@@ -1039,61 +1582,86 @@ function normalizeFindEmailResult(data) {
1039
1582
  lastName: data.last_name,
1040
1583
  confidence: failed ? 0 : data.confidence ?? data.confidence_score ?? (verified ? 1 : 0),
1041
1584
  verificationStatus: failed ? "error" : verificationStatus,
1585
+ deliverabilityStatus: failed ? "error" : deliverabilityStatus,
1586
+ verificationVerdict: failed ? "error" : verificationVerdict,
1042
1587
  isVerified: !failed && verified,
1043
1588
  isVerifiedForSending: !failed && !needsReverification && data.verified_for_sending === true,
1589
+ isDeliverable: !failed && !needsReverification && data.verified_for_sending === true,
1044
1590
  isCatchAll: data.email_is_catch_all === true || data.is_catch_all === true,
1045
1591
  verificationFreshness,
1046
1592
  verificationAgeDays,
1047
1593
  verifiedAt: typeof data.verified_at === "string" ? data.verified_at : null,
1048
1594
  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,
1595
+ providerUsed,
1596
+ verificationSource,
1597
+ verificationObservedAt: typeof data.verification_observed_at === "string" ? data.verification_observed_at : void 0,
1598
+ failureReason: data.failure_reason ?? (failed || !found ? "no_verified_email_found" : "none"),
1599
+ creditsUsed: data.credits_used ?? billingMetadata?.credits_used,
1600
+ estimatedExternalCostUsd: data.estimated_external_cost_usd ?? billingMetadata?.estimated_external_cost_usd,
1601
+ billingMetadata,
1602
+ historicalBillingMetadata: data.historical_billing_metadata,
1603
+ resultReturnedFrom: data.result_returned_from ?? billingMetadata?.result_returned_from,
1604
+ cacheHit: typeof data.cache_hit === "boolean" ? data.cache_hit : billingMetadata?.cache_hit,
1055
1605
  negativeCacheHit: data.negative_cache_hit,
1056
1606
  negativeCacheTtlSeconds: data.negative_cache_ttl_seconds,
1057
1607
  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,
1608
+ freshEnrichmentUsed: typeof data.fresh_enrichment_used === "boolean" ? data.fresh_enrichment_used : billingMetadata?.fresh_enrichment_used,
1609
+ liveProviderCalled: typeof data.live_provider_called === "boolean" ? data.live_provider_called : billingMetadata?.live_provider_called,
1610
+ openrouterCalled: typeof data.openrouter_called === "boolean" ? data.openrouter_called : billingMetadata?.openrouter_called,
1611
+ byoOpenrouterUsed: typeof data.byo_openrouter_used === "boolean" ? data.byo_openrouter_used : billingMetadata?.byo_openrouter_used,
1062
1612
  error: processing || found ? void 0 : coreProductErrorMessage(data) ?? "No verified email found",
1063
1613
  errorCode: processing || found ? void 0 : coreProductErrorCode(data) ?? "EMAIL_NOT_FOUND",
1064
1614
  status: processing ? "processing" : "completed",
1065
1615
  runId: data.run_id,
1066
1616
  nextPollAfterSeconds: data.next_poll_after_seconds,
1067
1617
  suggestedAction: data.suggested_action,
1618
+ cachePublicationStatus: data.cache_publication_status,
1619
+ cachePublicationRetryEligible: data.cache_publication_retry_eligible,
1068
1620
  raw: data
1069
1621
  };
1070
1622
  }
1071
1623
  function normalizeVerifyEmailResult(email, data) {
1072
1624
  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
1625
  const malformed = data.is_malformed === true || data.verification_verdict === "malformed" || data.verification_status === "malformed" || data.deliverability_status === "malformed";
1626
+ const verificationStatus = String(
1627
+ failed ? malformed ? "malformed" : "error" : data.verification_status ?? data.verification_verdict ?? data.deliverability_status ?? "unknown"
1628
+ ).toLowerCase();
1629
+ const deliverabilityStatus = String(data.deliverability_status ?? verificationStatus).toLowerCase();
1630
+ const verified = !failed && (data.email_verified === true || ["valid", "deliverable"].includes(verificationStatus));
1631
+ const isCatchAll = data.email_is_catch_all ?? data.is_catch_all ?? data.catch_all ?? data.diagnostics?.is_catch_all ?? false;
1632
+ const verifiedForSending = !failed && data.verified_for_sending === true;
1633
+ const verificationRunId = data.verification_run_id ?? data.run_id;
1076
1634
  return {
1077
1635
  success: !failed,
1078
1636
  error: failed ? coreProductErrorMessage(data) : void 0,
1079
1637
  errorCode: failed ? coreProductErrorCode(data) : void 0,
1080
1638
  email: data.email ?? email,
1081
1639
  status: !failed && String(data.status || "").toLowerCase() === "processing" ? "processing" : "completed",
1082
- verificationRunId: data.verification_run_id ?? data.run_id,
1640
+ verificationRunId,
1083
1641
  retryAfterMs: data.retry_after_ms,
1084
1642
  nextPollAfterSeconds: data.next_poll_after_seconds,
1085
1643
  isValid: failed ? false : data.is_valid ?? data.valid ?? verified,
1086
1644
  isDeliverable: failed ? false : data.is_deliverable ?? data.deliverable ?? verified,
1087
1645
  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,
1646
+ isCatchAll,
1647
+ verifiedForSending,
1648
+ verificationStatus,
1649
+ deliverabilityStatus,
1090
1650
  verificationVerdict: failed ? malformed ? "malformed" : "error" : data.verification_verdict ?? data.verification_status ?? "unknown",
1651
+ isRoleAccount: data.is_role_account === true || data.email_is_role_account === true || data.role === true,
1652
+ quality: typeof data.quality === "string" ? data.quality : null,
1653
+ recommendation: typeof data.recommendation === "string" ? data.recommendation : verifiedForSending ? "send" : ["unknown", "verifier_error"].includes(verificationStatus) ? "manual_review" : "do_not_send",
1654
+ smtpStatus: data.smtp_status ?? (isCatchAll ? "accept_all" : verifiedForSending ? "accepted" : "not_checked"),
1655
+ providerStatus: data.provider_status ?? verificationStatus,
1656
+ failureReason: data.failure_reason ?? (verifiedForSending ? "none" : isCatchAll ? "catch_all_domain_not_send_safe" : "verification_not_send_safe"),
1091
1657
  confidenceScore: failed ? 0 : data.confidence_score ?? data.confidence ?? data.diagnostics?.confidence_score ?? (verified ? 1 : 0),
1092
1658
  provider: data.provider,
1093
1659
  verificationSource: data.verification_source,
1660
+ verificationObservedAt: typeof data.verification_observed_at === "string" ? data.verification_observed_at : void 0,
1094
1661
  billingReplayed: data.billing_replayed,
1095
1662
  creditsUsed: data.credits_used,
1096
1663
  billingMetadata: data.billing_metadata,
1664
+ historicalBillingMetadata: data.historical_billing_metadata,
1097
1665
  resultReturnedFrom: data.result_returned_from,
1098
1666
  cacheHit: data.cache_hit,
1099
1667
  negativeCacheHit: data.negative_cache_hit,
@@ -1103,6 +1671,11 @@ function normalizeVerifyEmailResult(email, data) {
1103
1671
  liveProviderCalled: data.live_provider_called,
1104
1672
  openrouterCalled: data.openrouter_called,
1105
1673
  byoOpenrouterUsed: data.byo_openrouter_used,
1674
+ estimatedExternalCostUsd: data.estimated_external_cost_usd,
1675
+ runId: data.run_id ?? verificationRunId,
1676
+ cachePublicationStatus: data.cache_publication_status,
1677
+ cachePublicationRetryEligible: data.cache_publication_retry_eligible,
1678
+ suggestedAction: data.suggested_action,
1106
1679
  raw: data
1107
1680
  };
1108
1681
  }
@@ -1130,6 +1703,54 @@ function companySignalRequestBody(params) {
1130
1703
  idempotency_key: params.idempotencyKey
1131
1704
  });
1132
1705
  }
1706
+ function companySignalParamsFromRecoveredRow(row) {
1707
+ return {
1708
+ companyDomain: row.company?.domain ?? row.company_domain,
1709
+ companyName: row.company?.name ?? row.company_name,
1710
+ signalRunId: row.signal_run_id ?? row.run_id
1711
+ };
1712
+ }
1713
+ var COMPANY_SIGNAL_LARGE_BATCH_CONTROL_FIELDS = [
1714
+ "research_prompt",
1715
+ "signal_types",
1716
+ "target_signal_count",
1717
+ "lookback_days",
1718
+ "online",
1719
+ "enable_deep_search",
1720
+ "search_mode",
1721
+ "include_summary",
1722
+ "enable_outreach_intelligence",
1723
+ "enable_predictive_intelligence",
1724
+ "skip_cache",
1725
+ "include_candidate_evidence"
1726
+ ];
1727
+ function companySignalLargeBatchSubmission(requests) {
1728
+ if (requests.length <= 25) return null;
1729
+ if (requests.some(
1730
+ (request) => typeof request.signal_run_id === "string" && request.signal_run_id.trim() || typeof request.idempotency_key === "string" && request.idempotency_key.trim()
1731
+ )) {
1732
+ return null;
1733
+ }
1734
+ const submissionFields = Object.fromEntries(
1735
+ COMPANY_SIGNAL_LARGE_BATCH_CONTROL_FIELDS.filter((field) => requests[0]?.[field] !== void 0).map((field) => [field, requests[0][field]])
1736
+ );
1737
+ const controlKey = JSON.stringify(submissionFields);
1738
+ for (const request of requests) {
1739
+ const requestControls = Object.fromEntries(
1740
+ COMPANY_SIGNAL_LARGE_BATCH_CONTROL_FIELDS.filter((field) => request[field] !== void 0).map((field) => [field, request[field]])
1741
+ );
1742
+ if (JSON.stringify(requestControls) !== controlKey) return null;
1743
+ const hasIdentity = typeof request.company_domain === "string" && request.company_domain.trim() || typeof request.company_name === "string" && request.company_name.trim();
1744
+ if (!hasIdentity) return null;
1745
+ }
1746
+ return {
1747
+ requests: requests.map((request) => compact({
1748
+ company_domain: request.company_domain,
1749
+ company_name: request.company_name
1750
+ })),
1751
+ submissionFields
1752
+ };
1753
+ }
1133
1754
  function normalizeCompanySignalResult(params, data) {
1134
1755
  const failed = coreProductResponseFailed(data);
1135
1756
  const rawSignals = failed ? [] : data.signals ?? data.signal_feed ?? [];
@@ -1380,6 +2001,9 @@ function dedupeExactBatchItems(items, keyForItem) {
1380
2001
  function normalizedBatchText(value) {
1381
2002
  return typeof value === "string" ? value.trim().toLowerCase().replace(/\s+/g, " ") : "";
1382
2003
  }
2004
+ function normalizedBatchIdempotencyKey(value) {
2005
+ return typeof value === "string" ? value.trim() : "";
2006
+ }
1383
2007
  function normalizedBatchDomain(value) {
1384
2008
  return normalizedBatchText(value).replace(/^https?:\/\//, "").replace(/^www\./, "").split(/[/?#]/, 1)[0].replace(/\.+$/, "");
1385
2009
  }
@@ -1403,7 +2027,8 @@ function coreProductBatchRequestKey(path2, request) {
1403
2027
  normalizedBatchDomain(request.company_domain),
1404
2028
  normalizedBatchLinkedIn(request.linkedin_url),
1405
2029
  normalizedBatchText(request.company_name),
1406
- request.skip_cache === true || request.bypass_cache === true
2030
+ request.skip_cache === true || request.bypass_cache === true,
2031
+ normalizedBatchIdempotencyKey(request.idempotency_key)
1407
2032
  ]);
1408
2033
  }
1409
2034
  if (path2 === "api/v1/verify-email") {
@@ -1411,7 +2036,8 @@ function coreProductBatchRequestKey(path2, request) {
1411
2036
  normalizedBatchText(request.email),
1412
2037
  request.skip_cache === true || request.bypass_cache === true,
1413
2038
  request.skip_catch_all_verification === true,
1414
- request.skip_esp_check === true
2039
+ request.skip_esp_check === true,
2040
+ normalizedBatchIdempotencyKey(request.idempotency_key)
1415
2041
  ]);
1416
2042
  }
1417
2043
  if (path2 === "api/v1/company-signals") {
@@ -1433,7 +2059,8 @@ function coreProductBatchRequestKey(path2, request) {
1433
2059
  normalizedBatchText(request.search_mode || "balanced"),
1434
2060
  request.enable_outreach_intelligence === true,
1435
2061
  request.enable_predictive_intelligence === true,
1436
- request.skip_cache === true || request.bypass_cache === true
2062
+ request.skip_cache === true || request.bypass_cache === true,
2063
+ normalizedBatchIdempotencyKey(request.idempotency_key)
1437
2064
  ]);
1438
2065
  }
1439
2066
  return JSON.stringify([
@@ -1442,9 +2069,11 @@ function coreProductBatchRequestKey(path2, request) {
1442
2069
  typeof request.title === "string" ? request.title.trim() : "",
1443
2070
  typeof request.campaign_offer === "string" ? request.campaign_offer.trim() : "",
1444
2071
  typeof request.research_prompt === "string" ? request.research_prompt.trim() : "",
2072
+ Number(request.lookback_days) || null,
1445
2073
  typeof request.signal_run_id === "string" ? request.signal_run_id.trim() : "",
1446
2074
  request.skip_cache === true,
1447
- request.enable_deep_search === true
2075
+ request.enable_deep_search === true,
2076
+ normalizedBatchIdempotencyKey(request.idempotency_key)
1448
2077
  ]);
1449
2078
  }
1450
2079
  function finalizeCoreBatch(total, startedAt, round, normalize, options) {
@@ -1482,6 +2111,37 @@ function finalizeCoreBatch(total, startedAt, round, normalize, options) {
1482
2111
  results: compactedResults
1483
2112
  };
1484
2113
  }
2114
+ function finalizeRecoveredCoreBatch(total, startedAt, round, normalize, options) {
2115
+ const results = round.map((item) => {
2116
+ const duplicateOf = item.raw ? recoveredDuplicateOf(item.raw) : void 0;
2117
+ if (duplicateOf !== void 0) return { index: item.index, success: true, duplicateOf };
2118
+ if (!item.success || !item.raw) {
2119
+ return batchItemFailure(
2120
+ item.index,
2121
+ item.error || "Signaliz batch item failed",
2122
+ item.raw ?? item
2123
+ );
2124
+ }
2125
+ try {
2126
+ return { index: item.index, success: true, data: normalize(item) };
2127
+ } catch (error) {
2128
+ return {
2129
+ index: item.index,
2130
+ success: false,
2131
+ error: error instanceof Error ? error.message : String(error)
2132
+ };
2133
+ }
2134
+ });
2135
+ const compactedResults = compactExactDuplicateResults(results, options?.compactDuplicates === true);
2136
+ const succeeded = compactedResults.filter((item) => item.success).length;
2137
+ return {
2138
+ total,
2139
+ succeeded,
2140
+ failed: total - succeeded,
2141
+ durationMs: Date.now() - startedAt,
2142
+ results: compactedResults
2143
+ };
2144
+ }
1485
2145
  function compactExactDuplicateResults(results, enabled) {
1486
2146
  if (!enabled) return results;
1487
2147
  const sourceIndexByRaw = /* @__PURE__ */ new WeakMap();
@@ -1497,6 +2157,19 @@ function compactExactDuplicateResults(results, enabled) {
1497
2157
  return item;
1498
2158
  });
1499
2159
  }
2160
+ function compactKnownDuplicateBatch(batch, sourceIndexByInput, enabled) {
2161
+ if (!enabled) return batch;
2162
+ const canonicalInputByUniqueIndex = /* @__PURE__ */ new Map();
2163
+ sourceIndexByInput.forEach((uniqueIndex, inputIndex) => {
2164
+ if (!canonicalInputByUniqueIndex.has(uniqueIndex)) canonicalInputByUniqueIndex.set(uniqueIndex, inputIndex);
2165
+ });
2166
+ const results = batch.results.map((item, index) => {
2167
+ const canonicalIndex = canonicalInputByUniqueIndex.get(sourceIndexByInput[index]) ?? index;
2168
+ if (canonicalIndex === index || !item.success || !batch.results[canonicalIndex]?.success) return item;
2169
+ return { index, success: true, duplicateOf: canonicalIndex };
2170
+ });
2171
+ return { ...batch, results };
2172
+ }
1500
2173
  function signalToCopyRequestBody(params) {
1501
2174
  return compact({
1502
2175
  company_domain: params.companyDomain,
@@ -1513,6 +2186,28 @@ function signalToCopyRequestBody(params) {
1513
2186
  idempotency_key: params.idempotencyKey
1514
2187
  });
1515
2188
  }
2189
+ function validateLargeAvailableDataSignalCopyBatch(requests) {
2190
+ if (requests.length <= 25) return;
2191
+ const invalidIndex = requests.findIndex(
2192
+ (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()
2193
+ );
2194
+ if (invalidIndex >= 0) {
2195
+ throw new RangeError(
2196
+ `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.`
2197
+ );
2198
+ }
2199
+ }
2200
+ function availableDataSignalCopyRequestBody(params) {
2201
+ return compact({
2202
+ company_domain: params.companyDomain,
2203
+ person_name: params.personName,
2204
+ title: params.title,
2205
+ campaign_offer: params.campaignOffer,
2206
+ research_prompt: params.researchPrompt,
2207
+ lookback_days: params.lookbackDays,
2208
+ signal_run_id: params.signalRunId
2209
+ });
2210
+ }
1516
2211
  function validateSignalToCopyParams(params) {
1517
2212
  validateCoreProductMaxWaitMs(params.maxWaitMs);
1518
2213
  if (params.lookbackDays !== void 0 && (!Number.isInteger(params.lookbackDays) || params.lookbackDays < 1 || params.lookbackDays > 365)) {
@@ -1563,6 +2258,8 @@ function normalizeSignalToCopyResult(data) {
1563
2258
  liveProviderCalled: data.live_provider_called,
1564
2259
  aiProviderCalled: data.ai_provider_called,
1565
2260
  byoAiUsed: data.byo_ai_used,
2261
+ unlimitedSignalToCopy: data.unlimited_signal_to_copy,
2262
+ signalToCopyEntitlement: data.signal_to_copy_entitlement,
1566
2263
  variations: (failed ? [] : data.variations ?? []).map((variation) => ({
1567
2264
  style: variation.style,
1568
2265
  subjectLine: variation.subject_line ?? "",
@@ -1646,6 +2343,29 @@ 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 validateCoreBatchPollingOptions(options) {
2347
+ if (options.maxWaitMs !== void 0 && (!Number.isFinite(options.maxWaitMs) || options.maxWaitMs <= 0)) {
2348
+ throw new RangeError("maxWaitMs must be a positive finite number");
2349
+ }
2350
+ if (options.pollIntervalMs !== void 0 && (!Number.isFinite(options.pollIntervalMs) || options.pollIntervalMs <= 0)) {
2351
+ throw new RangeError("pollIntervalMs must be a positive finite number");
2352
+ }
2353
+ }
2354
+ function assertRecoverableBatchResultsRetained(status, label, jobId, idempotencyKey) {
2355
+ if (status.results_expired !== true && status.results_cleaned !== true) return;
2356
+ throw new SignalizError({
2357
+ code: "BATCH_RESULTS_EXPIRED",
2358
+ message: `${label} batch ${jobId} completed, but its retained result pages have expired and cannot be recovered. Do not automatically resubmit provider work.`,
2359
+ errorType: "not_found",
2360
+ details: {
2361
+ job_id: jobId,
2362
+ ...idempotencyKey ? { idempotency_key: idempotencyKey } : {},
2363
+ suggested_action: "review_before_resubmitting",
2364
+ retry_eligible: false,
2365
+ results_expired: true
2366
+ }
2367
+ });
2368
+ }
1649
2369
  async function runBatch(items, execute, options) {
1650
2370
  validateBatchSize(items);
1651
2371
  const concurrency = validatedBatchConcurrency(options);