@portproof/mcp 0.0.0-stage → 0.2.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 +150 -2
- package/dist/index.js +1814 -0
- package/llms.txt +42 -0
- package/package.json +56 -4
package/dist/index.js
ADDED
|
@@ -0,0 +1,1814 @@
|
|
|
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
|
+
"opc",
|
|
48
|
+
"opm",
|
|
49
|
+
"opt",
|
|
50
|
+
"opx"
|
|
51
|
+
];
|
|
52
|
+
var ULID_RE = /^[0-9A-HJKMNP-TV-Z]{26}$/;
|
|
53
|
+
var prefixSet = /* @__PURE__ */ new Set(ID_PREFIXES);
|
|
54
|
+
function isId(value, prefix) {
|
|
55
|
+
if (typeof value !== "string")
|
|
56
|
+
return false;
|
|
57
|
+
const idx = value.indexOf("_");
|
|
58
|
+
if (idx <= 0)
|
|
59
|
+
return false;
|
|
60
|
+
const p = value.slice(0, idx);
|
|
61
|
+
const body = value.slice(idx + 1);
|
|
62
|
+
if (prefix !== void 0 ? p !== prefix : !prefixSet.has(p))
|
|
63
|
+
return false;
|
|
64
|
+
return ULID_RE.test(body) && isValidUlid(body);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
var SKUS = ["lt-4g", "pl-4g", "uk-4g", "es-4g", "de-4g", "nl-4g", "us-4g", "trial-24h"];
|
|
68
|
+
var GIB = 1024 ** 3;
|
|
69
|
+
var DEFAULT_DAILY_CAP_BYTES = 20 * GIB;
|
|
70
|
+
var MAX_ALLOWLIST_CIDRS = 10;
|
|
71
|
+
var ROTATION_INTERVAL_MIN_S = 60;
|
|
72
|
+
var ROTATION_INTERVAL_MAX_S = 86400;
|
|
73
|
+
var TELEMETRY_HOURS_MIN = 72;
|
|
74
|
+
var EXCLUSIVITY = "modem+sim+credentials";
|
|
75
|
+
var RECEIPT_ALG = "Ed25519";
|
|
76
|
+
var WELL_KNOWN_KEYS_PATH = "/.well-known/portproof-keys.json";
|
|
77
|
+
var PRODUCT_KINDS = ["port", "bandwidth", "topup", "sms", "esim"];
|
|
78
|
+
var PRODUCT_TERMS = ["monthly", "weekly", "daily", "gb", "none"];
|
|
79
|
+
var CUSTOMER_SUPPLY_KINDS = ["owned", "generic_pool", "mock"];
|
|
80
|
+
var EVIDENCE_LEVELS = ["signed", "observed"];
|
|
81
|
+
var PORT_NETWORKS = ["own", "partner"];
|
|
82
|
+
var PAYMENT_METHODS = ["stripe", "coingate", "nowpayments", "wallet", "bank_transfer", "mock"];
|
|
83
|
+
var ORDER_STATUSES = [
|
|
84
|
+
"draft",
|
|
85
|
+
"awaiting_payment",
|
|
86
|
+
"awaiting_kyb",
|
|
87
|
+
"paid",
|
|
88
|
+
"provisioning",
|
|
89
|
+
"fulfilled",
|
|
90
|
+
"partially_fulfilled",
|
|
91
|
+
"failed_refunded",
|
|
92
|
+
"cancelled",
|
|
93
|
+
"refunded"
|
|
94
|
+
];
|
|
95
|
+
var TRAFFIC_TRIAL = { sku: "traffic-trial", gb: 0.5, price_cents: 290 };
|
|
96
|
+
var TRAFFIC_MOBILE_COUNTRIES = ["US", "GB", "FR", "NL", "PL", "GE"];
|
|
97
|
+
var TRAFFIC_PROTOCOLS = ["http", "socks5"];
|
|
98
|
+
var OPS_PRINCIPALS = ["admin-key", "claude", "codex"];
|
|
99
|
+
var OPS_ACTORS = [...OPS_PRINCIPALS, "system"];
|
|
100
|
+
|
|
101
|
+
import { z } from "zod";
|
|
102
|
+
var IsoDateTime = /* @__PURE__ */ z.string().datetime({ offset: true });
|
|
103
|
+
var CountryCode = /* @__PURE__ */ z.string().regex(/^[A-Z]{2}$/, "ISO-3166 alpha-2");
|
|
104
|
+
var MccMnc = /* @__PURE__ */ z.string().regex(/^\d{5,6}$/, "MCC+MNC");
|
|
105
|
+
var Cents = /* @__PURE__ */ z.number().int().min(0);
|
|
106
|
+
var Currency = /* @__PURE__ */ z.enum(["EUR", "USD"]);
|
|
107
|
+
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");
|
|
108
|
+
function idSchema(prefix) {
|
|
109
|
+
return z.string().refine((v) => isId(v, prefix), { message: prefix ? `expected ${prefix}_<ULID>` : `expected <prefix>_<ULID>` });
|
|
110
|
+
}
|
|
111
|
+
var Operator = /* @__PURE__ */ z.enum(["owned", "partner"]);
|
|
112
|
+
var Evidence = /* @__PURE__ */ z.enum(EVIDENCE_LEVELS);
|
|
113
|
+
var IpStack = /* @__PURE__ */ z.enum(["v4", "v6_464xlat", "dual"]);
|
|
114
|
+
var ContractType = /* @__PURE__ */ z.enum(["prepaid", "business", "m2m"]);
|
|
115
|
+
var PortState = /* @__PURE__ */ z.enum([
|
|
116
|
+
"provisioning",
|
|
117
|
+
"available",
|
|
118
|
+
"reserved",
|
|
119
|
+
"sold",
|
|
120
|
+
"leased",
|
|
121
|
+
"migrating",
|
|
122
|
+
"quarantined",
|
|
123
|
+
"retired"
|
|
124
|
+
]);
|
|
125
|
+
var Term = /* @__PURE__ */ z.enum(["monthly", "weekly", "daily"]);
|
|
126
|
+
var PricingTerm = /* @__PURE__ */ z.enum(["monthly", "weekly", "daily", "hourly"]);
|
|
127
|
+
var SubscriptionState = /* @__PURE__ */ z.enum(["active", "grace", "suspended", "ended"]);
|
|
128
|
+
var RequestedBy = /* @__PURE__ */ z.enum(["manual", "api", "token", "schedule", "health", "migration"]);
|
|
129
|
+
var RotationReason = /* @__PURE__ */ z.enum(["ok", "carrier_sticky", "attach_timeout", "driver_error", "cooldown"]);
|
|
130
|
+
var SiteStatus = /* @__PURE__ */ z.enum(["provisioning", "live", "degraded", "offline"]);
|
|
131
|
+
var SkuSchema = /* @__PURE__ */ z.enum(SKUS);
|
|
132
|
+
var ProductSku = /* @__PURE__ */ z.string().regex(/^[a-z0-9][a-z0-9-]{1,63}$/, "lower-case slug");
|
|
133
|
+
var PortNetwork = /* @__PURE__ */ z.enum(PORT_NETWORKS);
|
|
134
|
+
var IncidentScope = /* @__PURE__ */ z.enum(["port", "site", "carrier", "platform"]);
|
|
135
|
+
var PlatformStatus = /* @__PURE__ */ z.enum(["ok", "degraded", "outage"]);
|
|
136
|
+
function paginated(item) {
|
|
137
|
+
return z.object({ data: z.array(item), next_cursor: z.string().nullable() });
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
import { z as z3 } from "zod";
|
|
141
|
+
|
|
142
|
+
import { z as z2 } from "zod";
|
|
143
|
+
var ProxyCredentialDto = /* @__PURE__ */ z2.object({
|
|
144
|
+
host: /* @__PURE__ */ z2.string().min(1),
|
|
145
|
+
port: /* @__PURE__ */ z2.number().int().min(1).max(65535),
|
|
146
|
+
username: /* @__PURE__ */ z2.string().min(1),
|
|
147
|
+
password: /* @__PURE__ */ z2.string().min(1)
|
|
148
|
+
});
|
|
149
|
+
var PortCapabilitiesDto = /* @__PURE__ */ z2.object({
|
|
150
|
+
replace: /* @__PURE__ */ z2.boolean(),
|
|
151
|
+
cancel: /* @__PURE__ */ z2.boolean(),
|
|
152
|
+
allowlist: /* @__PURE__ */ z2.boolean(),
|
|
153
|
+
auto_swap: /* @__PURE__ */ z2.boolean(),
|
|
154
|
+
rotation_interval: /* @__PURE__ */ z2.boolean(),
|
|
155
|
+
password_change: /* @__PURE__ */ z2.boolean()
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
var Subscription = /* @__PURE__ */ z3.object({
|
|
159
|
+
id: /* @__PURE__ */ idSchema("sub"),
|
|
160
|
+
org_id: /* @__PURE__ */ idSchema("org"),
|
|
161
|
+
port_id: /* @__PURE__ */ idSchema("prt"),
|
|
162
|
+
sku: /* @__PURE__ */ z3.string().min(1),
|
|
163
|
+
term: Term,
|
|
164
|
+
price_cents: Cents,
|
|
165
|
+
currency: Currency,
|
|
166
|
+
starts_at: IsoDateTime,
|
|
167
|
+
ends_at: IsoDateTime,
|
|
168
|
+
auto_renew: /* @__PURE__ */ z3.boolean(),
|
|
169
|
+
state: SubscriptionState,
|
|
170
|
+
provider_ref: /* @__PURE__ */ z3.string().nullable(),
|
|
171
|
+
created_at: IsoDateTime,
|
|
172
|
+
updated_at: IsoDateTime
|
|
173
|
+
});
|
|
174
|
+
var PortCarrier = /* @__PURE__ */ z3.object({
|
|
175
|
+
id: /* @__PURE__ */ idSchema("car"),
|
|
176
|
+
name: /* @__PURE__ */ z3.string().min(1),
|
|
177
|
+
mcc_mnc: MccMnc,
|
|
178
|
+
ip_stack: IpStack
|
|
179
|
+
});
|
|
180
|
+
var PortListeners = /* @__PURE__ */ z3.object({
|
|
181
|
+
host: /* @__PURE__ */ z3.string().min(1),
|
|
182
|
+
http_port: /* @__PURE__ */ z3.number().int().min(1).max(65535),
|
|
183
|
+
socks_port: /* @__PURE__ */ z3.number().int().min(1).max(65535)
|
|
184
|
+
});
|
|
185
|
+
var PortCredentials = /* @__PURE__ */ z3.object({
|
|
186
|
+
username: /* @__PURE__ */ z3.string().min(1),
|
|
187
|
+
password: /* @__PURE__ */ z3.string().min(1),
|
|
188
|
+
http: /* @__PURE__ */ ProxyCredentialDto.optional(),
|
|
189
|
+
socks5: /* @__PURE__ */ ProxyCredentialDto.optional()
|
|
190
|
+
});
|
|
191
|
+
var PortRenewal = /* @__PURE__ */ z3.object({
|
|
192
|
+
auto_renew: /* @__PURE__ */ z3.boolean(),
|
|
193
|
+
ends_at: IsoDateTime,
|
|
194
|
+
price_cents: Cents,
|
|
195
|
+
can_renew: /* @__PURE__ */ z3.boolean()
|
|
196
|
+
});
|
|
197
|
+
var UsageToday = /* @__PURE__ */ z3.object({ bytes: /* @__PURE__ */ z3.number().int().min(0), cap_bytes: /* @__PURE__ */ z3.number().int().min(0) });
|
|
198
|
+
var Port = /* @__PURE__ */ z3.object({
|
|
199
|
+
id: /* @__PURE__ */ idSchema("prt"),
|
|
200
|
+
state: PortState,
|
|
201
|
+
site_code: /* @__PURE__ */ z3.string().min(1),
|
|
202
|
+
country: CountryCode,
|
|
203
|
+
carrier: PortCarrier,
|
|
204
|
+
operator: Operator,
|
|
205
|
+
listeners: PortListeners,
|
|
206
|
+
credentials: /* @__PURE__ */ PortCredentials.nullable(),
|
|
207
|
+
allowlist: /* @__PURE__ */ z3.array(Cidr).max(MAX_ALLOWLIST_CIDRS),
|
|
208
|
+
egress_v4: /* @__PURE__ */ z3.string().nullable(),
|
|
209
|
+
egress_v6_prefix: /* @__PURE__ */ z3.string().nullable(),
|
|
210
|
+
v4_is_nat64: /* @__PURE__ */ z3.boolean(),
|
|
211
|
+
ip_since: /* @__PURE__ */ IsoDateTime.nullable(),
|
|
212
|
+
rotation_interval_s: /* @__PURE__ */ z3.number().int().min(0),
|
|
213
|
+
sticky_until: /* @__PURE__ */ IsoDateTime.nullable(),
|
|
214
|
+
cooldown_until: /* @__PURE__ */ IsoDateTime.nullable(),
|
|
215
|
+
last_rotation_at: /* @__PURE__ */ IsoDateTime.nullable(),
|
|
216
|
+
telemetry_since: /* @__PURE__ */ IsoDateTime.nullable(),
|
|
217
|
+
usage_today: UsageToday,
|
|
218
|
+
uptime_30d_pct: /* @__PURE__ */ z3.number().min(0).max(100).nullable(),
|
|
219
|
+
subscription: /* @__PURE__ */ Subscription.nullable(),
|
|
220
|
+
evidence: Evidence,
|
|
221
|
+
attestation: Evidence,
|
|
222
|
+
network: /* @__PURE__ */ PortNetwork.optional(),
|
|
223
|
+
capabilities: /* @__PURE__ */ PortCapabilitiesDto.nullable().optional(),
|
|
224
|
+
renewal: /* @__PURE__ */ PortRenewal.nullable().optional(),
|
|
225
|
+
rotation_url: /* @__PURE__ */ z3.string().url().nullable().optional()
|
|
226
|
+
});
|
|
227
|
+
var RotatePendingResponse = /* @__PURE__ */ z3.object({ rotation_id: /* @__PURE__ */ idSchema("rot"), status: /* @__PURE__ */ z3.literal("pending") });
|
|
228
|
+
var RotationScheduleRequest = /* @__PURE__ */ z3.object({
|
|
229
|
+
interval_s: /* @__PURE__ */ z3.number().int().refine((v) => v === 0 || v >= ROTATION_INTERVAL_MIN_S && v <= ROTATION_INTERVAL_MAX_S, {
|
|
230
|
+
message: `interval_s must be 0 (sticky) or ${ROTATION_INTERVAL_MIN_S}..${ROTATION_INTERVAL_MAX_S}`
|
|
231
|
+
})
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
import { z as z4 } from "zod";
|
|
235
|
+
var Pct = /* @__PURE__ */ z4.number().min(0).max(100);
|
|
236
|
+
var PassportCarrier = /* @__PURE__ */ z4.object({
|
|
237
|
+
name: /* @__PURE__ */ z4.string().min(1),
|
|
238
|
+
mcc_mnc: MccMnc,
|
|
239
|
+
asn: /* @__PURE__ */ z4.string().nullable(),
|
|
240
|
+
gateway_region: /* @__PURE__ */ z4.string().nullable(),
|
|
241
|
+
ip_stack: IpStack
|
|
242
|
+
});
|
|
243
|
+
var PassportEgress = /* @__PURE__ */ z4.object({
|
|
244
|
+
v4: /* @__PURE__ */ z4.string().nullable(),
|
|
245
|
+
v6_prefix: /* @__PURE__ */ z4.string().nullable(),
|
|
246
|
+
v4_is_nat64: /* @__PURE__ */ z4.boolean(),
|
|
247
|
+
ip_since: IsoDateTime
|
|
248
|
+
});
|
|
249
|
+
var GeoSource = /* @__PURE__ */ z4.object({
|
|
250
|
+
provider: /* @__PURE__ */ z4.string().min(1),
|
|
251
|
+
country: /* @__PURE__ */ z4.string().nullable(),
|
|
252
|
+
region: /* @__PURE__ */ z4.string().nullable(),
|
|
253
|
+
city: /* @__PURE__ */ z4.string().nullable()
|
|
254
|
+
});
|
|
255
|
+
var PassportGeo = /* @__PURE__ */ z4.object({
|
|
256
|
+
country: /* @__PURE__ */ z4.string().nullable(),
|
|
257
|
+
region: /* @__PURE__ */ z4.string().nullable(),
|
|
258
|
+
city: /* @__PURE__ */ z4.string().nullable(),
|
|
259
|
+
sources: /* @__PURE__ */ z4.array(GeoSource)
|
|
260
|
+
});
|
|
261
|
+
var PassportReputation = /* @__PURE__ */ z4.object({
|
|
262
|
+
flagged: /* @__PURE__ */ z4.boolean(),
|
|
263
|
+
hosting: /* @__PURE__ */ z4.boolean(),
|
|
264
|
+
providers: /* @__PURE__ */ z4.array(/* @__PURE__ */ z4.string()),
|
|
265
|
+
scored_at: /* @__PURE__ */ IsoDateTime.nullable()
|
|
266
|
+
});
|
|
267
|
+
var PassportRotation = /* @__PURE__ */ z4.object({
|
|
268
|
+
interval_s: /* @__PURE__ */ z4.number().int().min(0),
|
|
269
|
+
sticky_until: /* @__PURE__ */ IsoDateTime.nullable(),
|
|
270
|
+
last_rotation_at: /* @__PURE__ */ IsoDateTime.nullable(),
|
|
271
|
+
last_30d: /* @__PURE__ */ z4.object({
|
|
272
|
+
count: /* @__PURE__ */ z4.number().int().min(0),
|
|
273
|
+
first_attempt_success_pct: /* @__PURE__ */ Pct.nullable(),
|
|
274
|
+
p50_ms: /* @__PURE__ */ z4.number().min(0).nullable()
|
|
275
|
+
})
|
|
276
|
+
});
|
|
277
|
+
var Passport = /* @__PURE__ */ z4.object({
|
|
278
|
+
port_id: /* @__PURE__ */ idSchema("prt"),
|
|
279
|
+
generated_at: IsoDateTime,
|
|
280
|
+
site_code: /* @__PURE__ */ z4.string().min(1),
|
|
281
|
+
country: CountryCode,
|
|
282
|
+
operator: Operator,
|
|
283
|
+
evidence: /* @__PURE__ */ z4.enum(["signed", "observed"]).optional(),
|
|
284
|
+
network: /* @__PURE__ */ PortNetwork.optional(),
|
|
285
|
+
carrier: PassportCarrier,
|
|
286
|
+
sim_contract_class: ContractType,
|
|
287
|
+
egress: PassportEgress,
|
|
288
|
+
geo: PassportGeo,
|
|
289
|
+
reputation: PassportReputation,
|
|
290
|
+
rotation: PassportRotation,
|
|
291
|
+
uptime: /* @__PURE__ */ z4.object({
|
|
292
|
+
d30_pct: /* @__PURE__ */ Pct.nullable(),
|
|
293
|
+
d90_pct: /* @__PURE__ */ Pct.nullable(),
|
|
294
|
+
probes_30d: /* @__PURE__ */ z4.number().int().min(0)
|
|
295
|
+
}),
|
|
296
|
+
throughput: /* @__PURE__ */ z4.object({
|
|
297
|
+
last_sample_mbps: /* @__PURE__ */ z4.number().min(0).nullable(),
|
|
298
|
+
p50_7d_mbps: /* @__PURE__ */ z4.number().min(0).nullable()
|
|
299
|
+
}),
|
|
300
|
+
usage: /* @__PURE__ */ z4.object({ bytes_today: /* @__PURE__ */ z4.number().int().min(0), cap_bytes: /* @__PURE__ */ z4.number().int().min(0) }),
|
|
301
|
+
sessions: /* @__PURE__ */ z4.object({ concurrent_now: /* @__PURE__ */ z4.number().int().min(0), peak_24h: /* @__PURE__ */ z4.number().int().min(0) }),
|
|
302
|
+
ip_shared_with_own_ports: /* @__PURE__ */ z4.number().int().min(0),
|
|
303
|
+
exclusivity: /* @__PURE__ */ z4.literal(EXCLUSIVITY),
|
|
304
|
+
receipts_url: /* @__PURE__ */ z4.string().url()
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
import { z as z5 } from "zod";
|
|
308
|
+
var ReceiptGeo = /* @__PURE__ */ z5.object({
|
|
309
|
+
country: /* @__PURE__ */ z5.string().nullable(),
|
|
310
|
+
region: /* @__PURE__ */ z5.string().nullable(),
|
|
311
|
+
sources: /* @__PURE__ */ z5.number().int().min(0)
|
|
312
|
+
});
|
|
313
|
+
var ReceiptReputation = /* @__PURE__ */ z5.object({
|
|
314
|
+
flagged: /* @__PURE__ */ z5.boolean(),
|
|
315
|
+
hosting: /* @__PURE__ */ z5.boolean(),
|
|
316
|
+
scored_at: /* @__PURE__ */ IsoDateTime.nullable()
|
|
317
|
+
});
|
|
318
|
+
var ReceiptUnsigned = /* @__PURE__ */ z5.object({
|
|
319
|
+
id: /* @__PURE__ */ idSchema("rot"),
|
|
320
|
+
port_id: /* @__PURE__ */ idSchema("prt"),
|
|
321
|
+
site_code: /* @__PURE__ */ z5.string().min(1),
|
|
322
|
+
requested_by: RequestedBy,
|
|
323
|
+
old_ip4: /* @__PURE__ */ z5.string().nullable(),
|
|
324
|
+
new_ip4: /* @__PURE__ */ z5.string().nullable(),
|
|
325
|
+
old_ip6_prefix: /* @__PURE__ */ z5.string().nullable(),
|
|
326
|
+
new_ip6_prefix: /* @__PURE__ */ z5.string().nullable(),
|
|
327
|
+
v4_is_nat64: /* @__PURE__ */ z5.boolean(),
|
|
328
|
+
rotated: /* @__PURE__ */ z5.boolean(),
|
|
329
|
+
attempts: /* @__PURE__ */ z5.number().int().min(0),
|
|
330
|
+
elapsed_ms: /* @__PURE__ */ z5.number().int().min(0),
|
|
331
|
+
reason: RotationReason,
|
|
332
|
+
driver: /* @__PURE__ */ z5.string().min(1),
|
|
333
|
+
carrier: /* @__PURE__ */ z5.string().min(1),
|
|
334
|
+
mcc_mnc: MccMnc,
|
|
335
|
+
asn: /* @__PURE__ */ z5.string().nullable(),
|
|
336
|
+
geo: ReceiptGeo,
|
|
337
|
+
reputation: /* @__PURE__ */ ReceiptReputation.nullable(),
|
|
338
|
+
sticky_window_s: /* @__PURE__ */ z5.number().int().min(0),
|
|
339
|
+
ip_shared_with_own_ports: /* @__PURE__ */ z5.number().int().min(0),
|
|
340
|
+
sim_contract_class: ContractType,
|
|
341
|
+
signed_at: IsoDateTime,
|
|
342
|
+
key_id: /* @__PURE__ */ z5.string().regex(/^[a-z0-9-]+-\d{4}$/, "<site_code>-<yyyy>"),
|
|
343
|
+
attestation: /* @__PURE__ */ z5.enum(["signed", "observed"]).optional()
|
|
344
|
+
});
|
|
345
|
+
var Receipt = /* @__PURE__ */ ReceiptUnsigned.extend({
|
|
346
|
+
sig: /* @__PURE__ */ z5.string().regex(/^[A-Za-z0-9+/]+={0,2}$/, "base64").min(86)
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
import { z as z9 } from "zod";
|
|
350
|
+
|
|
351
|
+
import { z as z8 } from "zod";
|
|
352
|
+
|
|
353
|
+
import { z as z6 } from "zod";
|
|
354
|
+
var ProductKind = /* @__PURE__ */ z6.enum(PRODUCT_KINDS);
|
|
355
|
+
var ProductTerm = /* @__PURE__ */ z6.enum(PRODUCT_TERMS);
|
|
356
|
+
var CustomerSupplyKind = /* @__PURE__ */ z6.enum(CUSTOMER_SUPPLY_KINDS);
|
|
357
|
+
var SupplyHealth = /* @__PURE__ */ z6.enum(["ok", "degraded", "down", "unknown"]);
|
|
358
|
+
var OrderStatus = /* @__PURE__ */ z6.enum(ORDER_STATUSES);
|
|
359
|
+
var OrderItemStatus = /* @__PURE__ */ z6.enum(["pending", "provisioning", "fulfilled", "failed", "refunded", "held"]);
|
|
360
|
+
var PaymentMethod = /* @__PURE__ */ z6.enum(PAYMENT_METHODS);
|
|
361
|
+
var PriceTier = /* @__PURE__ */ z6.object({ min_qty: /* @__PURE__ */ z6.number().int().min(1).max(1e5), unit_cents: Cents });
|
|
362
|
+
var SkuSnapshot = /* @__PURE__ */ z6.object({
|
|
363
|
+
sku: ProductSku,
|
|
364
|
+
kind: ProductKind,
|
|
365
|
+
title: /* @__PURE__ */ z6.string().min(1),
|
|
366
|
+
country: /* @__PURE__ */ CountryCode.nullable(),
|
|
367
|
+
carrier_hint: /* @__PURE__ */ z6.string().nullable(),
|
|
368
|
+
term: ProductTerm,
|
|
369
|
+
sell_cents: Cents,
|
|
370
|
+
currency: Currency,
|
|
371
|
+
evidence: Evidence,
|
|
372
|
+
operator_label: /* @__PURE__ */ z6.string().min(1),
|
|
373
|
+
supply_kind: CustomerSupplyKind
|
|
374
|
+
});
|
|
375
|
+
|
|
376
|
+
import { z as z7 } from "zod";
|
|
377
|
+
|
|
378
|
+
var ORDER_NUMBER_RE = /^PP-\d{4}-\d{6,}$/;
|
|
379
|
+
|
|
380
|
+
var OrderNumber = /* @__PURE__ */ z7.string().regex(ORDER_NUMBER_RE, "PP-YYYY-NNNNNN");
|
|
381
|
+
var ANALYTICS_REF_RE = /^[0-9a-f]{20}$/;
|
|
382
|
+
var FulfilmentCredentials = /* @__PURE__ */ z7.object({
|
|
383
|
+
host: /* @__PURE__ */ z7.string().min(1),
|
|
384
|
+
http_port: /* @__PURE__ */ z7.number().int().positive().nullable(),
|
|
385
|
+
socks_port: /* @__PURE__ */ z7.number().int().positive().nullable(),
|
|
386
|
+
username: /* @__PURE__ */ z7.string().min(1),
|
|
387
|
+
password: /* @__PURE__ */ z7.string().min(1),
|
|
388
|
+
expires_at: /* @__PURE__ */ IsoDateTime.nullable(),
|
|
389
|
+
http: /* @__PURE__ */ ProxyCredentialDto.optional(),
|
|
390
|
+
socks5: /* @__PURE__ */ ProxyCredentialDto.optional()
|
|
391
|
+
});
|
|
392
|
+
var OrderItemUnit = /* @__PURE__ */ z7.object({
|
|
393
|
+
unit: /* @__PURE__ */ z7.number().int().min(1),
|
|
394
|
+
port_id: /* @__PURE__ */ idSchema("prt"),
|
|
395
|
+
resource_url: /* @__PURE__ */ z7.string().nullable(),
|
|
396
|
+
credentials: /* @__PURE__ */ FulfilmentCredentials.nullable()
|
|
397
|
+
});
|
|
398
|
+
var OrderItem = /* @__PURE__ */ z7.object({
|
|
399
|
+
id: /* @__PURE__ */ idSchema("oi"),
|
|
400
|
+
product_id: /* @__PURE__ */ idSchema("prod"),
|
|
401
|
+
sku_snapshot: SkuSnapshot,
|
|
402
|
+
quantity: /* @__PURE__ */ z7.number().int().min(1),
|
|
403
|
+
unit_cents: Cents,
|
|
404
|
+
total_cents: Cents,
|
|
405
|
+
status: OrderItemStatus,
|
|
406
|
+
fulfilment_ref: /* @__PURE__ */ z7.string().nullable(),
|
|
407
|
+
attempts: /* @__PURE__ */ z7.number().int().min(0),
|
|
408
|
+
last_error: /* @__PURE__ */ z7.string().nullable().optional(),
|
|
409
|
+
customer_reason: /* @__PURE__ */ z7.string().nullable().optional(),
|
|
410
|
+
resource_url: /* @__PURE__ */ z7.string().nullable(),
|
|
411
|
+
credentials: /* @__PURE__ */ FulfilmentCredentials.nullable(),
|
|
412
|
+
units: /* @__PURE__ */ z7.array(OrderItemUnit).optional(),
|
|
413
|
+
updated_at: IsoDateTime
|
|
414
|
+
});
|
|
415
|
+
var Order = /* @__PURE__ */ z7.object({
|
|
416
|
+
id: /* @__PURE__ */ idSchema("ord"),
|
|
417
|
+
number: OrderNumber,
|
|
418
|
+
customer_id: /* @__PURE__ */ idSchema("cus"),
|
|
419
|
+
org_id: /* @__PURE__ */ idSchema("org"),
|
|
420
|
+
status: OrderStatus,
|
|
421
|
+
subtotal_cents: Cents,
|
|
422
|
+
discount_cents: Cents,
|
|
423
|
+
vat_rate_bps: /* @__PURE__ */ z7.number().int().min(0).max(1e4),
|
|
424
|
+
vat_cents: Cents,
|
|
425
|
+
total_cents: Cents,
|
|
426
|
+
currency: Currency,
|
|
427
|
+
payment_method: PaymentMethod,
|
|
428
|
+
coupon_code: /* @__PURE__ */ z7.string().nullable(),
|
|
429
|
+
items: /* @__PURE__ */ z7.array(OrderItem),
|
|
430
|
+
paid_at: /* @__PURE__ */ IsoDateTime.nullable(),
|
|
431
|
+
notes: /* @__PURE__ */ z7.string().nullable(),
|
|
432
|
+
invoice_url: /* @__PURE__ */ z7.string().nullable(),
|
|
433
|
+
payment_url: /* @__PURE__ */ z7.string().url().nullable().optional(),
|
|
434
|
+
analytics_ref: /* @__PURE__ */ z7.string().regex(ANALYTICS_REF_RE).nullable().optional(),
|
|
435
|
+
created_at: IsoDateTime,
|
|
436
|
+
updated_at: IsoDateTime
|
|
437
|
+
});
|
|
438
|
+
|
|
439
|
+
var TrafficProtocolName = /* @__PURE__ */ z8.enum(TRAFFIC_PROTOCOLS);
|
|
440
|
+
var TrafficCredentials = /* @__PURE__ */ z8.object({
|
|
441
|
+
host: /* @__PURE__ */ z8.string().min(1),
|
|
442
|
+
http_port: /* @__PURE__ */ z8.number().int().positive(),
|
|
443
|
+
socks_port: /* @__PURE__ */ z8.number().int().positive(),
|
|
444
|
+
username_base: /* @__PURE__ */ z8.string().min(1),
|
|
445
|
+
password: /* @__PURE__ */ z8.string().min(1)
|
|
446
|
+
});
|
|
447
|
+
var TrafficStatus = /* @__PURE__ */ z8.enum(["none", "active", "exhausted", "expired", "suspended"]);
|
|
448
|
+
var TrafficLimits = /* @__PURE__ */ z8.object({
|
|
449
|
+
validity_days: /* @__PURE__ */ z8.number().int().min(0),
|
|
450
|
+
kyb_tier: /* @__PURE__ */ z8.string(),
|
|
451
|
+
t0_monthly_gb_cap: /* @__PURE__ */ z8.number().min(0),
|
|
452
|
+
gb_bought_30d: /* @__PURE__ */ z8.number().min(0),
|
|
453
|
+
gb_remaining_30d: /* @__PURE__ */ z8.number().min(0).nullable(),
|
|
454
|
+
trial_available: /* @__PURE__ */ z8.boolean(),
|
|
455
|
+
max_order_gb: /* @__PURE__ */ z8.number().int().min(1)
|
|
456
|
+
});
|
|
457
|
+
var TrafficInfo = /* @__PURE__ */ z8.object({
|
|
458
|
+
id: /* @__PURE__ */ idSchema("pool").nullable(),
|
|
459
|
+
status: TrafficStatus,
|
|
460
|
+
gb_total: /* @__PURE__ */ z8.number().min(0),
|
|
461
|
+
gb_used: /* @__PURE__ */ z8.number().min(0),
|
|
462
|
+
gb_left: /* @__PURE__ */ z8.number().min(0),
|
|
463
|
+
expires_at: /* @__PURE__ */ IsoDateTime.nullable(),
|
|
464
|
+
enabled: /* @__PURE__ */ z8.boolean(),
|
|
465
|
+
last_synced_at: /* @__PURE__ */ IsoDateTime.nullable(),
|
|
466
|
+
credentials: /* @__PURE__ */ TrafficCredentials.nullable(),
|
|
467
|
+
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) })),
|
|
468
|
+
limits: TrafficLimits
|
|
469
|
+
});
|
|
470
|
+
var TrafficCountryRow = /* @__PURE__ */ z8.object({
|
|
471
|
+
country: /* @__PURE__ */ z8.string().length(2),
|
|
472
|
+
name: /* @__PURE__ */ z8.string().min(1),
|
|
473
|
+
online: /* @__PURE__ */ z8.number().int().min(0)
|
|
474
|
+
});
|
|
475
|
+
var TrafficCountries = /* @__PURE__ */ z8.object({
|
|
476
|
+
mobile: /* @__PURE__ */ z8.array(TrafficCountryRow),
|
|
477
|
+
residential: /* @__PURE__ */ z8.array(TrafficCountryRow),
|
|
478
|
+
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) }),
|
|
479
|
+
generated_at: IsoDateTime,
|
|
480
|
+
stale: /* @__PURE__ */ z8.boolean()
|
|
481
|
+
});
|
|
482
|
+
var BuildUrlResult = /* @__PURE__ */ z8.object({
|
|
483
|
+
host: /* @__PURE__ */ z8.string().min(1),
|
|
484
|
+
port: /* @__PURE__ */ z8.number().int().positive(),
|
|
485
|
+
protocol: TrafficProtocolName,
|
|
486
|
+
username: /* @__PURE__ */ z8.string().min(1),
|
|
487
|
+
session: /* @__PURE__ */ z8.string().nullable(),
|
|
488
|
+
url: /* @__PURE__ */ z8.string().min(1),
|
|
489
|
+
url_masked: /* @__PURE__ */ z8.string().min(1),
|
|
490
|
+
snippets: /* @__PURE__ */ z8.object({ curl: /* @__PURE__ */ z8.string(), python: /* @__PURE__ */ z8.string(), node: /* @__PURE__ */ z8.string(), playwright: /* @__PURE__ */ z8.string() })
|
|
491
|
+
});
|
|
492
|
+
var TrafficTestResult = /* @__PURE__ */ z8.object({
|
|
493
|
+
ok: /* @__PURE__ */ z8.boolean(),
|
|
494
|
+
exit_country: /* @__PURE__ */ z8.string().nullable(),
|
|
495
|
+
country_verified: /* @__PURE__ */ z8.boolean(),
|
|
496
|
+
exit_ip: /* @__PURE__ */ z8.string().nullable(),
|
|
497
|
+
latency_ms: /* @__PURE__ */ z8.number().int().min(0).nullable(),
|
|
498
|
+
error_code: /* @__PURE__ */ z8.string().optional(),
|
|
499
|
+
simulated: /* @__PURE__ */ z8.boolean().optional()
|
|
500
|
+
});
|
|
501
|
+
var TrafficBuyResponse = /* @__PURE__ */ z8.object({ order: Order, payment_id: /* @__PURE__ */ idSchema("pay") });
|
|
502
|
+
var TrafficPricing = /* @__PURE__ */ z8.object({
|
|
503
|
+
sku: /* @__PURE__ */ z8.string().min(1),
|
|
504
|
+
currency: /* @__PURE__ */ z8.literal("EUR"),
|
|
505
|
+
tiers: /* @__PURE__ */ z8.array(PriceTier),
|
|
506
|
+
packages_gb: /* @__PURE__ */ z8.array(/* @__PURE__ */ z8.number().int().min(1)),
|
|
507
|
+
max_order_gb: /* @__PURE__ */ z8.number().int().min(1),
|
|
508
|
+
validity_days: /* @__PURE__ */ z8.number().int().min(0),
|
|
509
|
+
t0_monthly_gb_cap: /* @__PURE__ */ z8.number().min(0),
|
|
510
|
+
trial: /* @__PURE__ */ z8.object({ sku: /* @__PURE__ */ z8.string().min(1), gb: /* @__PURE__ */ z8.number().positive(), price_cents: Cents }).nullable(),
|
|
511
|
+
available: /* @__PURE__ */ z8.boolean()
|
|
512
|
+
});
|
|
513
|
+
|
|
514
|
+
var PricingSku = /* @__PURE__ */ z9.object({
|
|
515
|
+
sku: /* @__PURE__ */ z9.string().min(1),
|
|
516
|
+
country: CountryCode,
|
|
517
|
+
operator: Operator,
|
|
518
|
+
term: PricingTerm,
|
|
519
|
+
price_cents: Cents,
|
|
520
|
+
currency: Currency,
|
|
521
|
+
floor_cents: /* @__PURE__ */ Cents.optional(),
|
|
522
|
+
evidence: Evidence,
|
|
523
|
+
buy_via: /* @__PURE__ */ z9.enum(["ports", "cart"]),
|
|
524
|
+
terms: /* @__PURE__ */ z9.object({ monthly: Cents, weekly: Cents, daily: Cents }).optional()
|
|
525
|
+
});
|
|
526
|
+
var LadderRung = /* @__PURE__ */ z9.object({ min_ports: /* @__PURE__ */ z9.number().int().min(1), discount_pct: /* @__PURE__ */ z9.number().min(0).max(100) });
|
|
527
|
+
var PricingResponse = /* @__PURE__ */ z9.object({
|
|
528
|
+
currency_note: /* @__PURE__ */ z9.string(),
|
|
529
|
+
updated_at: IsoDateTime,
|
|
530
|
+
skus: /* @__PURE__ */ z9.array(PricingSku),
|
|
531
|
+
ladder: /* @__PURE__ */ z9.array(LadderRung),
|
|
532
|
+
term_ratios: /* @__PURE__ */ z9.object({ weekly: /* @__PURE__ */ z9.number().min(0).max(1), daily: /* @__PURE__ */ z9.number().min(0).max(1) }),
|
|
533
|
+
trial: /* @__PURE__ */ z9.object({ price_cents: Cents, hours: /* @__PURE__ */ z9.number().int().min(1) }),
|
|
534
|
+
traffic: /* @__PURE__ */ TrafficPricing.nullable().optional()
|
|
535
|
+
});
|
|
536
|
+
var InventoryItem = /* @__PURE__ */ z9.object({
|
|
537
|
+
country: CountryCode,
|
|
538
|
+
site_code: /* @__PURE__ */ z9.string().min(1),
|
|
539
|
+
operator: Operator,
|
|
540
|
+
carrier: /* @__PURE__ */ z9.string().min(1),
|
|
541
|
+
carrier_id: /* @__PURE__ */ idSchema("car"),
|
|
542
|
+
available: /* @__PURE__ */ z9.number().int().min(0),
|
|
543
|
+
telemetry_hours_min: /* @__PURE__ */ z9.number().int().min(0).default(TELEMETRY_HOURS_MIN),
|
|
544
|
+
waitlist: /* @__PURE__ */ z9.boolean()
|
|
545
|
+
});
|
|
546
|
+
var InventoryResponse = /* @__PURE__ */ z9.object({ data: /* @__PURE__ */ z9.array(InventoryItem) });
|
|
547
|
+
var QuoteResponse = /* @__PURE__ */ z9.object({
|
|
548
|
+
unit_cents: Cents,
|
|
549
|
+
discount_pct: /* @__PURE__ */ z9.number().min(0).max(100),
|
|
550
|
+
total_cents: Cents,
|
|
551
|
+
currency: Currency
|
|
552
|
+
});
|
|
553
|
+
|
|
554
|
+
import { z as z10 } from "zod";
|
|
555
|
+
var Pct2 = /* @__PURE__ */ z10.number().min(0).max(100);
|
|
556
|
+
var Incident = /* @__PURE__ */ z10.object({
|
|
557
|
+
id: /* @__PURE__ */ idSchema("inc"),
|
|
558
|
+
scope: IncidentScope,
|
|
559
|
+
scope_id: /* @__PURE__ */ z10.string().nullable(),
|
|
560
|
+
title: /* @__PURE__ */ z10.string().min(1),
|
|
561
|
+
cause: /* @__PURE__ */ z10.string().nullable(),
|
|
562
|
+
started_at: IsoDateTime,
|
|
563
|
+
resolved_at: /* @__PURE__ */ IsoDateTime.nullable(),
|
|
564
|
+
sla_excluded: /* @__PURE__ */ z10.boolean(),
|
|
565
|
+
public: /* @__PURE__ */ z10.boolean()
|
|
566
|
+
});
|
|
567
|
+
var StatusSite = /* @__PURE__ */ z10.object({
|
|
568
|
+
code: /* @__PURE__ */ z10.string().min(1),
|
|
569
|
+
country: CountryCode,
|
|
570
|
+
operator: Operator,
|
|
571
|
+
status: SiteStatus,
|
|
572
|
+
ports_live: /* @__PURE__ */ z10.number().int().min(0),
|
|
573
|
+
uptime_30d_pct: /* @__PURE__ */ Pct2.nullable()
|
|
574
|
+
});
|
|
575
|
+
var StatusCarrier = /* @__PURE__ */ z10.object({
|
|
576
|
+
id: /* @__PURE__ */ idSchema("car"),
|
|
577
|
+
name: /* @__PURE__ */ z10.string().min(1),
|
|
578
|
+
country: CountryCode,
|
|
579
|
+
uptime_30d_pct: /* @__PURE__ */ Pct2.nullable(),
|
|
580
|
+
rotation_first_attempt_pct: /* @__PURE__ */ Pct2.nullable(),
|
|
581
|
+
rotation_p50_ms: /* @__PURE__ */ z10.number().min(0).nullable(),
|
|
582
|
+
rotation_floor_pct: /* @__PURE__ */ Pct2.nullable()
|
|
583
|
+
});
|
|
584
|
+
var StatusResponse = /* @__PURE__ */ z10.object({
|
|
585
|
+
updated_at: IsoDateTime,
|
|
586
|
+
platform: PlatformStatus,
|
|
587
|
+
sites: /* @__PURE__ */ z10.array(StatusSite),
|
|
588
|
+
carriers: /* @__PURE__ */ z10.array(StatusCarrier),
|
|
589
|
+
incidents: /* @__PURE__ */ z10.array(Incident),
|
|
590
|
+
support: /* @__PURE__ */ z10.object({ median_first_response_min_30d: /* @__PURE__ */ z10.number().min(0).nullable() }),
|
|
591
|
+
traffic: /* @__PURE__ */ z10.object({
|
|
592
|
+
health: /* @__PURE__ */ SupplyHealth.nullable(),
|
|
593
|
+
last_sync_at: /* @__PURE__ */ IsoDateTime.nullable(),
|
|
594
|
+
countries_generated_at: /* @__PURE__ */ IsoDateTime.nullable()
|
|
595
|
+
})
|
|
596
|
+
});
|
|
597
|
+
var WellKnownKey = /* @__PURE__ */ z10.object({
|
|
598
|
+
key_id: /* @__PURE__ */ z10.string().min(1),
|
|
599
|
+
site_code: /* @__PURE__ */ z10.string().min(1),
|
|
600
|
+
alg: /* @__PURE__ */ z10.literal(RECEIPT_ALG),
|
|
601
|
+
public_key_b64: /* @__PURE__ */ z10.string().min(1),
|
|
602
|
+
valid_from: IsoDateTime,
|
|
603
|
+
valid_to: IsoDateTime
|
|
604
|
+
});
|
|
605
|
+
var WellKnownKeys = /* @__PURE__ */ z10.object({ keys: /* @__PURE__ */ z10.array(WellKnownKey) });
|
|
606
|
+
|
|
607
|
+
import { z as z11 } from "zod";
|
|
608
|
+
var UsageGranularity = /* @__PURE__ */ z11.enum(["minute", "hour", "day"]);
|
|
609
|
+
var UsagePoint = /* @__PURE__ */ z11.object({
|
|
610
|
+
ts: IsoDateTime,
|
|
611
|
+
bytes_in: /* @__PURE__ */ z11.number().int().min(0),
|
|
612
|
+
bytes_out: /* @__PURE__ */ z11.number().int().min(0),
|
|
613
|
+
sessions_peak: /* @__PURE__ */ z11.number().int().min(0),
|
|
614
|
+
conns_new: /* @__PURE__ */ z11.number().int().min(0),
|
|
615
|
+
rotations: /* @__PURE__ */ z11.number().int().min(0)
|
|
616
|
+
});
|
|
617
|
+
var UsageResponse = /* @__PURE__ */ z11.object({ data: /* @__PURE__ */ z11.array(UsagePoint) });
|
|
618
|
+
|
|
619
|
+
import { z as z12 } from "zod";
|
|
620
|
+
var ProblemDetails = /* @__PURE__ */ z12.object({
|
|
621
|
+
type: /* @__PURE__ */ z12.string().url().or(/* @__PURE__ */ z12.literal("about:blank")),
|
|
622
|
+
title: /* @__PURE__ */ z12.string(),
|
|
623
|
+
status: /* @__PURE__ */ z12.number().int().min(100).max(599),
|
|
624
|
+
detail: /* @__PURE__ */ z12.string().optional(),
|
|
625
|
+
instance: /* @__PURE__ */ z12.string().optional(),
|
|
626
|
+
code: /* @__PURE__ */ z12.string().min(1),
|
|
627
|
+
retry_after: /* @__PURE__ */ z12.number().int().min(0).optional(),
|
|
628
|
+
errors: /* @__PURE__ */ z12.array(/* @__PURE__ */ z12.object({ path: /* @__PURE__ */ z12.string(), message: /* @__PURE__ */ z12.string() })).optional(),
|
|
629
|
+
current_revision: /* @__PURE__ */ z12.number().int().min(1).optional()
|
|
630
|
+
});
|
|
631
|
+
|
|
632
|
+
import { createPrivateKey, createPublicKey, generateKeyPairSync, sign, verify } from "node:crypto";
|
|
633
|
+
function canonicalize(value) {
|
|
634
|
+
return JSON.stringify(sortKeys(value));
|
|
635
|
+
}
|
|
636
|
+
function sortKeys(value) {
|
|
637
|
+
if (Array.isArray(value))
|
|
638
|
+
return value.map(sortKeys);
|
|
639
|
+
if (value !== null && typeof value === "object") {
|
|
640
|
+
const src = value;
|
|
641
|
+
const out = {};
|
|
642
|
+
for (const key of Object.keys(src).sort()) {
|
|
643
|
+
if (key === "sig")
|
|
644
|
+
continue;
|
|
645
|
+
const v = src[key];
|
|
646
|
+
if (v === void 0)
|
|
647
|
+
continue;
|
|
648
|
+
out[key] = sortKeys(v);
|
|
649
|
+
}
|
|
650
|
+
return out;
|
|
651
|
+
}
|
|
652
|
+
return value;
|
|
653
|
+
}
|
|
654
|
+
var SPKI_PREFIX = /* @__PURE__ */ Buffer.from("302a300506032b6570032100", "hex");
|
|
655
|
+
function toPublicKey(key) {
|
|
656
|
+
if (typeof key === "object" && !(key instanceof Uint8Array))
|
|
657
|
+
return key;
|
|
658
|
+
if (typeof key === "string" && key.includes("-----BEGIN"))
|
|
659
|
+
return createPublicKey(key);
|
|
660
|
+
const raw = typeof key === "string" ? Buffer.from(key, "base64") : Buffer.from(key);
|
|
661
|
+
if (raw.length === 32)
|
|
662
|
+
return createPublicKey({ key: Buffer.concat([SPKI_PREFIX, raw]), format: "der", type: "spki" });
|
|
663
|
+
return createPublicKey({ key: raw, format: "der", type: "spki" });
|
|
664
|
+
}
|
|
665
|
+
function verifyReceipt(receipt, publicKey) {
|
|
666
|
+
const parsed = Receipt.safeParse(receipt);
|
|
667
|
+
if (!parsed.success)
|
|
668
|
+
return false;
|
|
669
|
+
try {
|
|
670
|
+
const { sig, ...unsigned } = parsed.data;
|
|
671
|
+
return verify(null, Buffer.from(canonicalize(unsigned), "utf8"), toPublicKey(publicKey), Buffer.from(sig, "base64"));
|
|
672
|
+
} catch {
|
|
673
|
+
return false;
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
function verifyReceiptWithKeys(receipt, keys) {
|
|
677
|
+
const parsed = Receipt.safeParse(receipt);
|
|
678
|
+
if (!parsed.success)
|
|
679
|
+
return { ok: false, key_id: null, reason: "invalid_receipt" };
|
|
680
|
+
const key = keys.find((k) => k.key_id === parsed.data.key_id);
|
|
681
|
+
if (!key)
|
|
682
|
+
return { ok: false, key_id: parsed.data.key_id, reason: "unknown_key" };
|
|
683
|
+
const ok = verifyReceipt(parsed.data, key.public_key_b64);
|
|
684
|
+
return { ok, key_id: key.key_id, reason: ok ? "ok" : "bad_signature" };
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
var SERVER_NAME = "portproof";
|
|
688
|
+
var SERVER_VERSION = "0.2.0";
|
|
689
|
+
var USER_AGENT = `portproof-mcp/${SERVER_VERSION}`;
|
|
690
|
+
var DEFAULT_API_URL = "https://api.portproof.org";
|
|
691
|
+
var DEFAULT_TIMEOUT_MS = 3e4;
|
|
692
|
+
var ROTATE_TIMEOUT_MS = 2e5;
|
|
693
|
+
var ROTATION_POLL_INTERVAL_MS = 5e3;
|
|
694
|
+
var ROTATION_TOTAL_WAIT_MS = 18e4;
|
|
695
|
+
function normaliseBaseUrl(raw) {
|
|
696
|
+
const trimmed = (raw ?? "").trim().replace(/\/+$/, "");
|
|
697
|
+
const origin = trimmed === "" ? DEFAULT_API_URL : trimmed;
|
|
698
|
+
return origin.endsWith("/v1") ? origin : `${origin}/v1`;
|
|
699
|
+
}
|
|
700
|
+
function shopOrigin(baseUrl) {
|
|
701
|
+
let url;
|
|
702
|
+
try {
|
|
703
|
+
url = new URL(baseUrl);
|
|
704
|
+
} catch {
|
|
705
|
+
return null;
|
|
706
|
+
}
|
|
707
|
+
if (!url.hostname.startsWith("api.")) return null;
|
|
708
|
+
const port = url.port === "" ? "" : `:${url.port}`;
|
|
709
|
+
return `${url.protocol}//${url.hostname.slice("api.".length)}${port}`;
|
|
710
|
+
}
|
|
711
|
+
function loadConfig(env = process.env) {
|
|
712
|
+
const key = env.PORTPROOF_API_KEY?.trim();
|
|
713
|
+
return {
|
|
714
|
+
baseUrl: normaliseBaseUrl(env.PORTPROOF_API_URL),
|
|
715
|
+
apiKey: key === void 0 || key === "" ? void 0 : key
|
|
716
|
+
};
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
var ApiProblemError = class _ApiProblemError extends Error {
|
|
720
|
+
body;
|
|
721
|
+
constructor(body) {
|
|
722
|
+
super(body.detail ? `${body.code}: ${body.detail}` : `${body.code}: ${body.title}`);
|
|
723
|
+
this.name = "ApiProblemError";
|
|
724
|
+
this.body = body;
|
|
725
|
+
}
|
|
726
|
+
get code() {
|
|
727
|
+
return this.body.code;
|
|
728
|
+
}
|
|
729
|
+
get status() {
|
|
730
|
+
return this.body.status;
|
|
731
|
+
}
|
|
732
|
+
static local(code, title, detail, status = 0) {
|
|
733
|
+
const body = { code, status, title };
|
|
734
|
+
if (detail !== void 0) body.detail = detail;
|
|
735
|
+
return new _ApiProblemError(body);
|
|
736
|
+
}
|
|
737
|
+
static fromResponse(status, json, rawText, url) {
|
|
738
|
+
const parsed = ProblemDetails.safeParse(json);
|
|
739
|
+
if (parsed.success) {
|
|
740
|
+
const p = parsed.data;
|
|
741
|
+
const body = { code: p.code, status: p.status, title: p.title };
|
|
742
|
+
if (p.detail !== void 0) body.detail = p.detail;
|
|
743
|
+
if (p.instance !== void 0) body.instance = p.instance;
|
|
744
|
+
if (p.retry_after !== void 0) body.retry_after = p.retry_after;
|
|
745
|
+
if (p.errors !== void 0) body.errors = p.errors;
|
|
746
|
+
return new _ApiProblemError(body);
|
|
747
|
+
}
|
|
748
|
+
if (json && typeof json === "object" && typeof json.code === "string") {
|
|
749
|
+
const j = json;
|
|
750
|
+
const body = {
|
|
751
|
+
code: j.code,
|
|
752
|
+
status,
|
|
753
|
+
title: typeof j.title === "string" ? j.title : `HTTP ${status}`
|
|
754
|
+
};
|
|
755
|
+
if (typeof j.detail === "string") body.detail = j.detail;
|
|
756
|
+
if (typeof j.retry_after === "number") body.retry_after = j.retry_after;
|
|
757
|
+
return new _ApiProblemError(body);
|
|
758
|
+
}
|
|
759
|
+
const snippet = rawText.length > 200 ? `${rawText.slice(0, 200)}\u2026` : rawText;
|
|
760
|
+
return new _ApiProblemError({
|
|
761
|
+
code: "http_error",
|
|
762
|
+
status,
|
|
763
|
+
title: `HTTP ${status}`,
|
|
764
|
+
detail: `Non-problem+json response from ${url}${snippet ? `: ${snippet}` : ""}`
|
|
765
|
+
});
|
|
766
|
+
}
|
|
767
|
+
static fromTransport(err, url, timeoutMs) {
|
|
768
|
+
const name = err instanceof Error ? err.name : "";
|
|
769
|
+
if (name === "TimeoutError" || name === "AbortError") {
|
|
770
|
+
return _ApiProblemError.local(
|
|
771
|
+
"timeout",
|
|
772
|
+
"API request timed out",
|
|
773
|
+
`${url} did not respond within ${timeoutMs} ms`
|
|
774
|
+
);
|
|
775
|
+
}
|
|
776
|
+
const cause = err instanceof Error ? err.cause instanceof Error ? err.cause.message : err.message : String(err);
|
|
777
|
+
return _ApiProblemError.local(
|
|
778
|
+
"api_unreachable",
|
|
779
|
+
"Portproof API unreachable",
|
|
780
|
+
`Could not reach ${url} (${cause}). Is the API running? Check PORTPROOF_API_URL.`
|
|
781
|
+
);
|
|
782
|
+
}
|
|
783
|
+
};
|
|
784
|
+
|
|
785
|
+
var PortPage = paginated(Port);
|
|
786
|
+
function apiOrigin(baseUrl) {
|
|
787
|
+
return baseUrl.replace(/\/+$/, "").replace(/\/v1$/, "");
|
|
788
|
+
}
|
|
789
|
+
var ApiClient = class {
|
|
790
|
+
baseUrl;
|
|
791
|
+
wellKnownKeysUrl;
|
|
792
|
+
shopOrigin;
|
|
793
|
+
hasApiKey;
|
|
794
|
+
apiKey;
|
|
795
|
+
fetchImpl;
|
|
796
|
+
timeoutMs;
|
|
797
|
+
warn;
|
|
798
|
+
constructor(opts) {
|
|
799
|
+
this.baseUrl = opts.baseUrl.replace(/\/+$/, "");
|
|
800
|
+
this.wellKnownKeysUrl = apiOrigin(this.baseUrl) + WELL_KNOWN_KEYS_PATH;
|
|
801
|
+
this.shopOrigin = shopOrigin(this.baseUrl);
|
|
802
|
+
this.apiKey = opts.apiKey;
|
|
803
|
+
this.hasApiKey = Boolean(opts.apiKey);
|
|
804
|
+
this.fetchImpl = opts.fetchImpl ?? globalThis.fetch;
|
|
805
|
+
this.timeoutMs = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
806
|
+
this.warn = opts.warn ?? ((m) => console.error(`[portproof-mcp] ${m}`));
|
|
807
|
+
}
|
|
808
|
+
async request(method, path, opts = {}) {
|
|
809
|
+
const url = new URL((opts.root ? apiOrigin(this.baseUrl) : this.baseUrl) + path);
|
|
810
|
+
for (const [k, v] of Object.entries(opts.query ?? {})) {
|
|
811
|
+
if (v !== void 0) url.searchParams.set(k, String(v));
|
|
812
|
+
}
|
|
813
|
+
const headers = {
|
|
814
|
+
accept: "application/json, application/problem+json",
|
|
815
|
+
"user-agent": USER_AGENT
|
|
816
|
+
};
|
|
817
|
+
const auth = opts.auth ?? "required";
|
|
818
|
+
if (this.apiKey) headers.authorization = `Bearer ${this.apiKey}`;
|
|
819
|
+
else if (auth === "required") {
|
|
820
|
+
throw ApiProblemError.local(
|
|
821
|
+
"config_missing",
|
|
822
|
+
"PORTPROOF_API_KEY is not set",
|
|
823
|
+
"This tool needs a customer API key. Set PORTPROOF_API_KEY (pk_test_... or pk_live_...) in the MCP server environment."
|
|
824
|
+
);
|
|
825
|
+
}
|
|
826
|
+
if (opts.body !== void 0) headers["content-type"] = "application/json";
|
|
827
|
+
if (opts.idempotencyKey !== void 0) headers["idempotency-key"] = opts.idempotencyKey;
|
|
828
|
+
const timeoutMs = opts.timeoutMs ?? this.timeoutMs;
|
|
829
|
+
let res;
|
|
830
|
+
try {
|
|
831
|
+
res = await this.fetchImpl(url, {
|
|
832
|
+
method,
|
|
833
|
+
headers,
|
|
834
|
+
body: opts.body !== void 0 ? JSON.stringify(opts.body) : void 0,
|
|
835
|
+
signal: AbortSignal.timeout(timeoutMs)
|
|
836
|
+
});
|
|
837
|
+
} catch (err) {
|
|
838
|
+
throw ApiProblemError.fromTransport(err, url.toString(), timeoutMs);
|
|
839
|
+
}
|
|
840
|
+
const text = await res.text();
|
|
841
|
+
const json = parseJson(text);
|
|
842
|
+
if (!res.ok) throw ApiProblemError.fromResponse(res.status, json, text, url.toString());
|
|
843
|
+
return { status: res.status, data: json };
|
|
844
|
+
}
|
|
845
|
+
lenient(schema, data, label) {
|
|
846
|
+
const parsed = schema.safeParse(data);
|
|
847
|
+
if (parsed.success) return parsed.data;
|
|
848
|
+
const first = parsed.error.issues[0];
|
|
849
|
+
this.warn(
|
|
850
|
+
`${label}: response does not match the shared schema (${first ? `${first.path.join(".") || "<root>"}: ${first.message}` : "unknown issue"}); passing it through`
|
|
851
|
+
);
|
|
852
|
+
return data;
|
|
853
|
+
}
|
|
854
|
+
async getPricing() {
|
|
855
|
+
const r = await this.request("GET", "/pricing", { auth: "optional" });
|
|
856
|
+
return this.lenient(PricingResponse, r.data, "GET /pricing");
|
|
857
|
+
}
|
|
858
|
+
async getInventory(query) {
|
|
859
|
+
const r = await this.request("GET", "/inventory", { query, auth: "optional" });
|
|
860
|
+
return this.lenient(InventoryResponse, r.data, "GET /inventory");
|
|
861
|
+
}
|
|
862
|
+
async postQuote(body) {
|
|
863
|
+
const r = await this.request("POST", "/quotes", {
|
|
864
|
+
body,
|
|
865
|
+
auth: "optional",
|
|
866
|
+
idempotencyKey: `quote-${randomUUID()}`
|
|
867
|
+
});
|
|
868
|
+
return this.lenient(QuoteResponse, r.data, "POST /quotes");
|
|
869
|
+
}
|
|
870
|
+
async getStatus() {
|
|
871
|
+
const r = await this.request("GET", "/status", { auth: "optional" });
|
|
872
|
+
return this.lenient(StatusResponse, r.data, "GET /status");
|
|
873
|
+
}
|
|
874
|
+
async getWellKnownKeys() {
|
|
875
|
+
const r = await this.request("GET", WELL_KNOWN_KEYS_PATH, { auth: "optional", root: true });
|
|
876
|
+
return this.lenient(WellKnownKeys, r.data, `GET ${WELL_KNOWN_KEYS_PATH}`);
|
|
877
|
+
}
|
|
878
|
+
async listPorts(query) {
|
|
879
|
+
const r = await this.request("GET", "/ports", { query });
|
|
880
|
+
return this.lenient(PortPage, r.data, "GET /ports");
|
|
881
|
+
}
|
|
882
|
+
async getPort(portId) {
|
|
883
|
+
const r = await this.request("GET", `/ports/${encodeURIComponent(portId)}`);
|
|
884
|
+
return this.lenient(Port, r.data, "GET /ports/{id}");
|
|
885
|
+
}
|
|
886
|
+
async createPort(body, idempotencyKey) {
|
|
887
|
+
const r = await this.request("POST", "/ports", { body, idempotencyKey });
|
|
888
|
+
return { status: r.status, data: this.lenient(Port, r.data, "POST /ports") };
|
|
889
|
+
}
|
|
890
|
+
async rotatePort(portId, idempotencyKey) {
|
|
891
|
+
const r = await this.request("POST", `/ports/${encodeURIComponent(portId)}/rotate`, {
|
|
892
|
+
body: { wait: true },
|
|
893
|
+
idempotencyKey,
|
|
894
|
+
timeoutMs: ROTATE_TIMEOUT_MS
|
|
895
|
+
});
|
|
896
|
+
return this.rotationOutcome(r, "POST /ports/{id}/rotate");
|
|
897
|
+
}
|
|
898
|
+
async getRotationStatus(portId, rotationId) {
|
|
899
|
+
const r = await this.request(
|
|
900
|
+
"GET",
|
|
901
|
+
`/ports/${encodeURIComponent(portId)}/rotation/${encodeURIComponent(rotationId)}`
|
|
902
|
+
);
|
|
903
|
+
return this.rotationOutcome(r, "GET /ports/{id}/rotation/{rotation_id}");
|
|
904
|
+
}
|
|
905
|
+
rotationOutcome(r, label) {
|
|
906
|
+
if (r.status === 202) {
|
|
907
|
+
return {
|
|
908
|
+
kind: "pending",
|
|
909
|
+
pending: this.lenient(RotatePendingResponse, r.data, `${label} 202`)
|
|
910
|
+
};
|
|
911
|
+
}
|
|
912
|
+
return { kind: "receipt", receipt: this.lenient(Receipt, r.data, label) };
|
|
913
|
+
}
|
|
914
|
+
async renewPort(portId, idempotencyKey) {
|
|
915
|
+
const r = await this.request("POST", `/ports/${encodeURIComponent(portId)}/renew`, {
|
|
916
|
+
idempotencyKey
|
|
917
|
+
});
|
|
918
|
+
return this.lenient(Subscription, r.data, "POST /ports/{id}/renew");
|
|
919
|
+
}
|
|
920
|
+
async setAutoRenew(portId, enabled, idempotencyKey) {
|
|
921
|
+
const r = await this.request("PUT", `/ports/${encodeURIComponent(portId)}/auto-renew`, {
|
|
922
|
+
body: { enabled },
|
|
923
|
+
idempotencyKey
|
|
924
|
+
});
|
|
925
|
+
return this.lenient(Subscription, r.data, "PUT /ports/{id}/auto-renew");
|
|
926
|
+
}
|
|
927
|
+
async setRotationSchedule(portId, intervalS, idempotencyKey) {
|
|
928
|
+
const r = await this.request("PUT", `/ports/${encodeURIComponent(portId)}/rotation`, {
|
|
929
|
+
body: { interval_s: intervalS },
|
|
930
|
+
idempotencyKey
|
|
931
|
+
});
|
|
932
|
+
return this.lenient(Port, r.data, "PUT /ports/{id}/rotation");
|
|
933
|
+
}
|
|
934
|
+
async getPassport(portId) {
|
|
935
|
+
const r = await this.request("GET", `/ports/${encodeURIComponent(portId)}/passport`, {
|
|
936
|
+
query: { format: "json" }
|
|
937
|
+
});
|
|
938
|
+
return this.lenient(Passport, r.data, "GET /ports/{id}/passport");
|
|
939
|
+
}
|
|
940
|
+
async getReceipt(receiptId) {
|
|
941
|
+
const r = await this.request("GET", `/receipts/${encodeURIComponent(receiptId)}`);
|
|
942
|
+
return this.lenient(Receipt, r.data, "GET /receipts/{id}");
|
|
943
|
+
}
|
|
944
|
+
async getTraffic() {
|
|
945
|
+
const r = await this.request("GET", "/traffic");
|
|
946
|
+
return this.lenient(TrafficInfo, r.data, "GET /traffic");
|
|
947
|
+
}
|
|
948
|
+
async getTrafficCountries() {
|
|
949
|
+
const r = await this.request("GET", "/traffic/countries", { auth: "optional" });
|
|
950
|
+
return this.lenient(TrafficCountries, r.data, "GET /traffic/countries");
|
|
951
|
+
}
|
|
952
|
+
async buildProxyUrl(body) {
|
|
953
|
+
const r = await this.request("POST", "/traffic/build-url", { body });
|
|
954
|
+
return this.lenient(BuildUrlResult, r.data, "POST /traffic/build-url");
|
|
955
|
+
}
|
|
956
|
+
async testConnection(body) {
|
|
957
|
+
const r = await this.request("POST", "/traffic/test", { body });
|
|
958
|
+
return this.lenient(TrafficTestResult, r.data, "POST /traffic/test");
|
|
959
|
+
}
|
|
960
|
+
async buyTraffic(body, idempotencyKey) {
|
|
961
|
+
const r = await this.request("POST", "/traffic/buy", { body, idempotencyKey });
|
|
962
|
+
return this.lenient(TrafficBuyResponse, r.data, "POST /traffic/buy");
|
|
963
|
+
}
|
|
964
|
+
async getUsage(query) {
|
|
965
|
+
const r = await this.request("GET", "/usage", { query });
|
|
966
|
+
return this.lenient(UsageResponse, r.data, "GET /usage");
|
|
967
|
+
}
|
|
968
|
+
};
|
|
969
|
+
function parseJson(text) {
|
|
970
|
+
if (text.trim() === "") return void 0;
|
|
971
|
+
try {
|
|
972
|
+
return JSON.parse(text);
|
|
973
|
+
} catch {
|
|
974
|
+
return void 0;
|
|
975
|
+
}
|
|
976
|
+
}
|
|
977
|
+
|
|
978
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
979
|
+
|
|
980
|
+
import { z as z13 } from "zod";
|
|
981
|
+
var IdempotencyKey = z13.string().min(1).max(255).regex(/^[\x21-\x7e]+$/, "printable ASCII without whitespace");
|
|
982
|
+
var ConfirmFields = {
|
|
983
|
+
confirm: z13.boolean().describe(
|
|
984
|
+
"Must be exactly true. Set it only after the user has agreed to this specific action."
|
|
985
|
+
),
|
|
986
|
+
idempotency_key: IdempotencyKey.describe(
|
|
987
|
+
"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."
|
|
988
|
+
)
|
|
989
|
+
};
|
|
990
|
+
var Confirmation = z13.object(ConfirmFields);
|
|
991
|
+
var SearchInventoryInput = z13.object({
|
|
992
|
+
country: CountryCode.optional().describe('ISO-3166 alpha-2, e.g. "LT"'),
|
|
993
|
+
carrier: z13.string().min(1).optional().describe("Carrier name filter, exactly as search_inventory returns it")
|
|
994
|
+
});
|
|
995
|
+
var PortSku = ProductSku.describe('SKU from get_pricing (field skus), e.g. "lt-4g"');
|
|
996
|
+
var QuoteInput = z13.object({
|
|
997
|
+
sku: PortSku,
|
|
998
|
+
term: Term,
|
|
999
|
+
quantity: z13.number().int().min(1).max(1e3)
|
|
1000
|
+
});
|
|
1001
|
+
var BuyPortInput = z13.object({
|
|
1002
|
+
sku: PortSku,
|
|
1003
|
+
term: Term,
|
|
1004
|
+
carrier_id: idSchema("car").optional().describe("Pin a carrier (from search_inventory); omit for any"),
|
|
1005
|
+
auto_renew: z13.boolean().optional(),
|
|
1006
|
+
...ConfirmFields
|
|
1007
|
+
});
|
|
1008
|
+
var ListPortsInput = z13.object({
|
|
1009
|
+
limit: z13.number().int().min(1).max(200).optional().describe("Page size, default 50"),
|
|
1010
|
+
cursor: z13.string().min(1).optional().describe("next_cursor from the previous page")
|
|
1011
|
+
});
|
|
1012
|
+
var PortIdInput = z13.object({ port_id: idSchema("prt") });
|
|
1013
|
+
var RotateInput = z13.object({
|
|
1014
|
+
port_id: idSchema("prt"),
|
|
1015
|
+
...ConfirmFields
|
|
1016
|
+
});
|
|
1017
|
+
var RenewPortInput = z13.object({
|
|
1018
|
+
port_id: idSchema("prt"),
|
|
1019
|
+
...ConfirmFields
|
|
1020
|
+
});
|
|
1021
|
+
var SetAutoRenewInput = z13.object({
|
|
1022
|
+
port_id: idSchema("prt"),
|
|
1023
|
+
enabled: z13.boolean().describe(
|
|
1024
|
+
"true = renew from the account balance before the term ends; false = the port ends at the end of its term"
|
|
1025
|
+
),
|
|
1026
|
+
...ConfirmFields
|
|
1027
|
+
});
|
|
1028
|
+
var SetRotationScheduleInput = z13.object({
|
|
1029
|
+
port_id: idSchema("prt"),
|
|
1030
|
+
interval_s: z13.number().int().refine((v) => v === 0 || v >= ROTATION_INTERVAL_MIN_S && v <= ROTATION_INTERVAL_MAX_S, {
|
|
1031
|
+
message: `interval_s must be 0 (sticky) or ${ROTATION_INTERVAL_MIN_S}..${ROTATION_INTERVAL_MAX_S}`
|
|
1032
|
+
}).describe(
|
|
1033
|
+
"0 = sticky (no scheduled rotation), otherwise seconds between rotations (60..86400)"
|
|
1034
|
+
),
|
|
1035
|
+
...ConfirmFields
|
|
1036
|
+
});
|
|
1037
|
+
var ReceiptIdInput = z13.object({
|
|
1038
|
+
receipt_id: idSchema("rot").describe("Rotation record id (equals the rotation id, rot_...)")
|
|
1039
|
+
});
|
|
1040
|
+
var GetUsageInput = z13.object({
|
|
1041
|
+
port_id: idSchema("prt"),
|
|
1042
|
+
from: IsoDateTime.optional().describe("ISO 8601 UTC, default: API default window"),
|
|
1043
|
+
to: IsoDateTime.optional(),
|
|
1044
|
+
granularity: UsageGranularity.optional().describe("minute | hour | day (default hour)")
|
|
1045
|
+
});
|
|
1046
|
+
var EmptyInput = z13.object({});
|
|
1047
|
+
var MOBILE_COUNTRIES_TEXT = TRAFFIC_MOBILE_COUNTRIES.map((c) => c === "GE" ? "GE = Georgia (country)" : c).join(", ");
|
|
1048
|
+
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`);
|
|
1049
|
+
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');
|
|
1050
|
+
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)");
|
|
1051
|
+
var BuildProxyUrlInput = z13.object({
|
|
1052
|
+
pool: TrafficPoolField,
|
|
1053
|
+
country: TrafficCountryField,
|
|
1054
|
+
rotation: TrafficRotationField,
|
|
1055
|
+
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."),
|
|
1056
|
+
protocol: z13.enum(["http", "socks5"]).optional().describe("Default http"),
|
|
1057
|
+
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.")
|
|
1058
|
+
});
|
|
1059
|
+
var TestConnectionInput = z13.object({
|
|
1060
|
+
pool: TrafficPoolField.optional(),
|
|
1061
|
+
country: TrafficCountryField.optional(),
|
|
1062
|
+
rotation: TrafficRotationField.optional()
|
|
1063
|
+
});
|
|
1064
|
+
var BuyTrafficInput = z13.object({
|
|
1065
|
+
gb: z13.number().int().min(1).max(1e3).optional().describe("Whole GB to add to the balance. Omit when trial is true."),
|
|
1066
|
+
trial: z13.boolean().optional().describe("Buy the one-off 0.5 GB trial instead (once per customer)."),
|
|
1067
|
+
accept_terms: z13.boolean().optional().describe(
|
|
1068
|
+
"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."
|
|
1069
|
+
),
|
|
1070
|
+
...ConfirmFields
|
|
1071
|
+
});
|
|
1072
|
+
var PortSummary = z13.object({
|
|
1073
|
+
id: idSchema("prt"),
|
|
1074
|
+
label: z13.string(),
|
|
1075
|
+
network: PortNetwork,
|
|
1076
|
+
state: z13.string(),
|
|
1077
|
+
site_code: z13.string().optional(),
|
|
1078
|
+
country: z13.string(),
|
|
1079
|
+
carrier: z13.string(),
|
|
1080
|
+
ip_stack: z13.string(),
|
|
1081
|
+
egress_v4: z13.string().nullable(),
|
|
1082
|
+
egress_v6_prefix: z13.string().nullable(),
|
|
1083
|
+
rotation_interval_s: z13.number().int(),
|
|
1084
|
+
last_rotation_at: z13.string().nullable(),
|
|
1085
|
+
usage_today_bytes: z13.number().int().nullable(),
|
|
1086
|
+
daily_cap_bytes: z13.number().int().nullable(),
|
|
1087
|
+
uptime_30d_pct: z13.number().nullable(),
|
|
1088
|
+
subscription_ends_at: z13.string().nullable(),
|
|
1089
|
+
auto_renew: z13.boolean().nullable(),
|
|
1090
|
+
operator: z13.enum(["owned", "partner"]),
|
|
1091
|
+
evidence: z13.enum(["signed", "observed"]),
|
|
1092
|
+
attestation: z13.enum(["signed", "observed"])
|
|
1093
|
+
});
|
|
1094
|
+
|
|
1095
|
+
function jsonResult(payload) {
|
|
1096
|
+
return { content: [{ type: "text", text: JSON.stringify(payload) }] };
|
|
1097
|
+
}
|
|
1098
|
+
function errorResult(body) {
|
|
1099
|
+
return { isError: true, content: [{ type: "text", text: JSON.stringify({ error: body }) }] };
|
|
1100
|
+
}
|
|
1101
|
+
function localError(code, title, detail) {
|
|
1102
|
+
return errorResult(ApiProblemError.local(code, title, detail).body);
|
|
1103
|
+
}
|
|
1104
|
+
function errorFromUnknown(err) {
|
|
1105
|
+
if (err instanceof ApiProblemError) return errorResult(err.body);
|
|
1106
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1107
|
+
return localError("unexpected_response", "Tool failed", message);
|
|
1108
|
+
}
|
|
1109
|
+
|
|
1110
|
+
var TOOL_NAMES = [
|
|
1111
|
+
"get_pricing",
|
|
1112
|
+
"search_inventory",
|
|
1113
|
+
"quote",
|
|
1114
|
+
"buy_port",
|
|
1115
|
+
"list_ports",
|
|
1116
|
+
"get_port",
|
|
1117
|
+
"rotate",
|
|
1118
|
+
"set_rotation_schedule",
|
|
1119
|
+
"renew_port",
|
|
1120
|
+
"set_auto_renew",
|
|
1121
|
+
"get_passport",
|
|
1122
|
+
"get_receipt",
|
|
1123
|
+
"get_usage",
|
|
1124
|
+
"get_status",
|
|
1125
|
+
"get_traffic",
|
|
1126
|
+
"list_countries",
|
|
1127
|
+
"build_proxy_url",
|
|
1128
|
+
"buy_traffic",
|
|
1129
|
+
"test_connection"
|
|
1130
|
+
];
|
|
1131
|
+
|
|
1132
|
+
var regionNames = new Intl.DisplayNames(["en"], { type: "region" });
|
|
1133
|
+
function countryName(code) {
|
|
1134
|
+
const cc = code.toUpperCase();
|
|
1135
|
+
if (cc === "GE") return "Georgia (country)";
|
|
1136
|
+
try {
|
|
1137
|
+
const name = regionNames.of(cc);
|
|
1138
|
+
if (name === void 0 || name === cc || /^unknown/i.test(name)) return cc;
|
|
1139
|
+
return name;
|
|
1140
|
+
} catch {
|
|
1141
|
+
return cc;
|
|
1142
|
+
}
|
|
1143
|
+
}
|
|
1144
|
+
function networkOf(port) {
|
|
1145
|
+
if (port.network !== void 0) return port.network;
|
|
1146
|
+
if (port.evidence === "observed") return "partner";
|
|
1147
|
+
return "own";
|
|
1148
|
+
}
|
|
1149
|
+
function neutralPortLabel(port) {
|
|
1150
|
+
const parts = [countryName(port.country)];
|
|
1151
|
+
if (port.carrierName) parts.push(port.carrierName);
|
|
1152
|
+
parts.push(port.id);
|
|
1153
|
+
return parts.join(" \xB7 ");
|
|
1154
|
+
}
|
|
1155
|
+
function urlHost(host) {
|
|
1156
|
+
if (host.includes(":") && !host.startsWith("[")) return `[${host}]`;
|
|
1157
|
+
return host;
|
|
1158
|
+
}
|
|
1159
|
+
function proxyUrl(scheme, credential) {
|
|
1160
|
+
const auth = `${encodeURIComponent(credential.username)}:${encodeURIComponent(credential.password)}`;
|
|
1161
|
+
return `${scheme}://${auth}@${urlHost(credential.host)}:${credential.port}`;
|
|
1162
|
+
}
|
|
1163
|
+
function proxyUrls(port) {
|
|
1164
|
+
const credentials = port.credentials;
|
|
1165
|
+
if (!credentials) return null;
|
|
1166
|
+
const shared = { username: credentials.username, password: credentials.password };
|
|
1167
|
+
const http = credentials.http ?? {
|
|
1168
|
+
host: port.listeners.host,
|
|
1169
|
+
port: port.listeners.http_port,
|
|
1170
|
+
...shared
|
|
1171
|
+
};
|
|
1172
|
+
const socks5 = credentials.socks5 ?? {
|
|
1173
|
+
host: port.listeners.host,
|
|
1174
|
+
port: port.listeners.socks_port,
|
|
1175
|
+
...shared
|
|
1176
|
+
};
|
|
1177
|
+
return { http: proxyUrl("http", http), socks5: proxyUrl("socks5", socks5) };
|
|
1178
|
+
}
|
|
1179
|
+
function carrierView(carrier, network) {
|
|
1180
|
+
if (network === "own") return carrier;
|
|
1181
|
+
const { mcc_mnc: _placeholder, ...rest } = carrier;
|
|
1182
|
+
return rest;
|
|
1183
|
+
}
|
|
1184
|
+
function portView(port) {
|
|
1185
|
+
const { site_code: siteCode, usage_today: usageToday, carrier, ...rest } = port;
|
|
1186
|
+
const network = networkOf(port);
|
|
1187
|
+
const label = neutralPortLabel({ id: port.id, country: port.country, carrierName: carrier.name });
|
|
1188
|
+
const view = { label, ...rest, carrier: carrierView(carrier, network), network };
|
|
1189
|
+
if (network === "own") {
|
|
1190
|
+
view.site_code = siteCode;
|
|
1191
|
+
view.usage_today = usageToday;
|
|
1192
|
+
}
|
|
1193
|
+
return view;
|
|
1194
|
+
}
|
|
1195
|
+
function summarisePort(port) {
|
|
1196
|
+
const network = networkOf(port);
|
|
1197
|
+
const summary = {
|
|
1198
|
+
id: port.id,
|
|
1199
|
+
label: neutralPortLabel({ id: port.id, country: port.country, carrierName: port.carrier.name }),
|
|
1200
|
+
network,
|
|
1201
|
+
state: port.state,
|
|
1202
|
+
country: port.country,
|
|
1203
|
+
carrier: port.carrier.name,
|
|
1204
|
+
ip_stack: port.carrier.ip_stack,
|
|
1205
|
+
egress_v4: port.egress_v4,
|
|
1206
|
+
egress_v6_prefix: port.egress_v6_prefix,
|
|
1207
|
+
rotation_interval_s: port.rotation_interval_s,
|
|
1208
|
+
last_rotation_at: port.last_rotation_at,
|
|
1209
|
+
usage_today_bytes: network === "own" ? port.usage_today.bytes : null,
|
|
1210
|
+
daily_cap_bytes: network === "own" ? port.usage_today.cap_bytes : null,
|
|
1211
|
+
uptime_30d_pct: port.uptime_30d_pct,
|
|
1212
|
+
subscription_ends_at: port.subscription?.ends_at ?? null,
|
|
1213
|
+
auto_renew: port.subscription?.auto_renew ?? null,
|
|
1214
|
+
operator: port.operator,
|
|
1215
|
+
evidence: port.evidence,
|
|
1216
|
+
attestation: port.attestation
|
|
1217
|
+
};
|
|
1218
|
+
if (network === "own") summary.site_code = port.site_code;
|
|
1219
|
+
return summary;
|
|
1220
|
+
}
|
|
1221
|
+
function passportView(passport) {
|
|
1222
|
+
const {
|
|
1223
|
+
site_code: siteCode,
|
|
1224
|
+
usage: usage2,
|
|
1225
|
+
sessions,
|
|
1226
|
+
sim_contract_class: simClass,
|
|
1227
|
+
ip_shared_with_own_ports: sharedWithOwn,
|
|
1228
|
+
carrier,
|
|
1229
|
+
...rest
|
|
1230
|
+
} = passport;
|
|
1231
|
+
const network = networkOf(passport);
|
|
1232
|
+
const label = neutralPortLabel({
|
|
1233
|
+
id: passport.port_id,
|
|
1234
|
+
country: passport.country,
|
|
1235
|
+
carrierName: carrier.name
|
|
1236
|
+
});
|
|
1237
|
+
const view = { label, ...rest, carrier: carrierView(carrier, network), network };
|
|
1238
|
+
if (network === "own") {
|
|
1239
|
+
view.site_code = siteCode;
|
|
1240
|
+
view.usage = usage2;
|
|
1241
|
+
view.sessions = sessions;
|
|
1242
|
+
view.sim_contract_class = simClass;
|
|
1243
|
+
view.ip_shared_with_own_ports = sharedWithOwn;
|
|
1244
|
+
}
|
|
1245
|
+
return view;
|
|
1246
|
+
}
|
|
1247
|
+
|
|
1248
|
+
function defineTool(def) {
|
|
1249
|
+
return def;
|
|
1250
|
+
}
|
|
1251
|
+
var READ_ONLY = {
|
|
1252
|
+
readOnlyHint: true,
|
|
1253
|
+
destructiveHint: false,
|
|
1254
|
+
idempotentHint: true,
|
|
1255
|
+
openWorldHint: false
|
|
1256
|
+
};
|
|
1257
|
+
var MUTATING = {
|
|
1258
|
+
readOnlyHint: false,
|
|
1259
|
+
destructiveHint: false,
|
|
1260
|
+
idempotentHint: true,
|
|
1261
|
+
openWorldHint: false
|
|
1262
|
+
};
|
|
1263
|
+
|
|
1264
|
+
var getPassport = defineTool({
|
|
1265
|
+
name: "get_passport",
|
|
1266
|
+
title: "Get port details",
|
|
1267
|
+
description: "Port details (API object: passport): label (country, carrier, port id), network (own = Own network, partner = Partner network), carrier, egress IPs, geo consensus, reputation, rotation and uptime statistics, throughput and exclusivity. Own network ports add the SIM contract class, usage against the daily cap and concurrent sessions (proof the port is not shared); a Partner network port has none of these, because nothing of ours runs on its device. Credentials are never included.",
|
|
1268
|
+
inputSchema: PortIdInput,
|
|
1269
|
+
annotations: READ_ONLY,
|
|
1270
|
+
run: async (args, ctx) => jsonResult(passportView(await ctx.api.getPassport(args.port_id)))
|
|
1271
|
+
});
|
|
1272
|
+
async function verifySignature(receipt, ctx) {
|
|
1273
|
+
try {
|
|
1274
|
+
const { keys } = await ctx.api.getWellKnownKeys();
|
|
1275
|
+
const v = verifyReceiptWithKeys(receipt, keys);
|
|
1276
|
+
return {
|
|
1277
|
+
verified: v.ok,
|
|
1278
|
+
key_id: v.key_id,
|
|
1279
|
+
reason: v.reason,
|
|
1280
|
+
keys_url: ctx.api.wellKnownKeysUrl
|
|
1281
|
+
};
|
|
1282
|
+
} catch (err) {
|
|
1283
|
+
const detail = err instanceof Error ? err.message : String(err);
|
|
1284
|
+
return {
|
|
1285
|
+
verified: null,
|
|
1286
|
+
key_id: null,
|
|
1287
|
+
reason: `keys_unavailable: ${detail}`,
|
|
1288
|
+
keys_url: ctx.api.wellKnownKeysUrl
|
|
1289
|
+
};
|
|
1290
|
+
}
|
|
1291
|
+
}
|
|
1292
|
+
var getReceipt = defineTool({
|
|
1293
|
+
name: "get_receipt",
|
|
1294
|
+
title: "Get rotation record",
|
|
1295
|
+
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.",
|
|
1296
|
+
inputSchema: ReceiptIdInput,
|
|
1297
|
+
annotations: READ_ONLY,
|
|
1298
|
+
run: async (args, ctx) => {
|
|
1299
|
+
const receipt = await ctx.api.getReceipt(args.receipt_id);
|
|
1300
|
+
const signature = await verifySignature(receipt, ctx);
|
|
1301
|
+
return jsonResult({ receipt, signature });
|
|
1302
|
+
}
|
|
1303
|
+
});
|
|
1304
|
+
|
|
1305
|
+
function checkConfirmation(args, action) {
|
|
1306
|
+
if (args.confirm !== true) {
|
|
1307
|
+
return localError(
|
|
1308
|
+
"confirmation_required",
|
|
1309
|
+
`Refused: ${action} needs confirm: true`,
|
|
1310
|
+
`${action} changes the account (money or live proxy state). Ask the user, then call again with confirm: true and an idempotency_key.`
|
|
1311
|
+
);
|
|
1312
|
+
}
|
|
1313
|
+
const key = IdempotencyKey.safeParse(args.idempotency_key);
|
|
1314
|
+
if (!key.success) {
|
|
1315
|
+
return localError(
|
|
1316
|
+
"idempotency_key_required",
|
|
1317
|
+
`Refused: ${action} needs an idempotency_key`,
|
|
1318
|
+
"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."
|
|
1319
|
+
);
|
|
1320
|
+
}
|
|
1321
|
+
return null;
|
|
1322
|
+
}
|
|
1323
|
+
|
|
1324
|
+
var PRICE_TABLE_SKUS = new Set(SkuSchema.options);
|
|
1325
|
+
function isPriceTableSku(sku) {
|
|
1326
|
+
return PRICE_TABLE_SKUS.has(sku);
|
|
1327
|
+
}
|
|
1328
|
+
async function findCatalogueListing(sku, ctx) {
|
|
1329
|
+
const pricing = await ctx.api.getPricing();
|
|
1330
|
+
return pricing.skus.find((row) => row.sku === sku && row.buy_via === "cart") ?? null;
|
|
1331
|
+
}
|
|
1332
|
+
function shopAnswer(listing, requestedTerm, ctx) {
|
|
1333
|
+
const shopPath = `/shop/${encodeURIComponent(listing.sku)}`;
|
|
1334
|
+
const shopUrl = ctx.api.shopOrigin === null ? null : `${ctx.api.shopOrigin}${shopPath}`;
|
|
1335
|
+
const steps = [
|
|
1336
|
+
"This port is bought in the Portproof web shop, not through buy_port: open shop_url, add it to the cart and check out.",
|
|
1337
|
+
"The port is delivered after payment and company verification."
|
|
1338
|
+
];
|
|
1339
|
+
if (listing.term !== requestedTerm) {
|
|
1340
|
+
steps.push(
|
|
1341
|
+
`This SKU is the ${listing.term} term; get_pricing lists the SKUs of the other terms.`
|
|
1342
|
+
);
|
|
1343
|
+
}
|
|
1344
|
+
return {
|
|
1345
|
+
buy_via: "cart",
|
|
1346
|
+
sku: listing.sku,
|
|
1347
|
+
term: listing.term,
|
|
1348
|
+
price_cents: listing.price_cents,
|
|
1349
|
+
currency: listing.currency,
|
|
1350
|
+
shop_path: shopPath,
|
|
1351
|
+
shop_url: shopUrl,
|
|
1352
|
+
next_step: steps.join(" ")
|
|
1353
|
+
};
|
|
1354
|
+
}
|
|
1355
|
+
var NOT_ON_SALE_DETAIL = "This SKU is not on sale for your organisation right now. get_pricing (field skus) lists what can be bought; dedicated ports are coming soon.";
|
|
1356
|
+
function notOnSale() {
|
|
1357
|
+
return localError("not_on_sale", "Not on sale", NOT_ON_SALE_DETAIL);
|
|
1358
|
+
}
|
|
1359
|
+
|
|
1360
|
+
async function costOf(port, input, ctx) {
|
|
1361
|
+
if (port.subscription) {
|
|
1362
|
+
return {
|
|
1363
|
+
cost_cents: port.subscription.price_cents,
|
|
1364
|
+
currency: port.subscription.currency,
|
|
1365
|
+
cost_source: "subscription"
|
|
1366
|
+
};
|
|
1367
|
+
}
|
|
1368
|
+
try {
|
|
1369
|
+
const q = await ctx.api.postQuote({ sku: input.sku, term: input.term, quantity: 1 });
|
|
1370
|
+
return { cost_cents: q.total_cents, currency: q.currency, cost_source: "quote" };
|
|
1371
|
+
} catch {
|
|
1372
|
+
return { cost_cents: null, currency: null, cost_source: "unknown" };
|
|
1373
|
+
}
|
|
1374
|
+
}
|
|
1375
|
+
async function answerCatalogueBuy(args, ctx) {
|
|
1376
|
+
const listing = await findCatalogueListing(args.sku, ctx);
|
|
1377
|
+
if (listing === null) return notOnSale();
|
|
1378
|
+
return jsonResult({ purchased: false, charged: false, ...shopAnswer(listing, args.term, ctx) });
|
|
1379
|
+
}
|
|
1380
|
+
var buyPort = defineTool({
|
|
1381
|
+
name: "buy_port",
|
|
1382
|
+
title: "Buy a port",
|
|
1383
|
+
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. Partner network SKUs (buy_via "cart" in get_pricing) are bought in the web shop: for them nothing is charged and the answer is purchased: false with shop_url, or not_on_sale when they are not on sale for your organisation.',
|
|
1384
|
+
inputSchema: BuyPortInput,
|
|
1385
|
+
annotations: MUTATING,
|
|
1386
|
+
run: async (args, ctx) => {
|
|
1387
|
+
const refusal = checkConfirmation(args, "buy_port");
|
|
1388
|
+
if (refusal) return refusal;
|
|
1389
|
+
if (!isPriceTableSku(args.sku)) return answerCatalogueBuy(args, ctx);
|
|
1390
|
+
const body = { sku: args.sku, term: args.term, quantity: 1 };
|
|
1391
|
+
if (args.carrier_id !== void 0) body.carrier_id = args.carrier_id;
|
|
1392
|
+
if (args.auto_renew !== void 0) body.auto_renew = args.auto_renew;
|
|
1393
|
+
const { data: port } = await ctx.api.createPort(body, args.idempotency_key);
|
|
1394
|
+
const cost = await costOf(port, args, ctx);
|
|
1395
|
+
return jsonResult({ ...cost, port: portView(port), proxy_urls: proxyUrls(port) });
|
|
1396
|
+
}
|
|
1397
|
+
});
|
|
1398
|
+
var listPorts = defineTool({
|
|
1399
|
+
name: "list_ports",
|
|
1400
|
+
title: "List ports",
|
|
1401
|
+
description: "List the ports owned by this API key as compact summaries: label (country, carrier, port id), network (own = Own network, partner = Partner network), state, egress IPs, rotation, usage, subscription end and auto_renew. No credentials: use get_port for those. Paginated: pass next_cursor as cursor to continue.",
|
|
1402
|
+
inputSchema: ListPortsInput,
|
|
1403
|
+
annotations: READ_ONLY,
|
|
1404
|
+
run: async (args, ctx) => {
|
|
1405
|
+
const query = {};
|
|
1406
|
+
if (args.limit !== void 0) query.limit = args.limit;
|
|
1407
|
+
if (args.cursor !== void 0) query.cursor = args.cursor;
|
|
1408
|
+
const page = await ctx.api.listPorts(query);
|
|
1409
|
+
return jsonResult({ data: page.data.map(summarisePort), next_cursor: page.next_cursor });
|
|
1410
|
+
}
|
|
1411
|
+
});
|
|
1412
|
+
var getPort = defineTool({
|
|
1413
|
+
name: "get_port",
|
|
1414
|
+
title: "Get port",
|
|
1415
|
+
description: "Full detail for one port: label, network, state, carrier, listeners, credentials (only if you own it), allowlist, egress IPs, rotation settings, usage today, uptime and subscription. Partner network ports also carry credentials.http and credentials.socks5 (each with its own host, port, login and password), capabilities (controls the port supports), renewal (price_cents, ends_at, auto_renew, can_renew) and rotation_url: the Portproof link that rotates the IP when fetched (60 s cooldown). proxy_urls gives ready-to-use http and socks5 URLs, each built from its own credential set.",
|
|
1416
|
+
inputSchema: PortIdInput,
|
|
1417
|
+
annotations: READ_ONLY,
|
|
1418
|
+
run: async (args, ctx) => {
|
|
1419
|
+
const port = await ctx.api.getPort(args.port_id);
|
|
1420
|
+
return jsonResult({ port: portView(port), proxy_urls: proxyUrls(port) });
|
|
1421
|
+
}
|
|
1422
|
+
});
|
|
1423
|
+
function sleep(ms) {
|
|
1424
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
1425
|
+
}
|
|
1426
|
+
async function awaitRotation(portId, rotationId, startedAt, ctx) {
|
|
1427
|
+
const intervalMs = ctx.rotationPoll?.intervalMs ?? ROTATION_POLL_INTERVAL_MS;
|
|
1428
|
+
const deadline = startedAt + (ctx.rotationPoll?.totalWaitMs ?? ROTATION_TOTAL_WAIT_MS);
|
|
1429
|
+
while (Date.now() + intervalMs <= deadline) {
|
|
1430
|
+
await sleep(intervalMs);
|
|
1431
|
+
const status = await ctx.api.getRotationStatus(portId, rotationId);
|
|
1432
|
+
if (status.kind === "receipt") return status.receipt;
|
|
1433
|
+
}
|
|
1434
|
+
return null;
|
|
1435
|
+
}
|
|
1436
|
+
function rotationDone(receipt) {
|
|
1437
|
+
return jsonResult({
|
|
1438
|
+
status: "done",
|
|
1439
|
+
rotated: receipt.rotated,
|
|
1440
|
+
reason: receipt.reason,
|
|
1441
|
+
old_ip4: receipt.old_ip4,
|
|
1442
|
+
new_ip4: receipt.new_ip4,
|
|
1443
|
+
elapsed_ms: receipt.elapsed_ms,
|
|
1444
|
+
receipt
|
|
1445
|
+
});
|
|
1446
|
+
}
|
|
1447
|
+
var rotate = defineTool({
|
|
1448
|
+
name: "rotate",
|
|
1449
|
+
title: "Rotate IP",
|
|
1450
|
+
description: "Rotate IP: request a new egress IP for a port and wait (up to 180 s in all) 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). A rotation still running after the API's first answer is followed on its status link. 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.",
|
|
1451
|
+
inputSchema: RotateInput,
|
|
1452
|
+
annotations: MUTATING,
|
|
1453
|
+
run: async (args, ctx) => {
|
|
1454
|
+
const refusal = checkConfirmation(args, "rotate");
|
|
1455
|
+
if (refusal) return refusal;
|
|
1456
|
+
const startedAt = Date.now();
|
|
1457
|
+
const result = await ctx.api.rotatePort(args.port_id, args.idempotency_key);
|
|
1458
|
+
if (result.kind === "receipt") return rotationDone(result.receipt);
|
|
1459
|
+
const rotationId = result.pending.rotation_id;
|
|
1460
|
+
const receipt = await awaitRotation(args.port_id, rotationId, startedAt, ctx);
|
|
1461
|
+
if (receipt !== null) return rotationDone(receipt);
|
|
1462
|
+
return jsonResult({
|
|
1463
|
+
status: "pending",
|
|
1464
|
+
rotation_id: rotationId,
|
|
1465
|
+
port_id: args.port_id,
|
|
1466
|
+
detail: "The rotation is still running. Do not rotate again: read get_receipt with this rotation_id in a minute, or get_port for the new egress IP."
|
|
1467
|
+
});
|
|
1468
|
+
}
|
|
1469
|
+
});
|
|
1470
|
+
var setRotationSchedule = defineTool({
|
|
1471
|
+
name: "set_rotation_schedule",
|
|
1472
|
+
title: "Set rotation schedule",
|
|
1473
|
+
description: "Set the automatic rotation interval for a port: 0 = sticky (never rotate on a schedule), otherwise 60..86400 seconds. The schedule keeps running between API calls. When get_port shows capabilities.rotation_interval: false the port has no schedule control. A change the network has not confirmed yet answers conflict (409); try again later. Changes live proxy state: requires confirm: true and an idempotency_key; refused otherwise. No charge.",
|
|
1474
|
+
inputSchema: SetRotationScheduleInput,
|
|
1475
|
+
annotations: MUTATING,
|
|
1476
|
+
run: async (args, ctx) => {
|
|
1477
|
+
const refusal = checkConfirmation(args, "set_rotation_schedule");
|
|
1478
|
+
if (refusal) return refusal;
|
|
1479
|
+
const port = await ctx.api.setRotationSchedule(
|
|
1480
|
+
args.port_id,
|
|
1481
|
+
args.interval_s,
|
|
1482
|
+
args.idempotency_key
|
|
1483
|
+
);
|
|
1484
|
+
return jsonResult({
|
|
1485
|
+
port_id: port.id,
|
|
1486
|
+
rotation_interval_s: port.rotation_interval_s,
|
|
1487
|
+
sticky: port.rotation_interval_s === 0,
|
|
1488
|
+
sticky_until: port.sticky_until,
|
|
1489
|
+
port: summarisePort(port)
|
|
1490
|
+
});
|
|
1491
|
+
}
|
|
1492
|
+
});
|
|
1493
|
+
var renewPort = defineTool({
|
|
1494
|
+
name: "renew_port",
|
|
1495
|
+
title: "Renew a port",
|
|
1496
|
+
description: "Dedicated ports: renew one port for one more term now, paid from the account balance. Read get_port first: renewal.can_renew says whether it can be renewed now and renewal.price_cents what one more term costs. A Partner network port is renewed on the network first and ends_at is the end it confirmed; there is no grace period after a term ends. CHARGES MONEY: requires confirm: true and an idempotency_key; refused otherwise. Errors: insufficient_funds (402, nothing charged: top up the balance), conflict (409, for example while an earlier renewal is still being confirmed), supply_source_error (502, the renewal was refused and the charge refunded). Returns ends_at and cost_cents.",
|
|
1497
|
+
inputSchema: RenewPortInput,
|
|
1498
|
+
annotations: MUTATING,
|
|
1499
|
+
run: async (args, ctx) => {
|
|
1500
|
+
const refusal = checkConfirmation(args, "renew_port");
|
|
1501
|
+
if (refusal) return refusal;
|
|
1502
|
+
const subscription = await ctx.api.renewPort(args.port_id, args.idempotency_key);
|
|
1503
|
+
return jsonResult({
|
|
1504
|
+
port_id: args.port_id,
|
|
1505
|
+
renewed: true,
|
|
1506
|
+
ends_at: subscription.ends_at,
|
|
1507
|
+
auto_renew: subscription.auto_renew,
|
|
1508
|
+
cost_cents: subscription.price_cents,
|
|
1509
|
+
currency: subscription.currency,
|
|
1510
|
+
subscription
|
|
1511
|
+
});
|
|
1512
|
+
}
|
|
1513
|
+
});
|
|
1514
|
+
var setAutoRenew = defineTool({
|
|
1515
|
+
name: "set_auto_renew",
|
|
1516
|
+
title: "Set auto-renew",
|
|
1517
|
+
description: "Dedicated ports: switch auto-renew of one port on or off. On: the port is renewed from the account balance shortly before its term ends (a short balance means it ends). Off: the port ends at the end of its current term, with no refund. Changes the subscription: requires confirm: true and an idempotency_key; refused otherwise. No charge now.",
|
|
1518
|
+
inputSchema: SetAutoRenewInput,
|
|
1519
|
+
annotations: MUTATING,
|
|
1520
|
+
run: async (args, ctx) => {
|
|
1521
|
+
const refusal = checkConfirmation(args, "set_auto_renew");
|
|
1522
|
+
if (refusal) return refusal;
|
|
1523
|
+
const subscription = await ctx.api.setAutoRenew(
|
|
1524
|
+
args.port_id,
|
|
1525
|
+
args.enabled,
|
|
1526
|
+
args.idempotency_key
|
|
1527
|
+
);
|
|
1528
|
+
return jsonResult({
|
|
1529
|
+
port_id: args.port_id,
|
|
1530
|
+
auto_renew: subscription.auto_renew,
|
|
1531
|
+
ends_at: subscription.ends_at,
|
|
1532
|
+
subscription
|
|
1533
|
+
});
|
|
1534
|
+
}
|
|
1535
|
+
});
|
|
1536
|
+
|
|
1537
|
+
var getPricing = defineTool({
|
|
1538
|
+
name: "get_pricing",
|
|
1539
|
+
title: "Get pricing",
|
|
1540
|
+
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, one SKU per term, sold only through the web shop (quote and buy_port answer them with the shop link). Partner network ports are listed only while they are on sale for your organisation, so the list can differ with and without an API key. No API key needed. Prices are quotes only; nothing is charged.',
|
|
1541
|
+
inputSchema: EmptyInput,
|
|
1542
|
+
annotations: READ_ONLY,
|
|
1543
|
+
run: async (_args, ctx) => jsonResult(await ctx.api.getPricing())
|
|
1544
|
+
});
|
|
1545
|
+
var searchInventory = defineTool({
|
|
1546
|
+
name: "search_inventory",
|
|
1547
|
+
title: "Search inventory",
|
|
1548
|
+
description: "Dedicated ports (coming soon, not on sale yet): available Own network 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. Partner network ports are not listed here: get_pricing lists the ones on sale for you. No API key needed.",
|
|
1549
|
+
inputSchema: SearchInventoryInput,
|
|
1550
|
+
annotations: READ_ONLY,
|
|
1551
|
+
run: async (args, ctx) => {
|
|
1552
|
+
const query = {};
|
|
1553
|
+
if (args.country !== void 0) query.country = args.country;
|
|
1554
|
+
if (args.carrier !== void 0) query.carrier = args.carrier;
|
|
1555
|
+
return jsonResult(await ctx.api.getInventory(query));
|
|
1556
|
+
}
|
|
1557
|
+
});
|
|
1558
|
+
var quote = defineTool({
|
|
1559
|
+
name: "quote",
|
|
1560
|
+
title: "Quote a purchase",
|
|
1561
|
+
description: 'Price a purchase before buying: unit price, volume discount and total in cents for a SKU, term (monthly | weekly | daily) and quantity. For a Partner network SKU (buy_via "cart") the answer is its listed price per port and the web shop link, where the cart shows the total; not_on_sale when it is not on sale for your organisation. Read-only, nothing is charged.',
|
|
1562
|
+
inputSchema: QuoteInput,
|
|
1563
|
+
annotations: READ_ONLY,
|
|
1564
|
+
run: async (args, ctx) => {
|
|
1565
|
+
if (!isPriceTableSku(args.sku)) {
|
|
1566
|
+
const listing = await findCatalogueListing(args.sku, ctx);
|
|
1567
|
+
if (listing === null) return notOnSale();
|
|
1568
|
+
return jsonResult({
|
|
1569
|
+
...shopAnswer(listing, args.term, ctx),
|
|
1570
|
+
quantity: args.quantity,
|
|
1571
|
+
charged: false
|
|
1572
|
+
});
|
|
1573
|
+
}
|
|
1574
|
+
const q = await ctx.api.postQuote({ sku: args.sku, term: args.term, quantity: args.quantity });
|
|
1575
|
+
return jsonResult({
|
|
1576
|
+
...q,
|
|
1577
|
+
sku: args.sku,
|
|
1578
|
+
term: args.term,
|
|
1579
|
+
quantity: args.quantity,
|
|
1580
|
+
charged: false
|
|
1581
|
+
});
|
|
1582
|
+
}
|
|
1583
|
+
});
|
|
1584
|
+
|
|
1585
|
+
var getStatus = defineTool({
|
|
1586
|
+
name: "get_status",
|
|
1587
|
+
title: "Get platform status",
|
|
1588
|
+
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.",
|
|
1589
|
+
inputSchema: EmptyInput,
|
|
1590
|
+
annotations: READ_ONLY,
|
|
1591
|
+
run: async (_args, ctx) => jsonResult(await ctx.api.getStatus())
|
|
1592
|
+
});
|
|
1593
|
+
|
|
1594
|
+
var MASK = "pak_****";
|
|
1595
|
+
var getTraffic = defineTool({
|
|
1596
|
+
name: "get_traffic",
|
|
1597
|
+
title: "Get the traffic balance",
|
|
1598
|
+
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.",
|
|
1599
|
+
inputSchema: EmptyInput,
|
|
1600
|
+
annotations: READ_ONLY,
|
|
1601
|
+
run: async (_args, ctx) => {
|
|
1602
|
+
const t = await ctx.api.getTraffic();
|
|
1603
|
+
return jsonResult({ ...t, credentials: t.credentials ? { ...t.credentials, password: MASK } : null });
|
|
1604
|
+
}
|
|
1605
|
+
});
|
|
1606
|
+
var listCountries = defineTool({
|
|
1607
|
+
name: "list_countries",
|
|
1608
|
+
title: "List countries online now",
|
|
1609
|
+
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.`,
|
|
1610
|
+
inputSchema: EmptyInput,
|
|
1611
|
+
annotations: READ_ONLY,
|
|
1612
|
+
run: async (_args, ctx) => jsonResult(await ctx.api.getTrafficCountries())
|
|
1613
|
+
});
|
|
1614
|
+
var buildProxyUrl = defineTool({
|
|
1615
|
+
name: "build_proxy_url",
|
|
1616
|
+
title: "Build a proxy URL",
|
|
1617
|
+
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.`,
|
|
1618
|
+
inputSchema: BuildProxyUrlInput,
|
|
1619
|
+
annotations: READ_ONLY,
|
|
1620
|
+
run: async (args, ctx) => {
|
|
1621
|
+
const { reveal_password, ...body } = args;
|
|
1622
|
+
const built = await ctx.api.buildProxyUrl(body);
|
|
1623
|
+
if (reveal_password === true) return jsonResult({ ...built, password_revealed: true });
|
|
1624
|
+
const secret = /:([^:@/]+)@/.exec(built.url)?.[1];
|
|
1625
|
+
const variants = secret ? [secret, decodeURIComponent(secret)] : [];
|
|
1626
|
+
const mask = (text) => variants.reduce((t, v) => v ? t.split(v).join(MASK) : t, text);
|
|
1627
|
+
const snippets = Object.fromEntries(Object.entries(built.snippets).map(([k, v]) => [k, mask(v)]));
|
|
1628
|
+
return jsonResult({ ...built, url: built.url_masked, snippets, password_revealed: false });
|
|
1629
|
+
}
|
|
1630
|
+
});
|
|
1631
|
+
var buyTraffic = defineTool({
|
|
1632
|
+
name: "buy_traffic",
|
|
1633
|
+
title: "Buy proxy traffic",
|
|
1634
|
+
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.",
|
|
1635
|
+
inputSchema: BuyTrafficInput,
|
|
1636
|
+
annotations: MUTATING,
|
|
1637
|
+
run: async (args, ctx) => {
|
|
1638
|
+
const refusal = checkConfirmation(args, "buy_traffic");
|
|
1639
|
+
if (refusal) return refusal;
|
|
1640
|
+
if (args.accept_terms !== true) {
|
|
1641
|
+
return localError(
|
|
1642
|
+
"terms_acceptance_required",
|
|
1643
|
+
"Refused: buy_traffic needs accept_terms: true",
|
|
1644
|
+
"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."
|
|
1645
|
+
);
|
|
1646
|
+
}
|
|
1647
|
+
if (args.trial === true === (args.gb !== void 0)) {
|
|
1648
|
+
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.");
|
|
1649
|
+
}
|
|
1650
|
+
const out = await ctx.api.buyTraffic(args.trial === true ? { trial: true, accept_terms: true } : { gb: args.gb, accept_terms: true }, args.idempotency_key);
|
|
1651
|
+
const o = out.order;
|
|
1652
|
+
const gb = o.items.reduce((n, it) => n + it.quantity * (it.sku_snapshot.sku === TRAFFIC_TRIAL.sku ? TRAFFIC_TRIAL.gb : 1), 0);
|
|
1653
|
+
return jsonResult({
|
|
1654
|
+
order_id: o.id,
|
|
1655
|
+
order_number: o.number,
|
|
1656
|
+
status: o.status,
|
|
1657
|
+
gb,
|
|
1658
|
+
unit_cents: o.items[0]?.unit_cents ?? null,
|
|
1659
|
+
cost_cents: o.total_cents,
|
|
1660
|
+
vat_cents: o.vat_cents,
|
|
1661
|
+
currency: o.currency,
|
|
1662
|
+
payment_id: out.payment_id,
|
|
1663
|
+
note: "The balance updates within a few seconds of payment; call get_traffic to see it."
|
|
1664
|
+
});
|
|
1665
|
+
}
|
|
1666
|
+
});
|
|
1667
|
+
var testConnection = defineTool({
|
|
1668
|
+
name: "test_connection",
|
|
1669
|
+
title: "Test a proxy connection",
|
|
1670
|
+
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).",
|
|
1671
|
+
inputSchema: TestConnectionInput,
|
|
1672
|
+
annotations: { ...READ_ONLY, idempotentHint: false, openWorldHint: true },
|
|
1673
|
+
run: async (args, ctx) => {
|
|
1674
|
+
const body = {};
|
|
1675
|
+
if (args.pool !== void 0) body["pool"] = args.pool;
|
|
1676
|
+
if (args.country !== void 0) body["country"] = args.country;
|
|
1677
|
+
if (args.rotation !== void 0) body["rotation"] = args.rotation;
|
|
1678
|
+
return jsonResult(await ctx.api.testConnection(body));
|
|
1679
|
+
}
|
|
1680
|
+
});
|
|
1681
|
+
|
|
1682
|
+
var getUsage = defineTool({
|
|
1683
|
+
name: "get_usage",
|
|
1684
|
+
title: "Get usage",
|
|
1685
|
+
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.",
|
|
1686
|
+
inputSchema: GetUsageInput,
|
|
1687
|
+
annotations: READ_ONLY,
|
|
1688
|
+
run: async (args, ctx) => {
|
|
1689
|
+
const query = {
|
|
1690
|
+
port_id: args.port_id
|
|
1691
|
+
};
|
|
1692
|
+
if (args.from !== void 0) query.from = args.from;
|
|
1693
|
+
if (args.to !== void 0) query.to = args.to;
|
|
1694
|
+
if (args.granularity !== void 0) query.granularity = args.granularity;
|
|
1695
|
+
const { data } = await ctx.api.getUsage(query);
|
|
1696
|
+
const totals = data.reduce(
|
|
1697
|
+
(acc, p) => ({
|
|
1698
|
+
bytes_in: acc.bytes_in + p.bytes_in,
|
|
1699
|
+
bytes_out: acc.bytes_out + p.bytes_out,
|
|
1700
|
+
conns_new: acc.conns_new + p.conns_new,
|
|
1701
|
+
rotations: acc.rotations + p.rotations,
|
|
1702
|
+
sessions_peak: Math.max(acc.sessions_peak, p.sessions_peak)
|
|
1703
|
+
}),
|
|
1704
|
+
{ bytes_in: 0, bytes_out: 0, conns_new: 0, rotations: 0, sessions_peak: 0 }
|
|
1705
|
+
);
|
|
1706
|
+
return jsonResult({
|
|
1707
|
+
port_id: args.port_id,
|
|
1708
|
+
granularity: args.granularity ?? "hour",
|
|
1709
|
+
from: args.from ?? null,
|
|
1710
|
+
to: args.to ?? null,
|
|
1711
|
+
buckets: data.length,
|
|
1712
|
+
totals,
|
|
1713
|
+
data
|
|
1714
|
+
});
|
|
1715
|
+
}
|
|
1716
|
+
});
|
|
1717
|
+
|
|
1718
|
+
async function runTool(def, args, ctx) {
|
|
1719
|
+
try {
|
|
1720
|
+
return await def.run(args, ctx);
|
|
1721
|
+
} catch (err) {
|
|
1722
|
+
return errorFromUnknown(err);
|
|
1723
|
+
}
|
|
1724
|
+
}
|
|
1725
|
+
var TOOLS = [
|
|
1726
|
+
getPricing,
|
|
1727
|
+
searchInventory,
|
|
1728
|
+
quote,
|
|
1729
|
+
buyPort,
|
|
1730
|
+
listPorts,
|
|
1731
|
+
getPort,
|
|
1732
|
+
rotate,
|
|
1733
|
+
setRotationSchedule,
|
|
1734
|
+
renewPort,
|
|
1735
|
+
setAutoRenew,
|
|
1736
|
+
getPassport,
|
|
1737
|
+
getReceipt,
|
|
1738
|
+
getUsage,
|
|
1739
|
+
getStatus,
|
|
1740
|
+
getTraffic,
|
|
1741
|
+
listCountries,
|
|
1742
|
+
buildProxyUrl,
|
|
1743
|
+
buyTraffic,
|
|
1744
|
+
testConnection
|
|
1745
|
+
];
|
|
1746
|
+
var registered = new Set(TOOLS.map((t) => t.name));
|
|
1747
|
+
for (const name of TOOL_NAMES) {
|
|
1748
|
+
if (!registered.has(name)) throw new Error(`tool registry is missing contract tool ${name}`);
|
|
1749
|
+
}
|
|
1750
|
+
if (registered.size !== TOOL_NAMES.length)
|
|
1751
|
+
throw new Error("tool registry has duplicate or extra tools");
|
|
1752
|
+
|
|
1753
|
+
var INSTRUCTIONS = [
|
|
1754
|
+
`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.`,
|
|
1755
|
+
"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).",
|
|
1756
|
+
'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, renew_port, set_auto_renew, 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. Name a port by its label (country, carrier, port id) and say only "Partner network" about where a Partner network port runs.',
|
|
1757
|
+
'Partner network ports are bought in the web shop, one SKU per term (buy_via "cart" in get_pricing): quote and buy_port answer them with shop_url and never charge. Their HTTP and SOCKS5 connections may differ in host, port, login and password: give the user proxy_urls from get_port, never a URL assembled from one set. rotation_url is the Portproof link that rotates the IP when fetched. They renew from the account balance: renewal in get_port says what one more term costs and whether it can be renewed now; auto-renew off means the port ends at the end of its term.',
|
|
1758
|
+
"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.",
|
|
1759
|
+
"buy_traffic, buy_port and renew_port charge money, rotate and set_rotation_schedule change live proxy state and set_auto_renew changes the subscription: 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.",
|
|
1760
|
+
'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, use_cart, port_cooldown with retry_after, conflict, insufficient_scope, not_found, validation_failed, ...) plus local codes such as not_on_sale.',
|
|
1761
|
+
"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."
|
|
1762
|
+
].join(" ");
|
|
1763
|
+
function createServer(ctx) {
|
|
1764
|
+
const server = new McpServer(
|
|
1765
|
+
{ name: SERVER_NAME, version: SERVER_VERSION },
|
|
1766
|
+
{ instructions: INSTRUCTIONS }
|
|
1767
|
+
);
|
|
1768
|
+
for (const def of TOOLS) {
|
|
1769
|
+
server.registerTool(
|
|
1770
|
+
def.name,
|
|
1771
|
+
{
|
|
1772
|
+
title: def.title,
|
|
1773
|
+
description: def.description,
|
|
1774
|
+
inputSchema: def.inputSchema,
|
|
1775
|
+
annotations: def.annotations
|
|
1776
|
+
},
|
|
1777
|
+
(args) => runTool(def, args, ctx)
|
|
1778
|
+
);
|
|
1779
|
+
}
|
|
1780
|
+
return server;
|
|
1781
|
+
}
|
|
1782
|
+
|
|
1783
|
+
function usage() {
|
|
1784
|
+
return [
|
|
1785
|
+
`portproof-mcp ${SERVER_VERSION} - Portproof API as MCP tools over stdio`,
|
|
1786
|
+
"",
|
|
1787
|
+
"Environment:",
|
|
1788
|
+
" PORTPROOF_API_KEY customer API key (pk_test_... / pk_live_...); optional for public tools",
|
|
1789
|
+
" PORTPROOF_API_URL API origin, default https://api.portproof.org (/v1 is appended)",
|
|
1790
|
+
"",
|
|
1791
|
+
`Tools: ${TOOL_NAMES.join(", ")}`
|
|
1792
|
+
].join("\n");
|
|
1793
|
+
}
|
|
1794
|
+
async function main(argv) {
|
|
1795
|
+
if (argv.includes("--help") || argv.includes("-h")) {
|
|
1796
|
+
console.error(usage());
|
|
1797
|
+
return;
|
|
1798
|
+
}
|
|
1799
|
+
if (argv.includes("--version") || argv.includes("-v")) {
|
|
1800
|
+
console.error(SERVER_VERSION);
|
|
1801
|
+
return;
|
|
1802
|
+
}
|
|
1803
|
+
const cfg = loadConfig();
|
|
1804
|
+
const api = new ApiClient({ baseUrl: cfg.baseUrl, apiKey: cfg.apiKey });
|
|
1805
|
+
const server = createServer({ api });
|
|
1806
|
+
await server.connect(new StdioServerTransport());
|
|
1807
|
+
console.error(
|
|
1808
|
+
`[portproof-mcp] ready on stdio: api=${cfg.baseUrl} key=${cfg.apiKey ? "set" : "missing (public tools only)"}`
|
|
1809
|
+
);
|
|
1810
|
+
}
|
|
1811
|
+
main(process.argv.slice(2)).catch((err) => {
|
|
1812
|
+
console.error("[portproof-mcp] fatal:", err instanceof Error ? err.stack ?? err.message : err);
|
|
1813
|
+
process.exit(1);
|
|
1814
|
+
});
|