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