@carrierllc/mcp 0.9.1 → 0.9.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -5
- package/dist/{chunk-V4CYEMLJ.js → chunk-KHNJNJX3.js} +327 -5
- package/dist/chunk-KHNJNJX3.js.map +1 -0
- package/dist/cli.js +2 -2
- package/dist/cli.js.map +1 -1
- package/dist/index.js +590 -213
- package/dist/index.js.map +1 -1
- package/package.json +4 -3
- package/plugin/carrier/README.md +1 -1
- package/dist/chunk-V4CYEMLJ.js.map +0 -1
package/dist/index.js
CHANGED
|
@@ -1,25 +1,32 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
3
|
CARRIER_VERSION,
|
|
4
|
+
OCS_MAX_USAGE_WINDOW_DAYS,
|
|
4
5
|
ROUTER_RULES,
|
|
5
6
|
TARGET_IDS,
|
|
6
7
|
acquireEndpointSlot,
|
|
8
|
+
applyParamRenames,
|
|
7
9
|
applySubscriberFilters,
|
|
8
10
|
buildListSubscriberParams,
|
|
9
11
|
buildRouterCatalog,
|
|
10
12
|
buildSite,
|
|
11
13
|
clerkCliDeps,
|
|
12
14
|
configureClerkInstance,
|
|
15
|
+
daysUntil,
|
|
13
16
|
deploySite,
|
|
14
17
|
esimStatusPerAccountParams,
|
|
18
|
+
extractDailyUsage,
|
|
15
19
|
extractEsimStatusCounts,
|
|
20
|
+
filterActiveSubscribers,
|
|
16
21
|
fleetScreen,
|
|
22
|
+
formatBytes,
|
|
17
23
|
formatProbes,
|
|
18
24
|
generateStorefrontLogo,
|
|
19
25
|
getLimitForEndpoint,
|
|
20
26
|
getRateLimitWindowCounts,
|
|
21
27
|
imsiFromSubscriberRecord,
|
|
22
28
|
isTargetId,
|
|
29
|
+
lastNDaysPeriod,
|
|
23
30
|
loadStorefrontBrand,
|
|
24
31
|
locationParams,
|
|
25
32
|
mergeEnvLocal,
|
|
@@ -33,11 +40,14 @@ import {
|
|
|
33
40
|
recurringPackageParams,
|
|
34
41
|
renderHtml,
|
|
35
42
|
repairPlanFor,
|
|
43
|
+
runWithBudget,
|
|
36
44
|
storefrontClerkUrls,
|
|
37
45
|
subscriberIdParams,
|
|
46
|
+
subscriberRows,
|
|
38
47
|
usageOverPeriodParams,
|
|
39
|
-
verifyStorefront
|
|
40
|
-
|
|
48
|
+
verifyStorefront,
|
|
49
|
+
withOcsListSummary
|
|
50
|
+
} from "./chunk-KHNJNJX3.js";
|
|
41
51
|
import "./chunk-SHKKVIIA.js";
|
|
42
52
|
|
|
43
53
|
// src/index.ts
|
|
@@ -69,32 +79,35 @@ var OcsClient = class {
|
|
|
69
79
|
async call(method, params = {}) {
|
|
70
80
|
await acquireEndpointSlot(this.token, method, "interactive");
|
|
71
81
|
const url = `${this.baseUrl}/v1?token=${this.token}`;
|
|
72
|
-
const body2 = JSON.stringify({ [method]: params });
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
if (json.status?.code !== 0) {
|
|
83
|
-
throw new OcsApiError(json.status?.code ?? -1, json.status?.msg ?? "Unknown error", method);
|
|
84
|
-
}
|
|
85
|
-
if (method === "getCustomerTariff" && json["listTariffRule"] !== void 0) {
|
|
86
|
-
return json["listTariffRule"];
|
|
87
|
-
}
|
|
88
|
-
if (method === "getSubscriberLocationByCellId") {
|
|
89
|
-
const byMethod = json[method];
|
|
90
|
-
if (byMethod !== void 0) {
|
|
91
|
-
return byMethod;
|
|
82
|
+
const body2 = JSON.stringify({ [method]: applyParamRenames(method, params) });
|
|
83
|
+
return runWithBudget(method, async (signal) => {
|
|
84
|
+
const res = await fetch(url, {
|
|
85
|
+
method: "POST",
|
|
86
|
+
headers: { "Content-Type": "application/json" },
|
|
87
|
+
body: body2,
|
|
88
|
+
signal
|
|
89
|
+
});
|
|
90
|
+
if (!res.ok) {
|
|
91
|
+
throw new OcsApiError(res.status, `HTTP ${res.status} ${res.statusText}`, method);
|
|
92
92
|
}
|
|
93
|
-
|
|
94
|
-
|
|
93
|
+
const json = await res.json();
|
|
94
|
+
if (json.status?.code !== 0) {
|
|
95
|
+
throw new OcsApiError(json.status?.code ?? -1, json.status?.msg ?? "Unknown error", method);
|
|
95
96
|
}
|
|
96
|
-
|
|
97
|
-
|
|
97
|
+
if (method === "getCustomerTariff" && json["listTariffRule"] !== void 0) {
|
|
98
|
+
return json["listTariffRule"];
|
|
99
|
+
}
|
|
100
|
+
if (method === "getSubscriberLocationByCellId") {
|
|
101
|
+
const byMethod = json[method];
|
|
102
|
+
if (byMethod !== void 0) {
|
|
103
|
+
return byMethod;
|
|
104
|
+
}
|
|
105
|
+
if (json["subscriberLocation"] !== void 0) {
|
|
106
|
+
return json["subscriberLocation"];
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
return json[method] ?? json;
|
|
110
|
+
});
|
|
98
111
|
}
|
|
99
112
|
};
|
|
100
113
|
|
|
@@ -189,6 +202,9 @@ function estimateCostUsd(u, rates = HAIKU_RATES) {
|
|
|
189
202
|
var DEFAULT_BEDROCK_REGION = "us-east-1";
|
|
190
203
|
var DEFAULT_MODEL_ID = "us.anthropic.claude-haiku-4-5-20251001-v1:0";
|
|
191
204
|
var BEDROCK_TIMEOUT_MS = 3e4;
|
|
205
|
+
var MAX_ATTEMPTS = 4;
|
|
206
|
+
var INITIAL_RETRY_MS = 100;
|
|
207
|
+
var MIN_ATTEMPT_BUDGET_MS = 250;
|
|
192
208
|
var BedrockRateLimitError = class extends Error {
|
|
193
209
|
isRateLimit = true;
|
|
194
210
|
/**
|
|
@@ -218,7 +234,11 @@ function client(creds) {
|
|
|
218
234
|
secretAccessKey: creds.secretAccessKey,
|
|
219
235
|
...creds.sessionToken ? { sessionToken: creds.sessionToken } : {},
|
|
220
236
|
region,
|
|
221
|
-
service: "bedrock"
|
|
237
|
+
service: "bedrock",
|
|
238
|
+
// See MAX_ATTEMPTS. aws4fetch's retry loop cannot be cancelled, so this
|
|
239
|
+
// layer owns the policy instead. Changing this back to the default 10
|
|
240
|
+
// silently restores the ~50s stall that timeoutMs is supposed to prevent.
|
|
241
|
+
retries: 0
|
|
222
242
|
}),
|
|
223
243
|
region,
|
|
224
244
|
modelId: creds.modelId ?? DEFAULT_MODEL_ID
|
|
@@ -234,12 +254,12 @@ function body(req) {
|
|
|
234
254
|
...req.tools ? { tools: req.tools, tool_choice: { type: "any" } } : {}
|
|
235
255
|
};
|
|
236
256
|
}
|
|
237
|
-
function headersFor(
|
|
257
|
+
function headersFor(attribution, accept) {
|
|
238
258
|
const headers = {
|
|
239
259
|
"Content-Type": "application/json",
|
|
240
260
|
Accept: accept
|
|
241
261
|
};
|
|
242
|
-
const meta =
|
|
262
|
+
const meta = attribution ? requestMetadata(attribution) : null;
|
|
243
263
|
if (meta) headers[REQUEST_METADATA_HEADER] = JSON.stringify(meta);
|
|
244
264
|
return headers;
|
|
245
265
|
}
|
|
@@ -260,28 +280,81 @@ async function fail(resp) {
|
|
|
260
280
|
}
|
|
261
281
|
throw new BedrockError(text || `Bedrock returned HTTP ${resp.status}`, resp.status);
|
|
262
282
|
}
|
|
283
|
+
function timedOut() {
|
|
284
|
+
const err7 = new Error("Bedrock call timed out before a usable response");
|
|
285
|
+
err7.name = "TimeoutError";
|
|
286
|
+
return err7;
|
|
287
|
+
}
|
|
288
|
+
function sleep(ms, signal) {
|
|
289
|
+
if (signal.aborted) return Promise.resolve();
|
|
290
|
+
return new Promise((resolve) => {
|
|
291
|
+
const done = () => {
|
|
292
|
+
clearTimeout(timer);
|
|
293
|
+
signal.removeEventListener("abort", done);
|
|
294
|
+
resolve();
|
|
295
|
+
};
|
|
296
|
+
const timer = setTimeout(done, ms);
|
|
297
|
+
signal.addEventListener("abort", done, { once: true });
|
|
298
|
+
});
|
|
299
|
+
}
|
|
300
|
+
function retryable(status) {
|
|
301
|
+
return status === 429 || status >= 500;
|
|
302
|
+
}
|
|
303
|
+
async function sendWithRetry(aws, url, init, signal, deadlineAt) {
|
|
304
|
+
let throttled = null;
|
|
305
|
+
for (let attempt = 0; attempt < MAX_ATTEMPTS; attempt++) {
|
|
306
|
+
if (signal.aborted) break;
|
|
307
|
+
let resp;
|
|
308
|
+
try {
|
|
309
|
+
resp = await aws.fetch(url, init);
|
|
310
|
+
} catch (err7) {
|
|
311
|
+
if (signal.aborted && throttled) return throttled;
|
|
312
|
+
throw err7;
|
|
313
|
+
}
|
|
314
|
+
if (!retryable(resp.status)) return resp;
|
|
315
|
+
throttled = resp;
|
|
316
|
+
if (attempt === MAX_ATTEMPTS - 1) break;
|
|
317
|
+
const backoff = Math.random() * INITIAL_RETRY_MS * 2 ** attempt;
|
|
318
|
+
if (Date.now() + backoff + MIN_ATTEMPT_BUDGET_MS > deadlineAt) break;
|
|
319
|
+
await sleep(backoff, signal);
|
|
320
|
+
}
|
|
321
|
+
if (throttled) return throttled;
|
|
322
|
+
throw timedOut();
|
|
323
|
+
}
|
|
263
324
|
async function invokeTool(creds, req, signal) {
|
|
325
|
+
const json = await invokeModelRaw(
|
|
326
|
+
creds,
|
|
327
|
+
body(req),
|
|
328
|
+
{ attribution: req.attribution, timeoutMs: req.timeoutMs },
|
|
329
|
+
signal
|
|
330
|
+
);
|
|
331
|
+
reportUsage(req, extractUsage(json));
|
|
332
|
+
const block = json.content?.find((b) => b.type === "tool_use");
|
|
333
|
+
return block?.name ? { name: block.name, input: block.input ?? {} } : null;
|
|
334
|
+
}
|
|
335
|
+
async function invokeModelRaw(creds, payload, opts = {}, signal) {
|
|
264
336
|
const { aws, region, modelId } = client(creds);
|
|
337
|
+
const timeoutMs = opts.timeoutMs ?? BEDROCK_TIMEOUT_MS;
|
|
338
|
+
const deadlineAt = Date.now() + timeoutMs;
|
|
265
339
|
const controller = new AbortController();
|
|
266
|
-
const timer = setTimeout(() => controller.abort(),
|
|
340
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
267
341
|
const onAbort = () => controller.abort();
|
|
268
342
|
signal?.addEventListener("abort", onAbort, { once: true });
|
|
269
343
|
try {
|
|
270
|
-
const
|
|
271
|
-
|
|
344
|
+
const resp = await sendWithRetry(
|
|
345
|
+
aws,
|
|
272
346
|
`https://bedrock-runtime.${region}.amazonaws.com/model/${encodeURIComponent(modelId)}/invoke`,
|
|
273
347
|
{
|
|
274
348
|
method: "POST",
|
|
275
|
-
headers,
|
|
276
|
-
body: JSON.stringify(
|
|
349
|
+
headers: headersFor(opts.attribution, "application/json"),
|
|
350
|
+
body: JSON.stringify(payload),
|
|
277
351
|
signal: controller.signal
|
|
278
|
-
}
|
|
352
|
+
},
|
|
353
|
+
controller.signal,
|
|
354
|
+
deadlineAt
|
|
279
355
|
);
|
|
280
356
|
if (!resp.ok) await fail(resp);
|
|
281
|
-
|
|
282
|
-
reportUsage(req, extractUsage(json));
|
|
283
|
-
const block = json.content?.find((b) => b.type === "tool_use");
|
|
284
|
-
return block?.name ? { name: block.name, input: block.input ?? {} } : null;
|
|
357
|
+
return await resp.json();
|
|
285
358
|
} finally {
|
|
286
359
|
clearTimeout(timer);
|
|
287
360
|
signal?.removeEventListener("abort", onAbort);
|
|
@@ -762,11 +835,12 @@ function wrapHandler(toolName, ocsMethod, requiredScope, ctx, handler) {
|
|
|
762
835
|
return result2;
|
|
763
836
|
};
|
|
764
837
|
}
|
|
765
|
-
async function ocsCall(env, token2, method, params = {}) {
|
|
838
|
+
async function ocsCall(env, token2, method, params = {}, listKind) {
|
|
766
839
|
const client2 = new OcsClient(env.CARRIER_OCS_BASE_URL, token2);
|
|
767
840
|
const result2 = await client2.call(method, params);
|
|
841
|
+
const payload = listKind ? withOcsListSummary(result2, listKind) : result2;
|
|
768
842
|
return {
|
|
769
|
-
content: [{ type: "text", text: JSON.stringify(
|
|
843
|
+
content: [{ type: "text", text: JSON.stringify(payload, null, 2) }]
|
|
770
844
|
};
|
|
771
845
|
}
|
|
772
846
|
async function resolveSubscriberByIccid(env, token2, iccid, cache) {
|
|
@@ -796,7 +870,7 @@ function registerAllTools(server2, ctx) {
|
|
|
796
870
|
"list_reseller_accounts",
|
|
797
871
|
{
|
|
798
872
|
title: "List Reseller Accounts",
|
|
799
|
-
description: "Use this to enumerate the accounts (sub-resellers or customer accounts) BELOW a reseller. Params: `resellerId` (integer, optional \u2014 omit to list accounts under the token owner's reseller). Returns: `{ reseller: [ { id, name, account: [ { id, name, balance, packageOnly } ] } ] }` \u2014 the accounts are NESTED under each reseller, not a flat top-level array. Do NOT use this to fetch a single subscriber's details \u2014 use `get_subscriber` instead. Do NOT use this to check eSIM activation counts \u2014 use `esim_status_per_account` for that. Do NOT use this to find a reseller's PARENT, balance or charging plans \u2014 those are above or on the reseller itself, not in this list; use `get_reseller_info`.",
|
|
873
|
+
description: "Use this to enumerate the accounts (sub-resellers or customer accounts) BELOW a reseller. Params: `resellerId` (integer, optional \u2014 omit to list accounts under the token owner's reseller). Returns: `{ summary, reseller: [ { id, name, account: [ { id, name, balance, packageOnly } ] } ] }` \u2014 the accounts are NESTED under each reseller, not a flat top-level array. `summary.total` therefore counts RESELLERS; the account count is `summary.nested.accounts`. For any question about accounts use that, never `total`. Do NOT use this to fetch a single subscriber's details \u2014 use `get_subscriber` instead. Do NOT use this to check eSIM activation counts \u2014 use `esim_status_per_account` for that. Do NOT use this to find a reseller's PARENT, balance or charging plans \u2014 those are above or on the reseller itself, not in this list; use `get_reseller_info`.",
|
|
800
874
|
inputSchema: {
|
|
801
875
|
resellerId: z.number().optional().describe("Filter to a specific reseller by ID (omit for token owner's reseller)")
|
|
802
876
|
},
|
|
@@ -810,7 +884,7 @@ function registerAllTools(server2, ctx) {
|
|
|
810
884
|
async ({ resellerId }, token2) => {
|
|
811
885
|
const params = {};
|
|
812
886
|
if (resellerId !== void 0) params.resellerId = resellerId;
|
|
813
|
-
return ocsCall(ctx.env, token2, "listResellerAccount", params);
|
|
887
|
+
return ocsCall(ctx.env, token2, "listResellerAccount", params, "resellers");
|
|
814
888
|
}
|
|
815
889
|
)
|
|
816
890
|
);
|
|
@@ -958,7 +1032,7 @@ function registerAllTools(server2, ctx) {
|
|
|
958
1032
|
"list_subscribers",
|
|
959
1033
|
{
|
|
960
1034
|
title: "List Subscribers",
|
|
961
|
-
description: "Use this to list subscribers with optional filters and pagination. Good for fleet enumeration, bulk status checks, and finding subscribers by account or status. OCS REQUIRES at least one search key \u2014 provide exactly one of: `imsi`, `iccid`, `activationCode`, `accountId`, or `msisdn`. Calling with no key will be rejected before reaching OCS. Params: `imsi` (string), `iccid` (string), `activationCode` (string), `accountId` (integer), `msisdn` (string), `status` (string, e.g. 'ACTIVE'/'SUSPENDED'), `offset` (integer, pagination \u2014 default 0), `limit` (integer, max results \u2014 always set to avoid unbounded fetches; recommended max 100 per call). Returns:
|
|
1035
|
+
description: "Use this to list subscribers with optional filters and pagination. Good for fleet enumeration, bulk status checks, and finding subscribers by account or status. OCS REQUIRES at least one search key \u2014 provide exactly one of: `imsi`, `iccid`, `activationCode`, `accountId`, or `msisdn`. Calling with no key will be rejected before reaching OCS. Params: `imsi` (string), `iccid` (string), `activationCode` (string), `accountId` (integer), `msisdn` (string), `status` (string, e.g. 'ACTIVE'/'SUSPENDED'), `offset` (integer, pagination \u2014 default 0), `limit` (integer, max results \u2014 always set to avoid unbounded fetches; recommended max 100 per call). Returns: `{ summary, subscriberList: [...] }` \u2014 subscriber summary records with ICCID, status and account, behind a server-counted `summary.total` (rows returned) and `summary.breakdown.status`. Answer 'how many' from `summary`, not by counting rows \u2014 but when `summary.moreAvailable` is set this is one page, so report `summary.upstreamTotal` for the fleet-wide figure, never `total`. Do NOT use this to fetch full details for a specific subscriber \u2014 use `get_subscriber` for that.",
|
|
962
1036
|
inputSchema: z.object({
|
|
963
1037
|
imsi: z.string().optional().describe("Filter by IMSI"),
|
|
964
1038
|
iccid: z.string().optional().describe("Filter by ICCID"),
|
|
@@ -987,7 +1061,12 @@ function registerAllTools(server2, ctx) {
|
|
|
987
1061
|
const raw = await client2.call("listSubscriber", params);
|
|
988
1062
|
const payload = applySubscriberFilters(raw, args);
|
|
989
1063
|
return {
|
|
990
|
-
content: [
|
|
1064
|
+
content: [
|
|
1065
|
+
{
|
|
1066
|
+
type: "text",
|
|
1067
|
+
text: JSON.stringify(withOcsListSummary(payload, "subscribers"), null, 2)
|
|
1068
|
+
}
|
|
1069
|
+
]
|
|
991
1070
|
};
|
|
992
1071
|
}
|
|
993
1072
|
)
|
|
@@ -1314,7 +1393,7 @@ function registerAllTools(server2, ctx) {
|
|
|
1314
1393
|
"list_subscriber_packages",
|
|
1315
1394
|
{
|
|
1316
1395
|
title: "List Subscriber Packages",
|
|
1317
|
-
description: "Use this to retrieve all prepaid packages currently assigned to a subscriber. Returns each package's allowance (data/voice/SMS), consumed usage, expiry date, status, and packageId. Always call this before any package modification tool (`modify_package_limits`, `modify_package_expiry`, `modify_package_status`, `delete_subscriber_package`) to confirm the correct packageId and current state. Params: `iccid` (subscriber identifier). Returns:
|
|
1396
|
+
description: "Use this to retrieve all prepaid packages currently assigned to a subscriber. Returns each package's allowance (data/voice/SMS), consumed usage, expiry date, status, and packageId. Always call this before any package modification tool (`modify_package_limits`, `modify_package_expiry`, `modify_package_status`, `delete_subscriber_package`) to confirm the correct packageId and current state. Params: `iccid` (subscriber identifier). Returns: `{ summary, prepaidPackage: [...] }` \u2014 package records with `packageId`, `name`, `status`, `dataLimit`, `dataUsed`, `expirationDate` and a `recurring` flag, behind a server-counted `summary.total` and a `summary.breakdown` over status and recurring. Answer 'how many' from `summary`, not by counting rows. Do NOT use this to browse the product catalog \u2014 use `list_package_templates` for that.",
|
|
1318
1397
|
inputSchema: { iccid: z.string().describe("The subscriber ICCID") },
|
|
1319
1398
|
annotations: { readOnlyHint: true }
|
|
1320
1399
|
},
|
|
@@ -1323,7 +1402,7 @@ function registerAllTools(server2, ctx) {
|
|
|
1323
1402
|
"listSubscriberPrepaidPackages",
|
|
1324
1403
|
TOOL_SCOPES["list_subscriber_packages"],
|
|
1325
1404
|
ctx,
|
|
1326
|
-
async ({ iccid }, token2) => ocsCall(ctx.env, token2, "listSubscriberPrepaidPackages", { iccid })
|
|
1405
|
+
async ({ iccid }, token2) => ocsCall(ctx.env, token2, "listSubscriberPrepaidPackages", { iccid }, "packages")
|
|
1327
1406
|
)
|
|
1328
1407
|
);
|
|
1329
1408
|
server2.registerTool(
|
|
@@ -1556,10 +1635,11 @@ function registerAllTools(server2, ctx) {
|
|
|
1556
1635
|
"list_package_templates",
|
|
1557
1636
|
{
|
|
1558
1637
|
title: "List Package Templates",
|
|
1559
|
-
description: "Use this to browse the product catalog of prepaid package templates available for assignment. Returns each template's name, data/voice/SMS limits, pricing, validity period, location zone, and recurring configuration. Call this before `assign_package` or `assign_recurring_package` to obtain valid `packageTemplateId` values. Params: `
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1638
|
+
description: "Use this to browse the product catalog of prepaid package templates available for assignment. Returns each template's name, data/voice/SMS limits, pricing, validity period, location zone, and recurring configuration. Call this before `assign_package` or `assign_recurring_package` to obtain valid `packageTemplateId` values. Params: none \u2014 returns the whole catalog for the token owner's reseller. Returns: `{ summary, template: [...] }`. `summary.total` is the number of templates, counted by the server over the complete catalog, and `summary.breakdown` counts the templates per `recurring` value and per location zone. Answer any 'how many' question from `summary` \u2014 it is authoritative, and counting the rows yourself gets it wrong. Each row carries `prepaidpackagetemplateid`, `prepaidpackagetemplatename`, `databyte` (bytes), `cost`, `perioddays`, `locationzoneid`, `rdbLocationZones.locationzonename` and `recurring`. Do NOT use this to list packages assigned to a specific subscriber \u2014 use `list_subscriber_packages`.",
|
|
1639
|
+
// No `accountId` — see the matching note in apps/mcp-server/src/tools.ts.
|
|
1640
|
+
// OCS rejected every call that carried it, and it is not a rename of any
|
|
1641
|
+
// of the five properties OCS does accept.
|
|
1642
|
+
inputSchema: {},
|
|
1563
1643
|
annotations: { readOnlyHint: true }
|
|
1564
1644
|
},
|
|
1565
1645
|
wrapHandler(
|
|
@@ -1567,11 +1647,7 @@ function registerAllTools(server2, ctx) {
|
|
|
1567
1647
|
"listPrepaidPackageTemplate",
|
|
1568
1648
|
TOOL_SCOPES["list_package_templates"],
|
|
1569
1649
|
ctx,
|
|
1570
|
-
async (
|
|
1571
|
-
const params = {};
|
|
1572
|
-
if (accountId !== void 0) params.accountId = accountId;
|
|
1573
|
-
return ocsCall(ctx.env, token2, "listPrepaidPackageTemplate", params);
|
|
1574
|
-
}
|
|
1650
|
+
async (_args, token2) => ocsCall(ctx.env, token2, "listPrepaidPackageTemplate", {}, "templates")
|
|
1575
1651
|
)
|
|
1576
1652
|
);
|
|
1577
1653
|
server2.registerTool(
|
|
@@ -1824,7 +1900,7 @@ function registerAllTools(server2, ctx) {
|
|
|
1824
1900
|
"get_tariff",
|
|
1825
1901
|
{
|
|
1826
1902
|
title: "Get Customer Tariff",
|
|
1827
|
-
description: `Use this to retrieve the tariff table for a reseller: per-country, per-operator wholesale data/voice/SMS rates. Useful for cost analysis, margin calculations, and identifying expensive roaming countries before steering decisions. Params: \`resellerId\` (integer, optional \u2014 omit to use the token owner's reseller); \`country\` (optional, ISO-3166 alpha-2 such as "nl" OR a full country name such as "Netherlands", case-insensitive); \`trafficType\` (optional, "data" | "voice" | "sms" \u2014 keeps only rules whose corresponding rate is greater than zero); \`verbose\` (optional boolean, default false \u2014 returns the full untouched OCS rule shape, capped at ${TARIFF_VERBOSE_MAX_RULES} rows, so combine it with the filters). Returns BY DEFAULT a projected result: \`totalRules\`, \`matchedRules\`, \`returnedRules\`, \`truncated\`, a hoisted \`currency\`, a \`fields\` legend and \`rules[]\` of short-keyed rows (\`iso\`, \`op\`, \`data\`, \`moCall\`, \`mtCall\`, \`moSms\`, \`mtSms\`, \`active\`). Zero rates and inactive flags are omitted per row. Nested operator detail (mccMncs, tadigs, continent, countryCode, utcOffset), the sponsor object, plan ids and the validity/discount flags are dropped unless \`verbose\` is true. Rows are capped at ${TARIFF_MAX_RULES}; when the cap bites, \`truncated\` is true and \`note\` says so explicitly and names the filter params \u2014 the result is never silently shortened. Filtering happens server-side on the complete OCS table, so a filtered call sees every matching rule. Response key in OCS is \`listTariffRule\`. Do NOT use this to assign a pricing plan to a subscriber \u2014 use \`modify_subscriber_mobile_plan\`. This shows the RESELLER's wholesale cost, not what end-users are charged.`,
|
|
1903
|
+
description: `Use this to retrieve the tariff table for a reseller: per-country, per-operator wholesale data/voice/SMS rates. Useful for cost analysis, margin calculations, and identifying expensive roaming countries before steering decisions. Params: \`resellerId\` (integer, optional \u2014 omit to use the token owner's reseller); \`country\` (optional, ISO-3166 alpha-2 such as "nl" OR a full country name such as "Netherlands", case-insensitive); \`trafficType\` (optional, "data" | "voice" | "sms" \u2014 keeps only rules whose corresponding rate is greater than zero); \`verbose\` (optional boolean, default false \u2014 returns the full untouched OCS rule shape, capped at ${TARIFF_VERBOSE_MAX_RULES} rows, so combine it with the filters). Returns BY DEFAULT a projected result: \`totalRules\`, \`matchedRules\`, \`returnedRules\`, \`truncated\`, a hoisted \`currency\`, a \`fields\` legend and \`rules[]\` of short-keyed rows (\`iso\`, \`op\`, \`data\`, \`moCall\`, \`mtCall\`, \`moSms\`, \`mtSms\`, \`active\`). Zero rates and inactive flags are omitted per row. Nested operator detail (mccMncs, tadigs, continent, countryCode, utcOffset), the sponsor object, plan ids and the validity/discount flags are dropped unless \`verbose\` is true. Rows are capped at ${TARIFF_MAX_RULES}; when the cap bites, \`truncated\` is true and \`note\` says so explicitly and names the filter params \u2014 the result is never silently shortened. Filtering happens server-side on the complete OCS table, so a filtered call sees every matching rule. Response key in OCS is \`listTariffRule\`. This is the slowest tool on the platform: the OCS table is ~3.4 MB and takes 3 s in a fast window and up to ~2 minutes in a slow one, so the call carries a 180 s budget. Expect to wait, and do not call it in a loop. Do NOT use this to assign a pricing plan to a subscriber \u2014 use \`modify_subscriber_mobile_plan\`. This shows the RESELLER's wholesale cost, not what end-users are charged.`,
|
|
1828
1904
|
inputSchema: {
|
|
1829
1905
|
resellerId: z.number().optional().describe("Reseller ID (omit to use token owner's reseller)"),
|
|
1830
1906
|
country: z.string().optional().describe(
|
|
@@ -1950,8 +2026,9 @@ var ocs_methods_default = {
|
|
|
1950
2026
|
scope: "read",
|
|
1951
2027
|
description: "Retrieve reseller details",
|
|
1952
2028
|
params: {
|
|
1953
|
-
resellerId: { type: "number", required: false }
|
|
2029
|
+
resellerId: { type: "number", required: false, ocs_field: "id" }
|
|
1954
2030
|
},
|
|
2031
|
+
params_note: "`resellerId` is the MCP-facing name. OCS getResellerInfo accepts exactly one property, `id`; @carrier/ocs-client translates it at the serialisation chokepoint (OCS_PARAM_RENAMES).",
|
|
1955
2032
|
response: {},
|
|
1956
2033
|
annotations: "readOnlyHint",
|
|
1957
2034
|
verified_against_server: true,
|
|
@@ -2356,9 +2433,8 @@ var ocs_methods_default = {
|
|
|
2356
2433
|
category: "templates",
|
|
2357
2434
|
scope: "read",
|
|
2358
2435
|
description: "List all prepaid package templates",
|
|
2359
|
-
params: {
|
|
2360
|
-
|
|
2361
|
-
},
|
|
2436
|
+
params: {},
|
|
2437
|
+
params_note: "No account filter. OCS listPrepaidPackageTemplate accepts only locationZoneId, templateId, destinationListId, resellerId, sponsorId. The former `accountId` param was rejected by OCS on every call and is not a rename of any of those five: accounts are a different id space from resellers and sponsors.",
|
|
2362
2438
|
response: {},
|
|
2363
2439
|
annotations: "readOnlyHint",
|
|
2364
2440
|
verified_against_server: true,
|
|
@@ -3492,26 +3568,9 @@ async function fetchActiveSubscribers(env, token2, accountId, resellerId) {
|
|
|
3492
3568
|
{ accountId: acctId }
|
|
3493
3569
|
);
|
|
3494
3570
|
if (subResult.error) return { data: null, error: subResult.error };
|
|
3495
|
-
|
|
3496
|
-
const list = Array.isArray(raw) ? raw : raw?.subscriberList ?? [];
|
|
3497
|
-
aggregated.push(...list);
|
|
3571
|
+
aggregated.push(...subscriberRows(subResult.data));
|
|
3498
3572
|
}
|
|
3499
|
-
|
|
3500
|
-
return { data: active, error: null };
|
|
3501
|
-
}
|
|
3502
|
-
function formatBytes(bytes) {
|
|
3503
|
-
if (bytes === 0) return "0 B";
|
|
3504
|
-
const units = ["B", "KB", "MB", "GB", "TB"];
|
|
3505
|
-
const i = Math.floor(Math.log(bytes) / Math.log(1024));
|
|
3506
|
-
return `${(bytes / Math.pow(1024, i)).toFixed(2)} ${units[i]}`;
|
|
3507
|
-
}
|
|
3508
|
-
function daysUntil(dateStr) {
|
|
3509
|
-
const now = /* @__PURE__ */ new Date();
|
|
3510
|
-
const target = new Date(dateStr);
|
|
3511
|
-
return Math.ceil((target.getTime() - now.getTime()) / (1e3 * 60 * 60 * 24));
|
|
3512
|
-
}
|
|
3513
|
-
function toISODate(d) {
|
|
3514
|
-
return d.toISOString().split("T")[0];
|
|
3573
|
+
return { data: filterActiveSubscribers(aggregated), error: null };
|
|
3515
3574
|
}
|
|
3516
3575
|
function result(text, isError = false) {
|
|
3517
3576
|
return { content: [{ type: "text", text }], ...isError ? { isError: true } : {} };
|
|
@@ -3591,10 +3650,10 @@ function registerIntelligenceTools(server2, ctx) {
|
|
|
3591
3650
|
}
|
|
3592
3651
|
}
|
|
3593
3652
|
const now = /* @__PURE__ */ new Date();
|
|
3594
|
-
const
|
|
3653
|
+
const eventWindow = lastNDaysPeriod(3, now);
|
|
3595
3654
|
const events = await safeCall(ctx.env, token2, "subscriberNetworkEventsOverPeriod", {
|
|
3596
3655
|
subscriber: { iccid },
|
|
3597
|
-
period:
|
|
3656
|
+
period: eventWindow
|
|
3598
3657
|
});
|
|
3599
3658
|
if (events.data && Array.isArray(events.data)) {
|
|
3600
3659
|
if (events.data.length === 0) {
|
|
@@ -3771,11 +3830,11 @@ Package-only accounts at 0 balance require no action.`);
|
|
|
3771
3830
|
}, async ({ iccid }) => {
|
|
3772
3831
|
const token2 = await ctx.getUserToken(ctx.props.sub);
|
|
3773
3832
|
const now = /* @__PURE__ */ new Date();
|
|
3774
|
-
const
|
|
3833
|
+
const usagePeriod = lastNDaysPeriod(OCS_MAX_USAGE_WINDOW_DAYS, now);
|
|
3775
3834
|
const [usageResult, pkgResult] = await Promise.all([
|
|
3776
3835
|
safeCall(ctx.env, token2, "subscriberUsageOverPeriod", {
|
|
3777
3836
|
subscriber: { iccid },
|
|
3778
|
-
period:
|
|
3837
|
+
period: usagePeriod
|
|
3779
3838
|
}),
|
|
3780
3839
|
safeCall(ctx.env, token2, "listSubscriberPrepaidPackages", { iccid })
|
|
3781
3840
|
]);
|
|
@@ -3783,13 +3842,8 @@ Package-only accounts at 0 balance require no action.`);
|
|
|
3783
3842
|
const sections = [`# Usage Anomaly Report: ${iccid}
|
|
3784
3843
|
`];
|
|
3785
3844
|
const anomalies = [];
|
|
3786
|
-
|
|
3787
|
-
|
|
3788
|
-
for (const entry of usageResult.data) {
|
|
3789
|
-
const bytes = Number(entry.dataBytes ?? entry.dataVolume ?? entry.totalData ?? 0);
|
|
3790
|
-
const date = String(entry.date ?? entry.day ?? "?");
|
|
3791
|
-
dailyData.push({ date, bytes });
|
|
3792
|
-
}
|
|
3845
|
+
const dailyData = extractDailyUsage(usageResult.data);
|
|
3846
|
+
if (dailyData.length > 0) {
|
|
3793
3847
|
if (dailyData.length >= 2) {
|
|
3794
3848
|
const volumes = dailyData.map((d) => d.bytes);
|
|
3795
3849
|
const mean = volumes.reduce((a, b) => a + b, 0) / volumes.length;
|
|
@@ -3857,24 +3911,21 @@ Package-only accounts at 0 balance require no action.`);
|
|
|
3857
3911
|
}, async ({ iccid }) => {
|
|
3858
3912
|
const token2 = await ctx.getUserToken(ctx.props.sub);
|
|
3859
3913
|
const now = /* @__PURE__ */ new Date();
|
|
3860
|
-
const
|
|
3914
|
+
const usagePeriod = lastNDaysPeriod(OCS_MAX_USAGE_WINDOW_DAYS, now);
|
|
3861
3915
|
const [usageResult, pkgResult, templatesResult] = await Promise.all([
|
|
3862
3916
|
safeCall(ctx.env, token2, "subscriberUsageOverPeriod", {
|
|
3863
3917
|
subscriber: { iccid },
|
|
3864
|
-
period:
|
|
3918
|
+
period: usagePeriod
|
|
3865
3919
|
}),
|
|
3866
3920
|
safeCall(ctx.env, token2, "listSubscriberPrepaidPackages", { iccid }),
|
|
3867
3921
|
safeCall(ctx.env, token2, "listPrepaidPackageTemplate", {})
|
|
3868
3922
|
]);
|
|
3869
3923
|
const sections = [`# Package Optimization: ${iccid}
|
|
3870
3924
|
`];
|
|
3925
|
+
const optimizeRows = extractDailyUsage(usageResult.data);
|
|
3871
3926
|
let avgDailyData = 0;
|
|
3872
|
-
if (
|
|
3873
|
-
|
|
3874
|
-
(sum, e) => sum + Number(e.dataBytes ?? e.dataVolume ?? e.totalData ?? 0),
|
|
3875
|
-
0
|
|
3876
|
-
);
|
|
3877
|
-
avgDailyData = totalData / usageResult.data.length;
|
|
3927
|
+
if (optimizeRows.length > 0) {
|
|
3928
|
+
avgDailyData = optimizeRows.reduce((sum, r) => sum + r.bytes, 0) / optimizeRows.length;
|
|
3878
3929
|
sections.push(`## Current Usage Pattern`);
|
|
3879
3930
|
sections.push(`- Average daily data: ${formatBytes(avgDailyData)}`);
|
|
3880
3931
|
sections.push(`- Projected monthly: ${formatBytes(avgDailyData * 30)}`);
|
|
@@ -3945,22 +3996,21 @@ Package-only accounts at 0 balance require no action.`);
|
|
|
3945
3996
|
}, async ({ iccid }) => {
|
|
3946
3997
|
const token2 = await ctx.getUserToken(ctx.props.sub);
|
|
3947
3998
|
const now = /* @__PURE__ */ new Date();
|
|
3948
|
-
const
|
|
3999
|
+
const usagePeriod = lastNDaysPeriod(OCS_MAX_USAGE_WINDOW_DAYS, now);
|
|
3949
4000
|
const [subResult, usageResult, pkgResult, activeResult] = await Promise.all([
|
|
3950
4001
|
safeCall(ctx.env, token2, "getSingleSubscriber", { iccid }),
|
|
3951
4002
|
safeCall(ctx.env, token2, "subscriberUsageOverPeriod", {
|
|
3952
4003
|
subscriber: { iccid },
|
|
3953
|
-
period:
|
|
4004
|
+
period: usagePeriod
|
|
3954
4005
|
}),
|
|
3955
4006
|
safeCall(ctx.env, token2, "listSubscriberPrepaidPackages", { iccid }),
|
|
3956
4007
|
safeCall(ctx.env, token2, "getSubscriberActivePeriod", { iccid })
|
|
3957
4008
|
]);
|
|
3958
4009
|
let riskScore = 0;
|
|
3959
4010
|
const factors = [];
|
|
3960
|
-
|
|
3961
|
-
|
|
3962
|
-
|
|
3963
|
-
);
|
|
4011
|
+
const churnRows = extractDailyUsage(usageResult.data);
|
|
4012
|
+
if (churnRows.length >= 3) {
|
|
4013
|
+
const volumes = churnRows.map((r) => r.bytes);
|
|
3964
4014
|
const firstHalf = volumes.slice(0, Math.floor(volumes.length / 2));
|
|
3965
4015
|
const secondHalf = volumes.slice(Math.floor(volumes.length / 2));
|
|
3966
4016
|
const avgFirst = firstHalf.reduce((a, b) => a + b, 0) / firstHalf.length;
|
|
@@ -3977,7 +4027,7 @@ Package-only accounts at 0 balance require no action.`);
|
|
|
3977
4027
|
factors.push({ factor: "Moderately declining usage", impact, detail: `Usage dropped ${Math.abs(trend * 100).toFixed(0)}%` });
|
|
3978
4028
|
}
|
|
3979
4029
|
}
|
|
3980
|
-
} else if (
|
|
4030
|
+
} else if (churnRows.length === 0) {
|
|
3981
4031
|
riskScore += 25;
|
|
3982
4032
|
factors.push({ factor: "No recent usage", impact: 25, detail: "Zero data activity in last 7 days" });
|
|
3983
4033
|
}
|
|
@@ -4272,7 +4322,7 @@ ${networksSorted.length} different networks in ${country} \u2014 possible steeri
|
|
|
4272
4322
|
const sections = [`# High Cost Subscriber Report
|
|
4273
4323
|
`];
|
|
4274
4324
|
const now = /* @__PURE__ */ new Date();
|
|
4275
|
-
const
|
|
4325
|
+
const usagePeriod = lastNDaysPeriod(OCS_MAX_USAGE_WINDOW_DAYS, now);
|
|
4276
4326
|
const highCostSubs = [];
|
|
4277
4327
|
const batchSize = 5;
|
|
4278
4328
|
const subs = subsResult.data.slice(0, sampleSize);
|
|
@@ -4284,19 +4334,13 @@ ${networksSorted.length} different networks in ${country} \u2014 possible steeri
|
|
|
4284
4334
|
const [usage, pkgs, loc] = await Promise.all([
|
|
4285
4335
|
safeCall(ctx.env, token2, "subscriberUsageOverPeriod", {
|
|
4286
4336
|
subscriber: { iccid },
|
|
4287
|
-
period:
|
|
4337
|
+
period: usagePeriod
|
|
4288
4338
|
}),
|
|
4289
4339
|
safeCall(ctx.env, token2, "listSubscriberPrepaidPackages", { iccid }),
|
|
4290
4340
|
safeCall(ctx.env, token2, "getSubscriberLocation", { iccid })
|
|
4291
4341
|
]);
|
|
4292
|
-
|
|
4293
|
-
|
|
4294
|
-
const totalBytes = usage.data.reduce(
|
|
4295
|
-
(sum, e) => sum + Number(e.dataBytes ?? e.dataVolume ?? e.totalData ?? 0),
|
|
4296
|
-
0
|
|
4297
|
-
);
|
|
4298
|
-
dailyAvgBytes = totalBytes / usage.data.length;
|
|
4299
|
-
}
|
|
4342
|
+
const dailyRows = extractDailyUsage(usage.data);
|
|
4343
|
+
const dailyAvgBytes = dailyRows.length > 0 ? dailyRows.reduce((sum, r) => sum + r.bytes, 0) / dailyRows.length : 0;
|
|
4300
4344
|
if (pkgs.data && Array.isArray(pkgs.data)) {
|
|
4301
4345
|
const activePkg = pkgs.data.find(
|
|
4302
4346
|
(p) => String(p.status ?? "").toUpperCase() === "ACTIVE"
|
|
@@ -4371,7 +4415,7 @@ ${networksSorted.length} different networks in ${country} \u2014 possible steeri
|
|
|
4371
4415
|
});
|
|
4372
4416
|
server2.registerTool("detect_country_entry", {
|
|
4373
4417
|
title: "Detect Country Entry",
|
|
4374
|
-
description: "Detects when a subscriber has entered a new country by reading networkInfo.lastMcc from getSingleSubscriber (one cheap OCS call \u2014 avoids the per-call cost of getSubscriberLocationByCellId). Resolves MCC \u2192 ISO 3166-1 alpha-2 and optionally diffs against a caller-supplied expectedCountry to return countryChanged. Designed for downstream country-entry upsell workflows (e.g. mango.talk SMS/push offers). COST NOTE: This tool makes exactly one OCS call per invocation. Consumers running polling crons MUST enforce their own rate floor \u2014 this layer provides no throttle.",
|
|
4418
|
+
description: "Detects when a subscriber has entered a new country by reading networkInfo.lastMcc from getSingleSubscriber (one cheap OCS call \u2014 avoids the per-call cost of getSubscriberLocationByCellId). Resolves MCC \u2192 ISO 3166-1 alpha-2 and optionally diffs against a caller-supplied expectedCountry to return countryChanged. Designed for downstream country-entry upsell workflows (e.g. mango.talk SMS/push offers). NOT AN AI TOOL: this is a deterministic MCC lookup and returns no model-written analysis, unlike the eight intelligence composites. COST NOTE: This tool makes exactly one OCS call per invocation. Consumers running polling crons MUST enforce their own rate floor \u2014 this layer provides no throttle.",
|
|
4375
4419
|
inputSchema: {
|
|
4376
4420
|
subscriber: z2.union([
|
|
4377
4421
|
z2.object({ subscriberId: z2.number() }).describe("Internal subscriber ID"),
|
|
@@ -5528,6 +5572,21 @@ function computeExpiresAt(askedAt) {
|
|
|
5528
5572
|
if (isNaN(asked)) return "";
|
|
5529
5573
|
return new Date(asked + PENDING_ASK_TTL_SECONDS * 1e3).toISOString();
|
|
5530
5574
|
}
|
|
5575
|
+
async function callerOwnsTask(ctx, taskId) {
|
|
5576
|
+
let raw;
|
|
5577
|
+
try {
|
|
5578
|
+
raw = await ctx.env.CARRIER_USERS.get(`steel_task:${taskId}`);
|
|
5579
|
+
} catch {
|
|
5580
|
+
return false;
|
|
5581
|
+
}
|
|
5582
|
+
if (!raw) return false;
|
|
5583
|
+
try {
|
|
5584
|
+
const task = JSON.parse(raw);
|
|
5585
|
+
return task.owner_sub !== void 0 && task.owner_sub === ctx.props.sub;
|
|
5586
|
+
} catch {
|
|
5587
|
+
return false;
|
|
5588
|
+
}
|
|
5589
|
+
}
|
|
5531
5590
|
function registerUiAgentAskTools(server2, ctx) {
|
|
5532
5591
|
server2.registerTool(
|
|
5533
5592
|
"ui_agent_reply",
|
|
@@ -5577,7 +5636,7 @@ function registerUiAgentAskTools(server2, ctx) {
|
|
|
5577
5636
|
}
|
|
5578
5637
|
const pendingKey = `${PENDING_ASK_PREFIX}${task_id}`;
|
|
5579
5638
|
const pendingRaw = await ctx.env.CARRIER_USERS.get(pendingKey);
|
|
5580
|
-
if (pendingRaw === null) {
|
|
5639
|
+
if (pendingRaw === null || !await callerOwnsTask(ctx, task_id)) {
|
|
5581
5640
|
return {
|
|
5582
5641
|
isError: true,
|
|
5583
5642
|
content: [
|
|
@@ -5719,6 +5778,8 @@ function registerUiAgentAskTools(server2, ctx) {
|
|
|
5719
5778
|
if (!raw) return null;
|
|
5720
5779
|
try {
|
|
5721
5780
|
const parsed = JSON.parse(raw);
|
|
5781
|
+
const taskId = parsed.task_id ?? name.slice(PENDING_ASK_PREFIX.length);
|
|
5782
|
+
if (!await callerOwnsTask(ctx, taskId)) return null;
|
|
5722
5783
|
return {
|
|
5723
5784
|
...parsed,
|
|
5724
5785
|
expires_at: computeExpiresAt(parsed.asked_at)
|
|
@@ -6696,6 +6757,33 @@ async function resolveBillingSub(env, orgId) {
|
|
|
6696
6757
|
return pointer ?? orgId;
|
|
6697
6758
|
}
|
|
6698
6759
|
|
|
6760
|
+
// src/scope-guard.ts
|
|
6761
|
+
function denyUnlessScoped(props2, toolName, scopes) {
|
|
6762
|
+
const required = scopes[toolName];
|
|
6763
|
+
if (required === void 0) {
|
|
6764
|
+
return {
|
|
6765
|
+
isError: true,
|
|
6766
|
+
content: [
|
|
6767
|
+
{
|
|
6768
|
+
type: "text",
|
|
6769
|
+
text: `Scope denied: tool '${toolName}' declares no scope, so it cannot be authorised. This is a server-side omission \u2014 add it to the tool's scope table.`
|
|
6770
|
+
}
|
|
6771
|
+
]
|
|
6772
|
+
};
|
|
6773
|
+
}
|
|
6774
|
+
const held = props2.scope ?? [];
|
|
6775
|
+
if (held.includes(required)) return null;
|
|
6776
|
+
return {
|
|
6777
|
+
isError: true,
|
|
6778
|
+
content: [
|
|
6779
|
+
{
|
|
6780
|
+
type: "text",
|
|
6781
|
+
text: `Scope denied: tool '${toolName}' requires '${required}' scope. Your token has: [${held.join(", ")}].`
|
|
6782
|
+
}
|
|
6783
|
+
]
|
|
6784
|
+
};
|
|
6785
|
+
}
|
|
6786
|
+
|
|
6699
6787
|
// src/wallet-tools.ts
|
|
6700
6788
|
var PACKS = [
|
|
6701
6789
|
{ id: "pack_500", eurCents: 5e4, bonusPct: 0 },
|
|
@@ -6874,8 +6962,11 @@ var WALLET_TOOL_SCOPES = {
|
|
|
6874
6962
|
wallet_topup_checkout: "write",
|
|
6875
6963
|
wallet_auto_topup: "write"
|
|
6876
6964
|
};
|
|
6877
|
-
function
|
|
6878
|
-
return {
|
|
6965
|
+
function okStructured(payload) {
|
|
6966
|
+
return {
|
|
6967
|
+
content: [{ type: "text", text: JSON.stringify(payload) }],
|
|
6968
|
+
structuredContent: payload
|
|
6969
|
+
};
|
|
6879
6970
|
}
|
|
6880
6971
|
function err(text) {
|
|
6881
6972
|
return { isError: true, content: [{ type: "text", text }] };
|
|
@@ -6889,9 +6980,22 @@ function registerWalletTools(server2, ctx) {
|
|
|
6889
6980
|
title: "Wallet Balance",
|
|
6890
6981
|
description: "Read the caller organisation's Carrier prepaid wallet: current balance and auto-top-up settings. The wallet is what Carrier bills your own eSIM spend against \u2014 it is not an OCS balance and not your MCP plan credits. Params: none (the organisation is taken from the session). Returns: { org_id, balance_eur_cents, auto_topup_enabled, auto_topup_threshold_cents, auto_topup_pack_cents } \u2014 all amounts in EUR cents, not euros. Do NOT use this for a reseller account balance in OCS \u2014 use `get_reseller_info` or `list_reseller_accounts`. Do NOT use this for a subscriber's OCS balance \u2014 use `get_subscriber`. Do NOT use this for MCP plan credits \u2014 use `credit_balance`. Do NOT use this for money held at Stripe \u2014 use `stripe_connect_balance`.",
|
|
6891
6982
|
inputSchema: {},
|
|
6983
|
+
// Derived from the object literal this handler builds below, not from the
|
|
6984
|
+
// description. Every field is unconditional on the success path and comes
|
|
6985
|
+
// straight off the Wallet record (wallet-client.ts:31-34), so all five are
|
|
6986
|
+
// required rather than optional.
|
|
6987
|
+
outputSchema: {
|
|
6988
|
+
org_id: z7.string().describe("Carrier organisation the wallet belongs to"),
|
|
6989
|
+
balance_eur_cents: z7.number().int().describe("Current balance in EUR cents"),
|
|
6990
|
+
auto_topup_enabled: z7.boolean(),
|
|
6991
|
+
auto_topup_threshold_cents: z7.number().int().describe("Balance at or below which an auto top-up fires, EUR cents"),
|
|
6992
|
+
auto_topup_pack_cents: z7.number().int().describe("Amount an auto top-up charges, EUR cents")
|
|
6993
|
+
},
|
|
6892
6994
|
annotations: annotationsFor("wallet_balance", "read")
|
|
6893
6995
|
},
|
|
6894
6996
|
async () => {
|
|
6997
|
+
const denied = denyUnlessScoped(props2, "wallet_balance", WALLET_TOOL_SCOPES);
|
|
6998
|
+
if (denied) return denied;
|
|
6895
6999
|
const orgId = defaultOrgId;
|
|
6896
7000
|
const start = Date.now();
|
|
6897
7001
|
try {
|
|
@@ -6905,15 +7009,13 @@ function registerWalletTools(server2, ctx) {
|
|
|
6905
7009
|
sub: props2.sub,
|
|
6906
7010
|
reseller_id: props2.reseller_id
|
|
6907
7011
|
});
|
|
6908
|
-
return
|
|
6909
|
-
|
|
6910
|
-
|
|
6911
|
-
|
|
6912
|
-
|
|
6913
|
-
|
|
6914
|
-
|
|
6915
|
-
})
|
|
6916
|
-
);
|
|
7012
|
+
return okStructured({
|
|
7013
|
+
org_id: orgId,
|
|
7014
|
+
balance_eur_cents: wallet.balance_eur_cents,
|
|
7015
|
+
auto_topup_enabled: wallet.auto_topup_enabled,
|
|
7016
|
+
auto_topup_threshold_cents: wallet.auto_topup_threshold_cents,
|
|
7017
|
+
auto_topup_pack_cents: wallet.auto_topup_pack_cents
|
|
7018
|
+
});
|
|
6917
7019
|
} catch (e) {
|
|
6918
7020
|
if (e instanceof WalletClientError) return err(`Wallet error: ${e.message}`);
|
|
6919
7021
|
Sentry2.captureException(e);
|
|
@@ -6931,23 +7033,48 @@ function registerWalletTools(server2, ctx) {
|
|
|
6931
7033
|
"Pack id (pack_500/pack_1000/pack_2500/pack_5000) or exact EUR-cents amount. Omit to list packs."
|
|
6932
7034
|
)
|
|
6933
7035
|
},
|
|
7036
|
+
/**
|
|
7037
|
+
* This tool really does return two different shapes, so the schema says
|
|
7038
|
+
* so rather than pretending otherwise. `pack` omitted returns the
|
|
7039
|
+
* catalogue arm (`packs`); `pack` supplied returns the session arm. An
|
|
7040
|
+
* `outputSchema` is an object shape, not a union type, so the two arms
|
|
7041
|
+
* are expressed as mutually exclusive optional groups and the pairing is
|
|
7042
|
+
* documented here. Marking either arm required would break the other.
|
|
7043
|
+
*/
|
|
7044
|
+
outputSchema: {
|
|
7045
|
+
packs: z7.array(
|
|
7046
|
+
z7.object({
|
|
7047
|
+
id: z7.string(),
|
|
7048
|
+
eur_cents: z7.number().int(),
|
|
7049
|
+
bonus_pct: z7.number(),
|
|
7050
|
+
bonus_eur_cents: z7.number().int(),
|
|
7051
|
+
credited_eur_cents: z7.number().int()
|
|
7052
|
+
})
|
|
7053
|
+
).optional().describe("Catalogue arm \u2014 present only when `pack` was omitted."),
|
|
7054
|
+
org_id: z7.string().optional().describe("Session arm."),
|
|
7055
|
+
pack_id: z7.string().optional().describe("Session arm."),
|
|
7056
|
+
pack_eur_cents: z7.number().int().optional().describe("Session arm."),
|
|
7057
|
+
bonus_eur_cents: z7.number().int().optional().describe("Session arm."),
|
|
7058
|
+
checkout_session_id: z7.string().optional().describe("Session arm."),
|
|
7059
|
+
checkout_url: z7.string().nullable().optional().describe("Session arm. Stripe may return null for a session URL.")
|
|
7060
|
+
},
|
|
6934
7061
|
annotations: annotationsFor("wallet_topup_checkout", "write")
|
|
6935
7062
|
},
|
|
6936
7063
|
async ({ pack }) => {
|
|
7064
|
+
const denied = denyUnlessScoped(props2, "wallet_topup_checkout", WALLET_TOOL_SCOPES);
|
|
7065
|
+
if (denied) return denied;
|
|
6937
7066
|
const orgId = defaultOrgId;
|
|
6938
7067
|
const start = Date.now();
|
|
6939
7068
|
if (!pack) {
|
|
6940
|
-
return
|
|
6941
|
-
|
|
6942
|
-
|
|
6943
|
-
|
|
6944
|
-
|
|
6945
|
-
|
|
6946
|
-
|
|
6947
|
-
|
|
6948
|
-
|
|
6949
|
-
})
|
|
6950
|
-
);
|
|
7069
|
+
return okStructured({
|
|
7070
|
+
packs: PACKS.map((p) => ({
|
|
7071
|
+
id: p.id,
|
|
7072
|
+
eur_cents: p.eurCents,
|
|
7073
|
+
bonus_pct: p.bonusPct,
|
|
7074
|
+
bonus_eur_cents: bonusCentsFor(p),
|
|
7075
|
+
credited_eur_cents: p.eurCents + bonusCentsFor(p)
|
|
7076
|
+
}))
|
|
7077
|
+
});
|
|
6951
7078
|
}
|
|
6952
7079
|
const selected = resolvePack(pack);
|
|
6953
7080
|
if (!selected) {
|
|
@@ -6966,16 +7093,14 @@ function registerWalletTools(server2, ctx) {
|
|
|
6966
7093
|
sub: props2.sub,
|
|
6967
7094
|
reseller_id: props2.reseller_id
|
|
6968
7095
|
});
|
|
6969
|
-
return
|
|
6970
|
-
|
|
6971
|
-
|
|
6972
|
-
|
|
6973
|
-
|
|
6974
|
-
|
|
6975
|
-
|
|
6976
|
-
|
|
6977
|
-
})
|
|
6978
|
-
);
|
|
7096
|
+
return okStructured({
|
|
7097
|
+
org_id: orgId,
|
|
7098
|
+
pack_id: selected.id,
|
|
7099
|
+
pack_eur_cents: selected.eurCents,
|
|
7100
|
+
bonus_eur_cents: bonusCentsFor(selected),
|
|
7101
|
+
checkout_session_id: session.id,
|
|
7102
|
+
checkout_url: session.url
|
|
7103
|
+
});
|
|
6979
7104
|
} catch (e) {
|
|
6980
7105
|
Sentry2.captureException(e);
|
|
6981
7106
|
return err(`Error: ${e instanceof Error ? e.message : "unknown"}`);
|
|
@@ -6990,9 +7115,31 @@ function registerWalletTools(server2, ctx) {
|
|
|
6990
7115
|
inputSchema: {
|
|
6991
7116
|
pack_cents: z7.number().int().positive().optional().describe("Override pack amount in EUR cents (must match a catalog pack).")
|
|
6992
7117
|
},
|
|
7118
|
+
/**
|
|
7119
|
+
* Only the charged arm is described here — `charged: false` returns
|
|
7120
|
+
* through `err()`, and the SDK skips output validation on an `isError`
|
|
7121
|
+
* result.
|
|
7122
|
+
*
|
|
7123
|
+
* `balance_after_cents` is optional and the description above is wrong to
|
|
7124
|
+
* promise it. runAutoTopup has a second success arm (wallet-tools.ts:337)
|
|
7125
|
+
* where the card was charged but crediting Atlas failed: it carries
|
|
7126
|
+
* `paymentIntentId` and a `reason`, and no balance. Requiring the field
|
|
7127
|
+
* would turn that arm — a real charge the caller must be told about —
|
|
7128
|
+
* into a validation error.
|
|
7129
|
+
*/
|
|
7130
|
+
outputSchema: {
|
|
7131
|
+
org_id: z7.string(),
|
|
7132
|
+
charged: z7.literal(true),
|
|
7133
|
+
credited_eur_cents: z7.number().int(),
|
|
7134
|
+
balance_after_cents: z7.number().int().optional().describe("Absent when the charge succeeded but crediting is pending."),
|
|
7135
|
+
payment_intent_id: z7.string().optional(),
|
|
7136
|
+
note: z7.string().optional().describe("Present when the charge landed but the credit is still pending.")
|
|
7137
|
+
},
|
|
6993
7138
|
annotations: annotationsFor("wallet_auto_topup", "write")
|
|
6994
7139
|
},
|
|
6995
7140
|
async ({ pack_cents }) => {
|
|
7141
|
+
const denied = denyUnlessScoped(props2, "wallet_auto_topup", WALLET_TOOL_SCOPES);
|
|
7142
|
+
if (denied) return denied;
|
|
6996
7143
|
const orgId = defaultOrgId;
|
|
6997
7144
|
const start = Date.now();
|
|
6998
7145
|
try {
|
|
@@ -7011,16 +7158,18 @@ function registerWalletTools(server2, ctx) {
|
|
|
7011
7158
|
JSON.stringify({ org_id: orgId, charged: false, reason: result2.reason })
|
|
7012
7159
|
);
|
|
7013
7160
|
}
|
|
7014
|
-
return
|
|
7015
|
-
|
|
7016
|
-
|
|
7017
|
-
|
|
7018
|
-
|
|
7019
|
-
|
|
7020
|
-
|
|
7021
|
-
|
|
7022
|
-
}
|
|
7023
|
-
|
|
7161
|
+
return okStructured({
|
|
7162
|
+
org_id: orgId,
|
|
7163
|
+
charged: true,
|
|
7164
|
+
credited_eur_cents: result2.creditedCents,
|
|
7165
|
+
// Spread rather than assign, so the key is absent instead of set to
|
|
7166
|
+
// `undefined`. JSON.stringify drops an undefined value anyway, so
|
|
7167
|
+
// assigning it would make `content` and `structuredContent` disagree
|
|
7168
|
+
// about whether the field is there.
|
|
7169
|
+
...result2.balanceAfterCents !== void 0 ? { balance_after_cents: result2.balanceAfterCents } : {},
|
|
7170
|
+
...result2.paymentIntentId !== void 0 ? { payment_intent_id: result2.paymentIntentId } : {},
|
|
7171
|
+
...result2.reason ? { note: result2.reason } : {}
|
|
7172
|
+
});
|
|
7024
7173
|
} catch (e) {
|
|
7025
7174
|
Sentry2.captureException(e);
|
|
7026
7175
|
return err(`Error: ${e instanceof Error ? e.message : "unknown"}`);
|
|
@@ -7046,25 +7195,30 @@ async function issueConfirmToken(env, sub, toolName) {
|
|
|
7046
7195
|
await env.OAUTH_KV.put(key, token2, { expirationTtl: 300 });
|
|
7047
7196
|
return token2;
|
|
7048
7197
|
}
|
|
7049
|
-
function
|
|
7198
|
+
function ok(text) {
|
|
7050
7199
|
return { content: [{ type: "text", text }] };
|
|
7051
7200
|
}
|
|
7052
7201
|
function err2(text) {
|
|
7053
7202
|
return { isError: true, content: [{ type: "text", text }] };
|
|
7054
7203
|
}
|
|
7055
7204
|
async function stripeGet(stripeKey, path, connectedAccountId) {
|
|
7056
|
-
const headers = {
|
|
7205
|
+
const headers = {
|
|
7206
|
+
Authorization: `Bearer ${stripeKey}`,
|
|
7207
|
+
"Stripe-Version": "2025-08-27.basil"
|
|
7208
|
+
};
|
|
7057
7209
|
if (connectedAccountId) headers["Stripe-Account"] = connectedAccountId;
|
|
7058
7210
|
const res = await fetch(`https://api.stripe.com${path}`, { headers });
|
|
7059
7211
|
const data = await res.json();
|
|
7060
7212
|
return { ok: res.ok, status: res.status, data };
|
|
7061
7213
|
}
|
|
7062
|
-
async function stripePost(stripeKey, path, params, connectedAccountId) {
|
|
7214
|
+
async function stripePost(stripeKey, path, params, connectedAccountId, idempotencyKey) {
|
|
7063
7215
|
const headers = {
|
|
7064
7216
|
Authorization: `Bearer ${stripeKey}`,
|
|
7217
|
+
"Stripe-Version": "2025-08-27.basil",
|
|
7065
7218
|
"Content-Type": "application/x-www-form-urlencoded"
|
|
7066
7219
|
};
|
|
7067
7220
|
if (connectedAccountId) headers["Stripe-Account"] = connectedAccountId;
|
|
7221
|
+
if (idempotencyKey) headers["Idempotency-Key"] = idempotencyKey;
|
|
7068
7222
|
const res = await fetch(`https://api.stripe.com${path}`, {
|
|
7069
7223
|
method: "POST",
|
|
7070
7224
|
headers,
|
|
@@ -7073,6 +7227,9 @@ async function stripePost(stripeKey, path, params, connectedAccountId) {
|
|
|
7073
7227
|
const data = await res.json();
|
|
7074
7228
|
return { ok: res.ok, status: res.status, data };
|
|
7075
7229
|
}
|
|
7230
|
+
function refundIdempotencyKey(sub, charge, amount, reason) {
|
|
7231
|
+
return `carrier-refund:${sub}:${charge}:${amount ?? "full"}:${reason ?? "none"}`;
|
|
7232
|
+
}
|
|
7076
7233
|
var STRIPE_CONNECT_TOOL_SCOPES = {
|
|
7077
7234
|
stripe_connect_status: "read",
|
|
7078
7235
|
stripe_connect_payouts: "read",
|
|
@@ -7103,6 +7260,8 @@ function registerStripeConnectTools(server2, ctx) {
|
|
|
7103
7260
|
annotations: annotationsFor("stripe_connect_status", "read")
|
|
7104
7261
|
},
|
|
7105
7262
|
async ({ operator_id }) => {
|
|
7263
|
+
const denied = denyUnlessScoped(props2, "stripe_connect_status", STRIPE_CONNECT_TOOL_SCOPES);
|
|
7264
|
+
if (denied) return denied;
|
|
7106
7265
|
if (operator_id !== void 0 && operator_id !== operatorId) {
|
|
7107
7266
|
if (!isPlatformOperator(env, props2.org_id)) {
|
|
7108
7267
|
return err2(
|
|
@@ -7118,7 +7277,7 @@ function registerStripeConnectTools(server2, ctx) {
|
|
|
7118
7277
|
if (!stripeKey) return err2("Stripe not configured");
|
|
7119
7278
|
const accountId = await env.CARRIER_USERS.get(opKvKey);
|
|
7120
7279
|
if (!accountId) {
|
|
7121
|
-
return
|
|
7280
|
+
return ok(JSON.stringify({ status: "not_connected", operator_id: opId }));
|
|
7122
7281
|
}
|
|
7123
7282
|
const result2 = await stripeGet(stripeKey, `/v1/accounts/${accountId}`);
|
|
7124
7283
|
if (!result2.ok) return err2(`Stripe error: ${JSON.stringify(result2.data)}`);
|
|
@@ -7133,7 +7292,7 @@ function registerStripeConnectTools(server2, ctx) {
|
|
|
7133
7292
|
sub: props2.sub,
|
|
7134
7293
|
reseller_id: props2.reseller_id
|
|
7135
7294
|
});
|
|
7136
|
-
return
|
|
7295
|
+
return ok(JSON.stringify({
|
|
7137
7296
|
status,
|
|
7138
7297
|
operator_id: opId,
|
|
7139
7298
|
account_id: accountId,
|
|
@@ -7163,6 +7322,8 @@ function registerStripeConnectTools(server2, ctx) {
|
|
|
7163
7322
|
annotations: annotationsFor("stripe_connect_payouts", "read")
|
|
7164
7323
|
},
|
|
7165
7324
|
async ({ limit, status }) => {
|
|
7325
|
+
const denied = denyUnlessScoped(props2, "stripe_connect_payouts", STRIPE_CONNECT_TOOL_SCOPES);
|
|
7326
|
+
if (denied) return denied;
|
|
7166
7327
|
const start = Date.now();
|
|
7167
7328
|
try {
|
|
7168
7329
|
const stripeKey = env.STRIPE_SECRET_KEY;
|
|
@@ -7182,7 +7343,7 @@ function registerStripeConnectTools(server2, ctx) {
|
|
|
7182
7343
|
sub: props2.sub,
|
|
7183
7344
|
reseller_id: props2.reseller_id
|
|
7184
7345
|
});
|
|
7185
|
-
return
|
|
7346
|
+
return ok(JSON.stringify({ payouts: result2.data.data, has_more: result2.data.has_more }));
|
|
7186
7347
|
} catch (e) {
|
|
7187
7348
|
Sentry3.captureException(e);
|
|
7188
7349
|
return err2(`Error: ${e instanceof Error ? e.message : "unknown"}`);
|
|
@@ -7198,6 +7359,8 @@ function registerStripeConnectTools(server2, ctx) {
|
|
|
7198
7359
|
annotations: annotationsFor("stripe_connect_balance", "read")
|
|
7199
7360
|
},
|
|
7200
7361
|
async () => {
|
|
7362
|
+
const denied = denyUnlessScoped(props2, "stripe_connect_balance", STRIPE_CONNECT_TOOL_SCOPES);
|
|
7363
|
+
if (denied) return denied;
|
|
7201
7364
|
const start = Date.now();
|
|
7202
7365
|
try {
|
|
7203
7366
|
const stripeKey = env.STRIPE_SECRET_KEY;
|
|
@@ -7215,7 +7378,7 @@ function registerStripeConnectTools(server2, ctx) {
|
|
|
7215
7378
|
sub: props2.sub,
|
|
7216
7379
|
reseller_id: props2.reseller_id
|
|
7217
7380
|
});
|
|
7218
|
-
return
|
|
7381
|
+
return ok(JSON.stringify({
|
|
7219
7382
|
account_id: accountId,
|
|
7220
7383
|
available: result2.data.available ?? [],
|
|
7221
7384
|
pending: result2.data.pending ?? []
|
|
@@ -7240,11 +7403,13 @@ function registerStripeConnectTools(server2, ctx) {
|
|
|
7240
7403
|
annotations: annotationsFor("stripe_connect_refund", "write")
|
|
7241
7404
|
},
|
|
7242
7405
|
async ({ charge_id, amount_cents, reason, confirm_token }) => {
|
|
7406
|
+
const denied = denyUnlessScoped(props2, "stripe_connect_refund", STRIPE_CONNECT_TOOL_SCOPES);
|
|
7407
|
+
if (denied) return denied;
|
|
7243
7408
|
const start = Date.now();
|
|
7244
7409
|
const toolName = "stripe_connect_refund";
|
|
7245
7410
|
if (!confirm_token) {
|
|
7246
7411
|
const token2 = await issueConfirmToken(env, props2.sub, toolName);
|
|
7247
|
-
return
|
|
7412
|
+
return ok(
|
|
7248
7413
|
`HARD_BLOCK: Refund ${amount_cents ? `${amount_cents} cents on` : "(full) on"} charge ${charge_id} requires confirmation.
|
|
7249
7414
|
confirm_token: ${token2}
|
|
7250
7415
|
Call again with confirm_token="${token2}" to execute. Token expires in 5 minutes.`
|
|
@@ -7262,7 +7427,13 @@ Call again with confirm_token="${token2}" to execute. Token expires in 5 minutes
|
|
|
7262
7427
|
const params = { charge: charge_id };
|
|
7263
7428
|
if (amount_cents) params.amount = String(amount_cents);
|
|
7264
7429
|
if (reason) params.reason = reason;
|
|
7265
|
-
const result2 = await stripePost(
|
|
7430
|
+
const result2 = await stripePost(
|
|
7431
|
+
stripeKey,
|
|
7432
|
+
"/v1/refunds",
|
|
7433
|
+
params,
|
|
7434
|
+
accountId,
|
|
7435
|
+
refundIdempotencyKey(props2.sub, charge_id, amount_cents, reason)
|
|
7436
|
+
);
|
|
7266
7437
|
if (!result2.ok) return err2(`Stripe error: ${JSON.stringify(result2.data)}`);
|
|
7267
7438
|
writeAudit(env, {
|
|
7268
7439
|
tool_name: toolName,
|
|
@@ -7273,7 +7444,7 @@ Call again with confirm_token="${token2}" to execute. Token expires in 5 minutes
|
|
|
7273
7444
|
sub: props2.sub,
|
|
7274
7445
|
reseller_id: props2.reseller_id
|
|
7275
7446
|
});
|
|
7276
|
-
return
|
|
7447
|
+
return ok(`Refund issued: ${result2.data.id} \u2014 status: ${result2.data.status}`);
|
|
7277
7448
|
} catch (e) {
|
|
7278
7449
|
Sentry3.captureException(e);
|
|
7279
7450
|
return err2(`Error: ${e instanceof Error ? e.message : "unknown"}`);
|
|
@@ -7292,6 +7463,8 @@ Call again with confirm_token="${token2}" to execute. Token expires in 5 minutes
|
|
|
7292
7463
|
annotations: annotationsFor("stripe_connect_dispute_list", "read")
|
|
7293
7464
|
},
|
|
7294
7465
|
async ({ limit, status }) => {
|
|
7466
|
+
const denied = denyUnlessScoped(props2, "stripe_connect_dispute_list", STRIPE_CONNECT_TOOL_SCOPES);
|
|
7467
|
+
if (denied) return denied;
|
|
7295
7468
|
const start = Date.now();
|
|
7296
7469
|
try {
|
|
7297
7470
|
const stripeKey = env.STRIPE_SECRET_KEY;
|
|
@@ -7311,7 +7484,7 @@ Call again with confirm_token="${token2}" to execute. Token expires in 5 minutes
|
|
|
7311
7484
|
sub: props2.sub,
|
|
7312
7485
|
reseller_id: props2.reseller_id
|
|
7313
7486
|
});
|
|
7314
|
-
return
|
|
7487
|
+
return ok(JSON.stringify({ disputes: result2.data.data, has_more: result2.data.has_more }));
|
|
7315
7488
|
} catch (e) {
|
|
7316
7489
|
Sentry3.captureException(e);
|
|
7317
7490
|
return err2(`Error: ${e instanceof Error ? e.message : "unknown"}`);
|
|
@@ -7330,6 +7503,13 @@ Call again with confirm_token="${token2}" to execute. Token expires in 5 minutes
|
|
|
7330
7503
|
annotations: annotationsFor("radar_review_list", "read")
|
|
7331
7504
|
},
|
|
7332
7505
|
async ({ open_only, limit }) => {
|
|
7506
|
+
const denied = denyUnlessScoped(props2, "radar_review_list", STRIPE_CONNECT_TOOL_SCOPES);
|
|
7507
|
+
if (denied) return denied;
|
|
7508
|
+
if (!isPlatformOperator(env, props2.org_id)) {
|
|
7509
|
+
return err2(
|
|
7510
|
+
"This tool acts on Carrier's platform Stripe account and is restricted to Carrier's own organization."
|
|
7511
|
+
);
|
|
7512
|
+
}
|
|
7333
7513
|
const start = Date.now();
|
|
7334
7514
|
try {
|
|
7335
7515
|
const stripeKey = env.STRIPE_SECRET_KEY;
|
|
@@ -7347,7 +7527,7 @@ Call again with confirm_token="${token2}" to execute. Token expires in 5 minutes
|
|
|
7347
7527
|
sub: props2.sub,
|
|
7348
7528
|
reseller_id: props2.reseller_id
|
|
7349
7529
|
});
|
|
7350
|
-
return
|
|
7530
|
+
return ok(JSON.stringify({ reviews: result2.data.data, has_more: result2.data.has_more }));
|
|
7351
7531
|
} catch (e) {
|
|
7352
7532
|
Sentry3.captureException(e);
|
|
7353
7533
|
return err2(`Error: ${e instanceof Error ? e.message : "unknown"}`);
|
|
@@ -7366,11 +7546,18 @@ Call again with confirm_token="${token2}" to execute. Token expires in 5 minutes
|
|
|
7366
7546
|
annotations: annotationsFor("radar_review_approve", "admin")
|
|
7367
7547
|
},
|
|
7368
7548
|
async ({ review_id, confirm_token }) => {
|
|
7549
|
+
const denied = denyUnlessScoped(props2, "radar_review_approve", STRIPE_CONNECT_TOOL_SCOPES);
|
|
7550
|
+
if (denied) return denied;
|
|
7551
|
+
if (!isPlatformOperator(env, props2.org_id)) {
|
|
7552
|
+
return err2(
|
|
7553
|
+
"This tool acts on Carrier's platform Stripe account and is restricted to Carrier's own organization."
|
|
7554
|
+
);
|
|
7555
|
+
}
|
|
7369
7556
|
const toolName = "radar_review_approve";
|
|
7370
7557
|
const start = Date.now();
|
|
7371
7558
|
if (!confirm_token) {
|
|
7372
7559
|
const token2 = await issueConfirmToken(env, props2.sub, toolName);
|
|
7373
|
-
return
|
|
7560
|
+
return ok(
|
|
7374
7561
|
`HARD_BLOCK: Approving review ${review_id} allows the charge to proceed.
|
|
7375
7562
|
confirm_token: ${token2}
|
|
7376
7563
|
Call again with confirm_token="${token2}" to execute. Expires in 5 minutes.`
|
|
@@ -7396,7 +7583,7 @@ Call again with confirm_token="${token2}" to execute. Expires in 5 minutes.`
|
|
|
7396
7583
|
sub: props2.sub,
|
|
7397
7584
|
reseller_id: props2.reseller_id
|
|
7398
7585
|
});
|
|
7399
|
-
return
|
|
7586
|
+
return ok(`Review ${review_id} approved. Charge will proceed.`);
|
|
7400
7587
|
} catch (e) {
|
|
7401
7588
|
Sentry3.captureException(e);
|
|
7402
7589
|
return err2(`Error: ${e instanceof Error ? e.message : "unknown"}`);
|
|
@@ -7415,11 +7602,18 @@ Call again with confirm_token="${token2}" to execute. Expires in 5 minutes.`
|
|
|
7415
7602
|
annotations: annotationsFor("radar_review_decline", "admin")
|
|
7416
7603
|
},
|
|
7417
7604
|
async ({ review_id, confirm_token }) => {
|
|
7605
|
+
const denied = denyUnlessScoped(props2, "radar_review_decline", STRIPE_CONNECT_TOOL_SCOPES);
|
|
7606
|
+
if (denied) return denied;
|
|
7607
|
+
if (!isPlatformOperator(env, props2.org_id)) {
|
|
7608
|
+
return err2(
|
|
7609
|
+
"This tool acts on Carrier's platform Stripe account and is restricted to Carrier's own organization."
|
|
7610
|
+
);
|
|
7611
|
+
}
|
|
7418
7612
|
const toolName = "radar_review_decline";
|
|
7419
7613
|
const start = Date.now();
|
|
7420
7614
|
if (!confirm_token) {
|
|
7421
7615
|
const token2 = await issueConfirmToken(env, props2.sub, toolName);
|
|
7422
|
-
return
|
|
7616
|
+
return ok(
|
|
7423
7617
|
`HARD_BLOCK: Declining review ${review_id} will close/block the charge.
|
|
7424
7618
|
confirm_token: ${token2}
|
|
7425
7619
|
Expires in 5 minutes.`
|
|
@@ -7445,7 +7639,7 @@ Expires in 5 minutes.`
|
|
|
7445
7639
|
sub: props2.sub,
|
|
7446
7640
|
reseller_id: props2.reseller_id
|
|
7447
7641
|
});
|
|
7448
|
-
return
|
|
7642
|
+
return ok(`Review ${review_id} declined.`);
|
|
7449
7643
|
} catch (e) {
|
|
7450
7644
|
Sentry3.captureException(e);
|
|
7451
7645
|
return err2(`Error: ${e instanceof Error ? e.message : "unknown"}`);
|
|
@@ -7465,11 +7659,18 @@ Expires in 5 minutes.`
|
|
|
7465
7659
|
annotations: annotationsFor("radar_value_list_add", "admin")
|
|
7466
7660
|
},
|
|
7467
7661
|
async ({ value_list_id, value, confirm_token }) => {
|
|
7662
|
+
const denied = denyUnlessScoped(props2, "radar_value_list_add", STRIPE_CONNECT_TOOL_SCOPES);
|
|
7663
|
+
if (denied) return denied;
|
|
7664
|
+
if (!isPlatformOperator(env, props2.org_id)) {
|
|
7665
|
+
return err2(
|
|
7666
|
+
"This tool acts on Carrier's platform Stripe account and is restricted to Carrier's own organization."
|
|
7667
|
+
);
|
|
7668
|
+
}
|
|
7468
7669
|
const toolName = "radar_value_list_add";
|
|
7469
7670
|
const start = Date.now();
|
|
7470
7671
|
if (!confirm_token) {
|
|
7471
7672
|
const token2 = await issueConfirmToken(env, props2.sub, toolName);
|
|
7472
|
-
return
|
|
7673
|
+
return ok(
|
|
7473
7674
|
`HARD_BLOCK: Adding "${value}" to list ${value_list_id} will affect future charge decisions.
|
|
7474
7675
|
confirm_token: ${token2}
|
|
7475
7676
|
Expires in 5 minutes.`
|
|
@@ -7495,7 +7696,7 @@ Expires in 5 minutes.`
|
|
|
7495
7696
|
sub: props2.sub,
|
|
7496
7697
|
reseller_id: props2.reseller_id
|
|
7497
7698
|
});
|
|
7498
|
-
return
|
|
7699
|
+
return ok(`Added "${value}" to Radar list ${value_list_id}.`);
|
|
7499
7700
|
} catch (e) {
|
|
7500
7701
|
Sentry3.captureException(e);
|
|
7501
7702
|
return err2(`Error: ${e instanceof Error ? e.message : "unknown"}`);
|
|
@@ -7514,7 +7715,14 @@ Expires in 5 minutes.`
|
|
|
7514
7715
|
annotations: annotationsFor("radar_rule_toggle", "admin")
|
|
7515
7716
|
},
|
|
7516
7717
|
async ({ rule_id, enabled }) => {
|
|
7517
|
-
|
|
7718
|
+
const denied = denyUnlessScoped(props2, "radar_rule_toggle", STRIPE_CONNECT_TOOL_SCOPES);
|
|
7719
|
+
if (denied) return denied;
|
|
7720
|
+
if (!isPlatformOperator(env, props2.org_id)) {
|
|
7721
|
+
return err2(
|
|
7722
|
+
"This tool acts on Carrier's platform Stripe account and is restricted to Carrier's own organization."
|
|
7723
|
+
);
|
|
7724
|
+
}
|
|
7725
|
+
return ok(
|
|
7518
7726
|
`Stripe Radar does not expose rule enable/disable via the public API.
|
|
7519
7727
|
To ${enabled ? "enable" : "disable"} rule ${rule_id}:
|
|
7520
7728
|
1. Open https://dashboard.stripe.com/radar/rules
|
|
@@ -7531,9 +7739,9 @@ import { z as z9 } from "zod";
|
|
|
7531
7739
|
var STOREFRONT_LOGO_TOOL_SCOPES = {
|
|
7532
7740
|
generate_storefront_logo: "read"
|
|
7533
7741
|
};
|
|
7534
|
-
var
|
|
7742
|
+
var ok2 = (text) => ({ content: [{ type: "text", text }] });
|
|
7535
7743
|
var err3 = (text) => ({ isError: true, content: [{ type: "text", text }] });
|
|
7536
|
-
function registerStorefrontLogoTools(server2, env = {}) {
|
|
7744
|
+
function registerStorefrontLogoTools(server2, env = {}, props2) {
|
|
7537
7745
|
server2.registerTool(
|
|
7538
7746
|
"generate_storefront_logo",
|
|
7539
7747
|
{
|
|
@@ -7547,6 +7755,8 @@ function registerStorefrontLogoTools(server2, env = {}) {
|
|
|
7547
7755
|
annotations: annotationsFor("generate_storefront_logo", "read")
|
|
7548
7756
|
},
|
|
7549
7757
|
async ({ name, accent, tagline }) => {
|
|
7758
|
+
const denied = denyUnlessScoped(props2, "generate_storefront_logo", STOREFRONT_LOGO_TOOL_SCOPES);
|
|
7759
|
+
if (denied) return denied;
|
|
7550
7760
|
try {
|
|
7551
7761
|
const hex = accent ? accent.startsWith("#") ? accent : `#${accent}` : void 0;
|
|
7552
7762
|
const logo = await generateStorefrontLogo({
|
|
@@ -7555,7 +7765,7 @@ function registerStorefrontLogoTools(server2, env = {}) {
|
|
|
7555
7765
|
tagline,
|
|
7556
7766
|
env: { ...env, ...process.env }
|
|
7557
7767
|
});
|
|
7558
|
-
return
|
|
7768
|
+
return ok2(
|
|
7559
7769
|
JSON.stringify({
|
|
7560
7770
|
brand: name,
|
|
7561
7771
|
source: logo.source,
|
|
@@ -7580,11 +7790,11 @@ var STOREFRONT_DEPLOY_TOOL_SCOPES = {
|
|
|
7580
7790
|
deploy_storefront: "write",
|
|
7581
7791
|
provision_storefront_clerk: "write"
|
|
7582
7792
|
};
|
|
7583
|
-
var
|
|
7793
|
+
var ok3 = (value) => ({
|
|
7584
7794
|
content: [{ type: "text", text: JSON.stringify(value) }]
|
|
7585
7795
|
});
|
|
7586
7796
|
var err4 = (text) => ({ isError: true, content: [{ type: "text", text }] });
|
|
7587
|
-
function registerStorefrontDeployTools(server2) {
|
|
7797
|
+
function registerStorefrontDeployTools(server2, props2) {
|
|
7588
7798
|
server2.registerTool(
|
|
7589
7799
|
"list_deploy_targets",
|
|
7590
7800
|
{
|
|
@@ -7596,10 +7806,12 @@ function registerStorefrontDeployTools(server2) {
|
|
|
7596
7806
|
annotations: annotationsFor("list_deploy_targets", "read")
|
|
7597
7807
|
},
|
|
7598
7808
|
async ({ dir }) => {
|
|
7809
|
+
const denied = denyUnlessScoped(props2, "list_deploy_targets", STOREFRONT_DEPLOY_TOOL_SCOPES);
|
|
7810
|
+
if (denied) return denied;
|
|
7599
7811
|
try {
|
|
7600
7812
|
const statuses = await probeAll(dir);
|
|
7601
7813
|
const ranked = rankTargets(statuses);
|
|
7602
|
-
return
|
|
7814
|
+
return ok3({
|
|
7603
7815
|
targets: statuses,
|
|
7604
7816
|
default: ranked[0]?.id ?? null,
|
|
7605
7817
|
reason: ranked.length ? void 0 : "No host is installed and logged in."
|
|
@@ -7626,6 +7838,8 @@ function registerStorefrontDeployTools(server2) {
|
|
|
7626
7838
|
annotations: annotationsFor("deploy_storefront", "write")
|
|
7627
7839
|
},
|
|
7628
7840
|
async ({ dir, target, name, skip_build, push_secrets, verify, domain }) => {
|
|
7841
|
+
const denied = denyUnlessScoped(props2, "deploy_storefront", STOREFRONT_DEPLOY_TOOL_SCOPES);
|
|
7842
|
+
if (denied) return denied;
|
|
7629
7843
|
try {
|
|
7630
7844
|
if (target && !isTargetId(target)) return err4(`Unknown target "${target}".`);
|
|
7631
7845
|
const brand = await loadStorefrontBrand(dir, name ? { name } : void 0);
|
|
@@ -7645,7 +7859,7 @@ function registerStorefrontDeployTools(server2) {
|
|
|
7645
7859
|
return err4(result2.reason ?? "Deploy failed.");
|
|
7646
7860
|
}
|
|
7647
7861
|
const verification = verify !== false && result2.url ? await verifyStorefront(result2.url, dir, process.env) : void 0;
|
|
7648
|
-
return
|
|
7862
|
+
return ok3({
|
|
7649
7863
|
deployed: true,
|
|
7650
7864
|
target: result2.target,
|
|
7651
7865
|
project: result2.projectName,
|
|
@@ -7678,6 +7892,12 @@ function registerStorefrontDeployTools(server2) {
|
|
|
7678
7892
|
annotations: annotationsFor("provision_storefront_clerk", "write")
|
|
7679
7893
|
},
|
|
7680
7894
|
async ({ dir, name, production, no_create, url }) => {
|
|
7895
|
+
const denied = denyUnlessScoped(
|
|
7896
|
+
props2,
|
|
7897
|
+
"provision_storefront_clerk",
|
|
7898
|
+
STOREFRONT_DEPLOY_TOOL_SCOPES
|
|
7899
|
+
);
|
|
7900
|
+
if (denied) return denied;
|
|
7681
7901
|
try {
|
|
7682
7902
|
const brand = await loadStorefrontBrand(dir, name ? { name } : void 0);
|
|
7683
7903
|
const result2 = await provisionClerk({
|
|
@@ -7690,7 +7910,7 @@ function registerStorefrontDeployTools(server2) {
|
|
|
7690
7910
|
cli: clerkCliDeps()
|
|
7691
7911
|
});
|
|
7692
7912
|
if (!result2.ok || !result2.credentials) {
|
|
7693
|
-
return
|
|
7913
|
+
return ok3({
|
|
7694
7914
|
provisioned: false,
|
|
7695
7915
|
tier: result2.tier,
|
|
7696
7916
|
reason: result2.reason,
|
|
@@ -7706,7 +7926,7 @@ function registerStorefrontDeployTools(server2) {
|
|
|
7706
7926
|
allowedOrigins,
|
|
7707
7927
|
redirectUrls
|
|
7708
7928
|
});
|
|
7709
|
-
return
|
|
7929
|
+
return ok3({
|
|
7710
7930
|
provisioned: true,
|
|
7711
7931
|
tier: result2.tier,
|
|
7712
7932
|
application_id: result2.credentials.applicationId ?? null,
|
|
@@ -10014,7 +10234,82 @@ function buildExamples(toolName) {
|
|
|
10014
10234
|
// src/list-recent-ocs-events.ts
|
|
10015
10235
|
import { z as z12 } from "zod";
|
|
10016
10236
|
|
|
10237
|
+
// src/ocs-scoping.ts
|
|
10238
|
+
function managedOwnAccountId(auth) {
|
|
10239
|
+
if (!auth.managed || auth.accountId === void 0) return void 0;
|
|
10240
|
+
const n = Number(auth.accountId);
|
|
10241
|
+
return Number.isFinite(n) ? n : void 0;
|
|
10242
|
+
}
|
|
10243
|
+
function subscriberAccountId(data) {
|
|
10244
|
+
if (!data || typeof data !== "object") return void 0;
|
|
10245
|
+
const root = data;
|
|
10246
|
+
const sub = root.subscriber && typeof root.subscriber === "object" ? root.subscriber : root;
|
|
10247
|
+
const raw = sub.accountId ?? sub.account_id ?? sub.account;
|
|
10248
|
+
if (raw === void 0 || raw === null) return void 0;
|
|
10249
|
+
const n = Number(raw);
|
|
10250
|
+
return Number.isFinite(n) ? n : void 0;
|
|
10251
|
+
}
|
|
10252
|
+
function isManagedSubscriberDenied(auth, subscriberData) {
|
|
10253
|
+
const ownId = managedOwnAccountId(auth);
|
|
10254
|
+
if (ownId === void 0) return false;
|
|
10255
|
+
const subAccountId = subscriberAccountId(subscriberData);
|
|
10256
|
+
if (subAccountId === void 0) return true;
|
|
10257
|
+
return subAccountId !== ownId;
|
|
10258
|
+
}
|
|
10259
|
+
async function assertManagedIccidAccess(baseUrl2, auth, iccid) {
|
|
10260
|
+
if (managedOwnAccountId(auth) === void 0) return { ok: true };
|
|
10261
|
+
try {
|
|
10262
|
+
const client2 = new OcsClient(baseUrl2, auth.token);
|
|
10263
|
+
const data = await client2.call("getSingleSubscriber", { iccid });
|
|
10264
|
+
if (isManagedSubscriberDenied(auth, data)) {
|
|
10265
|
+
return { ok: false, error: "Subscriber not found" };
|
|
10266
|
+
}
|
|
10267
|
+
return { ok: true };
|
|
10268
|
+
} catch (err7) {
|
|
10269
|
+
return {
|
|
10270
|
+
ok: false,
|
|
10271
|
+
error: err7 instanceof Error ? err7.message : String(err7)
|
|
10272
|
+
};
|
|
10273
|
+
}
|
|
10274
|
+
}
|
|
10275
|
+
|
|
10017
10276
|
// ../../packages/ocs-spec/src/ocs-event-buffer-read.ts
|
|
10277
|
+
function ocsEventRingBufferIccidFromDigitsReference(raw) {
|
|
10278
|
+
const digits = raw.replace(/\D/g, "");
|
|
10279
|
+
if (digits.length === 0) {
|
|
10280
|
+
throw new Error("reference must contain at least one digit");
|
|
10281
|
+
}
|
|
10282
|
+
if (digits.length > 20) {
|
|
10283
|
+
return digits.slice(-20);
|
|
10284
|
+
}
|
|
10285
|
+
if (digits.length >= 19) {
|
|
10286
|
+
return digits;
|
|
10287
|
+
}
|
|
10288
|
+
return digits.padStart(19, "0");
|
|
10289
|
+
}
|
|
10290
|
+
function msisdnFieldFromSubscriberPayload(data) {
|
|
10291
|
+
if (!data || typeof data !== "object") return null;
|
|
10292
|
+
const r = data;
|
|
10293
|
+
const v = r["msisdn"] ?? r["MSISDN"];
|
|
10294
|
+
if (v === void 0 || v === null) return null;
|
|
10295
|
+
if (typeof v === "string") return /\d/.test(v) ? v : null;
|
|
10296
|
+
if (typeof v === "number") return String(v);
|
|
10297
|
+
return null;
|
|
10298
|
+
}
|
|
10299
|
+
function mergeOcsEventOutputsNewestFirst(parts, limit) {
|
|
10300
|
+
const seen = /* @__PURE__ */ new Set();
|
|
10301
|
+
const out = [];
|
|
10302
|
+
const combined = [...parts].flat().sort(
|
|
10303
|
+
(x, y) => x.timestamp < y.timestamp ? 1 : x.timestamp > y.timestamp ? -1 : x.event_id.localeCompare(y.event_id)
|
|
10304
|
+
);
|
|
10305
|
+
for (const e of combined) {
|
|
10306
|
+
if (seen.has(e.event_id)) continue;
|
|
10307
|
+
seen.add(e.event_id);
|
|
10308
|
+
out.push(e);
|
|
10309
|
+
if (out.length >= limit) break;
|
|
10310
|
+
}
|
|
10311
|
+
return out;
|
|
10312
|
+
}
|
|
10018
10313
|
async function listRecentOcsEvents(iccid, limit, eventTypes, since, kv) {
|
|
10019
10314
|
const routingRaw = await kv.get(`iccid:${iccid}`, "json");
|
|
10020
10315
|
const resellerId = routingRaw?.reseller_id ?? 0;
|
|
@@ -10097,7 +10392,8 @@ var listRecentOcsEventsSchema = {
|
|
|
10097
10392
|
async function listRecentOcsEvents2(iccid, limit, eventTypes, since, kv) {
|
|
10098
10393
|
return listRecentOcsEvents(iccid, limit, eventTypes, since, kv);
|
|
10099
10394
|
}
|
|
10100
|
-
function registerListRecentOcsEventsTool(server2,
|
|
10395
|
+
function registerListRecentOcsEventsTool(server2, ctx) {
|
|
10396
|
+
const env = ctx.env;
|
|
10101
10397
|
server2.registerTool(
|
|
10102
10398
|
"list_recent_ocs_events",
|
|
10103
10399
|
{
|
|
@@ -10113,6 +10409,19 @@ function registerListRecentOcsEventsTool(server2, env) {
|
|
|
10113
10409
|
async (args) => {
|
|
10114
10410
|
const { iccid, limit, event_types, since } = args;
|
|
10115
10411
|
try {
|
|
10412
|
+
if (managedOwnAccountId(ctx.managedAuth) !== void 0) {
|
|
10413
|
+
const access = await assertManagedIccidAccess(
|
|
10414
|
+
env.CARRIER_OCS_BASE_URL,
|
|
10415
|
+
{ token: await ctx.getUserToken(ctx.props.sub), ...ctx.managedAuth },
|
|
10416
|
+
iccid
|
|
10417
|
+
);
|
|
10418
|
+
if (!access.ok) {
|
|
10419
|
+
return {
|
|
10420
|
+
isError: true,
|
|
10421
|
+
content: [{ type: "text", text: access.error }]
|
|
10422
|
+
};
|
|
10423
|
+
}
|
|
10424
|
+
}
|
|
10116
10425
|
const result2 = await listRecentOcsEvents2(
|
|
10117
10426
|
iccid,
|
|
10118
10427
|
limit,
|
|
@@ -10224,12 +10533,26 @@ function registerRateLimitStatusTool(server2, ctx) {
|
|
|
10224
10533
|
|
|
10225
10534
|
// src/tools-depletion-events.ts
|
|
10226
10535
|
import { z as z14 } from "zod";
|
|
10227
|
-
function
|
|
10536
|
+
function ok4(payload) {
|
|
10228
10537
|
return { content: [{ type: "text", text: JSON.stringify(payload, null, 2) }] };
|
|
10229
10538
|
}
|
|
10230
10539
|
function err5(message) {
|
|
10231
10540
|
return { isError: true, content: [{ type: "text", text: message }] };
|
|
10232
10541
|
}
|
|
10542
|
+
async function ownershipToken(ctx) {
|
|
10543
|
+
if (managedOwnAccountId(ctx.managedAuth) === void 0) return null;
|
|
10544
|
+
return ctx.getUserToken(ctx.props.sub);
|
|
10545
|
+
}
|
|
10546
|
+
async function ownsEvent(ctx, token2, iccid) {
|
|
10547
|
+
if (token2 === null) return true;
|
|
10548
|
+
if (!iccid) return false;
|
|
10549
|
+
const access = await assertManagedIccidAccess(
|
|
10550
|
+
ctx.env.CARRIER_OCS_BASE_URL,
|
|
10551
|
+
{ token: token2, ...ctx.managedAuth },
|
|
10552
|
+
iccid
|
|
10553
|
+
);
|
|
10554
|
+
return access.ok;
|
|
10555
|
+
}
|
|
10233
10556
|
function registerDepletionEventsTool(server2, ctx) {
|
|
10234
10557
|
server2.registerTool(
|
|
10235
10558
|
"subscriber_depletion_events",
|
|
@@ -10259,10 +10582,18 @@ function registerDepletionEventsTool(server2, ctx) {
|
|
|
10259
10582
|
);
|
|
10260
10583
|
}
|
|
10261
10584
|
const { subscriberId, since } = args;
|
|
10585
|
+
let scopeToken;
|
|
10586
|
+
try {
|
|
10587
|
+
scopeToken = await ownershipToken(ctx);
|
|
10588
|
+
} catch (e) {
|
|
10589
|
+
return err5(
|
|
10590
|
+
`Could not resolve the OCS credential needed to scope this read: ${e instanceof Error ? e.message : String(e)}`
|
|
10591
|
+
);
|
|
10592
|
+
}
|
|
10262
10593
|
if (subscriberId) {
|
|
10263
10594
|
const raw = await kv.get(`bundle-depleted:${subscriberId}`, "text");
|
|
10264
10595
|
if (!raw) {
|
|
10265
|
-
return
|
|
10596
|
+
return ok4({
|
|
10266
10597
|
subscriber_id: subscriberId,
|
|
10267
10598
|
depleted: false,
|
|
10268
10599
|
event: null,
|
|
@@ -10273,22 +10604,30 @@ function registerDepletionEventsTool(server2, ctx) {
|
|
|
10273
10604
|
try {
|
|
10274
10605
|
event = JSON.parse(raw);
|
|
10275
10606
|
} catch {
|
|
10276
|
-
return
|
|
10607
|
+
return ok4({
|
|
10277
10608
|
subscriber_id: subscriberId,
|
|
10278
10609
|
depleted: false,
|
|
10279
10610
|
event: null,
|
|
10280
10611
|
note: "Stored depletion entry was invalid JSON and was ignored."
|
|
10281
10612
|
});
|
|
10282
10613
|
}
|
|
10614
|
+
if (!await ownsEvent(ctx, scopeToken, event.iccid)) {
|
|
10615
|
+
return ok4({
|
|
10616
|
+
subscriber_id: subscriberId,
|
|
10617
|
+
depleted: false,
|
|
10618
|
+
event: null,
|
|
10619
|
+
note: "No bundle depletion event found in the last 7 days."
|
|
10620
|
+
});
|
|
10621
|
+
}
|
|
10283
10622
|
if (since && event.depletedAt < since) {
|
|
10284
|
-
return
|
|
10623
|
+
return ok4({
|
|
10285
10624
|
subscriber_id: subscriberId,
|
|
10286
10625
|
depleted: false,
|
|
10287
10626
|
event: null,
|
|
10288
10627
|
note: `No bundle depletion after ${since}.`
|
|
10289
10628
|
});
|
|
10290
10629
|
}
|
|
10291
|
-
return
|
|
10630
|
+
return ok4({
|
|
10292
10631
|
subscriber_id: subscriberId,
|
|
10293
10632
|
depleted: true,
|
|
10294
10633
|
event: {
|
|
@@ -10302,7 +10641,7 @@ function registerDepletionEventsTool(server2, ctx) {
|
|
|
10302
10641
|
}
|
|
10303
10642
|
const listResult = await kv.list({ prefix: "bundle-depleted:", limit: 20 });
|
|
10304
10643
|
if (listResult.keys.length === 0) {
|
|
10305
|
-
return
|
|
10644
|
+
return ok4({
|
|
10306
10645
|
depletions: [],
|
|
10307
10646
|
total: 0,
|
|
10308
10647
|
note: "No bundle depletion events in the last 7 days."
|
|
@@ -10316,6 +10655,7 @@ function registerDepletionEventsTool(server2, ctx) {
|
|
|
10316
10655
|
try {
|
|
10317
10656
|
const ev = JSON.parse(raw);
|
|
10318
10657
|
if (since && ev.depletedAt < since) return;
|
|
10658
|
+
if (!await ownsEvent(ctx, scopeToken, ev.iccid)) return;
|
|
10319
10659
|
events.push({
|
|
10320
10660
|
subscriber_id: ev.subscriberId,
|
|
10321
10661
|
iccid: ev.iccid,
|
|
@@ -10328,7 +10668,7 @@ function registerDepletionEventsTool(server2, ctx) {
|
|
|
10328
10668
|
})
|
|
10329
10669
|
);
|
|
10330
10670
|
events.sort((a, b) => b.depleted_at.localeCompare(a.depleted_at));
|
|
10331
|
-
return
|
|
10671
|
+
return ok4({
|
|
10332
10672
|
depletions: events,
|
|
10333
10673
|
total: events.length,
|
|
10334
10674
|
list_truncated: !listResult.list_complete
|
|
@@ -10353,19 +10693,46 @@ function registerCountryHistoryTool(server2, ctx) {
|
|
|
10353
10693
|
},
|
|
10354
10694
|
wrapHandler(
|
|
10355
10695
|
"subscriber_country_history",
|
|
10356
|
-
"
|
|
10696
|
+
"getSingleSubscriber",
|
|
10357
10697
|
"read",
|
|
10358
10698
|
ctx,
|
|
10359
|
-
async ({ subscriberId, limit }) => {
|
|
10699
|
+
async ({ subscriberId, limit }, token2) => {
|
|
10360
10700
|
const effectiveLimit = limit ?? 20;
|
|
10361
|
-
const
|
|
10701
|
+
const client2 = new OcsClient(ctx.env.CARRIER_OCS_BASE_URL, token2);
|
|
10702
|
+
const subscriberPayload = await client2.call("getSingleSubscriber", {
|
|
10703
|
+
iccid: subscriberId
|
|
10704
|
+
});
|
|
10705
|
+
let syntheticRingIccid = null;
|
|
10706
|
+
const msisdnRaw = msisdnFieldFromSubscriberPayload(subscriberPayload);
|
|
10707
|
+
if (msisdnRaw) {
|
|
10708
|
+
try {
|
|
10709
|
+
const k = ocsEventRingBufferIccidFromDigitsReference(msisdnRaw);
|
|
10710
|
+
if (k !== subscriberId) syntheticRingIccid = k;
|
|
10711
|
+
} catch {
|
|
10712
|
+
syntheticRingIccid = null;
|
|
10713
|
+
}
|
|
10714
|
+
}
|
|
10715
|
+
const primary = await listRecentOcsEvents(
|
|
10362
10716
|
subscriberId,
|
|
10363
10717
|
effectiveLimit,
|
|
10364
10718
|
["country.entered"],
|
|
10365
10719
|
void 0,
|
|
10366
10720
|
ctx.env.OCS_EVENT_ROUTING
|
|
10367
10721
|
);
|
|
10368
|
-
|
|
10722
|
+
let countryEvents = primary.events;
|
|
10723
|
+
if (syntheticRingIccid) {
|
|
10724
|
+
const secondary = await listRecentOcsEvents(
|
|
10725
|
+
syntheticRingIccid,
|
|
10726
|
+
effectiveLimit,
|
|
10727
|
+
["country.entered"],
|
|
10728
|
+
void 0,
|
|
10729
|
+
ctx.env.OCS_EVENT_ROUTING
|
|
10730
|
+
);
|
|
10731
|
+
countryEvents = mergeOcsEventOutputsNewestFirst(
|
|
10732
|
+
[primary.events, secondary.events],
|
|
10733
|
+
effectiveLimit
|
|
10734
|
+
);
|
|
10735
|
+
}
|
|
10369
10736
|
return {
|
|
10370
10737
|
content: [
|
|
10371
10738
|
{
|
|
@@ -10383,7 +10750,7 @@ function registerCountryHistoryTool(server2, ctx) {
|
|
|
10383
10750
|
source: e.data["source"] ?? "relay-lu"
|
|
10384
10751
|
})),
|
|
10385
10752
|
total_country_events: countryEvents.length,
|
|
10386
|
-
total_in_buffer:
|
|
10753
|
+
total_in_buffer: primary.total_in_buffer,
|
|
10387
10754
|
note: countryEvents.length === 0 ? "No cross-border events recorded in the 24h ring-buffer for this subscriber." : void 0
|
|
10388
10755
|
},
|
|
10389
10756
|
null,
|
|
@@ -11677,7 +12044,7 @@ function resolvePortalBaseUrl(env) {
|
|
|
11677
12044
|
function getGreenzoneKv(env) {
|
|
11678
12045
|
return env.GREENZONE_STATE_KV ?? null;
|
|
11679
12046
|
}
|
|
11680
|
-
function
|
|
12047
|
+
function ok5(payload) {
|
|
11681
12048
|
return { content: [{ type: "text", text: JSON.stringify(payload, null, 2) }] };
|
|
11682
12049
|
}
|
|
11683
12050
|
function err6(message) {
|
|
@@ -11713,7 +12080,7 @@ function registerGreenzoneTools(server2, ctx) {
|
|
|
11713
12080
|
...ip ? buildAddIpSteps(portalBaseUrl, ip) : []
|
|
11714
12081
|
];
|
|
11715
12082
|
if (dry_run) {
|
|
11716
|
-
return
|
|
12083
|
+
return ok5({
|
|
11717
12084
|
dry_run: true,
|
|
11718
12085
|
message: "Would execute the following Kapture steps against the OCS portal:",
|
|
11719
12086
|
kapture_steps: steps,
|
|
@@ -11736,7 +12103,7 @@ function registerGreenzoneTools(server2, ctx) {
|
|
|
11736
12103
|
} else {
|
|
11737
12104
|
kvWarning = "GREENZONE_STATE_KV not bound in stdio mode \u2014 state not persisted.";
|
|
11738
12105
|
}
|
|
11739
|
-
return
|
|
12106
|
+
return ok5({
|
|
11740
12107
|
requires_kapture: true,
|
|
11741
12108
|
message: "Execute the Kapture steps below to apply the change in the OCS portal. " + KAPTURE_UNAVAILABLE_MSG,
|
|
11742
12109
|
kapture_steps: steps,
|
|
@@ -11774,7 +12141,7 @@ function registerGreenzoneTools(server2, ctx) {
|
|
|
11774
12141
|
...ip ? buildRemoveIpSteps(portalBaseUrl, ip) : []
|
|
11775
12142
|
];
|
|
11776
12143
|
if (dry_run) {
|
|
11777
|
-
return
|
|
12144
|
+
return ok5({
|
|
11778
12145
|
dry_run: true,
|
|
11779
12146
|
message: "Would execute the following Kapture steps to remove from OCS portal:",
|
|
11780
12147
|
kapture_steps: steps,
|
|
@@ -11797,7 +12164,7 @@ function registerGreenzoneTools(server2, ctx) {
|
|
|
11797
12164
|
} else {
|
|
11798
12165
|
kvWarning = "GREENZONE_STATE_KV not bound in stdio mode \u2014 state not persisted.";
|
|
11799
12166
|
}
|
|
11800
|
-
return
|
|
12167
|
+
return ok5({
|
|
11801
12168
|
requires_kapture: true,
|
|
11802
12169
|
message: "Execute the Kapture steps below to apply the removal in the OCS portal. " + KAPTURE_UNAVAILABLE_MSG,
|
|
11803
12170
|
kapture_steps: steps,
|
|
@@ -11828,7 +12195,7 @@ function registerGreenzoneTools(server2, ctx) {
|
|
|
11828
12195
|
const { from_portal } = args;
|
|
11829
12196
|
if (from_portal) {
|
|
11830
12197
|
const portalBaseUrl = resolvePortalBaseUrl(ctx.env);
|
|
11831
|
-
return
|
|
12198
|
+
return ok5({
|
|
11832
12199
|
requires_kapture: true,
|
|
11833
12200
|
message: "Execute the Kapture steps below to read live Greenzone whitelist state from the OCS portal.",
|
|
11834
12201
|
kapture_steps: buildListSteps(portalBaseUrl),
|
|
@@ -11837,7 +12204,7 @@ function registerGreenzoneTools(server2, ctx) {
|
|
|
11837
12204
|
}
|
|
11838
12205
|
const kv = getGreenzoneKv(ctx.env);
|
|
11839
12206
|
if (!kv) {
|
|
11840
|
-
return
|
|
12207
|
+
return ok5({
|
|
11841
12208
|
source: "kv_cache",
|
|
11842
12209
|
hosts: [],
|
|
11843
12210
|
ips: [],
|
|
@@ -11849,7 +12216,7 @@ function registerGreenzoneTools(server2, ctx) {
|
|
|
11849
12216
|
}
|
|
11850
12217
|
try {
|
|
11851
12218
|
const state = await readState(kv);
|
|
11852
|
-
return
|
|
12219
|
+
return ok5({
|
|
11853
12220
|
source: "kv_cache",
|
|
11854
12221
|
hosts: state.hosts,
|
|
11855
12222
|
ips: state.ips,
|
|
@@ -11949,7 +12316,13 @@ var props = {
|
|
|
11949
12316
|
var audit = (_row) => {
|
|
11950
12317
|
};
|
|
11951
12318
|
var getUserToken = async (_sub) => token;
|
|
11952
|
-
var toolCtx = {
|
|
12319
|
+
var toolCtx = {
|
|
12320
|
+
env: stdioEnv,
|
|
12321
|
+
props,
|
|
12322
|
+
managedAuth: {},
|
|
12323
|
+
audit,
|
|
12324
|
+
getUserToken
|
|
12325
|
+
};
|
|
11953
12326
|
var server = new McpServer(
|
|
11954
12327
|
{ name: "carrier-mcp", version: CARRIER_VERSION },
|
|
11955
12328
|
{
|
|
@@ -11961,7 +12334,7 @@ registerAllTools(server, toolCtx);
|
|
|
11961
12334
|
registerIntelligenceTools(server, toolCtx);
|
|
11962
12335
|
registerAllBacklogTools(server, toolCtx);
|
|
11963
12336
|
registerAllCarrierAskTools(server, toolCtx);
|
|
11964
|
-
registerListRecentOcsEventsTool(server,
|
|
12337
|
+
registerListRecentOcsEventsTool(server, toolCtx);
|
|
11965
12338
|
registerRateLimitStatusTool(server, toolCtx);
|
|
11966
12339
|
registerCountryHistoryTool(server, toolCtx);
|
|
11967
12340
|
registerDepletionEventsTool(server, toolCtx);
|
|
@@ -11974,8 +12347,12 @@ registerStripeConnectTools(server, { env: stdioEnv, props });
|
|
|
11974
12347
|
registerWalletTools(server, { env: stdioEnv, props });
|
|
11975
12348
|
registerGreenzoneTools(server, toolCtx);
|
|
11976
12349
|
registerUiAgentGenericTools(server, toolCtx);
|
|
11977
|
-
registerStorefrontLogoTools(
|
|
11978
|
-
|
|
12350
|
+
registerStorefrontLogoTools(
|
|
12351
|
+
server,
|
|
12352
|
+
stdioEnv,
|
|
12353
|
+
props
|
|
12354
|
+
);
|
|
12355
|
+
registerStorefrontDeployTools(server, props);
|
|
11979
12356
|
registerAllPrompts(server);
|
|
11980
12357
|
var transport = new StdioServerTransport();
|
|
11981
12358
|
await server.connect(transport);
|