@attrkit/mcp 0.4.6 → 1.0.0

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.
Files changed (4) hide show
  1. package/CHANGELOG.md +125 -0
  2. package/README.md +24 -5
  3. package/dist/cli.js +1520 -457
  4. package/package.json +6 -4
package/dist/cli.js CHANGED
@@ -5,7 +5,14 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
5
5
 
6
6
  // src/index.ts
7
7
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
8
- import { z as z4 } from "zod";
8
+ import { randomUUID } from "node:crypto";
9
+ import { z as z5 } from "zod";
10
+
11
+ // ../shared/src/json-contracts.ts
12
+ import { z } from "zod";
13
+ var jsonValueSchema = z.json();
14
+ var jsonObjectSchema = z.record(z.string(), jsonValueSchema);
15
+ var nonEmptyStringSchema = z.string().min(1);
9
16
  // ../shared/src/device-matching.ts
10
17
  import { isIP } from "node:net";
11
18
  // ../shared/ip-class-ranges.json
@@ -13634,7 +13641,9 @@ function ipv6ToBigInt(value) {
13634
13641
  const [head, tail = ""] = value.split("::");
13635
13642
  const headGroups = head ? head.split(":") : [];
13636
13643
  const tailGroups = tail ? tail.split(":") : [];
13637
- const missing = 8 - headGroups.length - tailGroups.length;
13644
+ const groupWidth = (group) => group.includes(".") ? 2 : 1;
13645
+ const explicitGroups = [...headGroups, ...tailGroups].reduce((sum, group) => sum + groupWidth(group), 0);
13646
+ const missing = 8 - explicitGroups;
13638
13647
  if (missing < 0 || missing > 0 && !value.includes("::"))
13639
13648
  return;
13640
13649
  let n = 0n;
@@ -13650,22 +13659,30 @@ function ipv6ToBigInt(value) {
13650
13659
  }
13651
13660
  return n;
13652
13661
  }
