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