@carrierllc/mcp 0.3.1 → 0.3.2
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.
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// package.json
|
|
2
2
|
var package_default = {
|
|
3
3
|
name: "@carrierllc/mcp",
|
|
4
|
-
version: "0.3.
|
|
4
|
+
version: "0.3.2",
|
|
5
5
|
description: "Carrier MCP \u2014 natural-language control of MVNO/eSIM fleets via eSIMVault OCS. Stdio mode for direct integration with Claude Desktop, Cursor, Windsurf, and MCP-compatible clients. Ships the `carrier` CLI (plugin install + white-label eSIM storefront scaffold).",
|
|
6
6
|
license: "MIT",
|
|
7
7
|
author: "Carrier (Lifecycle Innovations Limited)",
|
|
@@ -458,4 +458,4 @@ export {
|
|
|
458
458
|
generateStorefrontLogo,
|
|
459
459
|
CARRIER_VERSION
|
|
460
460
|
};
|
|
461
|
-
//# sourceMappingURL=chunk-
|
|
461
|
+
//# sourceMappingURL=chunk-IYCGWBQU.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../package.json","../src/version.ts","../../../packages/ocs-client/src/rate-floor.ts","../../../packages/ocs-client/src/rate-governor.ts","../../../packages/ocs-client/src/client.ts","../../../packages/ocs-client/src/ocs-params.ts","../src/lib/storefront-logo.ts"],"sourcesContent":["{\n \"name\": \"@carrierllc/mcp\",\n \"version\": \"0.3.1\",\n \"description\": \"Carrier MCP \\u2014 natural-language control of MVNO/eSIM fleets via eSIMVault OCS. Stdio mode for direct integration with Claude Desktop, Cursor, Windsurf, and MCP-compatible clients. Ships the `carrier` CLI (plugin install + white-label eSIM storefront scaffold).\",\n \"license\": \"MIT\",\n \"author\": \"Carrier (Lifecycle Innovations Limited)\",\n \"homepage\": \"https://mcp.carrier.llc\",\n \"repository\": {\n \"type\": \"git\",\n \"url\": \"https://github.com/Lifecycle-Innovations-Limited/carrier.llc\"\n },\n \"type\": \"module\",\n \"main\": \"./dist/index.js\",\n \"files\": [\n \"dist\",\n \"plugin\",\n \"templates\",\n \"README.md\"\n ],\n \"scripts\": {\n \"build\": \"tsup\",\n \"type-check\": \"tsc --noEmit\",\n \"lint\": \"eslint src\",\n \"prepublishOnly\": \"pnpm run build\",\n \"test\": \"pnpm run build && node --test test/*.test.js\"\n },\n \"dependencies\": {\n \"@clack/prompts\": \"^0.7.0\",\n \"@clerk/backend\": \"^3.14.0\",\n \"@modelcontextprotocol/ext-apps\": \"^1.7.5\",\n \"@modelcontextprotocol/sdk\": \"^1.29.0\",\n \"@noble/hashes\": \"^1.8.0\",\n \"@sentry/cloudflare\": \"^10.65.0\",\n \"aws4fetch\": \"^1.0.20\",\n \"commander\": \"^15.0.0\",\n \"picocolors\": \"^1.1.1\",\n \"zod\": \"^4.4.3\"\n },\n \"devDependencies\": {\n \"@cloudflare/workers-types\": \"^4.20260521.1\",\n \"@types/node\": \"^26.1.2\",\n \"@typescript-eslint/eslint-plugin\": \"^8.59.4\",\n \"@typescript-eslint/parser\": \"^8.67.0\",\n \"eslint\": \"^9.39.4\",\n \"tsup\": \"^8.5.1\",\n \"typescript\": \"^5.9.3\",\n \"@carrier/ocs-client\": \"workspace:*\",\n \"@carrier/ocs-spec\": \"workspace:*\"\n },\n \"keywords\": [\n \"mcp\",\n \"model-context-protocol\",\n \"esim\",\n \"mvno\",\n \"carrier\",\n \"fleet-management\",\n \"esimvault\"\n ],\n \"engines\": {\n \"node\": \">=22\"\n },\n \"publishConfig\": {\n \"access\": \"public\"\n },\n \"bin\": {\n \"carrier-mcp\": \"dist/index.js\",\n \"carrier\": \"dist/cli.js\"\n }\n}\n","// JSON modules are default-only, so this pulls the manifest in and reads one field.\nimport pkg from \"../package.json\" with { type: \"json\" };\n\n/**\n * Single source of truth for the published version.\n *\n * The CLI banner, `carrier --version`, the stdio MCP server's clientInfo and the\n * `carrier ask` handshake all read this. It used to be typed out in four places,\n * which is how seven commits shipped under a stale version string. Bump\n * package.json and every surface follows.\n */\nexport const CARRIER_VERSION: string = pkg.version;\n","/**\n * Global sliding-window rate floor for the OCS client.\n *\n * Scoped per resellerId (derived from the API token). Lives on a module-level\n * Map so it is shared across all OcsClient instances within one Worker isolate.\n *\n * Enforces ≤ 10 calls per 1000ms per reseller key.\n * Uses a sliding-window log: tracks the last 10 call timestamps.\n * Before the 11th call, waits until the oldest recorded call is at least\n * 1000ms in the past — guaranteeing ≤10 in any 1000ms span.\n *\n * Uses a serialised promise-chain (gate) so concurrent callers queue up\n * atomically. Without serialisation, multiple callers could read the same\n * window state simultaneously and all proceed, defeating the floor.\n *\n * Set env var RATE_FLOOR_DISABLED=true to bypass (never do this in production).\n */\n\nconst RATE_LIMIT = 10; // max calls per window\nconst WINDOW_MS = 1000; // window size in ms\n\ninterface WindowState {\n /** Timestamps of the last RATE_LIMIT calls (oldest first). */\n log: number[];\n /** Serialisation gate: every acquire chains onto this promise. */\n gate: Promise<void>;\n}\n\n// Module-level — shared across all OcsClient instances per isolate.\nconst windows = new Map<string, WindowState>();\n\nfunction getWindowState(key: string): WindowState {\n let s = windows.get(key);\n if (!s) {\n s = { log: [], gate: Promise.resolve() };\n windows.set(key, s);\n }\n return s;\n}\n\n/**\n * Acquire one slot for the given reseller key, awaiting if necessary.\n * No-op when RATE_FLOOR_DISABLED=true.\n */\nexport async function acquireToken(resellerId: string): Promise<void> {\n // Allow env-based bypass for integration/load tests that need to spike.\n // Tests asserting the floor works must NOT set this flag.\n // Resolve `process` via globalThis so TS doesn't need @types/node\n // (Workers tsconfig excludes Node types; runtime guard handles both envs).\n const proc = (globalThis as Record<string, unknown>)[\"process\"] as\n | { env?: Record<string, string | undefined> }\n | undefined;\n if (\n proc?.env?.[\"RATE_FLOOR_DISABLED\"] === \"true\" ||\n (globalThis as Record<string, unknown>)[\"RATE_FLOOR_DISABLED\"] === \"true\"\n ) {\n return;\n }\n\n const state = getWindowState(resellerId);\n\n // Chain onto the existing gate so only one acquire runs at a time.\n const ticket = state.gate.then(async () => {\n while (true) {\n const now = Date.now();\n\n if (state.log.length < RATE_LIMIT) {\n state.log.push(now);\n return;\n }\n\n const oldest = state.log[0]!;\n const age = now - oldest;\n\n if (age >= WINDOW_MS) {\n state.log.shift();\n state.log.push(now);\n return;\n }\n\n const waitMs = WINDOW_MS - age + 1;\n await new Promise<void>((r) => setTimeout(r, waitMs));\n }\n });\n\n // Advance the gate so the next caller queues behind this one.\n state.gate = ticket;\n\n return ticket;\n}\n\n/** Exposed for testing: reset all window state. */\nexport function _resetBucketsForTest(): void {\n windows.clear();\n}\n","/**\n * Per-endpoint rate-limit governor for Bridge4IP / OCS API calls.\n *\n * LIMITS (per reseller, confirmed by Bridge4IP):\n * Global: 600 / min\n * modifySubscriberMobilePlan: 300 / min\n * AffectRecurringPackageToSub: 300 / min\n * AffectSubscriberFakePhoneNumber: 300 / min\n * AffectSubscriberRealPhoneNumber: 300 / min\n * EsimStatusPerAccount: 300 / min\n * GetSubscriberActivePeriod: 300 / min\n * HlrGetBitrate / HlrSetBitrate: 300 / min\n * ListSubscriber: 300 / min\n * ListSubscriberPrepaidPackages: 300 / min\n * MoveSubscriberRangeToAccount: 300 / min\n * PushSteeringToSubs: 300 / min\n * SetSubscriberTrafficRestrictions:300 / min\n * ListLocationZoneElement: 150 / min\n * SendMtSms: 150 / min\n * SubscriberUsageOverPeriod: 100 / min ← tight\n * SubscriberNetworkEventsOverPeriod: 30 / min ← VERY TIGHT\n * getSingleSubscriber (TBD): 300 / min (assumed; alert on 429)\n * getSubscriberLocationByCellId (TBD): 300 / min (assumed; alert on 429)\n *\n * CAPACITY RESERVATION:\n * 80% of each bucket is reserved for interactive (MCP tool) calls.\n * 20% is reserved for batch / cron callers (ocs-bridge polls).\n * Callers pass `priority: \"interactive\" | \"batch\"` to acquireEndpointSlot().\n * Interactive callers draw from the full bucket capacity.\n * Batch callers can only draw up to 20% of the per-minute limit per minute.\n *\n * ALGORITHM:\n * Sliding-window log per (resellerId, endpoint) key.\n * Separate batch-window log per key to enforce the 20% ceiling.\n * Serialised promise-chain gate prevents TOCTOU races within a single isolate.\n * (CF Workers: mcp-server DO serialises per-session; ocs-bridge uses KV for cross-isolate.)\n *\n * NEVER LEAK MONEY:\n * Set RATE_FLOOR_DISABLED=true ONLY in test environments.\n * Any process-global flag bypasses all per-endpoint governors.\n */\n\nexport type CallPriority = \"interactive\" | \"batch\";\n\n// ---------------------------------------------------------------------------\n// Endpoint limit table\n// ---------------------------------------------------------------------------\n\n/** Per-minute cap for a given OCS method (camelCase). Default: 300/min. */\nconst ENDPOINT_LIMITS_PER_MIN: Record<string, number> = {\n // Global sentinel (keyed as \"__global__\")\n __global__: 600,\n\n // 30/min — tightest endpoint\n subscriberNetworkEventsOverPeriod: 30,\n\n // 100/min\n subscriberUsageOverPeriod: 100,\n\n // 150/min\n listLocationZoneElement: 150,\n sendMtSms: 150,\n\n // 300/min (explicit; also the fallback default)\n modifySubscriberMobilePlan: 300,\n affectRecurringPackageToSubscriber: 300,\n affectSubscriberFakePhoneNumber: 300,\n affectSubscriberRealPhoneNumber: 300,\n esimStatusPerAccount: 300,\n getSubscriberActivePeriod: 300,\n hlrGetBitrate: 300,\n hlrSetBitrate: 300,\n listSubscriber: 300,\n listSubscriberPrepaidPackages: 300,\n moveSubscriberRangeToAccount: 300,\n pushSteeringToSubs: 300,\n setSubscriberTrafficRestrictions: 300,\n getSingleSubscriber: 300,\n getSubscriberLocationByCellId: 300,\n};\n\nconst DEFAULT_LIMIT_PER_MIN = 300;\nconst WINDOW_MS = 60_000; // 1 minute\n\n/** Wall-clock throttle per endpoint bucket — avoids JSON log spam on every OCS call. */\nconst METRIC_EMIT_MIN_INTERVAL_MS = 10_000;\n\n/** Fraction of capacity reserved for batch callers (20%). */\nconst BATCH_FRACTION = 0.2;\n\n/** Non-reversible id for metrics only (callers may pass API tokens as reseller keys). */\nfunction resellerKeyHash(resellerKey: string | undefined): string {\n if (!resellerKey) return \"00000000\";\n let h = 5381;\n for (let i = 0; i < resellerKey.length; i++) {\n h = (((h << 5) + h) ^ resellerKey.charCodeAt(i)) >>> 0;\n }\n return h.toString(16).padStart(8, \"0\");\n}\n\n// ---------------------------------------------------------------------------\n// Prometheus-style metric emission\n// ---------------------------------------------------------------------------\n\n/**\n * Emit a structured metric line for external scraping / CF Analytics Engine.\n * Format: metric{labels} value timestamp_ms\n */\nfunction emitMetric(\n endpoint: string,\n resellerKey: string,\n callsInWindow: number,\n limitPerMin: number,\n): void {\n const pct = Math.round((callsInWindow / limitPerMin) * 100);\n const reseller_hash = resellerKeyHash(resellerKey);\n // Structured log: picked up by CF Workers Tail / BetterStack / Prometheus remote-write.\n console.log(\n JSON.stringify({\n metric: \"carrier_ocs_calls_per_min\",\n endpoint,\n reseller_hash,\n calls_in_window: callsInWindow,\n limit_per_min: limitPerMin,\n utilisation_pct: pct,\n alert: pct >= 80,\n ts: Date.now(),\n }),\n );\n if (pct >= 80) {\n console.warn(\n `[rate-governor] ALERT: ${endpoint} at ${pct}% capacity (${callsInWindow}/${limitPerMin} per min) reseller=${reseller_hash}`,\n );\n }\n}\n\n// ---------------------------------------------------------------------------\n// Window state\n// ---------------------------------------------------------------------------\n\ninterface EndpointWindowState {\n /** All call timestamps in the current sliding window (oldest first). */\n log: number[];\n /** Batch-only call timestamps (subset of log). */\n batchLog: number[];\n /** Serialisation gate — one acquire at a time. */\n gate: Promise<void>;\n /** Last `emitMetric` wall time for this bucket (throttles log volume). */\n lastMetricEmitAt: number;\n /**\n * When true, the next sample at ≥80% utilisation should emit immediately\n * (edge-trigger) so alerts are not delayed by the interval throttle.\n */\n metricBelowHighUtil: boolean;\n}\n\n// Keyed as `${resellerId}:${endpoint}` (or `${resellerId}:__global__`)\nconst _endpointWindows = new Map<string, EndpointWindowState>();\n\nfunction _getEndpointState(key: string): EndpointWindowState {\n let s = _endpointWindows.get(key);\n if (!s) {\n s = {\n log: [],\n batchLog: [],\n gate: Promise.resolve(),\n lastMetricEmitAt: 0,\n metricBelowHighUtil: true,\n };\n _endpointWindows.set(key, s);\n }\n return s;\n}\n\nfunction _prune(log: number[], windowStart: number): number[] {\n // Remove entries older than the window boundary.\n let i = 0;\n while (i < log.length && log[i]! <= windowStart) i++;\n return i > 0 ? log.slice(i) : log;\n}\n\n// ---------------------------------------------------------------------------\n// Public API\n// ---------------------------------------------------------------------------\n\n/**\n * Acquire one OCS call slot for the given (resellerId, endpoint) pair.\n *\n * Enforces:\n * 1. Per-endpoint limit (from ENDPOINT_LIMITS_PER_MIN table).\n * 2. Global 600/min limit across all endpoints for this reseller.\n * 3. Batch callers capped at 20% of each bucket.\n *\n * Emits throttled `carrier_ocs_calls_per_min` JSON metrics (and warns at ≥80%\n * utilisation when a sample is emitted).\n *\n * No-op when RATE_FLOOR_DISABLED=true (never set in production).\n */\nexport async function acquireEndpointSlot(\n resellerId: string,\n endpoint: string,\n priority: CallPriority = \"interactive\",\n): Promise<void> {\n // Env bypass — test environments only.\n const proc = (globalThis as Record<string, unknown>)[\"process\"] as\n | { env?: Record<string, string | undefined> }\n | undefined;\n if (\n proc?.env?.[\"RATE_FLOOR_DISABLED\"] === \"true\" ||\n (globalThis as Record<string, unknown>)[\"RATE_FLOOR_DISABLED\"] === \"true\"\n ) {\n return;\n }\n\n // Global first: avoid holding a tight endpoint slot while waiting on the 600/min bucket.\n await _acquireOneSlot(resellerId, \"__global__\", priority);\n await _acquireOneSlot(resellerId, endpoint, priority);\n}\n\nasync function _acquireOneSlot(\n resellerId: string,\n endpoint: string,\n priority: CallPriority,\n): Promise<void> {\n const bucketKey = `${resellerId}:${endpoint}`;\n const limitPerMin = ENDPOINT_LIMITS_PER_MIN[endpoint] ?? DEFAULT_LIMIT_PER_MIN;\n const batchCap = Math.floor(limitPerMin * BATCH_FRACTION);\n\n const state = _getEndpointState(bucketKey);\n\n const ticket = state.gate.then(async () => {\n while (true) {\n const now = Date.now();\n const windowStart = now - WINDOW_MS;\n\n // Prune expired entries.\n state.log = _prune(state.log, windowStart);\n state.batchLog = _prune(state.batchLog, windowStart);\n\n const totalInWindow = state.log.length;\n const batchInWindow = state.batchLog.length;\n\n // Emit metric on a wall-clock interval; also emit immediately when util\n // first crosses ≥80% after having been below (so alerts are not delayed).\n if (totalInWindow > 0 && endpoint !== \"__global__\") {\n const utilPct = Math.round((totalInWindow / limitPerMin) * 100);\n const intervalOk =\n now - state.lastMetricEmitAt >= METRIC_EMIT_MIN_INTERVAL_MS;\n const enteredHighUtil = utilPct >= 80 && state.metricBelowHighUtil;\n if (intervalOk || enteredHighUtil) {\n emitMetric(endpoint, resellerId, totalInWindow, limitPerMin);\n state.lastMetricEmitAt = now;\n if (utilPct >= 80) {\n state.metricBelowHighUtil = false;\n }\n }\n if (utilPct < 80) {\n state.metricBelowHighUtil = true;\n }\n }\n\n // Check capacity:\n // - Global cap: can't exceed limitPerMin regardless of priority.\n // - Batch cap: batch callers additionally can't exceed batchCap.\n const globalFull = totalInWindow >= limitPerMin;\n const batchFull = priority === \"batch\" && batchInWindow >= batchCap;\n\n if (!globalFull && !batchFull) {\n // Slot available — record call.\n state.log.push(now);\n if (priority === \"batch\") {\n state.batchLog.push(now);\n }\n return;\n }\n\n // Determine how long to wait.\n let waitMs: number;\n if (globalFull) {\n // Wait until oldest entry in the full window ages out.\n const oldest = state.log[0]!;\n waitMs = WINDOW_MS - (now - oldest) + 1;\n } else {\n // Batch cap hit — wait until oldest batch entry ages out.\n const oldestBatch = state.batchLog[0]!;\n waitMs = WINDOW_MS - (now - oldestBatch) + 1;\n }\n\n await new Promise<void>((r) => setTimeout(r, waitMs));\n }\n });\n\n state.gate = ticket;\n return ticket;\n}\n\n/**\n * Returns the per-minute limit for a given OCS method.\n * Exported for testing and monitoring.\n */\nexport function getLimitForEndpoint(endpoint: string): number {\n return ENDPOINT_LIMITS_PER_MIN[endpoint] ?? DEFAULT_LIMIT_PER_MIN;\n}\n\n/**\n * Read-only counts for the current sliding window (same keying as acquireEndpointSlot).\n * Scans timestamps without mutating state — safe for status tools alongside acquires.\n */\nexport function getRateLimitWindowCounts(\n resellerKey: string,\n endpoint: string,\n): { calls_in_window: number; batch_calls_in_window: number } {\n const bucketKey = `${resellerKey}:${endpoint}`;\n const state = _endpointWindows.get(bucketKey);\n if (!state) {\n return { calls_in_window: 0, batch_calls_in_window: 0 };\n }\n const windowStart = Date.now() - WINDOW_MS;\n let calls_in_window = 0;\n for (const ts of state.log) {\n if (ts > windowStart) calls_in_window++;\n }\n let batch_calls_in_window = 0;\n for (const ts of state.batchLog) {\n if (ts > windowStart) batch_calls_in_window++;\n }\n return { calls_in_window, batch_calls_in_window };\n}\n\n/** Exposed for testing: reset all endpoint window state. */\nexport function _resetGovernorForTest(): void {\n _endpointWindows.clear();\n}\n","/**\n * @carrier/ocs-client — OCS API client (Workers-compatible).\n *\n * Shared between apps/mcp-server and apps/api.\n * All requests POST to `${baseUrl}/v1?token=<api_key>` with body `{ methodName: params }`.\n * Responses: `{ status: { code, msg }, methodName: { ...data } }`.\n *\n * Money-leak guardrails (NEVER LEAK MONEY):\n * - Per-second floor: ≤10 calls/sec/reseller (see rate-floor.ts).\n * - Per-endpoint per-minute governor: enforces Bridge4IP limits per endpoint\n * (30–600/min depending on method) with 80/20 interactive/batch split (see rate-governor.ts).\n * On 429 / OCS error code 100 (TRAFFIC_CONTROL_LIMIT_EXCEEDED): exponential backoff + log.\n * - Specific error codes 12, 17, 100, 10001-10004 carry structured messages.\n */\n\nimport { acquireToken } from \"./rate-floor.js\";\nimport { acquireEndpointSlot, type CallPriority } from \"./rate-governor.js\";\nimport { OCS_V1_METHODS, RESPONSE_SCHEMAS } from \"./schemas.js\";\n\nexport interface OcsStatus {\n code: number;\n msg: string;\n}\n\nexport interface OcsResponse<T = Record<string, unknown>> {\n status: OcsStatus;\n [method: string]: T | OcsStatus;\n}\n\nexport interface OcsApiErrorDetails {\n /** For create_location_zone validation errors (codes 10001-10004). */\n invalidTadigs?: string[];\n existingLZ?: unknown;\n [key: string]: unknown;\n}\n\nexport class OcsApiError extends Error {\n public readonly details: OcsApiErrorDetails;\n\n constructor(\n public readonly code: number,\n message: string,\n public readonly method: string,\n details: OcsApiErrorDetails = {},\n ) {\n super(`[${method}] OCS error ${code}: ${message}`);\n this.name = \"OcsApiError\";\n this.details = details;\n }\n}\n\n/**\n * Thrown when `call()` is invoked with a method that is NOT one of the\n * known-valid OCS v1 methods (see `OCS_V1_METHODS`). Catches phantom methods\n * like `createAccount` / `createSubscriber` at dev time — these are not OCS\n * methods and the server silently returns a 200 + error-2 envelope for them.\n * Only thrown in strict mode (`{ strictMethods: true }`); otherwise `call()`\n * warns once and proceeds.\n */\nexport class OcsUnknownMethodError extends Error {\n constructor(public readonly method: string) {\n super(\n `[${method}] is not a known OCS v1 method. ` +\n `It is not in the verified set of ${OCS_V1_METHODS.size} methods ` +\n `(createAccount/createSubscriber do not exist on the OCS). ` +\n `If this is a genuinely new method, add it to OCS_V1_METHODS or ` +\n `construct the client without { strictMethods: true }.`,\n );\n this.name = \"OcsUnknownMethodError\";\n }\n}\n\n/**\n * Thrown when a response payload that has a pinned schema in\n * `RESPONSE_SCHEMAS` fails shape validation — i.e. the OCS returned a 200 with\n * status.code 0 but a body shaped differently than the contract expects. This\n * surfaces provider drift loudly instead of returning a mis-shaped object that\n * blows up far away from the boundary. Skip per-client with\n * `{ validateResponses: false }` for forward-compat.\n */\n/** A single contract-validation failure: where it failed and why. */\nexport interface OcsContractIssue {\n /** Dotted path into the payload (e.g. `reseller.0.account`); `<root>` for top-level. */\n path: string;\n /** Human-readable reason from the underlying Zod issue. */\n message: string;\n}\n\nexport class OcsContractError extends Error {\n constructor(\n public readonly method: string,\n /** Structured list of what failed — inspect programmatically, don't parse the message. */\n public readonly issues: OcsContractIssue[],\n public readonly payload: unknown,\n ) {\n super(\n `[${method}] OCS response failed contract validation: ${OcsContractError.summarize(issues)}`,\n );\n this.name = \"OcsContractError\";\n }\n\n /** One-line, human-readable join of the issues — for logs and the error message. */\n get summary(): string {\n return OcsContractError.summarize(this.issues);\n }\n\n private static summarize(issues: OcsContractIssue[]): string {\n return issues.map((i) => `${i.path}: ${i.message}`).join(\"; \");\n }\n}\n\n// ---------------------------------------------------------------------------\n// Error code message routing\n// ---------------------------------------------------------------------------\n\nfunction ocsErrorMessage(\n code: number,\n rawMsg: string,\n rawBody: Record<string, unknown>,\n): { message: string; details: OcsApiErrorDetails } {\n switch (code) {\n case 12:\n return {\n message:\n \"OCS resource is read-only (code 12). The package/subscriber/account state \" +\n \"does not allow this mutation. Often returned for end-of-life subscribers or \" +\n \"finalized invoices.\",\n details: {},\n };\n case 17:\n return {\n message:\n \"Subscriber is end-of-life (code 17). It can no longer be mutated. \" +\n \"Use modify_subscriber_status to revive before further changes.\",\n details: {},\n };\n default:\n if (code >= 10001 && code <= 10004) {\n // create_location_zone validation — preserve invalidTadigs / existingLZ\n const details: OcsApiErrorDetails = {};\n if (rawBody[\"invalidTadigs\"] !== undefined) {\n details.invalidTadigs = rawBody[\"invalidTadigs\"] as string[];\n }\n if (rawBody[\"existingLZ\"] !== undefined) {\n details.existingLZ = rawBody[\"existingLZ\"];\n }\n return { message: rawMsg || \"Location zone validation failed\", details };\n }\n return { message: rawMsg || \"Unknown error\", details: {} };\n }\n}\n\n// ---------------------------------------------------------------------------\n// Retry with exponential backoff + jitter\n// ---------------------------------------------------------------------------\n\nconst BACKOFF_BASE_MS = 500;\nconst BACKOFF_MAX_MS = 4_000;\nconst BACKOFF_MAX_RETRIES = 3;\nconst BACKOFF_JITTER = 0.25; // ±25%\n\n/**\n * Exported so callers (e.g. intelligence.ts) can wrap per-iteration OCS calls.\n * Strategy: 3 retries, exponential base 500ms, ±25% jitter, capped at 4s.\n */\nexport async function retryWithBackoff<T>(fn: () => Promise<T>): Promise<T> {\n let lastError: unknown;\n for (let attempt = 0; attempt <= BACKOFF_MAX_RETRIES; attempt++) {\n if (attempt > 0) {\n const base = Math.min(BACKOFF_BASE_MS * Math.pow(2, attempt - 1), BACKOFF_MAX_MS);\n const jitter = base * BACKOFF_JITTER * (2 * Math.random() - 1);\n const delay = Math.max(0, Math.round(base + jitter));\n await new Promise<void>((r) => setTimeout(r, delay));\n }\n try {\n return await fn();\n } catch (err) {\n lastError = err;\n // Only retry on code 100 (TRAFFIC_CONTROL_LIMIT_EXCEEDED) or network errors.\n if (err instanceof OcsApiError && err.code !== 100) {\n throw err;\n }\n // Contract failures are deterministic — don't waste retries on them.\n if (err instanceof OcsContractError) {\n throw err;\n }\n }\n }\n throw lastError;\n}\n\n// ---------------------------------------------------------------------------\n// OCS Client\n// ---------------------------------------------------------------------------\n\nconst REQUEST_TIMEOUT_MS = 20_000;\n\nexport interface OcsClientOptions {\n /**\n * Enforce the known-method guard: when true, calling a method not in\n * `OCS_V1_METHODS` throws `OcsUnknownMethodError`. Default **false**\n * (permissive) — the repo currently calls a number of method names that\n * aren't in the live-verified OCS v1 set (a separate reconciliation effort),\n * so enforcing here would break existing call sites. Opt in (`true`) for\n * dev/tests or once the codebase's method vocabulary is reconciled. An\n * unrecognized method is warned once regardless, so the signal isn't lost.\n */\n strictMethods?: boolean;\n /**\n * Run response-shape validation against `RESPONSE_SCHEMAS`. Default true. A\n * mismatch WARNS once (does not throw) unless `strictValidation` is set. Set\n * false to skip validation entirely (e.g. consumers that do their own\n * shape-tolerant parsing).\n */\n validateResponses?: boolean;\n /**\n * Throw `OcsContractError` on a response-shape mismatch instead of warning.\n * Default false. Enable in dev/CI/tests to fail loudly on provider drift;\n * left off in prod so an un-sampled shape variant can't break live traffic.\n */\n strictValidation?: boolean;\n}\n\nexport class OcsClient {\n private readonly baseUrl: string;\n private readonly _strictMethods: boolean;\n private readonly _validateResponses: boolean;\n private readonly _strictValidation: boolean;\n // Per-instance (NOT static): a static set would dedup across requests in a\n // reused Worker isolate and silently swallow recurring contract-drift warnings.\n private readonly _warnedMethods = new Set<string>();\n private readonly _warnedValidation = new Set<string>();\n\n /**\n * @param baseUrl OCS base URL without trailing slash (e.g. https://ocs.esimvault.cloud).\n * @param _defaultToken Default API token; identifies the reseller for rate-floor keying.\n * @param _priority Call priority: \"interactive\" (MCP tool calls, 80% capacity) or\n * \"batch\" (cron/poll callers, 20% capacity). Default: \"interactive\".\n * @param options Contract-enforcement opt-outs (see `OcsClientOptions`).\n */\n constructor(\n baseUrl: string,\n private readonly _defaultToken?: string,\n private readonly _priority: CallPriority = \"interactive\",\n options: OcsClientOptions = {},\n ) {\n this.baseUrl = baseUrl.replace(/\\/+$/, \"\");\n this._strictMethods = options.strictMethods ?? false;\n this._validateResponses = options.validateResponses ?? true;\n this._strictValidation = options.strictValidation ?? false;\n }\n\n async call<T = Record<string, unknown>>(\n method: string,\n params: Record<string, unknown> | number | string = {},\n token?: string,\n ): Promise<T> {\n // Method guard. The live-verified OCS v1 set (OCS_V1_METHODS) does not (yet)\n // cover every name the repo calls, so by default we WARN once rather than\n // throw — surfacing phantom methods (createAccount/createSubscriber, etc.)\n // without breaking existing call sites. Strict mode (opt-in) throws.\n if (!OCS_V1_METHODS.has(method)) {\n if (this._strictMethods) throw new OcsUnknownMethodError(method);\n if (!this._warnedMethods.has(method)) {\n this._warnedMethods.add(method);\n console.warn(\n `[ocs-client] method \"${method}\" is not in the verified OCS v1 set — ` +\n `it may not exist on this OCS instance (see OCS_V1_METHODS).`,\n );\n }\n }\n\n const tok = token ?? this._defaultToken;\n if (!tok) {\n throw new OcsApiError(-1, \"No OCS API token provided\", method);\n }\n\n const url = `${this.baseUrl}/v1?token=${tok}`;\n const body = JSON.stringify({ [method]: params });\n\n return retryWithBackoff(async () => {\n // Layer 1 — Per-second floor: ≤10 calls/sec keyed by token.\n await acquireToken(tok);\n\n // Layer 2 — Per-endpoint per-minute governor (each retryable attempt consumes one slot).\n await acquireEndpointSlot(tok, method, this._priority);\n\n let res: Response;\n try {\n res = await fetch(url, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body,\n signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),\n });\n } catch (err) {\n // Network error — retryWithBackoff will retry on non-OcsApiError\n throw err;\n }\n\n if (res.status >= 500) {\n // Treat 5xx as retryable network-layer errors (not OcsApiError, so backoff retries)\n throw new Error(`HTTP ${res.status} ${res.statusText}`);\n }\n\n if (!res.ok) {\n throw new OcsApiError(\n res.status,\n `HTTP ${res.status} ${res.statusText}`,\n method,\n );\n }\n\n const json = (await res.json()) as OcsResponse<T>;\n\n if (json.status?.code !== 0) {\n const ocsCode = json.status?.code ?? -1;\n const rawMsg = json.status?.msg ?? \"Unknown error\";\n // Pass full json body for error-detail extraction (e.g. invalidTadigs)\n const rawBody = json as unknown as Record<string, unknown>;\n const { message, details } = ocsErrorMessage(ocsCode, rawMsg, rawBody);\n throw new OcsApiError(ocsCode, message, method, details);\n }\n\n // Resolve the response payload, honoring the live-OCS key remaps.\n let payload: T;\n if (method === \"getCustomerTariff\" && json[\"listTariffRule\"] !== undefined) {\n // Fix #14: getCustomerTariff response keyed as \"listTariffRule\"\n payload = json[\"listTariffRule\"] as T;\n } else if (method === \"getSubscriberLocationByCellId\") {\n const byMethod = json[method] as T | undefined;\n if (byMethod !== undefined) {\n // Present null is a valid payload; do not fall through to envelope fallback.\n payload = byMethod;\n } else if (json[\"subscriberLocation\"] !== undefined) {\n // GeoSense: live OCS returns coordinates under \"subscriberLocation\", not the method name\n payload = json[\"subscriberLocation\"] as T;\n } else {\n payload = (json[method] as T) ?? (json as unknown as T);\n }\n } else {\n payload = (json[method] as T) ?? (json as unknown as T);\n }\n\n // Contract validation: if this method has a pinned response schema, assert\n // the payload shape. A mismatch = provider drift. By default we WARN once\n // (surfacing the drift without breaking the call — the live shapes are a\n // sampled snapshot, so we don't hard-fail prod traffic on an un-sampled\n // variant). Opt into `{ strictValidation: true }` to throw OcsContractError.\n if (this._validateResponses) {\n const schema = RESPONSE_SCHEMAS[method];\n if (schema) {\n const parsed = schema.safeParse(payload);\n if (!parsed.success) {\n const issues: OcsContractIssue[] = parsed.error.issues.map((i) => ({\n path: i.path.join(\".\") || \"<root>\",\n message: i.message,\n }));\n if (this._strictValidation) {\n throw new OcsContractError(method, issues, payload);\n }\n if (!this._warnedValidation.has(method)) {\n this._warnedValidation.add(method);\n console.warn(`[ocs-client] ${new OcsContractError(method, issues, payload).message}`);\n }\n }\n }\n }\n\n return payload;\n });\n }\n}\n","/**\n * Shared OCS request builders + response parsers.\n *\n * Console, REST API, MCP, and CLI MUST call these instead of inventing\n * per-surface shapes. Live eSIMVault OCS is picky:\n * - getSubscriberLocation takes a flat SubscriberId ({ iccid }), NOT\n * { subscriber: { iccid } } (OCS error 2 \"Unrecognized field subscriber\").\n * - subscriberUsageOverPeriod takes { subscriber: { iccid }, period }\n * and a max inclusive window of 7 days. Wider windows 400/empty.\n * - Usage bytes live in quantityPerType[\"33\"]; daily rollup may sit at\n * data.subsPeriodUsages OR data.usages[0].subsPeriodUsages.\n */\n\nexport const OCS_MAX_USAGE_WINDOW_DAYS = 7;\nexport const OCS_DATA_USAGE_TYPE = \"33\";\n\nexport function clampUsagePeriod(\n start: string,\n end: string,\n maxDays = OCS_MAX_USAGE_WINDOW_DAYS,\n): { start: string; end: string } {\n const endMs = Date.parse(`${end}T00:00:00Z`);\n const startMs = Date.parse(`${start}T00:00:00Z`);\n if (Number.isNaN(endMs) || Number.isNaN(startMs) || startMs > endMs) {\n return { start, end };\n }\n const minStartMs = endMs - (maxDays - 1) * 86_400_000;\n if (startMs < minStartMs) {\n return { start: new Date(minStartMs).toISOString().slice(0, 10), end };\n }\n return { start, end };\n}\n\n/** Inclusive last-N-days window ending today (UTC). days=7 → start = today-6. */\nexport function lastNDaysPeriod(days = OCS_MAX_USAGE_WINDOW_DAYS, now = new Date()): { start: string; end: string } {\n const end = new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate()));\n const start = new Date(end);\n start.setUTCDate(end.getUTCDate() - (days - 1));\n return {\n start: start.toISOString().slice(0, 10),\n end: end.toISOString().slice(0, 10),\n };\n}\n\nexport function locationParams(iccid: string): { iccid: string } {\n return { iccid };\n}\n\n/** Flat SubscriberId — same shape as location. Active period rejects a nested subscriber wrapper. */\nexport function subscriberIdParams(iccid: string): { iccid: string } {\n return { iccid };\n}\n\nexport function networkEventsOverPeriodParams(\n iccid: string,\n start: string,\n end: string,\n): { subscriber: { iccid: string }; period: { start: string; end: string } } {\n return usageOverPeriodParams(iccid, start, end);\n}\n\nexport type OcsRadioType = \"2G\" | \"3G\" | \"4G\" | \"5G\" | \"NB-IoT\";\n\nexport function radioTypeFromRat(rat: unknown): OcsRadioType | undefined {\n const s = String(rat ?? \"\").toUpperCase();\n if (!s) return undefined;\n if (s.includes(\"NB\")) return \"NB-IoT\";\n if (s.includes(\"5G\") || s.includes(\"NR\")) return \"5G\";\n if (s.includes(\"4G\") || s.includes(\"LTE\")) return \"4G\";\n if (s.includes(\"3G\") || s.includes(\"UMTS\") || s.includes(\"HSPA\") || s.includes(\"WCDMA\")) return \"3G\";\n if (s.includes(\"2G\") || s.includes(\"GSM\") || s.includes(\"GPRS\") || s.includes(\"EDGE\")) return \"2G\";\n return undefined;\n}\n\nexport interface CellTuple {\n radioType: OcsRadioType;\n mcc: number;\n mnc: number;\n lac: number;\n cellId?: number;\n}\n\nexport function cellTupleFromNetworkInfo(networkInfo: unknown): CellTuple | undefined {\n if (!networkInfo || typeof networkInfo !== \"object\") return undefined;\n const rec = networkInfo as Record<string, unknown>;\n const radioType = radioTypeFromRat(rec.lastRat ?? rec.rat ?? rec.radioType);\n const mcc = Number(rec.lastMcc ?? rec.mcc);\n const mnc = Number(rec.lastMnc ?? rec.mnc);\n const lac = Number(rec.lastLac ?? rec.lac);\n const cellIdRaw = rec.lastCellId ?? rec.cellId;\n if (!radioType || !Number.isFinite(mcc) || !Number.isFinite(mnc) || !Number.isFinite(lac)) return undefined;\n const out: CellTuple = { radioType, mcc, mnc, lac };\n const cellId = Number(cellIdRaw);\n if (Number.isFinite(cellId) && cellId > 0) out.cellId = cellId;\n return out;\n}\n\nexport function cellLocationParams(cell: CellTuple): {\n radioType: OcsRadioType;\n mcc: number;\n mnc: number;\n lac: number;\n cellId?: number;\n} {\n const params: { radioType: OcsRadioType; mcc: number; mnc: number; lac: number; cellId?: number } = {\n radioType: cell.radioType,\n mcc: cell.mcc,\n mnc: cell.mnc,\n lac: cell.lac,\n };\n if (cell.cellId != null) params.cellId = cell.cellId;\n return params;\n}\n\nexport function usageOverPeriodParams(\n iccid: string,\n start: string,\n end: string,\n): { subscriber: { iccid: string }; period: { start: string; end: string } } {\n const period = clampUsagePeriod(start, end);\n return { subscriber: { iccid }, period };\n}\n\nexport interface UsageDayPoint {\n date: string;\n mb: number;\n}\n\nexport interface UsageCountrySlice {\n country: string;\n mb: number;\n operators: { name: string; mb: number }[];\n}\n\nexport function mbFromBytes(bytes: number | undefined): number {\n if (!bytes) return 0;\n return Math.round(bytes / 1_048_576);\n}\n\nexport function dataBytesFromQuantity(qty: unknown): number {\n if (!qty || typeof qty !== \"object\") return 0;\n const rec = qty as Record<string, unknown>;\n const raw = rec[OCS_DATA_USAGE_TYPE] ?? rec[33];\n return typeof raw === \"number\" ? raw : 0;\n}\n\nfunction periodRows(data: Record<string, unknown> | undefined): unknown[] {\n if (!data) return [];\n if (Array.isArray(data.subsPeriodUsages) && data.subsPeriodUsages.length) return data.subsPeriodUsages;\n const usages = Array.isArray(data.usages) ? data.usages : [];\n return usages.flatMap((u) => {\n const rec = u as { subsPeriodUsages?: unknown[] };\n return Array.isArray(rec.subsPeriodUsages) ? rec.subsPeriodUsages : [];\n });\n}\n\n/** Accepts the REST `{ data }` envelope or the raw OCS usage object. */\nexport function extractUsageTimeline(json: unknown): UsageDayPoint[] {\n const root = (json ?? {}) as Record<string, unknown>;\n const data = (root.data ?? root.usage ?? root.subscriberUsageOverPeriod ?? root) as Record<string, unknown>;\n if (Array.isArray(data)) {\n return data\n .map((entry: { date?: string; day?: string; mb?: number; bytes?: number }) => ({\n date: entry.date ?? entry.day ?? \"\",\n mb: entry.mb ?? mbFromBytes(entry.bytes),\n }))\n .filter((p) => p.date);\n }\n if (!data || typeof data !== \"object\") return [];\n\n const periods = periodRows(data);\n if (periods.length > 0) {\n return periods\n .map((p) => {\n const rec = p as { day?: string; date?: string; total?: { quantityPerType?: unknown } };\n return {\n date: rec.day ?? rec.date ?? \"\",\n mb: mbFromBytes(dataBytesFromQuantity(rec.total?.quantityPerType)),\n };\n })\n .filter((p) => p.date);\n }\n\n const sessions = Array.isArray(data.usages)\n ? (data.usages as { subsDailyUsages?: unknown[]; usageDateUtc?: string; quantity?: number }[]).flatMap((u) =>\n Array.isArray(u.subsDailyUsages) && u.subsDailyUsages.length ? u.subsDailyUsages : [u],\n )\n : [];\n if (sessions.length === 0) return [];\n const byDay = new Map<string, number>();\n for (const s of sessions) {\n const rec = s as { day?: string; usageDateUtc?: string; quantity?: number; total?: { quantityPerType?: unknown } };\n const day = rec.day ?? (typeof rec.usageDateUtc === \"string\" ? rec.usageDateUtc.slice(0, 10) : \"\");\n if (!day) continue;\n const bytes =\n typeof rec.quantity === \"number\" ? rec.quantity : dataBytesFromQuantity(rec.total?.quantityPerType);\n byDay.set(day, (byDay.get(day) ?? 0) + bytes);\n }\n return [...byDay.entries()]\n .sort(([a], [b]) => a.localeCompare(b))\n .map(([date, bytes]) => ({ date, mb: mbFromBytes(bytes) }));\n}\n\nexport function extractUsageBreakdown(json: unknown): { totalMb: number; countries: UsageCountrySlice[] } {\n const root = (json ?? {}) as Record<string, unknown>;\n const data = (root.data ?? root.usage ?? root.subscriberUsageOverPeriod ?? root) as Record<string, unknown> | undefined;\n const totalObj =\n data && typeof data === \"object\"\n ? ((data.total ?? (Array.isArray(data.usages) ? (data.usages[0] as { total?: unknown })?.total : undefined)) as\n | { quantityPerType?: unknown; quantityPerCountry?: unknown[] }\n | undefined)\n : undefined;\n const totalMb = mbFromBytes(dataBytesFromQuantity(totalObj?.quantityPerType));\n const rows = (Array.isArray(totalObj?.quantityPerCountry) ? totalObj.quantityPerCountry : []) as {\n name?: string;\n qty?: number;\n quantityPerOperator?: { name?: string; qty?: number }[];\n }[];\n const countries: UsageCountrySlice[] = rows.map((c) => ({\n country: c.name ?? \"Unknown\",\n mb: mbFromBytes(typeof c.qty === \"number\" ? c.qty : 0),\n operators: (c.quantityPerOperator ?? []).map((o) => ({\n name: o.name ?? \"Unknown\",\n mb: mbFromBytes(typeof o.qty === \"number\" ? o.qty : 0),\n })),\n }));\n return { totalMb, countries };\n}\n\nexport function parseSubscriberLocation(json: unknown): {\n latitude: number;\n longitude: number;\n accuracy?: number;\n at?: string;\n} | undefined {\n const root = (json ?? {}) as Record<string, unknown>;\n const loc = (root.subscriberLocation ??\n (root.data as Record<string, unknown> | undefined)?.subscriberLocation ??\n root.data ??\n root) as Record<string, unknown> | undefined;\n if (!loc || typeof loc !== \"object\") return undefined;\n const lat = loc.latitude;\n const lng = loc.longitude;\n if (typeof lat !== \"number\" || typeof lng !== \"number\") return undefined;\n const out: { latitude: number; longitude: number; accuracy?: number; at?: string } = {\n latitude: lat,\n longitude: lng,\n };\n if (typeof loc.accuracy === \"number\") out.accuracy = loc.accuracy;\n const at = typeof loc.dateTime === \"string\" ? loc.dateTime : typeof loc.at === \"string\" ? loc.at : undefined;\n if (at) out.at = at;\n return out;\n}\n\nexport interface NetworkEvent {\n timestamp: string;\n type: string;\n description: string;\n country?: string;\n operator?: string;\n mcc?: number;\n mnc?: number;\n protocol?: string;\n}\n\nfunction flattenNetworkEventGroups(payload: unknown): unknown[] {\n if (Array.isArray(payload)) return payload;\n if (!payload || typeof payload !== \"object\") return [];\n const rec = payload as Record<string, unknown>;\n if (Array.isArray(rec.events)) return rec.events;\n const groups = [\"mapAnswer\", \"s6aAnswer\", \"gyAnswer\", \"camelAnswer\", \"mocAnswer\", \"mtcAnswer\"] as const;\n const out: unknown[] = [];\n for (const key of groups) {\n const group = rec[key];\n if (group && typeof group === \"object\" && Array.isArray((group as { events?: unknown[] }).events)) {\n out.push(...((group as { events: unknown[] }).events));\n }\n }\n return out;\n}\n\nexport function extractNetworkEvents(json: unknown): NetworkEvent[] {\n const root = (json ?? {}) as Record<string, unknown>;\n const payload = root.subscriberNetworkEventsOverPeriod ?? root.data ?? root.events ?? root;\n const rows = flattenNetworkEventGroups(payload);\n return rows\n .map((row) => {\n const rec = (row ?? {}) as Record<string, unknown>;\n const timestamp = String(rec.eventTime ?? rec.timestamp ?? rec.time ?? rec.dateTime ?? \"\");\n const longCode = typeof rec.longCode === \"string\" ? rec.longCode : undefined;\n const shortCode = typeof rec.shortCode === \"string\" ? rec.shortCode : undefined;\n const type = String(rec.eventType ?? rec.type ?? longCode ?? shortCode ?? \"network\");\n const country = typeof rec.countryName === \"string\" ? rec.countryName : typeof rec.country === \"string\" ? rec.country : undefined;\n const operator = typeof rec.operator === \"string\" ? rec.operator : undefined;\n const mcc = typeof rec.mcc === \"number\" ? rec.mcc : undefined;\n const mnc = typeof rec.mnc === \"number\" ? rec.mnc : undefined;\n const protocol = typeof rec.protocol === \"string\" ? rec.protocol : undefined;\n const result = typeof rec.result === \"string\" ? rec.result : undefined;\n const parts = [type, operator, country, result].filter(Boolean);\n const event: NetworkEvent = {\n timestamp,\n type,\n description: parts.join(\" · \") || \"Network event\",\n };\n if (country) event.country = country;\n if (operator) event.operator = operator;\n if (mcc != null) event.mcc = mcc;\n if (mnc != null) event.mnc = mnc;\n if (protocol) event.protocol = protocol;\n return event;\n })\n .filter((e) => e.timestamp)\n .sort((a, b) => b.timestamp.localeCompare(a.timestamp));\n}\n\nexport function parseActivePeriod(json: unknown): { start?: string; end?: string } | undefined {\n const root = (json ?? {}) as Record<string, unknown>;\n const payload = (root.getSubscriberActivePeriod ?? root.data ?? root) as Record<string, unknown>;\n const period = (payload.period ?? payload) as Record<string, unknown>;\n const start = typeof period.start === \"string\" ? period.start : undefined;\n const end = typeof period.end === \"string\" ? period.end : undefined;\n if (!start && !end) return undefined;\n const out: { start?: string; end?: string } = {};\n if (start) out.start = start;\n if (end) out.end = end;\n return out;\n}\n\nexport interface EsimStatusCounts {\n active: number;\n suspended: number;\n inventory: number;\n other: number;\n total: number;\n}\n\nfunction emptyEsimStatusCounts(): EsimStatusCounts {\n return { active: 0, suspended: 0, inventory: 0, other: 0, total: 0 };\n}\n\nfunction addEsimStatusRow(acc: EsimStatusCounts, rec: Record<string, unknown>): void {\n const count = Number(rec.count ?? 0);\n if (!Number.isFinite(count) || count === 0) return;\n // Live OCS (account 5259, 2026-08-18): statusNum 0=Free, 2=Affected.\n // Do not trust statusNum 2 as Suspended. Prefer statusStr when present.\n const str = String(rec.statusStr ?? rec.name ?? rec.status ?? \"\").toUpperCase();\n if (str === \"ACTIVATED\" || str === \"ACTIVE\" || str === \"AFFECTED\") {\n acc.active += count;\n return;\n }\n if (str === \"SUSPENDED\") {\n acc.suspended += count;\n return;\n }\n if (str === \"FREE\" || str === \"INVENTORY\" || str === \"NOT_ACTIVATED\" || str === \"AVAILABLE\") {\n acc.inventory += count;\n return;\n }\n const num = rec.statusNum;\n if (typeof num === \"number\") {\n if (num === 1) acc.active += count;\n else if (num === 0) acc.inventory += count;\n else if (num === 2) acc.suspended += count;\n else acc.other += count;\n return;\n }\n acc.other += count;\n}\n\n/**\n * Unwrap esimStatusPerAccount.\n * Live shape: { account: [{ sponsor: [{ esim: { status: [{ statusNum, statusStr, count }] } }] }] }\n * statusNum: 0 Free / 1 Activated / 2 Suspended / 3+ other.\n */\nexport function extractEsimStatusCounts(json: unknown): EsimStatusCounts {\n const acc = emptyEsimStatusCounts();\n const root = (json ?? {}) as Record<string, unknown>;\n const payload = (root.data ?? root.esimStatusPerAccount ?? root) as Record<string, unknown> | unknown[];\n const accounts: unknown[] = Array.isArray(payload)\n ? payload\n : Array.isArray((payload as Record<string, unknown>).account)\n ? ((payload as Record<string, unknown>).account as unknown[])\n : [];\n\n for (const account of accounts) {\n const rec = (account ?? {}) as Record<string, unknown>;\n const sponsors = Array.isArray(rec.sponsor) ? rec.sponsor : [];\n if (sponsors.length > 0) {\n for (const sp of sponsors) {\n const esim = ((sp as Record<string, unknown>)?.esim ?? {}) as Record<string, unknown>;\n const statuses = Array.isArray(esim.status) ? esim.status : [];\n for (const row of statuses) addEsimStatusRow(acc, (row ?? {}) as Record<string, unknown>);\n }\n continue;\n }\n acc.active += Number(rec.active ?? 0);\n acc.suspended += Number(rec.suspended ?? 0);\n acc.inventory += Number(rec.inventory ?? rec.notActivated ?? rec.free ?? 0);\n acc.other += Number(rec.other ?? rec.terminated ?? 0);\n }\n\n acc.total = acc.active + acc.suspended + acc.inventory + acc.other;\n return acc;\n}\n\nexport function esimStatusPerAccountParams(accountId: number): { accountId: number } {\n return { accountId };\n}\n\n/** Surfaces that must stay aligned on these builders. Used by the contract test. */\nexport const OCS_PARITY_SURFACES = [\"api\", \"console\", \"mcp-stdio\", \"mcp-server\", \"cli\"] as const;\n","/**\n * Storefront logo generator.\n *\n * Works on any box that has the Carrier CLI or MCP — no image-API key required.\n * Produces a distinctive SVG mark from brand name + accent. If an OpenAI key is\n * available (worker secret or local env), optionally returns a raster PNG too.\n */\n\nexport type LogoMark = {\n svg: string;\n filename: string;\n source: \"svg\" | \"openai\";\n pngBase64?: string;\n};\n\nconst OPENAI_KEY_NAMES = [\n \"OPENAI_API_KEY\",\n \"OPENAI_KEY\",\n \"CARRIER_OPENAI_API_KEY\",\n] as const;\n\nexport function firstImageKey(env: Record<string, string | undefined> = process.env): string | undefined {\n for (const name of OPENAI_KEY_NAMES) {\n const v = env[name];\n if (v && v.trim() && !v.startsWith(\"__\") && !v.includes(\"PLACEHOLD\")) return v.trim();\n }\n return undefined;\n}\n\nfunction clampHex(hex: string, fallback: string): string {\n const h = hex.trim();\n return /^#[0-9a-fA-F]{6}$/.test(h) ? h.toUpperCase() : fallback;\n}\n\nfunction hashName(name: string): number {\n let h = 2166136261;\n for (let i = 0; i < name.length; i++) {\n h ^= name.charCodeAt(i);\n h = Math.imul(h, 16777619);\n }\n return h >>> 0;\n}\n\nfunction mix(hex: string, toward: string, t: number): string {\n const parse = (x: string) => [\n parseInt(x.slice(1, 3), 16),\n parseInt(x.slice(3, 5), 16),\n parseInt(x.slice(5, 7), 16),\n ] as const;\n const a = parse(hex);\n const b = parse(toward);\n const ch = (i: number) =>\n Math.round(a[i] + (b[i] - a[i]) * t)\n .toString(16)\n .padStart(2, \"0\");\n return `#${ch(0)}${ch(1)}${ch(2)}`;\n}\n\n/** Distinctive 512×512 SVG mark — letter + geometric seal from the brand name. */\nexport function generateStorefrontLogoSvg(\n name: string,\n accent = \"#FF6B35\",\n): string {\n const brand = (name.trim() || \"C\").slice(0, 24);\n const letter = brand[0]!.toUpperCase();\n const accentHex = clampHex(accent, \"#FF6B35\");\n const deep = mix(accentHex, \"#1A1812\", 0.45);\n const light = mix(accentHex, \"#FFF8EB\", 0.35);\n const h = hashName(brand);\n const style = h % 4;\n const uid = `lg${(h % 1_000_000).toString(16)}`;\n\n let seal = \"\";\n if (style === 0) {\n seal = `<rect x=\"56\" y=\"56\" width=\"400\" height=\"400\" rx=\"96\" fill=\"url(#${uid}g)\"/>`;\n } else if (style === 1) {\n seal = `<circle cx=\"256\" cy=\"256\" r=\"196\" fill=\"url(#${uid}g)\"/>`;\n } else if (style === 2) {\n seal = `<path d=\"M256 48 L432 152 V360 L256 464 L80 360 V152 Z\" fill=\"url(#${uid}g)\"/>`;\n } else {\n seal = `<path d=\"M256 64 C320 64 400 96 424 176 C448 256 424 352 344 416 C280 464 232 464 168 416 C88 352 64 256 88 176 C112 96 192 64 256 64 Z\" fill=\"url(#${uid}g)\"/>`;\n }\n\n return `<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 512 512\" width=\"512\" height=\"512\" role=\"img\" aria-label=\"${escapeXml(brand)} logo\">\n <defs>\n <linearGradient id=\"${uid}g\" x1=\"0\" y1=\"0\" x2=\"1\" y2=\"1\">\n <stop offset=\"0%\" stop-color=\"${light}\"/>\n <stop offset=\"55%\" stop-color=\"${accentHex}\"/>\n <stop offset=\"100%\" stop-color=\"${deep}\"/>\n </linearGradient>\n </defs>\n <rect width=\"512\" height=\"512\" rx=\"96\" fill=\"#FAFAF7\"/>\n ${seal}\n <text x=\"256\" y=\"300\" text-anchor=\"middle\" font-family=\"Georgia, 'Times New Roman', serif\" font-size=\"220\" font-weight=\"700\" fill=\"#FFF8EB\">${escapeXml(letter)}</text>\n</svg>\n`;\n}\n\nfunction escapeXml(s: string): string {\n return s.replace(/&/g, \"&\").replace(/</g, \"<\").replace(/>/g, \">\").replace(/\"/g, \""\");\n}\n\nexport function logoPrompt(name: string, accent: string, tagline?: string): string {\n return [\n `App icon / brand mark for \"${name}\", a travel eSIM storefront.`,\n tagline ? `Tagline: ${tagline}.` : \"\",\n `Flat vector, single centered symbol, no letters unless they are a refined monogram of \"${name[0] ?? \"C\"}\".`,\n `Color: ${accent} on warm cream #FAFAF7. Soft squircle icon, premium, simple, high contrast.`,\n `No photorealism, no mockups, no shadows of devices, no watermarks.`,\n ]\n .filter(Boolean)\n .join(\" \");\n}\n\n/** Best-effort OpenAI image. Returns undefined on any failure. */\nexport async function tryOpenAiLogoPng(\n name: string,\n accent: string,\n apiKey: string,\n tagline?: string,\n): Promise<string | undefined> {\n try {\n const res = await fetch(\"https://api.openai.com/v1/images/generations\", {\n method: \"POST\",\n headers: {\n Authorization: `Bearer ${apiKey}`,\n \"Content-Type\": \"application/json\",\n },\n body: JSON.stringify({\n model: \"dall-e-3\",\n prompt: logoPrompt(name, accent, tagline),\n size: \"1024x1024\",\n quality: \"hd\",\n response_format: \"b64_json\",\n n: 1,\n }),\n });\n if (!res.ok) return undefined;\n const json = (await res.json()) as { data?: Array<{ b64_json?: string }> };\n return json.data?.[0]?.b64_json;\n } catch {\n return undefined;\n }\n}\n\nexport async function generateStorefrontLogo(\n input: { name: string; accent?: string; tagline?: string; env?: Record<string, string | undefined> },\n): Promise<LogoMark> {\n const name = input.name.trim() || \"Carrier\";\n const accent = clampHex(input.accent ?? \"#FF6B35\", \"#FF6B35\");\n const svg = generateStorefrontLogoSvg(name, accent);\n const key = firstImageKey(input.env ?? (typeof process !== \"undefined\" ? process.env : {}));\n if (key) {\n const png = await tryOpenAiLogoPng(name, accent, key, input.tagline);\n if (png) {\n return { svg, filename: \"logo.svg\", source: \"openai\", pngBase64: png };\n }\n }\n return { svg, filename: \"logo.svg\", source: \"svg\" };\n}\n"],"mappings":"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","names":["WINDOW_MS"]}
|
|
1
|
+
{"version":3,"sources":["../package.json","../src/version.ts","../../../packages/ocs-client/src/rate-floor.ts","../../../packages/ocs-client/src/rate-governor.ts","../../../packages/ocs-client/src/client.ts","../../../packages/ocs-client/src/ocs-params.ts","../src/lib/storefront-logo.ts"],"sourcesContent":["{\n \"name\": \"@carrierllc/mcp\",\n \"version\": \"0.3.2\",\n \"description\": \"Carrier MCP \\u2014 natural-language control of MVNO/eSIM fleets via eSIMVault OCS. Stdio mode for direct integration with Claude Desktop, Cursor, Windsurf, and MCP-compatible clients. Ships the `carrier` CLI (plugin install + white-label eSIM storefront scaffold).\",\n \"license\": \"MIT\",\n \"author\": \"Carrier (Lifecycle Innovations Limited)\",\n \"homepage\": \"https://mcp.carrier.llc\",\n \"repository\": {\n \"type\": \"git\",\n \"url\": \"https://github.com/Lifecycle-Innovations-Limited/carrier.llc\"\n },\n \"type\": \"module\",\n \"main\": \"./dist/index.js\",\n \"files\": [\n \"dist\",\n \"plugin\",\n \"templates\",\n \"README.md\"\n ],\n \"scripts\": {\n \"build\": \"tsup\",\n \"type-check\": \"tsc --noEmit\",\n \"lint\": \"eslint src\",\n \"prepublishOnly\": \"pnpm run build\",\n \"test\": \"pnpm run build && node --test test/*.test.js\"\n },\n \"dependencies\": {\n \"@clack/prompts\": \"^0.7.0\",\n \"@clerk/backend\": \"^3.14.0\",\n \"@modelcontextprotocol/ext-apps\": \"^1.7.5\",\n \"@modelcontextprotocol/sdk\": \"^1.29.0\",\n \"@noble/hashes\": \"^1.8.0\",\n \"@sentry/cloudflare\": \"^10.65.0\",\n \"aws4fetch\": \"^1.0.20\",\n \"commander\": \"^15.0.0\",\n \"picocolors\": \"^1.1.1\",\n \"zod\": \"^4.4.3\"\n },\n \"devDependencies\": {\n \"@cloudflare/workers-types\": \"^4.20260521.1\",\n \"@types/node\": \"^26.1.2\",\n \"@typescript-eslint/eslint-plugin\": \"^8.59.4\",\n \"@typescript-eslint/parser\": \"^8.67.0\",\n \"eslint\": \"^9.39.4\",\n \"tsup\": \"^8.5.1\",\n \"typescript\": \"^5.9.3\",\n \"@carrier/ocs-client\": \"workspace:*\",\n \"@carrier/ocs-spec\": \"workspace:*\"\n },\n \"keywords\": [\n \"mcp\",\n \"model-context-protocol\",\n \"esim\",\n \"mvno\",\n \"carrier\",\n \"fleet-management\",\n \"esimvault\"\n ],\n \"engines\": {\n \"node\": \">=22\"\n },\n \"publishConfig\": {\n \"access\": \"public\"\n },\n \"bin\": {\n \"carrier-mcp\": \"dist/index.js\",\n \"carrier\": \"dist/cli.js\"\n }\n}\n","// JSON modules are default-only, so this pulls the manifest in and reads one field.\nimport pkg from \"../package.json\" with { type: \"json\" };\n\n/**\n * Single source of truth for the published version.\n *\n * The CLI banner, `carrier --version`, the stdio MCP server's clientInfo and the\n * `carrier ask` handshake all read this. It used to be typed out in four places,\n * which is how seven commits shipped under a stale version string. Bump\n * package.json and every surface follows.\n */\nexport const CARRIER_VERSION: string = pkg.version;\n","/**\n * Global sliding-window rate floor for the OCS client.\n *\n * Scoped per resellerId (derived from the API token). Lives on a module-level\n * Map so it is shared across all OcsClient instances within one Worker isolate.\n *\n * Enforces ≤ 10 calls per 1000ms per reseller key.\n * Uses a sliding-window log: tracks the last 10 call timestamps.\n * Before the 11th call, waits until the oldest recorded call is at least\n * 1000ms in the past — guaranteeing ≤10 in any 1000ms span.\n *\n * Uses a serialised promise-chain (gate) so concurrent callers queue up\n * atomically. Without serialisation, multiple callers could read the same\n * window state simultaneously and all proceed, defeating the floor.\n *\n * Set env var RATE_FLOOR_DISABLED=true to bypass (never do this in production).\n */\n\nconst RATE_LIMIT = 10; // max calls per window\nconst WINDOW_MS = 1000; // window size in ms\n\ninterface WindowState {\n /** Timestamps of the last RATE_LIMIT calls (oldest first). */\n log: number[];\n /** Serialisation gate: every acquire chains onto this promise. */\n gate: Promise<void>;\n}\n\n// Module-level — shared across all OcsClient instances per isolate.\nconst windows = new Map<string, WindowState>();\n\nfunction getWindowState(key: string): WindowState {\n let s = windows.get(key);\n if (!s) {\n s = { log: [], gate: Promise.resolve() };\n windows.set(key, s);\n }\n return s;\n}\n\n/**\n * Acquire one slot for the given reseller key, awaiting if necessary.\n * No-op when RATE_FLOOR_DISABLED=true.\n */\nexport async function acquireToken(resellerId: string): Promise<void> {\n // Allow env-based bypass for integration/load tests that need to spike.\n // Tests asserting the floor works must NOT set this flag.\n // Resolve `process` via globalThis so TS doesn't need @types/node\n // (Workers tsconfig excludes Node types; runtime guard handles both envs).\n const proc = (globalThis as Record<string, unknown>)[\"process\"] as\n | { env?: Record<string, string | undefined> }\n | undefined;\n if (\n proc?.env?.[\"RATE_FLOOR_DISABLED\"] === \"true\" ||\n (globalThis as Record<string, unknown>)[\"RATE_FLOOR_DISABLED\"] === \"true\"\n ) {\n return;\n }\n\n const state = getWindowState(resellerId);\n\n // Chain onto the existing gate so only one acquire runs at a time.\n const ticket = state.gate.then(async () => {\n while (true) {\n const now = Date.now();\n\n if (state.log.length < RATE_LIMIT) {\n state.log.push(now);\n return;\n }\n\n const oldest = state.log[0]!;\n const age = now - oldest;\n\n if (age >= WINDOW_MS) {\n state.log.shift();\n state.log.push(now);\n return;\n }\n\n const waitMs = WINDOW_MS - age + 1;\n await new Promise<void>((r) => setTimeout(r, waitMs));\n }\n });\n\n // Advance the gate so the next caller queues behind this one.\n state.gate = ticket;\n\n return ticket;\n}\n\n/** Exposed for testing: reset all window state. */\nexport function _resetBucketsForTest(): void {\n windows.clear();\n}\n","/**\n * Per-endpoint rate-limit governor for Bridge4IP / OCS API calls.\n *\n * LIMITS (per reseller, confirmed by Bridge4IP):\n * Global: 600 / min\n * modifySubscriberMobilePlan: 300 / min\n * AffectRecurringPackageToSub: 300 / min\n * AffectSubscriberFakePhoneNumber: 300 / min\n * AffectSubscriberRealPhoneNumber: 300 / min\n * EsimStatusPerAccount: 300 / min\n * GetSubscriberActivePeriod: 300 / min\n * HlrGetBitrate / HlrSetBitrate: 300 / min\n * ListSubscriber: 300 / min\n * ListSubscriberPrepaidPackages: 300 / min\n * MoveSubscriberRangeToAccount: 300 / min\n * PushSteeringToSubs: 300 / min\n * SetSubscriberTrafficRestrictions:300 / min\n * ListLocationZoneElement: 150 / min\n * SendMtSms: 150 / min\n * SubscriberUsageOverPeriod: 100 / min ← tight\n * SubscriberNetworkEventsOverPeriod: 30 / min ← VERY TIGHT\n * getSingleSubscriber (TBD): 300 / min (assumed; alert on 429)\n * getSubscriberLocationByCellId (TBD): 300 / min (assumed; alert on 429)\n *\n * CAPACITY RESERVATION:\n * 80% of each bucket is reserved for interactive (MCP tool) calls.\n * 20% is reserved for batch / cron callers (ocs-bridge polls).\n * Callers pass `priority: \"interactive\" | \"batch\"` to acquireEndpointSlot().\n * Interactive callers draw from the full bucket capacity.\n * Batch callers can only draw up to 20% of the per-minute limit per minute.\n *\n * ALGORITHM:\n * Sliding-window log per (resellerId, endpoint) key.\n * Separate batch-window log per key to enforce the 20% ceiling.\n * Serialised promise-chain gate prevents TOCTOU races within a single isolate.\n * (CF Workers: mcp-server DO serialises per-session; ocs-bridge uses KV for cross-isolate.)\n *\n * NEVER LEAK MONEY:\n * Set RATE_FLOOR_DISABLED=true ONLY in test environments.\n * Any process-global flag bypasses all per-endpoint governors.\n */\n\nexport type CallPriority = \"interactive\" | \"batch\";\n\n// ---------------------------------------------------------------------------\n// Endpoint limit table\n// ---------------------------------------------------------------------------\n\n/** Per-minute cap for a given OCS method (camelCase). Default: 300/min. */\nconst ENDPOINT_LIMITS_PER_MIN: Record<string, number> = {\n // Global sentinel (keyed as \"__global__\")\n __global__: 600,\n\n // 30/min — tightest endpoint\n subscriberNetworkEventsOverPeriod: 30,\n\n // 100/min\n subscriberUsageOverPeriod: 100,\n\n // 150/min\n listLocationZoneElement: 150,\n sendMtSms: 150,\n\n // 300/min (explicit; also the fallback default)\n modifySubscriberMobilePlan: 300,\n affectRecurringPackageToSubscriber: 300,\n affectSubscriberFakePhoneNumber: 300,\n affectSubscriberRealPhoneNumber: 300,\n esimStatusPerAccount: 300,\n getSubscriberActivePeriod: 300,\n hlrGetBitrate: 300,\n hlrSetBitrate: 300,\n listSubscriber: 300,\n listSubscriberPrepaidPackages: 300,\n moveSubscriberRangeToAccount: 300,\n pushSteeringToSubs: 300,\n setSubscriberTrafficRestrictions: 300,\n getSingleSubscriber: 300,\n getSubscriberLocationByCellId: 300,\n};\n\nconst DEFAULT_LIMIT_PER_MIN = 300;\nconst WINDOW_MS = 60_000; // 1 minute\n\n/** Wall-clock throttle per endpoint bucket — avoids JSON log spam on every OCS call. */\nconst METRIC_EMIT_MIN_INTERVAL_MS = 10_000;\n\n/** Fraction of capacity reserved for batch callers (20%). */\nconst BATCH_FRACTION = 0.2;\n\n/** Non-reversible id for metrics only (callers may pass API tokens as reseller keys). */\nfunction resellerKeyHash(resellerKey: string | undefined): string {\n if (!resellerKey) return \"00000000\";\n let h = 5381;\n for (let i = 0; i < resellerKey.length; i++) {\n h = (((h << 5) + h) ^ resellerKey.charCodeAt(i)) >>> 0;\n }\n return h.toString(16).padStart(8, \"0\");\n}\n\n// ---------------------------------------------------------------------------\n// Prometheus-style metric emission\n// ---------------------------------------------------------------------------\n\n/**\n * Emit a structured metric line for external scraping / CF Analytics Engine.\n * Format: metric{labels} value timestamp_ms\n */\nfunction emitMetric(\n endpoint: string,\n resellerKey: string,\n callsInWindow: number,\n limitPerMin: number,\n): void {\n const pct = Math.round((callsInWindow / limitPerMin) * 100);\n const reseller_hash = resellerKeyHash(resellerKey);\n // Structured log: picked up by CF Workers Tail / BetterStack / Prometheus remote-write.\n console.log(\n JSON.stringify({\n metric: \"carrier_ocs_calls_per_min\",\n endpoint,\n reseller_hash,\n calls_in_window: callsInWindow,\n limit_per_min: limitPerMin,\n utilisation_pct: pct,\n alert: pct >= 80,\n ts: Date.now(),\n }),\n );\n if (pct >= 80) {\n console.warn(\n `[rate-governor] ALERT: ${endpoint} at ${pct}% capacity (${callsInWindow}/${limitPerMin} per min) reseller=${reseller_hash}`,\n );\n }\n}\n\n// ---------------------------------------------------------------------------\n// Window state\n// ---------------------------------------------------------------------------\n\ninterface EndpointWindowState {\n /** All call timestamps in the current sliding window (oldest first). */\n log: number[];\n /** Batch-only call timestamps (subset of log). */\n batchLog: number[];\n /** Serialisation gate — one acquire at a time. */\n gate: Promise<void>;\n /** Last `emitMetric` wall time for this bucket (throttles log volume). */\n lastMetricEmitAt: number;\n /**\n * When true, the next sample at ≥80% utilisation should emit immediately\n * (edge-trigger) so alerts are not delayed by the interval throttle.\n */\n metricBelowHighUtil: boolean;\n}\n\n// Keyed as `${resellerId}:${endpoint}` (or `${resellerId}:__global__`)\nconst _endpointWindows = new Map<string, EndpointWindowState>();\n\nfunction _getEndpointState(key: string): EndpointWindowState {\n let s = _endpointWindows.get(key);\n if (!s) {\n s = {\n log: [],\n batchLog: [],\n gate: Promise.resolve(),\n lastMetricEmitAt: 0,\n metricBelowHighUtil: true,\n };\n _endpointWindows.set(key, s);\n }\n return s;\n}\n\nfunction _prune(log: number[], windowStart: number): number[] {\n // Remove entries older than the window boundary.\n let i = 0;\n while (i < log.length && log[i]! <= windowStart) i++;\n return i > 0 ? log.slice(i) : log;\n}\n\n// ---------------------------------------------------------------------------\n// Public API\n// ---------------------------------------------------------------------------\n\n/**\n * Acquire one OCS call slot for the given (resellerId, endpoint) pair.\n *\n * Enforces:\n * 1. Per-endpoint limit (from ENDPOINT_LIMITS_PER_MIN table).\n * 2. Global 600/min limit across all endpoints for this reseller.\n * 3. Batch callers capped at 20% of each bucket.\n *\n * Emits throttled `carrier_ocs_calls_per_min` JSON metrics (and warns at ≥80%\n * utilisation when a sample is emitted).\n *\n * No-op when RATE_FLOOR_DISABLED=true (never set in production).\n */\nexport async function acquireEndpointSlot(\n resellerId: string,\n endpoint: string,\n priority: CallPriority = \"interactive\",\n): Promise<void> {\n // Env bypass — test environments only.\n const proc = (globalThis as Record<string, unknown>)[\"process\"] as\n | { env?: Record<string, string | undefined> }\n | undefined;\n if (\n proc?.env?.[\"RATE_FLOOR_DISABLED\"] === \"true\" ||\n (globalThis as Record<string, unknown>)[\"RATE_FLOOR_DISABLED\"] === \"true\"\n ) {\n return;\n }\n\n // Global first: avoid holding a tight endpoint slot while waiting on the 600/min bucket.\n await _acquireOneSlot(resellerId, \"__global__\", priority);\n await _acquireOneSlot(resellerId, endpoint, priority);\n}\n\nasync function _acquireOneSlot(\n resellerId: string,\n endpoint: string,\n priority: CallPriority,\n): Promise<void> {\n const bucketKey = `${resellerId}:${endpoint}`;\n const limitPerMin = ENDPOINT_LIMITS_PER_MIN[endpoint] ?? DEFAULT_LIMIT_PER_MIN;\n const batchCap = Math.floor(limitPerMin * BATCH_FRACTION);\n\n const state = _getEndpointState(bucketKey);\n\n const ticket = state.gate.then(async () => {\n while (true) {\n const now = Date.now();\n const windowStart = now - WINDOW_MS;\n\n // Prune expired entries.\n state.log = _prune(state.log, windowStart);\n state.batchLog = _prune(state.batchLog, windowStart);\n\n const totalInWindow = state.log.length;\n const batchInWindow = state.batchLog.length;\n\n // Emit metric on a wall-clock interval; also emit immediately when util\n // first crosses ≥80% after having been below (so alerts are not delayed).\n if (totalInWindow > 0 && endpoint !== \"__global__\") {\n const utilPct = Math.round((totalInWindow / limitPerMin) * 100);\n const intervalOk =\n now - state.lastMetricEmitAt >= METRIC_EMIT_MIN_INTERVAL_MS;\n const enteredHighUtil = utilPct >= 80 && state.metricBelowHighUtil;\n if (intervalOk || enteredHighUtil) {\n emitMetric(endpoint, resellerId, totalInWindow, limitPerMin);\n state.lastMetricEmitAt = now;\n if (utilPct >= 80) {\n state.metricBelowHighUtil = false;\n }\n }\n if (utilPct < 80) {\n state.metricBelowHighUtil = true;\n }\n }\n\n // Check capacity:\n // - Global cap: can't exceed limitPerMin regardless of priority.\n // - Batch cap: batch callers additionally can't exceed batchCap.\n const globalFull = totalInWindow >= limitPerMin;\n const batchFull = priority === \"batch\" && batchInWindow >= batchCap;\n\n if (!globalFull && !batchFull) {\n // Slot available — record call.\n state.log.push(now);\n if (priority === \"batch\") {\n state.batchLog.push(now);\n }\n return;\n }\n\n // Determine how long to wait.\n let waitMs: number;\n if (globalFull) {\n // Wait until oldest entry in the full window ages out.\n const oldest = state.log[0]!;\n waitMs = WINDOW_MS - (now - oldest) + 1;\n } else {\n // Batch cap hit — wait until oldest batch entry ages out.\n const oldestBatch = state.batchLog[0]!;\n waitMs = WINDOW_MS - (now - oldestBatch) + 1;\n }\n\n await new Promise<void>((r) => setTimeout(r, waitMs));\n }\n });\n\n state.gate = ticket;\n return ticket;\n}\n\n/**\n * Returns the per-minute limit for a given OCS method.\n * Exported for testing and monitoring.\n */\nexport function getLimitForEndpoint(endpoint: string): number {\n return ENDPOINT_LIMITS_PER_MIN[endpoint] ?? DEFAULT_LIMIT_PER_MIN;\n}\n\n/**\n * Read-only counts for the current sliding window (same keying as acquireEndpointSlot).\n * Scans timestamps without mutating state — safe for status tools alongside acquires.\n */\nexport function getRateLimitWindowCounts(\n resellerKey: string,\n endpoint: string,\n): { calls_in_window: number; batch_calls_in_window: number } {\n const bucketKey = `${resellerKey}:${endpoint}`;\n const state = _endpointWindows.get(bucketKey);\n if (!state) {\n return { calls_in_window: 0, batch_calls_in_window: 0 };\n }\n const windowStart = Date.now() - WINDOW_MS;\n let calls_in_window = 0;\n for (const ts of state.log) {\n if (ts > windowStart) calls_in_window++;\n }\n let batch_calls_in_window = 0;\n for (const ts of state.batchLog) {\n if (ts > windowStart) batch_calls_in_window++;\n }\n return { calls_in_window, batch_calls_in_window };\n}\n\n/** Exposed for testing: reset all endpoint window state. */\nexport function _resetGovernorForTest(): void {\n _endpointWindows.clear();\n}\n","/**\n * @carrier/ocs-client — OCS API client (Workers-compatible).\n *\n * Shared between apps/mcp-server and apps/api.\n * All requests POST to `${baseUrl}/v1?token=<api_key>` with body `{ methodName: params }`.\n * Responses: `{ status: { code, msg }, methodName: { ...data } }`.\n *\n * Money-leak guardrails (NEVER LEAK MONEY):\n * - Per-second floor: ≤10 calls/sec/reseller (see rate-floor.ts).\n * - Per-endpoint per-minute governor: enforces Bridge4IP limits per endpoint\n * (30–600/min depending on method) with 80/20 interactive/batch split (see rate-governor.ts).\n * On 429 / OCS error code 100 (TRAFFIC_CONTROL_LIMIT_EXCEEDED): exponential backoff + log.\n * - Specific error codes 12, 17, 100, 10001-10004 carry structured messages.\n */\n\nimport { acquireToken } from \"./rate-floor.js\";\nimport { acquireEndpointSlot, type CallPriority } from \"./rate-governor.js\";\nimport { OCS_V1_METHODS, RESPONSE_SCHEMAS } from \"./schemas.js\";\n\nexport interface OcsStatus {\n code: number;\n msg: string;\n}\n\nexport interface OcsResponse<T = Record<string, unknown>> {\n status: OcsStatus;\n [method: string]: T | OcsStatus;\n}\n\nexport interface OcsApiErrorDetails {\n /** For create_location_zone validation errors (codes 10001-10004). */\n invalidTadigs?: string[];\n existingLZ?: unknown;\n [key: string]: unknown;\n}\n\nexport class OcsApiError extends Error {\n public readonly details: OcsApiErrorDetails;\n\n constructor(\n public readonly code: number,\n message: string,\n public readonly method: string,\n details: OcsApiErrorDetails = {},\n ) {\n super(`[${method}] OCS error ${code}: ${message}`);\n this.name = \"OcsApiError\";\n this.details = details;\n }\n}\n\n/**\n * Thrown when `call()` is invoked with a method that is NOT one of the\n * known-valid OCS v1 methods (see `OCS_V1_METHODS`). Catches phantom methods\n * like `createAccount` / `createSubscriber` at dev time — these are not OCS\n * methods and the server silently returns a 200 + error-2 envelope for them.\n * Only thrown in strict mode (`{ strictMethods: true }`); otherwise `call()`\n * warns once and proceeds.\n */\nexport class OcsUnknownMethodError extends Error {\n constructor(public readonly method: string) {\n super(\n `[${method}] is not a known OCS v1 method. ` +\n `It is not in the verified set of ${OCS_V1_METHODS.size} methods ` +\n `(createAccount/createSubscriber do not exist on the OCS). ` +\n `If this is a genuinely new method, add it to OCS_V1_METHODS or ` +\n `construct the client without { strictMethods: true }.`,\n );\n this.name = \"OcsUnknownMethodError\";\n }\n}\n\n/**\n * Thrown when a response payload that has a pinned schema in\n * `RESPONSE_SCHEMAS` fails shape validation — i.e. the OCS returned a 200 with\n * status.code 0 but a body shaped differently than the contract expects. This\n * surfaces provider drift loudly instead of returning a mis-shaped object that\n * blows up far away from the boundary. Skip per-client with\n * `{ validateResponses: false }` for forward-compat.\n */\n/** A single contract-validation failure: where it failed and why. */\nexport interface OcsContractIssue {\n /** Dotted path into the payload (e.g. `reseller.0.account`); `<root>` for top-level. */\n path: string;\n /** Human-readable reason from the underlying Zod issue. */\n message: string;\n}\n\nexport class OcsContractError extends Error {\n constructor(\n public readonly method: string,\n /** Structured list of what failed — inspect programmatically, don't parse the message. */\n public readonly issues: OcsContractIssue[],\n public readonly payload: unknown,\n ) {\n super(\n `[${method}] OCS response failed contract validation: ${OcsContractError.summarize(issues)}`,\n );\n this.name = \"OcsContractError\";\n }\n\n /** One-line, human-readable join of the issues — for logs and the error message. */\n get summary(): string {\n return OcsContractError.summarize(this.issues);\n }\n\n private static summarize(issues: OcsContractIssue[]): string {\n return issues.map((i) => `${i.path}: ${i.message}`).join(\"; \");\n }\n}\n\n// ---------------------------------------------------------------------------\n// Error code message routing\n// ---------------------------------------------------------------------------\n\nfunction ocsErrorMessage(\n code: number,\n rawMsg: string,\n rawBody: Record<string, unknown>,\n): { message: string; details: OcsApiErrorDetails } {\n switch (code) {\n case 12:\n return {\n message:\n \"OCS resource is read-only (code 12). The package/subscriber/account state \" +\n \"does not allow this mutation. Often returned for end-of-life subscribers or \" +\n \"finalized invoices.\",\n details: {},\n };\n case 17:\n return {\n message:\n \"Subscriber is end-of-life (code 17). It can no longer be mutated. \" +\n \"Use modify_subscriber_status to revive before further changes.\",\n details: {},\n };\n default:\n if (code >= 10001 && code <= 10004) {\n // create_location_zone validation — preserve invalidTadigs / existingLZ\n const details: OcsApiErrorDetails = {};\n if (rawBody[\"invalidTadigs\"] !== undefined) {\n details.invalidTadigs = rawBody[\"invalidTadigs\"] as string[];\n }\n if (rawBody[\"existingLZ\"] !== undefined) {\n details.existingLZ = rawBody[\"existingLZ\"];\n }\n return { message: rawMsg || \"Location zone validation failed\", details };\n }\n return { message: rawMsg || \"Unknown error\", details: {} };\n }\n}\n\n// ---------------------------------------------------------------------------\n// Retry with exponential backoff + jitter\n// ---------------------------------------------------------------------------\n\nconst BACKOFF_BASE_MS = 500;\nconst BACKOFF_MAX_MS = 4_000;\nconst BACKOFF_MAX_RETRIES = 3;\nconst BACKOFF_JITTER = 0.25; // ±25%\n\n/**\n * Exported so callers (e.g. intelligence.ts) can wrap per-iteration OCS calls.\n * Strategy: 3 retries, exponential base 500ms, ±25% jitter, capped at 4s.\n */\nexport async function retryWithBackoff<T>(fn: () => Promise<T>): Promise<T> {\n let lastError: unknown;\n for (let attempt = 0; attempt <= BACKOFF_MAX_RETRIES; attempt++) {\n if (attempt > 0) {\n const base = Math.min(BACKOFF_BASE_MS * Math.pow(2, attempt - 1), BACKOFF_MAX_MS);\n const jitter = base * BACKOFF_JITTER * (2 * Math.random() - 1);\n const delay = Math.max(0, Math.round(base + jitter));\n await new Promise<void>((r) => setTimeout(r, delay));\n }\n try {\n return await fn();\n } catch (err) {\n lastError = err;\n // Only retry on code 100 (TRAFFIC_CONTROL_LIMIT_EXCEEDED) or network errors.\n if (err instanceof OcsApiError && err.code !== 100) {\n throw err;\n }\n // Contract failures are deterministic — don't waste retries on them.\n if (err instanceof OcsContractError) {\n throw err;\n }\n }\n }\n throw lastError;\n}\n\n// ---------------------------------------------------------------------------\n// OCS Client\n// ---------------------------------------------------------------------------\n\nconst REQUEST_TIMEOUT_MS = 20_000;\n\nexport interface OcsClientOptions {\n /**\n * Enforce the known-method guard: when true, calling a method not in\n * `OCS_V1_METHODS` throws `OcsUnknownMethodError`. Default **false**\n * (permissive) — the repo currently calls a number of method names that\n * aren't in the live-verified OCS v1 set (a separate reconciliation effort),\n * so enforcing here would break existing call sites. Opt in (`true`) for\n * dev/tests or once the codebase's method vocabulary is reconciled. An\n * unrecognized method is warned once regardless, so the signal isn't lost.\n */\n strictMethods?: boolean;\n /**\n * Run response-shape validation against `RESPONSE_SCHEMAS`. Default true. A\n * mismatch WARNS once (does not throw) unless `strictValidation` is set. Set\n * false to skip validation entirely (e.g. consumers that do their own\n * shape-tolerant parsing).\n */\n validateResponses?: boolean;\n /**\n * Throw `OcsContractError` on a response-shape mismatch instead of warning.\n * Default false. Enable in dev/CI/tests to fail loudly on provider drift;\n * left off in prod so an un-sampled shape variant can't break live traffic.\n */\n strictValidation?: boolean;\n}\n\nexport class OcsClient {\n private readonly baseUrl: string;\n private readonly _strictMethods: boolean;\n private readonly _validateResponses: boolean;\n private readonly _strictValidation: boolean;\n // Per-instance (NOT static): a static set would dedup across requests in a\n // reused Worker isolate and silently swallow recurring contract-drift warnings.\n private readonly _warnedMethods = new Set<string>();\n private readonly _warnedValidation = new Set<string>();\n\n /**\n * @param baseUrl OCS base URL without trailing slash (e.g. https://ocs.esimvault.cloud).\n * @param _defaultToken Default API token; identifies the reseller for rate-floor keying.\n * @param _priority Call priority: \"interactive\" (MCP tool calls, 80% capacity) or\n * \"batch\" (cron/poll callers, 20% capacity). Default: \"interactive\".\n * @param options Contract-enforcement opt-outs (see `OcsClientOptions`).\n */\n constructor(\n baseUrl: string,\n private readonly _defaultToken?: string,\n private readonly _priority: CallPriority = \"interactive\",\n options: OcsClientOptions = {},\n ) {\n this.baseUrl = baseUrl.replace(/\\/+$/, \"\");\n this._strictMethods = options.strictMethods ?? false;\n this._validateResponses = options.validateResponses ?? true;\n this._strictValidation = options.strictValidation ?? false;\n }\n\n async call<T = Record<string, unknown>>(\n method: string,\n params: Record<string, unknown> | number | string = {},\n token?: string,\n ): Promise<T> {\n // Method guard. The live-verified OCS v1 set (OCS_V1_METHODS) does not (yet)\n // cover every name the repo calls, so by default we WARN once rather than\n // throw — surfacing phantom methods (createAccount/createSubscriber, etc.)\n // without breaking existing call sites. Strict mode (opt-in) throws.\n if (!OCS_V1_METHODS.has(method)) {\n if (this._strictMethods) throw new OcsUnknownMethodError(method);\n if (!this._warnedMethods.has(method)) {\n this._warnedMethods.add(method);\n console.warn(\n `[ocs-client] method \"${method}\" is not in the verified OCS v1 set — ` +\n `it may not exist on this OCS instance (see OCS_V1_METHODS).`,\n );\n }\n }\n\n const tok = token ?? this._defaultToken;\n if (!tok) {\n throw new OcsApiError(-1, \"No OCS API token provided\", method);\n }\n\n const url = `${this.baseUrl}/v1?token=${tok}`;\n const body = JSON.stringify({ [method]: params });\n\n return retryWithBackoff(async () => {\n // Layer 1 — Per-second floor: ≤10 calls/sec keyed by token.\n await acquireToken(tok);\n\n // Layer 2 — Per-endpoint per-minute governor (each retryable attempt consumes one slot).\n await acquireEndpointSlot(tok, method, this._priority);\n\n let res: Response;\n try {\n res = await fetch(url, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body,\n signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),\n });\n } catch (err) {\n // Network error — retryWithBackoff will retry on non-OcsApiError\n throw err;\n }\n\n if (res.status >= 500) {\n // Treat 5xx as retryable network-layer errors (not OcsApiError, so backoff retries)\n throw new Error(`HTTP ${res.status} ${res.statusText}`);\n }\n\n if (!res.ok) {\n throw new OcsApiError(\n res.status,\n `HTTP ${res.status} ${res.statusText}`,\n method,\n );\n }\n\n const json = (await res.json()) as OcsResponse<T>;\n\n if (json.status?.code !== 0) {\n const ocsCode = json.status?.code ?? -1;\n const rawMsg = json.status?.msg ?? \"Unknown error\";\n // Pass full json body for error-detail extraction (e.g. invalidTadigs)\n const rawBody = json as unknown as Record<string, unknown>;\n const { message, details } = ocsErrorMessage(ocsCode, rawMsg, rawBody);\n throw new OcsApiError(ocsCode, message, method, details);\n }\n\n // Resolve the response payload, honoring the live-OCS key remaps.\n let payload: T;\n if (method === \"getCustomerTariff\" && json[\"listTariffRule\"] !== undefined) {\n // Fix #14: getCustomerTariff response keyed as \"listTariffRule\"\n payload = json[\"listTariffRule\"] as T;\n } else if (method === \"getSubscriberLocationByCellId\") {\n const byMethod = json[method] as T | undefined;\n if (byMethod !== undefined) {\n // Present null is a valid payload; do not fall through to envelope fallback.\n payload = byMethod;\n } else if (json[\"subscriberLocation\"] !== undefined) {\n // GeoSense: live OCS returns coordinates under \"subscriberLocation\", not the method name\n payload = json[\"subscriberLocation\"] as T;\n } else {\n payload = (json[method] as T) ?? (json as unknown as T);\n }\n } else {\n payload = (json[method] as T) ?? (json as unknown as T);\n }\n\n // Contract validation: if this method has a pinned response schema, assert\n // the payload shape. A mismatch = provider drift. By default we WARN once\n // (surfacing the drift without breaking the call — the live shapes are a\n // sampled snapshot, so we don't hard-fail prod traffic on an un-sampled\n // variant). Opt into `{ strictValidation: true }` to throw OcsContractError.\n if (this._validateResponses) {\n const schema = RESPONSE_SCHEMAS[method];\n if (schema) {\n const parsed = schema.safeParse(payload);\n if (!parsed.success) {\n const issues: OcsContractIssue[] = parsed.error.issues.map((i) => ({\n path: i.path.join(\".\") || \"<root>\",\n message: i.message,\n }));\n if (this._strictValidation) {\n throw new OcsContractError(method, issues, payload);\n }\n if (!this._warnedValidation.has(method)) {\n this._warnedValidation.add(method);\n console.warn(`[ocs-client] ${new OcsContractError(method, issues, payload).message}`);\n }\n }\n }\n }\n\n return payload;\n });\n }\n}\n","/**\n * Shared OCS request builders + response parsers.\n *\n * Console, REST API, MCP, and CLI MUST call these instead of inventing\n * per-surface shapes. Live eSIMVault OCS is picky:\n * - getSubscriberLocation takes a flat SubscriberId ({ iccid }), NOT\n * { subscriber: { iccid } } (OCS error 2 \"Unrecognized field subscriber\").\n * - subscriberUsageOverPeriod takes { subscriber: { iccid }, period }\n * and a max inclusive window of 7 days. Wider windows 400/empty.\n * - Usage bytes live in quantityPerType[\"33\"]; daily rollup may sit at\n * data.subsPeriodUsages OR data.usages[0].subsPeriodUsages.\n */\n\nexport const OCS_MAX_USAGE_WINDOW_DAYS = 7;\nexport const OCS_DATA_USAGE_TYPE = \"33\";\n\nexport function clampUsagePeriod(\n start: string,\n end: string,\n maxDays = OCS_MAX_USAGE_WINDOW_DAYS,\n): { start: string; end: string } {\n const endMs = Date.parse(`${end}T00:00:00Z`);\n const startMs = Date.parse(`${start}T00:00:00Z`);\n if (Number.isNaN(endMs) || Number.isNaN(startMs) || startMs > endMs) {\n return { start, end };\n }\n const minStartMs = endMs - (maxDays - 1) * 86_400_000;\n if (startMs < minStartMs) {\n return { start: new Date(minStartMs).toISOString().slice(0, 10), end };\n }\n return { start, end };\n}\n\n/** Inclusive last-N-days window ending today (UTC). days=7 → start = today-6. */\nexport function lastNDaysPeriod(days = OCS_MAX_USAGE_WINDOW_DAYS, now = new Date()): { start: string; end: string } {\n const end = new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate()));\n const start = new Date(end);\n start.setUTCDate(end.getUTCDate() - (days - 1));\n return {\n start: start.toISOString().slice(0, 10),\n end: end.toISOString().slice(0, 10),\n };\n}\n\nexport function locationParams(iccid: string): { iccid: string } {\n return { iccid };\n}\n\n/** Flat SubscriberId — same shape as location. Active period rejects a nested subscriber wrapper. */\nexport function subscriberIdParams(iccid: string): { iccid: string } {\n return { iccid };\n}\n\nexport function networkEventsOverPeriodParams(\n iccid: string,\n start: string,\n end: string,\n): { subscriber: { iccid: string }; period: { start: string; end: string } } {\n return usageOverPeriodParams(iccid, start, end);\n}\n\nexport type OcsRadioType = \"2G\" | \"3G\" | \"4G\" | \"5G\" | \"NB-IoT\";\n\nexport function radioTypeFromRat(rat: unknown): OcsRadioType | undefined {\n const s = String(rat ?? \"\").toUpperCase();\n if (!s) return undefined;\n if (s.includes(\"NB\")) return \"NB-IoT\";\n if (s.includes(\"5G\") || s.includes(\"NR\")) return \"5G\";\n if (s.includes(\"4G\") || s.includes(\"LTE\")) return \"4G\";\n if (s.includes(\"3G\") || s.includes(\"UMTS\") || s.includes(\"HSPA\") || s.includes(\"WCDMA\")) return \"3G\";\n if (s.includes(\"2G\") || s.includes(\"GSM\") || s.includes(\"GPRS\") || s.includes(\"EDGE\")) return \"2G\";\n return undefined;\n}\n\nexport interface CellTuple {\n radioType: OcsRadioType;\n mcc: number;\n mnc: number;\n lac: number;\n cellId?: number;\n}\n\nexport function cellTupleFromNetworkInfo(networkInfo: unknown): CellTuple | undefined {\n if (!networkInfo || typeof networkInfo !== \"object\") return undefined;\n const rec = networkInfo as Record<string, unknown>;\n const radioType = radioTypeFromRat(rec.lastRat ?? rec.rat ?? rec.radioType);\n const mcc = Number(rec.lastMcc ?? rec.mcc);\n const mnc = Number(rec.lastMnc ?? rec.mnc);\n const lac = Number(rec.lastLac ?? rec.lac);\n const cellIdRaw = rec.lastCellId ?? rec.cellId;\n if (!radioType || !Number.isFinite(mcc) || !Number.isFinite(mnc) || !Number.isFinite(lac)) return undefined;\n const out: CellTuple = { radioType, mcc, mnc, lac };\n const cellId = Number(cellIdRaw);\n if (Number.isFinite(cellId) && cellId > 0) out.cellId = cellId;\n return out;\n}\n\nexport function cellLocationParams(cell: CellTuple): {\n radioType: OcsRadioType;\n mcc: number;\n mnc: number;\n lac: number;\n cellId?: number;\n} {\n const params: { radioType: OcsRadioType; mcc: number; mnc: number; lac: number; cellId?: number } = {\n radioType: cell.radioType,\n mcc: cell.mcc,\n mnc: cell.mnc,\n lac: cell.lac,\n };\n if (cell.cellId != null) params.cellId = cell.cellId;\n return params;\n}\n\nexport function usageOverPeriodParams(\n iccid: string,\n start: string,\n end: string,\n): { subscriber: { iccid: string }; period: { start: string; end: string } } {\n const period = clampUsagePeriod(start, end);\n return { subscriber: { iccid }, period };\n}\n\nexport interface UsageDayPoint {\n date: string;\n mb: number;\n}\n\nexport interface UsageCountrySlice {\n country: string;\n mb: number;\n operators: { name: string; mb: number }[];\n}\n\nexport function mbFromBytes(bytes: number | undefined): number {\n if (!bytes) return 0;\n return Math.round(bytes / 1_048_576);\n}\n\nexport function dataBytesFromQuantity(qty: unknown): number {\n if (!qty || typeof qty !== \"object\") return 0;\n const rec = qty as Record<string, unknown>;\n const raw = rec[OCS_DATA_USAGE_TYPE] ?? rec[33];\n return typeof raw === \"number\" ? raw : 0;\n}\n\nfunction periodRows(data: Record<string, unknown> | undefined): unknown[] {\n if (!data) return [];\n if (Array.isArray(data.subsPeriodUsages) && data.subsPeriodUsages.length) return data.subsPeriodUsages;\n const usages = Array.isArray(data.usages) ? data.usages : [];\n return usages.flatMap((u) => {\n const rec = u as { subsPeriodUsages?: unknown[] };\n return Array.isArray(rec.subsPeriodUsages) ? rec.subsPeriodUsages : [];\n });\n}\n\n/** Accepts the REST `{ data }` envelope or the raw OCS usage object. */\nexport function extractUsageTimeline(json: unknown): UsageDayPoint[] {\n const root = (json ?? {}) as Record<string, unknown>;\n const data = (root.data ?? root.usage ?? root.subscriberUsageOverPeriod ?? root) as Record<string, unknown>;\n if (Array.isArray(data)) {\n return data\n .map((entry: { date?: string; day?: string; mb?: number; bytes?: number }) => ({\n date: entry.date ?? entry.day ?? \"\",\n mb: entry.mb ?? mbFromBytes(entry.bytes),\n }))\n .filter((p) => p.date);\n }\n if (!data || typeof data !== \"object\") return [];\n\n const periods = periodRows(data);\n if (periods.length > 0) {\n return periods\n .map((p) => {\n const rec = p as { day?: string; date?: string; total?: { quantityPerType?: unknown } };\n return {\n date: rec.day ?? rec.date ?? \"\",\n mb: mbFromBytes(dataBytesFromQuantity(rec.total?.quantityPerType)),\n };\n })\n .filter((p) => p.date);\n }\n\n const sessions = Array.isArray(data.usages)\n ? (data.usages as { subsDailyUsages?: unknown[]; usageDateUtc?: string; quantity?: number }[]).flatMap((u) =>\n Array.isArray(u.subsDailyUsages) && u.subsDailyUsages.length ? u.subsDailyUsages : [u],\n )\n : [];\n if (sessions.length === 0) return [];\n const byDay = new Map<string, number>();\n for (const s of sessions) {\n const rec = s as { day?: string; usageDateUtc?: string; quantity?: number; total?: { quantityPerType?: unknown } };\n const day = rec.day ?? (typeof rec.usageDateUtc === \"string\" ? rec.usageDateUtc.slice(0, 10) : \"\");\n if (!day) continue;\n const bytes =\n typeof rec.quantity === \"number\" ? rec.quantity : dataBytesFromQuantity(rec.total?.quantityPerType);\n byDay.set(day, (byDay.get(day) ?? 0) + bytes);\n }\n return [...byDay.entries()]\n .sort(([a], [b]) => a.localeCompare(b))\n .map(([date, bytes]) => ({ date, mb: mbFromBytes(bytes) }));\n}\n\nexport function extractUsageBreakdown(json: unknown): { totalMb: number; countries: UsageCountrySlice[] } {\n const root = (json ?? {}) as Record<string, unknown>;\n const data = (root.data ?? root.usage ?? root.subscriberUsageOverPeriod ?? root) as Record<string, unknown> | undefined;\n const totalObj =\n data && typeof data === \"object\"\n ? ((data.total ?? (Array.isArray(data.usages) ? (data.usages[0] as { total?: unknown })?.total : undefined)) as\n | { quantityPerType?: unknown; quantityPerCountry?: unknown[] }\n | undefined)\n : undefined;\n const totalMb = mbFromBytes(dataBytesFromQuantity(totalObj?.quantityPerType));\n const rows = (Array.isArray(totalObj?.quantityPerCountry) ? totalObj.quantityPerCountry : []) as {\n name?: string;\n qty?: number;\n quantityPerOperator?: { name?: string; qty?: number }[];\n }[];\n const countries: UsageCountrySlice[] = rows.map((c) => ({\n country: c.name ?? \"Unknown\",\n mb: mbFromBytes(typeof c.qty === \"number\" ? c.qty : 0),\n operators: (c.quantityPerOperator ?? []).map((o) => ({\n name: o.name ?? \"Unknown\",\n mb: mbFromBytes(typeof o.qty === \"number\" ? o.qty : 0),\n })),\n }));\n return { totalMb, countries };\n}\n\nexport function parseSubscriberLocation(json: unknown): {\n latitude: number;\n longitude: number;\n accuracy?: number;\n at?: string;\n} | undefined {\n const root = (json ?? {}) as Record<string, unknown>;\n const loc = (root.subscriberLocation ??\n (root.data as Record<string, unknown> | undefined)?.subscriberLocation ??\n root.data ??\n root) as Record<string, unknown> | undefined;\n if (!loc || typeof loc !== \"object\") return undefined;\n const lat = loc.latitude;\n const lng = loc.longitude;\n if (typeof lat !== \"number\" || typeof lng !== \"number\") return undefined;\n const out: { latitude: number; longitude: number; accuracy?: number; at?: string } = {\n latitude: lat,\n longitude: lng,\n };\n if (typeof loc.accuracy === \"number\") out.accuracy = loc.accuracy;\n const at = typeof loc.dateTime === \"string\" ? loc.dateTime : typeof loc.at === \"string\" ? loc.at : undefined;\n if (at) out.at = at;\n return out;\n}\n\nexport interface NetworkEvent {\n timestamp: string;\n type: string;\n description: string;\n country?: string;\n operator?: string;\n mcc?: number;\n mnc?: number;\n protocol?: string;\n}\n\nfunction flattenNetworkEventGroups(payload: unknown): unknown[] {\n if (Array.isArray(payload)) return payload;\n if (!payload || typeof payload !== \"object\") return [];\n const rec = payload as Record<string, unknown>;\n if (Array.isArray(rec.events)) return rec.events;\n const groups = [\"mapAnswer\", \"s6aAnswer\", \"gyAnswer\", \"camelAnswer\", \"mocAnswer\", \"mtcAnswer\"] as const;\n const out: unknown[] = [];\n for (const key of groups) {\n const group = rec[key];\n if (group && typeof group === \"object\" && Array.isArray((group as { events?: unknown[] }).events)) {\n out.push(...((group as { events: unknown[] }).events));\n }\n }\n return out;\n}\n\nexport function extractNetworkEvents(json: unknown): NetworkEvent[] {\n const root = (json ?? {}) as Record<string, unknown>;\n const payload = root.subscriberNetworkEventsOverPeriod ?? root.data ?? root.events ?? root;\n const rows = flattenNetworkEventGroups(payload);\n return rows\n .map((row) => {\n const rec = (row ?? {}) as Record<string, unknown>;\n const timestamp = String(rec.eventTime ?? rec.timestamp ?? rec.time ?? rec.dateTime ?? \"\");\n const longCode = typeof rec.longCode === \"string\" ? rec.longCode : undefined;\n const shortCode = typeof rec.shortCode === \"string\" ? rec.shortCode : undefined;\n const type = String(rec.eventType ?? rec.type ?? longCode ?? shortCode ?? \"network\");\n const country = typeof rec.countryName === \"string\" ? rec.countryName : typeof rec.country === \"string\" ? rec.country : undefined;\n const operator = typeof rec.operator === \"string\" ? rec.operator : undefined;\n const mcc = typeof rec.mcc === \"number\" ? rec.mcc : undefined;\n const mnc = typeof rec.mnc === \"number\" ? rec.mnc : undefined;\n const protocol = typeof rec.protocol === \"string\" ? rec.protocol : undefined;\n const result = typeof rec.result === \"string\" ? rec.result : undefined;\n const parts = [type, operator, country, result].filter(Boolean);\n const event: NetworkEvent = {\n timestamp,\n type,\n description: parts.join(\" · \") || \"Network event\",\n };\n if (country) event.country = country;\n if (operator) event.operator = operator;\n if (mcc != null) event.mcc = mcc;\n if (mnc != null) event.mnc = mnc;\n if (protocol) event.protocol = protocol;\n return event;\n })\n .filter((e) => e.timestamp)\n .sort((a, b) => b.timestamp.localeCompare(a.timestamp));\n}\n\nexport function parseActivePeriod(json: unknown): { start?: string; end?: string } | undefined {\n const root = (json ?? {}) as Record<string, unknown>;\n const payload = (root.getSubscriberActivePeriod ?? root.data ?? root) as Record<string, unknown>;\n const period = (payload.period ?? payload) as Record<string, unknown>;\n const start = typeof period.start === \"string\" ? period.start : undefined;\n const end = typeof period.end === \"string\" ? period.end : undefined;\n if (!start && !end) return undefined;\n const out: { start?: string; end?: string } = {};\n if (start) out.start = start;\n if (end) out.end = end;\n return out;\n}\n\nexport interface EsimStatusCounts {\n active: number;\n suspended: number;\n inventory: number;\n other: number;\n total: number;\n}\n\nfunction emptyEsimStatusCounts(): EsimStatusCounts {\n return { active: 0, suspended: 0, inventory: 0, other: 0, total: 0 };\n}\n\nfunction addEsimStatusRow(acc: EsimStatusCounts, rec: Record<string, unknown>): void {\n const count = Number(rec.count ?? 0);\n if (!Number.isFinite(count) || count === 0) return;\n // Live OCS (account 5259, 2026-08-18): statusNum 0=Free, 2=Affected.\n // Do not trust statusNum 2 as Suspended. Prefer statusStr when present.\n const str = String(rec.statusStr ?? rec.name ?? rec.status ?? \"\").toUpperCase();\n if (str === \"ACTIVATED\" || str === \"ACTIVE\" || str === \"AFFECTED\") {\n acc.active += count;\n return;\n }\n if (str === \"SUSPENDED\") {\n acc.suspended += count;\n return;\n }\n if (str === \"FREE\" || str === \"INVENTORY\" || str === \"NOT_ACTIVATED\" || str === \"AVAILABLE\") {\n acc.inventory += count;\n return;\n }\n const num = rec.statusNum;\n if (typeof num === \"number\") {\n if (num === 1) acc.active += count;\n else if (num === 0) acc.inventory += count;\n else if (num === 2) acc.suspended += count;\n else acc.other += count;\n return;\n }\n acc.other += count;\n}\n\n/**\n * Unwrap esimStatusPerAccount.\n * Live shape: { account: [{ sponsor: [{ esim: { status: [{ statusNum, statusStr, count }] } }] }] }\n * statusNum: 0 Free / 1 Activated / 2 Suspended / 3+ other.\n */\nexport function extractEsimStatusCounts(json: unknown): EsimStatusCounts {\n const acc = emptyEsimStatusCounts();\n const root = (json ?? {}) as Record<string, unknown>;\n const payload = (root.data ?? root.esimStatusPerAccount ?? root) as Record<string, unknown> | unknown[];\n const accounts: unknown[] = Array.isArray(payload)\n ? payload\n : Array.isArray((payload as Record<string, unknown>).account)\n ? ((payload as Record<string, unknown>).account as unknown[])\n : [];\n\n for (const account of accounts) {\n const rec = (account ?? {}) as Record<string, unknown>;\n const sponsors = Array.isArray(rec.sponsor) ? rec.sponsor : [];\n if (sponsors.length > 0) {\n for (const sp of sponsors) {\n const esim = ((sp as Record<string, unknown>)?.esim ?? {}) as Record<string, unknown>;\n const statuses = Array.isArray(esim.status) ? esim.status : [];\n for (const row of statuses) addEsimStatusRow(acc, (row ?? {}) as Record<string, unknown>);\n }\n continue;\n }\n acc.active += Number(rec.active ?? 0);\n acc.suspended += Number(rec.suspended ?? 0);\n acc.inventory += Number(rec.inventory ?? rec.notActivated ?? rec.free ?? 0);\n acc.other += Number(rec.other ?? rec.terminated ?? 0);\n }\n\n acc.total = acc.active + acc.suspended + acc.inventory + acc.other;\n return acc;\n}\n\nexport function esimStatusPerAccountParams(accountId: number): { accountId: number } {\n return { accountId };\n}\n\n/** Surfaces that must stay aligned on these builders. Used by the contract test. */\nexport const OCS_PARITY_SURFACES = [\"api\", \"console\", \"mcp-stdio\", \"mcp-server\", \"cli\"] as const;\n","/**\n * Storefront logo generator.\n *\n * Works on any box that has the Carrier CLI or MCP — no image-API key required.\n * Produces a distinctive SVG mark from brand name + accent. If an OpenAI key is\n * available (worker secret or local env), optionally returns a raster PNG too.\n */\n\nexport type LogoMark = {\n svg: string;\n filename: string;\n source: \"svg\" | \"openai\";\n pngBase64?: string;\n};\n\nconst OPENAI_KEY_NAMES = [\n \"OPENAI_API_KEY\",\n \"OPENAI_KEY\",\n \"CARRIER_OPENAI_API_KEY\",\n] as const;\n\nexport function firstImageKey(env: Record<string, string | undefined> = process.env): string | undefined {\n for (const name of OPENAI_KEY_NAMES) {\n const v = env[name];\n if (v && v.trim() && !v.startsWith(\"__\") && !v.includes(\"PLACEHOLD\")) return v.trim();\n }\n return undefined;\n}\n\nfunction clampHex(hex: string, fallback: string): string {\n const h = hex.trim();\n return /^#[0-9a-fA-F]{6}$/.test(h) ? h.toUpperCase() : fallback;\n}\n\nfunction hashName(name: string): number {\n let h = 2166136261;\n for (let i = 0; i < name.length; i++) {\n h ^= name.charCodeAt(i);\n h = Math.imul(h, 16777619);\n }\n return h >>> 0;\n}\n\nfunction mix(hex: string, toward: string, t: number): string {\n const parse = (x: string) => [\n parseInt(x.slice(1, 3), 16),\n parseInt(x.slice(3, 5), 16),\n parseInt(x.slice(5, 7), 16),\n ] as const;\n const a = parse(hex);\n const b = parse(toward);\n const ch = (i: number) =>\n Math.round(a[i] + (b[i] - a[i]) * t)\n .toString(16)\n .padStart(2, \"0\");\n return `#${ch(0)}${ch(1)}${ch(2)}`;\n}\n\n/** Distinctive 512×512 SVG mark — letter + geometric seal from the brand name. */\nexport function generateStorefrontLogoSvg(\n name: string,\n accent = \"#FF6B35\",\n): string {\n const brand = (name.trim() || \"C\").slice(0, 24);\n const letter = brand[0]!.toUpperCase();\n const accentHex = clampHex(accent, \"#FF6B35\");\n const deep = mix(accentHex, \"#1A1812\", 0.45);\n const light = mix(accentHex, \"#FFF8EB\", 0.35);\n const h = hashName(brand);\n const style = h % 4;\n const uid = `lg${(h % 1_000_000).toString(16)}`;\n\n let seal = \"\";\n if (style === 0) {\n seal = `<rect x=\"56\" y=\"56\" width=\"400\" height=\"400\" rx=\"96\" fill=\"url(#${uid}g)\"/>`;\n } else if (style === 1) {\n seal = `<circle cx=\"256\" cy=\"256\" r=\"196\" fill=\"url(#${uid}g)\"/>`;\n } else if (style === 2) {\n seal = `<path d=\"M256 48 L432 152 V360 L256 464 L80 360 V152 Z\" fill=\"url(#${uid}g)\"/>`;\n } else {\n seal = `<path d=\"M256 64 C320 64 400 96 424 176 C448 256 424 352 344 416 C280 464 232 464 168 416 C88 352 64 256 88 176 C112 96 192 64 256 64 Z\" fill=\"url(#${uid}g)\"/>`;\n }\n\n return `<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 512 512\" width=\"512\" height=\"512\" role=\"img\" aria-label=\"${escapeXml(brand)} logo\">\n <defs>\n <linearGradient id=\"${uid}g\" x1=\"0\" y1=\"0\" x2=\"1\" y2=\"1\">\n <stop offset=\"0%\" stop-color=\"${light}\"/>\n <stop offset=\"55%\" stop-color=\"${accentHex}\"/>\n <stop offset=\"100%\" stop-color=\"${deep}\"/>\n </linearGradient>\n </defs>\n <rect width=\"512\" height=\"512\" rx=\"96\" fill=\"#FAFAF7\"/>\n ${seal}\n <text x=\"256\" y=\"300\" text-anchor=\"middle\" font-family=\"Georgia, 'Times New Roman', serif\" font-size=\"220\" font-weight=\"700\" fill=\"#FFF8EB\">${escapeXml(letter)}</text>\n</svg>\n`;\n}\n\nfunction escapeXml(s: string): string {\n return s.replace(/&/g, \"&\").replace(/</g, \"<\").replace(/>/g, \">\").replace(/\"/g, \""\");\n}\n\nexport function logoPrompt(name: string, accent: string, tagline?: string): string {\n return [\n `App icon / brand mark for \"${name}\", a travel eSIM storefront.`,\n tagline ? `Tagline: ${tagline}.` : \"\",\n `Flat vector, single centered symbol, no letters unless they are a refined monogram of \"${name[0] ?? \"C\"}\".`,\n `Color: ${accent} on warm cream #FAFAF7. Soft squircle icon, premium, simple, high contrast.`,\n `No photorealism, no mockups, no shadows of devices, no watermarks.`,\n ]\n .filter(Boolean)\n .join(\" \");\n}\n\n/** Best-effort OpenAI image. Returns undefined on any failure. */\nexport async function tryOpenAiLogoPng(\n name: string,\n accent: string,\n apiKey: string,\n tagline?: string,\n): Promise<string | undefined> {\n try {\n const res = await fetch(\"https://api.openai.com/v1/images/generations\", {\n method: \"POST\",\n headers: {\n Authorization: `Bearer ${apiKey}`,\n \"Content-Type\": \"application/json\",\n },\n body: JSON.stringify({\n model: \"dall-e-3\",\n prompt: logoPrompt(name, accent, tagline),\n size: \"1024x1024\",\n quality: \"hd\",\n response_format: \"b64_json\",\n n: 1,\n }),\n });\n if (!res.ok) return undefined;\n const json = (await res.json()) as { data?: Array<{ b64_json?: string }> };\n return json.data?.[0]?.b64_json;\n } catch {\n return undefined;\n }\n}\n\nexport async function generateStorefrontLogo(\n input: { name: string; accent?: string; tagline?: string; env?: Record<string, string | undefined> },\n): Promise<LogoMark> {\n const name = input.name.trim() || \"Carrier\";\n const accent = clampHex(input.accent ?? \"#FF6B35\", \"#FF6B35\");\n const svg = generateStorefrontLogoSvg(name, accent);\n const key = firstImageKey(input.env ?? (typeof process !== \"undefined\" ? process.env : {}));\n if (key) {\n const png = await tryOpenAiLogoPng(name, accent, key, input.tagline);\n if (png) {\n return { svg, filename: \"logo.svg\", source: \"openai\", pngBase64: png };\n }\n }\n return { svg, filename: \"logo.svg\", source: \"svg\" };\n}\n"],"mappings":";AAAA;AAAA,EACE,MAAQ;AAAA,EACR,SAAW;AAAA,EACX,aAAe;AAAA,EACf,SAAW;AAAA,EACX,QAAU;AAAA,EACV,UAAY;AAAA,EACZ,YAAc;AAAA,IACZ,MAAQ;AAAA,IACR,KAAO;AAAA,EACT;AAAA,EACA,MAAQ;AAAA,EACR,MAAQ;AAAA,EACR,OAAS;AAAA,IACP;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EACF;AAAA,EACA,SAAW;AAAA,IACT,OAAS;AAAA,IACT,cAAc;AAAA,IACd,MAAQ;AAAA,IACR,gBAAkB;AAAA,IAClB,MAAQ;AAAA,EACV;AAAA,EACA,cAAgB;AAAA,IACd,kBAAkB;AAAA,IAClB,kBAAkB;AAAA,IAClB,kCAAkC;AAAA,IAClC,6BAA6B;AAAA,IAC7B,iBAAiB;AAAA,IACjB,sBAAsB;AAAA,IACtB,WAAa;AAAA,IACb,WAAa;AAAA,IACb,YAAc;AAAA,IACd,KAAO;AAAA,EACT;AAAA,EACA,iBAAmB;AAAA,IACjB,6BAA6B;AAAA,IAC7B,eAAe;AAAA,IACf,oCAAoC;AAAA,IACpC,6BAA6B;AAAA,IAC7B,QAAU;AAAA,IACV,MAAQ;AAAA,IACR,YAAc;AAAA,IACd,uBAAuB;AAAA,IACvB,qBAAqB;AAAA,EACvB;AAAA,EACA,UAAY;AAAA,IACV;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EACF;AAAA,EACA,SAAW;AAAA,IACT,MAAQ;AAAA,EACV;AAAA,EACA,eAAiB;AAAA,IACf,QAAU;AAAA,EACZ;AAAA,EACA,KAAO;AAAA,IACL,eAAe;AAAA,IACf,SAAW;AAAA,EACb;AACF;;;ACzDO,IAAM,kBAA0B,gBAAI;;;AEsC3C,IAAM,0BAAkD;;EAEtD,YAAY;;EAGZ,mCAAmC;;EAGnC,2BAA2B;;EAG3B,yBAAyB;EACzB,WAAW;;EAGX,4BAA4B;EAC5B,oCAAoC;EACpC,iCAAiC;EACjC,iCAAiC;EACjC,sBAAsB;EACtB,2BAA2B;EAC3B,eAAe;EACf,eAAe;EACf,gBAAgB;EAChB,+BAA+B;EAC/B,8BAA8B;EAC9B,oBAAoB;EACpB,kCAAkC;EAClC,qBAAqB;EACrB,+BAA+B;AACjC;AAEA,IAAM,wBAAwB;AAC9B,IAAMA,aAAY;AAGlB,IAAM,8BAA8B;AAGpC,IAAM,iBAAiB;AAGvB,SAAS,gBAAgB,aAAyC;AAChE,MAAI,CAAC,YAAa,QAAO;AACzB,MAAI,IAAI;AACR,WAAS,IAAI,GAAG,IAAI,YAAY,QAAQ,KAAK;AAC3C,UAAO,KAAK,KAAK,IAAK,YAAY,WAAW,CAAC,OAAO;EACvD;AACA,SAAO,EAAE,SAAS,EAAE,EAAE,SAAS,GAAG,GAAG;AACvC;AAUA,SAAS,WACP,UACA,aACA,eACA,aACM;AACN,QAAM,MAAM,KAAK,MAAO,gBAAgB,cAAe,GAAG;AAC1D,QAAM,gBAAgB,gBAAgB,WAAW;AAEjD,UAAQ;IACN,KAAK,UAAU;MACb,QAAQ;MACR;MACA;MACA,iBAAiB;MACjB,eAAe;MACf,iBAAiB;MACjB,OAAO,OAAO;MACd,IAAI,KAAK,IAAI;IACf,CAAC;EACH;AACA,MAAI,OAAO,IAAI;AACb,YAAQ;MACN,0BAA0B,QAAQ,OAAO,GAAG,eAAe,aAAa,IAAI,WAAW,sBAAsB,aAAa;IAC5H;EACF;AACF;AAuBA,IAAM,mBAAmB,oBAAI,IAAiC;AAE9D,SAAS,kBAAkB,KAAkC;AAC3D,MAAI,IAAI,iBAAiB,IAAI,GAAG;AAChC,MAAI,CAAC,GAAG;AACN,QAAI;MACF,KAAK,CAAC;MACN,UAAU,CAAC;MACX,MAAM,QAAQ,QAAQ;MACtB,kBAAkB;MAClB,qBAAqB;IACvB;AACA,qBAAiB,IAAI,KAAK,CAAC;EAC7B;AACA,SAAO;AACT;AAEA,SAAS,OAAO,KAAe,aAA+B;AAE5D,MAAI,IAAI;AACR,SAAO,IAAI,IAAI,UAAU,IAAI,CAAC,KAAM,YAAa;AACjD,SAAO,IAAI,IAAI,IAAI,MAAM,CAAC,IAAI;AAChC;AAmBA,eAAsB,oBACpB,YACA,UACA,WAAyB,eACV;AAEf,QAAM,OAAQ,WAAuC,SAAS;AAG9D,MACE,MAAM,MAAM,qBAAqB,MAAM,UACtC,WAAuC,qBAAqB,MAAM,QACnE;AACA;EACF;AAGA,QAAM,gBAAgB,YAAY,cAAc,QAAQ;AACxD,QAAM,gBAAgB,YAAY,UAAU,QAAQ;AACtD;AAEA,eAAe,gBACb,YACA,UACA,UACe;AACf,QAAM,YAAY,GAAG,UAAU,IAAI,QAAQ;AAC3C,QAAM,cAAc,wBAAwB,QAAQ,KAAK;AACzD,QAAM,WAAW,KAAK,MAAM,cAAc,cAAc;AAExD,QAAM,QAAQ,kBAAkB,SAAS;AAEzC,QAAM,SAAS,MAAM,KAAK,KAAK,YAAY;AACzC,WAAO,MAAM;AACX,YAAM,MAAM,KAAK,IAAI;AACrB,YAAM,cAAc,MAAMA;AAG1B,YAAM,MAAM,OAAO,MAAM,KAAK,WAAW;AACzC,YAAM,WAAW,OAAO,MAAM,UAAU,WAAW;AAEnD,YAAM,gBAAgB,MAAM,IAAI;AAChC,YAAM,gBAAgB,MAAM,SAAS;AAIrC,UAAI,gBAAgB,KAAK,aAAa,cAAc;AAClD,cAAM,UAAU,KAAK,MAAO,gBAAgB,cAAe,GAAG;AAC9D,cAAM,aACJ,MAAM,MAAM,oBAAoB;AAClC,cAAM,kBAAkB,WAAW,MAAM,MAAM;AAC/C,YAAI,cAAc,iBAAiB;AACjC,qBAAW,UAAU,YAAY,eAAe,WAAW;AAC3D,gBAAM,mBAAmB;AACzB,cAAI,WAAW,IAAI;AACjB,kBAAM,sBAAsB;UAC9B;QACF;AACA,YAAI,UAAU,IAAI;AAChB,gBAAM,sBAAsB;QAC9B;MACF;AAKA,YAAM,aAAa,iBAAiB;AACpC,YAAM,YAAY,aAAa,WAAW,iBAAiB;AAE3D,UAAI,CAAC,cAAc,CAAC,WAAW;AAE7B,cAAM,IAAI,KAAK,GAAG;AAClB,YAAI,aAAa,SAAS;AACxB,gBAAM,SAAS,KAAK,GAAG;QACzB;AACA;MACF;AAGA,UAAI;AACJ,UAAI,YAAY;AAEd,cAAM,SAAS,MAAM,IAAI,CAAC;AAC1B,iBAASA,cAAa,MAAM,UAAU;MACxC,OAAO;AAEL,cAAM,cAAc,MAAM,SAAS,CAAC;AACpC,iBAASA,cAAa,MAAM,eAAe;MAC7C;AAEA,YAAM,IAAI,QAAc,CAAC,MAAM,WAAW,GAAG,MAAM,CAAC;IACtD;EACF,CAAC;AAED,QAAM,OAAO;AACb,SAAO;AACT;AAMO,SAAS,oBAAoB,UAA0B;AAC5D,SAAO,wBAAwB,QAAQ,KAAK;AAC9C;AAMO,SAAS,yBACd,aACA,UAC4D;AAC5D,QAAM,YAAY,GAAG,WAAW,IAAI,QAAQ;AAC5C,QAAM,QAAQ,iBAAiB,IAAI,SAAS;AAC5C,MAAI,CAAC,OAAO;AACV,WAAO,EAAE,iBAAiB,GAAG,uBAAuB,EAAE;EACxD;AACA,QAAM,cAAc,KAAK,IAAI,IAAIA;AACjC,MAAI,kBAAkB;AACtB,aAAW,MAAM,MAAM,KAAK;AAC1B,QAAI,KAAK,YAAa;EACxB;AACA,MAAI,wBAAwB;AAC5B,aAAW,MAAM,MAAM,UAAU;AAC/B,QAAI,KAAK,YAAa;EACxB;AACA,SAAO,EAAE,iBAAiB,sBAAsB;AAClD;;;AE1TO,IAAM,4BAA4B;AAGlC,SAAS,iBACd,OACA,KACA,UAAU,2BACsB;AAChC,QAAM,QAAQ,KAAK,MAAM,GAAG,GAAG,YAAY;AAC3C,QAAM,UAAU,KAAK,MAAM,GAAG,KAAK,YAAY;AAC/C,MAAI,OAAO,MAAM,KAAK,KAAK,OAAO,MAAM,OAAO,KAAK,UAAU,OAAO;AACnE,WAAO,EAAE,OAAO,IAAI;EACtB;AACA,QAAM,aAAa,SAAS,UAAU,KAAK;AAC3C,MAAI,UAAU,YAAY;AACxB,WAAO,EAAE,OAAO,IAAI,KAAK,UAAU,EAAE,YAAY,EAAE,MAAM,GAAG,EAAE,GAAG,IAAI;EACvE;AACA,SAAO,EAAE,OAAO,IAAI;AACtB;AAGO,SAAS,gBAAgB,OAAO,2BAA2B,MAAM,oBAAI,KAAK,GAAmC;AAClH,QAAM,MAAM,IAAI,KAAK,KAAK,IAAI,IAAI,eAAe,GAAG,IAAI,YAAY,GAAG,IAAI,WAAW,CAAC,CAAC;AACxF,QAAM,QAAQ,IAAI,KAAK,GAAG;AAC1B,QAAM,WAAW,IAAI,WAAW,KAAK,OAAO,EAAE;AAC9C,SAAO;IACL,OAAO,MAAM,YAAY,EAAE,MAAM,GAAG,EAAE;IACtC,KAAK,IAAI,YAAY,EAAE,MAAM,GAAG,EAAE;EACpC;AACF;AAEO,SAAS,eAAe,OAAkC;AAC/D,SAAO,EAAE,MAAM;AACjB;AAGO,SAAS,mBAAmB,OAAkC;AACnE,SAAO,EAAE,MAAM;AACjB;AAEO,SAAS,8BACd,OACA,OACA,KAC2E;AAC3E,SAAO,sBAAsB,OAAO,OAAO,GAAG;AAChD;AAuDO,SAAS,sBACd,OACA,OACA,KAC2E;AAC3E,QAAM,SAAS,iBAAiB,OAAO,GAAG;AAC1C,SAAO,EAAE,YAAY,EAAE,MAAM,GAAG,OAAO;AACzC;AAuNA,SAAS,wBAA0C;AACjD,SAAO,EAAE,QAAQ,GAAG,WAAW,GAAG,WAAW,GAAG,OAAO,GAAG,OAAO,EAAE;AACrE;AAEA,SAAS,iBAAiB,KAAuB,KAAoC;AACnF,QAAM,QAAQ,OAAO,IAAI,SAAS,CAAC;AACnC,MAAI,CAAC,OAAO,SAAS,KAAK,KAAK,UAAU,EAAG;AAG5C,QAAM,MAAM,OAAO,IAAI,aAAa,IAAI,QAAQ,IAAI,UAAU,EAAE,EAAE,YAAY;AAC9E,MAAI,QAAQ,eAAe,QAAQ,YAAY,QAAQ,YAAY;AACjE,QAAI,UAAU;AACd;EACF;AACA,MAAI,QAAQ,aAAa;AACvB,QAAI,aAAa;AACjB;EACF;AACA,MAAI,QAAQ,UAAU,QAAQ,eAAe,QAAQ,mBAAmB,QAAQ,aAAa;AAC3F,QAAI,aAAa;AACjB;EACF;AACA,QAAM,MAAM,IAAI;AAChB,MAAI,OAAO,QAAQ,UAAU;AAC3B,QAAI,QAAQ,EAAG,KAAI,UAAU;aACpB,QAAQ,EAAG,KAAI,aAAa;aAC5B,QAAQ,EAAG,KAAI,aAAa;QAChC,KAAI,SAAS;AAClB;EACF;AACA,MAAI,SAAS;AACf;AAOO,SAAS,wBAAwB,MAAiC;AACvE,QAAM,MAAM,sBAAsB;AAClC,QAAM,OAAQ,QAAQ,CAAC;AACvB,QAAM,UAAW,KAAK,QAAQ,KAAK,wBAAwB;AAC3D,QAAM,WAAsB,MAAM,QAAQ,OAAO,IAC7C,UACA,MAAM,QAAS,QAAoC,OAAO,IACtD,QAAoC,UACtC,CAAC;AAEP,aAAW,WAAW,UAAU;AAC9B,UAAM,MAAO,WAAW,CAAC;AACzB,UAAM,WAAW,MAAM,QAAQ,IAAI,OAAO,IAAI,IAAI,UAAU,CAAC;AAC7D,QAAI,SAAS,SAAS,GAAG;AACvB,iBAAW,MAAM,UAAU;AACzB,cAAM,OAAS,IAAgC,QAAQ,CAAC;AACxD,cAAM,WAAW,MAAM,QAAQ,KAAK,MAAM,IAAI,KAAK,SAAS,CAAC;AAC7D,mBAAW,OAAO,SAAU,kBAAiB,KAAM,OAAO,CAAC,CAA6B;MAC1F;AACA;IACF;AACA,QAAI,UAAU,OAAO,IAAI,UAAU,CAAC;AACpC,QAAI,aAAa,OAAO,IAAI,aAAa,CAAC;AAC1C,QAAI,aAAa,OAAO,IAAI,aAAa,IAAI,gBAAgB,IAAI,QAAQ,CAAC;AAC1E,QAAI,SAAS,OAAO,IAAI,SAAS,IAAI,cAAc,CAAC;EACtD;AAEA,MAAI,QAAQ,IAAI,SAAS,IAAI,YAAY,IAAI,YAAY,IAAI;AAC7D,SAAO;AACT;;;ACpYA,IAAM,mBAAmB;AAAA,EACvB;AAAA,EACA;AAAA,EACA;AACF;AAEO,SAAS,cAAc,MAA0C,QAAQ,KAAyB;AACvG,aAAW,QAAQ,kBAAkB;AACnC,UAAM,IAAI,IAAI,IAAI;AAClB,QAAI,KAAK,EAAE,KAAK,KAAK,CAAC,EAAE,WAAW,IAAI,KAAK,CAAC,EAAE,SAAS,WAAW,EAAG,QAAO,EAAE,KAAK;AAAA,EACtF;AACA,SAAO;AACT;AAEA,SAAS,SAAS,KAAa,UAA0B;AACvD,QAAM,IAAI,IAAI,KAAK;AACnB,SAAO,oBAAoB,KAAK,CAAC,IAAI,EAAE,YAAY,IAAI;AACzD;AAEA,SAAS,SAAS,MAAsB;AACtC,MAAI,IAAI;AACR,WAAS,IAAI,GAAG,IAAI,KAAK,QAAQ,KAAK;AACpC,SAAK,KAAK,WAAW,CAAC;AACtB,QAAI,KAAK,KAAK,GAAG,QAAQ;AAAA,EAC3B;AACA,SAAO,MAAM;AACf;AAEA,SAAS,IAAI,KAAa,QAAgB,GAAmB;AAC3D,QAAM,QAAQ,CAAC,MAAc;AAAA,IAC3B,SAAS,EAAE,MAAM,GAAG,CAAC,GAAG,EAAE;AAAA,IAC1B,SAAS,EAAE,MAAM,GAAG,CAAC,GAAG,EAAE;AAAA,IAC1B,SAAS,EAAE,MAAM,GAAG,CAAC,GAAG,EAAE;AAAA,EAC5B;AACA,QAAM,IAAI,MAAM,GAAG;AACnB,QAAM,IAAI,MAAM,MAAM;AACtB,QAAM,KAAK,CAAC,MACV,KAAK,MAAM,EAAE,CAAC,KAAK,EAAE,CAAC,IAAI,EAAE,CAAC,KAAK,CAAC,EAChC,SAAS,EAAE,EACX,SAAS,GAAG,GAAG;AACpB,SAAO,IAAI,GAAG,CAAC,CAAC,GAAG,GAAG,CAAC,CAAC,GAAG,GAAG,CAAC,CAAC;AAClC;AAGO,SAAS,0BACd,MACA,SAAS,WACD;AACR,QAAM,SAAS,KAAK,KAAK,KAAK,KAAK,MAAM,GAAG,EAAE;AAC9C,QAAM,SAAS,MAAM,CAAC,EAAG,YAAY;AACrC,QAAM,YAAY,SAAS,QAAQ,SAAS;AAC5C,QAAM,OAAO,IAAI,WAAW,WAAW,IAAI;AAC3C,QAAM,QAAQ,IAAI,WAAW,WAAW,IAAI;AAC5C,QAAM,IAAI,SAAS,KAAK;AACxB,QAAM,QAAQ,IAAI;AAClB,QAAM,MAAM,MAAM,IAAI,KAAW,SAAS,EAAE,CAAC;AAE7C,MAAI,OAAO;AACX,MAAI,UAAU,GAAG;AACf,WAAO,mEAAmE,GAAG;AAAA,EAC/E,WAAW,UAAU,GAAG;AACtB,WAAO,gDAAgD,GAAG;AAAA,EAC5D,WAAW,UAAU,GAAG;AACtB,WAAO,sEAAsE,GAAG;AAAA,EAClF,OAAO;AACL,WAAO,uJAAuJ,GAAG;AAAA,EACnK;AAEA,SAAO;AAAA,gHACuG,UAAU,KAAK,CAAC;AAAA;AAAA,0BAEtG,GAAG;AAAA,sCACS,KAAK;AAAA,uCACJ,SAAS;AAAA,wCACR,IAAI;AAAA;AAAA;AAAA;AAAA,IAIxC,IAAI;AAAA,gJACwI,UAAU,MAAM,CAAC;AAAA;AAAA;AAGjK;AAEA,SAAS,UAAU,GAAmB;AACpC,SAAO,EAAE,QAAQ,MAAM,OAAO,EAAE,QAAQ,MAAM,MAAM,EAAE,QAAQ,MAAM,MAAM,EAAE,QAAQ,MAAM,QAAQ;AACpG;AAEO,SAAS,WAAW,MAAc,QAAgB,SAA0B;AACjF,SAAO;AAAA,IACL,8BAA8B,IAAI;AAAA,IAClC,UAAU,YAAY,OAAO,MAAM;AAAA,IACnC,0FAA0F,KAAK,CAAC,KAAK,GAAG;AAAA,IACxG,UAAU,MAAM;AAAA,IAChB;AAAA,EACF,EACG,OAAO,OAAO,EACd,KAAK,GAAG;AACb;AAGA,eAAsB,iBACpB,MACA,QACA,QACA,SAC6B;AAC7B,MAAI;AACF,UAAM,MAAM,MAAM,MAAM,gDAAgD;AAAA,MACtE,QAAQ;AAAA,MACR,SAAS;AAAA,QACP,eAAe,UAAU,MAAM;AAAA,QAC/B,gBAAgB;AAAA,MAClB;AAAA,MACA,MAAM,KAAK,UAAU;AAAA,QACnB,OAAO;AAAA,QACP,QAAQ,WAAW,MAAM,QAAQ,OAAO;AAAA,QACxC,MAAM;AAAA,QACN,SAAS;AAAA,QACT,iBAAiB;AAAA,QACjB,GAAG;AAAA,MACL,CAAC;AAAA,IACH,CAAC;AACD,QAAI,CAAC,IAAI,GAAI,QAAO;AACpB,UAAM,OAAQ,MAAM,IAAI,KAAK;AAC7B,WAAO,KAAK,OAAO,CAAC,GAAG;AAAA,EACzB,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAEA,eAAsB,uBACpB,OACmB;AACnB,QAAM,OAAO,MAAM,KAAK,KAAK,KAAK;AAClC,QAAM,SAAS,SAAS,MAAM,UAAU,WAAW,SAAS;AAC5D,QAAM,MAAM,0BAA0B,MAAM,MAAM;AAClD,QAAM,MAAM,cAAc,MAAM,QAAQ,OAAO,YAAY,cAAc,QAAQ,MAAM,CAAC,EAAE;AAC1F,MAAI,KAAK;AACP,UAAM,MAAM,MAAM,iBAAiB,MAAM,QAAQ,KAAK,MAAM,OAAO;AACnE,QAAI,KAAK;AACP,aAAO,EAAE,KAAK,UAAU,YAAY,QAAQ,UAAU,WAAW,IAAI;AAAA,IACvE;AAAA,EACF;AACA,SAAO,EAAE,KAAK,UAAU,YAAY,QAAQ,MAAM;AACpD;","names":["WINDOW_MS"]}
|
package/dist/cli.js
CHANGED
|
@@ -5,7 +5,7 @@ import {
|
|
|
5
5
|
clampUsagePeriod,
|
|
6
6
|
generateStorefrontLogo,
|
|
7
7
|
lastNDaysPeriod
|
|
8
|
-
} from "./chunk-
|
|
8
|
+
} from "./chunk-IYCGWBQU.js";
|
|
9
9
|
|
|
10
10
|
// src/cli/index.ts
|
|
11
11
|
import { Command } from "commander";
|
|
@@ -1150,6 +1150,12 @@ var CLI_DOMAINS = [
|
|
|
1150
1150
|
{ flags: "--iccid <iccid>", description: "Filter by ICCID", arg: "iccid", type: "string" },
|
|
1151
1151
|
{ flags: "--msisdn <msisdn>", description: "Filter by MSISDN", arg: "msisdn", type: "string" },
|
|
1152
1152
|
{ flags: "--imsi <imsi>", description: "Filter by IMSI", arg: "imsi", type: "string" },
|
|
1153
|
+
{
|
|
1154
|
+
flags: "--activation-code <code>",
|
|
1155
|
+
description: "Filter by eSIM activation code",
|
|
1156
|
+
arg: "activationCode",
|
|
1157
|
+
type: "string"
|
|
1158
|
+
},
|
|
1153
1159
|
{ flags: "--status <status>", description: "Filter by status, e.g. ACTIVE", arg: "status", type: "string" },
|
|
1154
1160
|
{ flags: "--limit <n>", description: "Max rows to return", arg: "limit", type: "number" },
|
|
1155
1161
|
{ flags: "--offset <n>", description: "Pagination offset", arg: "offset", type: "number" }
|
|
@@ -1668,7 +1674,15 @@ var CLI_DOMAINS = [
|
|
|
1668
1674
|
name: "list",
|
|
1669
1675
|
tool: "list_detailed_location_zones",
|
|
1670
1676
|
summary: "Location zones with countries and operators. Prefer this over `elements`.",
|
|
1671
|
-
options: [
|
|
1677
|
+
options: [
|
|
1678
|
+
RESELLER_ID,
|
|
1679
|
+
{
|
|
1680
|
+
flags: "--verbose <bool>",
|
|
1681
|
+
description: "Return the full OCS zone shape instead of the compact projection",
|
|
1682
|
+
arg: "verbose",
|
|
1683
|
+
type: "boolean"
|
|
1684
|
+
}
|
|
1685
|
+
]
|
|
1672
1686
|
},
|
|
1673
1687
|
{
|
|
1674
1688
|
name: "elements",
|
|
@@ -1854,7 +1868,27 @@ var CLI_DOMAINS = [
|
|
|
1854
1868
|
name: "show",
|
|
1855
1869
|
tool: "get_tariff",
|
|
1856
1870
|
summary: "Per-country wholesale rates for data, voice and SMS.",
|
|
1857
|
-
options: [
|
|
1871
|
+
options: [
|
|
1872
|
+
RESELLER_ID,
|
|
1873
|
+
{
|
|
1874
|
+
flags: "--country <name|iso2>",
|
|
1875
|
+
description: 'Filter to one country: ISO2 ("nl") or name ("Netherlands")',
|
|
1876
|
+
arg: "country",
|
|
1877
|
+
type: "string"
|
|
1878
|
+
},
|
|
1879
|
+
{
|
|
1880
|
+
flags: "--traffic-type <type>",
|
|
1881
|
+
description: "Filter to data, voice or sms",
|
|
1882
|
+
arg: "trafficType",
|
|
1883
|
+
type: "string"
|
|
1884
|
+
},
|
|
1885
|
+
{
|
|
1886
|
+
flags: "--verbose <bool>",
|
|
1887
|
+
description: "Return the full OCS rule shape instead of the compact projection",
|
|
1888
|
+
arg: "verbose",
|
|
1889
|
+
type: "boolean"
|
|
1890
|
+
}
|
|
1891
|
+
]
|
|
1858
1892
|
},
|
|
1859
1893
|
{
|
|
1860
1894
|
name: "voip",
|