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