@signaliz/sdk 1.0.55 → 1.0.57

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/README.md CHANGED
@@ -29,6 +29,8 @@ const plan = await signaliz.findEmail({
29
29
  dryRun: true,
30
30
  });
31
31
  console.log(plan.estimatedCredits.max, plan.creditsCharged); // 2, 0
32
+ // Batch plans price only semantic unique work and expose duplicate suppression.
33
+ console.log(plan.inputCount, plan.uniqueFreshInputCount, plan.inputDuplicatesSuppressed);
32
34
 
33
35
  const found = await signaliz.findEmail({
34
36
  companyDomain: 'example.com',
@@ -111,8 +113,11 @@ when the REST API supplies them, including after automatic row retries are
111
113
  exhausted.
112
114
 
113
115
  For an ambiguous single request or batch submission failure, retry with the
114
- same `idempotencyKey` to recover the original request or durable job without duplicate
115
- work.
116
+ same `idempotencyKey` to recover the original request, durable job, or provider
117
+ run without duplicate work or spend. The key is portable across REST API, MCP,
118
+ SDK, and CLI calls for the same workspace and logical input. Chunked batches
119
+ derive a stable key from each row's original input index, including after exact
120
+ duplicate compaction and rate-limit retries.
116
121
 
117
122
  Each batch accepts up to 5,000 items. For the four row-oriented core products, batches larger
118
123
  than 25 use one recoverable REST job; the SDK waits for completion and retrieves
@@ -532,11 +532,16 @@ var Signaliz = class {
532
532
  );
533
533
  }
534
534
  const startedAt = Date.now();
535
- const deduplicated = dedupeExactBatchItems(requests, (params) => JSON.stringify({
536
- request: signalToCopyRequestBody(params)
537
- }));
535
+ const deduplicated = dedupeExactBatchItems(
536
+ requests,
537
+ (params) => coreProductBatchRequestKey("api/v1/signal-to-copy", signalToCopyRequestBody(params))
538
+ );
538
539
  const uniqueRequests = deduplicated.uniqueItems;
539
- const uniqueResults = await this.runSignalCopyBatchChunks(uniqueRequests, options);
540
+ const uniqueInputIndices = new Array(uniqueRequests.length);
541
+ deduplicated.sourceIndexByInput.forEach((uniqueIndex, inputIndex) => {
542
+ if (uniqueInputIndices[uniqueIndex] === void 0) uniqueInputIndices[uniqueIndex] = inputIndex;
543
+ });
544
+ const uniqueResults = await this.runSignalCopyBatchChunks(uniqueRequests, options, uniqueInputIndices);
540
545
  const results = requests.map((_, index) => ({
541
546
  ...uniqueResults[deduplicated.sourceIndexByInput[index]],
542
547
  index
@@ -562,15 +567,25 @@ var Signaliz = class {
562
567
  }
563
568
  return normalizeCoreProductDryRunResult(data);
564
569
  }
565
- async runSignalCopyBatchChunks(uniqueRequests, options) {
570
+ async runSignalCopyBatchChunks(uniqueRequests, options, idempotencyItemIndices = uniqueRequests.map((_, index) => index)) {
566
571
  const chunks = [];
567
572
  for (let offset = 0; offset < uniqueRequests.length; offset += 25) {
568
- chunks.push({ offset, requests: uniqueRequests.slice(offset, offset + 25) });
573
+ chunks.push({
574
+ offset,
575
+ requests: uniqueRequests.slice(offset, offset + 25),
576
+ idempotencyItemIndices: idempotencyItemIndices.slice(offset, offset + 25)
577
+ });
569
578
  }
570
579
  const { serverConcurrency, chunkConcurrency } = batchConcurrencyPlan(options, 25, 25);
571
580
  const chunkBatch = await runBatch(
572
581
  chunks,
573
- (chunk) => this.runSignalCopyBatchChunk(chunk.requests, serverConcurrency),
582
+ (chunk) => this.runSignalCopyBatchChunk(
583
+ chunk.requests,
584
+ serverConcurrency,
585
+ 0,
586
+ options?.idempotencyKey,
587
+ chunk.idempotencyItemIndices
588
+ ),
574
589
  { concurrency: chunkConcurrency }
575
590
  );
576
591
  const uniqueResults = new Array(uniqueRequests.length);
@@ -592,14 +607,15 @@ var Signaliz = class {
592
607
  }
593
608
  return uniqueResults;
594
609
  }
595
- async runRecoverableCoreBatchJob(path, requests, label, pageSize = 500, maxWaitMs = 20 * 6e4, idempotencyKey) {
610
+ async runRecoverableCoreBatchJob(path, requests, label, pageSize = 500, maxWaitMs = 20 * 6e4, idempotencyKey, idempotencyItemIndices) {
596
611
  const startedAt = Date.now();
597
612
  const submissionIdempotencyKey = idempotencyKey?.trim() || newBatchIdempotencyKey(label);
598
613
  let submission;
599
614
  try {
600
615
  submission = await this.client.post(path, {
601
616
  requests,
602
- idempotency_key: submissionIdempotencyKey
617
+ idempotency_key: submissionIdempotencyKey,
618
+ batch_item_indices: idempotencyItemIndices
603
619
  });
604
620
  } catch (error) {
605
621
  if (error instanceof SignalizError && !error.isRetryable) throw error;
@@ -662,12 +678,14 @@ var Signaliz = class {
662
678
  if (seen.size !== requests.length) throw new Error(`${label} batch returned incomplete indexed results`);
663
679
  return rows;
664
680
  }
665
- async runSignalCopyBatchChunk(requests, concurrency, rowRateLimitAttempt = 0) {
681
+ async runSignalCopyBatchChunk(requests, concurrency, rowRateLimitAttempt = 0, idempotencyKey, idempotencyItemIndices = requests.map((_, index) => index)) {
666
682
  const results = new Array(requests.length);
667
683
  const rateLimited = [];
668
684
  const data = await this.client.post("api/v1/signal-to-copy", {
669
685
  requests: requests.map(signalToCopyRequestBody),
670
- concurrency
686
+ concurrency,
687
+ idempotency_key: idempotencyKey,
688
+ batch_item_indices: idempotencyItemIndices
671
689
  });
672
690
  if (!Array.isArray(data.results) || data.results.length !== requests.length) {
673
691
  throw new Error("Signal to Copy batch returned an invalid result count");
@@ -702,7 +720,9 @@ var Signaliz = class {
702
720
  const retried = await this.runSignalCopyBatchChunk(
703
721
  rateLimited.map((item) => item.params),
704
722
  concurrency,
705
- rowRateLimitAttempt + 1
723
+ rowRateLimitAttempt + 1,
724
+ idempotencyKey,
725
+ rateLimited.map((item) => idempotencyItemIndices[item.index])
706
726
  );
707
727
  for (let index = 0; index < rateLimited.length; index += 1) {
708
728
  results[rateLimited[index].index] = { ...retried[index], index: rateLimited[index].index };
@@ -712,7 +732,10 @@ var Signaliz = class {
712
732
  }
713
733
  async runCoreBatchRound(path, tasks, options, rowRateLimitAttempt = 0) {
714
734
  const { serverConcurrency, chunkConcurrency } = path === "api/v1/company-signals" ? batchConcurrencyPlan(options, 25, 25) : batchConcurrencyPlan(options);
715
- const deduplicated = dedupeExactBatchItems(tasks, (task) => JSON.stringify(task.request));
735
+ const deduplicated = dedupeExactBatchItems(
736
+ tasks,
737
+ (task) => coreProductBatchRequestKey(path, task.request)
738
+ );
716
739
  const uniqueTasks = deduplicated.uniqueItems;
717
740
  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;
718
741
  if (path !== "api/v1/company-signals" && recoverableBatch && uniqueTasks.length > 25) {
@@ -722,7 +745,8 @@ var Signaliz = class {
722
745
  recoverableBatch.label,
723
746
  recoverableBatch.pageSize,
724
747
  recoverableBatch.maxWaitMs,
725
- options?.idempotencyKey
748
+ options?.idempotencyKey,
749
+ uniqueTasks.map((task) => task.index)
726
750
  );
727
751
  const uniqueResults2 = new Array(uniqueTasks.length);
728
752
  for (const raw of rows) {
@@ -746,7 +770,9 @@ var Signaliz = class {
746
770
  const chunkBatch = await runBatch(chunks, async (chunk) => {
747
771
  const data = await this.client.post(path, {
748
772
  requests: chunk.tasks.map((task) => task.request),
749
- concurrency: serverConcurrency
773
+ concurrency: serverConcurrency,
774
+ idempotency_key: options?.idempotencyKey,
775
+ batch_item_indices: chunk.tasks.map((task) => task.index)
750
776
  });
751
777
  if (!Array.isArray(data.results) || data.results.length !== chunk.tasks.length) {
752
778
  throw new Error(`${path} batch returned an invalid result count`);
@@ -881,7 +907,11 @@ function normalizeCoreProductDryRunResult(data) {
881
907
  status: "planned",
882
908
  capability: String(data.capability || ""),
883
909
  inputCount: Number(data.input_count || 0),
910
+ freshInputCount: Number(data.fresh_input_count ?? data.input_count ?? 0),
911
+ uniqueFreshInputCount: Number(data.unique_fresh_input_count ?? data.fresh_input_count ?? data.input_count ?? 0),
912
+ inputDuplicatesSuppressed: Number(data.input_duplicates_suppressed || 0),
884
913
  recoveryReadCount: Number(data.recovery_read_count || 0),
914
+ uniqueRecoveryReadCount: Number(data.unique_recovery_read_count ?? data.recovery_read_count ?? 0),
885
915
  plan: data.plan && typeof data.plan === "object" ? data.plan : {},
886
916
  estimate: data.estimate && typeof data.estimate === "object" ? data.estimate : {},
887
917
  estimatedCredits: {
@@ -896,12 +926,12 @@ function normalizeCoreProductDryRunResult(data) {
896
926
  function normalizeFindEmailResult(data) {
897
927
  const email = typeof data.email === "string" && data.email.trim() ? data.email.trim() : null;
898
928
  const found = Boolean(email) && data.found !== false;
899
- const verificationStatus = data.verification_status ?? data.status ?? "unknown";
929
+ const verificationStatus = data.verification_status ?? (data.is_valid === true ? "valid" : data.status ?? "unknown");
900
930
  const rawFreshness = String(data.verification_freshness ?? "").toLowerCase();
901
931
  const verificationFreshness = rawFreshness === "fresh" || rawFreshness === "stale" ? rawFreshness : "unknown";
902
932
  const needsReverification = data.needs_reverification === true || verificationFreshness === "stale";
903
933
  const explicitlyNotSendSafe = data.verified_for_sending === false || needsReverification;
904
- const verified = !explicitlyNotSendSafe && (data.email_verified === true || data.verified_for_sending === true || ["valid", "deliverable"].includes(String(verificationStatus).toLowerCase()));
934
+ 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()));
905
935
  const verificationAgeDays = typeof data.verification_age_days === "number" && Number.isFinite(data.verification_age_days) ? data.verification_age_days : null;
906
936
  return {
907
937
  success: data.success !== false && found,
@@ -924,6 +954,9 @@ function normalizeFindEmailResult(data) {
924
954
  billingMetadata: data.billing_metadata,
925
955
  resultReturnedFrom: data.result_returned_from,
926
956
  cacheHit: data.cache_hit,
957
+ negativeCacheHit: data.negative_cache_hit,
958
+ negativeCacheTtlSeconds: data.negative_cache_ttl_seconds,
959
+ cacheTier: data.cache_tier,
927
960
  freshEnrichmentUsed: data.fresh_enrichment_used,
928
961
  liveProviderCalled: data.live_provider_called,
929
962
  openrouterCalled: data.openrouter_called,
@@ -945,6 +978,7 @@ function normalizeVerifyEmailResult(email, data) {
945
978
  nextPollAfterSeconds: data.next_poll_after_seconds,
946
979
  isValid: data.is_valid ?? data.valid ?? verified,
947
980
  isDeliverable: data.is_deliverable ?? data.deliverable ?? verified,
981
+ isMalformed: data.is_malformed === true || data.verification_verdict === "malformed" || data.verification_status === "malformed" || data.deliverability_status === "malformed",
948
982
  isCatchAll: data.email_is_catch_all ?? data.is_catch_all ?? data.catch_all ?? data.diagnostics?.is_catch_all ?? false,
949
983
  verifiedForSending: data.verified_for_sending === true,
950
984
  verificationVerdict: data.verification_verdict ?? data.verification_status ?? "unknown",
@@ -956,6 +990,9 @@ function normalizeVerifyEmailResult(email, data) {
956
990
  billingMetadata: data.billing_metadata,
957
991
  resultReturnedFrom: data.result_returned_from,
958
992
  cacheHit: data.cache_hit,
993
+ negativeCacheHit: data.negative_cache_hit,
994
+ negativeCacheTtlSeconds: data.negative_cache_ttl_seconds,
995
+ cacheTier: data.cache_tier,
959
996
  freshEnrichmentUsed: data.fresh_enrichment_used,
960
997
  liveProviderCalled: data.live_provider_called,
961
998
  openrouterCalled: data.openrouter_called,
@@ -1025,6 +1062,14 @@ function normalizeCompanySignalResult(params, data) {
1025
1062
  evidenceCountTotal: data.evidence_count_total,
1026
1063
  evidenceSourcesOmitted: data.evidence_sources_omitted,
1027
1064
  evidenceScope: data.evidence_scope,
1065
+ billingMetadata: data.billing_metadata,
1066
+ resultReturnedFrom: data.result_returned_from,
1067
+ cacheHit: data.cache_hit,
1068
+ freshEnrichmentUsed: data.fresh_enrichment_used,
1069
+ creditsUsed: data.credits_used,
1070
+ liveProviderCalled: data.live_provider_called,
1071
+ aiProviderCalled: data.ai_provider_called,
1072
+ byoAiUsed: data.byo_ai_used,
1028
1073
  raw: data
1029
1074
  };
1030
1075
  }
@@ -1206,6 +1251,75 @@ function dedupeExactBatchItems(items, keyForItem) {
1206
1251
  }
1207
1252
  return { uniqueItems, sourceIndexByInput };
1208
1253
  }
1254
+ function normalizedBatchText(value) {
1255
+ return typeof value === "string" ? value.trim().toLowerCase().replace(/\s+/g, " ") : "";
1256
+ }
1257
+ function normalizedBatchDomain(value) {
1258
+ return normalizedBatchText(value).replace(/^https?:\/\//, "").replace(/^www\./, "").split(/[/?#]/, 1)[0].replace(/\.+$/, "");
1259
+ }
1260
+ function normalizedBatchLinkedIn(value) {
1261
+ return normalizedBatchText(value).replace(/\/+$/, "");
1262
+ }
1263
+ function normalizedBatchList(value) {
1264
+ return Array.isArray(value) ? value.map(normalizedBatchText).filter(Boolean).sort() : [];
1265
+ }
1266
+ function normalizedFindEmailBatchName(request) {
1267
+ const fullName = normalizedBatchText(request.full_name);
1268
+ const componentName = [normalizedBatchText(request.first_name), normalizedBatchText(request.last_name)].filter(Boolean).join(" ");
1269
+ if (!fullName || !componentName || fullName === componentName) return fullName || componentName;
1270
+ return JSON.stringify([fullName, componentName]);
1271
+ }
1272
+ function coreProductBatchRequestKey(path, request) {
1273
+ if (path === "api/v1/find-email") {
1274
+ return JSON.stringify([
1275
+ normalizedFindEmailBatchName(request),
1276
+ normalizedBatchDomain(request.company_domain),
1277
+ normalizedBatchLinkedIn(request.linkedin_url),
1278
+ normalizedBatchText(request.company_name),
1279
+ request.skip_cache === true || request.bypass_cache === true
1280
+ ]);
1281
+ }
1282
+ if (path === "api/v1/verify-email") {
1283
+ return JSON.stringify([
1284
+ normalizedBatchText(request.email),
1285
+ request.skip_cache === true || request.bypass_cache === true,
1286
+ request.skip_catch_all_verification === true,
1287
+ request.skip_esp_check === true
1288
+ ]);
1289
+ }
1290
+ if (path === "api/v1/company-signals") {
1291
+ const online = request.online !== false;
1292
+ const deepSearch = request.enable_deep_search === void 0 ? online : request.enable_deep_search !== false;
1293
+ return JSON.stringify([
1294
+ normalizedBatchText(request.signal_run_id),
1295
+ normalizedBatchDomain(request.company_domain || request.domain),
1296
+ normalizedBatchText(request.company_name),
1297
+ normalizedBatchText(request.company_id),
1298
+ normalizedBatchLinkedIn(request.linkedin_url),
1299
+ normalizedBatchText(request.industry),
1300
+ typeof request.research_prompt === "string" ? request.research_prompt.trim() : "",
1301
+ normalizedBatchList(request.signal_types),
1302
+ Number(request.target_signal_count) || null,
1303
+ Number(request.lookback_days) || null,
1304
+ online,
1305
+ deepSearch,
1306
+ normalizedBatchText(request.search_mode || "balanced"),
1307
+ request.enable_outreach_intelligence === true,
1308
+ request.enable_predictive_intelligence === true,
1309
+ request.skip_cache === true || request.bypass_cache === true
1310
+ ]);
1311
+ }
1312
+ return JSON.stringify([
1313
+ normalizedBatchDomain(request.company_domain),
1314
+ typeof request.person_name === "string" ? request.person_name.trim() : "",
1315
+ typeof request.title === "string" ? request.title.trim() : "",
1316
+ typeof request.campaign_offer === "string" ? request.campaign_offer.trim() : "",
1317
+ typeof request.research_prompt === "string" ? request.research_prompt.trim() : "",
1318
+ typeof request.signal_run_id === "string" ? request.signal_run_id.trim() : "",
1319
+ request.skip_cache === true,
1320
+ request.enable_deep_search === true
1321
+ ]);
1322
+ }
1209
1323
  function finalizeCoreBatch(total, startedAt, round, normalize, options) {
1210
1324
  const results = new Array(total);
1211
1325
  for (const item of round) {
@@ -1302,6 +1416,14 @@ function normalizeSignalToCopyResult(data) {
1302
1416
  empty: data.empty,
1303
1417
  emptyReason: data.empty_reason,
1304
1418
  researchMetadata: data.research_metadata,
1419
+ billingMetadata: data.billing_metadata,
1420
+ resultReturnedFrom: data.result_returned_from,
1421
+ cacheHit: data.cache_hit,
1422
+ freshEnrichmentUsed: data.fresh_enrichment_used,
1423
+ creditsUsed: data.credits_used,
1424
+ liveProviderCalled: data.live_provider_called,
1425
+ aiProviderCalled: data.ai_provider_called,
1426
+ byoAiUsed: data.byo_ai_used,
1305
1427
  variations: (data.variations ?? []).map((variation) => ({
1306
1428
  style: variation.style,
1307
1429
  subjectLine: variation.subject_line ?? "",
package/dist/index.d.mts CHANGED
@@ -9,7 +9,7 @@ interface SignalizConfig {
9
9
  interface BatchOptions {
10
10
  /** Maximum simultaneous API requests. Defaults to 10; allowed range is 1-50. */
11
11
  concurrency?: number;
12
- /** Stable key for safely retrying an ambiguous recoverable batch submission. */
12
+ /** Stable cross-surface key. Retries reuse the same provider work; chunked batches derive one collision-free key per original row. */
13
13
  idempotencyKey?: string;
14
14
  /** Return exact repeated successes as duplicateOf references. Defaults to false. */
15
15
  compactDuplicates?: boolean;
@@ -51,7 +51,11 @@ interface CoreProductDryRunResult {
51
51
  status: 'planned';
52
52
  capability: string;
53
53
  inputCount: number;
54
+ freshInputCount: number;
55
+ uniqueFreshInputCount: number;
56
+ inputDuplicatesSuppressed: number;
54
57
  recoveryReadCount: number;
58
+ uniqueRecoveryReadCount: number;
55
59
  plan: Record<string, unknown>;
56
60
  estimate: Record<string, unknown>;
57
61
  estimatedCredits: {
@@ -97,6 +101,11 @@ interface FindEmailResult {
97
101
  billingMetadata?: Record<string, unknown>;
98
102
  resultReturnedFrom?: string;
99
103
  cacheHit?: boolean;
104
+ /** True when a definitive no-email result came from the short-lived negative cache. */
105
+ negativeCacheHit?: boolean;
106
+ /** Remaining lifetime of the negative cache entry. */
107
+ negativeCacheTtlSeconds?: number;
108
+ cacheTier?: string;
100
109
  freshEnrichmentUsed?: boolean;
101
110
  liveProviderCalled?: boolean;
102
111
  openrouterCalled?: boolean;
@@ -116,6 +125,8 @@ interface VerifyEmailResult {
116
125
  nextPollAfterSeconds?: number;
117
126
  isValid: boolean;
118
127
  isDeliverable: boolean;
128
+ /** True when the address failed syntax validation before any provider call. */
129
+ isMalformed: boolean;
119
130
  isCatchAll: boolean;
120
131
  /** Explicit send-safety verdict. Missing or false never becomes true by inference. */
121
132
  verifiedForSending: boolean;
@@ -128,6 +139,11 @@ interface VerifyEmailResult {
128
139
  billingMetadata?: Record<string, unknown>;
129
140
  resultReturnedFrom?: string;
130
141
  cacheHit?: boolean;
142
+ /** True when a conclusive send-blocking verdict came from the short-lived rejection cache. */
143
+ negativeCacheHit?: boolean;
144
+ /** Remaining lifetime of the rejection cache entry. */
145
+ negativeCacheTtlSeconds?: number;
146
+ cacheTier?: string;
131
147
  freshEnrichmentUsed?: boolean;
132
148
  liveProviderCalled?: boolean;
133
149
  openrouterCalled?: boolean;
@@ -217,6 +233,14 @@ interface CompanySignalEnrichmentResult {
217
233
  evidenceCountTotal?: number;
218
234
  evidenceSourcesOmitted?: number;
219
235
  evidenceScope?: 'selected_signals' | 'all_candidates';
236
+ billingMetadata?: Record<string, unknown>;
237
+ resultReturnedFrom?: string;
238
+ cacheHit?: boolean;
239
+ freshEnrichmentUsed?: boolean;
240
+ creditsUsed?: number;
241
+ liveProviderCalled?: boolean;
242
+ aiProviderCalled?: boolean;
243
+ byoAiUsed?: boolean;
220
244
  raw: Record<string, unknown>;
221
245
  }
222
246
  type SignalDiscoverySource = 'web' | 'news';
@@ -311,7 +335,7 @@ interface SignalToCopyParams {
311
335
  signalRunId?: string;
312
336
  /** Bypass cached company signals and start fresh research. */
313
337
  skipCache?: boolean;
314
- /** Opt into slower deep research. Defaults to false. */
338
+ /** Use the full deep-research provider portfolio. The default uses bounded credit-free first-party and Bing grounding. */
315
339
  enableDeepSearch?: boolean;
316
340
  /** @deprecated Signal to Copy is always synchronous; this option is ignored. */
317
341
  waitForResult?: boolean;
@@ -346,6 +370,14 @@ interface SignalToCopyResult {
346
370
  empty?: boolean;
347
371
  emptyReason?: string;
348
372
  researchMetadata?: Record<string, unknown>;
373
+ billingMetadata?: Record<string, unknown>;
374
+ resultReturnedFrom?: string;
375
+ cacheHit?: boolean;
376
+ freshEnrichmentUsed?: boolean;
377
+ creditsUsed?: number;
378
+ liveProviderCalled?: boolean;
379
+ aiProviderCalled?: boolean;
380
+ byoAiUsed?: boolean;
349
381
  variations: SignalToCopyVariation[];
350
382
  raw: Record<string, unknown>;
351
383
  }
package/dist/index.d.ts CHANGED
@@ -9,7 +9,7 @@ interface SignalizConfig {
9
9
  interface BatchOptions {
10
10
  /** Maximum simultaneous API requests. Defaults to 10; allowed range is 1-50. */
11
11
  concurrency?: number;
12
- /** Stable key for safely retrying an ambiguous recoverable batch submission. */
12
+ /** Stable cross-surface key. Retries reuse the same provider work; chunked batches derive one collision-free key per original row. */
13
13
  idempotencyKey?: string;
14
14
  /** Return exact repeated successes as duplicateOf references. Defaults to false. */
15
15
  compactDuplicates?: boolean;
@@ -51,7 +51,11 @@ interface CoreProductDryRunResult {
51
51
  status: 'planned';
52
52
  capability: string;
53
53
  inputCount: number;
54
+ freshInputCount: number;
55
+ uniqueFreshInputCount: number;
56
+ inputDuplicatesSuppressed: number;
54
57
  recoveryReadCount: number;
58
+ uniqueRecoveryReadCount: number;
55
59
  plan: Record<string, unknown>;
56
60
  estimate: Record<string, unknown>;
57
61
  estimatedCredits: {
@@ -97,6 +101,11 @@ interface FindEmailResult {
97
101
  billingMetadata?: Record<string, unknown>;
98
102
  resultReturnedFrom?: string;
99
103
  cacheHit?: boolean;
104
+ /** True when a definitive no-email result came from the short-lived negative cache. */
105
+ negativeCacheHit?: boolean;
106
+ /** Remaining lifetime of the negative cache entry. */
107
+ negativeCacheTtlSeconds?: number;
108
+ cacheTier?: string;
100
109
  freshEnrichmentUsed?: boolean;
101
110
  liveProviderCalled?: boolean;
102
111
  openrouterCalled?: boolean;
@@ -116,6 +125,8 @@ interface VerifyEmailResult {
116
125
  nextPollAfterSeconds?: number;
117
126
  isValid: boolean;
118
127
  isDeliverable: boolean;
128
+ /** True when the address failed syntax validation before any provider call. */
129
+ isMalformed: boolean;
119
130
  isCatchAll: boolean;
120
131
  /** Explicit send-safety verdict. Missing or false never becomes true by inference. */
121
132
  verifiedForSending: boolean;
@@ -128,6 +139,11 @@ interface VerifyEmailResult {
128
139
  billingMetadata?: Record<string, unknown>;
129
140
  resultReturnedFrom?: string;
130
141
  cacheHit?: boolean;
142
+ /** True when a conclusive send-blocking verdict came from the short-lived rejection cache. */
143
+ negativeCacheHit?: boolean;
144
+ /** Remaining lifetime of the rejection cache entry. */
145
+ negativeCacheTtlSeconds?: number;
146
+ cacheTier?: string;
131
147
  freshEnrichmentUsed?: boolean;
132
148
  liveProviderCalled?: boolean;
133
149
  openrouterCalled?: boolean;
@@ -217,6 +233,14 @@ interface CompanySignalEnrichmentResult {
217
233
  evidenceCountTotal?: number;
218
234
  evidenceSourcesOmitted?: number;
219
235
  evidenceScope?: 'selected_signals' | 'all_candidates';
236
+ billingMetadata?: Record<string, unknown>;
237
+ resultReturnedFrom?: string;
238
+ cacheHit?: boolean;
239
+ freshEnrichmentUsed?: boolean;
240
+ creditsUsed?: number;
241
+ liveProviderCalled?: boolean;
242
+ aiProviderCalled?: boolean;
243
+ byoAiUsed?: boolean;
220
244
  raw: Record<string, unknown>;
221
245
  }
222
246
  type SignalDiscoverySource = 'web' | 'news';
@@ -311,7 +335,7 @@ interface SignalToCopyParams {
311
335
  signalRunId?: string;
312
336
  /** Bypass cached company signals and start fresh research. */
313
337
  skipCache?: boolean;
314
- /** Opt into slower deep research. Defaults to false. */
338
+ /** Use the full deep-research provider portfolio. The default uses bounded credit-free first-party and Bing grounding. */
315
339
  enableDeepSearch?: boolean;
316
340
  /** @deprecated Signal to Copy is always synchronous; this option is ignored. */
317
341
  waitForResult?: boolean;
@@ -346,6 +370,14 @@ interface SignalToCopyResult {
346
370
  empty?: boolean;
347
371
  emptyReason?: string;
348
372
  researchMetadata?: Record<string, unknown>;
373
+ billingMetadata?: Record<string, unknown>;
374
+ resultReturnedFrom?: string;
375
+ cacheHit?: boolean;
376
+ freshEnrichmentUsed?: boolean;
377
+ creditsUsed?: number;
378
+ liveProviderCalled?: boolean;
379
+ aiProviderCalled?: boolean;
380
+ byoAiUsed?: boolean;
349
381
  variations: SignalToCopyVariation[];
350
382
  raw: Record<string, unknown>;
351
383
  }
package/dist/index.js CHANGED
@@ -559,11 +559,16 @@ var Signaliz = class {
559
559
  );
560
560
  }
561
561
  const startedAt = Date.now();
562
- const deduplicated = dedupeExactBatchItems(requests, (params) => JSON.stringify({
563
- request: signalToCopyRequestBody(params)
564
- }));
562
+ const deduplicated = dedupeExactBatchItems(
563
+ requests,
564
+ (params) => coreProductBatchRequestKey("api/v1/signal-to-copy", signalToCopyRequestBody(params))
565
+ );
565
566
  const uniqueRequests = deduplicated.uniqueItems;
566
- const uniqueResults = await this.runSignalCopyBatchChunks(uniqueRequests, options);
567
+ const uniqueInputIndices = new Array(uniqueRequests.length);
568
+ deduplicated.sourceIndexByInput.forEach((uniqueIndex, inputIndex) => {
569
+ if (uniqueInputIndices[uniqueIndex] === void 0) uniqueInputIndices[uniqueIndex] = inputIndex;
570
+ });
571
+ const uniqueResults = await this.runSignalCopyBatchChunks(uniqueRequests, options, uniqueInputIndices);
567
572
  const results = requests.map((_, index) => ({
568
573
  ...uniqueResults[deduplicated.sourceIndexByInput[index]],
569
574
  index
@@ -589,15 +594,25 @@ var Signaliz = class {
589
594
  }
590
595
  return normalizeCoreProductDryRunResult(data);
591
596
  }
592
- async runSignalCopyBatchChunks(uniqueRequests, options) {
597
+ async runSignalCopyBatchChunks(uniqueRequests, options, idempotencyItemIndices = uniqueRequests.map((_, index) => index)) {
593
598
  const chunks = [];
594
599
  for (let offset = 0; offset < uniqueRequests.length; offset += 25) {
595
- chunks.push({ offset, requests: uniqueRequests.slice(offset, offset + 25) });
600
+ chunks.push({
601
+ offset,
602
+ requests: uniqueRequests.slice(offset, offset + 25),
603
+ idempotencyItemIndices: idempotencyItemIndices.slice(offset, offset + 25)
604
+ });
596
605
  }
597
606
  const { serverConcurrency, chunkConcurrency } = batchConcurrencyPlan(options, 25, 25);
598
607
  const chunkBatch = await runBatch(
599
608
  chunks,
600
- (chunk) => this.runSignalCopyBatchChunk(chunk.requests, serverConcurrency),
609
+ (chunk) => this.runSignalCopyBatchChunk(
610
+ chunk.requests,
611
+ serverConcurrency,
612
+ 0,
613
+ options?.idempotencyKey,
614
+ chunk.idempotencyItemIndices
615
+ ),
601
616
  { concurrency: chunkConcurrency }
602
617
  );
603
618
  const uniqueResults = new Array(uniqueRequests.length);
@@ -619,14 +634,15 @@ var Signaliz = class {
619
634
  }
620
635
  return uniqueResults;
621
636
  }
622
- async runRecoverableCoreBatchJob(path, requests, label, pageSize = 500, maxWaitMs = 20 * 6e4, idempotencyKey) {
637
+ async runRecoverableCoreBatchJob(path, requests, label, pageSize = 500, maxWaitMs = 20 * 6e4, idempotencyKey, idempotencyItemIndices) {
623
638
  const startedAt = Date.now();
624
639
  const submissionIdempotencyKey = idempotencyKey?.trim() || newBatchIdempotencyKey(label);
625
640
  let submission;
626
641
  try {
627
642
  submission = await this.client.post(path, {
628
643
  requests,
629
- idempotency_key: submissionIdempotencyKey
644
+ idempotency_key: submissionIdempotencyKey,
645
+ batch_item_indices: idempotencyItemIndices
630
646
  });
631
647
  } catch (error) {
632
648
  if (error instanceof SignalizError && !error.isRetryable) throw error;
@@ -689,12 +705,14 @@ var Signaliz = class {
689
705
  if (seen.size !== requests.length) throw new Error(`${label} batch returned incomplete indexed results`);
690
706
  return rows;
691
707
  }
692
- async runSignalCopyBatchChunk(requests, concurrency, rowRateLimitAttempt = 0) {
708
+ async runSignalCopyBatchChunk(requests, concurrency, rowRateLimitAttempt = 0, idempotencyKey, idempotencyItemIndices = requests.map((_, index) => index)) {
693
709
  const results = new Array(requests.length);
694
710
  const rateLimited = [];
695
711
  const data = await this.client.post("api/v1/signal-to-copy", {
696
712
  requests: requests.map(signalToCopyRequestBody),
697
- concurrency
713
+ concurrency,
714
+ idempotency_key: idempotencyKey,
715
+ batch_item_indices: idempotencyItemIndices
698
716
  });
699
717
  if (!Array.isArray(data.results) || data.results.length !== requests.length) {
700
718
  throw new Error("Signal to Copy batch returned an invalid result count");
@@ -729,7 +747,9 @@ var Signaliz = class {
729
747
  const retried = await this.runSignalCopyBatchChunk(
730
748
  rateLimited.map((item) => item.params),
731
749
  concurrency,
732
- rowRateLimitAttempt + 1
750
+ rowRateLimitAttempt + 1,
751
+ idempotencyKey,
752
+ rateLimited.map((item) => idempotencyItemIndices[item.index])
733
753
  );
734
754
  for (let index = 0; index < rateLimited.length; index += 1) {
735
755
  results[rateLimited[index].index] = { ...retried[index], index: rateLimited[index].index };
@@ -739,7 +759,10 @@ var Signaliz = class {
739
759
  }
740
760
  async runCoreBatchRound(path, tasks, options, rowRateLimitAttempt = 0) {
741
761
  const { serverConcurrency, chunkConcurrency } = path === "api/v1/company-signals" ? batchConcurrencyPlan(options, 25, 25) : batchConcurrencyPlan(options);
742
- const deduplicated = dedupeExactBatchItems(tasks, (task) => JSON.stringify(task.request));
762
+ const deduplicated = dedupeExactBatchItems(
763
+ tasks,
764
+ (task) => coreProductBatchRequestKey(path, task.request)
765
+ );
743
766
  const uniqueTasks = deduplicated.uniqueItems;
744
767
  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;
745
768
  if (path !== "api/v1/company-signals" && recoverableBatch && uniqueTasks.length > 25) {
@@ -749,7 +772,8 @@ var Signaliz = class {
749
772
  recoverableBatch.label,
750
773
  recoverableBatch.pageSize,
751
774
  recoverableBatch.maxWaitMs,
752
- options?.idempotencyKey
775
+ options?.idempotencyKey,
776
+ uniqueTasks.map((task) => task.index)
753
777
  );
754
778
  const uniqueResults2 = new Array(uniqueTasks.length);
755
779
  for (const raw of rows) {
@@ -773,7 +797,9 @@ var Signaliz = class {
773
797
  const chunkBatch = await runBatch(chunks, async (chunk) => {
774
798
  const data = await this.client.post(path, {
775
799
  requests: chunk.tasks.map((task) => task.request),
776
- concurrency: serverConcurrency
800
+ concurrency: serverConcurrency,
801
+ idempotency_key: options?.idempotencyKey,
802
+ batch_item_indices: chunk.tasks.map((task) => task.index)
777
803
  });
778
804
  if (!Array.isArray(data.results) || data.results.length !== chunk.tasks.length) {
779
805
  throw new Error(`${path} batch returned an invalid result count`);
@@ -908,7 +934,11 @@ function normalizeCoreProductDryRunResult(data) {
908
934
  status: "planned",
909
935
  capability: String(data.capability || ""),
910
936
  inputCount: Number(data.input_count || 0),
937
+ freshInputCount: Number(data.fresh_input_count ?? data.input_count ?? 0),
938
+ uniqueFreshInputCount: Number(data.unique_fresh_input_count ?? data.fresh_input_count ?? data.input_count ?? 0),
939
+ inputDuplicatesSuppressed: Number(data.input_duplicates_suppressed || 0),
911
940
  recoveryReadCount: Number(data.recovery_read_count || 0),
941
+ uniqueRecoveryReadCount: Number(data.unique_recovery_read_count ?? data.recovery_read_count ?? 0),
912
942
  plan: data.plan && typeof data.plan === "object" ? data.plan : {},
913
943
  estimate: data.estimate && typeof data.estimate === "object" ? data.estimate : {},
914
944
  estimatedCredits: {
@@ -923,12 +953,12 @@ function normalizeCoreProductDryRunResult(data) {
923
953
  function normalizeFindEmailResult(data) {
924
954
  const email = typeof data.email === "string" && data.email.trim() ? data.email.trim() : null;
925
955
  const found = Boolean(email) && data.found !== false;
926
- const verificationStatus = data.verification_status ?? data.status ?? "unknown";
956
+ const verificationStatus = data.verification_status ?? (data.is_valid === true ? "valid" : data.status ?? "unknown");
927
957
  const rawFreshness = String(data.verification_freshness ?? "").toLowerCase();
928
958
  const verificationFreshness = rawFreshness === "fresh" || rawFreshness === "stale" ? rawFreshness : "unknown";
929
959
  const needsReverification = data.needs_reverification === true || verificationFreshness === "stale";
930
960
  const explicitlyNotSendSafe = data.verified_for_sending === false || needsReverification;
931
- const verified = !explicitlyNotSendSafe && (data.email_verified === true || data.verified_for_sending === true || ["valid", "deliverable"].includes(String(verificationStatus).toLowerCase()));
961
+ 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()));
932
962
  const verificationAgeDays = typeof data.verification_age_days === "number" && Number.isFinite(data.verification_age_days) ? data.verification_age_days : null;
933
963
  return {
934
964
  success: data.success !== false && found,
@@ -951,6 +981,9 @@ function normalizeFindEmailResult(data) {
951
981
  billingMetadata: data.billing_metadata,
952
982
  resultReturnedFrom: data.result_returned_from,
953
983
  cacheHit: data.cache_hit,
984
+ negativeCacheHit: data.negative_cache_hit,
985
+ negativeCacheTtlSeconds: data.negative_cache_ttl_seconds,
986
+ cacheTier: data.cache_tier,
954
987
  freshEnrichmentUsed: data.fresh_enrichment_used,
955
988
  liveProviderCalled: data.live_provider_called,
956
989
  openrouterCalled: data.openrouter_called,
@@ -972,6 +1005,7 @@ function normalizeVerifyEmailResult(email, data) {
972
1005
  nextPollAfterSeconds: data.next_poll_after_seconds,
973
1006
  isValid: data.is_valid ?? data.valid ?? verified,
974
1007
  isDeliverable: data.is_deliverable ?? data.deliverable ?? verified,
1008
+ isMalformed: data.is_malformed === true || data.verification_verdict === "malformed" || data.verification_status === "malformed" || data.deliverability_status === "malformed",
975
1009
  isCatchAll: data.email_is_catch_all ?? data.is_catch_all ?? data.catch_all ?? data.diagnostics?.is_catch_all ?? false,
976
1010
  verifiedForSending: data.verified_for_sending === true,
977
1011
  verificationVerdict: data.verification_verdict ?? data.verification_status ?? "unknown",
@@ -983,6 +1017,9 @@ function normalizeVerifyEmailResult(email, data) {
983
1017
  billingMetadata: data.billing_metadata,
984
1018
  resultReturnedFrom: data.result_returned_from,
985
1019
  cacheHit: data.cache_hit,
1020
+ negativeCacheHit: data.negative_cache_hit,
1021
+ negativeCacheTtlSeconds: data.negative_cache_ttl_seconds,
1022
+ cacheTier: data.cache_tier,
986
1023
  freshEnrichmentUsed: data.fresh_enrichment_used,
987
1024
  liveProviderCalled: data.live_provider_called,
988
1025
  openrouterCalled: data.openrouter_called,
@@ -1052,6 +1089,14 @@ function normalizeCompanySignalResult(params, data) {
1052
1089
  evidenceCountTotal: data.evidence_count_total,
1053
1090
  evidenceSourcesOmitted: data.evidence_sources_omitted,
1054
1091
  evidenceScope: data.evidence_scope,
1092
+ billingMetadata: data.billing_metadata,
1093
+ resultReturnedFrom: data.result_returned_from,
1094
+ cacheHit: data.cache_hit,
1095
+ freshEnrichmentUsed: data.fresh_enrichment_used,
1096
+ creditsUsed: data.credits_used,
1097
+ liveProviderCalled: data.live_provider_called,
1098
+ aiProviderCalled: data.ai_provider_called,
1099
+ byoAiUsed: data.byo_ai_used,
1055
1100
  raw: data
1056
1101
  };
1057
1102
  }
@@ -1233,6 +1278,75 @@ function dedupeExactBatchItems(items, keyForItem) {
1233
1278
  }
1234
1279
  return { uniqueItems, sourceIndexByInput };
1235
1280
  }
1281
+ function normalizedBatchText(value) {
1282
+ return typeof value === "string" ? value.trim().toLowerCase().replace(/\s+/g, " ") : "";
1283
+ }
1284
+ function normalizedBatchDomain(value) {
1285
+ return normalizedBatchText(value).replace(/^https?:\/\//, "").replace(/^www\./, "").split(/[/?#]/, 1)[0].replace(/\.+$/, "");
1286
+ }
1287
+ function normalizedBatchLinkedIn(value) {
1288
+ return normalizedBatchText(value).replace(/\/+$/, "");
1289
+ }
1290
+ function normalizedBatchList(value) {
1291
+ return Array.isArray(value) ? value.map(normalizedBatchText).filter(Boolean).sort() : [];
1292
+ }
1293
+ function normalizedFindEmailBatchName(request) {
1294
+ const fullName = normalizedBatchText(request.full_name);
1295
+ const componentName = [normalizedBatchText(request.first_name), normalizedBatchText(request.last_name)].filter(Boolean).join(" ");
1296
+ if (!fullName || !componentName || fullName === componentName) return fullName || componentName;
1297
+ return JSON.stringify([fullName, componentName]);
1298
+ }
1299
+ function coreProductBatchRequestKey(path, request) {
1300
+ if (path === "api/v1/find-email") {
1301
+ return JSON.stringify([
1302
+ normalizedFindEmailBatchName(request),
1303
+ normalizedBatchDomain(request.company_domain),
1304
+ normalizedBatchLinkedIn(request.linkedin_url),
1305
+ normalizedBatchText(request.company_name),
1306
+ request.skip_cache === true || request.bypass_cache === true
1307
+ ]);
1308
+ }
1309
+ if (path === "api/v1/verify-email") {
1310
+ return JSON.stringify([
1311
+ normalizedBatchText(request.email),
1312
+ request.skip_cache === true || request.bypass_cache === true,
1313
+ request.skip_catch_all_verification === true,
1314
+ request.skip_esp_check === true
1315
+ ]);
1316
+ }
1317
+ if (path === "api/v1/company-signals") {
1318
+ const online = request.online !== false;
1319
+ const deepSearch = request.enable_deep_search === void 0 ? online : request.enable_deep_search !== false;
1320
+ return JSON.stringify([
1321
+ normalizedBatchText(request.signal_run_id),
1322
+ normalizedBatchDomain(request.company_domain || request.domain),
1323
+ normalizedBatchText(request.company_name),
1324
+ normalizedBatchText(request.company_id),
1325
+ normalizedBatchLinkedIn(request.linkedin_url),
1326
+ normalizedBatchText(request.industry),
1327
+ typeof request.research_prompt === "string" ? request.research_prompt.trim() : "",
1328
+ normalizedBatchList(request.signal_types),
1329
+ Number(request.target_signal_count) || null,
1330
+ Number(request.lookback_days) || null,
1331
+ online,
1332
+ deepSearch,
1333
+ normalizedBatchText(request.search_mode || "balanced"),
1334
+ request.enable_outreach_intelligence === true,
1335
+ request.enable_predictive_intelligence === true,
1336
+ request.skip_cache === true || request.bypass_cache === true
1337
+ ]);
1338
+ }
1339
+ return JSON.stringify([
1340
+ normalizedBatchDomain(request.company_domain),
1341
+ typeof request.person_name === "string" ? request.person_name.trim() : "",
1342
+ typeof request.title === "string" ? request.title.trim() : "",
1343
+ typeof request.campaign_offer === "string" ? request.campaign_offer.trim() : "",
1344
+ typeof request.research_prompt === "string" ? request.research_prompt.trim() : "",
1345
+ typeof request.signal_run_id === "string" ? request.signal_run_id.trim() : "",
1346
+ request.skip_cache === true,
1347
+ request.enable_deep_search === true
1348
+ ]);
1349
+ }
1236
1350
  function finalizeCoreBatch(total, startedAt, round, normalize, options) {
1237
1351
  const results = new Array(total);
1238
1352
  for (const item of round) {
@@ -1329,6 +1443,14 @@ function normalizeSignalToCopyResult(data) {
1329
1443
  empty: data.empty,
1330
1444
  emptyReason: data.empty_reason,
1331
1445
  researchMetadata: data.research_metadata,
1446
+ billingMetadata: data.billing_metadata,
1447
+ resultReturnedFrom: data.result_returned_from,
1448
+ cacheHit: data.cache_hit,
1449
+ freshEnrichmentUsed: data.fresh_enrichment_used,
1450
+ creditsUsed: data.credits_used,
1451
+ liveProviderCalled: data.live_provider_called,
1452
+ aiProviderCalled: data.ai_provider_called,
1453
+ byoAiUsed: data.byo_ai_used,
1332
1454
  variations: (data.variations ?? []).map((variation) => ({
1333
1455
  style: variation.style,
1334
1456
  subjectLine: variation.subject_line ?? "",
package/dist/index.mjs CHANGED
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  Signaliz,
3
3
  SignalizError
4
- } from "./chunk-QXMVHZIQ.mjs";
4
+ } from "./chunk-SZ3H5W5L.mjs";
5
5
  export {
6
6
  Signaliz,
7
7
  SignalizError
@@ -563,11 +563,16 @@ var Signaliz = class {
563
563
  );
564
564
  }
565
565
  const startedAt = Date.now();
566
- const deduplicated = dedupeExactBatchItems(requests, (params) => JSON.stringify({
567
- request: signalToCopyRequestBody(params)
568
- }));
566
+ const deduplicated = dedupeExactBatchItems(
567
+ requests,
568
+ (params) => coreProductBatchRequestKey("api/v1/signal-to-copy", signalToCopyRequestBody(params))
569
+ );
569
570
  const uniqueRequests = deduplicated.uniqueItems;
570
- const uniqueResults = await this.runSignalCopyBatchChunks(uniqueRequests, options);
571
+ const uniqueInputIndices = new Array(uniqueRequests.length);
572
+ deduplicated.sourceIndexByInput.forEach((uniqueIndex, inputIndex) => {
573
+ if (uniqueInputIndices[uniqueIndex] === void 0) uniqueInputIndices[uniqueIndex] = inputIndex;
574
+ });
575
+ const uniqueResults = await this.runSignalCopyBatchChunks(uniqueRequests, options, uniqueInputIndices);
571
576
  const results = requests.map((_, index) => ({
572
577
  ...uniqueResults[deduplicated.sourceIndexByInput[index]],
573
578
  index
@@ -593,15 +598,25 @@ var Signaliz = class {
593
598
  }
594
599
  return normalizeCoreProductDryRunResult(data);
595
600
  }
596
- async runSignalCopyBatchChunks(uniqueRequests, options) {
601
+ async runSignalCopyBatchChunks(uniqueRequests, options, idempotencyItemIndices = uniqueRequests.map((_, index) => index)) {
597
602
  const chunks = [];
598
603
  for (let offset = 0; offset < uniqueRequests.length; offset += 25) {
599
- chunks.push({ offset, requests: uniqueRequests.slice(offset, offset + 25) });
604
+ chunks.push({
605
+ offset,
606
+ requests: uniqueRequests.slice(offset, offset + 25),
607
+ idempotencyItemIndices: idempotencyItemIndices.slice(offset, offset + 25)
608
+ });
600
609
  }
601
610
  const { serverConcurrency, chunkConcurrency } = batchConcurrencyPlan(options, 25, 25);
602
611
  const chunkBatch = await runBatch(
603
612
  chunks,
604
- (chunk) => this.runSignalCopyBatchChunk(chunk.requests, serverConcurrency),
613
+ (chunk) => this.runSignalCopyBatchChunk(
614
+ chunk.requests,
615
+ serverConcurrency,
616
+ 0,
617
+ options?.idempotencyKey,
618
+ chunk.idempotencyItemIndices
619
+ ),
605
620
  { concurrency: chunkConcurrency }
606
621
  );
607
622
  const uniqueResults = new Array(uniqueRequests.length);
@@ -623,14 +638,15 @@ var Signaliz = class {
623
638
  }
624
639
  return uniqueResults;
625
640
  }
626
- async runRecoverableCoreBatchJob(path2, requests, label, pageSize = 500, maxWaitMs = 20 * 6e4, idempotencyKey) {
641
+ async runRecoverableCoreBatchJob(path2, requests, label, pageSize = 500, maxWaitMs = 20 * 6e4, idempotencyKey, idempotencyItemIndices) {
627
642
  const startedAt = Date.now();
628
643
  const submissionIdempotencyKey = idempotencyKey?.trim() || newBatchIdempotencyKey(label);
629
644
  let submission;
630
645
  try {
631
646
  submission = await this.client.post(path2, {
632
647
  requests,
633
- idempotency_key: submissionIdempotencyKey
648
+ idempotency_key: submissionIdempotencyKey,
649
+ batch_item_indices: idempotencyItemIndices
634
650
  });
635
651
  } catch (error) {
636
652
  if (error instanceof SignalizError && !error.isRetryable) throw error;
@@ -693,12 +709,14 @@ var Signaliz = class {
693
709
  if (seen.size !== requests.length) throw new Error(`${label} batch returned incomplete indexed results`);
694
710
  return rows;
695
711
  }
696
- async runSignalCopyBatchChunk(requests, concurrency, rowRateLimitAttempt = 0) {
712
+ async runSignalCopyBatchChunk(requests, concurrency, rowRateLimitAttempt = 0, idempotencyKey, idempotencyItemIndices = requests.map((_, index) => index)) {
697
713
  const results = new Array(requests.length);
698
714
  const rateLimited = [];
699
715
  const data = await this.client.post("api/v1/signal-to-copy", {
700
716
  requests: requests.map(signalToCopyRequestBody),
701
- concurrency
717
+ concurrency,
718
+ idempotency_key: idempotencyKey,
719
+ batch_item_indices: idempotencyItemIndices
702
720
  });
703
721
  if (!Array.isArray(data.results) || data.results.length !== requests.length) {
704
722
  throw new Error("Signal to Copy batch returned an invalid result count");
@@ -733,7 +751,9 @@ var Signaliz = class {
733
751
  const retried = await this.runSignalCopyBatchChunk(
734
752
  rateLimited.map((item) => item.params),
735
753
  concurrency,
736
- rowRateLimitAttempt + 1
754
+ rowRateLimitAttempt + 1,
755
+ idempotencyKey,
756
+ rateLimited.map((item) => idempotencyItemIndices[item.index])
737
757
  );
738
758
  for (let index = 0; index < rateLimited.length; index += 1) {
739
759
  results[rateLimited[index].index] = { ...retried[index], index: rateLimited[index].index };
@@ -743,7 +763,10 @@ var Signaliz = class {
743
763
  }
744
764
  async runCoreBatchRound(path2, tasks, options, rowRateLimitAttempt = 0) {
745
765
  const { serverConcurrency, chunkConcurrency } = path2 === "api/v1/company-signals" ? batchConcurrencyPlan(options, 25, 25) : batchConcurrencyPlan(options);
746
- const deduplicated = dedupeExactBatchItems(tasks, (task) => JSON.stringify(task.request));
766
+ const deduplicated = dedupeExactBatchItems(
767
+ tasks,
768
+ (task) => coreProductBatchRequestKey(path2, task.request)
769
+ );
747
770
  const uniqueTasks = deduplicated.uniqueItems;
748
771
  const recoverableBatch = path2 === "api/v1/find-email" ? { label: "Find Email", pageSize: 500, maxWaitMs: 20 * 6e4 } : path2 === "api/v1/verify-email" ? { label: "Verify Email", pageSize: 500, maxWaitMs: 20 * 6e4 } : void 0;
749
772
  if (path2 !== "api/v1/company-signals" && recoverableBatch && uniqueTasks.length > 25) {
@@ -753,7 +776,8 @@ var Signaliz = class {
753
776
  recoverableBatch.label,
754
777
  recoverableBatch.pageSize,
755
778
  recoverableBatch.maxWaitMs,
756
- options?.idempotencyKey
779
+ options?.idempotencyKey,
780
+ uniqueTasks.map((task) => task.index)
757
781
  );
758
782
  const uniqueResults2 = new Array(uniqueTasks.length);
759
783
  for (const raw of rows) {
@@ -777,7 +801,9 @@ var Signaliz = class {
777
801
  const chunkBatch = await runBatch(chunks, async (chunk) => {
778
802
  const data = await this.client.post(path2, {
779
803
  requests: chunk.tasks.map((task) => task.request),
780
- concurrency: serverConcurrency
804
+ concurrency: serverConcurrency,
805
+ idempotency_key: options?.idempotencyKey,
806
+ batch_item_indices: chunk.tasks.map((task) => task.index)
781
807
  });
782
808
  if (!Array.isArray(data.results) || data.results.length !== chunk.tasks.length) {
783
809
  throw new Error(`${path2} batch returned an invalid result count`);
@@ -912,7 +938,11 @@ function normalizeCoreProductDryRunResult(data) {
912
938
  status: "planned",
913
939
  capability: String(data.capability || ""),
914
940
  inputCount: Number(data.input_count || 0),
941
+ freshInputCount: Number(data.fresh_input_count ?? data.input_count ?? 0),
942
+ uniqueFreshInputCount: Number(data.unique_fresh_input_count ?? data.fresh_input_count ?? data.input_count ?? 0),
943
+ inputDuplicatesSuppressed: Number(data.input_duplicates_suppressed || 0),
915
944
  recoveryReadCount: Number(data.recovery_read_count || 0),
945
+ uniqueRecoveryReadCount: Number(data.unique_recovery_read_count ?? data.recovery_read_count ?? 0),
916
946
  plan: data.plan && typeof data.plan === "object" ? data.plan : {},
917
947
  estimate: data.estimate && typeof data.estimate === "object" ? data.estimate : {},
918
948
  estimatedCredits: {
@@ -927,12 +957,12 @@ function normalizeCoreProductDryRunResult(data) {
927
957
  function normalizeFindEmailResult(data) {
928
958
  const email = typeof data.email === "string" && data.email.trim() ? data.email.trim() : null;
929
959
  const found = Boolean(email) && data.found !== false;
930
- const verificationStatus = data.verification_status ?? data.status ?? "unknown";
960
+ const verificationStatus = data.verification_status ?? (data.is_valid === true ? "valid" : data.status ?? "unknown");
931
961
  const rawFreshness = String(data.verification_freshness ?? "").toLowerCase();
932
962
  const verificationFreshness = rawFreshness === "fresh" || rawFreshness === "stale" ? rawFreshness : "unknown";
933
963
  const needsReverification = data.needs_reverification === true || verificationFreshness === "stale";
934
964
  const explicitlyNotSendSafe = data.verified_for_sending === false || needsReverification;
935
- const verified = !explicitlyNotSendSafe && (data.email_verified === true || data.verified_for_sending === true || ["valid", "deliverable"].includes(String(verificationStatus).toLowerCase()));
965
+ 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()));
936
966
  const verificationAgeDays = typeof data.verification_age_days === "number" && Number.isFinite(data.verification_age_days) ? data.verification_age_days : null;
937
967
  return {
938
968
  success: data.success !== false && found,
@@ -955,6 +985,9 @@ function normalizeFindEmailResult(data) {
955
985
  billingMetadata: data.billing_metadata,
956
986
  resultReturnedFrom: data.result_returned_from,
957
987
  cacheHit: data.cache_hit,
988
+ negativeCacheHit: data.negative_cache_hit,
989
+ negativeCacheTtlSeconds: data.negative_cache_ttl_seconds,
990
+ cacheTier: data.cache_tier,
958
991
  freshEnrichmentUsed: data.fresh_enrichment_used,
959
992
  liveProviderCalled: data.live_provider_called,
960
993
  openrouterCalled: data.openrouter_called,
@@ -976,6 +1009,7 @@ function normalizeVerifyEmailResult(email, data) {
976
1009
  nextPollAfterSeconds: data.next_poll_after_seconds,
977
1010
  isValid: data.is_valid ?? data.valid ?? verified,
978
1011
  isDeliverable: data.is_deliverable ?? data.deliverable ?? verified,
1012
+ isMalformed: data.is_malformed === true || data.verification_verdict === "malformed" || data.verification_status === "malformed" || data.deliverability_status === "malformed",
979
1013
  isCatchAll: data.email_is_catch_all ?? data.is_catch_all ?? data.catch_all ?? data.diagnostics?.is_catch_all ?? false,
980
1014
  verifiedForSending: data.verified_for_sending === true,
981
1015
  verificationVerdict: data.verification_verdict ?? data.verification_status ?? "unknown",
@@ -987,6 +1021,9 @@ function normalizeVerifyEmailResult(email, data) {
987
1021
  billingMetadata: data.billing_metadata,
988
1022
  resultReturnedFrom: data.result_returned_from,
989
1023
  cacheHit: data.cache_hit,
1024
+ negativeCacheHit: data.negative_cache_hit,
1025
+ negativeCacheTtlSeconds: data.negative_cache_ttl_seconds,
1026
+ cacheTier: data.cache_tier,
990
1027
  freshEnrichmentUsed: data.fresh_enrichment_used,
991
1028
  liveProviderCalled: data.live_provider_called,
992
1029
  openrouterCalled: data.openrouter_called,
@@ -1056,6 +1093,14 @@ function normalizeCompanySignalResult(params, data) {
1056
1093
  evidenceCountTotal: data.evidence_count_total,
1057
1094
  evidenceSourcesOmitted: data.evidence_sources_omitted,
1058
1095
  evidenceScope: data.evidence_scope,
1096
+ billingMetadata: data.billing_metadata,
1097
+ resultReturnedFrom: data.result_returned_from,
1098
+ cacheHit: data.cache_hit,
1099
+ freshEnrichmentUsed: data.fresh_enrichment_used,
1100
+ creditsUsed: data.credits_used,
1101
+ liveProviderCalled: data.live_provider_called,
1102
+ aiProviderCalled: data.ai_provider_called,
1103
+ byoAiUsed: data.byo_ai_used,
1059
1104
  raw: data
1060
1105
  };
1061
1106
  }
@@ -1237,6 +1282,75 @@ function dedupeExactBatchItems(items, keyForItem) {
1237
1282
  }
1238
1283
  return { uniqueItems, sourceIndexByInput };
1239
1284
  }
1285
+ function normalizedBatchText(value) {
1286
+ return typeof value === "string" ? value.trim().toLowerCase().replace(/\s+/g, " ") : "";
1287
+ }
1288
+ function normalizedBatchDomain(value) {
1289
+ return normalizedBatchText(value).replace(/^https?:\/\//, "").replace(/^www\./, "").split(/[/?#]/, 1)[0].replace(/\.+$/, "");
1290
+ }
1291
+ function normalizedBatchLinkedIn(value) {
1292
+ return normalizedBatchText(value).replace(/\/+$/, "");
1293
+ }
1294
+ function normalizedBatchList(value) {
1295
+ return Array.isArray(value) ? value.map(normalizedBatchText).filter(Boolean).sort() : [];
1296
+ }
1297
+ function normalizedFindEmailBatchName(request) {
1298
+ const fullName = normalizedBatchText(request.full_name);
1299
+ const componentName = [normalizedBatchText(request.first_name), normalizedBatchText(request.last_name)].filter(Boolean).join(" ");
1300
+ if (!fullName || !componentName || fullName === componentName) return fullName || componentName;
1301
+ return JSON.stringify([fullName, componentName]);
1302
+ }
1303
+ function coreProductBatchRequestKey(path2, request) {
1304
+ if (path2 === "api/v1/find-email") {
1305
+ return JSON.stringify([
1306
+ normalizedFindEmailBatchName(request),
1307
+ normalizedBatchDomain(request.company_domain),
1308
+ normalizedBatchLinkedIn(request.linkedin_url),
1309
+ normalizedBatchText(request.company_name),
1310
+ request.skip_cache === true || request.bypass_cache === true
1311
+ ]);
1312
+ }
1313
+ if (path2 === "api/v1/verify-email") {
1314
+ return JSON.stringify([
1315
+ normalizedBatchText(request.email),
1316
+ request.skip_cache === true || request.bypass_cache === true,
1317
+ request.skip_catch_all_verification === true,
1318
+ request.skip_esp_check === true
1319
+ ]);
1320
+ }
1321
+ if (path2 === "api/v1/company-signals") {
1322
+ const online = request.online !== false;
1323
+ const deepSearch = request.enable_deep_search === void 0 ? online : request.enable_deep_search !== false;
1324
+ return JSON.stringify([
1325
+ normalizedBatchText(request.signal_run_id),
1326
+ normalizedBatchDomain(request.company_domain || request.domain),
1327
+ normalizedBatchText(request.company_name),
1328
+ normalizedBatchText(request.company_id),
1329
+ normalizedBatchLinkedIn(request.linkedin_url),
1330
+ normalizedBatchText(request.industry),
1331
+ typeof request.research_prompt === "string" ? request.research_prompt.trim() : "",
1332
+ normalizedBatchList(request.signal_types),
1333
+ Number(request.target_signal_count) || null,
1334
+ Number(request.lookback_days) || null,
1335
+ online,
1336
+ deepSearch,
1337
+ normalizedBatchText(request.search_mode || "balanced"),
1338
+ request.enable_outreach_intelligence === true,
1339
+ request.enable_predictive_intelligence === true,
1340
+ request.skip_cache === true || request.bypass_cache === true
1341
+ ]);
1342
+ }
1343
+ return JSON.stringify([
1344
+ normalizedBatchDomain(request.company_domain),
1345
+ typeof request.person_name === "string" ? request.person_name.trim() : "",
1346
+ typeof request.title === "string" ? request.title.trim() : "",
1347
+ typeof request.campaign_offer === "string" ? request.campaign_offer.trim() : "",
1348
+ typeof request.research_prompt === "string" ? request.research_prompt.trim() : "",
1349
+ typeof request.signal_run_id === "string" ? request.signal_run_id.trim() : "",
1350
+ request.skip_cache === true,
1351
+ request.enable_deep_search === true
1352
+ ]);
1353
+ }
1240
1354
  function finalizeCoreBatch(total, startedAt, round, normalize, options) {
1241
1355
  const results = new Array(total);
1242
1356
  for (const item of round) {
@@ -1333,6 +1447,14 @@ function normalizeSignalToCopyResult(data) {
1333
1447
  empty: data.empty,
1334
1448
  emptyReason: data.empty_reason,
1335
1449
  researchMetadata: data.research_metadata,
1450
+ billingMetadata: data.billing_metadata,
1451
+ resultReturnedFrom: data.result_returned_from,
1452
+ cacheHit: data.cache_hit,
1453
+ freshEnrichmentUsed: data.fresh_enrichment_used,
1454
+ creditsUsed: data.credits_used,
1455
+ liveProviderCalled: data.live_provider_called,
1456
+ aiProviderCalled: data.ai_provider_called,
1457
+ byoAiUsed: data.byo_ai_used,
1336
1458
  variations: (data.variations ?? []).map((variation) => ({
1337
1459
  style: variation.style,
1338
1460
  subjectLine: variation.subject_line ?? "",
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  Signaliz
4
- } from "./chunk-QXMVHZIQ.mjs";
4
+ } from "./chunk-SZ3H5W5L.mjs";
5
5
 
6
6
  // src/mcp-config.ts
7
7
  import * as fs from "fs";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@signaliz/sdk",
3
- "version": "1.0.55",
3
+ "version": "1.0.57",
4
4
  "description": "Signaliz SDK for Signals Everything, Find Email, Verify Email, Company Signal Enrichment, and Signal to Copy AI.",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.mjs",