13653
- function parseRanges(entries) {
13662
+ var CIDR_PREFIX = /^(?:0|[1-9]\d{0,2})$/;
13663
+ function parseCidr(entry, label) {
13664
+ const parts = entry.split("/");
13665
+ const network = parts[0];
13666
+ const rawPrefix = parts[1];
13667
+ if (parts.length !== 2 || !network || rawPrefix === undefined || !CIDR_PREFIX.test(rawPrefix)) {
13668
+ throw new Error(`Invalid ${label} CIDR entry: ${entry}`);
13669
+ }
13670
+ const prefix = Number(rawPrefix);
13671
+ const v6 = network.includes(":");
13672
+ const bits = v6 ? 128 : 32;
13673
+ if (prefix > bits)
13674
+ throw new Error(`Invalid ${label} CIDR prefix: ${entry}`);
13675
+ const base = v6 ? ipv6ToBigInt(network) : ipv4ToBigInt(network);
13676
+ if (base === undefined)
13677
+ throw new Error(`Invalid ${label} CIDR address: ${entry}`);
13678
+ return { base, prefix, bits, v6 };
13679
+ }
13680
+ function parseRanges(entries, label) {
13654
13681
  const parsed = { v4: [], v6: [] };
13655
13682
  for (const entry of entries) {
13656
13683
  if (entry.startsWith("_TODO"))
13657
13684
  continue;
13658
- const [network, rawPrefix] = entry.split("/");
13659
- const prefix = Number(rawPrefix);
13660
- if (!network || !Number.isInteger(prefix))
13661
- continue;
13662
- const v6 = network.includes(":");
13663
- const bits = v6 ? 128 : 32;
13664
- if (prefix < 0 || prefix > bits)
13665
- continue;
13666
- const base = v6 ? ipv6ToBigInt(network) : ipv4ToBigInt(network);
13667
- if (base === undefined)
13668
- continue;
13685
+ const { base, prefix, bits, v6 } = parseCidr(entry, label);
13669
13686
  const hostBits = BigInt(bits - prefix);
13670
13687
  const start = base >> hostBits << hostBits;
13671
13688
  (v6 ? parsed.v6 : parsed.v4).push([start, start + (1n << hostBits) - 1n]);
@@ -13689,10 +13706,12 @@ function mergeIntervals(intervals) {
13689
13706
  return merged;
13690
13707
  }
13691
13708
  var CLASS_LISTS = [
13692
- { ipClass: "cgnat", parsed: parseRanges(ip_class_ranges_default.cgnat) },
13693
- { ipClass: "relay", parsed: parseRanges(ip_class_ranges_default.apple_private_relay_egress) }
13709
+ { ipClass: "cgnat", parsed: parseRanges(ip_class_ranges_default.cgnat, "cgnat") },
13710
+ { ipClass: "relay", parsed: parseRanges(ip_class_ranges_default.apple_private_relay_egress, "relay") }
13694
13711
  ];
13695
13712
  var IP_CLASS_RANGES_VERSION = ip_class_ranges_default.ranges_version;
13713
+ // ../shared/src/canonical-events.ts
13714
+ var LEDGER_ONLY_CANONICAL_EVENT_NAMES = Object.freeze(["refund_reversed"]);
13696
13715
  // ../shared/src/derived-events.ts
13697
13716
  var TWO_HOURS_MS = 2 * 60 * 60 * 1000;
13698
13717
  var STRICT_TRIAL_INVALIDATORS = Object.freeze([
@@ -13702,11 +13721,13 @@ var STRICT_TRIAL_INVALIDATORS = Object.freeze([
13702
13721
  "chargeback",
13703
13722
  "subscription_expired"
13704
13723
  ]);
13705
- function leg(armedBy, windowMs = 0, invalidatedBy = []) {
13724
+ function leg(armedBy, windowMs = 0, invalidatedBy = [], requiresPrior = [], armedWhen = []) {
13706
13725
  return Object.freeze({
13707
13726
  armedBy: Object.freeze([...armedBy]),
13708
13727
  windowMs,
13709
- invalidatedBy: Object.freeze([...invalidatedBy])
13728
+ invalidatedBy: Object.freeze([...invalidatedBy]),
13729
+ requiresPrior: Object.freeze([...requiresPrior]),
13730
+ armedWhen: Object.freeze(armedWhen.map((match) => Object.freeze({ ...match })))
13710
13731
  });
13711
13732
  }
13712
13733
  function rule(name, family, legs, fanoutEligible = true) {
@@ -13727,7 +13748,8 @@ var DERIVED_EVENT_RULES = Object.freeze([
13727
13748
  leg(["trial_started"], TWO_HOURS_MS, STRICT_TRIAL_INVALIDATORS)
13728
13749
  ]),
13729
13750
  rule("rc_trial_converted", "trial", [
13730
- leg(["subscription_renewed"])
13751
+ leg(["subscription_renewed"], 0, [], ["trial_started"]),
13752
+ leg(["subscription_renewed"], 0, [], [], [{ key: "is_trial_conversion", equals: "true" }])
13731
13753
  ]),
13732
13754
  rule("rc_intro_started", "trial", [
13733
13755
  leg(["intro_started"])
@@ -13761,7 +13783,7 @@ var DERIVED_EVENT_RULES = Object.freeze([
13761
13783
  ])
13762
13784
  ]);
13763
13785
  // ../shared/src/ownership.ts
13764
- import { z } from "zod";
13786
+ import { z as z2 } from "zod";
13765
13787
  var EVENT_FAMILIES = [
13766
13788
  "install",
13767
13789
  "session",
@@ -13773,13 +13795,13 @@ var EVENT_FAMILIES = [
13773
13795
  "business"
13774
13796
  ];
13775
13797
  var DELIVERY_OWNERS = ["attrkit", "revenuecat", "customer"];
13776
- var eventFamilySchema = z.enum(EVENT_FAMILIES);
13777
- var deliveryOwnerSchema = z.enum(DELIVERY_OWNERS);
13778
- var ownershipMatrixSchema = z.record(eventFamilySchema, deliveryOwnerSchema);
13779
- var destinationActivationSchema = z.object({
13780
- enabled: z.boolean(),
13781
- revenuecat_connected: z.boolean().default(false),
13782
- ownership: z.partialRecord(eventFamilySchema, deliveryOwnerSchema)
13798
+ var eventFamilySchema = z2.enum(EVENT_FAMILIES);
13799
+ var deliveryOwnerSchema = z2.enum(DELIVERY_OWNERS);
13800
+ var ownershipMatrixSchema = z2.record(eventFamilySchema, deliveryOwnerSchema);
13801
+ var destinationActivationSchema = z2.object({
13802
+ enabled: z2.boolean(),
13803
+ revenuecat_connected: z2.boolean().default(false),
13804
+ ownership: z2.partialRecord(eventFamilySchema, deliveryOwnerSchema)
13783
13805
  }).strict().superRefine((value, context) => {
13784
13806
  if (!value.enabled)
13785
13807
  return;
@@ -13799,6 +13821,8 @@ var HKDF_EMPTY_SALT = Buffer.alloc(0);
13799
13821
  // ../shared/src/management-key.ts
13800
13822
  var MANAGEMENT_KEY_HMAC_INFO = Buffer.from("attrkit/management-key-hmac/v1", "utf8");
13801
13823
  var HKDF_EMPTY_SALT2 = Buffer.alloc(0);
13824
+ // ../shared/src/attribution-context.ts
13825
+ var NO_MEASUREMENT_BASIS = new Set(["unknown", "denied", "revoked"]);
13802
13826
  // ../shared/src/ssrf-guard.ts
13803
13827
  import { BlockList, isIP as isIP2 } from "node:net";
13804
13828
  var blockedV4 = new BlockList;
@@ -13811,8 +13835,11 @@ for (const [network, prefix] of [
13811
13835
  ["169.254.0.0", 16],
13812
13836
  ["172.16.0.0", 12],
13813
13837
  ["192.0.0.0", 24],
13838
+ ["192.0.2.0", 24],
13814
13839
  ["192.168.0.0", 16],
13815
13840
  ["198.18.0.0", 15],
13841
+ ["198.51.100.0", 24],
13842
+ ["203.0.113.0", 24],
13816
13843
  ["224.0.0.0", 4],
13817
13844
  ["240.0.0.0", 4]
13818
13845
  ]) {
@@ -13823,6 +13850,12 @@ for (const [network, prefix] of [
13823
13850
  ["::1", 128],
13824
13851
  ["::", 96],
13825
13852
  ["::ffff:0:0", 96],
13853
+ ["64:ff9b::", 96],
13854
+ ["64:ff9b:1::", 48],
13855
+ ["100::", 64],
13856
+ ["2001::", 23],
13857
+ ["2002::", 16],
13858
+ ["fec0::", 10],
13826
13859
  ["fc00::", 7],
13827
13860
  ["fe80::", 10],
13828
13861
  ["ff00::", 8],
@@ -13837,124 +13870,160 @@ var blockedHostnames = new Set([
13837
13870
  "metadata.google.internal"
13838
13871
  ]);
13839
13872
  // ../shared/src/ingestion.ts
13840
- import { z as z2 } from "zod";
13841
- var consentStateSchema = z2.object({
13842
- state: z2.enum(["unknown", "measurement_granted", "tracking_granted", "denied", "revoked"]),
13843
- policy_version: z2.number().int().positive(),
13844
- framework_string: z2.string().max(512).optional()
13845
- }).strict();
13846
- var coarseContextSchema = z2.object({
13847
- country_code: z2.string().regex(/^[A-Z]{2}$/).optional(),
13848
- region_code: z2.string().max(16).optional(),
13849
- os_major: z2.string().max(16).optional(),
13850
- device_class: z2.enum(["phone", "tablet", "desktop", "unknown"]).optional(),
13851
- locale: z2.string().max(35).optional()
13873
+ import { z as z3 } from "zod";
13874
+ var consentStateSchema = z3.object({
13875
+ state: z3.enum(["unknown", "measurement_granted", "tracking_granted", "denied", "revoked"]),
13876
+ policy_version: z3.number().int().positive(),
13877
+ framework_string: z3.string().max(512).optional()
13852
13878
  }).strict();
13853
- var exactTokenReferenceSchema = z2.object({
13854
- token: z2.string().min(16).max(512),
13855
- kind: z2.enum(["clipboard", "owned_deferred", "customer_signed"]),
13856
- clipboard_opt_in: z2.boolean().optional()
13879
+ var coarseContextSchema = z3.object({
13880
+ country_code: z3.string().regex(/^[A-Z]{2}$/).optional(),
13881
+ region_code: z3.string().max(16).optional(),
13882
+ os_major: z3.string().max(16).optional(),
13883
+ device_class: z3.enum(["phone", "tablet", "desktop", "unknown"]).optional(),
13884
+ locale: z3.string().max(35).optional()
13857
13885
  }).strict();
13858
- var playInstallReferrerSchema = z2.object({
13859
- referrer: z2.string().max(1024),
13860
- captured_at: z2.string().datetime({ offset: true })
13886
+ var exactTokenReferenceSchema = z3.object({
13887
+ token: z3.string().min(16).max(512),
13888
+ kind: z3.enum(["clipboard", "owned_deferred", "customer_signed"]),
13889
+ clipboard_opt_in: z3.boolean().optional()
13890
+ }).strict().refine((value) => value.kind !== "clipboard" || value.clipboard_opt_in === true, { message: "clipboard tokens require explicit opt-in", path: ["clipboard_opt_in"] });
13891
+ var playInstallReferrerSchema = z3.object({
13892
+ referrer: z3.string().max(1024),
13893
+ captured_at: z3.string().datetime({ offset: true })
13861
13894
  }).strict();
13862
13895
  var NIL_UUID = "00000000-0000-0000-0000-000000000000";
13863
- var sha256HexSchema = z2.string().regex(/^[0-9a-f]{64}$/);
13864
- var idfaSchema = z2.string().uuid().refine((value) => value.toLowerCase() !== NIL_UUID, { message: "IDFA must not be all zeros" }).transform((value) => value.toLowerCase());
13865
- var idfvSchema = z2.string().uuid().transform((value) => value.toLowerCase());
13866
- var genderSchema = z2.enum(["m", "f"]).nullable();
13867
- var deviceSignalsSchema = z2.object({
13868
- device_model: z2.string().min(1).max(128).optional(),
13869
- os_build: z2.string().min(1).max(128).optional(),
13870
- timezone: z2.string().min(1).max(128).optional(),
13871
- screen: z2.object({
13872
- w: z2.number().int().positive().max(16384),
13873
- h: z2.number().int().positive().max(16384),
13874
- scale: z2.number().positive().max(8)
13896
+ var EMAIL_PATTERN = /\b[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}\b/i;
13897
+ var PHONE_PATTERN = /(?:^|\D)(?:\+?\d[\d\s().-]{7,}\d)(?:$|\D)/;
13898
+ var NON_PHONE_VALUE_FORMS = [
13899
+ /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i,
13900
+ /^\d{4}-\d{2}-\d{2}(?:[T ]\d{2}:\d{2}(?::\d{2}(?:\.\d+)?)?(?:Z|[+-]\d{2}:?\d{2})?)?$/,
13901
+ /^-?\d+\.\d+$/
13902
+ ];
13903
+ function looksLikePii(value) {
13904
+ if (EMAIL_PATTERN.test(value))
13905
+ return true;
13906
+ if (NON_PHONE_VALUE_FORMS.some((form) => form.test(value.trim())))
13907
+ return false;
13908
+ return PHONE_PATTERN.test(value.replace(EMBEDDED_UUID_PATTERN, ""));
13909
+ }
13910
+ var EMBEDDED_UUID_PATTERN = /[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/gi;
13911
+ var sha256HexSchema = z3.string().regex(/^[0-9a-f]{64}$/);
13912
+ var idfaSchema = z3.string().uuid().refine((value) => value.toLowerCase() !== NIL_UUID, { message: "IDFA must not be all zeros" }).transform((value) => value.toLowerCase());
13913
+ var idfvSchema = z3.string().uuid().transform((value) => value.toLowerCase());
13914
+ var genderSchema = z3.enum(["m", "f"]).nullable();
13915
+ var deviceSignalsSchema = z3.object({
13916
+ device_model: z3.string().min(1).max(128).optional(),
13917
+ os_build: z3.string().min(1).max(128).optional(),
13918
+ timezone: z3.string().min(1).max(128).optional(),
13919
+ screen: z3.object({
13920
+ w: z3.number().int().positive().max(16384),
13921
+ h: z3.number().int().positive().max(16384),
13922
+ scale: z3.number().positive().max(8)
13875
13923
  }).strict().optional(),
13876
- languages: z2.array(z2.string().min(1).max(35)).max(16).optional()
13924
+ languages: z3.array(z3.string().min(1).max(35)).max(16).optional()
13877
13925
  }).strict();
13878
- var webFirstPartySchema = z2.object({
13879
- fbc: z2.string().min(1).max(512).optional(),
13880
- fbp: z2.string().min(1).max(512).optional(),
13926
+ var TIKTOK_BROWSER_ID_PATTERN = /^[A-Za-z0-9._~-]{1,512}$/;
13927
+ var webFirstPartySchema = z3.object({
13928
+ fbc: z3.string().min(1).max(512).optional(),
13929
+ fbp: z3.string().min(1).max(512).optional(),
13930
+ ttp: z3.string().regex(TIKTOK_BROWSER_ID_PATTERN).optional(),
13881
13931
  email_hash: sha256HexSchema.optional(),
13882
13932
  phone_hash: sha256HexSchema.optional(),
13883
13933
  dob_hash: sha256HexSchema.optional(),
13884
13934
  gender: genderSchema.optional()
13885
13935
  }).strict();
13886
- var firstOpenEnvelopeSchema = z2.object({
13887
- schema_version: z2.literal(1),
13888
- installation_id: z2.string().uuid(),
13889
- install_epoch_id: z2.string().uuid(),
13890
- occurred_at: z2.string().datetime({ offset: true }),
13891
- app_version: z2.string().min(1).max(128),
13936
+ var firstOpenEnvelopeSchema = z3.object({
13937
+ schema_version: z3.literal(1),
13938
+ installation_id: z3.string().uuid(),
13939
+ install_epoch_id: z3.string().uuid(),
13940
+ occurred_at: z3.string().datetime({ offset: true }),
13941
+ app_version: z3.string().min(1).max(128),
13892
13942
  coarse_context: coarseContextSchema,
13893
13943
  consent: consentStateSchema,
13894
- app_transaction_jws: z2.string().min(16).max(32768).optional(),
13895
- asa_token: z2.string().min(8).max(8192).optional(),
13944
+ app_transaction_jws: z3.string().min(16).max(32768).optional(),
13945
+ asa_token: z3.string().min(8).max(8192).optional(),
13896
13946
  exact_token_ref: exactTokenReferenceSchema.optional(),
13897
13947
  play_install_referrer: playInstallReferrerSchema.optional(),
13898
13948
  device_signals: deviceSignalsSchema.optional(),
13899
13949
  web_first_party: webFirstPartySchema.optional(),
13900
13950
  idfa: idfaSchema.optional(),
13901
13951
  idfv: idfvSchema.optional(),
13902
- local_lineage_present: z2.boolean().default(false),
13903
- local_epoch_present: z2.boolean().default(false),
13904
- local_signals_conflict: z2.boolean().default(false)
13952
+ local_lineage_present: z3.boolean().default(false),
13953
+ local_epoch_present: z3.boolean().default(false),
13954
+ local_signals_conflict: z3.boolean().default(false)
13905
13955
  }).strict().refine((value) => value.idfa === undefined || value.consent.state === "tracking_granted", { message: "IDFA requires ATT tracking authorization", path: ["idfa"] });
13906
- var identifyEnvelopeSchema = z2.object({
13907
- schema_version: z2.literal(1),
13908
- installation_id: z2.string().uuid(),
13909
- install_epoch_id: z2.string().uuid(),
13910
- occurred_at: z2.string().datetime({ offset: true }),
13911
- customer_user_id: z2.string().min(1).max(256).refine((value) => !value.includes("@"), "customer_user_id must be an opaque identifier").optional(),
13956
+ var customerUserIdSchema = z3.string().min(1).max(256).refine((value) => !looksLikePii(value), "customer_user_id must be an opaque identifier, not raw email or phone PII");
13957
+ var identifyEnvelopeSchema = z3.object({
13958
+ schema_version: z3.literal(1),
13959
+ installation_id: z3.string().uuid(),
13960
+ install_epoch_id: z3.string().uuid(),
13961
+ occurred_at: z3.string().datetime({ offset: true }),
13962
+ customer_user_id: customerUserIdSchema.optional(),
13912
13963
  email_hash: sha256HexSchema.optional(),
13913
13964
  phone_hash: sha256HexSchema.optional(),
13914
13965
  exact_token_ref: exactTokenReferenceSchema.optional(),
13915
13966
  idfa: idfaSchema.optional(),
13916
13967
  idfv: idfvSchema.optional()
13917
13968
  }).strict().refine((value) => value.customer_user_id !== undefined || value.email_hash !== undefined || value.phone_hash !== undefined || value.exact_token_ref !== undefined || value.idfa !== undefined || value.idfv !== undefined, { message: "at least one opaque or hashed identity, platform identifier, or exact token is required" });
13918
- var eventConsentSchema = z2.object({
13919
- measurement: z2.enum(["granted", "denied", "unknown"]),
13920
- tracking: z2.enum(["granted", "denied", "unknown"]),
13921
- policy_version: z2.number().int().positive()
13969
+ var eventConsentSchema = z3.object({
13970
+ measurement: z3.enum(["granted", "denied", "unknown"]),
13971
+ tracking: z3.enum(["granted", "denied", "unknown"]),
13972
+ policy_version: z3.number().int().positive()
13922
13973
  }).strict();
13923
- var EMAIL_PATTERN = /\b[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}\b/i;
13924
- var PHONE_PATTERN = /(?:^|\D)(?:\+?\d[\d\s().-]{7,}\d)(?:$|\D)/;
13925
- var propertyScalarSchema = z2.union([
13926
- z2.string().max(1024),
13927
- z2.number().finite(),
13928
- z2.boolean(),
13929
- z2.null()
13974
+ var propertyScalarSchema = z3.union([
13975
+ z3.string().max(1024),
13976
+ z3.number().finite(),
13977
+ z3.boolean(),
13978
+ z3.null()
13930
13979
  ]);
13931
- var eventPropertiesSchema = z2.record(z2.string().min(1).max(64), propertyScalarSchema).superRefine((properties, context) => {
13980
+ var piiCandidateSchema = z3.string();
13981
+ function isPiiPropertyKey(key) {
13982
+ if (/email|e-mail|phone|mobile|address/i.test(key))
13983
+ return true;
13984
+ const normalized = key.replace(/([a-z0-9])([A-Z])/g, "$1_$2").replaceAll("-", "_").toLowerCase();
13985
+ return /^(?:name|first_?name|last_?name|full_?name|given_?name|family_?name)$/.test(normalized);
13986
+ }
13987
+ var eventPropertiesSchema = z3.record(z3.string().min(1).max(64), propertyScalarSchema).superRefine((properties, context) => {
13988
+ if (Object.keys(properties).length > 64) {
13989
+ context.addIssue({ code: "custom", message: "at most 64 event properties are allowed" });
13990
+ }
13932
13991
  for (const [key, value] of Object.entries(properties)) {
13933
- if (/email|e-mail|phone|mobile|address|name/i.test(key)) {
13992
+ if (isPiiPropertyKey(key)) {
13934
13993
  context.addIssue({ code: "custom", path: [key], message: "PII property keys are forbidden" });
13935
13994
  }
13936
- if (typeof value === "string" && (EMAIL_PATTERN.test(value) || PHONE_PATTERN.test(value))) {
13995
+ const piiCandidate = piiCandidateSchema.safeParse(value);
13996
+ if (piiCandidate.success && looksLikePii(piiCandidate.data)) {
13937
13997
  context.addIssue({ code: "custom", path: [key], message: "PII-shaped property values are forbidden" });
13938
13998
  }
13939
13999
  }
13940
14000
  });
13941
- var canonicalEventEnvelopeSchema = z2.object({
13942
- schema_version: z2.literal(1),
13943
- event_id: z2.string().uuid(),
13944
- event_name: z2.string().min(1).max(128).regex(/^[a-z][a-z0-9_.-]*$/),
13945
- event_version: z2.number().int().positive(),
13946
- occurred_at: z2.string().datetime({ offset: true }),
13947
- sent_at: z2.string().datetime({ offset: true }),
13948
- installation_id: z2.string().uuid(),
13949
- install_epoch_id: z2.string().uuid(),
13950
- session_id: z2.string().uuid(),
13951
- source: z2.enum(["ios_sdk", "android_sdk", "web_sdk", "server", "provider"]),
14001
+ var canonicalEventFieldSchemas = {
14002
+ event_id: z3.string().uuid(),
14003
+ event_name: z3.string().min(1).max(128).regex(/^[a-z][a-z0-9_.-]*$/),
14004
+ event_version: z3.number().int().positive(),
14005
+ occurred_at: z3.string().datetime({ offset: true }),
13952
14006
  consent: eventConsentSchema,
13953
- properties: eventPropertiesSchema,
13954
- provider: z2.string().min(1).max(64).optional(),
13955
- provider_event_id: z2.string().min(1).max(256).optional(),
13956
- supersedes_event_id: z2.string().uuid().optional()
14007
+ properties: eventPropertiesSchema
14008
+ };
14009
+ var canonicalEventEnvelopeSchema = z3.object({
14010
+ schema_version: z3.literal(1),
14011
+ event_id: canonicalEventFieldSchemas.event_id,
14012
+ event_name: canonicalEventFieldSchemas.event_name,
14013
+ event_version: canonicalEventFieldSchemas.event_version,
14014
+ occurred_at: canonicalEventFieldSchemas.occurred_at,
14015
+ sent_at: z3.string().datetime({ offset: true }),
14016
+ installation_id: z3.string().uuid(),
14017
+ install_epoch_id: z3.string().uuid(),
14018
+ session_id: z3.string().uuid(),
14019
+ source: z3.enum(["ios_sdk", "android_sdk", "web_sdk", "server", "provider"]),
14020
+ consent: canonicalEventFieldSchemas.consent,
14021
+ properties: canonicalEventFieldSchemas.properties,
14022
+ provider: z3.string().min(1).max(64).optional(),
14023
+ provider_event_id: z3.string().min(1).max(256).optional(),
14024
+ supersedes_event_id: z3.string().uuid().optional()
13957
14025
  }).strict().superRefine((event, context) => {
14026
+ const hasProviderFields = event.provider !== undefined || event.provider_event_id !== undefined;
13958
14027
  if (event.provider === undefined !== (event.provider_event_id === undefined)) {
13959
14028
  context.addIssue({
13960
14029
  code: "custom",
@@ -13962,26 +14031,40 @@ var canonicalEventEnvelopeSchema = z2.object({
13962
14031
  message: "provider and provider_event_id must be supplied together"
13963
14032
  });
13964
14033
  }
14034
+ if (event.source === "provider" && !hasProviderFields) {
14035
+ context.addIssue({
14036
+ code: "custom",
14037
+ path: ["provider"],
14038
+ message: "provider-sourced events require provider and provider_event_id"
14039
+ });
14040
+ }
14041
+ if (event.source !== "provider" && hasProviderFields) {
14042
+ context.addIssue({
14043
+ code: "custom",
14044
+ path: ["provider"],
14045
+ message: "provider fields are forbidden unless source is provider"
14046
+ });
14047
+ }
13965
14048
  });
13966
- var canonicalEventBatchSchema = z2.object({
13967
- batch_id: z2.string().min(1).max(256),
13968
- events: z2.array(canonicalEventEnvelopeSchema).min(1).max(100)
14049
+ var canonicalEventBatchSchema = z3.object({
14050
+ batch_id: z3.string().min(1).max(256),
14051
+ events: z3.array(canonicalEventEnvelopeSchema).min(1).max(100)
13969
14052
  }).strict();
13970
- var consentReceiptSchema = z2.object({
13971
- installation_id: z2.string().uuid(),
13972
- install_epoch_id: z2.string().uuid(),
13973
- scope: z2.enum(["measurement", "tracking"]),
14053
+ var consentReceiptSchema = z3.object({
14054
+ installation_id: z3.string().uuid(),
14055
+ install_epoch_id: z3.string().uuid(),
14056
+ scope: z3.enum(["measurement", "tracking"]),
13974
14057
  consent: consentStateSchema,
13975
- occurred_at: z2.string().datetime({ offset: true }),
13976
- source: z2.enum(["ios_sdk", "android_sdk", "web_sdk", "server"])
14058
+ occurred_at: z3.string().datetime({ offset: true }),
14059
+ source: z3.enum(["ios_sdk", "android_sdk", "web_sdk", "server"])
13977
14060
  }).strict();
13978
- var sealedIngestEnvelopeSchema = z2.object({
14061
+ var sealedIngestEnvelopeSchema = z3.object({
13979
14062
  input: firstOpenEnvelopeSchema,
13980
- received_at: z2.string().datetime({ offset: true }),
13981
- client_skew_ms: z2.number().int()
14063
+ received_at: z3.string().datetime({ offset: true }),
14064
+ client_skew_ms: z3.number().int()
13982
14065
  }).strict();
13983
14066
  // ../shared/src/revenuecat-provider-app-id.ts
13984
- import { z as z3 } from "zod";
14067
+ import { z as z4 } from "zod";
13985
14068
  var KNOWN_WRONG_CREDENTIAL_PREFIXES = [
13986
14069
  ["appl_", "the iOS platform SDK key"],
13987
14070
  ["goog_", "the Android platform SDK key"],
@@ -14000,159 +14083,995 @@ function revenueCatProviderAppIdIssue(value) {
14000
14083
  }
14001
14084
  return null;
14002
14085
  }
14003
- var revenueCatProviderAppIdSchema = z3.string().trim().min(1).max(255).superRefine((value, context) => {
14086
+ var revenueCatProviderAppIdSchema = z4.string().trim().min(1).max(255).superRefine((value, context) => {
14004
14087
  const issue = revenueCatProviderAppIdIssue(value);
14005
14088
  if (issue)
14006
14089
  context.addIssue({ code: "custom", message: issue });
14007
14090
  });
14091
+ // ../shared/src/iso-4217-exponents.json
14092
+ var iso_4217_exponents_default = {
14093
+ schemaVersion: 1,
14094
+ published: "2026-01-01",
14095
+ sources: {
14096
+ listOneSha256: "838dfb991648cf36df939edd5fe3811737962b75a32252847d239cedd1e291c9",
14097
+ listThreeSha256: "98fde2423cdb916dd59dcf5fe96222edad8fa198d865c1c83dbc464b9cc52387"
14098
+ },
14099
+ currencies: {
14100
+ AED: {
14101
+ exponent: 2,
14102
+ status: "current"
14103
+ },
14104
+ AFN: {
14105
+ exponent: 2,
14106
+ status: "current"
14107
+ },
14108
+ ALL: {
14109
+ exponent: 2,
14110
+ status: "current"
14111
+ },
14112
+ AMD: {
14113
+ exponent: 2,
14114
+ status: "current"
14115
+ },
14116
+ AOA: {
14117
+ exponent: 2,
14118
+ status: "current"
14119
+ },
14120
+ ARS: {
14121
+ exponent: 2,
14122
+ status: "current"
14123
+ },
14124
+ AUD: {
14125
+ exponent: 2,
14126
+ status: "current"
14127
+ },
14128
+ AWG: {
14129
+ exponent: 2,
14130
+ status: "current"
14131
+ },
14132
+ AZN: {
14133
+ exponent: 2,
14134
+ status: "current"
14135
+ },
14136
+ BAM: {
14137
+ exponent: 2,
14138
+ status: "current"
14139
+ },
14140
+ BBD: {
14141
+ exponent: 2,
14142
+ status: "current"
14143
+ },
14144
+ BDT: {
14145
+ exponent: 2,
14146
+ status: "current"
14147
+ },
14148
+ BHD: {
14149
+ exponent: 3,
14150
+ status: "current"
14151
+ },
14152
+ BIF: {
14153
+ exponent: 0,
14154
+ status: "current"
14155
+ },
14156
+ BMD: {
14157
+ exponent: 2,
14158
+ status: "current"
14159
+ },
14160
+ BND: {
14161
+ exponent: 2,
14162
+ status: "current"
14163
+ },
14164
+ BOB: {
14165
+ exponent: 2,
14166
+ status: "current"
14167
+ },
14168
+ BOV: {
14169
+ exponent: 2,
14170
+ status: "current"
14171
+ },
14172
+ BRL: {
14173
+ exponent: 2,
14174
+ status: "current"
14175
+ },
14176
+ BSD: {
14177
+ exponent: 2,
14178
+ status: "current"
14179
+ },
14180
+ BTN: {
14181
+ exponent: 2,
14182
+ status: "current"
14183
+ },
14184
+ BWP: {
14185
+ exponent: 2,
14186
+ status: "current"
14187
+ },
14188
+ BYN: {
14189
+ exponent: 2,
14190
+ status: "current"
14191
+ },
14192
+ BZD: {
14193
+ exponent: 2,
14194
+ status: "current"
14195
+ },
14196
+ CAD: {
14197
+ exponent: 2,
14198
+ status: "current"
14199
+ },
14200
+ CDF: {
14201
+ exponent: 2,
14202
+ status: "current"
14203
+ },
14204
+ CHE: {
14205
+ exponent: 2,
14206
+ status: "current"
14207
+ },
14208
+ CHF: {
14209
+ exponent: 2,
14210
+ status: "current"
14211
+ },
14212
+ CHW: {
14213
+ exponent: 2,
14214
+ status: "current"
14215
+ },
14216
+ CLF: {
14217
+ exponent: 4,
14218
+ status: "current"
14219
+ },
14220
+ CLP: {
14221
+ exponent: 0,
14222
+ status: "current"
14223
+ },
14224
+ CNY: {
14225
+ exponent: 2,
14226
+ status: "current"
14227
+ },
14228
+ COP: {
14229
+ exponent: 2,
14230
+ status: "current"
14231
+ },
14232
+ COU: {
14233
+ exponent: 2,
14234
+ status: "current"
14235
+ },
14236
+ CRC: {
14237
+ exponent: 2,
14238
+ status: "current"
14239
+ },
14240
+ CUP: {
14241
+ exponent: 2,
14242
+ status: "current"
14243
+ },
14244
+ CVE: {
14245
+ exponent: 2,
14246
+ status: "current"
14247
+ },
14248
+ CZK: {
14249
+ exponent: 2,
14250
+ status: "current"
14251
+ },
14252
+ DJF: {
14253
+ exponent: 0,
14254
+ status: "current"
14255
+ },
14256
+ DKK: {
14257
+ exponent: 2,
14258
+ status: "current"
14259
+ },
14260
+ DOP: {
14261
+ exponent: 2,
14262
+ status: "current"
14263
+ },
14264
+ DZD: {
14265
+ exponent: 2,
14266
+ status: "current"
14267
+ },
14268
+ EGP: {
14269
+ exponent: 2,
14270
+ status: "current"
14271
+ },
14272
+ ERN: {
14273
+ exponent: 2,
14274
+ status: "current"
14275
+ },
14276
+ ETB: {
14277
+ exponent: 2,
14278
+ status: "current"
14279
+ },
14280
+ EUR: {
14281
+ exponent: 2,
14282
+ status: "current"
14283
+ },
14284
+ FJD: {
14285
+ exponent: 2,
14286
+ status: "current"
14287
+ },
14288
+ FKP: {
14289
+ exponent: 2,
14290
+ status: "current"
14291
+ },
14292
+ GBP: {
14293
+ exponent: 2,
14294
+ status: "current"
14295
+ },
14296
+ GEL: {
14297
+ exponent: 2,
14298
+ status: "current"
14299
+ },
14300
+ GHS: {
14301
+ exponent: 2,
14302
+ status: "current"
14303
+ },
14304
+ GIP: {
14305
+ exponent: 2,
14306
+ status: "current"
14307
+ },
14308
+ GMD: {
14309
+ exponent: 2,
14310
+ status: "current"
14311
+ },
14312
+ GNF: {
14313
+ exponent: 0,
14314
+ status: "current"
14315
+ },
14316
+ GTQ: {
14317
+ exponent: 2,
14318
+ status: "current"
14319
+ },
14320
+ GYD: {
14321
+ exponent: 2,
14322
+ status: "current"
14323
+ },
14324
+ HKD: {
14325
+ exponent: 2,
14326
+ status: "current"
14327
+ },
14328
+ HNL: {
14329
+ exponent: 2,
14330
+ status: "current"
14331
+ },
14332
+ HTG: {
14333
+ exponent: 2,
14334
+ status: "current"
14335
+ },
14336
+ HUF: {
14337
+ exponent: 2,
14338
+ status: "current"
14339
+ },
14340
+ IDR: {
14341
+ exponent: 2,
14342
+ status: "current"
14343
+ },
14344
+ ILS: {
14345
+ exponent: 2,
14346
+ status: "current"
14347
+ },
14348
+ INR: {
14349
+ exponent: 2,
14350
+ status: "current"
14351
+ },
14352
+ IQD: {
14353
+ exponent: 3,
14354
+ status: "current"
14355
+ },
14356
+ IRR: {
14357
+ exponent: 2,
14358
+ status: "current"
14359
+ },
14360
+ ISK: {
14361
+ exponent: 0,
14362
+ status: "current"
14363
+ },
14364
+ JMD: {
14365
+ exponent: 2,
14366
+ status: "current"
14367
+ },
14368
+ JOD: {
14369
+ exponent: 3,
14370
+ status: "current"
14371
+ },
14372
+ JPY: {
14373
+ exponent: 0,
14374
+ status: "current"
14375
+ },
14376
+ KES: {
14377
+ exponent: 2,
14378
+ status: "current"
14379
+ },
14380
+ KGS: {
14381
+ exponent: 2,
14382
+ status: "current"
14383
+ },
14384
+ KHR: {
14385
+ exponent: 2,
14386
+ status: "current"
14387
+ },
14388
+ KMF: {
14389
+ exponent: 0,
14390
+ status: "current"
14391
+ },
14392
+ KPW: {
14393
+ exponent: 2,
14394
+ status: "current"
14395
+ },
14396
+ KRW: {
14397
+ exponent: 0,
14398
+ status: "current"
14399
+ },
14400
+ KWD: {
14401
+ exponent: 3,
14402
+ status: "current"
14403
+ },
14404
+ KYD: {
14405
+ exponent: 2,
14406
+ status: "current"
14407
+ },
14408
+ KZT: {
14409
+ exponent: 2,
14410
+ status: "current"
14411
+ },
14412
+ LAK: {
14413
+ exponent: 2,
14414
+ status: "current"
14415
+ },
14416
+ LBP: {
14417
+ exponent: 2,
14418
+ status: "current"
14419
+ },
14420
+ LKR: {
14421
+ exponent: 2,
14422
+ status: "current"
14423
+ },
14424
+ LRD: {
14425
+ exponent: 2,
14426
+ status: "current"
14427
+ },
14428
+ LSL: {
14429
+ exponent: 2,
14430
+ status: "current"
14431
+ },
14432
+ LYD: {
14433
+ exponent: 3,
14434
+ status: "current"
14435
+ },
14436
+ MAD: {
14437
+ exponent: 2,
14438
+ status: "current"
14439
+ },
14440
+ MDL: {
14441
+ exponent: 2,
14442
+ status: "current"
14443
+ },
14444
+ MGA: {
14445
+ exponent: 2,
14446
+ status: "current"
14447
+ },
14448
+ MKD: {
14449
+ exponent: 2,
14450
+ status: "current"
14451
+ },
14452
+ MMK: {
14453
+ exponent: 2,
14454
+ status: "current"
14455
+ },
14456
+ MNT: {
14457
+ exponent: 2,
14458
+ status: "current"
14459
+ },
14460
+ MOP: {
14461
+ exponent: 2,
14462
+ status: "current"
14463
+ },
14464
+ MRU: {
14465
+ exponent: 2,
14466
+ status: "current"
14467
+ },
14468
+ MUR: {
14469
+ exponent: 2,
14470
+ status: "current"
14471
+ },
14472
+ MVR: {
14473
+ exponent: 2,
14474
+ status: "current"
14475
+ },
14476
+ MWK: {
14477
+ exponent: 2,
14478
+ status: "current"
14479
+ },
14480
+ MXN: {
14481
+ exponent: 2,
14482
+ status: "current"
14483
+ },
14484
+ MXV: {
14485
+ exponent: 2,
14486
+ status: "current"
14487
+ },
14488
+ MYR: {
14489
+ exponent: 2,
14490
+ status: "current"
14491
+ },
14492
+ MZN: {
14493
+ exponent: 2,
14494
+ status: "current"
14495
+ },
14496
+ NAD: {
14497
+ exponent: 2,
14498
+ status: "current"
14499
+ },
14500
+ NGN: {
14501
+ exponent: 2,
14502
+ status: "current"
14503
+ },
14504
+ NIO: {
14505
+ exponent: 2,
14506
+ status: "current"
14507
+ },
14508
+ NOK: {
14509
+ exponent: 2,
14510
+ status: "current"
14511
+ },
14512
+ NPR: {
14513
+ exponent: 2,
14514
+ status: "current"
14515
+ },
14516
+ NZD: {
14517
+ exponent: 2,
14518
+ status: "current"
14519
+ },
14520
+ OMR: {
14521
+ exponent: 3,
14522
+ status: "current"
14523
+ },
14524
+ PAB: {
14525
+ exponent: 2,
14526
+ status: "current"
14527
+ },
14528
+ PEN: {
14529
+ exponent: 2,
14530
+ status: "current"
14531
+ },
14532
+ PGK: {
14533
+ exponent: 2,
14534
+ status: "current"
14535
+ },
14536
+ PHP: {
14537
+ exponent: 2,
14538
+ status: "current"
14539
+ },
14540
+ PKR: {
14541
+ exponent: 2,
14542
+ status: "current"
14543
+ },
14544
+ PLN: {
14545
+ exponent: 2,
14546
+ status: "current"
14547
+ },
14548
+ PYG: {
14549
+ exponent: 0,
14550
+ status: "current"
14551
+ },
14552
+ QAR: {
14553
+ exponent: 2,
14554
+ status: "current"
14555
+ },
14556
+ RON: {
14557
+ exponent: 2,
14558
+ status: "current"
14559
+ },
14560
+ RSD: {
14561
+ exponent: 2,
14562
+ status: "current"
14563
+ },
14564
+ RUB: {
14565
+ exponent: 2,
14566
+ status: "current"
14567
+ },
14568
+ RWF: {
14569
+ exponent: 0,
14570
+ status: "current"
14571
+ },
14572
+ SAR: {
14573
+ exponent: 2,
14574
+ status: "current"
14575
+ },
14576
+ SBD: {
14577
+ exponent: 2,
14578
+ status: "current"
14579
+ },
14580
+ SCR: {
14581
+ exponent: 2,
14582
+ status: "current"
14583
+ },
14584
+ SDG: {
14585
+ exponent: 2,
14586
+ status: "current"
14587
+ },
14588
+ SEK: {
14589
+ exponent: 2,
14590
+ status: "current"
14591
+ },
14592
+ SGD: {
14593
+ exponent: 2,
14594
+ status: "current"
14595
+ },
14596
+ SHP: {
14597
+ exponent: 2,
14598
+ status: "current"
14599
+ },
14600
+ SLE: {
14601
+ exponent: 2,
14602
+ status: "current"
14603
+ },
14604
+ SOS: {
14605
+ exponent: 2,
14606
+ status: "current"
14607
+ },
14608
+ SRD: {
14609
+ exponent: 2,
14610
+ status: "current"
14611
+ },
14612
+ SSP: {
14613
+ exponent: 2,
14614
+ status: "current"
14615
+ },
14616
+ STN: {
14617
+ exponent: 2,
14618
+ status: "current"
14619
+ },
14620
+ SVC: {
14621
+ exponent: 2,
14622
+ status: "current"
14623
+ },
14624
+ SYP: {
14625
+ exponent: 2,
14626
+ status: "current"
14627
+ },
14628
+ SZL: {
14629
+ exponent: 2,
14630
+ status: "current"
14631
+ },
14632
+ THB: {
14633
+ exponent: 2,
14634
+ status: "current"
14635
+ },
14636
+ TJS: {
14637
+ exponent: 2,
14638
+ status: "current"
14639
+ },
14640
+ TMT: {
14641
+ exponent: 2,
14642
+ status: "current"
14643
+ },
14644
+ TND: {
14645
+ exponent: 3,
14646
+ status: "current"
14647
+ },
14648
+ TOP: {
14649
+ exponent: 2,
14650
+ status: "current"
14651
+ },
14652
+ TRY: {
14653
+ exponent: 2,
14654
+ status: "current"
14655
+ },
14656
+ TTD: {
14657
+ exponent: 2,
14658
+ status: "current"
14659
+ },
14660
+ TWD: {
14661
+ exponent: 2,
14662
+ status: "current"
14663
+ },
14664
+ TZS: {
14665
+ exponent: 2,
14666
+ status: "current"
14667
+ },
14668
+ UAH: {
14669
+ exponent: 2,
14670
+ status: "current"
14671
+ },
14672
+ UGX: {
14673
+ exponent: 0,
14674
+ status: "current"
14675
+ },
14676
+ USD: {
14677
+ exponent: 2,
14678
+ status: "current"
14679
+ },
14680
+ USN: {
14681
+ exponent: 2,
14682
+ status: "current"
14683
+ },
14684
+ UYI: {
14685
+ exponent: 0,
14686
+ status: "current"
14687
+ },
14688
+ UYU: {
14689
+ exponent: 2,
14690
+ status: "current"
14691
+ },
14692
+ UYW: {
14693
+ exponent: 4,
14694
+ status: "current"
14695
+ },
14696
+ UZS: {
14697
+ exponent: 2,
14698
+ status: "current"
14699
+ },
14700
+ VED: {
14701
+ exponent: 2,
14702
+ status: "current"
14703
+ },
14704
+ VES: {
14705
+ exponent: 2,
14706
+ status: "current"
14707
+ },
14708
+ VND: {
14709
+ exponent: 0,
14710
+ status: "current"
14711
+ },
14712
+ VUV: {
14713
+ exponent: 0,
14714
+ status: "current"
14715
+ },
14716
+ WST: {
14717
+ exponent: 2,
14718
+ status: "current"
14719
+ },
14720
+ XAD: {
14721
+ exponent: 2,
14722
+ status: "current"
14723
+ },
14724
+ XAF: {
14725
+ exponent: 0,
14726
+ status: "current"
14727
+ },
14728
+ XCD: {
14729
+ exponent: 2,
14730
+ status: "current"
14731
+ },
14732
+ XCG: {
14733
+ exponent: 2,
14734
+ status: "current"
14735
+ },
14736
+ XOF: {
14737
+ exponent: 0,
14738
+ status: "current"
14739
+ },
14740
+ XPF: {
14741
+ exponent: 0,
14742
+ status: "current"
14743
+ },
14744
+ YER: {
14745
+ exponent: 2,
14746
+ status: "current"
14747
+ },
14748
+ ZAR: {
14749
+ exponent: 2,
14750
+ status: "current"
14751
+ },
14752
+ ZMW: {
14753
+ exponent: 2,
14754
+ status: "current"
14755
+ },
14756
+ ZWG: {
14757
+ exponent: 2,
14758
+ status: "current"
14759
+ }
14760
+ }
14761
+ };
14762
+
14763
+ // ../shared/src/money.ts
14764
+ var entries = iso_4217_exponents_default.currencies;
14008
14765
  // src/index.ts
14766
+ function validatedApiBaseUrl(value) {
14767
+ const url = new URL(value);
14768
+ const isLoopback = url.hostname === "localhost" || url.hostname === "127.0.0.1" || url.hostname === "[::1]";
14769
+ if (url.protocol !== "https:" && !(url.protocol === "http:" && isLoopback)) {
14770
+ throw new Error("protocol must be HTTPS, except HTTP is allowed for localhost development");
14771
+ }
14772
+ if (url.username || url.password) {
14773
+ throw new Error("credentials are not allowed in ATTRKIT_API_URL");
14774
+ }
14775
+ if (value.includes("?")) {
14776
+ throw new Error("query strings are not allowed in ATTRKIT_API_URL");
14777
+ }
14778
+ if (value.includes("#")) {
14779
+ throw new Error("fragments are not allowed in ATTRKIT_API_URL");
14780
+ }
14781
+ return url;
14782
+ }
14009
14783
  function readConfig(env = process.env) {
14010
14784
  const apiKey = env.ATTRKIT_API_KEY;
14011
14785
  if (!apiKey) {
14012
- throw new Error("ATTRKIT_API_KEY is required (create one in AttrKit → Settings → API keys).");
14786
+ throw new Error("ATTRKIT_API_KEY is required (create one in AttriKit → Settings → API keys).");
14013
14787
  }
14014
14788
  const apiUrl = (env.ATTRKIT_API_URL ?? "https://attrikit.io").replace(/\/+$/, "");
14789
+ validatedApiBaseUrl(apiUrl);
14015
14790
  return { apiKey, apiUrl };
14016
14791
  }
14792
+ var STRING_TAG = "[object String]";
14793
+ var NUMBER_TAG = "[object Number]";
14794
+ var BOOLEAN_TAG = "[object Boolean]";
14795
+ var OBJECT_TAG = "[object Object]";
14796
+ function hasRuntimeTag(value, tag) {
14797
+ return Object.prototype.toString.call(value) === tag;
14798
+ }
14799
+ function isString(value) {
14800
+ return hasRuntimeTag(value, STRING_TAG);
14801
+ }
14802
+ function isNumber(value) {
14803
+ return hasRuntimeTag(value, NUMBER_TAG);
14804
+ }
14805
+ function isBoolean(value) {
14806
+ return hasRuntimeTag(value, BOOLEAN_TAG);
14807
+ }
14808
+ function isObject(value) {
14809
+ return hasRuntimeTag(value, OBJECT_TAG);
14810
+ }
14811
+ function isRequestObject(value) {
14812
+ return Object.prototype.toString.call(value) === OBJECT_TAG;
14813
+ }
14814
+ function isJsonObject(value) {
14815
+ return isObject(value) && Object.values(value).every(isJsonValue);
14816
+ }
14817
+ function isJsonValue(value) {
14818
+ return value === null || isString(value) || isNumber(value) || isBoolean(value) || Array.isArray(value) && value.every(isJsonValue) || isJsonObject(value);
14819
+ }
14820
+ function isApiErrorEnvelope(value) {
14821
+ return isJsonObject(value) && (value.error === undefined || isJsonObject(value.error));
14822
+ }
14017
14823
  var REQUEST_TIMEOUT_MS = 15000;
14018
- async function callApi(config, path, options = {}) {
14019
- const doFetch = options.fetchImpl ?? fetch;
14020
- const url = new URL(`${config.apiUrl}${path}`);
14021
- for (const [key, value] of Object.entries(options.query ?? {})) {
14824
+ var LONG_REQUEST_TIMEOUT_MS = 120000;
14825
+ function requestTimeoutMs(path, body) {
14826
+ if (path === "/api/v1/export")
14827
+ return LONG_REQUEST_TIMEOUT_MS;
14828
+ if (path !== "/api/v1/spend" || !isRequestObject(body))
14829
+ return REQUEST_TIMEOUT_MS;
14830
+ return body.kind === "network_pull" || Array.isArray(body.rows) ? LONG_REQUEST_TIMEOUT_MS : REQUEST_TIMEOUT_MS;
14831
+ }
14832
+ function hasUnsafePathCharacter(path) {
14833
+ return Array.from(path).some((character) => {
14834
+ const code = character.charCodeAt(0);
14835
+ return code <= 32 || code === 127;
14836
+ });
14837
+ }
14838
+ function invalidResult(code, message) {
14839
+ return {
14840
+ ok: false,
14841
+ status: 0,
14842
+ json: { error: { code, message } }
14843
+ };
14844
+ }
14845
+ function buildRequestUrl(baseUrl2, path, query) {
14846
+ if (!path.startsWith("/") || hasUnsafePathCharacter(path)) {
14847
+ throw new Error("path must be an absolute URL path without whitespace or control characters");
14848
+ }
14849
+ const url = new URL(`${baseUrl2.href.replace(/\/+$/, "")}${path}`);
14850
+ for (const [key, value] of Object.entries(query ?? {})) {
14022
14851
  if (value !== undefined && value !== "")
14023
14852
  url.searchParams.set(key, value);
14024
14853
  }
14854
+ return url;
14855
+ }
14856
+ function parseJsonBody(text) {
14857
+ if (!text)
14858
+ return null;
14859
+ try {
14860
+ const parsed = JSON.parse(text);
14861
+ return isJsonValue(parsed) ? parsed : text;
14862
+ } catch {
14863
+ return text;
14864
+ }
14865
+ }
14866
+ function requestHeaders(config, idempotencyKey) {
14867
+ const headers = {
14868
+ authorization: `AttriKit-Management ${config.apiKey}`,
14869
+ "content-type": "application/json"
14870
+ };
14871
+ if (idempotencyKey)
14872
+ headers["idempotency-key"] = idempotencyKey;
14873
+ return headers;
14874
+ }
14875
+ function timeoutError(timeoutMs, method, idempotencyKey) {
14876
+ const readOrRemoveOnly = method === "GET" || method === "DELETE";
14877
+ return {
14878
+ code: "timeout",
14879
+ message: readOrRemoveOnly ? `no response within ${timeoutMs / 1000}s. This request was read-only or removal-only; no generated idempotency_key was used.` : `no response within ${timeoutMs / 1000}s. The request may still have been applied on the server; retry with idempotency_key ${idempotencyKey}.`
14880
+ };
14881
+ }
14882
+ function networkError(cause, method, idempotencyKey) {
14883
+ const message = cause instanceof Error ? cause.message : String(cause);
14884
+ return {
14885
+ code: "network_error",
14886
+ message: method === "GET" || method === "DELETE" ? message : `${message}. The request may still have been applied on the server; retry with idempotency_key ${idempotencyKey}.`
14887
+ };
14888
+ }
14889
+ function resolveIdempotencyKey(idempotencyKey, method) {
14890
+ const mutating = method === "POST" || method === "PATCH" || method === "PUT";
14891
+ return idempotencyKey === undefined && mutating ? randomUUID() : idempotencyKey;
14892
+ }
14893
+ function apiResultFromResponse(response, text, expectedResponse) {
14894
+ const contentType = response.headers.get("content-type") ?? undefined;
14895
+ const retryAfter = response.headers.get("retry-after") ?? undefined;
14896
+ const nextCursor = response.headers.get("attrikit-next-cursor") ?? undefined;
14897
+ const apiResult = {
14898
+ ok: response.ok,
14899
+ status: response.status,
14900
+ json: parseJsonBody(text),
14901
+ rawBody: text,
14902
+ expectedResponse: expectedResponse ?? "json"
14903
+ };
14904
+ if (contentType)
14905
+ apiResult.contentType = contentType;
14906
+ if (retryAfter)
14907
+ apiResult.retryAfter = retryAfter;
14908
+ if (nextCursor)
14909
+ apiResult.nextCursor = nextCursor;
14910
+ return apiResult;
14911
+ }
14912
+ async function callApi(config, path, options = {}) {
14913
+ const doFetch = options.fetchImpl ?? fetch;
14914
+ let baseUrl2;
14915
+ try {
14916
+ baseUrl2 = validatedApiBaseUrl(config.apiUrl);
14917
+ } catch (error) {
14918
+ return invalidResult("invalid_api_url", `ATTRKIT_API_URL is invalid: ${error instanceof Error ? error.message : String(error)}`);
14919
+ }
14920
+ let url;
14921
+ try {
14922
+ url = buildRequestUrl(baseUrl2, path, options.query);
14923
+ } catch (error) {
14924
+ return invalidResult("invalid_path", `AttriKit API path is invalid: ${error instanceof Error ? error.message : String(error)}`);
14925
+ }
14025
14926
  const controller = new AbortController;
14026
- const timer = setTimeout(() => controller.abort(), REQUEST_TIMEOUT_MS);
14927
+ const timeoutMs = requestTimeoutMs(path, options.body);
14928
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
14929
+ const method = options.method ?? "GET";
14930
+ const semanticMethod = options.readsOnly ? "GET" : method;
14931
+ const idempotencyKey = resolveIdempotencyKey(options.idempotencyKey, semanticMethod);
14027
14932
  try {
14028
- const response = await doFetch(url.toString(), {
14029
- method: options.method ?? "GET",
14030
- headers: {
14031
- authorization: `AttriKit-Management ${config.apiKey}`,
14032
- "content-type": "application/json",
14033
- ...options.idempotencyKey ? { "idempotency-key": options.idempotencyKey } : {}
14034
- },
14035
- ...options.body !== undefined ? { body: JSON.stringify(options.body) } : {},
14933
+ const request = {
14934
+ method,
14935
+ headers: requestHeaders(config, idempotencyKey),
14036
14936
  signal: controller.signal
14037
- });
14038
- const text = await response.text();
14039
- let json = null;
14040
- if (text) {
14041
- try {
14042
- json = JSON.parse(text);
14043
- } catch {
14044
- json = text;
14045
- }
14046
- }
14047
- const contentType = response.headers.get("content-type") ?? undefined;
14048
- const retryAfter = response.headers.get("retry-after") ?? undefined;
14049
- return {
14050
- ok: response.ok,
14051
- status: response.status,
14052
- json,
14053
- rawBody: text,
14054
- ...contentType ? { contentType } : {},
14055
- ...retryAfter ? { retryAfter } : {},
14056
- expectedResponse: options.expectedResponse ?? "json"
14057
14937
  };
14938
+ if (options.body !== undefined)
14939
+ request.body = JSON.stringify(options.body);
14940
+ const response = await doFetch(url.toString(), request);
14941
+ const text = await response.text();
14942
+ return apiResultFromResponse(response, text, options.expectedResponse);
14058
14943
  } catch (error) {
14059
14944
  return {
14060
14945
  ok: false,
14061
14946
  status: 0,
14062
14947
  json: {
14063
- error: error instanceof Error && error.name === "AbortError" ? {
14064
- code: "timeout",
14065
- message: `no response within ${REQUEST_TIMEOUT_MS / 1000}s. The request may still have been applied on the server; retry with the same idempotency_key rather than a new one.`
14066
- } : { code: "network_error", message: error instanceof Error ? error.message : String(error) }
14948
+ error: error instanceof Error && error.name === "AbortError" ? timeoutError(timeoutMs, semanticMethod, idempotencyKey) : networkError(error, semanticMethod, idempotencyKey)
14067
14949
  }
14068
14950
  };
14069
14951
  } finally {
14070
14952
  clearTimeout(timer);
14071
14953
  }
14072
14954
  }
14073
- var redact = (text) => text.replace(/\b(?:mk|pk|sk|rk)_[A-Za-z0-9_-]{8,}/g, "[redacted-key]").replace(/\b[Bb]earer\s+[A-Za-z0-9._-]{8,}/g, "Bearer [redacted]").replace(/\bAttriKit-(?:Management|Publishable)\s+\S+/g, "AttriKit-$1 [redacted]").replace(/([?&](?:token|secret|key|password|code|signature)=)[^&\s]+/gi, "$1[redacted]");
14955
+ var redact = (text) => text.replace(/(["'](?:credential|authorization_secret|signing_secret|access_token|refresh_token|client_secret)["']\s*:\s*["'])[^"']+(["'])/gi, "$1[redacted]$2").replace(/(?<![A-Za-z0-9])(?:mk|pk|sk|rk)_[A-Za-z0-9_-]{8,}/g, "[redacted-key]").replace(/\bBearer\s+[A-Za-z0-9\-._~+/]+=*/gi, "Bearer [redacted]").replace(/\bAttriKit-(Management|Publishable)\s+\S+/g, "AttriKit-$1 [redacted]").replace(/\bEAA[A-Za-z0-9_-]{8,}/g, "[redacted-meta-token]").replace(/\b1\/\/[A-Za-z0-9._-]{8,}/g, "[redacted-google-token]").replace(/([?&](?:token|secret|key|password|code|signature)=)[^&\s]+/gi, "$1[redacted]");
14956
+ var OPAQUE_CURSOR_PATTERN = /^[A-Za-z0-9_\-=.]{1,4096}$/;
14957
+ function withNextCursor(tool, nextCursor) {
14958
+ if (nextCursor === undefined || !OPAQUE_CURSOR_PATTERN.test(nextCursor))
14959
+ return tool;
14960
+ return {
14961
+ ...tool,
14962
+ content: [...tool.content, { type: "text", text: `next_cursor: ${nextCursor}` }],
14963
+ structuredContent: { ...tool.structuredContent, next_cursor: nextCursor }
14964
+ };
14965
+ }
14966
+ function ndjsonToolResult(result) {
14967
+ const body = result.rawBody ?? "";
14968
+ const contentType = (result.contentType ?? "").toLowerCase();
14969
+ const firstLine = body.split(`
14970
+ `, 1)[0]?.trim() ?? "";
14971
+ const looksLikeRecord = (() => {
14972
+ try {
14973
+ const parsed = JSON.parse(firstLine);
14974
+ return isJsonObject(parsed) && (parsed.type === "install" || parsed.type === "event") && isString(parsed.id);
14975
+ } catch {
14976
+ return false;
14977
+ }
14978
+ })();
14979
+ const isEmptyPage = body.trim() === "" && contentType.includes("ndjson");
14980
+ if (!isEmptyPage && (contentType.includes("text/html") || !looksLikeRecord)) {
14981
+ return {
14982
+ content: [{
14983
+ type: "text",
14984
+ text: `AttriKit API error (${result.status}): invalid_response: expected an NDJSON export but received ${body === "" ? "an empty body" : "a body that is not export records"} with Content-Type ${result.contentType ?? "unknown content type"}. Body:
14985
+ ${redact(body).slice(0, 600)}`
14986
+ }],
14987
+ isError: true
14988
+ };
14989
+ }
14990
+ return withNextCursor({ content: [{ type: "text", text: body }], structuredContent: { value: body } }, result.nextCursor);
14991
+ }
14074
14992
  function toToolResult(result) {
14075
14993
  if (!result.ok) {
14076
- const envelope = result.json;
14077
- const code = envelope?.error?.code ?? `http_${result.status}`;
14994
+ const envelope = isApiErrorEnvelope(result.json) ? result.json : null;
14995
+ const rawCode = envelope?.error?.code;
14996
+ const code = isString(rawCode) && rawCode !== "" ? redact(rawCode).slice(0, 120) : `http_${result.status}`;
14078
14997
  const rawMessage = envelope?.error?.message;
14079
- const message = typeof rawMessage === "string" && rawMessage !== "" ? `: ${redact(rawMessage).slice(0, 400)}` : "";
14080
- const retryAfter = result.status === 429 && result.retryAfter ? ` (Retry-After: ${result.retryAfter})` : "";
14081
- const excerpt = typeof rawMessage !== "string" && result.rawBody ? ` — body: ${redact(result.rawBody.replace(/\s+/g, " ").trim()).slice(0, 300)}` : "";
14998
+ const hasMessage = isString(rawMessage) && rawMessage.trim() !== "";
14999
+ const message = hasMessage ? `: ${redact(rawMessage).slice(0, 400)}` : "";
15000
+ const retryAfterValue = result.retryAfter === undefined ? "" : redact(result.retryAfter).trim().slice(0, 120);
15001
+ const validRetryAfter = /^\d{1,10}$/.test(retryAfterValue) || /^(?:Mon|Tue|Wed|Thu|Fri|Sat|Sun), \d{2} (?:Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec) \d{4} \d{2}:\d{2}:\d{2} GMT$/.test(retryAfterValue);
15002
+ const retryAfter = result.status === 429 && validRetryAfter ? ` (Retry-After: ${retryAfterValue})` : "";
15003
+ const excerpt = !hasMessage && result.rawBody ? ` — body: ${redact(result.rawBody.replace(/\s+/g, " ").trim()).slice(0, 300)}` : "";
14082
15004
  const hint = result.status === 401 ? " Check ATTRKIT_API_KEY: this is the same error for a missing, wrong, revoked or expired key." : "";
14083
15005
  return {
14084
15006
  content: [{
14085
15007
  type: "text",
14086
- text: `AttrKit API error (${result.status || "network"}): ${code}${message}${retryAfter}${excerpt}${hint}`
15008
+ text: `AttriKit API error (${result.status || "network"}): ${code}${message}${retryAfter}${excerpt}${hint}`
14087
15009
  }],
14088
15010
  isError: true
14089
15011
  };
14090
15012
  }
14091
15013
  const expectedResponse = result.expectedResponse ?? "json";
15014
+ if (expectedResponse === "ndjson")
15015
+ return ndjsonToolResult(result);
14092
15016
  const isEmptySuccess = result.status === 204 && (result.rawBody === undefined || result.rawBody === "");
14093
- const isJsonEnvelope = result.json !== null && typeof result.json === "object" && !Array.isArray(result.json) && Object.hasOwn(result.json, "data");
15017
+ const isJsonEnvelope = isJsonObject(result.json) && Object.hasOwn(result.json, "data");
14094
15018
  const contentType = (result.contentType ?? "").toLowerCase();
14095
15019
  const looksLikeCsv = (() => {
14096
- if (typeof result.json !== "string")
15020
+ if (!isString(result.json))
14097
15021
  return false;
14098
15022
  const firstLine = result.json.split(`
14099
15023
  `, 1)[0]?.trim() ?? "";
14100
15024
  if (!firstLine || firstLine.startsWith("<"))
14101
15025
  return false;
14102
15026
  const columns = firstLine.split(",").map((field) => field.trim().replace(/^"(.*)"$/s, "$1"));
14103
- return columns.length >= 2 && columns.every((field) => /^[A-Za-z][A-Za-z0-9_]*$/.test(field));
15027
+ return columns.length >= 2 && (columns[0] === "type" || columns[0] === "kind") && columns[1] === "id" && columns.every((field) => /^[A-Za-z][A-Za-z0-9_]*$/.test(field));
14104
15028
  })();
14105
- const isExpectedText = expectedResponse === "text" && typeof result.json === "string" && !contentType.includes("text/html") && looksLikeCsv;
15029
+ const isExpectedText = expectedResponse === "text" && isString(result.json) && !contentType.includes("text/html") && looksLikeCsv;
14106
15030
  if (!isEmptySuccess && !isJsonEnvelope && !isExpectedText) {
14107
15031
  const reportedContentType = result.contentType ?? "unknown content type";
14108
- const received = result.json === null ? "an empty body" : Array.isArray(result.json) ? "a JSON array" : typeof result.json === "object" ? "a JSON object without a data field" : `a ${typeof result.json} body`;
14109
- const body = redact(result.rawBody ?? (typeof result.json === "string" ? result.json : JSON.stringify(result.json, null, 2))).slice(0, 600);
15032
+ const received = result.json === null ? "an empty body" : Array.isArray(result.json) ? "a JSON array" : isJsonObject(result.json) ? "a JSON object without a data field" : `a ${isString(result.json) ? "string" : isNumber(result.json) ? "number" : "boolean"} body`;
15033
+ const body = redact(result.rawBody ?? (isString(result.json) ? result.json : JSON.stringify(result.json, null, 2))).slice(0, 600);
15034
+ const expectation = expectedResponse === "text" ? "expected a CSV export" : "expected a JSON data envelope";
14110
15035
  return {
14111
15036
  content: [{
14112
15037
  type: "text",
14113
- text: `AttrKit API error (${result.status}): invalid_response: expected a JSON data envelope but received ${received} with Content-Type ${reportedContentType}. Body:
15038
+ text: `AttriKit API error (${result.status}): invalid_response: ${expectation} but received ${received} with Content-Type ${reportedContentType}. Body:
14114
15039
  ${body}`
14115
15040
  }],
14116
15041
  isError: true
14117
15042
  };
14118
15043
  }
14119
- const structured = result.json && typeof result.json === "object" ? result.json : { value: result.json };
14120
- return {
15044
+ const structured = isJsonObject(result.json) ? result.json : { value: result.json };
15045
+ return withNextCursor({
14121
15046
  content: [{ type: "text", text: JSON.stringify(result.json, null, 2) }],
14122
15047
  structuredContent: structured
14123
- };
15048
+ }, result.nextCursor);
14124
15049
  }
14125
- var SERVER_VERSION = "0.4.6";
14126
- var periodSchema = z4.enum(["7d", "30d", "90d", "all"]).describe("Reporting window. Defaults to 30d.").optional();
14127
- var appIdSchema = z4.string().describe("Optional app id to scope the result to one app.").optional();
14128
- var requiredAppIdSchema = z4.string().describe("The AttrKit app id.");
14129
- var idempotencyKeySchema = z4.string().max(255).describe("Stable retry key for this exact mutation.").optional();
14130
- var ownersSchema = z4.record(z4.string(), z4.enum(["attrkit", "revenuecat", "customer"]));
14131
- var cursorSchema = z4.string().optional().describe("Pass the next_cursor from the previous response to fetch the following page. Omit for the first page.");
14132
- var listLimitSchema = z4.number().int().min(1).max(200).optional().describe("Rows per page, 1-200. The API defaults to 50, so a workspace with more than that needs paging.");
14133
- var spendRowSchema = z4.object({
14134
- campaign_id: z4.string().optional().describe("The AttrKit campaign UUID, NOT the ad network's own campaign id. It is the opposite of create_campaign_link.campaign_id and is the most common mistake here: read it from list_campaigns. The API rejects a network id by name. Preferred snake_case spelling."),
14135
- campaignId: z4.string().optional().describe("Accepted camelCase alias for campaign_id."),
14136
- adset_id: z4.string().optional().nullable().describe("Ad set id. Must be sent together with ad_id; either one alone is rejected."),
14137
- adsetId: z4.string().optional().nullable(),
14138
- ad_id: z4.string().optional().nullable(),
14139
- adId: z4.string().optional().nullable(),
14140
- adset_name: z4.string().optional().nullable(),
14141
- adsetName: z4.string().optional().nullable(),
14142
- ad_name: z4.string().optional().nullable(),
14143
- adName: z4.string().optional().nullable(),
14144
- metric_date: z4.string().regex(/^\d{4}-\d{2}-\d{2}$/).optional().describe("Required metric date. Preferred snake_case spelling."),
14145
- metricDate: z4.string().regex(/^\d{4}-\d{2}-\d{2}$/).optional().describe("Accepted camelCase alias for metric_date."),
14146
- clicks: z4.number().int().nonnegative().optional(),
14147
- impressions: z4.number().int().nonnegative().optional(),
14148
- spend_minor_units: z4.number().int().nonnegative().optional().describe("Required spend amount. Preferred snake_case spelling."),
14149
- spendMinorUnits: z4.number().int().nonnegative().optional().describe("Accepted camelCase alias for spend_minor_units.")
14150
- }).superRefine((row, context) => {
14151
- for (const [path, value] of [
14152
- ["campaign_id", row.campaign_id ?? row.campaignId],
14153
- ["metric_date", row.metric_date ?? row.metricDate],
14154
- ["spend_minor_units", row.spend_minor_units ?? row.spendMinorUnits]
14155
- ]) {
15050
+ var SERVER_VERSION = "1.0.0";
15051
+ var periodSchema = z5.enum(["7d", "30d", "90d", "all"]).describe("Reporting window. Omit to use the endpoint's documented default.").optional();
15052
+ var appIdSchema = z5.string().describe("Optional app id to scope the result to one app.").optional();
15053
+ var requiredAppIdSchema = z5.string().min(1).describe("The AttriKit app id.");
15054
+ var encodeOpaquePathSegment = (value) => encodeURIComponent(value).replaceAll(".", "%252E");
15055
+ var idempotencyKeySchema = z5.string().max(255).describe("Stable retry key for this exact mutation.").optional();
15056
+ var ownersSchema = z5.record(z5.string(), z5.enum(["attrkit", "revenuecat", "customer"]));
15057
+ var cursorSchema = z5.string().optional().describe("Pass the next_cursor from the previous response to fetch the following page. Omit for the first page.");
15058
+ var listLimitSchema = z5.number().int().min(1).max(200).optional().describe("Rows per page, 1-200. The API defaults to 50, so a workspace with more than that needs paging.");
15059
+ var calendarDaySchema = z5.string().regex(/^\d{4}-\d{2}-\d{2}$/, "Use a UTC calendar day, YYYY-MM-DD.");
15060
+ var rangeFromSchema = calendarDaySchema.optional().describe("First UTC day of an explicit window, YYYY-MM-DD, inclusive. Use instead of period; sending both is a 400.");
15061
+ var rangeToSchema = calendarDaySchema.optional().describe("Last UTC day of an explicit window, YYYY-MM-DD, inclusive. Use instead of period; sending both is a 400.");
15062
+ function validateMatchingSpendAliases(pairs, context) {
15063
+ for (const [preferred, alias, preferredValue, aliasValue] of pairs) {
15064
+ if (preferredValue !== undefined && aliasValue !== undefined && preferredValue !== aliasValue) {
15065
+ context.addIssue({
15066
+ code: "custom",
15067
+ path: [alias],
15068
+ message: `${preferred} and ${alias} must match when both are supplied`
15069
+ });
15070
+ }
15071
+ }
15072
+ }
15073
+ function validateRequiredSpendValues(values, context) {
15074
+ for (const [path, value] of values) {
14156
15075
  if (value === undefined) {
14157
15076
  context.addIssue({
14158
15077
  code: "custom",
@@ -14161,6 +15080,49 @@ var spendRowSchema = z4.object({
14161
15080
  });
14162
15081
  }
14163
15082
  }
15083
+ }
15084
+ function validatePairedAdIds(adsetId, adId, context) {
15085
+ if (adsetId == null !== (adId == null)) {
15086
+ context.addIssue({
15087
+ code: "custom",
15088
+ path: adsetId == null ? ["adset_id"] : ["ad_id"],
15089
+ message: "adset_id and ad_id must be supplied together"
15090
+ });
15091
+ }
15092
+ }
15093
+ var spendRowSchema = z5.object({
15094
+ campaign_id: z5.string().optional().describe("The AttriKit campaign UUID, NOT the ad network's own campaign id (that is create_campaign_link.network_campaign_id). It is the most common mistake here: read it from list_campaigns. The API rejects a network id by name. Preferred snake_case spelling."),
15095
+ campaignId: z5.string().optional().describe("Accepted camelCase alias for campaign_id."),
15096
+ adset_id: z5.string().optional().nullable().describe("Ad set id. Must be sent together with ad_id; either one alone is rejected."),
15097
+ adsetId: z5.string().optional().nullable(),
15098
+ ad_id: z5.string().optional().nullable(),
15099
+ adId: z5.string().optional().nullable(),
15100
+ adset_name: z5.string().optional().nullable(),
15101
+ adsetName: z5.string().optional().nullable(),
15102
+ ad_name: z5.string().optional().nullable(),
15103
+ adName: z5.string().optional().nullable(),
15104
+ metric_date: z5.string().regex(/^\d{4}-\d{2}-\d{2}$/).optional().describe("Required metric date. Preferred snake_case spelling."),
15105
+ metricDate: z5.string().regex(/^\d{4}-\d{2}-\d{2}$/).optional().describe("Accepted camelCase alias for metric_date."),
15106
+ clicks: z5.number().int().nonnegative().optional(),
15107
+ impressions: z5.number().int().nonnegative().optional(),
15108
+ spend_minor_units: z5.number().int().nonnegative().optional().describe("Required spend amount. Preferred snake_case spelling."),
15109
+ spendMinorUnits: z5.number().int().nonnegative().optional().describe("Accepted camelCase alias for spend_minor_units.")
15110
+ }).superRefine((row, context) => {
15111
+ validateMatchingSpendAliases([
15112
+ ["campaign_id", "campaignId", row.campaign_id, row.campaignId],
15113
+ ["adset_id", "adsetId", row.adset_id, row.adsetId],
15114
+ ["ad_id", "adId", row.ad_id, row.adId],
15115
+ ["adset_name", "adsetName", row.adset_name, row.adsetName],
15116
+ ["ad_name", "adName", row.ad_name, row.adName],
15117
+ ["metric_date", "metricDate", row.metric_date, row.metricDate],
15118
+ ["spend_minor_units", "spendMinorUnits", row.spend_minor_units, row.spendMinorUnits]
15119
+ ], context);
15120
+ validateRequiredSpendValues([
15121
+ ["campaign_id", row.campaign_id ?? row.campaignId],
15122
+ ["metric_date", row.metric_date ?? row.metricDate],
15123
+ ["spend_minor_units", row.spend_minor_units ?? row.spendMinorUnits]
15124
+ ], context);
15125
+ validatePairedAdIds(row.adset_id ?? row.adsetId, row.ad_id ?? row.adId, context);
14164
15126
  });
14165
15127
  function normalizeSpendRow(row) {
14166
15128
  return {
@@ -14191,12 +15153,12 @@ function createServer(config, fetchImpl) {
14191
15153
  inputSchema: {}
14192
15154
  }, async () => toToolResult(await call("/api/v1/workspace/usage-cap")));
14193
15155
  server.registerTool("set_usage_cap", {
14194
- annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true },
15156
+ annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: true },
14195
15157
  title: "Set the workspace usage cap",
14196
15158
  description: "Set a monthly qualified-install cap of at least 10,000 and its alert email. Pass null for both values to clear the cap. Requires a workspace-scoped write key.",
14197
15159
  inputSchema: {
14198
- monthly_qualified_install_cap: z4.number().int().min(1e4).max(Number.MAX_SAFE_INTEGER).nullable(),
14199
- alert_email: z4.string().trim().email().max(320).nullable(),
15160
+ monthly_qualified_install_cap: z5.number().int().min(1e4).max(Number.MAX_SAFE_INTEGER).nullable(),
15161
+ alert_email: z5.string().trim().email().max(320).nullable(),
14200
15162
  idempotency_key: idempotencyKeySchema
14201
15163
  }
14202
15164
  }, async ({
@@ -14208,7 +15170,7 @@ function createServer(config, fetchImpl) {
14208
15170
  return {
14209
15171
  content: [{
14210
15172
  type: "text",
14211
- text: "AttrKit usage-cap validation error: set both cap and alert email, or pass null for both to clear them."
15173
+ text: "AttriKit usage-cap validation error: set both cap and alert email, or pass null for both to clear them."
14212
15174
  }],
14213
15175
  isError: true
14214
15176
  };
@@ -14225,22 +15187,27 @@ function createServer(config, fetchImpl) {
14225
15187
  server.registerTool("list_apps", {
14226
15188
  annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true },
14227
15189
  title: "List apps",
14228
- description: "List the iOS and Android apps in your AttrKit workspace (id, name, platform, bundle id, store id, status).",
15190
+ description: "List the iOS and Android apps in your AttriKit workspace (id, name, platform, bundle id, store id, status).",
14229
15191
  inputSchema: { cursor: cursorSchema, limit: listLimitSchema }
14230
- }, async ({ cursor, limit }) => toToolResult(await call("/api/v1/apps", {
14231
- query: { ...cursor ? { cursor } : {}, ...limit === undefined ? {} : { limit: String(limit) } }
14232
- })));
15192
+ }, async ({ cursor, limit }) => {
15193
+ const query = {};
15194
+ if (cursor)
15195
+ query.cursor = cursor;
15196
+ if (limit !== undefined)
15197
+ query.limit = String(limit);
15198
+ return toToolResult(await call("/api/v1/apps", { query }));
15199
+ });
14233
15200
  server.registerTool("create_app", {
14234
15201
  annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true },
14235
15202
  title: "Create an app",
14236
15203
  description: "Create an iOS or Android app without the web UI. Returns the publishable key and a one-time app-scoped management key.",
14237
15204
  inputSchema: {
14238
- name: z4.string().min(1).max(80),
14239
- bundle_id: z4.string().min(3).max(255),
14240
- platform: z4.enum(["iOS", "Android"]),
14241
- store_url: z4.string().url(),
14242
- timezone: z4.string().max(64).optional(),
14243
- device_matching_mode: z4.enum(["on", "strict"]).optional(),
15205
+ name: z5.string().min(1).max(80),
15206
+ bundle_id: z5.string().min(3).max(255),
15207
+ platform: z5.enum(["iOS", "Android"]),
15208
+ store_url: z5.string().url(),
15209
+ timezone: z5.string().max(64).optional(),
15210
+ device_matching_mode: z5.enum(["on", "strict"]).optional(),
14244
15211
  idempotency_key: idempotencyKeySchema
14245
15212
  }
14246
15213
  }, async ({
@@ -14251,36 +15218,36 @@ function createServer(config, fetchImpl) {
14251
15218
  timezone,
14252
15219
  device_matching_mode,
14253
15220
  idempotency_key
14254
- }) => toToolResult(await call("/api/v1/apps", {
14255
- method: "POST",
14256
- idempotencyKey: idempotency_key,
14257
- body: {
14258
- name,
14259
- bundleId: bundle_id,
14260
- platform,
14261
- storeUrl: store_url,
14262
- ...timezone ? { timezone } : {},
14263
- ...device_matching_mode ? { deviceMatchingMode: device_matching_mode } : {}
14264
- }
14265
- })));
15221
+ }) => {
15222
+ const body = { name, bundleId: bundle_id, platform, storeUrl: store_url };
15223
+ if (timezone)
15224
+ body.timezone = timezone;
15225
+ if (device_matching_mode)
15226
+ body.deviceMatchingMode = device_matching_mode;
15227
+ return toToolResult(await call("/api/v1/apps", {
15228
+ method: "POST",
15229
+ idempotencyKey: idempotency_key,
15230
+ body
15231
+ }));
15232
+ });
14266
15233
  server.registerTool("get_app", {
14267
15234
  annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true },
14268
15235
  title: "Get an app",
14269
15236
  description: "Return one app's configuration.",
14270
15237
  inputSchema: { app_id: requiredAppIdSchema }
14271
- }, async ({ app_id }) => toToolResult(await call(`/api/v1/apps/${encodeURIComponent(app_id)}`)));
15238
+ }, async ({ app_id }) => toToolResult(await call(`/api/v1/apps/${encodeOpaquePathSegment(app_id)}`)));
14272
15239
  server.registerTool("update_app", {
14273
15240
  annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true },
14274
15241
  title: "Update an app",
14275
- description: "Update app metadata, store destination, timezone or device-matching mode.",
15242
+ description: "Update app metadata, store destination, timezone or coverage mode.",
14276
15243
  inputSchema: {
14277
15244
  app_id: requiredAppIdSchema,
14278
- name: z4.string().min(1).max(80).optional(),
14279
- bundle_id: z4.string().min(3).max(255).optional(),
14280
- platform: z4.enum(["iOS", "Android"]).optional(),
14281
- store_url: z4.string().url().optional(),
14282
- timezone: z4.string().max(64).optional(),
14283
- device_matching_mode: z4.enum(["on", "strict"]).optional(),
15245
+ name: z5.string().min(1).max(80).optional(),
15246
+ bundle_id: z5.string().min(3).max(255).optional(),
15247
+ platform: z5.enum(["iOS", "Android"]).optional(),
15248
+ store_url: z5.string().url().optional(),
15249
+ timezone: z5.string().max(64).optional(),
15250
+ device_matching_mode: z5.enum(["on", "strict"]).optional(),
14284
15251
  idempotency_key: idempotencyKeySchema
14285
15252
  }
14286
15253
  }, async ({
@@ -14292,24 +15259,32 @@ function createServer(config, fetchImpl) {
14292
15259
  timezone,
14293
15260
  device_matching_mode,
14294
15261
  idempotency_key
14295
- }) => toToolResult(await call(`/api/v1/apps/${encodeURIComponent(app_id)}`, {
14296
- method: "PATCH",
14297
- idempotencyKey: idempotency_key,
14298
- body: {
14299
- ...name ? { name } : {},
14300
- ...bundle_id ? { bundleId: bundle_id } : {},
14301
- ...platform ? { platform } : {},
14302
- ...store_url ? { storeUrl: store_url } : {},
14303
- ...timezone ? { timezone } : {},
14304
- ...device_matching_mode ? { deviceMatchingMode: device_matching_mode } : {}
14305
- }
14306
- })));
15262
+ }) => {
15263
+ const body = {};
15264
+ if (name)
15265
+ body.name = name;
15266
+ if (bundle_id)
15267
+ body.bundleId = bundle_id;
15268
+ if (platform)
15269
+ body.platform = platform;
15270
+ if (store_url)
15271
+ body.storeUrl = store_url;
15272
+ if (timezone)
15273
+ body.timezone = timezone;
15274
+ if (device_matching_mode)
15275
+ body.deviceMatchingMode = device_matching_mode;
15276
+ return toToolResult(await call(`/api/v1/apps/${encodeOpaquePathSegment(app_id)}`, {
15277
+ method: "PATCH",
15278
+ idempotencyKey: idempotency_key,
15279
+ body
15280
+ }));
15281
+ });
14307
15282
  server.registerTool("archive_app", {
14308
15283
  annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: true },
14309
15284
  title: "Archive an app",
14310
15285
  description: "Archive an app and remove it from active customer operations.",
14311
15286
  inputSchema: { app_id: requiredAppIdSchema }
14312
- }, async ({ app_id }) => toToolResult(await call(`/api/v1/apps/${encodeURIComponent(app_id)}`, {
15287
+ }, async ({ app_id }) => toToolResult(await call(`/api/v1/apps/${encodeOpaquePathSegment(app_id)}`, {
14313
15288
  method: "DELETE"
14314
15289
  })));
14315
15290
  server.registerTool("get_publishable_key", {
@@ -14317,7 +15292,7 @@ function createServer(config, fetchImpl) {
14317
15292
  title: "Get an app publishable key",
14318
15293
  description: "Reveal the recoverable SDK publishable key for an app.",
14319
15294
  inputSchema: { app_id: requiredAppIdSchema }
14320
- }, async ({ app_id }) => toToolResult(await call(`/api/v1/apps/${encodeURIComponent(app_id)}/publishable-key`)));
15295
+ }, async ({ app_id }) => toToolResult(await call(`/api/v1/apps/${encodeOpaquePathSegment(app_id)}/publishable-key`)));
14321
15296
  server.registerTool("rotate_publishable_key", {
14322
15297
  annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: true },
14323
15298
  title: "Rotate an app publishable key",
@@ -14326,7 +15301,7 @@ function createServer(config, fetchImpl) {
14326
15301
  app_id: requiredAppIdSchema,
14327
15302
  idempotency_key: idempotencyKeySchema
14328
15303
  }
14329
- }, async ({ app_id, idempotency_key }) => toToolResult(await call(`/api/v1/apps/${encodeURIComponent(app_id)}/publishable-key`, { method: "POST", idempotencyKey: idempotency_key })));
15304
+ }, async ({ app_id, idempotency_key }) => toToolResult(await call(`/api/v1/apps/${encodeOpaquePathSegment(app_id)}/publishable-key`, { method: "POST", idempotencyKey: idempotency_key })));
14330
15305
  server.registerTool("list_api_keys", {
14331
15306
  annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true },
14332
15307
  title: "List management keys",
@@ -14338,9 +15313,9 @@ function createServer(config, fetchImpl) {
14338
15313
  title: "Create a management key",
14339
15314
  description: "Create a read-only or read/write workspace or app key. The secret is returned once.",
14340
15315
  inputSchema: {
14341
- label: z4.string().min(1).max(80),
14342
- scopes: z4.array(z4.enum(["read", "write"])).min(1).max(2),
14343
- app_id: z4.string().uuid().optional().nullable(),
15316
+ label: z5.string().min(1).max(80),
15317
+ scopes: z5.array(z5.enum(["read", "write"])).min(1).max(2),
15318
+ app_id: z5.string().uuid().optional().nullable(),
14344
15319
  idempotency_key: idempotencyKeySchema
14345
15320
  }
14346
15321
  }, async ({ label, scopes, app_id, idempotency_key }) => toToolResult(await call("/api/v1/api-keys", {
@@ -14352,18 +15327,18 @@ function createServer(config, fetchImpl) {
14352
15327
  annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: true },
14353
15328
  title: "Revoke a management key",
14354
15329
  description: "Immediately revoke a management key by id.",
14355
- inputSchema: { key_id: z4.string().uuid() }
15330
+ inputSchema: { key_id: z5.string().uuid() }
14356
15331
  }, async ({ key_id }) => toToolResult(await call(`/api/v1/api-keys/${encodeURIComponent(key_id)}`, {
14357
15332
  method: "DELETE"
14358
15333
  })));
14359
15334
  server.registerTool("list_links", {
14360
15335
  annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true },
14361
15336
  title: "List campaign links",
14362
- description: "List tracking links, optionally for one app.",
15337
+ description: "List tracking links, optionally for one app. Each link's tracking_url is the campaign link to use in the ad (null if unavailable); url is the store destination.",
14363
15338
  inputSchema: {
14364
15339
  app_id: appIdSchema,
14365
- cursor: z4.string().optional(),
14366
- limit: z4.number().int().min(1).max(200).optional()
15340
+ cursor: z5.string().optional(),
15341
+ limit: z5.number().int().min(1).max(200).optional()
14367
15342
  }
14368
15343
  }, async ({ app_id, cursor, limit }) => toToolResult(await call("/api/v1/links", {
14369
15344
  query: {
@@ -14375,16 +15350,19 @@ function createServer(config, fetchImpl) {
14375
15350
  server.registerTool("list_campaigns", {
14376
15351
  annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true },
14377
15352
  title: "List campaigns",
14378
- description: "List ad campaigns with their honest profit surface: the scale/cut/hold call, profit p05/p50/p95, spend, retention, and payback. Use this to see how campaigns are performing.",
14379
- inputSchema: { app_id: appIdSchema, period: periodSchema, cursor: cursorSchema, limit: listLimitSchema }
14380
- }, async ({ app_id, period, cursor, limit }) => toToolResult(await call("/api/v1/reports/campaigns", {
14381
- query: {
15353
+ description: "List ad campaigns with their honest profit surface. `call` preserves the original v1 fail-closed automation behavior for multi-component rows; `recommended_call` is the additive SCALE/CUT/HOLD recommendation the current engine made. It is backed by `envelope_lower`/`envelope_central`/`envelope_upper`. Read `aggregation_basis` before quoting the low end: single_component_quantiles means it is a calibrated 5th percentile; marginal_endpoint_sum_no_joint_coverage means it is the sum of each component period's own low end, which is pessimistic against independence but is NOT a floor or a guarantee, so describe it as a scenario. The quantile-named `profit_p05`/`profit_p50`/`profit_p95` are null unless one component supplied calibrated ones. Also includes spend, retention, and payback.",
15354
+ inputSchema: { app_id: appIdSchema, period: periodSchema, from: rangeFromSchema, to: rangeToSchema, cursor: cursorSchema, limit: listLimitSchema }
15355
+ }, async ({ app_id, period, from, to, cursor, limit }) => {
15356
+ const query = {
14382
15357
  appId: app_id,
14383
15358
  period,
14384
- ...cursor ? { cursor } : {},
14385
- ...limit === undefined ? {} : { limit: String(limit) }
14386
- }
14387
- })));
15359
+ from,
15360
+ to,
15361
+ cursor: cursor || undefined,
15362
+ limit: limit === undefined ? undefined : String(limit)
15363
+ };
15364
+ return toToolResult(await call("/api/v1/reports/campaigns", { query }));
15365
+ });
14388
15366
  server.registerTool("list_ads", {
14389
15367
  annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true },
14390
15368
  title: "List creative-level ad performance",
@@ -14392,12 +15370,18 @@ function createServer(config, fetchImpl) {
14392
15370
  inputSchema: {
14393
15371
  app_id: appIdSchema,
14394
15372
  period: periodSchema,
14395
- limit: z4.number().int().min(1).max(200).optional()
15373
+ from: rangeFromSchema,
15374
+ to: rangeToSchema,
15375
+ cursor: cursorSchema,
15376
+ limit: z5.number().int().min(1).max(200).optional()
14396
15377
  }
14397
- }, async ({ app_id, period, limit }) => toToolResult(await call("/api/v1/reports/ads", {
15378
+ }, async ({ app_id, period, from, to, cursor, limit }) => toToolResult(await call("/api/v1/reports/ads", {
14398
15379
  query: {
14399
15380
  appId: app_id,
14400
15381
  period,
15382
+ from,
15383
+ to,
15384
+ cursor,
14401
15385
  limit: limit === undefined ? undefined : String(limit)
14402
15386
  }
14403
15387
  })));
@@ -14407,62 +15391,54 @@ function createServer(config, fetchImpl) {
14407
15391
  description: "Return the same recent attribution events and installs visible on the Live dashboard.",
14408
15392
  inputSchema: {
14409
15393
  app_id: appIdSchema,
14410
- limit: z4.number().int().min(1).max(50).optional()
15394
+ limit: z5.number().int().min(1).max(50).optional(),
15395
+ from: calendarDaySchema.optional().describe("First UTC day to include, YYYY-MM-DD. May be sent without to. Sending from, to or cursor switches the response to paged mode, which adds next_cursor."),
15396
+ to: calendarDaySchema.optional().describe("Last UTC day to include, YYYY-MM-DD. May be sent without from."),
15397
+ cursor: cursorSchema
14411
15398
  }
14412
- }, async ({ app_id, limit }) => toToolResult(await call("/api/v1/events", {
15399
+ }, async ({ app_id, limit, from, to, cursor }) => toToolResult(await call("/api/v1/events", {
14413
15400
  query: {
14414
15401
  appId: app_id,
15402
+ from,
15403
+ to,
15404
+ cursor,
14415
15405
  limit: limit === undefined ? undefined : String(limit)
14416
15406
  }
14417
15407
  })));
14418
15408
  server.registerTool("get_campaign_decision", {
14419
15409
  annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true },
14420
15410
  title: "Get campaign decisions (scale / cut / hold)",
14421
- description: "Return AttrKit's profit DECISIONS for your campaigns: the scale/cut/hold call and confidence, computed by the attribution engine. Report the `call` VERBATIM — do not recompute or override it; when it says not_enough_data, say so. This is the tool to drive budget automation.",
14422
- inputSchema: { app_id: appIdSchema }
14423
- }, async ({ app_id }) => toToolResult(await call("/api/v1/decisions", { query: { appId: app_id } })));
15411
+ description: "Return AttriKit's profit DECISIONS for your campaigns: the scale/cut/hold call and confidence, computed by the attribution engine. Report the `call` VERBATIM — do not recompute or override it; when it says not_enough_data, say so. This is the tool to drive budget automation.",
15412
+ inputSchema: {
15413
+ app_id: appIdSchema,
15414
+ period: periodSchema,
15415
+ cursor: cursorSchema,
15416
+ limit: listLimitSchema
15417
+ }
15418
+ }, async ({ app_id, period, cursor, limit }) => toToolResult(await call("/api/v1/decisions", {
15419
+ query: {
15420
+ appId: app_id,
15421
+ period,
15422
+ cursor,
15423
+ limit: limit === undefined ? undefined : String(limit)
15424
+ }
15425
+ })));
14424
15426
  server.registerTool("record_campaign_decision", {
14425
15427
  annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true },
14426
15428
  title: "Record a campaign decision",
14427
- description: "Freeze a scale/cut/hold decision and its exact model inputs for later maturity evaluation.",
15429
+ description: "Record a scale/cut/hold decision for the reporting period the caller reviewed. AttriKit freezes that period's current campaign range, spend, costs, revenue basis, and model version server-side; callers cannot author receipt provenance.",
14428
15430
  inputSchema: {
14429
15431
  app_id: requiredAppIdSchema,
14430
- campaign_id: z4.string().describe("The AttrKit campaign uuid, as returned by list_campaigns or get_campaign_decision. Not the ad network's own campaign id."),
14431
- call: z4.enum(["scale", "cut", "hold"]).describe("The decision being frozen. Report get_campaign_decision's own call verbatim rather than forming your own."),
14432
- actor: z4.enum(["dashboard", "auto_suggested"]).optional().describe("Who made the call. Use auto_suggested when an agent proposed it without a human confirming."),
14433
- p05: z4.number().describe("Low end of the profit range from get_campaign_decision, in minor units. Pass through unchanged."),
14434
- p50: z4.number().describe("Central profit estimate from get_campaign_decision, in minor units. Pass through unchanged."),
14435
- p95: z4.number().describe("High end of the profit range from get_campaign_decision, in minor units. Pass through unchanged."),
14436
- spend_minor_units: z4.number().nonnegative().describe("Spend the decision was taken against, in minor units of `currency`."),
14437
- currency: z4.string().length(3).describe("ISO 4217 code for spend and revenue on this decision, e.g. USD. Three letters."),
14438
- band_width_pct: z4.number().nonnegative().describe("Width of the profit band as a percentage, from get_campaign_decision. It is how the maturity check later judges whether the range was tight enough to act on."),
14439
- threshold: z4.number().describe("The profit threshold the call was made against, in minor units."),
14440
- model_version: z4.string().describe("Model version string from get_campaign_decision. Recorded so a later evaluation knows which model produced the range."),
14441
- cost_model_version: z4.number().int().positive().nullable().describe("Cost model version from get_campaign_decision, or null when no cost model applied."),
14442
- revenue_per_install_minor_units: z4.number().nonnegative().nullable().describe("Revenue per install used in the estimate, in minor units, or null when revenue was unavailable."),
14443
- revenue_per_install_quality: z4.enum([
14444
- "campaign_verified_cohort",
14445
- "app_verified_cohort_fallback",
14446
- "unavailable"
14447
- ]).describe("How that revenue figure was derived: from this campaign's own verified cohort, from the app-wide fallback, or not at all. Pass through from get_campaign_decision."),
15432
+ campaign_id: z5.string().uuid().describe("The AttriKit campaign uuid, as returned by list_campaigns or get_campaign_decision. Not the ad network's own campaign id."),
15433
+ call: z5.enum(["scale", "cut", "hold"]).describe("The decision being frozen. Report get_campaign_decision's own call verbatim rather than forming your own."),
15434
+ period: periodSchema.describe("The reporting window you reviewed; the receipt freezes that window. Pass the same period you read. Omitted, this tool and get_campaign_decision use all, while list_campaigns defaults to 30d."),
14448
15435
  idempotency_key: idempotencyKeySchema
14449
15436
  }
14450
15437
  }, async ({
14451
15438
  app_id,
14452
15439
  campaign_id,
14453
15440
  call: decisionCall,
14454
- actor,
14455
- p05,
14456
- p50,
14457
- p95,
14458
- spend_minor_units,
14459
- currency,
14460
- band_width_pct,
14461
- threshold,
14462
- model_version,
14463
- cost_model_version,
14464
- revenue_per_install_minor_units,
14465
- revenue_per_install_quality,
15441
+ period,
14466
15442
  idempotency_key
14467
15443
  }) => toToolResult(await call("/api/v1/decisions", {
14468
15444
  method: "POST",
@@ -14471,70 +15447,76 @@ function createServer(config, fetchImpl) {
14471
15447
  appId: app_id,
14472
15448
  campaignId: campaign_id,
14473
15449
  call: decisionCall,
14474
- ...actor ? { actor } : {},
14475
- inputs: {
14476
- p05,
14477
- p50,
14478
- p95,
14479
- spend_minor_units,
14480
- currency,
14481
- band_width_pct,
14482
- threshold,
14483
- model_version,
14484
- cost_model_version,
14485
- revenue_per_install_minor_units,
14486
- revenue_per_install_quality
14487
- }
15450
+ period
14488
15451
  }
14489
15452
  })));
14490
15453
  server.registerTool("get_attribution_rollup", {
14491
15454
  annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true },
14492
15455
  title: "Get attribution rollup",
14493
- description: "Return the account-level attribution overview: installs accounted-for by evidence class (verified / device-matched / modeled / organic), MER, revenue and data freshness.",
14494
- inputSchema: { app_id: appIdSchema, period: periodSchema }
14495
- }, async ({ app_id, period }) => toToolResult(await call("/api/v1/reports/overview", { query: { appId: app_id, period } })));
15456
+ description: "Return the account-level attribution overview: installs accounted-for by evidence class (verified / matched / modeled / organic), MER, revenue and data freshness.",
15457
+ inputSchema: { app_id: appIdSchema, period: periodSchema, from: rangeFromSchema, to: rangeToSchema }
15458
+ }, async ({ app_id, period, from, to }) => toToolResult(await call("/api/v1/reports/overview", { query: { appId: app_id, period, from, to } })));
14496
15459
  server.registerTool("create_campaign_link", {
14497
15460
  annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true },
14498
15461
  title: "Create a campaign tracking link",
14499
- description: "Create a web-to-app tracking link for an ad campaign. Requires a write-scoped API key. Returns the link id and url.",
15462
+ description: "Create a web-to-app tracking link for an ad campaign. Requires a write-scoped API key. Returns the link: tracking_url is the campaign link to use in the ad (for meta the ad's Website URL, for tiktok the ad's destination, for google the tracking template with the App Store URL kept as the final URL); url is only the App Store destination you passed, never the link to put in an ad. tracking_url is null when the server has no link origin configured or the network is unrecognized; then create the link in AttriKit, Links.",
14500
15463
  inputSchema: {
14501
- app_id: z4.string().describe("The app id the link belongs to."),
14502
- network: z4.enum(["meta", "google", "tiktok"]).describe("The ad network delivering the campaign."),
14503
- destination_url: z4.string().url().describe("The App Store URL to send users to."),
14504
- campaign_name: z4.string().max(120).optional().describe("Optional human-readable campaign name."),
14505
- campaign_id: z4.string().optional().describe("Optional external campaign id. This is the AD NETWORK's own campaign id, not an AttrKit uuid; it is the opposite of import_spend.campaign_id, which requires the AttrKit one."),
14506
- serve_landing: z4.boolean().optional().describe("Serve the interstitial landing bridge on this link. Defaults to true at the API. The bridge is what captures browser-side context for device matching, so turning it off narrows what can be matched."),
15464
+ app_id: requiredAppIdSchema.describe("The app id the link belongs to."),
15465
+ network: z5.enum(["meta", "google", "tiktok"]).describe("The ad network delivering the campaign."),
15466
+ destination_url: z5.string().url().describe("The App Store URL to send users to."),
15467
+ campaign_name: z5.string().max(120).optional().describe("Optional human-readable campaign name."),
15468
+ network_campaign_id: z5.string().optional().describe("The AD NETWORK's own campaign id (for Meta, the campaign ID in Ads Manager, which exists as soon as the campaign is saved, even as a draft). pull_network_spend matches report rows on it, so set it on every link whose spend you will pull: a link created without it can never match. Create the network campaign first, then the link, then paste the link into the ad."),
15469
+ campaign_id: z5.string().optional().describe("Optional EXISTING AttriKit campaign uuid (from list_campaigns) to attach this link to. Not the ad network's id: use network_campaign_id for that. Pass one or the other, not both."),
15470
+ serve_landing: z5.boolean().optional().describe("Serve the landing bridge on this link. Defaults to true at the API."),
14507
15471
  idempotency_key: idempotencyKeySchema
14508
15472
  }
14509
- }, async ({ app_id, network, destination_url, campaign_name, campaign_id, serve_landing, idempotency_key }) => toToolResult(await call("/api/v1/links", {
14510
- method: "POST",
14511
- idempotencyKey: idempotency_key,
14512
- body: {
14513
- appId: app_id,
14514
- network,
14515
- destinationUrl: destination_url,
14516
- ...campaign_name ? { campaignName: campaign_name } : {},
14517
- ...campaign_id ? { campaignId: campaign_id } : {},
14518
- ...serve_landing === undefined ? {} : { serveLanding: serve_landing }
14519
- }
14520
- })));
15473
+ }, async ({ app_id, network, destination_url, campaign_name, campaign_id, network_campaign_id, serve_landing, idempotency_key }) => {
15474
+ const body = { appId: app_id, network, destinationUrl: destination_url };
15475
+ if (campaign_name)
15476
+ body.campaignName = campaign_name;
15477
+ if (campaign_id)
15478
+ body.campaignId = campaign_id;
15479
+ if (network_campaign_id)
15480
+ body.networkCampaignId = network_campaign_id;
15481
+ if (serve_landing !== undefined)
15482
+ body.serveLanding = serve_landing;
15483
+ return toToolResult(await call("/api/v1/links", {
15484
+ method: "POST",
15485
+ idempotencyKey: idempotency_key,
15486
+ body
15487
+ }));
15488
+ });
14521
15489
  server.registerTool("update_link_status", {
14522
15490
  annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true },
14523
- title: "Enable or disable a campaign link",
14524
- description: "Change a tracking link between active and disabled.",
15491
+ title: "Enable or disable a campaign link, or attach its network campaign id",
15492
+ description: "Change a tracking link between active and disabled, and/or attach the ad network's own campaign id to a link created without one (then pull_network_spend can match its spend). Send status, network_campaign_id, or both. A campaign that already has a different network id is refused with 409 network_campaign_conflict.",
14525
15493
  inputSchema: {
14526
- link_id: z4.string().uuid(),
14527
- status: z4.enum(["active", "disabled"]),
15494
+ link_id: z5.string().uuid(),
15495
+ status: z5.enum(["active", "disabled"]).optional(),
15496
+ network_campaign_id: z5.string().optional().describe("The ad network's own campaign id (for Meta, the campaign ID in Ads Manager)."),
14528
15497
  idempotency_key: idempotencyKeySchema
14529
15498
  }
14530
- }, async ({ link_id, status, idempotency_key }) => toToolResult(await call(`/api/v1/links/${encodeURIComponent(link_id)}`, { method: "PATCH", idempotencyKey: idempotency_key, body: { status } })));
15499
+ }, async ({ link_id, status, network_campaign_id, idempotency_key }) => {
15500
+ const body = {};
15501
+ if (status !== undefined)
15502
+ body.status = status;
15503
+ if (network_campaign_id !== undefined)
15504
+ body.network_campaign_id = network_campaign_id;
15505
+ return toToolResult(await call(`/api/v1/links/${encodeURIComponent(link_id)}`, { method: "PATCH", idempotencyKey: idempotency_key, body }));
15506
+ });
14531
15507
  server.registerTool("get_spend", {
14532
15508
  annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true },
14533
15509
  title: "Get spend and cost models",
14534
- description: "Return imported ad spend, cost models, completeness and spend freshness.",
14535
- inputSchema: { app_id: appIdSchema }
14536
- }, async ({ app_id }) => toToolResult(await call("/api/v1/spend", {
14537
- query: { appId: app_id }
15510
+ description: "Return imported ad spend, cost models, completeness and spend freshness. Without from, to or cursor every row comes back at once; with any of them the rows are paged, ordered by network, campaign, ad set, ad and day, and next_cursor is null on the last page.",
15511
+ inputSchema: {
15512
+ app_id: appIdSchema,
15513
+ from: calendarDaySchema.optional().describe("First metric day to include (UTC, YYYY-MM-DD). May be sent without to."),
15514
+ to: calendarDaySchema.optional().describe("Last metric day to include (UTC, YYYY-MM-DD). May be sent without from."),
15515
+ cursor: cursorSchema,
15516
+ limit: z5.number().int().min(1).max(5000).optional().describe("Rows per page, 1-5000 (1000 when omitted). Read only in paged mode; alone it changes nothing.")
15517
+ }
15518
+ }, async ({ app_id, from, to, cursor, limit }) => toToolResult(await call("/api/v1/spend", {
15519
+ query: { appId: app_id, from, to, cursor, limit: limit === undefined ? undefined : String(limit) }
14538
15520
  })));
14539
15521
  server.registerTool("import_spend", {
14540
15522
  annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true },
@@ -14542,16 +15524,16 @@ function createServer(config, fetchImpl) {
14542
15524
  description: "Import campaign or creative-level spend rows.",
14543
15525
  inputSchema: {
14544
15526
  app_id: requiredAppIdSchema,
14545
- network: z4.enum(["meta", "google", "tiktok", "apple_ads"]),
14546
- source: z4.enum(["network_api", "csv", "manual"]).optional(),
14547
- account_ref: z4.string().optional(),
14548
- currency: z4.string().length(3),
14549
- report_timezone: z4.string().optional(),
14550
- covers_from: z4.string().regex(/^\d{4}-\d{2}-\d{2}$/),
14551
- covers_to: z4.string().regex(/^\d{4}-\d{2}-\d{2}$/),
14552
- completeness: z4.enum(["partial", "final"]).optional(),
14553
- notes: z4.string().optional(),
14554
- rows: z4.array(spendRowSchema).min(1).max(5000),
15527
+ network: z5.enum(["meta", "google", "tiktok", "apple_ads"]),
15528
+ source: z5.enum(["network_api", "csv", "manual"]).optional(),
15529
+ account_ref: z5.string().optional(),
15530
+ currency: z5.string().length(3),
15531
+ report_timezone: z5.string().optional(),
15532
+ covers_from: z5.string().regex(/^\d{4}-\d{2}-\d{2}$/),
15533
+ covers_to: z5.string().regex(/^\d{4}-\d{2}-\d{2}$/),
15534
+ completeness: z5.enum(["partial", "final"]).optional(),
15535
+ notes: z5.string().optional(),
15536
+ rows: z5.array(spendRowSchema).min(1).max(5000),
14555
15537
  idempotency_key: idempotencyKeySchema
14556
15538
  }
14557
15539
  }, async ({
@@ -14567,36 +15549,44 @@ function createServer(config, fetchImpl) {
14567
15549
  notes,
14568
15550
  rows,
14569
15551
  idempotency_key
14570
- }) => toToolResult(await call("/api/v1/spend", {
14571
- method: "POST",
14572
- idempotencyKey: idempotency_key,
14573
- body: {
15552
+ }) => {
15553
+ const body = {
14574
15554
  kind: "spend",
14575
15555
  appId: app_id,
14576
15556
  network,
14577
- ...source ? { source } : {},
14578
- ...account_ref ? { accountRef: account_ref } : {},
14579
15557
  currency,
14580
- ...report_timezone ? { reportTimezone: report_timezone } : {},
14581
15558
  coversFrom: covers_from,
14582
15559
  coversTo: covers_to,
14583
- ...completeness ? { completeness } : {},
14584
- ...notes ? { notes } : {},
14585
15560
  rows: rows.map(normalizeSpendRow)
14586
- }
14587
- })));
15561
+ };
15562
+ if (source)
15563
+ body.source = source;
15564
+ if (account_ref)
15565
+ body.accountRef = account_ref;
15566
+ if (report_timezone)
15567
+ body.reportTimezone = report_timezone;
15568
+ if (completeness)
15569
+ body.completeness = completeness;
15570
+ if (notes)
15571
+ body.notes = notes;
15572
+ return toToolResult(await call("/api/v1/spend", {
15573
+ method: "POST",
15574
+ idempotencyKey: idempotency_key,
15575
+ body
15576
+ }));
15577
+ });
14588
15578
  server.registerTool("set_cost_model", {
14589
15579
  annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true },
14590
15580
  title: "Set an app cost model",
14591
15581
  description: "Version commission, payment fees and variable COGS used in profit decisions.",
14592
15582
  inputSchema: {
14593
15583
  app_id: requiredAppIdSchema,
14594
- effective_at: z4.string(),
14595
- store_commission_bps: z4.number().int().min(0).max(1e4),
14596
- payment_fee_bps: z4.number().int().min(0).max(1e4),
14597
- variable_cogs_kind: z4.enum(["bps", "per_unit_minor"]),
14598
- variable_cogs_value: z4.number().nonnegative(),
14599
- assumptions: z4.record(z4.string(), z4.unknown()).optional(),
15584
+ effective_at: z5.string(),
15585
+ store_commission_bps: z5.number().int().min(0).max(1e4),
15586
+ payment_fee_bps: z5.number().int().min(0).max(1e4),
15587
+ variable_cogs_kind: z5.enum(["bps", "per_unit_minor"]),
15588
+ variable_cogs_value: z5.number().nonnegative(),
15589
+ assumptions: z5.record(z5.string(), z5.unknown()).optional(),
14600
15590
  idempotency_key: idempotencyKeySchema
14601
15591
  }
14602
15592
  }, async ({
@@ -14608,43 +15598,52 @@ function createServer(config, fetchImpl) {
14608
15598
  variable_cogs_value,
14609
15599
  assumptions,
14610
15600
  idempotency_key
14611
- }) => toToolResult(await call("/api/v1/spend", {
14612
- method: "POST",
14613
- idempotencyKey: idempotency_key,
14614
- body: {
15601
+ }) => {
15602
+ const body = {
14615
15603
  kind: "cost_model",
14616
15604
  appId: app_id,
14617
15605
  effectiveAt: effective_at,
14618
15606
  storeCommissionBps: store_commission_bps,
14619
15607
  paymentFeeBps: payment_fee_bps,
14620
- variableCogs: { kind: variable_cogs_kind, value: variable_cogs_value },
14621
- ...assumptions ? { assumptions } : {}
15608
+ variableCogs: { kind: variable_cogs_kind, value: variable_cogs_value }
15609
+ };
15610
+ if (assumptions) {
15611
+ body.assumptions = assumptions;
14622
15612
  }
14623
- })));
15613
+ return toToolResult(await call("/api/v1/spend", {
15614
+ method: "POST",
15615
+ idempotencyKey: idempotency_key,
15616
+ body
15617
+ }));
15618
+ });
14624
15619
  server.registerTool("pull_network_spend", {
14625
15620
  annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true },
14626
15621
  title: "Pull spend from an ad network",
14627
- description: "Fetch and import creative-level spend from a configured Meta, Google or TikTok connection.",
15622
+ description: "Fetch and import creative-level spend from a configured Meta, Google or TikTok connection. AttriKit already pulls every enabled connection every 6 hours, covering today and the 3 days before; call this for an immediate pull or a longer window. Report rows are kept only for campaigns whose network id AttriKit knows (create_campaign_link.network_campaign_id); if none match, the call fails and imports nothing. A network reports rows only for ads that have delivered, so the first useful pull is after the ads start spending.",
14628
15623
  inputSchema: {
14629
15624
  app_id: requiredAppIdSchema,
14630
- network: z4.enum(["meta", "google", "tiktok"]),
14631
- connection_id: z4.string().optional(),
14632
- covers_from: z4.string().regex(/^\d{4}-\d{2}-\d{2}$/),
14633
- covers_to: z4.string().regex(/^\d{4}-\d{2}-\d{2}$/),
15625
+ network: z5.enum(["meta", "google", "tiktok"]),
15626
+ connection_id: z5.string().optional(),
15627
+ covers_from: z5.string().regex(/^\d{4}-\d{2}-\d{2}$/),
15628
+ covers_to: z5.string().regex(/^\d{4}-\d{2}-\d{2}$/),
14634
15629
  idempotency_key: idempotencyKeySchema
14635
15630
  }
14636
- }, async ({ app_id, network, connection_id, covers_from, covers_to, idempotency_key }) => toToolResult(await call("/api/v1/spend", {
14637
- method: "POST",
14638
- idempotencyKey: idempotency_key,
14639
- body: {
15631
+ }, async ({ app_id, network, connection_id, covers_from, covers_to, idempotency_key }) => {
15632
+ const body = {
14640
15633
  kind: "network_pull",
14641
15634
  appId: app_id,
14642
15635
  network,
14643
- ...connection_id ? { connectionId: connection_id } : {},
14644
15636
  coversFrom: covers_from,
14645
15637
  coversTo: covers_to
14646
- }
14647
- })));
15638
+ };
15639
+ if (connection_id)
15640
+ body.connectionId = connection_id;
15641
+ return toToolResult(await call("/api/v1/spend", {
15642
+ method: "POST",
15643
+ idempotencyKey: idempotency_key,
15644
+ body
15645
+ }));
15646
+ });
14648
15647
  server.registerTool("list_integrations", {
14649
15648
  annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true },
14650
15649
  title: "List ad-network integrations",
@@ -14654,13 +15653,13 @@ function createServer(config, fetchImpl) {
14654
15653
  server.registerTool("create_integration", {
14655
15654
  annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true },
14656
15655
  title: "Create an ad-network integration",
14657
- description: "Store encrypted network credentials and configure event ownership.",
15656
+ description: "Store encrypted network credentials and choose, for each of the eight event families (install, session, trial, subscription, purchase, refund, paywall, business), which source's copy AttriKit forwards to the network. AttriKit always does the sending; the owner only picks the copy. attrkit = the events your app reports through the AttriKit SDK (the RevenueCat copy of that family is dropped). revenuecat = the events AttriKit received from your RevenueCat webhook (the SDK copy is dropped); RevenueCat itself sends nothing to the network. customer = events your own servers post to AttriKit. If your app reports subscriptions only through RevenueCat, set trial, subscription, purchase and refund to revenuecat, or none of them reach the network.",
14658
15657
  inputSchema: {
14659
- network: z4.enum(["meta", "google", "tiktok"]),
14660
- account_ref: z4.string().min(1),
14661
- credential: z4.string().min(1),
14662
- owners: ownersSchema.describe("Which destination owns each event family. Enabling a destination requires ALL eight families to be present, or the API rejects the call."),
14663
- enable: z4.boolean().optional(),
15658
+ network: z5.enum(["meta", "google", "tiktok"]),
15659
+ account_ref: z5.string().min(1),
15660
+ credential: z5.string().min(1),
15661
+ owners: ownersSchema.describe("For each event family, whose copy is forwarded: attrkit, revenuecat or customer (see the tool description). Enabling a destination requires ALL eight families to be present, or the API rejects the call."),
15662
+ enable: z5.boolean().optional(),
14664
15663
  idempotency_key: idempotencyKeySchema
14665
15664
  }
14666
15665
  }, async ({ network, account_ref, credential, owners, enable, idempotency_key }) => toToolResult(await call("/api/v1/integrations", {
@@ -14673,48 +15672,57 @@ function createServer(config, fetchImpl) {
14673
15672
  title: "Update an ad-network integration",
14674
15673
  description: "Rotate credentials or enable/disable an integration.",
14675
15674
  inputSchema: {
14676
- connection_id: z4.string(),
14677
- action: z4.enum(["rotate", "disable", "enable"]),
14678
- credential: z4.string().optional(),
15675
+ connection_id: z5.string(),
15676
+ action: z5.enum(["rotate", "disable", "enable"]),
15677
+ credential: z5.string().optional(),
14679
15678
  owners: ownersSchema.optional(),
14680
15679
  idempotency_key: idempotencyKeySchema
14681
15680
  }
14682
- }, async ({ connection_id, action, credential, owners, idempotency_key }) => toToolResult(await call(`/api/v1/integrations/${encodeURIComponent(connection_id)}`, {
14683
- method: "PATCH",
14684
- idempotencyKey: idempotency_key,
14685
- body: {
14686
- action,
14687
- ...credential ? { credential } : {},
14688
- ...owners ? { owners } : {}
14689
- }
14690
- })));
15681
+ }, async ({ connection_id, action, credential, owners, idempotency_key }) => {
15682
+ const body = { action };
15683
+ if (credential !== undefined)
15684
+ body.credential = credential;
15685
+ if (owners)
15686
+ body.owners = owners;
15687
+ return toToolResult(await call(`/api/v1/integrations/${encodeOpaquePathSegment(connection_id)}`, {
15688
+ method: "PATCH",
15689
+ idempotencyKey: idempotency_key,
15690
+ body
15691
+ }));
15692
+ });
14691
15693
  server.registerTool("delete_integration", {
14692
15694
  annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: true },
14693
15695
  title: "Delete an ad-network integration",
14694
15696
  description: "Delete a disabled integration. Enabled integrations must be disabled first.",
14695
- inputSchema: { connection_id: z4.string() }
14696
- }, async ({ connection_id }) => toToolResult(await call(`/api/v1/integrations/${encodeURIComponent(connection_id)}`, { method: "DELETE" })));
15697
+ inputSchema: { connection_id: z5.string() }
15698
+ }, async ({ connection_id }) => toToolResult(await call(`/api/v1/integrations/${encodeOpaquePathSegment(connection_id)}`, { method: "DELETE" })));
14697
15699
  server.registerTool("get_revenuecat", {
14698
15700
  annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true },
14699
15701
  title: "Get RevenueCat configuration",
14700
- description: "Return redacted RevenueCat status and the AttrKit webhook URL.",
15702
+ description: "Return redacted RevenueCat status and the AttriKit webhook URL.",
14701
15703
  inputSchema: { app_id: requiredAppIdSchema }
14702
- }, async ({ app_id }) => toToolResult(await call(`/api/v1/apps/${encodeURIComponent(app_id)}/revenuecat`)));
15704
+ }, async ({ app_id }) => toToolResult(await call(`/api/v1/apps/${encodeOpaquePathSegment(app_id)}/revenuecat`)));
14703
15705
  server.registerTool("configure_revenuecat", {
14704
- annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true },
15706
+ annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: true },
14705
15707
  title: "Configure RevenueCat",
14706
- description: "Store the RevenueCat provider app id and encrypted authorization secret for an app.",
15708
+ description: "Store the RevenueCat provider app id and encrypted authorization secret for an app, and optionally the webhook signing secret (omitted, the stored one is kept).",
14707
15709
  inputSchema: {
14708
15710
  app_id: requiredAppIdSchema,
14709
15711
  provider_app_id: revenueCatProviderAppIdSchema.describe("RevenueCat's PROJECT app id from their dashboard app settings, not the SDK key."),
14710
- authorization_secret: z4.string().min(16).max(1024),
15712
+ authorization_secret: z5.string().min(16).max(1024),
15713
+ signing_secret: z5.string().trim().min(16).max(1024).optional().describe("RevenueCat's HMAC signing secret, for the X-RevenueCat-Webhook-Signature header. Optional: omitted, the stored one is kept."),
14711
15714
  idempotency_key: idempotencyKeySchema
14712
15715
  }
14713
- }, async ({ app_id, provider_app_id, authorization_secret, idempotency_key }) => toToolResult(await call(`/api/v1/apps/${encodeURIComponent(app_id)}/revenuecat`, {
14714
- method: "PUT",
14715
- idempotencyKey: idempotency_key,
14716
- body: { provider_app_id, authorization_secret }
14717
- })));
15716
+ }, async ({ app_id, provider_app_id, authorization_secret, signing_secret, idempotency_key }) => {
15717
+ const body = { provider_app_id, authorization_secret };
15718
+ if (signing_secret !== undefined)
15719
+ body.signing_secret = signing_secret;
15720
+ return toToolResult(await call(`/api/v1/apps/${encodeOpaquePathSegment(app_id)}/revenuecat`, {
15721
+ method: "PUT",
15722
+ idempotencyKey: idempotency_key,
15723
+ body
15724
+ }));
15725
+ });
14718
15726
  server.registerTool("list_webhooks", {
14719
15727
  annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true },
14720
15728
  title: "List outbound webhooks",
@@ -14726,41 +15734,98 @@ function createServer(config, fetchImpl) {
14726
15734
  title: "Create an outbound webhook",
14727
15735
  description: "Create an SSRF-checked webhook subscription. Returns the signing secret once.",
14728
15736
  inputSchema: {
14729
- url: z4.string().url(),
14730
- event_types: z4.array(z4.enum(["install", "event", "decision", "fraud"])).min(1).max(4),
14731
- description: z4.string().max(500).optional(),
14732
- app_id: z4.string().uuid().optional(),
15737
+ url: z5.string().url(),
15738
+ event_types: z5.array(z5.enum(["install", "event", "decision", "fraud"])).min(1).max(4),
15739
+ description: z5.string().max(500).optional(),
15740
+ app_id: z5.string().uuid().optional(),
14733
15741
  idempotency_key: idempotencyKeySchema
14734
15742
  }
14735
- }, async ({ url, event_types, description, app_id, idempotency_key }) => toToolResult(await call("/api/v1/webhooks", {
14736
- method: "POST",
14737
- idempotencyKey: idempotency_key,
14738
- body: {
14739
- url,
14740
- event_types,
14741
- ...description ? { description } : {},
14742
- ...app_id ? { app_id } : {}
14743
- }
14744
- })));
15743
+ }, async ({ url, event_types, description, app_id, idempotency_key }) => {
15744
+ const body = { url, event_types };
15745
+ if (description)
15746
+ body.description = description;
15747
+ if (app_id)
15748
+ body.app_id = app_id;
15749
+ return toToolResult(await call("/api/v1/webhooks", {
15750
+ method: "POST",
15751
+ idempotencyKey: idempotency_key,
15752
+ body
15753
+ }));
15754
+ });
14745
15755
  server.registerTool("delete_webhook", {
14746
15756
  annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: true },
14747
15757
  title: "Delete an outbound webhook",
14748
15758
  description: "Delete a webhook subscription.",
14749
- inputSchema: { subscription_id: z4.string().uuid() }
15759
+ inputSchema: { subscription_id: z5.string().uuid() }
14750
15760
  }, async ({ subscription_id }) => toToolResult(await call(`/api/v1/webhooks/${encodeURIComponent(subscription_id)}`, { method: "DELETE" })));
14751
15761
  server.registerTool("export_events", {
14752
15762
  annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true },
14753
15763
  title: "Export events and installs",
14754
- description: "Export one app's events and installs as CSV for an inclusive date range.",
15764
+ description: "Export one app's events and installs for an inclusive date range. By default (version 1) the answer is the seven-column CSV, event rows capped at the 100,000 most recent INSIDE the range; narrow the range until it is safely below that cap when a complete snapshot is required. Pass version 2 for the wide record export (campaign, ad, evidence and revenue columns), oldest first, as csv or ndjson and paged: each page holds up to `limit` records (10,000 by default), and when another page follows the result carries next_cursor, which you send back as `cursor` with the same app_id, from and to. No next_cursor means the last page.",
14755
15765
  inputSchema: {
14756
15766
  app_id: requiredAppIdSchema,
14757
- from: z4.string().describe("ISO date or timestamp"),
14758
- to: z4.string().describe("ISO date or timestamp")
15767
+ from: z5.string().describe("ISO date or timestamp"),
15768
+ to: z5.string().describe("ISO date or timestamp"),
15769
+ version: z5.number().int().min(1).max(2).optional().describe("Export version, 1 or 2. Omit for version 1, the original CSV, unchanged. format, limit and cursor need version 2."),
15770
+ format: z5.enum(["csv", "ndjson"]).optional().describe("Version 2 only: csv (the default) or ndjson, one JSON record per line."),
15771
+ limit: z5.number().int().min(1).max(1e4).optional().describe("Version 2 only: records per page, 1-10,000 (10,000 when omitted)."),
15772
+ cursor: z5.string().optional().describe("Version 2 only: the next_cursor of the previous page. Omit for the first page.")
14759
15773
  }
14760
- }, async ({ app_id, from, to }) => toToolResult(await call("/api/v1/export", {
14761
- query: { appId: app_id, from, to },
14762
- expectedResponse: "text"
14763
- })));
15774
+ }, async ({ app_id, from, to, version, format, limit, cursor }) => {
15775
+ if (version !== 2 && (format !== undefined || limit !== undefined || cursor !== undefined)) {
15776
+ return toToolResult(invalidResult("invalid_request", "format, limit and cursor apply to version 2 only; pass version: 2."));
15777
+ }
15778
+ return toToolResult(await call("/api/v1/export", {
15779
+ query: {
15780
+ appId: app_id,
15781
+ from,
15782
+ to,
15783
+ version: version === undefined ? undefined : String(version),
15784
+ format,
15785
+ limit: limit === undefined ? undefined : String(limit),
15786
+ cursor
15787
+ },
15788
+ expectedResponse: format === "ndjson" ? "ndjson" : "text"
15789
+ }));
15790
+ });
15791
+ server.registerTool("send_server_event", {
15792
+ annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: true },
15793
+ title: "Report an event from your backend",
15794
+ description: "Report an event for one of your users from your own backend, by the customer_user_id your app passed to AttriKit.setUserID; no device installation id is needed. The event is attached to the install that id resolves to and reaches the destinations whose sender for its family is Your servers. Requires a write-scoped API key. Answers status accepted, duplicate (this event_id was already stored) or suppressed (consent.measurement is not granted, so nothing was collected), with the install_epoch_id it resolved to. 404 customer_user_not_found means no install identified with this id.",
15795
+ inputSchema: {
15796
+ app_id: requiredAppIdSchema,
15797
+ customer_user_id: z5.string().min(1).max(256).describe("The opaque id your app passed to AttriKit.setUserID. Refused when it looks like an email address or phone number. Hashed on arrival, never stored."),
15798
+ event_id: z5.string().uuid().describe("Your id for this event. A second event with the same id is a duplicate and is not written again."),
15799
+ event_name: z5.string().min(1).max(128).regex(/^[a-z][a-z0-9_.-]*$/).describe("Canonical event name, for example purchase or subscription_started."),
15800
+ event_version: z5.number().int().positive(),
15801
+ occurred_at: z5.string().datetime({ offset: true }).describe("When the event happened, ISO 8601 with a UTC offset."),
15802
+ consent: z5.object({
15803
+ measurement: z5.enum(["granted", "denied", "unknown"]),
15804
+ tracking: z5.enum(["granted", "denied", "unknown"]),
15805
+ policy_version: z5.number().int().positive()
15806
+ }).describe("The per-event consent SDK events carry. An event whose measurement is not granted is not collected."),
15807
+ properties: z5.record(z5.string(), z5.union([z5.string().max(1024), z5.number(), z5.boolean(), z5.null()])).optional().describe("Scalar values only, at most 64 keys. Revenue is value (major units) or price_minor_units with currency, an ISO 4217 code."),
15808
+ idempotency_key: idempotencyKeySchema
15809
+ }
15810
+ }, async ({ app_id, customer_user_id, event_id, event_name, event_version, occurred_at, consent, properties, idempotency_key }) => {
15811
+ const body = { customer_user_id, event_id, event_name, event_version, occurred_at, consent };
15812
+ if (properties !== undefined)
15813
+ body.properties = properties;
15814
+ return toToolResult(await call(`/api/v1/apps/${encodeOpaquePathSegment(app_id)}/events`, {
15815
+ method: "POST",
15816
+ idempotencyKey: idempotency_key,
15817
+ body
15818
+ }));
15819
+ });
15820
+ server.registerTool("lookup_customer_attribution", {
15821
+ annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true },
15822
+ title: "Look up a user's attribution",
15823
+ description: "Look up the attribution of one of your users by the customer_user_id your app passed to AttriKit.setUserID: the campaign, ad set, ad, network, match confidence and attribution_status (attributed, device_matched, organic, pending or consent_required) of the user's newest install. 404 customer_user_not_found means no install identified with this id. Read scope is enough.",
15824
+ inputSchema: {
15825
+ app_id: requiredAppIdSchema,
15826
+ customer_user_id: z5.string().min(1).max(256).describe("The opaque id your app passed to AttriKit.setUserID. Refused when it looks like an email address or phone number.")
15827
+ }
15828
+ }, async ({ app_id, customer_user_id }) => toToolResult(await call(`/api/v1/apps/${encodeOpaquePathSegment(app_id)}/attribution/lookup`, { method: "POST", body: { customer_user_id }, readsOnly: true })));
14764
15829
  return server;
14765
15830
  }
14766
15831
 
@@ -14771,8 +15836,7 @@ async function main() {
14771
15836
  config = readConfig();
14772
15837
  } catch (error) {
14773
15838
  process.stderr.write(`${error instanceof Error ? error.message : String(error)}
14774
- `);
14775
- process.exit(1);
15839
+ `, () => process.exit(1));
14776
15840
  return;
14777
15841
  }
14778
15842
  const server = createServer(config);
@@ -14783,6 +15847,5 @@ async function main() {
14783
15847
  }
14784
15848
  main().catch((error) => {
14785
15849
  process.stderr.write(`attrkit-mcp failed to start: ${error instanceof Error ? error.stack ?? error.message : String(error)}
14786
- `);
14787
- process.exit(1);
15850
+ `, () => process.exit(1));
14788
15851
  });