@portproof/mcp 0.0.0-stage → 0.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/LICENSE +21 -0
- package/README.md +144 -2
- package/dist/index.js +1550 -0
- package/llms.txt +40 -0
- package/package.json +56 -4
package/dist/index.js
ADDED
|
@@ -0,0 +1,1550 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
4
|
+
|
|
5
|
+
import { randomUUID } from "node:crypto";
|
|
6
|
+
|
|
7
|
+
import { isValid as isValidUlid, monotonicFactory, ulid as plainUlid } from "ulid";
|
|
8
|
+
var ID_PREFIXES = [
|
|
9
|
+
"org",
|
|
10
|
+
"usr",
|
|
11
|
+
"prj",
|
|
12
|
+
"key",
|
|
13
|
+
"site",
|
|
14
|
+
"host",
|
|
15
|
+
"car",
|
|
16
|
+
"sim",
|
|
17
|
+
"dev",
|
|
18
|
+
"prt",
|
|
19
|
+
"sub",
|
|
20
|
+
"rot",
|
|
21
|
+
"rcp",
|
|
22
|
+
"evt",
|
|
23
|
+
"whk",
|
|
24
|
+
"inc",
|
|
25
|
+
"inv",
|
|
26
|
+
"cmd",
|
|
27
|
+
"cus",
|
|
28
|
+
"ses",
|
|
29
|
+
"etk",
|
|
30
|
+
"sup",
|
|
31
|
+
"prod",
|
|
32
|
+
"fx",
|
|
33
|
+
"cart",
|
|
34
|
+
"ci",
|
|
35
|
+
"ord",
|
|
36
|
+
"oi",
|
|
37
|
+
"pay",
|
|
38
|
+
"wal",
|
|
39
|
+
"pool",
|
|
40
|
+
"cpn",
|
|
41
|
+
"aff",
|
|
42
|
+
"bt",
|
|
43
|
+
"aud",
|
|
44
|
+
"upp",
|
|
45
|
+
"led",
|
|
46
|
+
"wl"
|
|
47
|
+
];
|
|
48
|
+
var ULID_RE = /^[0-9A-HJKMNP-TV-Z]{26}$/;
|
|
49
|
+
var prefixSet = /* @__PURE__ */ new Set(ID_PREFIXES);
|
|
50
|
+
function isId(value, prefix) {
|
|
51
|
+
if (typeof value !== "string")
|
|
52
|
+
return false;
|
|
53
|
+
const idx = value.indexOf("_");
|
|
54
|
+
if (idx <= 0)
|
|
55
|
+
return false;
|
|
56
|
+
const p = value.slice(0, idx);
|
|
57
|
+
const body = value.slice(idx + 1);
|
|
58
|
+
if (prefix !== void 0 ? p !== prefix : !prefixSet.has(p))
|
|
59
|
+
return false;
|
|
60
|
+
return ULID_RE.test(body) && isValidUlid(body);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
var SKUS = ["lt-4g", "pl-4g", "uk-4g", "es-4g", "de-4g", "nl-4g", "us-4g", "trial-24h"];
|
|
64
|
+
var GIB = 1024 ** 3;
|
|
65
|
+
var DEFAULT_DAILY_CAP_BYTES = 20 * GIB;
|
|
66
|
+
var MAX_ALLOWLIST_CIDRS = 10;
|
|
67
|
+
var ROTATION_INTERVAL_MIN_S = 60;
|
|
68
|
+
var ROTATION_INTERVAL_MAX_S = 86400;
|
|
69
|
+
var TELEMETRY_HOURS_MIN = 72;
|
|
70
|
+
var EXCLUSIVITY = "modem+sim+credentials";
|
|
71
|
+
var RECEIPT_ALG = "Ed25519";
|
|
72
|
+
var WELL_KNOWN_KEYS_PATH = "/.well-known/portproof-keys.json";
|
|
73
|
+
var PRODUCT_KINDS = ["port", "bandwidth", "topup", "sms", "esim"];
|
|
74
|
+
var PRODUCT_TERMS = ["monthly", "weekly", "daily", "gb", "none"];
|
|
75
|
+
var CUSTOMER_SUPPLY_KINDS = ["owned", "generic_pool", "mock"];
|
|
76
|
+
var EVIDENCE_LEVELS = ["signed", "observed"];
|
|
77
|
+
var PORT_NETWORKS = ["own", "partner"];
|
|
78
|
+
var PAYMENT_METHODS = ["stripe", "coingate", "nowpayments", "wallet", "bank_transfer", "mock"];
|
|
79
|
+
var ORDER_STATUSES = [
|
|
80
|
+
"draft",
|
|
81
|
+
"awaiting_payment",
|
|
82
|
+
"awaiting_kyb",
|
|
83
|
+
"paid",
|
|
84
|
+
"provisioning",
|
|
85
|
+
"fulfilled",
|
|
86
|
+
"partially_fulfilled",
|
|
87
|
+
"failed_refunded",
|
|
88
|
+
"cancelled",
|
|
89
|
+
"refunded"
|
|
90
|
+
];
|
|
91
|
+
var TRAFFIC_TRIAL = { sku: "traffic-trial", gb: 0.5, price_cents: 290 };
|
|
92
|
+
var TRAFFIC_MOBILE_COUNTRIES = ["US", "GB", "FR", "NL", "PL", "GE"];
|
|
93
|
+
var TRAFFIC_PROTOCOLS = ["http", "socks5"];
|
|
94
|
+
|
|
95
|
+
import { z } from "zod";
|
|
96
|
+
var IsoDateTime = /* @__PURE__ */ z.string().datetime({ offset: true });
|
|
97
|
+
var CountryCode = /* @__PURE__ */ z.string().regex(/^[A-Z]{2}$/, "ISO-3166 alpha-2");
|
|
98
|
+
var MccMnc = /* @__PURE__ */ z.string().regex(/^\d{5,6}$/, "MCC+MNC");
|
|
99
|
+
var Cents = /* @__PURE__ */ z.number().int().min(0);
|
|
100
|
+
var Currency = /* @__PURE__ */ z.enum(["EUR", "USD"]);
|
|
101
|
+
var Cidr = /* @__PURE__ */ z.string().regex(/^(\d{1,3}\.){3}\d{1,3}(\/\d{1,2})?$|^[0-9a-fA-F:]+(\/\d{1,3})?$/, "IPv4/IPv6 CIDR");
|
|
102
|
+
function idSchema(prefix) {
|
|
103
|
+
return z.string().refine((v) => isId(v, prefix), { message: prefix ? `expected ${prefix}_<ULID>` : `expected <prefix>_<ULID>` });
|
|
104
|
+
}
|
|
105
|
+
var Operator = /* @__PURE__ */ z.enum(["owned", "partner"]);
|
|
106
|
+
var Evidence = /* @__PURE__ */ z.enum(EVIDENCE_LEVELS);
|
|
107
|
+
var IpStack = /* @__PURE__ */ z.enum(["v4", "v6_464xlat", "dual"]);
|
|
108
|
+
var ContractType = /* @__PURE__ */ z.enum(["prepaid", "business", "m2m"]);
|
|
109
|
+
var PortState = /* @__PURE__ */ z.enum([
|
|
110
|
+
"provisioning",
|
|
111
|
+
"available",
|
|
112
|
+
"reserved",
|
|
113
|
+
"sold",
|
|
114
|
+
"leased",
|
|
115
|
+
"migrating",
|
|
116
|
+
"quarantined",
|
|
117
|
+
"retired"
|
|
118
|
+
]);
|
|
119
|
+
var Term = /* @__PURE__ */ z.enum(["monthly", "weekly", "daily"]);
|
|
120
|
+
var PricingTerm = /* @__PURE__ */ z.enum(["monthly", "weekly", "daily", "hourly"]);
|
|
121
|
+
var SubscriptionState = /* @__PURE__ */ z.enum(["active", "grace", "suspended", "ended"]);
|
|
122
|
+
var RequestedBy = /* @__PURE__ */ z.enum(["manual", "api", "token", "schedule", "health", "migration"]);
|
|
123
|
+
var RotationReason = /* @__PURE__ */ z.enum(["ok", "carrier_sticky", "attach_timeout", "driver_error", "cooldown"]);
|
|
124
|
+
var SiteStatus = /* @__PURE__ */ z.enum(["provisioning", "live", "degraded", "offline"]);
|
|
125
|
+
var SkuSchema = /* @__PURE__ */ z.enum(SKUS);
|
|
126
|
+
var ProductSku = /* @__PURE__ */ z.string().regex(/^[a-z0-9][a-z0-9-]{1,63}$/, "lower-case slug");
|
|
127
|
+
var PortNetwork = /* @__PURE__ */ z.enum(PORT_NETWORKS);
|
|
128
|
+
var IncidentScope = /* @__PURE__ */ z.enum(["port", "site", "carrier", "platform"]);
|
|
129
|
+
var PlatformStatus = /* @__PURE__ */ z.enum(["ok", "degraded", "outage"]);
|
|
130
|
+
function paginated(item) {
|
|
131
|
+
return z.object({ data: z.array(item), next_cursor: z.string().nullable() });
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
import { z as z3 } from "zod";
|
|
135
|
+
|
|
136
|
+
import { z as z2 } from "zod";
|
|
137
|
+
var ProxyCredentialDto = /* @__PURE__ */ z2.object({
|
|
138
|
+
host: /* @__PURE__ */ z2.string().min(1),
|
|
139
|
+
port: /* @__PURE__ */ z2.number().int().min(1).max(65535),
|
|
140
|
+
username: /* @__PURE__ */ z2.string().min(1),
|
|
141
|
+
password: /* @__PURE__ */ z2.string().min(1)
|
|
142
|
+
});
|
|
143
|
+
var PortCapabilitiesDto = /* @__PURE__ */ z2.object({
|
|
144
|
+
replace: /* @__PURE__ */ z2.boolean(),
|
|
145
|
+
cancel: /* @__PURE__ */ z2.boolean(),
|
|
146
|
+
allowlist: /* @__PURE__ */ z2.boolean(),
|
|
147
|
+
auto_swap: /* @__PURE__ */ z2.boolean(),
|
|
148
|
+
rotation_interval: /* @__PURE__ */ z2.boolean(),
|
|
149
|
+
password_change: /* @__PURE__ */ z2.boolean()
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
var Subscription = /* @__PURE__ */ z3.object({
|
|
153
|
+
id: /* @__PURE__ */ idSchema("sub"),
|
|
154
|
+
org_id: /* @__PURE__ */ idSchema("org"),
|
|
155
|
+
port_id: /* @__PURE__ */ idSchema("prt"),
|
|
156
|
+
sku: /* @__PURE__ */ z3.string().min(1),
|
|
157
|
+
term: Term,
|
|
158
|
+
price_cents: Cents,
|
|
159
|
+
currency: Currency,
|
|
160
|
+
starts_at: IsoDateTime,
|
|
161
|
+
ends_at: IsoDateTime,
|
|
162
|
+
auto_renew: /* @__PURE__ */ z3.boolean(),
|
|
163
|
+
state: SubscriptionState,
|
|
164
|
+
provider_ref: /* @__PURE__ */ z3.string().nullable(),
|
|
165
|
+
created_at: IsoDateTime,
|
|
166
|
+
updated_at: IsoDateTime
|
|
167
|
+
});
|
|
168
|
+
var PortCarrier = /* @__PURE__ */ z3.object({
|
|
169
|
+
id: /* @__PURE__ */ idSchema("car"),
|
|
170
|
+
name: /* @__PURE__ */ z3.string().min(1),
|
|
171
|
+
mcc_mnc: MccMnc,
|
|
172
|
+
ip_stack: IpStack
|
|
173
|
+
});
|
|
174
|
+
var PortListeners = /* @__PURE__ */ z3.object({
|
|
175
|
+
host: /* @__PURE__ */ z3.string().min(1),
|
|
176
|
+
http_port: /* @__PURE__ */ z3.number().int().min(1).max(65535),
|
|
177
|
+
socks_port: /* @__PURE__ */ z3.number().int().min(1).max(65535)
|
|
178
|
+
});
|
|
179
|
+
var PortCredentials = /* @__PURE__ */ z3.object({
|
|
180
|
+
username: /* @__PURE__ */ z3.string().min(1),
|
|
181
|
+
password: /* @__PURE__ */ z3.string().min(1),
|
|
182
|
+
http: /* @__PURE__ */ ProxyCredentialDto.optional(),
|
|
183
|
+
socks5: /* @__PURE__ */ ProxyCredentialDto.optional()
|
|
184
|
+
});
|
|
185
|
+
var PortRenewal = /* @__PURE__ */ z3.object({
|
|
186
|
+
auto_renew: /* @__PURE__ */ z3.boolean(),
|
|
187
|
+
ends_at: IsoDateTime,
|
|
188
|
+
price_cents: Cents,
|
|
189
|
+
can_renew: /* @__PURE__ */ z3.boolean()
|
|
190
|
+
});
|
|
191
|
+
var UsageToday = /* @__PURE__ */ z3.object({ bytes: /* @__PURE__ */ z3.number().int().min(0), cap_bytes: /* @__PURE__ */ z3.number().int().min(0) });
|
|
192
|
+
var Port = /* @__PURE__ */ z3.object({
|
|
193
|
+
id: /* @__PURE__ */ idSchema("prt"),
|
|
194
|
+
state: PortState,
|
|
195
|
+
site_code: /* @__PURE__ */ z3.string().min(1),
|
|
196
|
+
country: CountryCode,
|
|
197
|
+
carrier: PortCarrier,
|
|
198
|
+
operator: Operator,
|
|
199
|
+
listeners: PortListeners,
|
|
200
|
+
credentials: /* @__PURE__ */ PortCredentials.nullable(),
|
|
201
|
+
allowlist: /* @__PURE__ */ z3.array(Cidr).max(MAX_ALLOWLIST_CIDRS),
|
|
202
|
+
egress_v4: /* @__PURE__ */ z3.string().nullable(),
|
|
203
|
+
egress_v6_prefix: /* @__PURE__ */ z3.string().nullable(),
|
|
204
|
+
v4_is_nat64: /* @__PURE__ */ z3.boolean(),
|
|
205
|
+
ip_since: /* @__PURE__ */ IsoDateTime.nullable(),
|
|
206
|
+
rotation_interval_s: /* @__PURE__ */ z3.number().int().min(0),
|
|
207
|
+
sticky_until: /* @__PURE__ */ IsoDateTime.nullable(),
|
|
208
|
+
cooldown_until: /* @__PURE__ */ IsoDateTime.nullable(),
|
|
209
|
+
last_rotation_at: /* @__PURE__ */ IsoDateTime.nullable(),
|
|
210
|
+
telemetry_since: /* @__PURE__ */ IsoDateTime.nullable(),
|
|
211
|
+
usage_today: UsageToday,
|
|
212
|
+
uptime_30d_pct: /* @__PURE__ */ z3.number().min(0).max(100).nullable(),
|
|
213
|
+
subscription: /* @__PURE__ */ Subscription.nullable(),
|
|
214
|
+
evidence: Evidence,
|
|
215
|
+
attestation: Evidence,
|
|
216
|
+
network: /* @__PURE__ */ PortNetwork.optional(),
|
|
217
|
+
capabilities: /* @__PURE__ */ PortCapabilitiesDto.nullable().optional(),
|
|
218
|
+
renewal: /* @__PURE__ */ PortRenewal.nullable().optional(),
|
|
219
|
+
rotation_url: /* @__PURE__ */ z3.string().url().nullable().optional()
|
|
220
|
+
});
|
|
221
|
+
var RotatePendingResponse = /* @__PURE__ */ z3.object({ rotation_id: /* @__PURE__ */ idSchema("rot"), status: /* @__PURE__ */ z3.literal("pending") });
|
|
222
|
+
var RotationScheduleRequest = /* @__PURE__ */ z3.object({
|
|
223
|
+
interval_s: /* @__PURE__ */ z3.number().int().refine((v) => v === 0 || v >= ROTATION_INTERVAL_MIN_S && v <= ROTATION_INTERVAL_MAX_S, {
|
|
224
|
+
message: `interval_s must be 0 (sticky) or ${ROTATION_INTERVAL_MIN_S}..${ROTATION_INTERVAL_MAX_S}`
|
|
225
|
+
})
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
import { z as z4 } from "zod";
|
|
229
|
+
var Pct = /* @__PURE__ */ z4.number().min(0).max(100);
|
|
230
|
+
var PassportCarrier = /* @__PURE__ */ z4.object({
|
|
231
|
+
name: /* @__PURE__ */ z4.string().min(1),
|
|
232
|
+
mcc_mnc: MccMnc,
|
|
233
|
+
asn: /* @__PURE__ */ z4.string().nullable(),
|
|
234
|
+
gateway_region: /* @__PURE__ */ z4.string().nullable(),
|
|
235
|
+
ip_stack: IpStack
|
|
236
|
+
});
|
|
237
|
+
var PassportEgress = /* @__PURE__ */ z4.object({
|
|
238
|
+
v4: /* @__PURE__ */ z4.string().nullable(),
|
|
239
|
+
v6_prefix: /* @__PURE__ */ z4.string().nullable(),
|
|
240
|
+
v4_is_nat64: /* @__PURE__ */ z4.boolean(),
|
|
241
|
+
ip_since: IsoDateTime
|
|
242
|
+
});
|
|
243
|
+
var GeoSource = /* @__PURE__ */ z4.object({
|
|
244
|
+
provider: /* @__PURE__ */ z4.string().min(1),
|
|
245
|
+
country: /* @__PURE__ */ z4.string().nullable(),
|
|
246
|
+
region: /* @__PURE__ */ z4.string().nullable(),
|
|
247
|
+
city: /* @__PURE__ */ z4.string().nullable()
|
|
248
|
+
});
|
|
249
|
+
var PassportGeo = /* @__PURE__ */ z4.object({
|
|
250
|
+
country: /* @__PURE__ */ z4.string().nullable(),
|
|
251
|
+
region: /* @__PURE__ */ z4.string().nullable(),
|
|
252
|
+
city: /* @__PURE__ */ z4.string().nullable(),
|
|
253
|
+
sources: /* @__PURE__ */ z4.array(GeoSource)
|
|
254
|
+
});
|
|
255
|
+
var PassportReputation = /* @__PURE__ */ z4.object({
|
|
256
|
+
flagged: /* @__PURE__ */ z4.boolean(),
|
|
257
|
+
hosting: /* @__PURE__ */ z4.boolean(),
|
|
258
|
+
providers: /* @__PURE__ */ z4.array(/* @__PURE__ */ z4.string()),
|
|
259
|
+
scored_at: /* @__PURE__ */ IsoDateTime.nullable()
|
|
260
|
+
});
|
|
261
|
+
var PassportRotation = /* @__PURE__ */ z4.object({
|
|
262
|
+
interval_s: /* @__PURE__ */ z4.number().int().min(0),
|
|
263
|
+
sticky_until: /* @__PURE__ */ IsoDateTime.nullable(),
|
|
264
|
+
last_rotation_at: /* @__PURE__ */ IsoDateTime.nullable(),
|
|
265
|
+
last_30d: /* @__PURE__ */ z4.object({
|
|
266
|
+
count: /* @__PURE__ */ z4.number().int().min(0),
|
|
267
|
+
first_attempt_success_pct: /* @__PURE__ */ Pct.nullable(),
|
|
268
|
+
p50_ms: /* @__PURE__ */ z4.number().min(0).nullable()
|
|
269
|
+
})
|
|
270
|
+
});
|
|
271
|
+
var Passport = /* @__PURE__ */ z4.object({
|
|
272
|
+
port_id: /* @__PURE__ */ idSchema("prt"),
|
|
273
|
+
generated_at: IsoDateTime,
|
|
274
|
+
site_code: /* @__PURE__ */ z4.string().min(1),
|
|
275
|
+
country: CountryCode,
|
|
276
|
+
operator: Operator,
|
|
277
|
+
evidence: /* @__PURE__ */ z4.enum(["signed", "observed"]).optional(),
|
|
278
|
+
network: /* @__PURE__ */ PortNetwork.optional(),
|
|
279
|
+
carrier: PassportCarrier,
|
|
280
|
+
sim_contract_class: ContractType,
|
|
281
|
+
egress: PassportEgress,
|
|
282
|
+
geo: PassportGeo,
|
|
283
|
+
reputation: PassportReputation,
|
|
284
|
+
rotation: PassportRotation,
|
|
285
|
+
uptime: /* @__PURE__ */ z4.object({
|
|
286
|
+
d30_pct: /* @__PURE__ */ Pct.nullable(),
|
|
287
|
+
d90_pct: /* @__PURE__ */ Pct.nullable(),
|
|
288
|
+
probes_30d: /* @__PURE__ */ z4.number().int().min(0)
|
|
289
|
+
}),
|
|
290
|
+
throughput: /* @__PURE__ */ z4.object({
|
|
291
|
+
last_sample_mbps: /* @__PURE__ */ z4.number().min(0).nullable(),
|
|
292
|
+
p50_7d_mbps: /* @__PURE__ */ z4.number().min(0).nullable()
|
|
293
|
+
}),
|
|
294
|
+
usage: /* @__PURE__ */ z4.object({ bytes_today: /* @__PURE__ */ z4.number().int().min(0), cap_bytes: /* @__PURE__ */ z4.number().int().min(0) }),
|
|
295
|
+
sessions: /* @__PURE__ */ z4.object({ concurrent_now: /* @__PURE__ */ z4.number().int().min(0), peak_24h: /* @__PURE__ */ z4.number().int().min(0) }),
|
|
296
|
+
ip_shared_with_own_ports: /* @__PURE__ */ z4.number().int().min(0),
|
|
297
|
+
exclusivity: /* @__PURE__ */ z4.literal(EXCLUSIVITY),
|
|
298
|
+
receipts_url: /* @__PURE__ */ z4.string().url()
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
import { z as z5 } from "zod";
|
|
302
|
+
var ReceiptGeo = /* @__PURE__ */ z5.object({
|
|
303
|
+
country: /* @__PURE__ */ z5.string().nullable(),
|
|
304
|
+
region: /* @__PURE__ */ z5.string().nullable(),
|
|
305
|
+
sources: /* @__PURE__ */ z5.number().int().min(0)
|
|
306
|
+
});
|
|
307
|
+
var ReceiptReputation = /* @__PURE__ */ z5.object({
|
|
308
|
+
flagged: /* @__PURE__ */ z5.boolean(),
|
|
309
|
+
hosting: /* @__PURE__ */ z5.boolean(),
|
|
310
|
+
scored_at: /* @__PURE__ */ IsoDateTime.nullable()
|
|
311
|
+
});
|
|
312
|
+
var ReceiptUnsigned = /* @__PURE__ */ z5.object({
|
|
313
|
+
id: /* @__PURE__ */ idSchema("rot"),
|
|
314
|
+
port_id: /* @__PURE__ */ idSchema("prt"),
|
|
315
|
+
site_code: /* @__PURE__ */ z5.string().min(1),
|
|
316
|
+
requested_by: RequestedBy,
|
|
317
|
+
old_ip4: /* @__PURE__ */ z5.string().nullable(),
|
|
318
|
+
new_ip4: /* @__PURE__ */ z5.string().nullable(),
|
|
319
|
+
old_ip6_prefix: /* @__PURE__ */ z5.string().nullable(),
|
|
320
|
+
new_ip6_prefix: /* @__PURE__ */ z5.string().nullable(),
|
|
321
|
+
v4_is_nat64: /* @__PURE__ */ z5.boolean(),
|
|
322
|
+
rotated: /* @__PURE__ */ z5.boolean(),
|
|
323
|
+
attempts: /* @__PURE__ */ z5.number().int().min(0),
|
|
324
|
+
elapsed_ms: /* @__PURE__ */ z5.number().int().min(0),
|
|
325
|
+
reason: RotationReason,
|
|
326
|
+
driver: /* @__PURE__ */ z5.string().min(1),
|
|
327
|
+
carrier: /* @__PURE__ */ z5.string().min(1),
|
|
328
|
+
mcc_mnc: MccMnc,
|
|
329
|
+
asn: /* @__PURE__ */ z5.string().nullable(),
|
|
330
|
+
geo: ReceiptGeo,
|
|
331
|
+
reputation: /* @__PURE__ */ ReceiptReputation.nullable(),
|
|
332
|
+
sticky_window_s: /* @__PURE__ */ z5.number().int().min(0),
|
|
333
|
+
ip_shared_with_own_ports: /* @__PURE__ */ z5.number().int().min(0),
|
|
334
|
+
sim_contract_class: ContractType,
|
|
335
|
+
signed_at: IsoDateTime,
|
|
336
|
+
key_id: /* @__PURE__ */ z5.string().regex(/^[a-z0-9-]+-\d{4}$/, "<site_code>-<yyyy>"),
|
|
337
|
+
attestation: /* @__PURE__ */ z5.enum(["signed", "observed"]).optional()
|
|
338
|
+
});
|
|
339
|
+
var Receipt = /* @__PURE__ */ ReceiptUnsigned.extend({
|
|
340
|
+
sig: /* @__PURE__ */ z5.string().regex(/^[A-Za-z0-9+/]+={0,2}$/, "base64").min(86)
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
import { z as z9 } from "zod";
|
|
344
|
+
|
|
345
|
+
import { z as z8 } from "zod";
|
|
346
|
+
|
|
347
|
+
import { z as z6 } from "zod";
|
|
348
|
+
var ProductKind = /* @__PURE__ */ z6.enum(PRODUCT_KINDS);
|
|
349
|
+
var ProductTerm = /* @__PURE__ */ z6.enum(PRODUCT_TERMS);
|
|
350
|
+
var CustomerSupplyKind = /* @__PURE__ */ z6.enum(CUSTOMER_SUPPLY_KINDS);
|
|
351
|
+
var SupplyHealth = /* @__PURE__ */ z6.enum(["ok", "degraded", "down", "unknown"]);
|
|
352
|
+
var OrderStatus = /* @__PURE__ */ z6.enum(ORDER_STATUSES);
|
|
353
|
+
var OrderItemStatus = /* @__PURE__ */ z6.enum(["pending", "provisioning", "fulfilled", "failed", "refunded", "held"]);
|
|
354
|
+
var PaymentMethod = /* @__PURE__ */ z6.enum(PAYMENT_METHODS);
|
|
355
|
+
var PriceTier = /* @__PURE__ */ z6.object({ min_qty: /* @__PURE__ */ z6.number().int().min(1).max(1e5), unit_cents: Cents });
|
|
356
|
+
var SkuSnapshot = /* @__PURE__ */ z6.object({
|
|
357
|
+
sku: ProductSku,
|
|
358
|
+
kind: ProductKind,
|
|
359
|
+
title: /* @__PURE__ */ z6.string().min(1),
|
|
360
|
+
country: /* @__PURE__ */ CountryCode.nullable(),
|
|
361
|
+
carrier_hint: /* @__PURE__ */ z6.string().nullable(),
|
|
362
|
+
term: ProductTerm,
|
|
363
|
+
sell_cents: Cents,
|
|
364
|
+
currency: Currency,
|
|
365
|
+
evidence: Evidence,
|
|
366
|
+
operator_label: /* @__PURE__ */ z6.string().min(1),
|
|
367
|
+
supply_kind: CustomerSupplyKind
|
|
368
|
+
});
|
|
369
|
+
|
|
370
|
+
import { z as z7 } from "zod";
|
|
371
|
+
|
|
372
|
+
var ORDER_NUMBER_RE = /^PP-\d{4}-\d{6,}$/;
|
|
373
|
+
|
|
374
|
+
var OrderNumber = /* @__PURE__ */ z7.string().regex(ORDER_NUMBER_RE, "PP-YYYY-NNNNNN");
|
|
375
|
+
var ANALYTICS_REF_RE = /^[0-9a-f]{20}$/;
|
|
376
|
+
var FulfilmentCredentials = /* @__PURE__ */ z7.object({
|
|
377
|
+
host: /* @__PURE__ */ z7.string().min(1),
|
|
378
|
+
http_port: /* @__PURE__ */ z7.number().int().positive().nullable(),
|
|
379
|
+
socks_port: /* @__PURE__ */ z7.number().int().positive().nullable(),
|
|
380
|
+
username: /* @__PURE__ */ z7.string().min(1),
|
|
381
|
+
password: /* @__PURE__ */ z7.string().min(1),
|
|
382
|
+
expires_at: /* @__PURE__ */ IsoDateTime.nullable(),
|
|
383
|
+
http: /* @__PURE__ */ ProxyCredentialDto.optional(),
|
|
384
|
+
socks5: /* @__PURE__ */ ProxyCredentialDto.optional()
|
|
385
|
+
});
|
|
386
|
+
var OrderItemUnit = /* @__PURE__ */ z7.object({
|
|
387
|
+
unit: /* @__PURE__ */ z7.number().int().min(1),
|
|
388
|
+
port_id: /* @__PURE__ */ idSchema("prt"),
|
|
389
|
+
resource_url: /* @__PURE__ */ z7.string().nullable(),
|
|
390
|
+
credentials: /* @__PURE__ */ FulfilmentCredentials.nullable()
|
|
391
|
+
});
|
|
392
|
+
var OrderItem = /* @__PURE__ */ z7.object({
|
|
393
|
+
id: /* @__PURE__ */ idSchema("oi"),
|
|
394
|
+
product_id: /* @__PURE__ */ idSchema("prod"),
|
|
395
|
+
sku_snapshot: SkuSnapshot,
|
|
396
|
+
quantity: /* @__PURE__ */ z7.number().int().min(1),
|
|
397
|
+
unit_cents: Cents,
|
|
398
|
+
total_cents: Cents,
|
|
399
|
+
status: OrderItemStatus,
|
|
400
|
+
fulfilment_ref: /* @__PURE__ */ z7.string().nullable(),
|
|
401
|
+
attempts: /* @__PURE__ */ z7.number().int().min(0),
|
|
402
|
+
last_error: /* @__PURE__ */ z7.string().nullable().optional(),
|
|
403
|
+
customer_reason: /* @__PURE__ */ z7.string().nullable().optional(),
|
|
404
|
+
resource_url: /* @__PURE__ */ z7.string().nullable(),
|
|
405
|
+
credentials: /* @__PURE__ */ FulfilmentCredentials.nullable(),
|
|
406
|
+
units: /* @__PURE__ */ z7.array(OrderItemUnit).optional(),
|
|
407
|
+
updated_at: IsoDateTime
|
|
408
|
+
});
|
|
409
|
+
var Order = /* @__PURE__ */ z7.object({
|
|
410
|
+
id: /* @__PURE__ */ idSchema("ord"),
|
|
411
|
+
number: OrderNumber,
|
|
412
|
+
customer_id: /* @__PURE__ */ idSchema("cus"),
|
|
413
|
+
org_id: /* @__PURE__ */ idSchema("org"),
|
|
414
|
+
status: OrderStatus,
|
|
415
|
+
subtotal_cents: Cents,
|
|
416
|
+
discount_cents: Cents,
|
|
417
|
+
vat_rate_bps: /* @__PURE__ */ z7.number().int().min(0).max(1e4),
|
|
418
|
+
vat_cents: Cents,
|
|
419
|
+
total_cents: Cents,
|
|
420
|
+
currency: Currency,
|
|
421
|
+
payment_method: PaymentMethod,
|
|
422
|
+
coupon_code: /* @__PURE__ */ z7.string().nullable(),
|
|
423
|
+
items: /* @__PURE__ */ z7.array(OrderItem),
|
|
424
|
+
paid_at: /* @__PURE__ */ IsoDateTime.nullable(),
|
|
425
|
+
notes: /* @__PURE__ */ z7.string().nullable(),
|
|
426
|
+
invoice_url: /* @__PURE__ */ z7.string().nullable(),
|
|
427
|
+
payment_url: /* @__PURE__ */ z7.string().url().nullable().optional(),
|
|
428
|
+
analytics_ref: /* @__PURE__ */ z7.string().regex(ANALYTICS_REF_RE).nullable().optional(),
|
|
429
|
+
created_at: IsoDateTime,
|
|
430
|
+
updated_at: IsoDateTime
|
|
431
|
+
});
|
|
432
|
+
|
|
433
|
+
var TrafficProtocolName = /* @__PURE__ */ z8.enum(TRAFFIC_PROTOCOLS);
|
|
434
|
+
var TrafficCredentials = /* @__PURE__ */ z8.object({
|
|
435
|
+
host: /* @__PURE__ */ z8.string().min(1),
|
|
436
|
+
http_port: /* @__PURE__ */ z8.number().int().positive(),
|
|
437
|
+
socks_port: /* @__PURE__ */ z8.number().int().positive(),
|
|
438
|
+
username_base: /* @__PURE__ */ z8.string().min(1),
|
|
439
|
+
password: /* @__PURE__ */ z8.string().min(1)
|
|
440
|
+
});
|
|
441
|
+
var TrafficStatus = /* @__PURE__ */ z8.enum(["none", "active", "exhausted", "expired", "suspended"]);
|
|
442
|
+
var TrafficLimits = /* @__PURE__ */ z8.object({
|
|
443
|
+
validity_days: /* @__PURE__ */ z8.number().int().min(0),
|
|
444
|
+
kyb_tier: /* @__PURE__ */ z8.string(),
|
|
445
|
+
t0_monthly_gb_cap: /* @__PURE__ */ z8.number().min(0),
|
|
446
|
+
gb_bought_30d: /* @__PURE__ */ z8.number().min(0),
|
|
447
|
+
gb_remaining_30d: /* @__PURE__ */ z8.number().min(0).nullable(),
|
|
448
|
+
trial_available: /* @__PURE__ */ z8.boolean(),
|
|
449
|
+
max_order_gb: /* @__PURE__ */ z8.number().int().min(1)
|
|
450
|
+
});
|
|
451
|
+
var TrafficInfo = /* @__PURE__ */ z8.object({
|
|
452
|
+
id: /* @__PURE__ */ idSchema("pool").nullable(),
|
|
453
|
+
status: TrafficStatus,
|
|
454
|
+
gb_total: /* @__PURE__ */ z8.number().min(0),
|
|
455
|
+
gb_used: /* @__PURE__ */ z8.number().min(0),
|
|
456
|
+
gb_left: /* @__PURE__ */ z8.number().min(0),
|
|
457
|
+
expires_at: /* @__PURE__ */ IsoDateTime.nullable(),
|
|
458
|
+
enabled: /* @__PURE__ */ z8.boolean(),
|
|
459
|
+
last_synced_at: /* @__PURE__ */ IsoDateTime.nullable(),
|
|
460
|
+
credentials: /* @__PURE__ */ TrafficCredentials.nullable(),
|
|
461
|
+
usage_daily: /* @__PURE__ */ z8.array(/* @__PURE__ */ z8.object({ date: /* @__PURE__ */ z8.string().regex(/^\d{4}-\d{2}-\d{2}$/), gb: /* @__PURE__ */ z8.number().min(0) })),
|
|
462
|
+
limits: TrafficLimits
|
|
463
|
+
});
|
|
464
|
+
var TrafficCountryRow = /* @__PURE__ */ z8.object({
|
|
465
|
+
country: /* @__PURE__ */ z8.string().length(2),
|
|
466
|
+
name: /* @__PURE__ */ z8.string().min(1),
|
|
467
|
+
online: /* @__PURE__ */ z8.number().int().min(0)
|
|
468
|
+
});
|
|
469
|
+
var TrafficCountries = /* @__PURE__ */ z8.object({
|
|
470
|
+
mobile: /* @__PURE__ */ z8.array(TrafficCountryRow),
|
|
471
|
+
residential: /* @__PURE__ */ z8.array(TrafficCountryRow),
|
|
472
|
+
totals: /* @__PURE__ */ z8.object({ mobile_online: /* @__PURE__ */ z8.number().int().min(0), residential_online: /* @__PURE__ */ z8.number().int().min(0), countries: /* @__PURE__ */ z8.number().int().min(0) }),
|
|
473
|
+
generated_at: IsoDateTime,
|
|
474
|
+
stale: /* @__PURE__ */ z8.boolean()
|
|
475
|
+
});
|
|
476
|
+
var BuildUrlResult = /* @__PURE__ */ z8.object({
|
|
477
|
+
host: /* @__PURE__ */ z8.string().min(1),
|
|
478
|
+
port: /* @__PURE__ */ z8.number().int().positive(),
|
|
479
|
+
protocol: TrafficProtocolName,
|
|
480
|
+
username: /* @__PURE__ */ z8.string().min(1),
|
|
481
|
+
session: /* @__PURE__ */ z8.string().nullable(),
|
|
482
|
+
url: /* @__PURE__ */ z8.string().min(1),
|
|
483
|
+
url_masked: /* @__PURE__ */ z8.string().min(1),
|
|
484
|
+
snippets: /* @__PURE__ */ z8.object({ curl: /* @__PURE__ */ z8.string(), python: /* @__PURE__ */ z8.string(), node: /* @__PURE__ */ z8.string(), playwright: /* @__PURE__ */ z8.string() })
|
|
485
|
+
});
|
|
486
|
+
var TrafficTestResult = /* @__PURE__ */ z8.object({
|
|
487
|
+
ok: /* @__PURE__ */ z8.boolean(),
|
|
488
|
+
exit_country: /* @__PURE__ */ z8.string().nullable(),
|
|
489
|
+
country_verified: /* @__PURE__ */ z8.boolean(),
|
|
490
|
+
exit_ip: /* @__PURE__ */ z8.string().nullable(),
|
|
491
|
+
latency_ms: /* @__PURE__ */ z8.number().int().min(0).nullable(),
|
|
492
|
+
error_code: /* @__PURE__ */ z8.string().optional(),
|
|
493
|
+
simulated: /* @__PURE__ */ z8.boolean().optional()
|
|
494
|
+
});
|
|
495
|
+
var TrafficBuyResponse = /* @__PURE__ */ z8.object({ order: Order, payment_id: /* @__PURE__ */ idSchema("pay") });
|
|
496
|
+
var TrafficPricing = /* @__PURE__ */ z8.object({
|
|
497
|
+
sku: /* @__PURE__ */ z8.string().min(1),
|
|
498
|
+
currency: /* @__PURE__ */ z8.literal("EUR"),
|
|
499
|
+
tiers: /* @__PURE__ */ z8.array(PriceTier),
|
|
500
|
+
packages_gb: /* @__PURE__ */ z8.array(/* @__PURE__ */ z8.number().int().min(1)),
|
|
501
|
+
max_order_gb: /* @__PURE__ */ z8.number().int().min(1),
|
|
502
|
+
validity_days: /* @__PURE__ */ z8.number().int().min(0),
|
|
503
|
+
t0_monthly_gb_cap: /* @__PURE__ */ z8.number().min(0),
|
|
504
|
+
trial: /* @__PURE__ */ z8.object({ sku: /* @__PURE__ */ z8.string().min(1), gb: /* @__PURE__ */ z8.number().positive(), price_cents: Cents }).nullable(),
|
|
505
|
+
available: /* @__PURE__ */ z8.boolean()
|
|
506
|
+
});
|
|
507
|
+
|
|
508
|
+
var PricingSku = /* @__PURE__ */ z9.object({
|
|
509
|
+
sku: /* @__PURE__ */ z9.string().min(1),
|
|
510
|
+
country: CountryCode,
|
|
511
|
+
operator: Operator,
|
|
512
|
+
term: PricingTerm,
|
|
513
|
+
price_cents: Cents,
|
|
514
|
+
currency: Currency,
|
|
515
|
+
floor_cents: /* @__PURE__ */ Cents.optional(),
|
|
516
|
+
evidence: Evidence,
|
|
517
|
+
buy_via: /* @__PURE__ */ z9.enum(["ports", "cart"]),
|
|
518
|
+
terms: /* @__PURE__ */ z9.object({ monthly: Cents, weekly: Cents, daily: Cents }).optional()
|
|
519
|
+
});
|
|
520
|
+
var LadderRung = /* @__PURE__ */ z9.object({ min_ports: /* @__PURE__ */ z9.number().int().min(1), discount_pct: /* @__PURE__ */ z9.number().min(0).max(100) });
|
|
521
|
+
var PricingResponse = /* @__PURE__ */ z9.object({
|
|
522
|
+
currency_note: /* @__PURE__ */ z9.string(),
|
|
523
|
+
updated_at: IsoDateTime,
|
|
524
|
+
skus: /* @__PURE__ */ z9.array(PricingSku),
|
|
525
|
+
ladder: /* @__PURE__ */ z9.array(LadderRung),
|
|
526
|
+
term_ratios: /* @__PURE__ */ z9.object({ weekly: /* @__PURE__ */ z9.number().min(0).max(1), daily: /* @__PURE__ */ z9.number().min(0).max(1) }),
|
|
527
|
+
trial: /* @__PURE__ */ z9.object({ price_cents: Cents, hours: /* @__PURE__ */ z9.number().int().min(1) }),
|
|
528
|
+
traffic: /* @__PURE__ */ TrafficPricing.nullable().optional()
|
|
529
|
+
});
|
|
530
|
+
var InventoryItem = /* @__PURE__ */ z9.object({
|
|
531
|
+
country: CountryCode,
|
|
532
|
+
site_code: /* @__PURE__ */ z9.string().min(1),
|
|
533
|
+
operator: Operator,
|
|
534
|
+
carrier: /* @__PURE__ */ z9.string().min(1),
|
|
535
|
+
carrier_id: /* @__PURE__ */ idSchema("car"),
|
|
536
|
+
available: /* @__PURE__ */ z9.number().int().min(0),
|
|
537
|
+
telemetry_hours_min: /* @__PURE__ */ z9.number().int().min(0).default(TELEMETRY_HOURS_MIN),
|
|
538
|
+
waitlist: /* @__PURE__ */ z9.boolean()
|
|
539
|
+
});
|
|
540
|
+
var InventoryResponse = /* @__PURE__ */ z9.object({ data: /* @__PURE__ */ z9.array(InventoryItem) });
|
|
541
|
+
var QuoteResponse = /* @__PURE__ */ z9.object({
|
|
542
|
+
unit_cents: Cents,
|
|
543
|
+
discount_pct: /* @__PURE__ */ z9.number().min(0).max(100),
|
|
544
|
+
total_cents: Cents,
|
|
545
|
+
currency: Currency
|
|
546
|
+
});
|
|
547
|
+
|
|
548
|
+
import { z as z10 } from "zod";
|
|
549
|
+
var Pct2 = /* @__PURE__ */ z10.number().min(0).max(100);
|
|
550
|
+
var Incident = /* @__PURE__ */ z10.object({
|
|
551
|
+
id: /* @__PURE__ */ idSchema("inc"),
|
|
552
|
+
scope: IncidentScope,
|
|
553
|
+
scope_id: /* @__PURE__ */ z10.string().nullable(),
|
|
554
|
+
title: /* @__PURE__ */ z10.string().min(1),
|
|
555
|
+
cause: /* @__PURE__ */ z10.string().nullable(),
|
|
556
|
+
started_at: IsoDateTime,
|
|
557
|
+
resolved_at: /* @__PURE__ */ IsoDateTime.nullable(),
|
|
558
|
+
sla_excluded: /* @__PURE__ */ z10.boolean(),
|
|
559
|
+
public: /* @__PURE__ */ z10.boolean()
|
|
560
|
+
});
|
|
561
|
+
var StatusSite = /* @__PURE__ */ z10.object({
|
|
562
|
+
code: /* @__PURE__ */ z10.string().min(1),
|
|
563
|
+
country: CountryCode,
|
|
564
|
+
operator: Operator,
|
|
565
|
+
status: SiteStatus,
|
|
566
|
+
ports_live: /* @__PURE__ */ z10.number().int().min(0),
|
|
567
|
+
uptime_30d_pct: /* @__PURE__ */ Pct2.nullable()
|
|
568
|
+
});
|
|
569
|
+
var StatusCarrier = /* @__PURE__ */ z10.object({
|
|
570
|
+
id: /* @__PURE__ */ idSchema("car"),
|
|
571
|
+
name: /* @__PURE__ */ z10.string().min(1),
|
|
572
|
+
country: CountryCode,
|
|
573
|
+
uptime_30d_pct: /* @__PURE__ */ Pct2.nullable(),
|
|
574
|
+
rotation_first_attempt_pct: /* @__PURE__ */ Pct2.nullable(),
|
|
575
|
+
rotation_p50_ms: /* @__PURE__ */ z10.number().min(0).nullable(),
|
|
576
|
+
rotation_floor_pct: /* @__PURE__ */ Pct2.nullable()
|
|
577
|
+
});
|
|
578
|
+
var StatusResponse = /* @__PURE__ */ z10.object({
|
|
579
|
+
updated_at: IsoDateTime,
|
|
580
|
+
platform: PlatformStatus,
|
|
581
|
+
sites: /* @__PURE__ */ z10.array(StatusSite),
|
|
582
|
+
carriers: /* @__PURE__ */ z10.array(StatusCarrier),
|
|
583
|
+
incidents: /* @__PURE__ */ z10.array(Incident),
|
|
584
|
+
support: /* @__PURE__ */ z10.object({ median_first_response_min_30d: /* @__PURE__ */ z10.number().min(0).nullable() }),
|
|
585
|
+
traffic: /* @__PURE__ */ z10.object({
|
|
586
|
+
health: /* @__PURE__ */ SupplyHealth.nullable(),
|
|
587
|
+
last_sync_at: /* @__PURE__ */ IsoDateTime.nullable(),
|
|
588
|
+
countries_generated_at: /* @__PURE__ */ IsoDateTime.nullable()
|
|
589
|
+
})
|
|
590
|
+
});
|
|
591
|
+
var WellKnownKey = /* @__PURE__ */ z10.object({
|
|
592
|
+
key_id: /* @__PURE__ */ z10.string().min(1),
|
|
593
|
+
site_code: /* @__PURE__ */ z10.string().min(1),
|
|
594
|
+
alg: /* @__PURE__ */ z10.literal(RECEIPT_ALG),
|
|
595
|
+
public_key_b64: /* @__PURE__ */ z10.string().min(1),
|
|
596
|
+
valid_from: IsoDateTime,
|
|
597
|
+
valid_to: IsoDateTime
|
|
598
|
+
});
|
|
599
|
+
var WellKnownKeys = /* @__PURE__ */ z10.object({ keys: /* @__PURE__ */ z10.array(WellKnownKey) });
|
|
600
|
+
|
|
601
|
+
import { z as z11 } from "zod";
|
|
602
|
+
var UsageGranularity = /* @__PURE__ */ z11.enum(["minute", "hour", "day"]);
|
|
603
|
+
var UsagePoint = /* @__PURE__ */ z11.object({
|
|
604
|
+
ts: IsoDateTime,
|
|
605
|
+
bytes_in: /* @__PURE__ */ z11.number().int().min(0),
|
|
606
|
+
bytes_out: /* @__PURE__ */ z11.number().int().min(0),
|
|
607
|
+
sessions_peak: /* @__PURE__ */ z11.number().int().min(0),
|
|
608
|
+
conns_new: /* @__PURE__ */ z11.number().int().min(0),
|
|
609
|
+
rotations: /* @__PURE__ */ z11.number().int().min(0)
|
|
610
|
+
});
|
|
611
|
+
var UsageResponse = /* @__PURE__ */ z11.object({ data: /* @__PURE__ */ z11.array(UsagePoint) });
|
|
612
|
+
|
|
613
|
+
import { z as z12 } from "zod";
|
|
614
|
+
var ProblemDetails = /* @__PURE__ */ z12.object({
|
|
615
|
+
type: /* @__PURE__ */ z12.string().url().or(/* @__PURE__ */ z12.literal("about:blank")),
|
|
616
|
+
title: /* @__PURE__ */ z12.string(),
|
|
617
|
+
status: /* @__PURE__ */ z12.number().int().min(100).max(599),
|
|
618
|
+
detail: /* @__PURE__ */ z12.string().optional(),
|
|
619
|
+
instance: /* @__PURE__ */ z12.string().optional(),
|
|
620
|
+
code: /* @__PURE__ */ z12.string().min(1),
|
|
621
|
+
retry_after: /* @__PURE__ */ z12.number().int().min(0).optional(),
|
|
622
|
+
errors: /* @__PURE__ */ z12.array(/* @__PURE__ */ z12.object({ path: /* @__PURE__ */ z12.string(), message: /* @__PURE__ */ z12.string() })).optional()
|
|
623
|
+
});
|
|
624
|
+
|
|
625
|
+
import { createPrivateKey, createPublicKey, generateKeyPairSync, sign, verify } from "node:crypto";
|
|
626
|
+
function canonicalize(value) {
|
|
627
|
+
return JSON.stringify(sortKeys(value));
|
|
628
|
+
}
|
|
629
|
+
function sortKeys(value) {
|
|
630
|
+
if (Array.isArray(value))
|
|
631
|
+
return value.map(sortKeys);
|
|
632
|
+
if (value !== null && typeof value === "object") {
|
|
633
|
+
const src = value;
|
|
634
|
+
const out = {};
|
|
635
|
+
for (const key of Object.keys(src).sort()) {
|
|
636
|
+
if (key === "sig")
|
|
637
|
+
continue;
|
|
638
|
+
const v = src[key];
|
|
639
|
+
if (v === void 0)
|
|
640
|
+
continue;
|
|
641
|
+
out[key] = sortKeys(v);
|
|
642
|
+
}
|
|
643
|
+
return out;
|
|
644
|
+
}
|
|
645
|
+
return value;
|
|
646
|
+
}
|
|
647
|
+
var SPKI_PREFIX = /* @__PURE__ */ Buffer.from("302a300506032b6570032100", "hex");
|
|
648
|
+
function toPublicKey(key) {
|
|
649
|
+
if (typeof key === "object" && !(key instanceof Uint8Array))
|
|
650
|
+
return key;
|
|
651
|
+
if (typeof key === "string" && key.includes("-----BEGIN"))
|
|
652
|
+
return createPublicKey(key);
|
|
653
|
+
const raw = typeof key === "string" ? Buffer.from(key, "base64") : Buffer.from(key);
|
|
654
|
+
if (raw.length === 32)
|
|
655
|
+
return createPublicKey({ key: Buffer.concat([SPKI_PREFIX, raw]), format: "der", type: "spki" });
|
|
656
|
+
return createPublicKey({ key: raw, format: "der", type: "spki" });
|
|
657
|
+
}
|
|
658
|
+
function verifyReceipt(receipt, publicKey) {
|
|
659
|
+
const parsed = Receipt.safeParse(receipt);
|
|
660
|
+
if (!parsed.success)
|
|
661
|
+
return false;
|
|
662
|
+
try {
|
|
663
|
+
const { sig, ...unsigned } = parsed.data;
|
|
664
|
+
return verify(null, Buffer.from(canonicalize(unsigned), "utf8"), toPublicKey(publicKey), Buffer.from(sig, "base64"));
|
|
665
|
+
} catch {
|
|
666
|
+
return false;
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
function verifyReceiptWithKeys(receipt, keys) {
|
|
670
|
+
const parsed = Receipt.safeParse(receipt);
|
|
671
|
+
if (!parsed.success)
|
|
672
|
+
return { ok: false, key_id: null, reason: "invalid_receipt" };
|
|
673
|
+
const key = keys.find((k) => k.key_id === parsed.data.key_id);
|
|
674
|
+
if (!key)
|
|
675
|
+
return { ok: false, key_id: parsed.data.key_id, reason: "unknown_key" };
|
|
676
|
+
const ok = verifyReceipt(parsed.data, key.public_key_b64);
|
|
677
|
+
return { ok, key_id: key.key_id, reason: ok ? "ok" : "bad_signature" };
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
var SERVER_NAME = "portproof";
|
|
681
|
+
var SERVER_VERSION = "0.1.0";
|
|
682
|
+
var USER_AGENT = `portproof-mcp/${SERVER_VERSION}`;
|
|
683
|
+
var DEFAULT_API_URL = "https://api.portproof.org";
|
|
684
|
+
var DEFAULT_TIMEOUT_MS = 3e4;
|
|
685
|
+
var ROTATE_TIMEOUT_MS = 2e5;
|
|
686
|
+
function normaliseBaseUrl(raw) {
|
|
687
|
+
const trimmed = (raw ?? "").trim().replace(/\/+$/, "");
|
|
688
|
+
const origin = trimmed === "" ? DEFAULT_API_URL : trimmed;
|
|
689
|
+
return origin.endsWith("/v1") ? origin : `${origin}/v1`;
|
|
690
|
+
}
|
|
691
|
+
function loadConfig(env = process.env) {
|
|
692
|
+
const key = env.PORTPROOF_API_KEY?.trim();
|
|
693
|
+
return {
|
|
694
|
+
baseUrl: normaliseBaseUrl(env.PORTPROOF_API_URL),
|
|
695
|
+
apiKey: key === void 0 || key === "" ? void 0 : key
|
|
696
|
+
};
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
var ApiProblemError = class _ApiProblemError extends Error {
|
|
700
|
+
body;
|
|
701
|
+
constructor(body) {
|
|
702
|
+
super(body.detail ? `${body.code}: ${body.detail}` : `${body.code}: ${body.title}`);
|
|
703
|
+
this.name = "ApiProblemError";
|
|
704
|
+
this.body = body;
|
|
705
|
+
}
|
|
706
|
+
get code() {
|
|
707
|
+
return this.body.code;
|
|
708
|
+
}
|
|
709
|
+
get status() {
|
|
710
|
+
return this.body.status;
|
|
711
|
+
}
|
|
712
|
+
static local(code, title, detail, status = 0) {
|
|
713
|
+
const body = { code, status, title };
|
|
714
|
+
if (detail !== void 0) body.detail = detail;
|
|
715
|
+
return new _ApiProblemError(body);
|
|
716
|
+
}
|
|
717
|
+
static fromResponse(status, json, rawText, url) {
|
|
718
|
+
const parsed = ProblemDetails.safeParse(json);
|
|
719
|
+
if (parsed.success) {
|
|
720
|
+
const p = parsed.data;
|
|
721
|
+
const body = { code: p.code, status: p.status, title: p.title };
|
|
722
|
+
if (p.detail !== void 0) body.detail = p.detail;
|
|
723
|
+
if (p.instance !== void 0) body.instance = p.instance;
|
|
724
|
+
if (p.retry_after !== void 0) body.retry_after = p.retry_after;
|
|
725
|
+
if (p.errors !== void 0) body.errors = p.errors;
|
|
726
|
+
return new _ApiProblemError(body);
|
|
727
|
+
}
|
|
728
|
+
if (json && typeof json === "object" && typeof json.code === "string") {
|
|
729
|
+
const j = json;
|
|
730
|
+
const body = {
|
|
731
|
+
code: j.code,
|
|
732
|
+
status,
|
|
733
|
+
title: typeof j.title === "string" ? j.title : `HTTP ${status}`
|
|
734
|
+
};
|
|
735
|
+
if (typeof j.detail === "string") body.detail = j.detail;
|
|
736
|
+
if (typeof j.retry_after === "number") body.retry_after = j.retry_after;
|
|
737
|
+
return new _ApiProblemError(body);
|
|
738
|
+
}
|
|
739
|
+
const snippet = rawText.length > 200 ? `${rawText.slice(0, 200)}\u2026` : rawText;
|
|
740
|
+
return new _ApiProblemError({
|
|
741
|
+
code: "http_error",
|
|
742
|
+
status,
|
|
743
|
+
title: `HTTP ${status}`,
|
|
744
|
+
detail: `Non-problem+json response from ${url}${snippet ? `: ${snippet}` : ""}`
|
|
745
|
+
});
|
|
746
|
+
}
|
|
747
|
+
static fromTransport(err, url, timeoutMs) {
|
|
748
|
+
const name = err instanceof Error ? err.name : "";
|
|
749
|
+
if (name === "TimeoutError" || name === "AbortError") {
|
|
750
|
+
return _ApiProblemError.local(
|
|
751
|
+
"timeout",
|
|
752
|
+
"API request timed out",
|
|
753
|
+
`${url} did not respond within ${timeoutMs} ms`
|
|
754
|
+
);
|
|
755
|
+
}
|
|
756
|
+
const cause = err instanceof Error ? err.cause instanceof Error ? err.cause.message : err.message : String(err);
|
|
757
|
+
return _ApiProblemError.local(
|
|
758
|
+
"api_unreachable",
|
|
759
|
+
"Portproof API unreachable",
|
|
760
|
+
`Could not reach ${url} (${cause}). Is the API running? Check PORTPROOF_API_URL.`
|
|
761
|
+
);
|
|
762
|
+
}
|
|
763
|
+
};
|
|
764
|
+
|
|
765
|
+
var PortPage = paginated(Port);
|
|
766
|
+
function apiOrigin(baseUrl) {
|
|
767
|
+
return baseUrl.replace(/\/+$/, "").replace(/\/v1$/, "");
|
|
768
|
+
}
|
|
769
|
+
var ApiClient = class {
|
|
770
|
+
baseUrl;
|
|
771
|
+
wellKnownKeysUrl;
|
|
772
|
+
hasApiKey;
|
|
773
|
+
apiKey;
|
|
774
|
+
fetchImpl;
|
|
775
|
+
timeoutMs;
|
|
776
|
+
warn;
|
|
777
|
+
constructor(opts) {
|
|
778
|
+
this.baseUrl = opts.baseUrl.replace(/\/+$/, "");
|
|
779
|
+
this.wellKnownKeysUrl = apiOrigin(this.baseUrl) + WELL_KNOWN_KEYS_PATH;
|
|
780
|
+
this.apiKey = opts.apiKey;
|
|
781
|
+
this.hasApiKey = Boolean(opts.apiKey);
|
|
782
|
+
this.fetchImpl = opts.fetchImpl ?? globalThis.fetch;
|
|
783
|
+
this.timeoutMs = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
784
|
+
this.warn = opts.warn ?? ((m) => console.error(`[portproof-mcp] ${m}`));
|
|
785
|
+
}
|
|
786
|
+
async request(method, path, opts = {}) {
|
|
787
|
+
const url = new URL((opts.root ? apiOrigin(this.baseUrl) : this.baseUrl) + path);
|
|
788
|
+
for (const [k, v] of Object.entries(opts.query ?? {})) {
|
|
789
|
+
if (v !== void 0) url.searchParams.set(k, String(v));
|
|
790
|
+
}
|
|
791
|
+
const headers = {
|
|
792
|
+
accept: "application/json, application/problem+json",
|
|
793
|
+
"user-agent": USER_AGENT
|
|
794
|
+
};
|
|
795
|
+
const auth = opts.auth ?? "required";
|
|
796
|
+
if (this.apiKey) headers.authorization = `Bearer ${this.apiKey}`;
|
|
797
|
+
else if (auth === "required") {
|
|
798
|
+
throw ApiProblemError.local(
|
|
799
|
+
"config_missing",
|
|
800
|
+
"PORTPROOF_API_KEY is not set",
|
|
801
|
+
"This tool needs a customer API key. Set PORTPROOF_API_KEY (pk_test_... or pk_live_...) in the MCP server environment."
|
|
802
|
+
);
|
|
803
|
+
}
|
|
804
|
+
if (opts.body !== void 0) headers["content-type"] = "application/json";
|
|
805
|
+
if (opts.idempotencyKey !== void 0) headers["idempotency-key"] = opts.idempotencyKey;
|
|
806
|
+
const timeoutMs = opts.timeoutMs ?? this.timeoutMs;
|
|
807
|
+
let res;
|
|
808
|
+
try {
|
|
809
|
+
res = await this.fetchImpl(url, {
|
|
810
|
+
method,
|
|
811
|
+
headers,
|
|
812
|
+
body: opts.body !== void 0 ? JSON.stringify(opts.body) : void 0,
|
|
813
|
+
signal: AbortSignal.timeout(timeoutMs)
|
|
814
|
+
});
|
|
815
|
+
} catch (err) {
|
|
816
|
+
throw ApiProblemError.fromTransport(err, url.toString(), timeoutMs);
|
|
817
|
+
}
|
|
818
|
+
const text = await res.text();
|
|
819
|
+
const json = parseJson(text);
|
|
820
|
+
if (!res.ok) throw ApiProblemError.fromResponse(res.status, json, text, url.toString());
|
|
821
|
+
return { status: res.status, data: json };
|
|
822
|
+
}
|
|
823
|
+
lenient(schema, data, label) {
|
|
824
|
+
const parsed = schema.safeParse(data);
|
|
825
|
+
if (parsed.success) return parsed.data;
|
|
826
|
+
const first = parsed.error.issues[0];
|
|
827
|
+
this.warn(
|
|
828
|
+
`${label}: response does not match the shared schema (${first ? `${first.path.join(".") || "<root>"}: ${first.message}` : "unknown issue"}); passing it through`
|
|
829
|
+
);
|
|
830
|
+
return data;
|
|
831
|
+
}
|
|
832
|
+
async getPricing() {
|
|
833
|
+
const r = await this.request("GET", "/pricing", { auth: "optional" });
|
|
834
|
+
return this.lenient(PricingResponse, r.data, "GET /pricing");
|
|
835
|
+
}
|
|
836
|
+
async getInventory(query) {
|
|
837
|
+
const r = await this.request("GET", "/inventory", { query, auth: "optional" });
|
|
838
|
+
return this.lenient(InventoryResponse, r.data, "GET /inventory");
|
|
839
|
+
}
|
|
840
|
+
async postQuote(body) {
|
|
841
|
+
const r = await this.request("POST", "/quotes", {
|
|
842
|
+
body,
|
|
843
|
+
auth: "optional",
|
|
844
|
+
idempotencyKey: `quote-${randomUUID()}`
|
|
845
|
+
});
|
|
846
|
+
return this.lenient(QuoteResponse, r.data, "POST /quotes");
|
|
847
|
+
}
|
|
848
|
+
async getStatus() {
|
|
849
|
+
const r = await this.request("GET", "/status", { auth: "optional" });
|
|
850
|
+
return this.lenient(StatusResponse, r.data, "GET /status");
|
|
851
|
+
}
|
|
852
|
+
async getWellKnownKeys() {
|
|
853
|
+
const r = await this.request("GET", WELL_KNOWN_KEYS_PATH, { auth: "optional", root: true });
|
|
854
|
+
return this.lenient(WellKnownKeys, r.data, `GET ${WELL_KNOWN_KEYS_PATH}`);
|
|
855
|
+
}
|
|
856
|
+
async listPorts(query) {
|
|
857
|
+
const r = await this.request("GET", "/ports", { query });
|
|
858
|
+
return this.lenient(PortPage, r.data, "GET /ports");
|
|
859
|
+
}
|
|
860
|
+
async getPort(portId) {
|
|
861
|
+
const r = await this.request("GET", `/ports/${encodeURIComponent(portId)}`);
|
|
862
|
+
return this.lenient(Port, r.data, "GET /ports/{id}");
|
|
863
|
+
}
|
|
864
|
+
async createPort(body, idempotencyKey) {
|
|
865
|
+
const r = await this.request("POST", "/ports", { body, idempotencyKey });
|
|
866
|
+
return { status: r.status, data: this.lenient(Port, r.data, "POST /ports") };
|
|
867
|
+
}
|
|
868
|
+
async rotatePort(portId, idempotencyKey) {
|
|
869
|
+
const r = await this.request("POST", `/ports/${encodeURIComponent(portId)}/rotate`, {
|
|
870
|
+
body: { wait: true },
|
|
871
|
+
idempotencyKey,
|
|
872
|
+
timeoutMs: ROTATE_TIMEOUT_MS
|
|
873
|
+
});
|
|
874
|
+
if (r.status === 202) {
|
|
875
|
+
return {
|
|
876
|
+
kind: "pending",
|
|
877
|
+
pending: this.lenient(RotatePendingResponse, r.data, "POST /ports/{id}/rotate 202")
|
|
878
|
+
};
|
|
879
|
+
}
|
|
880
|
+
return { kind: "receipt", receipt: this.lenient(Receipt, r.data, "POST /ports/{id}/rotate") };
|
|
881
|
+
}
|
|
882
|
+
async setRotationSchedule(portId, intervalS, idempotencyKey) {
|
|
883
|
+
const r = await this.request("PUT", `/ports/${encodeURIComponent(portId)}/rotation`, {
|
|
884
|
+
body: { interval_s: intervalS },
|
|
885
|
+
idempotencyKey
|
|
886
|
+
});
|
|
887
|
+
return this.lenient(Port, r.data, "PUT /ports/{id}/rotation");
|
|
888
|
+
}
|
|
889
|
+
async getPassport(portId) {
|
|
890
|
+
const r = await this.request("GET", `/ports/${encodeURIComponent(portId)}/passport`, {
|
|
891
|
+
query: { format: "json" }
|
|
892
|
+
});
|
|
893
|
+
return this.lenient(Passport, r.data, "GET /ports/{id}/passport");
|
|
894
|
+
}
|
|
895
|
+
async getReceipt(receiptId) {
|
|
896
|
+
const r = await this.request("GET", `/receipts/${encodeURIComponent(receiptId)}`);
|
|
897
|
+
return this.lenient(Receipt, r.data, "GET /receipts/{id}");
|
|
898
|
+
}
|
|
899
|
+
async getTraffic() {
|
|
900
|
+
const r = await this.request("GET", "/traffic");
|
|
901
|
+
return this.lenient(TrafficInfo, r.data, "GET /traffic");
|
|
902
|
+
}
|
|
903
|
+
async getTrafficCountries() {
|
|
904
|
+
const r = await this.request("GET", "/traffic/countries", { auth: "optional" });
|
|
905
|
+
return this.lenient(TrafficCountries, r.data, "GET /traffic/countries");
|
|
906
|
+
}
|
|
907
|
+
async buildProxyUrl(body) {
|
|
908
|
+
const r = await this.request("POST", "/traffic/build-url", { body });
|
|
909
|
+
return this.lenient(BuildUrlResult, r.data, "POST /traffic/build-url");
|
|
910
|
+
}
|
|
911
|
+
async testConnection(body) {
|
|
912
|
+
const r = await this.request("POST", "/traffic/test", { body });
|
|
913
|
+
return this.lenient(TrafficTestResult, r.data, "POST /traffic/test");
|
|
914
|
+
}
|
|
915
|
+
async buyTraffic(body, idempotencyKey) {
|
|
916
|
+
const r = await this.request("POST", "/traffic/buy", { body, idempotencyKey });
|
|
917
|
+
return this.lenient(TrafficBuyResponse, r.data, "POST /traffic/buy");
|
|
918
|
+
}
|
|
919
|
+
async getUsage(query) {
|
|
920
|
+
const r = await this.request("GET", "/usage", { query });
|
|
921
|
+
return this.lenient(UsageResponse, r.data, "GET /usage");
|
|
922
|
+
}
|
|
923
|
+
};
|
|
924
|
+
function parseJson(text) {
|
|
925
|
+
if (text.trim() === "") return void 0;
|
|
926
|
+
try {
|
|
927
|
+
return JSON.parse(text);
|
|
928
|
+
} catch {
|
|
929
|
+
return void 0;
|
|
930
|
+
}
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
934
|
+
|
|
935
|
+
import { z as z13 } from "zod";
|
|
936
|
+
var IdempotencyKey = z13.string().min(1).max(255).regex(/^[\x21-\x7e]+$/, "printable ASCII without whitespace");
|
|
937
|
+
var ConfirmFields = {
|
|
938
|
+
confirm: z13.boolean().describe(
|
|
939
|
+
"Must be exactly true. Set it only after the user has agreed to this specific action."
|
|
940
|
+
),
|
|
941
|
+
idempotency_key: IdempotencyKey.describe(
|
|
942
|
+
"Caller-chosen unique key (e.g. a UUID). Re-sending the same key with the same input replays the stored result instead of acting twice."
|
|
943
|
+
)
|
|
944
|
+
};
|
|
945
|
+
var Confirmation = z13.object(ConfirmFields);
|
|
946
|
+
var SearchInventoryInput = z13.object({
|
|
947
|
+
country: CountryCode.optional().describe('ISO-3166 alpha-2, e.g. "LT"'),
|
|
948
|
+
carrier: z13.string().min(1).optional().describe("Carrier name filter, exactly as search_inventory returns it")
|
|
949
|
+
});
|
|
950
|
+
var QuoteInput = z13.object({
|
|
951
|
+
sku: SkuSchema,
|
|
952
|
+
term: Term,
|
|
953
|
+
quantity: z13.number().int().min(1).max(1e3)
|
|
954
|
+
});
|
|
955
|
+
var BuyPortInput = z13.object({
|
|
956
|
+
sku: SkuSchema.describe('Price table SKU, e.g. "lt-4g" or "trial-24h"'),
|
|
957
|
+
term: Term,
|
|
958
|
+
carrier_id: idSchema("car").optional().describe("Pin a carrier (from search_inventory); omit for any"),
|
|
959
|
+
auto_renew: z13.boolean().optional(),
|
|
960
|
+
...ConfirmFields
|
|
961
|
+
});
|
|
962
|
+
var ListPortsInput = z13.object({
|
|
963
|
+
limit: z13.number().int().min(1).max(200).optional().describe("Page size, default 50"),
|
|
964
|
+
cursor: z13.string().min(1).optional().describe("next_cursor from the previous page")
|
|
965
|
+
});
|
|
966
|
+
var PortIdInput = z13.object({ port_id: idSchema("prt") });
|
|
967
|
+
var RotateInput = z13.object({
|
|
968
|
+
port_id: idSchema("prt"),
|
|
969
|
+
...ConfirmFields
|
|
970
|
+
});
|
|
971
|
+
var SetRotationScheduleInput = z13.object({
|
|
972
|
+
port_id: idSchema("prt"),
|
|
973
|
+
interval_s: z13.number().int().refine((v) => v === 0 || v >= ROTATION_INTERVAL_MIN_S && v <= ROTATION_INTERVAL_MAX_S, {
|
|
974
|
+
message: `interval_s must be 0 (sticky) or ${ROTATION_INTERVAL_MIN_S}..${ROTATION_INTERVAL_MAX_S}`
|
|
975
|
+
}).describe(
|
|
976
|
+
"0 = sticky (no scheduled rotation), otherwise seconds between rotations (60..86400)"
|
|
977
|
+
),
|
|
978
|
+
...ConfirmFields
|
|
979
|
+
});
|
|
980
|
+
var ReceiptIdInput = z13.object({
|
|
981
|
+
receipt_id: idSchema("rot").describe("Rotation record id (equals the rotation id, rot_...)")
|
|
982
|
+
});
|
|
983
|
+
var GetUsageInput = z13.object({
|
|
984
|
+
port_id: idSchema("prt"),
|
|
985
|
+
from: IsoDateTime.optional().describe("ISO 8601 UTC, default: API default window"),
|
|
986
|
+
to: IsoDateTime.optional(),
|
|
987
|
+
granularity: UsageGranularity.optional().describe("minute | hour | day (default hour)")
|
|
988
|
+
});
|
|
989
|
+
var EmptyInput = z13.object({});
|
|
990
|
+
var MOBILE_COUNTRIES_TEXT = TRAFFIC_MOBILE_COUNTRIES.map((c) => c === "GE" ? "GE = Georgia (country)" : c).join(", ");
|
|
991
|
+
var TrafficPoolField = z13.enum(["mobile", "residential", "best"]).describe(`mobile = 4G/5G carrier modems (${MOBILE_COUNTRIES_TEXT}); residential = opt-in peer devices in the countries list_countries reports online now; best = whichever is available`);
|
|
992
|
+
var TrafficCountryField = z13.string().regex(/^([a-z]{2}|any)$/i).describe('ISO-3166 alpha-2 (e.g. "GB") or "any"; see list_countries for what is online now');
|
|
993
|
+
var TrafficRotationField = z13.enum(["auto5", "auto10", "auto20", "auto60", "ondemand", "sticky"]).describe("auto5|auto10|auto20|auto60 = new IP every N minutes; ondemand = new IP on every connection; sticky = keep the same device for the session (the carrier may still change its IP)");
|
|
994
|
+
var BuildProxyUrlInput = z13.object({
|
|
995
|
+
pool: TrafficPoolField,
|
|
996
|
+
country: TrafficCountryField,
|
|
997
|
+
rotation: TrafficRotationField,
|
|
998
|
+
session: z13.string().regex(/^[a-z0-9_]{1,64}$/).optional().describe("Session name, a-z 0-9 _ only. Optional: a stable one is derived for sticky."),
|
|
999
|
+
protocol: z13.enum(["http", "socks5"]).optional().describe("Default http"),
|
|
1000
|
+
reveal_password: z13.boolean().optional().describe("Default false: the URL comes back with the password masked. Set true only when the user needs the working URL.")
|
|
1001
|
+
});
|
|
1002
|
+
var TestConnectionInput = z13.object({
|
|
1003
|
+
pool: TrafficPoolField.optional(),
|
|
1004
|
+
country: TrafficCountryField.optional(),
|
|
1005
|
+
rotation: TrafficRotationField.optional()
|
|
1006
|
+
});
|
|
1007
|
+
var BuyTrafficInput = z13.object({
|
|
1008
|
+
gb: z13.number().int().min(1).max(1e3).optional().describe("Whole GB to add to the balance. Omit when trial is true."),
|
|
1009
|
+
trial: z13.boolean().optional().describe("Buy the one-off 0.5 GB trial instead (once per customer)."),
|
|
1010
|
+
accept_terms: z13.boolean().optional().describe(
|
|
1011
|
+
"Must be exactly true: the user accepts the terms (https://portproof.org/legal/terms), the acceptable-use policy (https://portproof.org/legal/aup) and the immediate start of the service. Ask the user; the API refuses a purchase without it."
|
|
1012
|
+
),
|
|
1013
|
+
...ConfirmFields
|
|
1014
|
+
});
|
|
1015
|
+
var PortSummary = z13.object({
|
|
1016
|
+
id: idSchema("prt"),
|
|
1017
|
+
state: z13.string(),
|
|
1018
|
+
site_code: z13.string(),
|
|
1019
|
+
country: z13.string(),
|
|
1020
|
+
carrier: z13.string(),
|
|
1021
|
+
ip_stack: z13.string(),
|
|
1022
|
+
egress_v4: z13.string().nullable(),
|
|
1023
|
+
egress_v6_prefix: z13.string().nullable(),
|
|
1024
|
+
rotation_interval_s: z13.number().int(),
|
|
1025
|
+
last_rotation_at: z13.string().nullable(),
|
|
1026
|
+
usage_today_bytes: z13.number().int(),
|
|
1027
|
+
daily_cap_bytes: z13.number().int(),
|
|
1028
|
+
uptime_30d_pct: z13.number().nullable(),
|
|
1029
|
+
subscription_ends_at: z13.string().nullable(),
|
|
1030
|
+
operator: z13.enum(["owned", "partner"]),
|
|
1031
|
+
evidence: z13.enum(["signed", "observed"]),
|
|
1032
|
+
attestation: z13.enum(["signed", "observed"])
|
|
1033
|
+
});
|
|
1034
|
+
|
|
1035
|
+
function jsonResult(payload) {
|
|
1036
|
+
return { content: [{ type: "text", text: JSON.stringify(payload) }] };
|
|
1037
|
+
}
|
|
1038
|
+
function errorResult(body) {
|
|
1039
|
+
return { isError: true, content: [{ type: "text", text: JSON.stringify({ error: body }) }] };
|
|
1040
|
+
}
|
|
1041
|
+
function localError(code, title, detail) {
|
|
1042
|
+
return errorResult(ApiProblemError.local(code, title, detail).body);
|
|
1043
|
+
}
|
|
1044
|
+
function errorFromUnknown(err) {
|
|
1045
|
+
if (err instanceof ApiProblemError) return errorResult(err.body);
|
|
1046
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1047
|
+
return localError("unexpected_response", "Tool failed", message);
|
|
1048
|
+
}
|
|
1049
|
+
|
|
1050
|
+
var TOOL_NAMES = [
|
|
1051
|
+
"get_pricing",
|
|
1052
|
+
"search_inventory",
|
|
1053
|
+
"quote",
|
|
1054
|
+
"buy_port",
|
|
1055
|
+
"list_ports",
|
|
1056
|
+
"get_port",
|
|
1057
|
+
"rotate",
|
|
1058
|
+
"set_rotation_schedule",
|
|
1059
|
+
"get_passport",
|
|
1060
|
+
"get_receipt",
|
|
1061
|
+
"get_usage",
|
|
1062
|
+
"get_status",
|
|
1063
|
+
"get_traffic",
|
|
1064
|
+
"list_countries",
|
|
1065
|
+
"build_proxy_url",
|
|
1066
|
+
"buy_traffic",
|
|
1067
|
+
"test_connection"
|
|
1068
|
+
];
|
|
1069
|
+
|
|
1070
|
+
function defineTool(def) {
|
|
1071
|
+
return def;
|
|
1072
|
+
}
|
|
1073
|
+
var READ_ONLY = {
|
|
1074
|
+
readOnlyHint: true,
|
|
1075
|
+
destructiveHint: false,
|
|
1076
|
+
idempotentHint: true,
|
|
1077
|
+
openWorldHint: false
|
|
1078
|
+
};
|
|
1079
|
+
var MUTATING = {
|
|
1080
|
+
readOnlyHint: false,
|
|
1081
|
+
destructiveHint: false,
|
|
1082
|
+
idempotentHint: true,
|
|
1083
|
+
openWorldHint: false
|
|
1084
|
+
};
|
|
1085
|
+
|
|
1086
|
+
var getPassport = defineTool({
|
|
1087
|
+
name: "get_passport",
|
|
1088
|
+
title: "Get port details",
|
|
1089
|
+
description: "Port details (API object: passport): carrier, SIM contract class, egress IPs, geo consensus, reputation, rotation and uptime statistics, throughput, usage, concurrent sessions (proof the port is not shared) and exclusivity. Credentials are never included.",
|
|
1090
|
+
inputSchema: PortIdInput,
|
|
1091
|
+
annotations: READ_ONLY,
|
|
1092
|
+
run: async (args, ctx) => jsonResult(await ctx.api.getPassport(args.port_id))
|
|
1093
|
+
});
|
|
1094
|
+
async function verifySignature(receipt, ctx) {
|
|
1095
|
+
try {
|
|
1096
|
+
const { keys } = await ctx.api.getWellKnownKeys();
|
|
1097
|
+
const v = verifyReceiptWithKeys(receipt, keys);
|
|
1098
|
+
return {
|
|
1099
|
+
verified: v.ok,
|
|
1100
|
+
key_id: v.key_id,
|
|
1101
|
+
reason: v.reason,
|
|
1102
|
+
keys_url: ctx.api.wellKnownKeysUrl
|
|
1103
|
+
};
|
|
1104
|
+
} catch (err) {
|
|
1105
|
+
const detail = err instanceof Error ? err.message : String(err);
|
|
1106
|
+
return {
|
|
1107
|
+
verified: null,
|
|
1108
|
+
key_id: null,
|
|
1109
|
+
reason: `keys_unavailable: ${detail}`,
|
|
1110
|
+
keys_url: ctx.api.wellKnownKeysUrl
|
|
1111
|
+
};
|
|
1112
|
+
}
|
|
1113
|
+
}
|
|
1114
|
+
var getReceipt = defineTool({
|
|
1115
|
+
name: "get_receipt",
|
|
1116
|
+
title: "Get rotation record",
|
|
1117
|
+
description: "Fetch one rotation record from the rotation log by id (rot_...): old/new IPs, whether the IP actually changed, attempts, timing, carrier, geo and reputation snapshot, plus the Ed25519 signature. Records of Own network ports (evidence: signed) are signed on the device; records of Partner network ports (evidence: observed) are recorded by our monitoring and signed by the control plane key cp-2026. The server also verifies the signature against the published keys and reports the result.",
|
|
1118
|
+
inputSchema: ReceiptIdInput,
|
|
1119
|
+
annotations: READ_ONLY,
|
|
1120
|
+
run: async (args, ctx) => {
|
|
1121
|
+
const receipt = await ctx.api.getReceipt(args.receipt_id);
|
|
1122
|
+
const signature = await verifySignature(receipt, ctx);
|
|
1123
|
+
return jsonResult({ receipt, signature });
|
|
1124
|
+
}
|
|
1125
|
+
});
|
|
1126
|
+
|
|
1127
|
+
function checkConfirmation(args, action) {
|
|
1128
|
+
if (args.confirm !== true) {
|
|
1129
|
+
return localError(
|
|
1130
|
+
"confirmation_required",
|
|
1131
|
+
`Refused: ${action} needs confirm: true`,
|
|
1132
|
+
`${action} changes the account (money or live proxy state). Ask the user, then call again with confirm: true and an idempotency_key.`
|
|
1133
|
+
);
|
|
1134
|
+
}
|
|
1135
|
+
const key = IdempotencyKey.safeParse(args.idempotency_key);
|
|
1136
|
+
if (!key.success) {
|
|
1137
|
+
return localError(
|
|
1138
|
+
"idempotency_key_required",
|
|
1139
|
+
`Refused: ${action} needs an idempotency_key`,
|
|
1140
|
+
"Pass a caller-chosen unique string (1..255 printable ASCII chars, e.g. a UUID). Reuse the same key only to retry the identical request."
|
|
1141
|
+
);
|
|
1142
|
+
}
|
|
1143
|
+
return null;
|
|
1144
|
+
}
|
|
1145
|
+
|
|
1146
|
+
function summarisePort(p) {
|
|
1147
|
+
return {
|
|
1148
|
+
id: p.id,
|
|
1149
|
+
state: p.state,
|
|
1150
|
+
site_code: p.site_code,
|
|
1151
|
+
country: p.country,
|
|
1152
|
+
carrier: p.carrier.name,
|
|
1153
|
+
ip_stack: p.carrier.ip_stack,
|
|
1154
|
+
egress_v4: p.egress_v4,
|
|
1155
|
+
egress_v6_prefix: p.egress_v6_prefix,
|
|
1156
|
+
rotation_interval_s: p.rotation_interval_s,
|
|
1157
|
+
last_rotation_at: p.last_rotation_at,
|
|
1158
|
+
usage_today_bytes: p.usage_today.bytes,
|
|
1159
|
+
daily_cap_bytes: p.usage_today.cap_bytes,
|
|
1160
|
+
uptime_30d_pct: p.uptime_30d_pct,
|
|
1161
|
+
subscription_ends_at: p.subscription?.ends_at ?? null,
|
|
1162
|
+
operator: p.operator,
|
|
1163
|
+
evidence: p.evidence,
|
|
1164
|
+
attestation: p.attestation
|
|
1165
|
+
};
|
|
1166
|
+
}
|
|
1167
|
+
function proxyUrls(p) {
|
|
1168
|
+
if (!p.credentials) return null;
|
|
1169
|
+
const auth = `${encodeURIComponent(p.credentials.username)}:${encodeURIComponent(p.credentials.password)}`;
|
|
1170
|
+
return {
|
|
1171
|
+
http: `http://${auth}@${p.listeners.host}:${p.listeners.http_port}`,
|
|
1172
|
+
socks5: `socks5://${auth}@${p.listeners.host}:${p.listeners.socks_port}`
|
|
1173
|
+
};
|
|
1174
|
+
}
|
|
1175
|
+
async function costOf(port, input, ctx) {
|
|
1176
|
+
if (port.subscription) {
|
|
1177
|
+
return {
|
|
1178
|
+
cost_cents: port.subscription.price_cents,
|
|
1179
|
+
currency: port.subscription.currency,
|
|
1180
|
+
cost_source: "subscription"
|
|
1181
|
+
};
|
|
1182
|
+
}
|
|
1183
|
+
try {
|
|
1184
|
+
const q = await ctx.api.postQuote({ sku: input.sku, term: input.term, quantity: 1 });
|
|
1185
|
+
return { cost_cents: q.total_cents, currency: q.currency, cost_source: "quote" };
|
|
1186
|
+
} catch {
|
|
1187
|
+
return { cost_cents: null, currency: null, cost_source: "unknown" };
|
|
1188
|
+
}
|
|
1189
|
+
}
|
|
1190
|
+
var buyPort = defineTool({
|
|
1191
|
+
name: "buy_port",
|
|
1192
|
+
title: "Buy a port",
|
|
1193
|
+
description: "Dedicated ports (coming soon, not on sale yet; do not offer them): buy one dedicated mobile port (one modem, one SIM, one customer) for a SKU and term; check search_inventory first. For proxies by the GB use buy_traffic instead. CHARGES MONEY: requires confirm: true and an idempotency_key; refused otherwise. Fails with kyb_required (402) until the org has passed KYB tier t1, and no_inventory (409) when the SKU is sold out. Returns the Port with credentials and cost_cents.",
|
|
1194
|
+
inputSchema: BuyPortInput,
|
|
1195
|
+
annotations: MUTATING,
|
|
1196
|
+
run: async (args, ctx) => {
|
|
1197
|
+
const refusal = checkConfirmation(args, "buy_port");
|
|
1198
|
+
if (refusal) return refusal;
|
|
1199
|
+
const body = { sku: args.sku, term: args.term, quantity: 1 };
|
|
1200
|
+
if (args.carrier_id !== void 0) body.carrier_id = args.carrier_id;
|
|
1201
|
+
if (args.auto_renew !== void 0) body.auto_renew = args.auto_renew;
|
|
1202
|
+
const { data: port } = await ctx.api.createPort(body, args.idempotency_key);
|
|
1203
|
+
const cost = await costOf(port, args, ctx);
|
|
1204
|
+
return jsonResult({ ...cost, port, proxy_urls: proxyUrls(port) });
|
|
1205
|
+
}
|
|
1206
|
+
});
|
|
1207
|
+
var listPorts = defineTool({
|
|
1208
|
+
name: "list_ports",
|
|
1209
|
+
title: "List ports",
|
|
1210
|
+
description: "List the ports owned by this API key (compact summaries; use get_port for credentials and full detail). Paginated: pass next_cursor as cursor to continue.",
|
|
1211
|
+
inputSchema: ListPortsInput,
|
|
1212
|
+
annotations: READ_ONLY,
|
|
1213
|
+
run: async (args, ctx) => {
|
|
1214
|
+
const query = {};
|
|
1215
|
+
if (args.limit !== void 0) query.limit = args.limit;
|
|
1216
|
+
if (args.cursor !== void 0) query.cursor = args.cursor;
|
|
1217
|
+
const page = await ctx.api.listPorts(query);
|
|
1218
|
+
return jsonResult({ data: page.data.map(summarisePort), next_cursor: page.next_cursor });
|
|
1219
|
+
}
|
|
1220
|
+
});
|
|
1221
|
+
var getPort = defineTool({
|
|
1222
|
+
name: "get_port",
|
|
1223
|
+
title: "Get port",
|
|
1224
|
+
description: "Full detail for one port: state, carrier, listeners, credentials (only if you own it), allowlist, egress IPs, rotation settings, usage today, uptime and subscription. Also returns ready-to-use proxy_urls.",
|
|
1225
|
+
inputSchema: PortIdInput,
|
|
1226
|
+
annotations: READ_ONLY,
|
|
1227
|
+
run: async (args, ctx) => {
|
|
1228
|
+
const port = await ctx.api.getPort(args.port_id);
|
|
1229
|
+
return jsonResult({ port, proxy_urls: proxyUrls(port) });
|
|
1230
|
+
}
|
|
1231
|
+
});
|
|
1232
|
+
var rotate = defineTool({
|
|
1233
|
+
name: "rotate",
|
|
1234
|
+
title: "Rotate IP",
|
|
1235
|
+
description: "Rotate IP: request a new egress IP for a port and wait (up to 180 s) for the rotation record, signed on the device for Own network ports (evidence: signed) or recorded by our monitoring for Partner network ports (evidence: observed). Changes live proxy state: requires confirm: true and an idempotency_key; refused otherwise. Carriers sometimes return the same IP (rotated: false, reason carrier_sticky); a rotation record is written either way. 429 port_cooldown (with retry_after seconds) inside the 60 s cooldown. Included in the subscription: no charge.",
|
|
1236
|
+
inputSchema: RotateInput,
|
|
1237
|
+
annotations: MUTATING,
|
|
1238
|
+
run: async (args, ctx) => {
|
|
1239
|
+
const refusal = checkConfirmation(args, "rotate");
|
|
1240
|
+
if (refusal) return refusal;
|
|
1241
|
+
const result = await ctx.api.rotatePort(args.port_id, args.idempotency_key);
|
|
1242
|
+
if (result.kind === "pending") {
|
|
1243
|
+
return jsonResult({
|
|
1244
|
+
status: "pending",
|
|
1245
|
+
rotation_id: result.pending.rotation_id,
|
|
1246
|
+
port_id: args.port_id
|
|
1247
|
+
});
|
|
1248
|
+
}
|
|
1249
|
+
const r = result.receipt;
|
|
1250
|
+
return jsonResult({
|
|
1251
|
+
status: "done",
|
|
1252
|
+
rotated: r.rotated,
|
|
1253
|
+
reason: r.reason,
|
|
1254
|
+
old_ip4: r.old_ip4,
|
|
1255
|
+
new_ip4: r.new_ip4,
|
|
1256
|
+
elapsed_ms: r.elapsed_ms,
|
|
1257
|
+
receipt: r
|
|
1258
|
+
});
|
|
1259
|
+
}
|
|
1260
|
+
});
|
|
1261
|
+
var setRotationSchedule = defineTool({
|
|
1262
|
+
name: "set_rotation_schedule",
|
|
1263
|
+
title: "Set rotation schedule",
|
|
1264
|
+
description: "Set the automatic rotation interval for a port: 0 = sticky (never rotate on a schedule), otherwise 60..86400 seconds. Schedules run on the device even if the API is unreachable. Changes live proxy state: requires confirm: true and an idempotency_key; refused otherwise. No charge.",
|
|
1265
|
+
inputSchema: SetRotationScheduleInput,
|
|
1266
|
+
annotations: MUTATING,
|
|
1267
|
+
run: async (args, ctx) => {
|
|
1268
|
+
const refusal = checkConfirmation(args, "set_rotation_schedule");
|
|
1269
|
+
if (refusal) return refusal;
|
|
1270
|
+
const port = await ctx.api.setRotationSchedule(
|
|
1271
|
+
args.port_id,
|
|
1272
|
+
args.interval_s,
|
|
1273
|
+
args.idempotency_key
|
|
1274
|
+
);
|
|
1275
|
+
return jsonResult({
|
|
1276
|
+
port_id: port.id,
|
|
1277
|
+
rotation_interval_s: port.rotation_interval_s,
|
|
1278
|
+
sticky: port.rotation_interval_s === 0,
|
|
1279
|
+
sticky_until: port.sticky_until,
|
|
1280
|
+
port: summarisePort(port)
|
|
1281
|
+
});
|
|
1282
|
+
}
|
|
1283
|
+
});
|
|
1284
|
+
|
|
1285
|
+
var getPricing = defineTool({
|
|
1286
|
+
name: "get_pricing",
|
|
1287
|
+
title: "Get pricing",
|
|
1288
|
+
description: 'Public price table. Field traffic: proxy traffic by the GB (tier table with the per-GB price by order size in EUR cents ex VAT, package sizes, the 0.5 GB trial, validity days, monthly GB without company verification); buy it with buy_traffic. Field skus: dedicated ports (coming soon, not on sale yet) with monthly price in cents (EUR, USD for us-4g), supply (evidence signed = Own network, observed = Partner network), volume ladder, weekly/daily term ratios and the 24 h trial. Each SKU says how it is bought (buy_via): "ports" SKUs work with quote and buy_port; "cart" SKUs are Partner network ports sold only through the web shop and cannot be quoted or bought here. No API key needed. Prices are quotes only; nothing is charged.',
|
|
1289
|
+
inputSchema: EmptyInput,
|
|
1290
|
+
annotations: READ_ONLY,
|
|
1291
|
+
run: async (_args, ctx) => jsonResult(await ctx.api.getPricing())
|
|
1292
|
+
});
|
|
1293
|
+
var searchInventory = defineTool({
|
|
1294
|
+
name: "search_inventory",
|
|
1295
|
+
title: "Search inventory",
|
|
1296
|
+
description: "Dedicated ports (coming soon, not on sale yet): available dedicated mobile ports per country / site / carrier, with the number available and whether new ports are wait-listed. Use the returned carrier_id to pin a carrier in buy_port. No API key needed.",
|
|
1297
|
+
inputSchema: SearchInventoryInput,
|
|
1298
|
+
annotations: READ_ONLY,
|
|
1299
|
+
run: async (args, ctx) => {
|
|
1300
|
+
const query = {};
|
|
1301
|
+
if (args.country !== void 0) query.country = args.country;
|
|
1302
|
+
if (args.carrier !== void 0) query.carrier = args.carrier;
|
|
1303
|
+
return jsonResult(await ctx.api.getInventory(query));
|
|
1304
|
+
}
|
|
1305
|
+
});
|
|
1306
|
+
var quote = defineTool({
|
|
1307
|
+
name: "quote",
|
|
1308
|
+
title: "Quote a purchase",
|
|
1309
|
+
description: "Price a purchase before buying: unit price, volume discount and total in cents for a SKU, term (monthly | weekly | daily) and quantity. Read-only, nothing is charged.",
|
|
1310
|
+
inputSchema: QuoteInput,
|
|
1311
|
+
annotations: READ_ONLY,
|
|
1312
|
+
run: async (args, ctx) => {
|
|
1313
|
+
const q = await ctx.api.postQuote({ sku: args.sku, term: args.term, quantity: args.quantity });
|
|
1314
|
+
return jsonResult({
|
|
1315
|
+
...q,
|
|
1316
|
+
sku: args.sku,
|
|
1317
|
+
term: args.term,
|
|
1318
|
+
quantity: args.quantity,
|
|
1319
|
+
charged: false
|
|
1320
|
+
});
|
|
1321
|
+
}
|
|
1322
|
+
});
|
|
1323
|
+
|
|
1324
|
+
var getStatus = defineTool({
|
|
1325
|
+
name: "get_status",
|
|
1326
|
+
title: "Get platform status",
|
|
1327
|
+
description: "Public status page data: platform state, per-site uptime and live port counts, per-carrier uptime / first-attempt rotation success / p50 rotation time / published rotation floor, public incidents from the last 30 days and support response time. No API key needed.",
|
|
1328
|
+
inputSchema: EmptyInput,
|
|
1329
|
+
annotations: READ_ONLY,
|
|
1330
|
+
run: async (_args, ctx) => jsonResult(await ctx.api.getStatus())
|
|
1331
|
+
});
|
|
1332
|
+
|
|
1333
|
+
var MASK = "pak_****";
|
|
1334
|
+
var getTraffic = defineTool({
|
|
1335
|
+
name: "get_traffic",
|
|
1336
|
+
title: "Get the traffic balance",
|
|
1337
|
+
description: "Proxy traffic balance of the organisation: GB total / used / left, expiry, whether the key is enabled, daily usage and the purchase limits (validity days, monthly GB without company verification, whether the trial is still available). Connection details (host, ports, username base) are included when the API key holds ports:write (credentials is null otherwise); the password is masked here, use build_proxy_url with reveal_password: true when the user needs a working URL. Usage is synced every 5 minutes.",
|
|
1338
|
+
inputSchema: EmptyInput,
|
|
1339
|
+
annotations: READ_ONLY,
|
|
1340
|
+
run: async (_args, ctx) => {
|
|
1341
|
+
const t = await ctx.api.getTraffic();
|
|
1342
|
+
return jsonResult({ ...t, credentials: t.credentials ? { ...t.credentials, password: MASK } : null });
|
|
1343
|
+
}
|
|
1344
|
+
});
|
|
1345
|
+
var listCountries = defineTool({
|
|
1346
|
+
name: "list_countries",
|
|
1347
|
+
title: "List countries online now",
|
|
1348
|
+
description: `Countries with devices online now, per pool: mobile (4G/5G carrier modems, only in ${MOBILE_COUNTRIES_TEXT}) and residential (opt-in peer devices), with totals. Coverage changes through the day: quote the numbers this tool returns and never an older or rounded count. Counts only; exit addresses are never listed. Empty lists mean the numbers are not available right now. No API key needed.`,
|
|
1349
|
+
inputSchema: EmptyInput,
|
|
1350
|
+
annotations: READ_ONLY,
|
|
1351
|
+
run: async (_args, ctx) => jsonResult(await ctx.api.getTrafficCountries())
|
|
1352
|
+
});
|
|
1353
|
+
var buildProxyUrl = defineTool({
|
|
1354
|
+
name: "build_proxy_url",
|
|
1355
|
+
title: "Build a proxy URL",
|
|
1356
|
+
description: `Build the proxy username and URL for one connection: pool, country, rotation and an optional session name are encoded in the username (validated by the server: an ISO 3166-1 alpha-2 country or "any", and the mobile pool only in ${MOBILE_COUNTRIES_TEXT}). Returns host, port, username, the URL and curl / Python / Node / Playwright snippets. The password is masked unless reveal_password is true. A sticky session keeps the same device, and the carrier may still change its IP; never promise a fixed IP. Needs a traffic balance (buy_traffic first) and an API key with ports:write.`,
|
|
1357
|
+
inputSchema: BuildProxyUrlInput,
|
|
1358
|
+
annotations: READ_ONLY,
|
|
1359
|
+
run: async (args, ctx) => {
|
|
1360
|
+
const { reveal_password, ...body } = args;
|
|
1361
|
+
const built = await ctx.api.buildProxyUrl(body);
|
|
1362
|
+
if (reveal_password === true) return jsonResult({ ...built, password_revealed: true });
|
|
1363
|
+
const secret = /:([^:@/]+)@/.exec(built.url)?.[1];
|
|
1364
|
+
const variants = secret ? [secret, decodeURIComponent(secret)] : [];
|
|
1365
|
+
const mask = (text) => variants.reduce((t, v) => v ? t.split(v).join(MASK) : t, text);
|
|
1366
|
+
const snippets = Object.fromEntries(Object.entries(built.snippets).map(([k, v]) => [k, mask(v)]));
|
|
1367
|
+
return jsonResult({ ...built, url: built.url_masked, snippets, password_revealed: false });
|
|
1368
|
+
}
|
|
1369
|
+
});
|
|
1370
|
+
var buyTraffic = defineTool({
|
|
1371
|
+
name: "buy_traffic",
|
|
1372
|
+
title: "Buy proxy traffic",
|
|
1373
|
+
description: "Buy whole GB of proxy traffic (or the one-off 0.5 GB trial) from the wallet balance. CHARGES MONEY: requires confirm: true, accept_terms: true (the user accepts the terms, the acceptable-use policy and the immediate start of the service; ask them) and an idempotency_key; refused otherwise. Needs an API key with ports:write. The server prices the order from the tier table (see get_pricing, field traffic); the GB are added to the same balance and credentials. Validity is what get_traffic reports: limits.validity_days 0 means GB never expire. Fails with insufficient_funds (402) when the wallet is too low, kyb_required_for_volume (402) above the monthly volume for unverified organisations, trial_already_used (409), idempotency_conflict (409) when the idempotency_key was already used for a different purchase. A refused purchase stays stored under its idempotency_key for 24 h: after fixing the cause (for example a wallet top-up), buy with a new key. Returns the order with gb and cost_cents as the server recorded them.",
|
|
1374
|
+
inputSchema: BuyTrafficInput,
|
|
1375
|
+
annotations: MUTATING,
|
|
1376
|
+
run: async (args, ctx) => {
|
|
1377
|
+
const refusal = checkConfirmation(args, "buy_traffic");
|
|
1378
|
+
if (refusal) return refusal;
|
|
1379
|
+
if (args.accept_terms !== true) {
|
|
1380
|
+
return localError(
|
|
1381
|
+
"terms_acceptance_required",
|
|
1382
|
+
"Refused: buy_traffic needs accept_terms: true",
|
|
1383
|
+
"Ask the user to accept the terms (https://portproof.org/legal/terms), the acceptable-use policy (https://portproof.org/legal/aup) and the immediate start of the service, then call again with accept_terms: true."
|
|
1384
|
+
);
|
|
1385
|
+
}
|
|
1386
|
+
if (args.trial === true === (args.gb !== void 0)) {
|
|
1387
|
+
return localError("validation_failed", "Send either gb or trial: true", "gb is a whole number of GB (1 to 1000); trial: true buys the one-off 0.5 GB trial instead.");
|
|
1388
|
+
}
|
|
1389
|
+
const out = await ctx.api.buyTraffic(args.trial === true ? { trial: true, accept_terms: true } : { gb: args.gb, accept_terms: true }, args.idempotency_key);
|
|
1390
|
+
const o = out.order;
|
|
1391
|
+
const gb = o.items.reduce((n, it) => n + it.quantity * (it.sku_snapshot.sku === TRAFFIC_TRIAL.sku ? TRAFFIC_TRIAL.gb : 1), 0);
|
|
1392
|
+
return jsonResult({
|
|
1393
|
+
order_id: o.id,
|
|
1394
|
+
order_number: o.number,
|
|
1395
|
+
status: o.status,
|
|
1396
|
+
gb,
|
|
1397
|
+
unit_cents: o.items[0]?.unit_cents ?? null,
|
|
1398
|
+
cost_cents: o.total_cents,
|
|
1399
|
+
vat_cents: o.vat_cents,
|
|
1400
|
+
currency: o.currency,
|
|
1401
|
+
payment_id: out.payment_id,
|
|
1402
|
+
note: "The balance updates within a few seconds of payment; call get_traffic to see it."
|
|
1403
|
+
});
|
|
1404
|
+
}
|
|
1405
|
+
});
|
|
1406
|
+
var testConnection = defineTool({
|
|
1407
|
+
name: "test_connection",
|
|
1408
|
+
title: "Test a proxy connection",
|
|
1409
|
+
description: "The API sends one request through the gateway with the given pool, country and rotation and reports ok, the exit IP it saw, the latency and, on failure, the gateway error_code (for example E_NO_STOCK_COUNTRY, E_CAP_EXCEEDED, timeout). exit_country is only a country that was observed for the exit, never the requested one: when country_verified is false it is null, so do not tell the user the country was confirmed. Uses a few kilobytes of the balance; limited to 6 per minute; needs an API key with ports:write. With the development mock the result is simulated (simulated: true).",
|
|
1410
|
+
inputSchema: TestConnectionInput,
|
|
1411
|
+
annotations: { ...READ_ONLY, idempotentHint: false, openWorldHint: true },
|
|
1412
|
+
run: async (args, ctx) => {
|
|
1413
|
+
const body = {};
|
|
1414
|
+
if (args.pool !== void 0) body["pool"] = args.pool;
|
|
1415
|
+
if (args.country !== void 0) body["country"] = args.country;
|
|
1416
|
+
if (args.rotation !== void 0) body["rotation"] = args.rotation;
|
|
1417
|
+
return jsonResult(await ctx.api.testConnection(body));
|
|
1418
|
+
}
|
|
1419
|
+
});
|
|
1420
|
+
|
|
1421
|
+
var getUsage = defineTool({
|
|
1422
|
+
name: "get_usage",
|
|
1423
|
+
title: "Get usage",
|
|
1424
|
+
description: "Traffic for one port over a time window: bytes in/out, peak concurrent sessions, new connections and rotations per bucket, plus totals. Default granularity is hour; use day for long windows to keep the response small.",
|
|
1425
|
+
inputSchema: GetUsageInput,
|
|
1426
|
+
annotations: READ_ONLY,
|
|
1427
|
+
run: async (args, ctx) => {
|
|
1428
|
+
const query = {
|
|
1429
|
+
port_id: args.port_id
|
|
1430
|
+
};
|
|
1431
|
+
if (args.from !== void 0) query.from = args.from;
|
|
1432
|
+
if (args.to !== void 0) query.to = args.to;
|
|
1433
|
+
if (args.granularity !== void 0) query.granularity = args.granularity;
|
|
1434
|
+
const { data } = await ctx.api.getUsage(query);
|
|
1435
|
+
const totals = data.reduce(
|
|
1436
|
+
(acc, p) => ({
|
|
1437
|
+
bytes_in: acc.bytes_in + p.bytes_in,
|
|
1438
|
+
bytes_out: acc.bytes_out + p.bytes_out,
|
|
1439
|
+
conns_new: acc.conns_new + p.conns_new,
|
|
1440
|
+
rotations: acc.rotations + p.rotations,
|
|
1441
|
+
sessions_peak: Math.max(acc.sessions_peak, p.sessions_peak)
|
|
1442
|
+
}),
|
|
1443
|
+
{ bytes_in: 0, bytes_out: 0, conns_new: 0, rotations: 0, sessions_peak: 0 }
|
|
1444
|
+
);
|
|
1445
|
+
return jsonResult({
|
|
1446
|
+
port_id: args.port_id,
|
|
1447
|
+
granularity: args.granularity ?? "hour",
|
|
1448
|
+
from: args.from ?? null,
|
|
1449
|
+
to: args.to ?? null,
|
|
1450
|
+
buckets: data.length,
|
|
1451
|
+
totals,
|
|
1452
|
+
data
|
|
1453
|
+
});
|
|
1454
|
+
}
|
|
1455
|
+
});
|
|
1456
|
+
|
|
1457
|
+
async function runTool(def, args, ctx) {
|
|
1458
|
+
try {
|
|
1459
|
+
return await def.run(args, ctx);
|
|
1460
|
+
} catch (err) {
|
|
1461
|
+
return errorFromUnknown(err);
|
|
1462
|
+
}
|
|
1463
|
+
}
|
|
1464
|
+
var TOOLS = [
|
|
1465
|
+
getPricing,
|
|
1466
|
+
searchInventory,
|
|
1467
|
+
quote,
|
|
1468
|
+
buyPort,
|
|
1469
|
+
listPorts,
|
|
1470
|
+
getPort,
|
|
1471
|
+
rotate,
|
|
1472
|
+
setRotationSchedule,
|
|
1473
|
+
getPassport,
|
|
1474
|
+
getReceipt,
|
|
1475
|
+
getUsage,
|
|
1476
|
+
getStatus,
|
|
1477
|
+
getTraffic,
|
|
1478
|
+
listCountries,
|
|
1479
|
+
buildProxyUrl,
|
|
1480
|
+
buyTraffic,
|
|
1481
|
+
testConnection
|
|
1482
|
+
];
|
|
1483
|
+
var registered = new Set(TOOLS.map((t) => t.name));
|
|
1484
|
+
for (const name of TOOL_NAMES) {
|
|
1485
|
+
if (!registered.has(name)) throw new Error(`tool registry is missing contract tool ${name}`);
|
|
1486
|
+
}
|
|
1487
|
+
if (registered.size !== TOOL_NAMES.length)
|
|
1488
|
+
throw new Error("tool registry has duplicate or extra tools");
|
|
1489
|
+
|
|
1490
|
+
var INSTRUCTIONS = [
|
|
1491
|
+
`Portproof sells proxy traffic by the GB: one prepaid GB balance per organisation, usable on two shared pools, mobile 4G/5G (carrier modems in ${MOBILE_COUNTRIES_TEXT}) and residential (opt-in peer devices; the countries online now come from list_countries, so never state a country count from memory). Pool, country, rotation (new IP every 5 / 10 / 20 / 60 minutes, on every connection, or a sticky session) and the session name are chosen in the proxy username, so one credential serves everything. Be precise: the pools are shared, a sticky session keeps the same device and the carrier may still change its IP; never call this traffic dedicated, private, static or unlimited, and never list pool exit addresses. Validity is what get_traffic reports: limits.validity_days 0 (and expires_at null) means GB never expire; a positive number is the days of validity every purchase guarantees for the whole balance.`,
|
|
1492
|
+
"Traffic tools: get_traffic (balance, expiry, limits), list_countries (devices online now per pool), build_proxy_url (username, URL and code snippets; the password stays masked unless reveal_password is true), test_connection (one request through the gateway: exit IP and latency; exit_country is normally null because the exit country is not checked; never report the requested country as the exit country), buy_traffic (wallet purchase of whole GB or the one-off 0.5 GB trial; prices come from the tier table in get_pricing, field traffic).",
|
|
1493
|
+
'Dedicated ports are coming soon and not on sale yet: do not offer them. get_pricing (field skus), search_inventory, quote, buy_port, list_ports, get_port, rotate, set_rotation_schedule, get_passport, get_receipt and get_usage work on dedicated mobile ports (one modem, one SIM, one customer per port) for organisations that already have one. Every port states its supply: "signed" evidence means Own network (each rotation record is signed on the device), "observed" evidence means Partner network (recorded by our monitoring and signed by the control plane key); never describe a Partner network port as signed on the device.',
|
|
1494
|
+
"Read tools (get_pricing, search_inventory, quote, list_ports, get_port, get_passport, get_receipt, get_usage, get_status, get_traffic, list_countries, build_proxy_url) are safe to call freely; test_connection uses a few kilobytes of the balance and is limited to 6 per minute.",
|
|
1495
|
+
"buy_traffic and buy_port charge money and rotate / set_rotation_schedule change live proxy state: they are refused unless called with confirm: true and an idempotency_key. buy_traffic also needs accept_terms: true, which records that the user accepts the terms, the acceptable-use policy and the immediate start of the service. Ask the user before setting confirm: true or accept_terms: true, and reuse the same idempotency_key only to retry the identical request.",
|
|
1496
|
+
'Every response is compact JSON. Successful purchases include cost_cents. Errors are {"error": {code, status, title, detail}} using the API problem codes (insufficient_funds, kyb_required_for_volume, trial_already_used, traffic_not_purchased, kyb_required, no_inventory, port_cooldown with retry_after, insufficient_scope, not_found, validation_failed, ...).',
|
|
1497
|
+
"get_pricing, search_inventory, quote, get_status and list_countries work without an API key; everything else needs PORTPROOF_API_KEY. Key scopes are enforced: get_traffic needs ports:read (connection details only with ports:write), and build_proxy_url, test_connection and buy_traffic need ports:write; a missing scope answers insufficient_scope."
|
|
1498
|
+
].join(" ");
|
|
1499
|
+
function createServer(ctx) {
|
|
1500
|
+
const server = new McpServer(
|
|
1501
|
+
{ name: SERVER_NAME, version: SERVER_VERSION },
|
|
1502
|
+
{ instructions: INSTRUCTIONS }
|
|
1503
|
+
);
|
|
1504
|
+
for (const def of TOOLS) {
|
|
1505
|
+
server.registerTool(
|
|
1506
|
+
def.name,
|
|
1507
|
+
{
|
|
1508
|
+
title: def.title,
|
|
1509
|
+
description: def.description,
|
|
1510
|
+
inputSchema: def.inputSchema,
|
|
1511
|
+
annotations: def.annotations
|
|
1512
|
+
},
|
|
1513
|
+
(args) => runTool(def, args, ctx)
|
|
1514
|
+
);
|
|
1515
|
+
}
|
|
1516
|
+
return server;
|
|
1517
|
+
}
|
|
1518
|
+
|
|
1519
|
+
function usage() {
|
|
1520
|
+
return [
|
|
1521
|
+
`portproof-mcp ${SERVER_VERSION} - Portproof API as MCP tools over stdio`,
|
|
1522
|
+
"",
|
|
1523
|
+
"Environment:",
|
|
1524
|
+
" PORTPROOF_API_KEY customer API key (pk_test_... / pk_live_...); optional for public tools",
|
|
1525
|
+
" PORTPROOF_API_URL API origin, default https://api.portproof.org (/v1 is appended)",
|
|
1526
|
+
"",
|
|
1527
|
+
`Tools: ${TOOL_NAMES.join(", ")}`
|
|
1528
|
+
].join("\n");
|
|
1529
|
+
}
|
|
1530
|
+
async function main(argv) {
|
|
1531
|
+
if (argv.includes("--help") || argv.includes("-h")) {
|
|
1532
|
+
console.error(usage());
|
|
1533
|
+
return;
|
|
1534
|
+
}
|
|
1535
|
+
if (argv.includes("--version") || argv.includes("-v")) {
|
|
1536
|
+
console.error(SERVER_VERSION);
|
|
1537
|
+
return;
|
|
1538
|
+
}
|
|
1539
|
+
const cfg = loadConfig();
|
|
1540
|
+
const api = new ApiClient({ baseUrl: cfg.baseUrl, apiKey: cfg.apiKey });
|
|
1541
|
+
const server = createServer({ api });
|
|
1542
|
+
await server.connect(new StdioServerTransport());
|
|
1543
|
+
console.error(
|
|
1544
|
+
`[portproof-mcp] ready on stdio: api=${cfg.baseUrl} key=${cfg.apiKey ? "set" : "missing (public tools only)"}`
|
|
1545
|
+
);
|
|
1546
|
+
}
|
|
1547
|
+
main(process.argv.slice(2)).catch((err) => {
|
|
1548
|
+
console.error("[portproof-mcp] fatal:", err instanceof Error ? err.stack ?? err.message : err);
|
|
1549
|
+
process.exit(1);
|
|
1550
|
+
});
|