@attrkit/mcp 0.4.6 → 1.1.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.
- package/CHANGELOG.md +135 -0
- package/README.md +24 -5
- package/dist/cli.js +1546 -457
- 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 {
|
|
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
|
|
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
|
-
|
|
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
|
|
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 =
|
|
13777
|
-
var deliveryOwnerSchema =
|
|
13778
|
-
var ownershipMatrixSchema =
|
|
13779
|
-
var destinationActivationSchema =
|
|
13780
|
-
enabled:
|
|
13781
|
-
revenuecat_connected:
|
|
13782
|
-
ownership:
|
|
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,168 @@ var blockedHostnames = new Set([
|
|
|
13837
13870
|
"metadata.google.internal"
|
|
13838
13871
|
]);
|
|
13839
13872
|
// ../shared/src/ingestion.ts
|
|
13840
|
-
import { z as
|
|
13841
|
-
var consentStateSchema =
|
|
13842
|
-
state:
|
|
13843
|
-
policy_version:
|
|
13844
|
-
framework_string:
|
|
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()
|
|
13845
13878
|
}).strict();
|
|
13846
|
-
var
|
|
13847
|
-
|
|
13848
|
-
|
|
13849
|
-
|
|
13850
|
-
|
|
13851
|
-
locale: z2.string().max(35).optional()
|
|
13879
|
+
var dmaConsentSchema = z3.object({
|
|
13880
|
+
eea: z3.boolean(),
|
|
13881
|
+
ad_user_data: z3.boolean().optional(),
|
|
13882
|
+
ad_personalization: z3.boolean().optional(),
|
|
13883
|
+
source: z3.enum(["tcf", "manual"])
|
|
13852
13884
|
}).strict();
|
|
13853
|
-
var
|
|
13854
|
-
|
|
13855
|
-
|
|
13856
|
-
|
|
13885
|
+
var firstOpenConsentSchema = consentStateSchema.extend({ dma: dmaConsentSchema.optional() }).strict();
|
|
13886
|
+
var coarseContextSchema = z3.object({
|
|
13887
|
+
country_code: z3.string().regex(/^[A-Z]{2}$/).optional(),
|
|
13888
|
+
region_code: z3.string().max(16).optional(),
|
|
13889
|
+
os_major: z3.string().max(16).optional(),
|
|
13890
|
+
device_class: z3.enum(["phone", "tablet", "desktop", "unknown"]).optional(),
|
|
13891
|
+
locale: z3.string().max(35).optional()
|
|
13857
13892
|
}).strict();
|
|
13858
|
-
var
|
|
13859
|
-
|
|
13860
|
-
|
|
13893
|
+
var exactTokenReferenceSchema = z3.object({
|
|
13894
|
+
token: z3.string().min(16).max(512),
|
|
13895
|
+
kind: z3.enum(["clipboard", "owned_deferred", "customer_signed"]),
|
|
13896
|
+
clipboard_opt_in: z3.boolean().optional()
|
|
13897
|
+
}).strict().refine((value) => value.kind !== "clipboard" || value.clipboard_opt_in === true, { message: "clipboard tokens require explicit opt-in", path: ["clipboard_opt_in"] });
|
|
13898
|
+
var playInstallReferrerSchema = z3.object({
|
|
13899
|
+
referrer: z3.string().max(1024),
|
|
13900
|
+
captured_at: z3.string().datetime({ offset: true })
|
|
13861
13901
|
}).strict();
|
|
13862
13902
|
var NIL_UUID = "00000000-0000-0000-0000-000000000000";
|
|
13863
|
-
var
|
|
13864
|
-
var
|
|
13865
|
-
var
|
|
13866
|
-
|
|
13867
|
-
|
|
13868
|
-
|
|
13869
|
-
|
|
13870
|
-
|
|
13871
|
-
|
|
13872
|
-
|
|
13873
|
-
|
|
13874
|
-
|
|
13903
|
+
var EMAIL_PATTERN = /\b[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}\b/i;
|
|
13904
|
+
var PHONE_PATTERN = /(?:^|\D)(?:\+?\d[\d\s().-]{7,}\d)(?:$|\D)/;
|
|
13905
|
+
var NON_PHONE_VALUE_FORMS = [
|
|
13906
|
+
/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i,
|
|
13907
|
+
/^\d{4}-\d{2}-\d{2}(?:[T ]\d{2}:\d{2}(?::\d{2}(?:\.\d+)?)?(?:Z|[+-]\d{2}:?\d{2})?)?$/,
|
|
13908
|
+
/^-?\d+\.\d+$/
|
|
13909
|
+
];
|
|
13910
|
+
function looksLikePii(value) {
|
|
13911
|
+
if (EMAIL_PATTERN.test(value))
|
|
13912
|
+
return true;
|
|
13913
|
+
if (NON_PHONE_VALUE_FORMS.some((form) => form.test(value.trim())))
|
|
13914
|
+
return false;
|
|
13915
|
+
return PHONE_PATTERN.test(value.replace(EMBEDDED_UUID_PATTERN, ""));
|
|
13916
|
+
}
|
|
13917
|
+
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;
|
|
13918
|
+
var sha256HexSchema = z3.string().regex(/^[0-9a-f]{64}$/);
|
|
13919
|
+
var idfaSchema = z3.string().uuid().refine((value) => value.toLowerCase() !== NIL_UUID, { message: "IDFA must not be all zeros" }).transform((value) => value.toLowerCase());
|
|
13920
|
+
var idfvSchema = z3.string().uuid().transform((value) => value.toLowerCase());
|
|
13921
|
+
var genderSchema = z3.enum(["m", "f"]).nullable();
|
|
13922
|
+
var deviceSignalsSchema = z3.object({
|
|
13923
|
+
device_model: z3.string().min(1).max(128).optional(),
|
|
13924
|
+
os_build: z3.string().min(1).max(128).optional(),
|
|
13925
|
+
timezone: z3.string().min(1).max(128).optional(),
|
|
13926
|
+
screen: z3.object({
|
|
13927
|
+
w: z3.number().int().positive().max(16384),
|
|
13928
|
+
h: z3.number().int().positive().max(16384),
|
|
13929
|
+
scale: z3.number().positive().max(8)
|
|
13875
13930
|
}).strict().optional(),
|
|
13876
|
-
languages:
|
|
13931
|
+
languages: z3.array(z3.string().min(1).max(35)).max(16).optional()
|
|
13877
13932
|
}).strict();
|
|
13878
|
-
var
|
|
13879
|
-
|
|
13880
|
-
|
|
13933
|
+
var TIKTOK_BROWSER_ID_PATTERN = /^[A-Za-z0-9._~-]{1,512}$/;
|
|
13934
|
+
var webFirstPartySchema = z3.object({
|
|
13935
|
+
fbc: z3.string().min(1).max(512).optional(),
|
|
13936
|
+
fbp: z3.string().min(1).max(512).optional(),
|
|
13937
|
+
ttp: z3.string().regex(TIKTOK_BROWSER_ID_PATTERN).optional(),
|
|
13881
13938
|
email_hash: sha256HexSchema.optional(),
|
|
13882
13939
|
phone_hash: sha256HexSchema.optional(),
|
|
13883
13940
|
dob_hash: sha256HexSchema.optional(),
|
|
13884
13941
|
gender: genderSchema.optional()
|
|
13885
13942
|
}).strict();
|
|
13886
|
-
var firstOpenEnvelopeSchema =
|
|
13887
|
-
schema_version:
|
|
13888
|
-
installation_id:
|
|
13889
|
-
install_epoch_id:
|
|
13890
|
-
occurred_at:
|
|
13891
|
-
app_version:
|
|
13943
|
+
var firstOpenEnvelopeSchema = z3.object({
|
|
13944
|
+
schema_version: z3.literal(1),
|
|
13945
|
+
installation_id: z3.string().uuid(),
|
|
13946
|
+
install_epoch_id: z3.string().uuid(),
|
|
13947
|
+
occurred_at: z3.string().datetime({ offset: true }),
|
|
13948
|
+
app_version: z3.string().min(1).max(128),
|
|
13892
13949
|
coarse_context: coarseContextSchema,
|
|
13893
|
-
consent:
|
|
13894
|
-
app_transaction_jws:
|
|
13895
|
-
asa_token:
|
|
13950
|
+
consent: firstOpenConsentSchema,
|
|
13951
|
+
app_transaction_jws: z3.string().min(16).max(32768).optional(),
|
|
13952
|
+
asa_token: z3.string().min(8).max(8192).optional(),
|
|
13896
13953
|
exact_token_ref: exactTokenReferenceSchema.optional(),
|
|
13897
13954
|
play_install_referrer: playInstallReferrerSchema.optional(),
|
|
13898
13955
|
device_signals: deviceSignalsSchema.optional(),
|
|
13899
13956
|
web_first_party: webFirstPartySchema.optional(),
|
|
13900
13957
|
idfa: idfaSchema.optional(),
|
|
13901
13958
|
idfv: idfvSchema.optional(),
|
|
13902
|
-
local_lineage_present:
|
|
13903
|
-
local_epoch_present:
|
|
13904
|
-
local_signals_conflict:
|
|
13959
|
+
local_lineage_present: z3.boolean().default(false),
|
|
13960
|
+
local_epoch_present: z3.boolean().default(false),
|
|
13961
|
+
local_signals_conflict: z3.boolean().default(false)
|
|
13905
13962
|
}).strict().refine((value) => value.idfa === undefined || value.consent.state === "tracking_granted", { message: "IDFA requires ATT tracking authorization", path: ["idfa"] });
|
|
13906
|
-
var
|
|
13907
|
-
|
|
13908
|
-
|
|
13909
|
-
|
|
13910
|
-
|
|
13911
|
-
|
|
13963
|
+
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");
|
|
13964
|
+
var identifyEnvelopeSchema = z3.object({
|
|
13965
|
+
schema_version: z3.literal(1),
|
|
13966
|
+
installation_id: z3.string().uuid(),
|
|
13967
|
+
install_epoch_id: z3.string().uuid(),
|
|
13968
|
+
occurred_at: z3.string().datetime({ offset: true }),
|
|
13969
|
+
customer_user_id: customerUserIdSchema.optional(),
|
|
13912
13970
|
email_hash: sha256HexSchema.optional(),
|
|
13913
13971
|
phone_hash: sha256HexSchema.optional(),
|
|
13914
13972
|
exact_token_ref: exactTokenReferenceSchema.optional(),
|
|
13915
13973
|
idfa: idfaSchema.optional(),
|
|
13916
13974
|
idfv: idfvSchema.optional()
|
|
13917
13975
|
}).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 =
|
|
13919
|
-
measurement:
|
|
13920
|
-
tracking:
|
|
13921
|
-
policy_version:
|
|
13976
|
+
var eventConsentSchema = z3.object({
|
|
13977
|
+
measurement: z3.enum(["granted", "denied", "unknown"]),
|
|
13978
|
+
tracking: z3.enum(["granted", "denied", "unknown"]),
|
|
13979
|
+
policy_version: z3.number().int().positive(),
|
|
13980
|
+
dma: dmaConsentSchema.optional()
|
|
13922
13981
|
}).strict();
|
|
13923
|
-
var
|
|
13924
|
-
|
|
13925
|
-
|
|
13926
|
-
|
|
13927
|
-
|
|
13928
|
-
z2.boolean(),
|
|
13929
|
-
z2.null()
|
|
13982
|
+
var propertyScalarSchema = z3.union([
|
|
13983
|
+
z3.string().max(1024),
|
|
13984
|
+
z3.number().finite(),
|
|
13985
|
+
z3.boolean(),
|
|
13986
|
+
z3.null()
|
|
13930
13987
|
]);
|
|
13931
|
-
var
|
|
13988
|
+
var piiCandidateSchema = z3.string();
|
|
13989
|
+
function isPiiPropertyKey(key) {
|
|
13990
|
+
if (/email|e-mail|phone|mobile|address/i.test(key))
|
|
13991
|
+
return true;
|
|
13992
|
+
const normalized = key.replace(/([a-z0-9])([A-Z])/g, "$1_$2").replaceAll("-", "_").toLowerCase();
|
|
13993
|
+
return /^(?:name|first_?name|last_?name|full_?name|given_?name|family_?name)$/.test(normalized);
|
|
13994
|
+
}
|
|
13995
|
+
var eventPropertiesSchema = z3.record(z3.string().min(1).max(64), propertyScalarSchema).superRefine((properties, context) => {
|
|
13996
|
+
if (Object.keys(properties).length > 64) {
|
|
13997
|
+
context.addIssue({ code: "custom", message: "at most 64 event properties are allowed" });
|
|
13998
|
+
}
|
|
13932
13999
|
for (const [key, value] of Object.entries(properties)) {
|
|
13933
|
-
if (
|
|
14000
|
+
if (isPiiPropertyKey(key)) {
|
|
13934
14001
|
context.addIssue({ code: "custom", path: [key], message: "PII property keys are forbidden" });
|
|
13935
14002
|
}
|
|
13936
|
-
|
|
14003
|
+
const piiCandidate = piiCandidateSchema.safeParse(value);
|
|
14004
|
+
if (piiCandidate.success && looksLikePii(piiCandidate.data)) {
|
|
13937
14005
|
context.addIssue({ code: "custom", path: [key], message: "PII-shaped property values are forbidden" });
|
|
13938
14006
|
}
|
|
13939
14007
|
}
|
|
13940
14008
|
});
|
|
13941
|
-
var
|
|
13942
|
-
|
|
13943
|
-
|
|
13944
|
-
|
|
13945
|
-
|
|
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"]),
|
|
14009
|
+
var canonicalEventFieldSchemas = {
|
|
14010
|
+
event_id: z3.string().uuid(),
|
|
14011
|
+
event_name: z3.string().min(1).max(128).regex(/^[a-z][a-z0-9_.-]*$/),
|
|
14012
|
+
event_version: z3.number().int().positive(),
|
|
14013
|
+
occurred_at: z3.string().datetime({ offset: true }),
|
|
13952
14014
|
consent: eventConsentSchema,
|
|
13953
|
-
properties: eventPropertiesSchema
|
|
13954
|
-
|
|
13955
|
-
|
|
13956
|
-
|
|
14015
|
+
properties: eventPropertiesSchema
|
|
14016
|
+
};
|
|
14017
|
+
var canonicalEventEnvelopeSchema = z3.object({
|
|
14018
|
+
schema_version: z3.literal(1),
|
|
14019
|
+
event_id: canonicalEventFieldSchemas.event_id,
|
|
14020
|
+
event_name: canonicalEventFieldSchemas.event_name,
|
|
14021
|
+
event_version: canonicalEventFieldSchemas.event_version,
|
|
14022
|
+
occurred_at: canonicalEventFieldSchemas.occurred_at,
|
|
14023
|
+
sent_at: z3.string().datetime({ offset: true }),
|
|
14024
|
+
installation_id: z3.string().uuid(),
|
|
14025
|
+
install_epoch_id: z3.string().uuid(),
|
|
14026
|
+
session_id: z3.string().uuid(),
|
|
14027
|
+
source: z3.enum(["ios_sdk", "android_sdk", "web_sdk", "server", "provider"]),
|
|
14028
|
+
consent: canonicalEventFieldSchemas.consent,
|
|
14029
|
+
properties: canonicalEventFieldSchemas.properties,
|
|
14030
|
+
provider: z3.string().min(1).max(64).optional(),
|
|
14031
|
+
provider_event_id: z3.string().min(1).max(256).optional(),
|
|
14032
|
+
supersedes_event_id: z3.string().uuid().optional()
|
|
13957
14033
|
}).strict().superRefine((event, context) => {
|
|
14034
|
+
const hasProviderFields = event.provider !== undefined || event.provider_event_id !== undefined;
|
|
13958
14035
|
if (event.provider === undefined !== (event.provider_event_id === undefined)) {
|
|
13959
14036
|
context.addIssue({
|
|
13960
14037
|
code: "custom",
|
|
@@ -13962,26 +14039,40 @@ var canonicalEventEnvelopeSchema = z2.object({
|
|
|
13962
14039
|
message: "provider and provider_event_id must be supplied together"
|
|
13963
14040
|
});
|
|
13964
14041
|
}
|
|
14042
|
+
if (event.source === "provider" && !hasProviderFields) {
|
|
14043
|
+
context.addIssue({
|
|
14044
|
+
code: "custom",
|
|
14045
|
+
path: ["provider"],
|
|
14046
|
+
message: "provider-sourced events require provider and provider_event_id"
|
|
14047
|
+
});
|
|
14048
|
+
}
|
|
14049
|
+
if (event.source !== "provider" && hasProviderFields) {
|
|
14050
|
+
context.addIssue({
|
|
14051
|
+
code: "custom",
|
|
14052
|
+
path: ["provider"],
|
|
14053
|
+
message: "provider fields are forbidden unless source is provider"
|
|
14054
|
+
});
|
|
14055
|
+
}
|
|
13965
14056
|
});
|
|
13966
|
-
var canonicalEventBatchSchema =
|
|
13967
|
-
batch_id:
|
|
13968
|
-
events:
|
|
14057
|
+
var canonicalEventBatchSchema = z3.object({
|
|
14058
|
+
batch_id: z3.string().min(1).max(256),
|
|
14059
|
+
events: z3.array(canonicalEventEnvelopeSchema).min(1).max(100)
|
|
13969
14060
|
}).strict();
|
|
13970
|
-
var consentReceiptSchema =
|
|
13971
|
-
installation_id:
|
|
13972
|
-
install_epoch_id:
|
|
13973
|
-
scope:
|
|
14061
|
+
var consentReceiptSchema = z3.object({
|
|
14062
|
+
installation_id: z3.string().uuid(),
|
|
14063
|
+
install_epoch_id: z3.string().uuid(),
|
|
14064
|
+
scope: z3.enum(["measurement", "tracking"]),
|
|
13974
14065
|
consent: consentStateSchema,
|
|
13975
|
-
occurred_at:
|
|
13976
|
-
source:
|
|
14066
|
+
occurred_at: z3.string().datetime({ offset: true }),
|
|
14067
|
+
source: z3.enum(["ios_sdk", "android_sdk", "web_sdk", "server"])
|
|
13977
14068
|
}).strict();
|
|
13978
|
-
var sealedIngestEnvelopeSchema =
|
|
14069
|
+
var sealedIngestEnvelopeSchema = z3.object({
|
|
13979
14070
|
input: firstOpenEnvelopeSchema,
|
|
13980
|
-
received_at:
|
|
13981
|
-
client_skew_ms:
|
|
14071
|
+
received_at: z3.string().datetime({ offset: true }),
|
|
14072
|
+
client_skew_ms: z3.number().int()
|
|
13982
14073
|
}).strict();
|
|
13983
14074
|
// ../shared/src/revenuecat-provider-app-id.ts
|
|
13984
|
-
import { z as
|
|
14075
|
+
import { z as z4 } from "zod";
|
|
13985
14076
|
var KNOWN_WRONG_CREDENTIAL_PREFIXES = [
|
|
13986
14077
|
["appl_", "the iOS platform SDK key"],
|
|
13987
14078
|
["goog_", "the Android platform SDK key"],
|
|
@@ -14000,159 +14091,995 @@ function revenueCatProviderAppIdIssue(value) {
|
|
|
14000
14091
|
}
|
|
14001
14092
|
return null;
|
|
14002
14093
|
}
|
|
14003
|
-
var revenueCatProviderAppIdSchema =
|
|
14094
|
+
var revenueCatProviderAppIdSchema = z4.string().trim().min(1).max(255).superRefine((value, context) => {
|
|
14004
14095
|
const issue = revenueCatProviderAppIdIssue(value);
|
|
14005
14096
|
if (issue)
|
|
14006
14097
|
context.addIssue({ code: "custom", message: issue });
|
|
14007
14098
|
});
|
|
14099
|
+
// ../shared/src/iso-4217-exponents.json
|
|
14100
|
+
var iso_4217_exponents_default = {
|
|
14101
|
+
schemaVersion: 1,
|
|
14102
|
+
published: "2026-01-01",
|
|
14103
|
+
sources: {
|
|
14104
|
+
listOneSha256: "838dfb991648cf36df939edd5fe3811737962b75a32252847d239cedd1e291c9",
|
|
14105
|
+
listThreeSha256: "98fde2423cdb916dd59dcf5fe96222edad8fa198d865c1c83dbc464b9cc52387"
|
|
14106
|
+
},
|
|
14107
|
+
currencies: {
|
|
14108
|
+
AED: {
|
|
14109
|
+
exponent: 2,
|
|
14110
|
+
status: "current"
|
|
14111
|
+
},
|
|
14112
|
+
AFN: {
|
|
14113
|
+
exponent: 2,
|
|
14114
|
+
status: "current"
|
|
14115
|
+
},
|
|
14116
|
+
ALL: {
|
|
14117
|
+
exponent: 2,
|
|
14118
|
+
status: "current"
|
|
14119
|
+
},
|
|
14120
|
+
AMD: {
|
|
14121
|
+
exponent: 2,
|
|
14122
|
+
status: "current"
|
|
14123
|
+
},
|
|
14124
|
+
AOA: {
|
|
14125
|
+
exponent: 2,
|
|
14126
|
+
status: "current"
|
|
14127
|
+
},
|
|
14128
|
+
ARS: {
|
|
14129
|
+
exponent: 2,
|
|
14130
|
+
status: "current"
|
|
14131
|
+
},
|
|
14132
|
+
AUD: {
|
|
14133
|
+
exponent: 2,
|
|
14134
|
+
status: "current"
|
|
14135
|
+
},
|
|
14136
|
+
AWG: {
|
|
14137
|
+
exponent: 2,
|
|
14138
|
+
status: "current"
|
|
14139
|
+
},
|
|
14140
|
+
AZN: {
|
|
14141
|
+
exponent: 2,
|
|
14142
|
+
status: "current"
|
|
14143
|
+
},
|
|
14144
|
+
BAM: {
|
|
14145
|
+
exponent: 2,
|
|
14146
|
+
status: "current"
|
|
14147
|
+
},
|
|
14148
|
+
BBD: {
|
|
14149
|
+
exponent: 2,
|
|
14150
|
+
status: "current"
|
|
14151
|
+
},
|
|
14152
|
+
BDT: {
|
|
14153
|
+
exponent: 2,
|
|
14154
|
+
status: "current"
|
|
14155
|
+
},
|
|
14156
|
+
BHD: {
|
|
14157
|
+
exponent: 3,
|
|
14158
|
+
status: "current"
|
|
14159
|
+
},
|
|
14160
|
+
BIF: {
|
|
14161
|
+
exponent: 0,
|
|
14162
|
+
status: "current"
|
|
14163
|
+
},
|
|
14164
|
+
BMD: {
|
|
14165
|
+
exponent: 2,
|
|
14166
|
+
status: "current"
|
|
14167
|
+
},
|
|
14168
|
+
BND: {
|
|
14169
|
+
exponent: 2,
|
|
14170
|
+
status: "current"
|
|
14171
|
+
},
|
|
14172
|
+
BOB: {
|
|
14173
|
+
exponent: 2,
|
|
14174
|
+
status: "current"
|
|
14175
|
+
},
|
|
14176
|
+
BOV: {
|
|
14177
|
+
exponent: 2,
|
|
14178
|
+
status: "current"
|
|
14179
|
+
},
|
|
14180
|
+
BRL: {
|
|
14181
|
+
exponent: 2,
|
|
14182
|
+
status: "current"
|
|
14183
|
+
},
|
|
14184
|
+
BSD: {
|
|
14185
|
+
exponent: 2,
|
|
14186
|
+
status: "current"
|
|
14187
|
+
},
|
|
14188
|
+
BTN: {
|
|
14189
|
+
exponent: 2,
|
|
14190
|
+
status: "current"
|
|
14191
|
+
},
|
|
14192
|
+
BWP: {
|
|
14193
|
+
exponent: 2,
|
|
14194
|
+
status: "current"
|
|
14195
|
+
},
|
|
14196
|
+
BYN: {
|
|
14197
|
+
exponent: 2,
|
|
14198
|
+
status: "current"
|
|
14199
|
+
},
|
|
14200
|
+
BZD: {
|
|
14201
|
+
exponent: 2,
|
|
14202
|
+
status: "current"
|
|
14203
|
+
},
|
|
14204
|
+
CAD: {
|
|
14205
|
+
exponent: 2,
|
|
14206
|
+
status: "current"
|
|
14207
|
+
},
|
|
14208
|
+
CDF: {
|
|
14209
|
+
exponent: 2,
|
|
14210
|
+
status: "current"
|
|
14211
|
+
},
|
|
14212
|
+
CHE: {
|
|
14213
|
+
exponent: 2,
|
|
14214
|
+
status: "current"
|
|
14215
|
+
},
|
|
14216
|
+
CHF: {
|
|
14217
|
+
exponent: 2,
|
|
14218
|
+
status: "current"
|
|
14219
|
+
},
|
|
14220
|
+
CHW: {
|
|
14221
|
+
exponent: 2,
|
|
14222
|
+
status: "current"
|
|
14223
|
+
},
|
|
14224
|
+
CLF: {
|
|
14225
|
+
exponent: 4,
|
|
14226
|
+
status: "current"
|
|
14227
|
+
},
|
|
14228
|
+
CLP: {
|
|
14229
|
+
exponent: 0,
|
|
14230
|
+
status: "current"
|
|
14231
|
+
},
|
|
14232
|
+
CNY: {
|
|
14233
|
+
exponent: 2,
|
|
14234
|
+
status: "current"
|
|
14235
|
+
},
|
|
14236
|
+
COP: {
|
|
14237
|
+
exponent: 2,
|
|
14238
|
+
status: "current"
|
|
14239
|
+
},
|
|
14240
|
+
COU: {
|
|
14241
|
+
exponent: 2,
|
|
14242
|
+
status: "current"
|
|
14243
|
+
},
|
|
14244
|
+
CRC: {
|
|
14245
|
+
exponent: 2,
|
|
14246
|
+
status: "current"
|
|
14247
|
+
},
|
|
14248
|
+
CUP: {
|
|
14249
|
+
exponent: 2,
|
|
14250
|
+
status: "current"
|
|
14251
|
+
},
|
|
14252
|
+
CVE: {
|
|
14253
|
+
exponent: 2,
|
|
14254
|
+
status: "current"
|
|
14255
|
+
},
|
|
14256
|
+
CZK: {
|
|
14257
|
+
exponent: 2,
|
|
14258
|
+
status: "current"
|
|
14259
|
+
},
|
|
14260
|
+
DJF: {
|
|
14261
|
+
exponent: 0,
|
|
14262
|
+
status: "current"
|
|
14263
|
+
},
|
|
14264
|
+
DKK: {
|
|
14265
|
+
exponent: 2,
|
|
14266
|
+
status: "current"
|
|
14267
|
+
},
|
|
14268
|
+
DOP: {
|
|
14269
|
+
exponent: 2,
|
|
14270
|
+
status: "current"
|
|
14271
|
+
},
|
|
14272
|
+
DZD: {
|
|
14273
|
+
exponent: 2,
|
|
14274
|
+
status: "current"
|
|
14275
|
+
},
|
|
14276
|
+
EGP: {
|
|
14277
|
+
exponent: 2,
|
|
14278
|
+
status: "current"
|
|
14279
|
+
},
|
|
14280
|
+
ERN: {
|
|
14281
|
+
exponent: 2,
|
|
14282
|
+
status: "current"
|
|
14283
|
+
},
|
|
14284
|
+
ETB: {
|
|
14285
|
+
exponent: 2,
|
|
14286
|
+
status: "current"
|
|
14287
|
+
},
|
|
14288
|
+
EUR: {
|
|
14289
|
+
exponent: 2,
|
|
14290
|
+
status: "current"
|
|
14291
|
+
},
|
|
14292
|
+
FJD: {
|
|
14293
|
+
exponent: 2,
|
|
14294
|
+
status: "current"
|
|
14295
|
+
},
|
|
14296
|
+
FKP: {
|
|
14297
|
+
exponent: 2,
|
|
14298
|
+
status: "current"
|
|
14299
|
+
},
|
|
14300
|
+
GBP: {
|
|
14301
|
+
exponent: 2,
|
|
14302
|
+
status: "current"
|
|
14303
|
+
},
|
|
14304
|
+
GEL: {
|
|
14305
|
+
exponent: 2,
|
|
14306
|
+
status: "current"
|
|
14307
|
+
},
|
|
14308
|
+
GHS: {
|
|
14309
|
+
exponent: 2,
|
|
14310
|
+
status: "current"
|
|
14311
|
+
},
|
|
14312
|
+
GIP: {
|
|
14313
|
+
exponent: 2,
|
|
14314
|
+
status: "current"
|
|
14315
|
+
},
|
|
14316
|
+
GMD: {
|
|
14317
|
+
exponent: 2,
|
|
14318
|
+
status: "current"
|
|
14319
|
+
},
|
|
14320
|
+
GNF: {
|
|
14321
|
+
exponent: 0,
|
|
14322
|
+
status: "current"
|
|
14323
|
+
},
|
|
14324
|
+
GTQ: {
|
|
14325
|
+
exponent: 2,
|
|
14326
|
+
status: "current"
|
|
14327
|
+
},
|
|
14328
|
+
GYD: {
|
|
14329
|
+
exponent: 2,
|
|
14330
|
+
status: "current"
|
|
14331
|
+
},
|
|
14332
|
+
HKD: {
|
|
14333
|
+
exponent: 2,
|
|
14334
|
+
status: "current"
|
|
14335
|
+
},
|
|
14336
|
+
HNL: {
|
|
14337
|
+
exponent: 2,
|
|
14338
|
+
status: "current"
|
|
14339
|
+
},
|
|
14340
|
+
HTG: {
|
|
14341
|
+
exponent: 2,
|
|
14342
|
+
status: "current"
|
|
14343
|
+
},
|
|
14344
|
+
HUF: {
|
|
14345
|
+
exponent: 2,
|
|
14346
|
+
status: "current"
|
|
14347
|
+
},
|
|
14348
|
+
IDR: {
|
|
14349
|
+
exponent: 2,
|
|
14350
|
+
status: "current"
|
|
14351
|
+
},
|
|
14352
|
+
ILS: {
|
|
14353
|
+
exponent: 2,
|
|
14354
|
+
status: "current"
|
|
14355
|
+
},
|
|
14356
|
+
INR: {
|
|
14357
|
+
exponent: 2,
|
|
14358
|
+
status: "current"
|
|
14359
|
+
},
|
|
14360
|
+
IQD: {
|
|
14361
|
+
exponent: 3,
|
|
14362
|
+
status: "current"
|
|
14363
|
+
},
|
|
14364
|
+
IRR: {
|
|
14365
|
+
exponent: 2,
|
|
14366
|
+
status: "current"
|
|
14367
|
+
},
|
|
14368
|
+
ISK: {
|
|
14369
|
+
exponent: 0,
|
|
14370
|
+
status: "current"
|
|
14371
|
+
},
|
|
14372
|
+
JMD: {
|
|
14373
|
+
exponent: 2,
|
|
14374
|
+
status: "current"
|
|
14375
|
+
},
|
|
14376
|
+
JOD: {
|
|
14377
|
+
exponent: 3,
|
|
14378
|
+
status: "current"
|
|
14379
|
+
},
|
|
14380
|
+
JPY: {
|
|
14381
|
+
exponent: 0,
|
|
14382
|
+
status: "current"
|
|
14383
|
+
},
|
|
14384
|
+
KES: {
|
|
14385
|
+
exponent: 2,
|
|
14386
|
+
status: "current"
|
|
14387
|
+
},
|
|
14388
|
+
KGS: {
|
|
14389
|
+
exponent: 2,
|
|
14390
|
+
status: "current"
|
|
14391
|
+
},
|
|
14392
|
+
KHR: {
|
|
14393
|
+
exponent: 2,
|
|
14394
|
+
status: "current"
|
|
14395
|
+
},
|
|
14396
|
+
KMF: {
|
|
14397
|
+
exponent: 0,
|
|
14398
|
+
status: "current"
|
|
14399
|
+
},
|
|
14400
|
+
KPW: {
|
|
14401
|
+
exponent: 2,
|
|
14402
|
+
status: "current"
|
|
14403
|
+
},
|
|
14404
|
+
KRW: {
|
|
14405
|
+
exponent: 0,
|
|
14406
|
+
status: "current"
|
|
14407
|
+
},
|
|
14408
|
+
KWD: {
|
|
14409
|
+
exponent: 3,
|
|
14410
|
+
status: "current"
|
|
14411
|
+
},
|
|
14412
|
+
KYD: {
|
|
14413
|
+
exponent: 2,
|
|
14414
|
+
status: "current"
|
|
14415
|
+
},
|
|
14416
|
+
KZT: {
|
|
14417
|
+
exponent: 2,
|
|
14418
|
+
status: "current"
|
|
14419
|
+
},
|
|
14420
|
+
LAK: {
|
|
14421
|
+
exponent: 2,
|
|
14422
|
+
status: "current"
|
|
14423
|
+
},
|
|
14424
|
+
LBP: {
|
|
14425
|
+
exponent: 2,
|
|
14426
|
+
status: "current"
|
|
14427
|
+
},
|
|
14428
|
+
LKR: {
|
|
14429
|
+
exponent: 2,
|
|
14430
|
+
status: "current"
|
|
14431
|
+
},
|
|
14432
|
+
LRD: {
|
|
14433
|
+
exponent: 2,
|
|
14434
|
+
status: "current"
|
|
14435
|
+
},
|
|
14436
|
+
LSL: {
|
|
14437
|
+
exponent: 2,
|
|
14438
|
+
status: "current"
|
|
14439
|
+
},
|
|
14440
|
+
LYD: {
|
|
14441
|
+
exponent: 3,
|
|
14442
|
+
status: "current"
|
|
14443
|
+
},
|
|
14444
|
+
MAD: {
|
|
14445
|
+
exponent: 2,
|
|
14446
|
+
status: "current"
|
|
14447
|
+
},
|
|
14448
|
+
MDL: {
|
|
14449
|
+
exponent: 2,
|
|
14450
|
+
status: "current"
|
|
14451
|
+
},
|
|
14452
|
+
MGA: {
|
|
14453
|
+
exponent: 2,
|
|
14454
|
+
status: "current"
|
|
14455
|
+
},
|
|
14456
|
+
MKD: {
|
|
14457
|
+
exponent: 2,
|
|
14458
|
+
status: "current"
|
|
14459
|
+
},
|
|
14460
|
+
MMK: {
|
|
14461
|
+
exponent: 2,
|
|
14462
|
+
status: "current"
|
|
14463
|
+
},
|
|
14464
|
+
MNT: {
|
|
14465
|
+
exponent: 2,
|
|
14466
|
+
status: "current"
|
|
14467
|
+
},
|
|
14468
|
+
MOP: {
|
|
14469
|
+
exponent: 2,
|
|
14470
|
+
status: "current"
|
|
14471
|
+
},
|
|
14472
|
+
MRU: {
|
|
14473
|
+
exponent: 2,
|
|
14474
|
+
status: "current"
|
|
14475
|
+
},
|
|
14476
|
+
MUR: {
|
|
14477
|
+
exponent: 2,
|
|
14478
|
+
status: "current"
|
|
14479
|
+
},
|
|
14480
|
+
MVR: {
|
|
14481
|
+
exponent: 2,
|
|
14482
|
+
status: "current"
|
|
14483
|
+
},
|
|
14484
|
+
MWK: {
|
|
14485
|
+
exponent: 2,
|
|
14486
|
+
status: "current"
|
|
14487
|
+
},
|
|
14488
|
+
MXN: {
|
|
14489
|
+
exponent: 2,
|
|
14490
|
+
status: "current"
|
|
14491
|
+
},
|
|
14492
|
+
MXV: {
|
|
14493
|
+
exponent: 2,
|
|
14494
|
+
status: "current"
|
|
14495
|
+
},
|
|
14496
|
+
MYR: {
|
|
14497
|
+
exponent: 2,
|
|
14498
|
+
status: "current"
|
|
14499
|
+
},
|
|
14500
|
+
MZN: {
|
|
14501
|
+
exponent: 2,
|
|
14502
|
+
status: "current"
|
|
14503
|
+
},
|
|
14504
|
+
NAD: {
|
|
14505
|
+
exponent: 2,
|
|
14506
|
+
status: "current"
|
|
14507
|
+
},
|
|
14508
|
+
NGN: {
|
|
14509
|
+
exponent: 2,
|
|
14510
|
+
status: "current"
|
|
14511
|
+
},
|
|
14512
|
+
NIO: {
|
|
14513
|
+
exponent: 2,
|
|
14514
|
+
status: "current"
|
|
14515
|
+
},
|
|
14516
|
+
NOK: {
|
|
14517
|
+
exponent: 2,
|
|
14518
|
+
status: "current"
|
|
14519
|
+
},
|
|
14520
|
+
NPR: {
|
|
14521
|
+
exponent: 2,
|
|
14522
|
+
status: "current"
|
|
14523
|
+
},
|
|
14524
|
+
NZD: {
|
|
14525
|
+
exponent: 2,
|
|
14526
|
+
status: "current"
|
|
14527
|
+
},
|
|
14528
|
+
OMR: {
|
|
14529
|
+
exponent: 3,
|
|
14530
|
+
status: "current"
|
|
14531
|
+
},
|
|
14532
|
+
PAB: {
|
|
14533
|
+
exponent: 2,
|
|
14534
|
+
status: "current"
|
|
14535
|
+
},
|
|
14536
|
+
PEN: {
|
|
14537
|
+
exponent: 2,
|
|
14538
|
+
status: "current"
|
|
14539
|
+
},
|
|
14540
|
+
PGK: {
|
|
14541
|
+
exponent: 2,
|
|
14542
|
+
status: "current"
|
|
14543
|
+
},
|
|
14544
|
+
PHP: {
|
|
14545
|
+
exponent: 2,
|
|
14546
|
+
status: "current"
|
|
14547
|
+
},
|
|
14548
|
+
PKR: {
|
|
14549
|
+
exponent: 2,
|
|
14550
|
+
status: "current"
|
|
14551
|
+
},
|
|
14552
|
+
PLN: {
|
|
14553
|
+
exponent: 2,
|
|
14554
|
+
status: "current"
|
|
14555
|
+
},
|
|
14556
|
+
PYG: {
|
|
14557
|
+
exponent: 0,
|
|
14558
|
+
status: "current"
|
|
14559
|
+
},
|
|
14560
|
+
QAR: {
|
|
14561
|
+
exponent: 2,
|
|
14562
|
+
status: "current"
|
|
14563
|
+
},
|
|
14564
|
+
RON: {
|
|
14565
|
+
exponent: 2,
|
|
14566
|
+
status: "current"
|
|
14567
|
+
},
|
|
14568
|
+
RSD: {
|
|
14569
|
+
exponent: 2,
|
|
14570
|
+
status: "current"
|
|
14571
|
+
},
|
|
14572
|
+
RUB: {
|
|
14573
|
+
exponent: 2,
|
|
14574
|
+
status: "current"
|
|
14575
|
+
},
|
|
14576
|
+
RWF: {
|
|
14577
|
+
exponent: 0,
|
|
14578
|
+
status: "current"
|
|
14579
|
+
},
|
|
14580
|
+
SAR: {
|
|
14581
|
+
exponent: 2,
|
|
14582
|
+
status: "current"
|
|
14583
|
+
},
|
|
14584
|
+
SBD: {
|
|
14585
|
+
exponent: 2,
|
|
14586
|
+
status: "current"
|
|
14587
|
+
},
|
|
14588
|
+
SCR: {
|
|
14589
|
+
exponent: 2,
|
|
14590
|
+
status: "current"
|
|
14591
|
+
},
|
|
14592
|
+
SDG: {
|
|
14593
|
+
exponent: 2,
|
|
14594
|
+
status: "current"
|
|
14595
|
+
},
|
|
14596
|
+
SEK: {
|
|
14597
|
+
exponent: 2,
|
|
14598
|
+
status: "current"
|
|
14599
|
+
},
|
|
14600
|
+
SGD: {
|
|
14601
|
+
exponent: 2,
|
|
14602
|
+
status: "current"
|
|
14603
|
+
},
|
|
14604
|
+
SHP: {
|
|
14605
|
+
exponent: 2,
|
|
14606
|
+
status: "current"
|
|
14607
|
+
},
|
|
14608
|
+
SLE: {
|
|
14609
|
+
exponent: 2,
|
|
14610
|
+
status: "current"
|
|
14611
|
+
},
|
|
14612
|
+
SOS: {
|
|
14613
|
+
exponent: 2,
|
|
14614
|
+
status: "current"
|
|
14615
|
+
},
|
|
14616
|
+
SRD: {
|
|
14617
|
+
exponent: 2,
|
|
14618
|
+
status: "current"
|
|
14619
|
+
},
|
|
14620
|
+
SSP: {
|
|
14621
|
+
exponent: 2,
|
|
14622
|
+
status: "current"
|
|
14623
|
+
},
|
|
14624
|
+
STN: {
|
|
14625
|
+
exponent: 2,
|
|
14626
|
+
status: "current"
|
|
14627
|
+
},
|
|
14628
|
+
SVC: {
|
|
14629
|
+
exponent: 2,
|
|
14630
|
+
status: "current"
|
|
14631
|
+
},
|
|
14632
|
+
SYP: {
|
|
14633
|
+
exponent: 2,
|
|
14634
|
+
status: "current"
|
|
14635
|
+
},
|
|
14636
|
+
SZL: {
|
|
14637
|
+
exponent: 2,
|
|
14638
|
+
status: "current"
|
|
14639
|
+
},
|
|
14640
|
+
THB: {
|
|
14641
|
+
exponent: 2,
|
|
14642
|
+
status: "current"
|
|
14643
|
+
},
|
|
14644
|
+
TJS: {
|
|
14645
|
+
exponent: 2,
|
|
14646
|
+
status: "current"
|
|
14647
|
+
},
|
|
14648
|
+
TMT: {
|
|
14649
|
+
exponent: 2,
|
|
14650
|
+
status: "current"
|
|
14651
|
+
},
|
|
14652
|
+
TND: {
|
|
14653
|
+
exponent: 3,
|
|
14654
|
+
status: "current"
|
|
14655
|
+
},
|
|
14656
|
+
TOP: {
|
|
14657
|
+
exponent: 2,
|
|
14658
|
+
status: "current"
|
|
14659
|
+
},
|
|
14660
|
+
TRY: {
|
|
14661
|
+
exponent: 2,
|
|
14662
|
+
status: "current"
|
|
14663
|
+
},
|
|
14664
|
+
TTD: {
|
|
14665
|
+
exponent: 2,
|
|
14666
|
+
status: "current"
|
|
14667
|
+
},
|
|
14668
|
+
TWD: {
|
|
14669
|
+
exponent: 2,
|
|
14670
|
+
status: "current"
|
|
14671
|
+
},
|
|
14672
|
+
TZS: {
|
|
14673
|
+
exponent: 2,
|
|
14674
|
+
status: "current"
|
|
14675
|
+
},
|
|
14676
|
+
UAH: {
|
|
14677
|
+
exponent: 2,
|
|
14678
|
+
status: "current"
|
|
14679
|
+
},
|
|
14680
|
+
UGX: {
|
|
14681
|
+
exponent: 0,
|
|
14682
|
+
status: "current"
|
|
14683
|
+
},
|
|
14684
|
+
USD: {
|
|
14685
|
+
exponent: 2,
|
|
14686
|
+
status: "current"
|
|
14687
|
+
},
|
|
14688
|
+
USN: {
|
|
14689
|
+
exponent: 2,
|
|
14690
|
+
status: "current"
|
|
14691
|
+
},
|
|
14692
|
+
UYI: {
|
|
14693
|
+
exponent: 0,
|
|
14694
|
+
status: "current"
|
|
14695
|
+
},
|
|
14696
|
+
UYU: {
|
|
14697
|
+
exponent: 2,
|
|
14698
|
+
status: "current"
|
|
14699
|
+
},
|
|
14700
|
+
UYW: {
|
|
14701
|
+
exponent: 4,
|
|
14702
|
+
status: "current"
|
|
14703
|
+
},
|
|
14704
|
+
UZS: {
|
|
14705
|
+
exponent: 2,
|
|
14706
|
+
status: "current"
|
|
14707
|
+
},
|
|
14708
|
+
VED: {
|
|
14709
|
+
exponent: 2,
|
|
14710
|
+
status: "current"
|
|
14711
|
+
},
|
|
14712
|
+
VES: {
|
|
14713
|
+
exponent: 2,
|
|
14714
|
+
status: "current"
|
|
14715
|
+
},
|
|
14716
|
+
VND: {
|
|
14717
|
+
exponent: 0,
|
|
14718
|
+
status: "current"
|
|
14719
|
+
},
|
|
14720
|
+
VUV: {
|
|
14721
|
+
exponent: 0,
|
|
14722
|
+
status: "current"
|
|
14723
|
+
},
|
|
14724
|
+
WST: {
|
|
14725
|
+
exponent: 2,
|
|
14726
|
+
status: "current"
|
|
14727
|
+
},
|
|
14728
|
+
XAD: {
|
|
14729
|
+
exponent: 2,
|
|
14730
|
+
status: "current"
|
|
14731
|
+
},
|
|
14732
|
+
XAF: {
|
|
14733
|
+
exponent: 0,
|
|
14734
|
+
status: "current"
|
|
14735
|
+
},
|
|
14736
|
+
XCD: {
|
|
14737
|
+
exponent: 2,
|
|
14738
|
+
status: "current"
|
|
14739
|
+
},
|
|
14740
|
+
XCG: {
|
|
14741
|
+
exponent: 2,
|
|
14742
|
+
status: "current"
|
|
14743
|
+
},
|
|
14744
|
+
XOF: {
|
|
14745
|
+
exponent: 0,
|
|
14746
|
+
status: "current"
|
|
14747
|
+
},
|
|
14748
|
+
XPF: {
|
|
14749
|
+
exponent: 0,
|
|
14750
|
+
status: "current"
|
|
14751
|
+
},
|
|
14752
|
+
YER: {
|
|
14753
|
+
exponent: 2,
|
|
14754
|
+
status: "current"
|
|
14755
|
+
},
|
|
14756
|
+
ZAR: {
|
|
14757
|
+
exponent: 2,
|
|
14758
|
+
status: "current"
|
|
14759
|
+
},
|
|
14760
|
+
ZMW: {
|
|
14761
|
+
exponent: 2,
|
|
14762
|
+
status: "current"
|
|
14763
|
+
},
|
|
14764
|
+
ZWG: {
|
|
14765
|
+
exponent: 2,
|
|
14766
|
+
status: "current"
|
|
14767
|
+
}
|
|
14768
|
+
}
|
|
14769
|
+
};
|
|
14770
|
+
|
|
14771
|
+
// ../shared/src/money.ts
|
|
14772
|
+
var entries = iso_4217_exponents_default.currencies;
|
|
14008
14773
|
// src/index.ts
|
|
14774
|
+
function validatedApiBaseUrl(value) {
|
|
14775
|
+
const url = new URL(value);
|
|
14776
|
+
const isLoopback = url.hostname === "localhost" || url.hostname === "127.0.0.1" || url.hostname === "[::1]";
|
|
14777
|
+
if (url.protocol !== "https:" && !(url.protocol === "http:" && isLoopback)) {
|
|
14778
|
+
throw new Error("protocol must be HTTPS, except HTTP is allowed for localhost development");
|
|
14779
|
+
}
|
|
14780
|
+
if (url.username || url.password) {
|
|
14781
|
+
throw new Error("credentials are not allowed in ATTRKIT_API_URL");
|
|
14782
|
+
}
|
|
14783
|
+
if (value.includes("?")) {
|
|
14784
|
+
throw new Error("query strings are not allowed in ATTRKIT_API_URL");
|
|
14785
|
+
}
|
|
14786
|
+
if (value.includes("#")) {
|
|
14787
|
+
throw new Error("fragments are not allowed in ATTRKIT_API_URL");
|
|
14788
|
+
}
|
|
14789
|
+
return url;
|
|
14790
|
+
}
|
|
14009
14791
|
function readConfig(env = process.env) {
|
|
14010
14792
|
const apiKey = env.ATTRKIT_API_KEY;
|
|
14011
14793
|
if (!apiKey) {
|
|
14012
|
-
throw new Error("ATTRKIT_API_KEY is required (create one in
|
|
14794
|
+
throw new Error("ATTRKIT_API_KEY is required (create one in AttriKit → Settings → API keys).");
|
|
14013
14795
|
}
|
|
14014
14796
|
const apiUrl = (env.ATTRKIT_API_URL ?? "https://attrikit.io").replace(/\/+$/, "");
|
|
14797
|
+
validatedApiBaseUrl(apiUrl);
|
|
14015
14798
|
return { apiKey, apiUrl };
|
|
14016
14799
|
}
|
|
14800
|
+
var STRING_TAG = "[object String]";
|
|
14801
|
+
var NUMBER_TAG = "[object Number]";
|
|
14802
|
+
var BOOLEAN_TAG = "[object Boolean]";
|
|
14803
|
+
var OBJECT_TAG = "[object Object]";
|
|
14804
|
+
function hasRuntimeTag(value, tag) {
|
|
14805
|
+
return Object.prototype.toString.call(value) === tag;
|
|
14806
|
+
}
|
|
14807
|
+
function isString(value) {
|
|
14808
|
+
return hasRuntimeTag(value, STRING_TAG);
|
|
14809
|
+
}
|
|
14810
|
+
function isNumber(value) {
|
|
14811
|
+
return hasRuntimeTag(value, NUMBER_TAG);
|
|
14812
|
+
}
|
|
14813
|
+
function isBoolean(value) {
|
|
14814
|
+
return hasRuntimeTag(value, BOOLEAN_TAG);
|
|
14815
|
+
}
|
|
14816
|
+
function isObject(value) {
|
|
14817
|
+
return hasRuntimeTag(value, OBJECT_TAG);
|
|
14818
|
+
}
|
|
14819
|
+
function isRequestObject(value) {
|
|
14820
|
+
return Object.prototype.toString.call(value) === OBJECT_TAG;
|
|
14821
|
+
}
|
|
14822
|
+
function isJsonObject(value) {
|
|
14823
|
+
return isObject(value) && Object.values(value).every(isJsonValue);
|
|
14824
|
+
}
|
|
14825
|
+
function isJsonValue(value) {
|
|
14826
|
+
return value === null || isString(value) || isNumber(value) || isBoolean(value) || Array.isArray(value) && value.every(isJsonValue) || isJsonObject(value);
|
|
14827
|
+
}
|
|
14828
|
+
function isApiErrorEnvelope(value) {
|
|
14829
|
+
return isJsonObject(value) && (value.error === undefined || isJsonObject(value.error));
|
|
14830
|
+
}
|
|
14017
14831
|
var REQUEST_TIMEOUT_MS = 15000;
|
|
14018
|
-
|
|
14019
|
-
|
|
14020
|
-
|
|
14021
|
-
|
|
14832
|
+
var LONG_REQUEST_TIMEOUT_MS = 120000;
|
|
14833
|
+
function requestTimeoutMs(path, body) {
|
|
14834
|
+
if (path === "/api/v1/export")
|
|
14835
|
+
return LONG_REQUEST_TIMEOUT_MS;
|
|
14836
|
+
if (path !== "/api/v1/spend" || !isRequestObject(body))
|
|
14837
|
+
return REQUEST_TIMEOUT_MS;
|
|
14838
|
+
return body.kind === "network_pull" || Array.isArray(body.rows) ? LONG_REQUEST_TIMEOUT_MS : REQUEST_TIMEOUT_MS;
|
|
14839
|
+
}
|
|
14840
|
+
function hasUnsafePathCharacter(path) {
|
|
14841
|
+
return Array.from(path).some((character) => {
|
|
14842
|
+
const code = character.charCodeAt(0);
|
|
14843
|
+
return code <= 32 || code === 127;
|
|
14844
|
+
});
|
|
14845
|
+
}
|
|
14846
|
+
function invalidResult(code, message) {
|
|
14847
|
+
return {
|
|
14848
|
+
ok: false,
|
|
14849
|
+
status: 0,
|
|
14850
|
+
json: { error: { code, message } }
|
|
14851
|
+
};
|
|
14852
|
+
}
|
|
14853
|
+
function buildRequestUrl(baseUrl2, path, query) {
|
|
14854
|
+
if (!path.startsWith("/") || hasUnsafePathCharacter(path)) {
|
|
14855
|
+
throw new Error("path must be an absolute URL path without whitespace or control characters");
|
|
14856
|
+
}
|
|
14857
|
+
const url = new URL(`${baseUrl2.href.replace(/\/+$/, "")}${path}`);
|
|
14858
|
+
for (const [key, value] of Object.entries(query ?? {})) {
|
|
14022
14859
|
if (value !== undefined && value !== "")
|
|
14023
14860
|
url.searchParams.set(key, value);
|
|
14024
14861
|
}
|
|
14862
|
+
return url;
|
|
14863
|
+
}
|
|
14864
|
+
function parseJsonBody(text) {
|
|
14865
|
+
if (!text)
|
|
14866
|
+
return null;
|
|
14867
|
+
try {
|
|
14868
|
+
const parsed = JSON.parse(text);
|
|
14869
|
+
return isJsonValue(parsed) ? parsed : text;
|
|
14870
|
+
} catch {
|
|
14871
|
+
return text;
|
|
14872
|
+
}
|
|
14873
|
+
}
|
|
14874
|
+
function requestHeaders(config, idempotencyKey) {
|
|
14875
|
+
const headers = {
|
|
14876
|
+
authorization: `AttriKit-Management ${config.apiKey}`,
|
|
14877
|
+
"content-type": "application/json"
|
|
14878
|
+
};
|
|
14879
|
+
if (idempotencyKey)
|
|
14880
|
+
headers["idempotency-key"] = idempotencyKey;
|
|
14881
|
+
return headers;
|
|
14882
|
+
}
|
|
14883
|
+
function timeoutError(timeoutMs, method, idempotencyKey) {
|
|
14884
|
+
const readOrRemoveOnly = method === "GET" || method === "DELETE";
|
|
14885
|
+
return {
|
|
14886
|
+
code: "timeout",
|
|
14887
|
+
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}.`
|
|
14888
|
+
};
|
|
14889
|
+
}
|
|
14890
|
+
function networkError(cause, method, idempotencyKey) {
|
|
14891
|
+
const message = cause instanceof Error ? cause.message : String(cause);
|
|
14892
|
+
return {
|
|
14893
|
+
code: "network_error",
|
|
14894
|
+
message: method === "GET" || method === "DELETE" ? message : `${message}. The request may still have been applied on the server; retry with idempotency_key ${idempotencyKey}.`
|
|
14895
|
+
};
|
|
14896
|
+
}
|
|
14897
|
+
function resolveIdempotencyKey(idempotencyKey, method) {
|
|
14898
|
+
const mutating = method === "POST" || method === "PATCH" || method === "PUT";
|
|
14899
|
+
return idempotencyKey === undefined && mutating ? randomUUID() : idempotencyKey;
|
|
14900
|
+
}
|
|
14901
|
+
function apiResultFromResponse(response, text, expectedResponse) {
|
|
14902
|
+
const contentType = response.headers.get("content-type") ?? undefined;
|
|
14903
|
+
const retryAfter = response.headers.get("retry-after") ?? undefined;
|
|
14904
|
+
const nextCursor = response.headers.get("attrikit-next-cursor") ?? undefined;
|
|
14905
|
+
const apiResult = {
|
|
14906
|
+
ok: response.ok,
|
|
14907
|
+
status: response.status,
|
|
14908
|
+
json: parseJsonBody(text),
|
|
14909
|
+
rawBody: text,
|
|
14910
|
+
expectedResponse: expectedResponse ?? "json"
|
|
14911
|
+
};
|
|
14912
|
+
if (contentType)
|
|
14913
|
+
apiResult.contentType = contentType;
|
|
14914
|
+
if (retryAfter)
|
|
14915
|
+
apiResult.retryAfter = retryAfter;
|
|
14916
|
+
if (nextCursor)
|
|
14917
|
+
apiResult.nextCursor = nextCursor;
|
|
14918
|
+
return apiResult;
|
|
14919
|
+
}
|
|
14920
|
+
async function callApi(config, path, options = {}) {
|
|
14921
|
+
const doFetch = options.fetchImpl ?? fetch;
|
|
14922
|
+
let baseUrl2;
|
|
14923
|
+
try {
|
|
14924
|
+
baseUrl2 = validatedApiBaseUrl(config.apiUrl);
|
|
14925
|
+
} catch (error) {
|
|
14926
|
+
return invalidResult("invalid_api_url", `ATTRKIT_API_URL is invalid: ${error instanceof Error ? error.message : String(error)}`);
|
|
14927
|
+
}
|
|
14928
|
+
let url;
|
|
14929
|
+
try {
|
|
14930
|
+
url = buildRequestUrl(baseUrl2, path, options.query);
|
|
14931
|
+
} catch (error) {
|
|
14932
|
+
return invalidResult("invalid_path", `AttriKit API path is invalid: ${error instanceof Error ? error.message : String(error)}`);
|
|
14933
|
+
}
|
|
14025
14934
|
const controller = new AbortController;
|
|
14026
|
-
const
|
|
14935
|
+
const timeoutMs = requestTimeoutMs(path, options.body);
|
|
14936
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
14937
|
+
const method = options.method ?? "GET";
|
|
14938
|
+
const semanticMethod = options.readsOnly ? "GET" : method;
|
|
14939
|
+
const idempotencyKey = resolveIdempotencyKey(options.idempotencyKey, semanticMethod);
|
|
14027
14940
|
try {
|
|
14028
|
-
const
|
|
14029
|
-
method
|
|
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) } : {},
|
|
14941
|
+
const request = {
|
|
14942
|
+
method,
|
|
14943
|
+
headers: requestHeaders(config, idempotencyKey),
|
|
14036
14944
|
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
14945
|
};
|
|
14946
|
+
if (options.body !== undefined)
|
|
14947
|
+
request.body = JSON.stringify(options.body);
|
|
14948
|
+
const response = await doFetch(url.toString(), request);
|
|
14949
|
+
const text = await response.text();
|
|
14950
|
+
return apiResultFromResponse(response, text, options.expectedResponse);
|
|
14058
14951
|
} catch (error) {
|
|
14059
14952
|
return {
|
|
14060
14953
|
ok: false,
|
|
14061
14954
|
status: 0,
|
|
14062
14955
|
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) }
|
|
14956
|
+
error: error instanceof Error && error.name === "AbortError" ? timeoutError(timeoutMs, semanticMethod, idempotencyKey) : networkError(error, semanticMethod, idempotencyKey)
|
|
14067
14957
|
}
|
|
14068
14958
|
};
|
|
14069
14959
|
} finally {
|
|
14070
14960
|
clearTimeout(timer);
|
|
14071
14961
|
}
|
|
14072
14962
|
}
|
|
14073
|
-
var redact = (text) => text.replace(
|
|
14963
|
+
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]");
|
|
14964
|
+
var OPAQUE_CURSOR_PATTERN = /^[A-Za-z0-9_\-=.]{1,4096}$/;
|
|
14965
|
+
function withNextCursor(tool, nextCursor) {
|
|
14966
|
+
if (nextCursor === undefined || !OPAQUE_CURSOR_PATTERN.test(nextCursor))
|
|
14967
|
+
return tool;
|
|
14968
|
+
return {
|
|
14969
|
+
...tool,
|
|
14970
|
+
content: [...tool.content, { type: "text", text: `next_cursor: ${nextCursor}` }],
|
|
14971
|
+
structuredContent: { ...tool.structuredContent, next_cursor: nextCursor }
|
|
14972
|
+
};
|
|
14973
|
+
}
|
|
14974
|
+
function ndjsonToolResult(result) {
|
|
14975
|
+
const body = result.rawBody ?? "";
|
|
14976
|
+
const contentType = (result.contentType ?? "").toLowerCase();
|
|
14977
|
+
const firstLine = body.split(`
|
|
14978
|
+
`, 1)[0]?.trim() ?? "";
|
|
14979
|
+
const looksLikeRecord = (() => {
|
|
14980
|
+
try {
|
|
14981
|
+
const parsed = JSON.parse(firstLine);
|
|
14982
|
+
return isJsonObject(parsed) && (parsed.type === "install" || parsed.type === "event") && isString(parsed.id);
|
|
14983
|
+
} catch {
|
|
14984
|
+
return false;
|
|
14985
|
+
}
|
|
14986
|
+
})();
|
|
14987
|
+
const isEmptyPage = body.trim() === "" && contentType.includes("ndjson");
|
|
14988
|
+
if (!isEmptyPage && (contentType.includes("text/html") || !looksLikeRecord)) {
|
|
14989
|
+
return {
|
|
14990
|
+
content: [{
|
|
14991
|
+
type: "text",
|
|
14992
|
+
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:
|
|
14993
|
+
${redact(body).slice(0, 600)}`
|
|
14994
|
+
}],
|
|
14995
|
+
isError: true
|
|
14996
|
+
};
|
|
14997
|
+
}
|
|
14998
|
+
return withNextCursor({ content: [{ type: "text", text: body }], structuredContent: { value: body } }, result.nextCursor);
|
|
14999
|
+
}
|
|
14074
15000
|
function toToolResult(result) {
|
|
14075
15001
|
if (!result.ok) {
|
|
14076
|
-
const envelope = result.json;
|
|
14077
|
-
const
|
|
15002
|
+
const envelope = isApiErrorEnvelope(result.json) ? result.json : null;
|
|
15003
|
+
const rawCode = envelope?.error?.code;
|
|
15004
|
+
const code = isString(rawCode) && rawCode !== "" ? redact(rawCode).slice(0, 120) : `http_${result.status}`;
|
|
14078
15005
|
const rawMessage = envelope?.error?.message;
|
|
14079
|
-
const
|
|
14080
|
-
const
|
|
14081
|
-
const
|
|
15006
|
+
const hasMessage = isString(rawMessage) && rawMessage.trim() !== "";
|
|
15007
|
+
const message = hasMessage ? `: ${redact(rawMessage).slice(0, 400)}` : "";
|
|
15008
|
+
const retryAfterValue = result.retryAfter === undefined ? "" : redact(result.retryAfter).trim().slice(0, 120);
|
|
15009
|
+
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);
|
|
15010
|
+
const retryAfter = result.status === 429 && validRetryAfter ? ` (Retry-After: ${retryAfterValue})` : "";
|
|
15011
|
+
const excerpt = !hasMessage && result.rawBody ? ` — body: ${redact(result.rawBody.replace(/\s+/g, " ").trim()).slice(0, 300)}` : "";
|
|
14082
15012
|
const hint = result.status === 401 ? " Check ATTRKIT_API_KEY: this is the same error for a missing, wrong, revoked or expired key." : "";
|
|
14083
15013
|
return {
|
|
14084
15014
|
content: [{
|
|
14085
15015
|
type: "text",
|
|
14086
|
-
text: `
|
|
15016
|
+
text: `AttriKit API error (${result.status || "network"}): ${code}${message}${retryAfter}${excerpt}${hint}`
|
|
14087
15017
|
}],
|
|
14088
15018
|
isError: true
|
|
14089
15019
|
};
|
|
14090
15020
|
}
|
|
14091
15021
|
const expectedResponse = result.expectedResponse ?? "json";
|
|
15022
|
+
if (expectedResponse === "ndjson")
|
|
15023
|
+
return ndjsonToolResult(result);
|
|
14092
15024
|
const isEmptySuccess = result.status === 204 && (result.rawBody === undefined || result.rawBody === "");
|
|
14093
|
-
const isJsonEnvelope =
|
|
15025
|
+
const isJsonEnvelope = isJsonObject(result.json) && Object.hasOwn(result.json, "data");
|
|
14094
15026
|
const contentType = (result.contentType ?? "").toLowerCase();
|
|
14095
15027
|
const looksLikeCsv = (() => {
|
|
14096
|
-
if (
|
|
15028
|
+
if (!isString(result.json))
|
|
14097
15029
|
return false;
|
|
14098
15030
|
const firstLine = result.json.split(`
|
|
14099
15031
|
`, 1)[0]?.trim() ?? "";
|
|
14100
15032
|
if (!firstLine || firstLine.startsWith("<"))
|
|
14101
15033
|
return false;
|
|
14102
15034
|
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));
|
|
15035
|
+
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
15036
|
})();
|
|
14105
|
-
const isExpectedText = expectedResponse === "text" &&
|
|
15037
|
+
const isExpectedText = expectedResponse === "text" && isString(result.json) && !contentType.includes("text/html") && looksLikeCsv;
|
|
14106
15038
|
if (!isEmptySuccess && !isJsonEnvelope && !isExpectedText) {
|
|
14107
15039
|
const reportedContentType = result.contentType ?? "unknown content type";
|
|
14108
|
-
const received = result.json === null ? "an empty body" : Array.isArray(result.json) ? "a JSON array" :
|
|
14109
|
-
const body = redact(result.rawBody ?? (
|
|
15040
|
+
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`;
|
|
15041
|
+
const body = redact(result.rawBody ?? (isString(result.json) ? result.json : JSON.stringify(result.json, null, 2))).slice(0, 600);
|
|
15042
|
+
const expectation = expectedResponse === "text" ? "expected a CSV export" : "expected a JSON data envelope";
|
|
14110
15043
|
return {
|
|
14111
15044
|
content: [{
|
|
14112
15045
|
type: "text",
|
|
14113
|
-
text: `
|
|
15046
|
+
text: `AttriKit API error (${result.status}): invalid_response: ${expectation} but received ${received} with Content-Type ${reportedContentType}. Body:
|
|
14114
15047
|
${body}`
|
|
14115
15048
|
}],
|
|
14116
15049
|
isError: true
|
|
14117
15050
|
};
|
|
14118
15051
|
}
|
|
14119
|
-
const structured = result.json
|
|
14120
|
-
return {
|
|
15052
|
+
const structured = isJsonObject(result.json) ? result.json : { value: result.json };
|
|
15053
|
+
return withNextCursor({
|
|
14121
15054
|
content: [{ type: "text", text: JSON.stringify(result.json, null, 2) }],
|
|
14122
15055
|
structuredContent: structured
|
|
14123
|
-
};
|
|
15056
|
+
}, result.nextCursor);
|
|
14124
15057
|
}
|
|
14125
|
-
var SERVER_VERSION = "
|
|
14126
|
-
var periodSchema =
|
|
14127
|
-
var appIdSchema =
|
|
14128
|
-
var requiredAppIdSchema =
|
|
14129
|
-
var
|
|
14130
|
-
var
|
|
14131
|
-
var
|
|
14132
|
-
var
|
|
14133
|
-
var
|
|
14134
|
-
|
|
14135
|
-
|
|
14136
|
-
|
|
14137
|
-
|
|
14138
|
-
|
|
14139
|
-
|
|
14140
|
-
|
|
14141
|
-
|
|
14142
|
-
|
|
14143
|
-
|
|
14144
|
-
|
|
14145
|
-
|
|
14146
|
-
|
|
14147
|
-
|
|
14148
|
-
|
|
14149
|
-
|
|
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
|
-
]) {
|
|
15058
|
+
var SERVER_VERSION = "1.1.0";
|
|
15059
|
+
var periodSchema = z5.enum(["7d", "30d", "90d", "all"]).describe("Reporting window. Omit to use the endpoint's documented default.").optional();
|
|
15060
|
+
var appIdSchema = z5.string().describe("Optional app id to scope the result to one app.").optional();
|
|
15061
|
+
var requiredAppIdSchema = z5.string().min(1).describe("The AttriKit app id.");
|
|
15062
|
+
var encodeOpaquePathSegment = (value) => encodeURIComponent(value).replaceAll(".", "%252E");
|
|
15063
|
+
var idempotencyKeySchema = z5.string().max(255).describe("Stable retry key for this exact mutation.").optional();
|
|
15064
|
+
var ownersSchema = z5.record(z5.string(), z5.enum(["attrkit", "revenuecat", "customer"]));
|
|
15065
|
+
var cursorSchema = z5.string().optional().describe("Pass the next_cursor from the previous response to fetch the following page. Omit for the first page.");
|
|
15066
|
+
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.");
|
|
15067
|
+
var calendarDaySchema = z5.string().regex(/^\d{4}-\d{2}-\d{2}$/, "Use a UTC calendar day, YYYY-MM-DD.");
|
|
15068
|
+
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.");
|
|
15069
|
+
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.");
|
|
15070
|
+
function validateMatchingSpendAliases(pairs, context) {
|
|
15071
|
+
for (const [preferred, alias, preferredValue, aliasValue] of pairs) {
|
|
15072
|
+
if (preferredValue !== undefined && aliasValue !== undefined && preferredValue !== aliasValue) {
|
|
15073
|
+
context.addIssue({
|
|
15074
|
+
code: "custom",
|
|
15075
|
+
path: [alias],
|
|
15076
|
+
message: `${preferred} and ${alias} must match when both are supplied`
|
|
15077
|
+
});
|
|
15078
|
+
}
|
|
15079
|
+
}
|
|
15080
|
+
}
|
|
15081
|
+
function validateRequiredSpendValues(values, context) {
|
|
15082
|
+
for (const [path, value] of values) {
|
|
14156
15083
|
if (value === undefined) {
|
|
14157
15084
|
context.addIssue({
|
|
14158
15085
|
code: "custom",
|
|
@@ -14161,6 +15088,49 @@ var spendRowSchema = z4.object({
|
|
|
14161
15088
|
});
|
|
14162
15089
|
}
|
|
14163
15090
|
}
|
|
15091
|
+
}
|
|
15092
|
+
function validatePairedAdIds(adsetId, adId, context) {
|
|
15093
|
+
if (adsetId == null !== (adId == null)) {
|
|
15094
|
+
context.addIssue({
|
|
15095
|
+
code: "custom",
|
|
15096
|
+
path: adsetId == null ? ["adset_id"] : ["ad_id"],
|
|
15097
|
+
message: "adset_id and ad_id must be supplied together"
|
|
15098
|
+
});
|
|
15099
|
+
}
|
|
15100
|
+
}
|
|
15101
|
+
var spendRowSchema = z5.object({
|
|
15102
|
+
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."),
|
|
15103
|
+
campaignId: z5.string().optional().describe("Accepted camelCase alias for campaign_id."),
|
|
15104
|
+
adset_id: z5.string().optional().nullable().describe("Ad set id. Must be sent together with ad_id; either one alone is rejected."),
|
|
15105
|
+
adsetId: z5.string().optional().nullable(),
|
|
15106
|
+
ad_id: z5.string().optional().nullable(),
|
|
15107
|
+
adId: z5.string().optional().nullable(),
|
|
15108
|
+
adset_name: z5.string().optional().nullable(),
|
|
15109
|
+
adsetName: z5.string().optional().nullable(),
|
|
15110
|
+
ad_name: z5.string().optional().nullable(),
|
|
15111
|
+
adName: z5.string().optional().nullable(),
|
|
15112
|
+
metric_date: z5.string().regex(/^\d{4}-\d{2}-\d{2}$/).optional().describe("Required metric date. Preferred snake_case spelling."),
|
|
15113
|
+
metricDate: z5.string().regex(/^\d{4}-\d{2}-\d{2}$/).optional().describe("Accepted camelCase alias for metric_date."),
|
|
15114
|
+
clicks: z5.number().int().nonnegative().optional(),
|
|
15115
|
+
impressions: z5.number().int().nonnegative().optional(),
|
|
15116
|
+
spend_minor_units: z5.number().int().nonnegative().optional().describe("Required spend amount. Preferred snake_case spelling."),
|
|
15117
|
+
spendMinorUnits: z5.number().int().nonnegative().optional().describe("Accepted camelCase alias for spend_minor_units.")
|
|
15118
|
+
}).superRefine((row, context) => {
|
|
15119
|
+
validateMatchingSpendAliases([
|
|
15120
|
+
["campaign_id", "campaignId", row.campaign_id, row.campaignId],
|
|
15121
|
+
["adset_id", "adsetId", row.adset_id, row.adsetId],
|
|
15122
|
+
["ad_id", "adId", row.ad_id, row.adId],
|
|
15123
|
+
["adset_name", "adsetName", row.adset_name, row.adsetName],
|
|
15124
|
+
["ad_name", "adName", row.ad_name, row.adName],
|
|
15125
|
+
["metric_date", "metricDate", row.metric_date, row.metricDate],
|
|
15126
|
+
["spend_minor_units", "spendMinorUnits", row.spend_minor_units, row.spendMinorUnits]
|
|
15127
|
+
], context);
|
|
15128
|
+
validateRequiredSpendValues([
|
|
15129
|
+
["campaign_id", row.campaign_id ?? row.campaignId],
|
|
15130
|
+
["metric_date", row.metric_date ?? row.metricDate],
|
|
15131
|
+
["spend_minor_units", row.spend_minor_units ?? row.spendMinorUnits]
|
|
15132
|
+
], context);
|
|
15133
|
+
validatePairedAdIds(row.adset_id ?? row.adsetId, row.ad_id ?? row.adId, context);
|
|
14164
15134
|
});
|
|
14165
15135
|
function normalizeSpendRow(row) {
|
|
14166
15136
|
return {
|
|
@@ -14191,12 +15161,12 @@ function createServer(config, fetchImpl) {
|
|
|
14191
15161
|
inputSchema: {}
|
|
14192
15162
|
}, async () => toToolResult(await call("/api/v1/workspace/usage-cap")));
|
|
14193
15163
|
server.registerTool("set_usage_cap", {
|
|
14194
|
-
annotations: { readOnlyHint: false, destructiveHint:
|
|
15164
|
+
annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: true },
|
|
14195
15165
|
title: "Set the workspace usage cap",
|
|
14196
15166
|
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
15167
|
inputSchema: {
|
|
14198
|
-
monthly_qualified_install_cap:
|
|
14199
|
-
alert_email:
|
|
15168
|
+
monthly_qualified_install_cap: z5.number().int().min(1e4).max(Number.MAX_SAFE_INTEGER).nullable(),
|
|
15169
|
+
alert_email: z5.string().trim().email().max(320).nullable(),
|
|
14200
15170
|
idempotency_key: idempotencyKeySchema
|
|
14201
15171
|
}
|
|
14202
15172
|
}, async ({
|
|
@@ -14208,7 +15178,7 @@ function createServer(config, fetchImpl) {
|
|
|
14208
15178
|
return {
|
|
14209
15179
|
content: [{
|
|
14210
15180
|
type: "text",
|
|
14211
|
-
text: "
|
|
15181
|
+
text: "AttriKit usage-cap validation error: set both cap and alert email, or pass null for both to clear them."
|
|
14212
15182
|
}],
|
|
14213
15183
|
isError: true
|
|
14214
15184
|
};
|
|
@@ -14225,22 +15195,27 @@ function createServer(config, fetchImpl) {
|
|
|
14225
15195
|
server.registerTool("list_apps", {
|
|
14226
15196
|
annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true },
|
|
14227
15197
|
title: "List apps",
|
|
14228
|
-
description: "List the iOS and Android apps in your
|
|
15198
|
+
description: "List the iOS and Android apps in your AttriKit workspace (id, name, platform, bundle id, store id, status).",
|
|
14229
15199
|
inputSchema: { cursor: cursorSchema, limit: listLimitSchema }
|
|
14230
|
-
}, async ({ cursor, limit }) =>
|
|
14231
|
-
query
|
|
14232
|
-
|
|
15200
|
+
}, async ({ cursor, limit }) => {
|
|
15201
|
+
const query = {};
|
|
15202
|
+
if (cursor)
|
|
15203
|
+
query.cursor = cursor;
|
|
15204
|
+
if (limit !== undefined)
|
|
15205
|
+
query.limit = String(limit);
|
|
15206
|
+
return toToolResult(await call("/api/v1/apps", { query }));
|
|
15207
|
+
});
|
|
14233
15208
|
server.registerTool("create_app", {
|
|
14234
15209
|
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true },
|
|
14235
15210
|
title: "Create an app",
|
|
14236
15211
|
description: "Create an iOS or Android app without the web UI. Returns the publishable key and a one-time app-scoped management key.",
|
|
14237
15212
|
inputSchema: {
|
|
14238
|
-
name:
|
|
14239
|
-
bundle_id:
|
|
14240
|
-
platform:
|
|
14241
|
-
store_url:
|
|
14242
|
-
timezone:
|
|
14243
|
-
device_matching_mode:
|
|
15213
|
+
name: z5.string().min(1).max(80),
|
|
15214
|
+
bundle_id: z5.string().min(3).max(255),
|
|
15215
|
+
platform: z5.enum(["iOS", "Android"]),
|
|
15216
|
+
store_url: z5.string().url(),
|
|
15217
|
+
timezone: z5.string().max(64).optional(),
|
|
15218
|
+
device_matching_mode: z5.enum(["on", "strict"]).optional(),
|
|
14244
15219
|
idempotency_key: idempotencyKeySchema
|
|
14245
15220
|
}
|
|
14246
15221
|
}, async ({
|
|
@@ -14251,36 +15226,36 @@ function createServer(config, fetchImpl) {
|
|
|
14251
15226
|
timezone,
|
|
14252
15227
|
device_matching_mode,
|
|
14253
15228
|
idempotency_key
|
|
14254
|
-
}) =>
|
|
14255
|
-
|
|
14256
|
-
|
|
14257
|
-
|
|
14258
|
-
|
|
14259
|
-
|
|
14260
|
-
|
|
14261
|
-
|
|
14262
|
-
|
|
14263
|
-
|
|
14264
|
-
}
|
|
14265
|
-
})
|
|
15229
|
+
}) => {
|
|
15230
|
+
const body = { name, bundleId: bundle_id, platform, storeUrl: store_url };
|
|
15231
|
+
if (timezone)
|
|
15232
|
+
body.timezone = timezone;
|
|
15233
|
+
if (device_matching_mode)
|
|
15234
|
+
body.deviceMatchingMode = device_matching_mode;
|
|
15235
|
+
return toToolResult(await call("/api/v1/apps", {
|
|
15236
|
+
method: "POST",
|
|
15237
|
+
idempotencyKey: idempotency_key,
|
|
15238
|
+
body
|
|
15239
|
+
}));
|
|
15240
|
+
});
|
|
14266
15241
|
server.registerTool("get_app", {
|
|
14267
15242
|
annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true },
|
|
14268
15243
|
title: "Get an app",
|
|
14269
15244
|
description: "Return one app's configuration.",
|
|
14270
15245
|
inputSchema: { app_id: requiredAppIdSchema }
|
|
14271
|
-
}, async ({ app_id }) => toToolResult(await call(`/api/v1/apps/${
|
|
15246
|
+
}, async ({ app_id }) => toToolResult(await call(`/api/v1/apps/${encodeOpaquePathSegment(app_id)}`)));
|
|
14272
15247
|
server.registerTool("update_app", {
|
|
14273
15248
|
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true },
|
|
14274
15249
|
title: "Update an app",
|
|
14275
|
-
description: "Update app metadata, store destination, timezone or
|
|
15250
|
+
description: "Update app metadata, store destination, timezone or coverage mode.",
|
|
14276
15251
|
inputSchema: {
|
|
14277
15252
|
app_id: requiredAppIdSchema,
|
|
14278
|
-
name:
|
|
14279
|
-
bundle_id:
|
|
14280
|
-
platform:
|
|
14281
|
-
store_url:
|
|
14282
|
-
timezone:
|
|
14283
|
-
device_matching_mode:
|
|
15253
|
+
name: z5.string().min(1).max(80).optional(),
|
|
15254
|
+
bundle_id: z5.string().min(3).max(255).optional(),
|
|
15255
|
+
platform: z5.enum(["iOS", "Android"]).optional(),
|
|
15256
|
+
store_url: z5.string().url().optional(),
|
|
15257
|
+
timezone: z5.string().max(64).optional(),
|
|
15258
|
+
device_matching_mode: z5.enum(["on", "strict"]).optional(),
|
|
14284
15259
|
idempotency_key: idempotencyKeySchema
|
|
14285
15260
|
}
|
|
14286
15261
|
}, async ({
|
|
@@ -14292,24 +15267,32 @@ function createServer(config, fetchImpl) {
|
|
|
14292
15267
|
timezone,
|
|
14293
15268
|
device_matching_mode,
|
|
14294
15269
|
idempotency_key
|
|
14295
|
-
}) =>
|
|
14296
|
-
|
|
14297
|
-
|
|
14298
|
-
|
|
14299
|
-
|
|
14300
|
-
|
|
14301
|
-
|
|
14302
|
-
|
|
14303
|
-
|
|
14304
|
-
|
|
14305
|
-
|
|
14306
|
-
|
|
15270
|
+
}) => {
|
|
15271
|
+
const body = {};
|
|
15272
|
+
if (name)
|
|
15273
|
+
body.name = name;
|
|
15274
|
+
if (bundle_id)
|
|
15275
|
+
body.bundleId = bundle_id;
|
|
15276
|
+
if (platform)
|
|
15277
|
+
body.platform = platform;
|
|
15278
|
+
if (store_url)
|
|
15279
|
+
body.storeUrl = store_url;
|
|
15280
|
+
if (timezone)
|
|
15281
|
+
body.timezone = timezone;
|
|
15282
|
+
if (device_matching_mode)
|
|
15283
|
+
body.deviceMatchingMode = device_matching_mode;
|
|
15284
|
+
return toToolResult(await call(`/api/v1/apps/${encodeOpaquePathSegment(app_id)}`, {
|
|
15285
|
+
method: "PATCH",
|
|
15286
|
+
idempotencyKey: idempotency_key,
|
|
15287
|
+
body
|
|
15288
|
+
}));
|
|
15289
|
+
});
|
|
14307
15290
|
server.registerTool("archive_app", {
|
|
14308
15291
|
annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: true },
|
|
14309
15292
|
title: "Archive an app",
|
|
14310
15293
|
description: "Archive an app and remove it from active customer operations.",
|
|
14311
15294
|
inputSchema: { app_id: requiredAppIdSchema }
|
|
14312
|
-
}, async ({ app_id }) => toToolResult(await call(`/api/v1/apps/${
|
|
15295
|
+
}, async ({ app_id }) => toToolResult(await call(`/api/v1/apps/${encodeOpaquePathSegment(app_id)}`, {
|
|
14313
15296
|
method: "DELETE"
|
|
14314
15297
|
})));
|
|
14315
15298
|
server.registerTool("get_publishable_key", {
|
|
@@ -14317,7 +15300,7 @@ function createServer(config, fetchImpl) {
|
|
|
14317
15300
|
title: "Get an app publishable key",
|
|
14318
15301
|
description: "Reveal the recoverable SDK publishable key for an app.",
|
|
14319
15302
|
inputSchema: { app_id: requiredAppIdSchema }
|
|
14320
|
-
}, async ({ app_id }) => toToolResult(await call(`/api/v1/apps/${
|
|
15303
|
+
}, async ({ app_id }) => toToolResult(await call(`/api/v1/apps/${encodeOpaquePathSegment(app_id)}/publishable-key`)));
|
|
14321
15304
|
server.registerTool("rotate_publishable_key", {
|
|
14322
15305
|
annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: true },
|
|
14323
15306
|
title: "Rotate an app publishable key",
|
|
@@ -14326,7 +15309,7 @@ function createServer(config, fetchImpl) {
|
|
|
14326
15309
|
app_id: requiredAppIdSchema,
|
|
14327
15310
|
idempotency_key: idempotencyKeySchema
|
|
14328
15311
|
}
|
|
14329
|
-
}, async ({ app_id, idempotency_key }) => toToolResult(await call(`/api/v1/apps/${
|
|
15312
|
+
}, async ({ app_id, idempotency_key }) => toToolResult(await call(`/api/v1/apps/${encodeOpaquePathSegment(app_id)}/publishable-key`, { method: "POST", idempotencyKey: idempotency_key })));
|
|
14330
15313
|
server.registerTool("list_api_keys", {
|
|
14331
15314
|
annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true },
|
|
14332
15315
|
title: "List management keys",
|
|
@@ -14338,9 +15321,9 @@ function createServer(config, fetchImpl) {
|
|
|
14338
15321
|
title: "Create a management key",
|
|
14339
15322
|
description: "Create a read-only or read/write workspace or app key. The secret is returned once.",
|
|
14340
15323
|
inputSchema: {
|
|
14341
|
-
label:
|
|
14342
|
-
scopes:
|
|
14343
|
-
app_id:
|
|
15324
|
+
label: z5.string().min(1).max(80),
|
|
15325
|
+
scopes: z5.array(z5.enum(["read", "write"])).min(1).max(2),
|
|
15326
|
+
app_id: z5.string().uuid().optional().nullable(),
|
|
14344
15327
|
idempotency_key: idempotencyKeySchema
|
|
14345
15328
|
}
|
|
14346
15329
|
}, async ({ label, scopes, app_id, idempotency_key }) => toToolResult(await call("/api/v1/api-keys", {
|
|
@@ -14352,18 +15335,18 @@ function createServer(config, fetchImpl) {
|
|
|
14352
15335
|
annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: true },
|
|
14353
15336
|
title: "Revoke a management key",
|
|
14354
15337
|
description: "Immediately revoke a management key by id.",
|
|
14355
|
-
inputSchema: { key_id:
|
|
15338
|
+
inputSchema: { key_id: z5.string().uuid() }
|
|
14356
15339
|
}, async ({ key_id }) => toToolResult(await call(`/api/v1/api-keys/${encodeURIComponent(key_id)}`, {
|
|
14357
15340
|
method: "DELETE"
|
|
14358
15341
|
})));
|
|
14359
15342
|
server.registerTool("list_links", {
|
|
14360
15343
|
annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true },
|
|
14361
15344
|
title: "List campaign links",
|
|
14362
|
-
description: "List tracking links, optionally for one app.",
|
|
15345
|
+
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
15346
|
inputSchema: {
|
|
14364
15347
|
app_id: appIdSchema,
|
|
14365
|
-
cursor:
|
|
14366
|
-
limit:
|
|
15348
|
+
cursor: z5.string().optional(),
|
|
15349
|
+
limit: z5.number().int().min(1).max(200).optional()
|
|
14367
15350
|
}
|
|
14368
15351
|
}, async ({ app_id, cursor, limit }) => toToolResult(await call("/api/v1/links", {
|
|
14369
15352
|
query: {
|
|
@@ -14375,16 +15358,19 @@ function createServer(config, fetchImpl) {
|
|
|
14375
15358
|
server.registerTool("list_campaigns", {
|
|
14376
15359
|
annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true },
|
|
14377
15360
|
title: "List campaigns",
|
|
14378
|
-
description: "List ad campaigns with their honest profit surface
|
|
14379
|
-
inputSchema: { app_id: appIdSchema, period: periodSchema, cursor: cursorSchema, limit: listLimitSchema }
|
|
14380
|
-
}, async ({ app_id, period, cursor, limit }) =>
|
|
14381
|
-
query
|
|
15361
|
+
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.",
|
|
15362
|
+
inputSchema: { app_id: appIdSchema, period: periodSchema, from: rangeFromSchema, to: rangeToSchema, cursor: cursorSchema, limit: listLimitSchema }
|
|
15363
|
+
}, async ({ app_id, period, from, to, cursor, limit }) => {
|
|
15364
|
+
const query = {
|
|
14382
15365
|
appId: app_id,
|
|
14383
15366
|
period,
|
|
14384
|
-
|
|
14385
|
-
|
|
14386
|
-
|
|
14387
|
-
|
|
15367
|
+
from,
|
|
15368
|
+
to,
|
|
15369
|
+
cursor: cursor || undefined,
|
|
15370
|
+
limit: limit === undefined ? undefined : String(limit)
|
|
15371
|
+
};
|
|
15372
|
+
return toToolResult(await call("/api/v1/reports/campaigns", { query }));
|
|
15373
|
+
});
|
|
14388
15374
|
server.registerTool("list_ads", {
|
|
14389
15375
|
annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true },
|
|
14390
15376
|
title: "List creative-level ad performance",
|
|
@@ -14392,12 +15378,18 @@ function createServer(config, fetchImpl) {
|
|
|
14392
15378
|
inputSchema: {
|
|
14393
15379
|
app_id: appIdSchema,
|
|
14394
15380
|
period: periodSchema,
|
|
14395
|
-
|
|
15381
|
+
from: rangeFromSchema,
|
|
15382
|
+
to: rangeToSchema,
|
|
15383
|
+
cursor: cursorSchema,
|
|
15384
|
+
limit: z5.number().int().min(1).max(200).optional()
|
|
14396
15385
|
}
|
|
14397
|
-
}, async ({ app_id, period, limit }) => toToolResult(await call("/api/v1/reports/ads", {
|
|
15386
|
+
}, async ({ app_id, period, from, to, cursor, limit }) => toToolResult(await call("/api/v1/reports/ads", {
|
|
14398
15387
|
query: {
|
|
14399
15388
|
appId: app_id,
|
|
14400
15389
|
period,
|
|
15390
|
+
from,
|
|
15391
|
+
to,
|
|
15392
|
+
cursor,
|
|
14401
15393
|
limit: limit === undefined ? undefined : String(limit)
|
|
14402
15394
|
}
|
|
14403
15395
|
})));
|
|
@@ -14407,62 +15399,54 @@ function createServer(config, fetchImpl) {
|
|
|
14407
15399
|
description: "Return the same recent attribution events and installs visible on the Live dashboard.",
|
|
14408
15400
|
inputSchema: {
|
|
14409
15401
|
app_id: appIdSchema,
|
|
14410
|
-
limit:
|
|
15402
|
+
limit: z5.number().int().min(1).max(50).optional(),
|
|
15403
|
+
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."),
|
|
15404
|
+
to: calendarDaySchema.optional().describe("Last UTC day to include, YYYY-MM-DD. May be sent without from."),
|
|
15405
|
+
cursor: cursorSchema
|
|
14411
15406
|
}
|
|
14412
|
-
}, async ({ app_id, limit }) => toToolResult(await call("/api/v1/events", {
|
|
15407
|
+
}, async ({ app_id, limit, from, to, cursor }) => toToolResult(await call("/api/v1/events", {
|
|
14413
15408
|
query: {
|
|
14414
15409
|
appId: app_id,
|
|
15410
|
+
from,
|
|
15411
|
+
to,
|
|
15412
|
+
cursor,
|
|
14415
15413
|
limit: limit === undefined ? undefined : String(limit)
|
|
14416
15414
|
}
|
|
14417
15415
|
})));
|
|
14418
15416
|
server.registerTool("get_campaign_decision", {
|
|
14419
15417
|
annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true },
|
|
14420
15418
|
title: "Get campaign decisions (scale / cut / hold)",
|
|
14421
|
-
description: "Return
|
|
14422
|
-
inputSchema: {
|
|
14423
|
-
|
|
15419
|
+
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.",
|
|
15420
|
+
inputSchema: {
|
|
15421
|
+
app_id: appIdSchema,
|
|
15422
|
+
period: periodSchema,
|
|
15423
|
+
cursor: cursorSchema,
|
|
15424
|
+
limit: listLimitSchema
|
|
15425
|
+
}
|
|
15426
|
+
}, async ({ app_id, period, cursor, limit }) => toToolResult(await call("/api/v1/decisions", {
|
|
15427
|
+
query: {
|
|
15428
|
+
appId: app_id,
|
|
15429
|
+
period,
|
|
15430
|
+
cursor,
|
|
15431
|
+
limit: limit === undefined ? undefined : String(limit)
|
|
15432
|
+
}
|
|
15433
|
+
})));
|
|
14424
15434
|
server.registerTool("record_campaign_decision", {
|
|
14425
15435
|
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true },
|
|
14426
15436
|
title: "Record a campaign decision",
|
|
14427
|
-
description: "
|
|
15437
|
+
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
15438
|
inputSchema: {
|
|
14429
15439
|
app_id: requiredAppIdSchema,
|
|
14430
|
-
campaign_id:
|
|
14431
|
-
call:
|
|
14432
|
-
|
|
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."),
|
|
15440
|
+
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."),
|
|
15441
|
+
call: z5.enum(["scale", "cut", "hold"]).describe("The decision being frozen. Report get_campaign_decision's own call verbatim rather than forming your own."),
|
|
15442
|
+
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
15443
|
idempotency_key: idempotencyKeySchema
|
|
14449
15444
|
}
|
|
14450
15445
|
}, async ({
|
|
14451
15446
|
app_id,
|
|
14452
15447
|
campaign_id,
|
|
14453
15448
|
call: decisionCall,
|
|
14454
|
-
|
|
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,
|
|
15449
|
+
period,
|
|
14466
15450
|
idempotency_key
|
|
14467
15451
|
}) => toToolResult(await call("/api/v1/decisions", {
|
|
14468
15452
|
method: "POST",
|
|
@@ -14471,70 +15455,76 @@ function createServer(config, fetchImpl) {
|
|
|
14471
15455
|
appId: app_id,
|
|
14472
15456
|
campaignId: campaign_id,
|
|
14473
15457
|
call: decisionCall,
|
|
14474
|
-
|
|
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
|
-
}
|
|
15458
|
+
period
|
|
14488
15459
|
}
|
|
14489
15460
|
})));
|
|
14490
15461
|
server.registerTool("get_attribution_rollup", {
|
|
14491
15462
|
annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true },
|
|
14492
15463
|
title: "Get attribution rollup",
|
|
14493
|
-
description: "Return the account-level attribution overview: installs accounted-for by evidence class (verified /
|
|
14494
|
-
inputSchema: { app_id: appIdSchema, period: periodSchema }
|
|
14495
|
-
}, async ({ app_id, period }) => toToolResult(await call("/api/v1/reports/overview", { query: { appId: app_id, period } })));
|
|
15464
|
+
description: "Return the account-level attribution overview: installs accounted-for by evidence class (verified / matched / modeled / organic), MER, revenue and data freshness.",
|
|
15465
|
+
inputSchema: { app_id: appIdSchema, period: periodSchema, from: rangeFromSchema, to: rangeToSchema }
|
|
15466
|
+
}, async ({ app_id, period, from, to }) => toToolResult(await call("/api/v1/reports/overview", { query: { appId: app_id, period, from, to } })));
|
|
14496
15467
|
server.registerTool("create_campaign_link", {
|
|
14497
15468
|
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true },
|
|
14498
15469
|
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
|
|
15470
|
+
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
15471
|
inputSchema: {
|
|
14501
|
-
app_id:
|
|
14502
|
-
network:
|
|
14503
|
-
destination_url:
|
|
14504
|
-
campaign_name:
|
|
14505
|
-
|
|
14506
|
-
|
|
15472
|
+
app_id: requiredAppIdSchema.describe("The app id the link belongs to."),
|
|
15473
|
+
network: z5.enum(["meta", "google", "tiktok"]).describe("The ad network delivering the campaign."),
|
|
15474
|
+
destination_url: z5.string().url().describe("The App Store URL to send users to."),
|
|
15475
|
+
campaign_name: z5.string().max(120).optional().describe("Optional human-readable campaign name."),
|
|
15476
|
+
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."),
|
|
15477
|
+
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."),
|
|
15478
|
+
serve_landing: z5.boolean().optional().describe("Serve the landing bridge on this link. Defaults to true at the API."),
|
|
14507
15479
|
idempotency_key: idempotencyKeySchema
|
|
14508
15480
|
}
|
|
14509
|
-
}, async ({ app_id, network, destination_url, campaign_name, campaign_id, serve_landing, idempotency_key }) =>
|
|
14510
|
-
|
|
14511
|
-
|
|
14512
|
-
|
|
14513
|
-
|
|
14514
|
-
|
|
14515
|
-
|
|
14516
|
-
|
|
14517
|
-
|
|
14518
|
-
|
|
14519
|
-
|
|
14520
|
-
|
|
15481
|
+
}, async ({ app_id, network, destination_url, campaign_name, campaign_id, network_campaign_id, serve_landing, idempotency_key }) => {
|
|
15482
|
+
const body = { appId: app_id, network, destinationUrl: destination_url };
|
|
15483
|
+
if (campaign_name)
|
|
15484
|
+
body.campaignName = campaign_name;
|
|
15485
|
+
if (campaign_id)
|
|
15486
|
+
body.campaignId = campaign_id;
|
|
15487
|
+
if (network_campaign_id)
|
|
15488
|
+
body.networkCampaignId = network_campaign_id;
|
|
15489
|
+
if (serve_landing !== undefined)
|
|
15490
|
+
body.serveLanding = serve_landing;
|
|
15491
|
+
return toToolResult(await call("/api/v1/links", {
|
|
15492
|
+
method: "POST",
|
|
15493
|
+
idempotencyKey: idempotency_key,
|
|
15494
|
+
body
|
|
15495
|
+
}));
|
|
15496
|
+
});
|
|
14521
15497
|
server.registerTool("update_link_status", {
|
|
14522
15498
|
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.",
|
|
15499
|
+
title: "Enable or disable a campaign link, or attach its network campaign id",
|
|
15500
|
+
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
15501
|
inputSchema: {
|
|
14526
|
-
link_id:
|
|
14527
|
-
status:
|
|
15502
|
+
link_id: z5.string().uuid(),
|
|
15503
|
+
status: z5.enum(["active", "disabled"]).optional(),
|
|
15504
|
+
network_campaign_id: z5.string().optional().describe("The ad network's own campaign id (for Meta, the campaign ID in Ads Manager)."),
|
|
14528
15505
|
idempotency_key: idempotencyKeySchema
|
|
14529
15506
|
}
|
|
14530
|
-
}, async ({ link_id, status, idempotency_key }) =>
|
|
15507
|
+
}, async ({ link_id, status, network_campaign_id, idempotency_key }) => {
|
|
15508
|
+
const body = {};
|
|
15509
|
+
if (status !== undefined)
|
|
15510
|
+
body.status = status;
|
|
15511
|
+
if (network_campaign_id !== undefined)
|
|
15512
|
+
body.network_campaign_id = network_campaign_id;
|
|
15513
|
+
return toToolResult(await call(`/api/v1/links/${encodeURIComponent(link_id)}`, { method: "PATCH", idempotencyKey: idempotency_key, body }));
|
|
15514
|
+
});
|
|
14531
15515
|
server.registerTool("get_spend", {
|
|
14532
15516
|
annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true },
|
|
14533
15517
|
title: "Get spend and cost models",
|
|
14534
|
-
description: "Return imported ad spend, cost models, completeness and spend freshness.",
|
|
14535
|
-
inputSchema: {
|
|
14536
|
-
|
|
14537
|
-
|
|
15518
|
+
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.",
|
|
15519
|
+
inputSchema: {
|
|
15520
|
+
app_id: appIdSchema,
|
|
15521
|
+
from: calendarDaySchema.optional().describe("First metric day to include (UTC, YYYY-MM-DD). May be sent without to."),
|
|
15522
|
+
to: calendarDaySchema.optional().describe("Last metric day to include (UTC, YYYY-MM-DD). May be sent without from."),
|
|
15523
|
+
cursor: cursorSchema,
|
|
15524
|
+
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.")
|
|
15525
|
+
}
|
|
15526
|
+
}, async ({ app_id, from, to, cursor, limit }) => toToolResult(await call("/api/v1/spend", {
|
|
15527
|
+
query: { appId: app_id, from, to, cursor, limit: limit === undefined ? undefined : String(limit) }
|
|
14538
15528
|
})));
|
|
14539
15529
|
server.registerTool("import_spend", {
|
|
14540
15530
|
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true },
|
|
@@ -14542,16 +15532,16 @@ function createServer(config, fetchImpl) {
|
|
|
14542
15532
|
description: "Import campaign or creative-level spend rows.",
|
|
14543
15533
|
inputSchema: {
|
|
14544
15534
|
app_id: requiredAppIdSchema,
|
|
14545
|
-
network:
|
|
14546
|
-
source:
|
|
14547
|
-
account_ref:
|
|
14548
|
-
currency:
|
|
14549
|
-
report_timezone:
|
|
14550
|
-
covers_from:
|
|
14551
|
-
covers_to:
|
|
14552
|
-
completeness:
|
|
14553
|
-
notes:
|
|
14554
|
-
rows:
|
|
15535
|
+
network: z5.enum(["meta", "google", "tiktok", "apple_ads"]),
|
|
15536
|
+
source: z5.enum(["network_api", "csv", "manual"]).optional(),
|
|
15537
|
+
account_ref: z5.string().optional(),
|
|
15538
|
+
currency: z5.string().length(3),
|
|
15539
|
+
report_timezone: z5.string().optional(),
|
|
15540
|
+
covers_from: z5.string().regex(/^\d{4}-\d{2}-\d{2}$/),
|
|
15541
|
+
covers_to: z5.string().regex(/^\d{4}-\d{2}-\d{2}$/),
|
|
15542
|
+
completeness: z5.enum(["partial", "final"]).optional(),
|
|
15543
|
+
notes: z5.string().optional(),
|
|
15544
|
+
rows: z5.array(spendRowSchema).min(1).max(5000),
|
|
14555
15545
|
idempotency_key: idempotencyKeySchema
|
|
14556
15546
|
}
|
|
14557
15547
|
}, async ({
|
|
@@ -14567,36 +15557,44 @@ function createServer(config, fetchImpl) {
|
|
|
14567
15557
|
notes,
|
|
14568
15558
|
rows,
|
|
14569
15559
|
idempotency_key
|
|
14570
|
-
}) =>
|
|
14571
|
-
|
|
14572
|
-
idempotencyKey: idempotency_key,
|
|
14573
|
-
body: {
|
|
15560
|
+
}) => {
|
|
15561
|
+
const body = {
|
|
14574
15562
|
kind: "spend",
|
|
14575
15563
|
appId: app_id,
|
|
14576
15564
|
network,
|
|
14577
|
-
...source ? { source } : {},
|
|
14578
|
-
...account_ref ? { accountRef: account_ref } : {},
|
|
14579
15565
|
currency,
|
|
14580
|
-
...report_timezone ? { reportTimezone: report_timezone } : {},
|
|
14581
15566
|
coversFrom: covers_from,
|
|
14582
15567
|
coversTo: covers_to,
|
|
14583
|
-
...completeness ? { completeness } : {},
|
|
14584
|
-
...notes ? { notes } : {},
|
|
14585
15568
|
rows: rows.map(normalizeSpendRow)
|
|
14586
|
-
}
|
|
14587
|
-
|
|
15569
|
+
};
|
|
15570
|
+
if (source)
|
|
15571
|
+
body.source = source;
|
|
15572
|
+
if (account_ref)
|
|
15573
|
+
body.accountRef = account_ref;
|
|
15574
|
+
if (report_timezone)
|
|
15575
|
+
body.reportTimezone = report_timezone;
|
|
15576
|
+
if (completeness)
|
|
15577
|
+
body.completeness = completeness;
|
|
15578
|
+
if (notes)
|
|
15579
|
+
body.notes = notes;
|
|
15580
|
+
return toToolResult(await call("/api/v1/spend", {
|
|
15581
|
+
method: "POST",
|
|
15582
|
+
idempotencyKey: idempotency_key,
|
|
15583
|
+
body
|
|
15584
|
+
}));
|
|
15585
|
+
});
|
|
14588
15586
|
server.registerTool("set_cost_model", {
|
|
14589
15587
|
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true },
|
|
14590
15588
|
title: "Set an app cost model",
|
|
14591
15589
|
description: "Version commission, payment fees and variable COGS used in profit decisions.",
|
|
14592
15590
|
inputSchema: {
|
|
14593
15591
|
app_id: requiredAppIdSchema,
|
|
14594
|
-
effective_at:
|
|
14595
|
-
store_commission_bps:
|
|
14596
|
-
payment_fee_bps:
|
|
14597
|
-
variable_cogs_kind:
|
|
14598
|
-
variable_cogs_value:
|
|
14599
|
-
assumptions:
|
|
15592
|
+
effective_at: z5.string(),
|
|
15593
|
+
store_commission_bps: z5.number().int().min(0).max(1e4),
|
|
15594
|
+
payment_fee_bps: z5.number().int().min(0).max(1e4),
|
|
15595
|
+
variable_cogs_kind: z5.enum(["bps", "per_unit_minor"]),
|
|
15596
|
+
variable_cogs_value: z5.number().nonnegative(),
|
|
15597
|
+
assumptions: z5.record(z5.string(), z5.unknown()).optional(),
|
|
14600
15598
|
idempotency_key: idempotencyKeySchema
|
|
14601
15599
|
}
|
|
14602
15600
|
}, async ({
|
|
@@ -14608,43 +15606,52 @@ function createServer(config, fetchImpl) {
|
|
|
14608
15606
|
variable_cogs_value,
|
|
14609
15607
|
assumptions,
|
|
14610
15608
|
idempotency_key
|
|
14611
|
-
}) =>
|
|
14612
|
-
|
|
14613
|
-
idempotencyKey: idempotency_key,
|
|
14614
|
-
body: {
|
|
15609
|
+
}) => {
|
|
15610
|
+
const body = {
|
|
14615
15611
|
kind: "cost_model",
|
|
14616
15612
|
appId: app_id,
|
|
14617
15613
|
effectiveAt: effective_at,
|
|
14618
15614
|
storeCommissionBps: store_commission_bps,
|
|
14619
15615
|
paymentFeeBps: payment_fee_bps,
|
|
14620
|
-
variableCogs: { kind: variable_cogs_kind, value: variable_cogs_value }
|
|
14621
|
-
|
|
15616
|
+
variableCogs: { kind: variable_cogs_kind, value: variable_cogs_value }
|
|
15617
|
+
};
|
|
15618
|
+
if (assumptions) {
|
|
15619
|
+
body.assumptions = assumptions;
|
|
14622
15620
|
}
|
|
14623
|
-
|
|
15621
|
+
return toToolResult(await call("/api/v1/spend", {
|
|
15622
|
+
method: "POST",
|
|
15623
|
+
idempotencyKey: idempotency_key,
|
|
15624
|
+
body
|
|
15625
|
+
}));
|
|
15626
|
+
});
|
|
14624
15627
|
server.registerTool("pull_network_spend", {
|
|
14625
15628
|
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true },
|
|
14626
15629
|
title: "Pull spend from an ad network",
|
|
14627
|
-
description: "Fetch and import creative-level spend from a configured Meta, Google or TikTok connection.",
|
|
15630
|
+
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
15631
|
inputSchema: {
|
|
14629
15632
|
app_id: requiredAppIdSchema,
|
|
14630
|
-
network:
|
|
14631
|
-
connection_id:
|
|
14632
|
-
covers_from:
|
|
14633
|
-
covers_to:
|
|
15633
|
+
network: z5.enum(["meta", "google", "tiktok"]),
|
|
15634
|
+
connection_id: z5.string().optional(),
|
|
15635
|
+
covers_from: z5.string().regex(/^\d{4}-\d{2}-\d{2}$/),
|
|
15636
|
+
covers_to: z5.string().regex(/^\d{4}-\d{2}-\d{2}$/),
|
|
14634
15637
|
idempotency_key: idempotencyKeySchema
|
|
14635
15638
|
}
|
|
14636
|
-
}, async ({ app_id, network, connection_id, covers_from, covers_to, idempotency_key }) =>
|
|
14637
|
-
|
|
14638
|
-
idempotencyKey: idempotency_key,
|
|
14639
|
-
body: {
|
|
15639
|
+
}, async ({ app_id, network, connection_id, covers_from, covers_to, idempotency_key }) => {
|
|
15640
|
+
const body = {
|
|
14640
15641
|
kind: "network_pull",
|
|
14641
15642
|
appId: app_id,
|
|
14642
15643
|
network,
|
|
14643
|
-
...connection_id ? { connectionId: connection_id } : {},
|
|
14644
15644
|
coversFrom: covers_from,
|
|
14645
15645
|
coversTo: covers_to
|
|
14646
|
-
}
|
|
14647
|
-
|
|
15646
|
+
};
|
|
15647
|
+
if (connection_id)
|
|
15648
|
+
body.connectionId = connection_id;
|
|
15649
|
+
return toToolResult(await call("/api/v1/spend", {
|
|
15650
|
+
method: "POST",
|
|
15651
|
+
idempotencyKey: idempotency_key,
|
|
15652
|
+
body
|
|
15653
|
+
}));
|
|
15654
|
+
});
|
|
14648
15655
|
server.registerTool("list_integrations", {
|
|
14649
15656
|
annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true },
|
|
14650
15657
|
title: "List ad-network integrations",
|
|
@@ -14654,13 +15661,13 @@ function createServer(config, fetchImpl) {
|
|
|
14654
15661
|
server.registerTool("create_integration", {
|
|
14655
15662
|
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true },
|
|
14656
15663
|
title: "Create an ad-network integration",
|
|
14657
|
-
description: "Store encrypted network credentials and
|
|
15664
|
+
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
15665
|
inputSchema: {
|
|
14659
|
-
network:
|
|
14660
|
-
account_ref:
|
|
14661
|
-
credential:
|
|
14662
|
-
owners: ownersSchema.describe("
|
|
14663
|
-
enable:
|
|
15666
|
+
network: z5.enum(["meta", "google", "tiktok"]),
|
|
15667
|
+
account_ref: z5.string().min(1),
|
|
15668
|
+
credential: z5.string().min(1),
|
|
15669
|
+
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."),
|
|
15670
|
+
enable: z5.boolean().optional(),
|
|
14664
15671
|
idempotency_key: idempotencyKeySchema
|
|
14665
15672
|
}
|
|
14666
15673
|
}, async ({ network, account_ref, credential, owners, enable, idempotency_key }) => toToolResult(await call("/api/v1/integrations", {
|
|
@@ -14673,48 +15680,57 @@ function createServer(config, fetchImpl) {
|
|
|
14673
15680
|
title: "Update an ad-network integration",
|
|
14674
15681
|
description: "Rotate credentials or enable/disable an integration.",
|
|
14675
15682
|
inputSchema: {
|
|
14676
|
-
connection_id:
|
|
14677
|
-
action:
|
|
14678
|
-
credential:
|
|
15683
|
+
connection_id: z5.string(),
|
|
15684
|
+
action: z5.enum(["rotate", "disable", "enable"]),
|
|
15685
|
+
credential: z5.string().optional(),
|
|
14679
15686
|
owners: ownersSchema.optional(),
|
|
14680
15687
|
idempotency_key: idempotencyKeySchema
|
|
14681
15688
|
}
|
|
14682
|
-
}, async ({ connection_id, action, credential, owners, idempotency_key }) =>
|
|
14683
|
-
|
|
14684
|
-
|
|
14685
|
-
|
|
14686
|
-
|
|
14687
|
-
|
|
14688
|
-
|
|
14689
|
-
|
|
14690
|
-
|
|
15689
|
+
}, async ({ connection_id, action, credential, owners, idempotency_key }) => {
|
|
15690
|
+
const body = { action };
|
|
15691
|
+
if (credential !== undefined)
|
|
15692
|
+
body.credential = credential;
|
|
15693
|
+
if (owners)
|
|
15694
|
+
body.owners = owners;
|
|
15695
|
+
return toToolResult(await call(`/api/v1/integrations/${encodeOpaquePathSegment(connection_id)}`, {
|
|
15696
|
+
method: "PATCH",
|
|
15697
|
+
idempotencyKey: idempotency_key,
|
|
15698
|
+
body
|
|
15699
|
+
}));
|
|
15700
|
+
});
|
|
14691
15701
|
server.registerTool("delete_integration", {
|
|
14692
15702
|
annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: true },
|
|
14693
15703
|
title: "Delete an ad-network integration",
|
|
14694
15704
|
description: "Delete a disabled integration. Enabled integrations must be disabled first.",
|
|
14695
|
-
inputSchema: { connection_id:
|
|
14696
|
-
}, async ({ connection_id }) => toToolResult(await call(`/api/v1/integrations/${
|
|
15705
|
+
inputSchema: { connection_id: z5.string() }
|
|
15706
|
+
}, async ({ connection_id }) => toToolResult(await call(`/api/v1/integrations/${encodeOpaquePathSegment(connection_id)}`, { method: "DELETE" })));
|
|
14697
15707
|
server.registerTool("get_revenuecat", {
|
|
14698
15708
|
annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true },
|
|
14699
15709
|
title: "Get RevenueCat configuration",
|
|
14700
|
-
description: "Return redacted RevenueCat status and the
|
|
15710
|
+
description: "Return redacted RevenueCat status and the AttriKit webhook URL.",
|
|
14701
15711
|
inputSchema: { app_id: requiredAppIdSchema }
|
|
14702
|
-
}, async ({ app_id }) => toToolResult(await call(`/api/v1/apps/${
|
|
15712
|
+
}, async ({ app_id }) => toToolResult(await call(`/api/v1/apps/${encodeOpaquePathSegment(app_id)}/revenuecat`)));
|
|
14703
15713
|
server.registerTool("configure_revenuecat", {
|
|
14704
|
-
annotations: { readOnlyHint: false, destructiveHint:
|
|
15714
|
+
annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: true },
|
|
14705
15715
|
title: "Configure RevenueCat",
|
|
14706
|
-
description: "Store the RevenueCat provider app id and encrypted authorization secret for an app.",
|
|
15716
|
+
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
15717
|
inputSchema: {
|
|
14708
15718
|
app_id: requiredAppIdSchema,
|
|
14709
15719
|
provider_app_id: revenueCatProviderAppIdSchema.describe("RevenueCat's PROJECT app id from their dashboard app settings, not the SDK key."),
|
|
14710
|
-
authorization_secret:
|
|
15720
|
+
authorization_secret: z5.string().min(16).max(1024),
|
|
15721
|
+
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
15722
|
idempotency_key: idempotencyKeySchema
|
|
14712
15723
|
}
|
|
14713
|
-
}, async ({ app_id, provider_app_id, authorization_secret, idempotency_key }) =>
|
|
14714
|
-
|
|
14715
|
-
|
|
14716
|
-
|
|
14717
|
-
|
|
15724
|
+
}, async ({ app_id, provider_app_id, authorization_secret, signing_secret, idempotency_key }) => {
|
|
15725
|
+
const body = { provider_app_id, authorization_secret };
|
|
15726
|
+
if (signing_secret !== undefined)
|
|
15727
|
+
body.signing_secret = signing_secret;
|
|
15728
|
+
return toToolResult(await call(`/api/v1/apps/${encodeOpaquePathSegment(app_id)}/revenuecat`, {
|
|
15729
|
+
method: "PUT",
|
|
15730
|
+
idempotencyKey: idempotency_key,
|
|
15731
|
+
body
|
|
15732
|
+
}));
|
|
15733
|
+
});
|
|
14718
15734
|
server.registerTool("list_webhooks", {
|
|
14719
15735
|
annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true },
|
|
14720
15736
|
title: "List outbound webhooks",
|
|
@@ -14726,41 +15742,116 @@ function createServer(config, fetchImpl) {
|
|
|
14726
15742
|
title: "Create an outbound webhook",
|
|
14727
15743
|
description: "Create an SSRF-checked webhook subscription. Returns the signing secret once.",
|
|
14728
15744
|
inputSchema: {
|
|
14729
|
-
url:
|
|
14730
|
-
event_types:
|
|
14731
|
-
description:
|
|
14732
|
-
app_id:
|
|
15745
|
+
url: z5.string().url(),
|
|
15746
|
+
event_types: z5.array(z5.enum(["install", "event", "decision", "fraud"])).min(1).max(4),
|
|
15747
|
+
description: z5.string().max(500).optional(),
|
|
15748
|
+
app_id: z5.string().uuid().optional(),
|
|
14733
15749
|
idempotency_key: idempotencyKeySchema
|
|
14734
15750
|
}
|
|
14735
|
-
}, async ({ url, event_types, description, app_id, idempotency_key }) =>
|
|
14736
|
-
|
|
14737
|
-
|
|
14738
|
-
|
|
14739
|
-
|
|
14740
|
-
|
|
14741
|
-
|
|
14742
|
-
|
|
14743
|
-
|
|
14744
|
-
|
|
15751
|
+
}, async ({ url, event_types, description, app_id, idempotency_key }) => {
|
|
15752
|
+
const body = { url, event_types };
|
|
15753
|
+
if (description)
|
|
15754
|
+
body.description = description;
|
|
15755
|
+
if (app_id)
|
|
15756
|
+
body.app_id = app_id;
|
|
15757
|
+
return toToolResult(await call("/api/v1/webhooks", {
|
|
15758
|
+
method: "POST",
|
|
15759
|
+
idempotencyKey: idempotency_key,
|
|
15760
|
+
body
|
|
15761
|
+
}));
|
|
15762
|
+
});
|
|
14745
15763
|
server.registerTool("delete_webhook", {
|
|
14746
15764
|
annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: true },
|
|
14747
15765
|
title: "Delete an outbound webhook",
|
|
14748
15766
|
description: "Delete a webhook subscription.",
|
|
14749
|
-
inputSchema: { subscription_id:
|
|
15767
|
+
inputSchema: { subscription_id: z5.string().uuid() }
|
|
14750
15768
|
}, async ({ subscription_id }) => toToolResult(await call(`/api/v1/webhooks/${encodeURIComponent(subscription_id)}`, { method: "DELETE" })));
|
|
14751
15769
|
server.registerTool("export_events", {
|
|
14752
15770
|
annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true },
|
|
14753
15771
|
title: "Export events and installs",
|
|
14754
|
-
description: "Export one app's events and installs
|
|
15772
|
+
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
15773
|
inputSchema: {
|
|
14756
15774
|
app_id: requiredAppIdSchema,
|
|
14757
|
-
from:
|
|
14758
|
-
to:
|
|
15775
|
+
from: z5.string().describe("ISO date or timestamp"),
|
|
15776
|
+
to: z5.string().describe("ISO date or timestamp"),
|
|
15777
|
+
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."),
|
|
15778
|
+
format: z5.enum(["csv", "ndjson"]).optional().describe("Version 2 only: csv (the default) or ndjson, one JSON record per line."),
|
|
15779
|
+
limit: z5.number().int().min(1).max(1e4).optional().describe("Version 2 only: records per page, 1-10,000 (10,000 when omitted)."),
|
|
15780
|
+
cursor: z5.string().optional().describe("Version 2 only: the next_cursor of the previous page. Omit for the first page.")
|
|
14759
15781
|
}
|
|
14760
|
-
}, async ({ app_id, from, to }) =>
|
|
14761
|
-
|
|
14762
|
-
|
|
14763
|
-
|
|
15782
|
+
}, async ({ app_id, from, to, version, format, limit, cursor }) => {
|
|
15783
|
+
if (version !== 2 && (format !== undefined || limit !== undefined || cursor !== undefined)) {
|
|
15784
|
+
return toToolResult(invalidResult("invalid_request", "format, limit and cursor apply to version 2 only; pass version: 2."));
|
|
15785
|
+
}
|
|
15786
|
+
return toToolResult(await call("/api/v1/export", {
|
|
15787
|
+
query: {
|
|
15788
|
+
appId: app_id,
|
|
15789
|
+
from,
|
|
15790
|
+
to,
|
|
15791
|
+
version: version === undefined ? undefined : String(version),
|
|
15792
|
+
format,
|
|
15793
|
+
limit: limit === undefined ? undefined : String(limit),
|
|
15794
|
+
cursor
|
|
15795
|
+
},
|
|
15796
|
+
expectedResponse: format === "ndjson" ? "ndjson" : "text"
|
|
15797
|
+
}));
|
|
15798
|
+
});
|
|
15799
|
+
server.registerTool("send_server_event", {
|
|
15800
|
+
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: true },
|
|
15801
|
+
title: "Report an event from your backend",
|
|
15802
|
+
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.",
|
|
15803
|
+
inputSchema: {
|
|
15804
|
+
app_id: requiredAppIdSchema,
|
|
15805
|
+
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."),
|
|
15806
|
+
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."),
|
|
15807
|
+
event_name: z5.string().min(1).max(128).regex(/^[a-z][a-z0-9_.-]*$/).describe("Canonical event name, for example purchase or subscription_started."),
|
|
15808
|
+
event_version: z5.number().int().positive(),
|
|
15809
|
+
occurred_at: z5.string().datetime({ offset: true }).describe("When the event happened, ISO 8601 with a UTC offset."),
|
|
15810
|
+
consent: z5.object({
|
|
15811
|
+
measurement: z5.enum(["granted", "denied", "unknown"]),
|
|
15812
|
+
tracking: z5.enum(["granted", "denied", "unknown"]),
|
|
15813
|
+
policy_version: z5.number().int().positive(),
|
|
15814
|
+
dma: z5.object({
|
|
15815
|
+
eea: z5.boolean().describe("Whether EEA, UK or Swiss rules apply to this user."),
|
|
15816
|
+
ad_user_data: z5.boolean().optional().describe("The user's ad_user_data consent."),
|
|
15817
|
+
ad_personalization: z5.boolean().optional().describe("The user's ad_personalization consent.")
|
|
15818
|
+
}).strict().optional().describe("Optional. The user's answers to Google's EU consent questions, sent to Google with this event. A value left out is derived from tracking; an unknown key is refused, as the API refuses it.")
|
|
15819
|
+
}).describe("The per-event consent SDK events carry. An event whose measurement is not granted is not collected."),
|
|
15820
|
+
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."),
|
|
15821
|
+
idempotency_key: idempotencyKeySchema
|
|
15822
|
+
}
|
|
15823
|
+
}, async ({ app_id, customer_user_id, event_id, event_name, event_version, occurred_at, consent, properties, idempotency_key }) => {
|
|
15824
|
+
const forwardedConsent = {
|
|
15825
|
+
measurement: consent.measurement,
|
|
15826
|
+
tracking: consent.tracking,
|
|
15827
|
+
policy_version: consent.policy_version
|
|
15828
|
+
};
|
|
15829
|
+
if (consent.dma !== undefined) {
|
|
15830
|
+
const dma = { eea: consent.dma.eea, source: "manual" };
|
|
15831
|
+
if (consent.dma.ad_user_data !== undefined)
|
|
15832
|
+
dma.ad_user_data = consent.dma.ad_user_data;
|
|
15833
|
+
if (consent.dma.ad_personalization !== undefined)
|
|
15834
|
+
dma.ad_personalization = consent.dma.ad_personalization;
|
|
15835
|
+
forwardedConsent.dma = dma;
|
|
15836
|
+
}
|
|
15837
|
+
const body = { customer_user_id, event_id, event_name, event_version, occurred_at, consent: forwardedConsent };
|
|
15838
|
+
if (properties !== undefined)
|
|
15839
|
+
body.properties = properties;
|
|
15840
|
+
return toToolResult(await call(`/api/v1/apps/${encodeOpaquePathSegment(app_id)}/events`, {
|
|
15841
|
+
method: "POST",
|
|
15842
|
+
idempotencyKey: idempotency_key,
|
|
15843
|
+
body
|
|
15844
|
+
}));
|
|
15845
|
+
});
|
|
15846
|
+
server.registerTool("lookup_customer_attribution", {
|
|
15847
|
+
annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true },
|
|
15848
|
+
title: "Look up a user's attribution",
|
|
15849
|
+
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.",
|
|
15850
|
+
inputSchema: {
|
|
15851
|
+
app_id: requiredAppIdSchema,
|
|
15852
|
+
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.")
|
|
15853
|
+
}
|
|
15854
|
+
}, 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
15855
|
return server;
|
|
14765
15856
|
}
|
|
14766
15857
|
|
|
@@ -14771,8 +15862,7 @@ async function main() {
|
|
|
14771
15862
|
config = readConfig();
|
|
14772
15863
|
} catch (error) {
|
|
14773
15864
|
process.stderr.write(`${error instanceof Error ? error.message : String(error)}
|
|
14774
|
-
|
|
14775
|
-
process.exit(1);
|
|
15865
|
+
`, () => process.exit(1));
|
|
14776
15866
|
return;
|
|
14777
15867
|
}
|
|
14778
15868
|
const server = createServer(config);
|
|
@@ -14783,6 +15873,5 @@ async function main() {
|
|
|
14783
15873
|
}
|
|
14784
15874
|
main().catch((error) => {
|
|
14785
15875
|
process.stderr.write(`attrkit-mcp failed to start: ${error instanceof Error ? error.stack ?? error.message : String(error)}
|
|
14786
|
-
|
|
14787
|
-
process.exit(1);
|
|
15876
|
+
`, () => process.exit(1));
|
|
14788
15877
|
});
|