@carrierllc/sdk 0.2.12 → 0.2.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -13,7 +13,7 @@ const intent = await carrier.intents.create(
13
13
  )
14
14
  ```
15
15
 
16
- `intents.create` posts to `/v1/connectivity/intents`. The same idempotency key and body return the original intent. `products.create` posts to `/v1/connectivity/products`. The price on a product is catalogue data. Neither call debits a wallet.
16
+ `intents.create` posts to `/v1/connectivity/intents`. Issuance requires a stable caller key matching `[A-Za-z0-9][A-Za-z0-9_.:-]{0,199}` (1–200 ASCII characters). The same key and body identify the original operation; they do not prove provider exactly-once completion. After transport/body loss, `CarrierError` has `code: "outcome_unknown"`, the original `idempotencyKey`, and `retryable: false`. The SDK sends once, disables redirects, and never generates a replacement key. Preserve the key and reconcile; do not automatically retry or reissue. `products.create` posts to `/v1/connectivity/products`. The price on a product is catalogue data. Neither call debits a wallet.
17
17
 
18
18
  ```ts
19
19
  import { Cli, Mcp, restTools } from "@carrierllc/sdk"
@@ -35,4 +35,4 @@ const agentTools = restTools(carrier)
35
35
 
36
36
  `new Mcp(token, { auth: "bearer" })` sends an OAuth access token or a Clerk organization API key. The default sends `?TOKEN=` to `https://mcp.carrier.llc/mcp`. `Cli` never invokes a shell. Catalog entries marked `internal` do not accept a customer API key.
37
37
 
38
- `npm install @carrierllc/sdk@0.2.6` installs this surface. `0.1.0` only creates connectivity intents and products. The published tarball is the compiled `dist` output, including the generated operation catalog.
38
+ `npm install @carrierllc/sdk@0.2.11` installs this surface. `0.1.0` only creates connectivity intents and products. The published tarball is the compiled `dist` output, including the generated operation catalog.
package/dist/client.js CHANGED
@@ -6,7 +6,7 @@
6
6
  * debits a wallet.
7
7
  */
8
8
  import { CarrierError, errorFromBody } from "./error.js";
9
- import { requireIdempotencyKey } from "./idempotency.js";
9
+ import { isIssuance, requireIssuanceKey, requireIdempotencyKey } from "./idempotency.js";
10
10
  import { prepareCall } from "./operations.js";
11
11
  import { USER_AGENT } from "./version.js";
12
12
  export class Carrier {
@@ -67,17 +67,37 @@ export class Carrier {
67
67
  }
68
68
  if (options.idempotencyKey !== undefined)
69
69
  headers["Idempotency-Key"] = requireIdempotencyKey(options.idempotencyKey);
70
+ const issuance = isIssuance(method, path);
71
+ const issuanceKey = issuance ? requireIssuanceKey(options.idempotencyKey ?? headers["X-Idempotency-Key"]) : undefined;
72
+ const unknownOutcome = (status, cause) => new CarrierError(status, "outcome_unknown", "Issuance may have been dispatched. Do not retry or change the key; reconcile the original operation.", "api_error", { idempotencyKey: issuanceKey, retryable: false, cause });
70
73
  let response;
71
74
  try {
72
- response = await this.fetchImpl(url.toString(), { method, headers, body: payload });
75
+ response = await this.fetchImpl(url.toString(), { method, headers, body: payload, ...(issuance ? { redirect: "error" } : {}) });
73
76
  }
74
77
  catch (error) {
78
+ if (issuance)
79
+ throw unknownOutcome(0, error);
75
80
  const message = error instanceof Error ? error.message : "Network request failed.";
76
81
  throw new CarrierError(0, "network_error", message, "api_error");
77
82
  }
78
- const parsed = await readJson(response);
79
- if (!response.ok)
80
- throw errorFromBody(response.status, parsed);
83
+ let parsed;
84
+ try {
85
+ parsed = issuance ? JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(await response.arrayBuffer())) : await readJson(response);
86
+ }
87
+ catch (error) {
88
+ if (issuance)
89
+ throw unknownOutcome(response.status, error);
90
+ throw error;
91
+ }
92
+ if (issuance && response.status >= 300 && response.status < 400)
93
+ throw unknownOutcome(response.status);
94
+ if (!response.ok) {
95
+ const error = errorFromBody(response.status, parsed);
96
+ if (issuance && ["outcome_unknown", "manual_reconciliation_required"].includes(error.code.toLowerCase())) {
97
+ throw new CarrierError(error.status, error.code, error.message, error.type, { idempotencyKey: issuanceKey, retryable: false });
98
+ }
99
+ throw error;
100
+ }
81
101
  return parsed;
82
102
  }
83
103
  }
@@ -568,7 +568,47 @@
568
568
  },
569
569
  {
570
570
  "flags": "--account-id <id>",
571
- "description": "Auto-pick a free eSIM from this account instead of using --iccid",
571
+ "description": "Issue from inventory through the authenticated Carrier API; requires --request-key; preserve it after unknown outcomes",
572
+ "required": false
573
+ },
574
+ {
575
+ "flags": "--request-key <key>",
576
+ "description": "Required stable inventory key (1–200 safe characters); preserved after unknown outcomes",
577
+ "required": false
578
+ },
579
+ {
580
+ "flags": "--start-date <date>",
581
+ "description": "Inventory package start date",
582
+ "required": false
583
+ },
584
+ {
585
+ "flags": "--end-date <date>",
586
+ "description": "Inventory package end date",
587
+ "required": false
588
+ },
589
+ {
590
+ "flags": "--validity-period <days>",
591
+ "description": "Inventory package validity period",
592
+ "required": false
593
+ },
594
+ {
595
+ "flags": "--business-kind <kind>",
596
+ "description": "Authorized reference kind: storefront_order",
597
+ "required": false
598
+ },
599
+ {
600
+ "flags": "--business-id <id>",
601
+ "description": "Existing API-authorized order ID",
602
+ "required": false
603
+ },
604
+ {
605
+ "flags": "--business-line <line>",
606
+ "description": "Authorized reference line: esim",
607
+ "required": false
608
+ },
609
+ {
610
+ "flags": "--business-verb <verb>",
611
+ "description": "Authorized reference verb: issue",
572
612
  "required": false
573
613
  }
574
614
  ]
@@ -1885,7 +1925,7 @@
1885
1925
  "options": [
1886
1926
  {
1887
1927
  "flags": "--pack <pack>",
1888
- "description": "Pack id (pack_500/pack_1000/pack_2500/pack_5000) or exact EUR-cents amount.",
1928
+ "description": "Pack id (pack_25/pack_50/pack_100/pack_500/pack_1000/pack_2500/pack_5000) or exact EUR-cents amount.",
1889
1929
  "required": false
1890
1930
  }
1891
1931
  ]
@@ -2453,7 +2453,7 @@
2453
2453
  "headerParams": [
2454
2454
  {
2455
2455
  "name": "X-Idempotency-Key",
2456
- "required": false
2456
+ "required": true
2457
2457
  }
2458
2458
  ]
2459
2459
  },
package/dist/error.d.ts CHANGED
@@ -3,6 +3,13 @@ export declare class CarrierError extends Error {
3
3
  readonly status: number;
4
4
  readonly code: string;
5
5
  readonly type: string;
6
- constructor(status: number, code: string, message: string, type?: string);
6
+ /** Present for a possibly dispatched issuance; keep this key for reconciliation. */
7
+ readonly idempotencyKey?: string;
8
+ readonly retryable?: boolean;
9
+ constructor(status: number, code: string, message: string, type?: string, details?: {
10
+ idempotencyKey?: string;
11
+ retryable?: boolean;
12
+ cause?: unknown;
13
+ });
7
14
  }
8
15
  export declare function errorFromBody(status: number, body: unknown): CarrierError;
package/dist/error.js CHANGED
@@ -3,17 +3,26 @@ export class CarrierError extends Error {
3
3
  status;
4
4
  code;
5
5
  type;
6
- constructor(status, code, message, type = "invalid_request_error") {
7
- super(message);
6
+ /** Present for a possibly dispatched issuance; keep this key for reconciliation. */
7
+ idempotencyKey;
8
+ retryable;
9
+ constructor(status, code, message, type = "invalid_request_error", details = {}) {
10
+ super(message, { cause: details.cause });
8
11
  this.name = "CarrierError";
9
12
  this.status = status;
10
13
  this.code = code;
11
14
  this.type = type;
15
+ this.idempotencyKey = details.idempotencyKey;
16
+ this.retryable = details.retryable;
12
17
  }
13
18
  }
14
19
  export function errorFromBody(status, body) {
15
20
  if (body && typeof body === "object" && "error" in body) {
16
21
  const error = body.error;
22
+ if (typeof error === "string") {
23
+ const code = body.code;
24
+ return new CarrierError(status, typeof code === "string" ? code : "api_error", error, "api_error");
25
+ }
17
26
  if (error && typeof error === "object") {
18
27
  const record = error;
19
28
  const code = typeof record.code === "string" ? record.code : "api_error";
@@ -1,4 +1,7 @@
1
- /** Same limits as the API, so a bad key fails before a request is sent. */
1
+ /** Issuance keys are the verbatim stable operation identity, never normalized. */
2
+ export declare function requireIssuanceKey(key: string | undefined): string;
3
+ export declare function isIssuance(method: string, path: string): boolean;
4
+ /** Nonissuance operations retain their existing API limits. */
2
5
  export declare function requireIdempotencyKey(key: string | undefined): string;
3
6
  /** Reject a path segment that could escape the resource URL. */
4
7
  export declare function requirePathId(value: string, label: string): string;
@@ -1,5 +1,15 @@
1
1
  import { CarrierError } from "./error.js";
2
- /** Same limits as the API, so a bad key fails before a request is sent. */
2
+ /** Issuance keys are the verbatim stable operation identity, never normalized. */
3
+ export function requireIssuanceKey(key) {
4
+ if (typeof key !== "string" || !/^[A-Za-z0-9][A-Za-z0-9_.:-]{0,199}$/.test(key) || /[\r\n]/.test(key)) {
5
+ throw new CarrierError(400, "invalid_idempotency_key", "Issuance requires a 1–200 character ASCII key starting with a letter or digit and containing only letters, digits, _, ., :, or -.");
6
+ }
7
+ return key;
8
+ }
9
+ export function isIssuance(method, path) {
10
+ return method === "POST" && ["/v1/connectivity/intents", "/v1/subscribers/issue-from-inventory"].includes(path);
11
+ }
12
+ /** Nonissuance operations retain their existing API limits. */
3
13
  export function requireIdempotencyKey(key) {
4
14
  if (key === undefined || key.trim() === "") {
5
15
  throw new CarrierError(400, "invalid_idempotency_key", "Idempotency-Key is required.");
@@ -2,7 +2,7 @@
2
2
  * Generic caller for every operation id in the public OpenAPI document.
3
3
  */
4
4
  import { CarrierError } from "./error.js";
5
- import { requireIdempotencyKey, requirePathId } from "./idempotency.js";
5
+ import { isIssuance, requireIssuanceKey, requireIdempotencyKey, requirePathId } from "./idempotency.js";
6
6
  import { HEADER_FIELDS, operation } from "./catalog.js";
7
7
  const IDEMPOTENCY_HEADERS = new Set(["Idempotency-Key", "X-Idempotency-Key"]);
8
8
  export function prepareCall(operationId, params = {}) {
@@ -18,7 +18,7 @@ export function prepareCall(operationId, params = {}) {
18
18
  requireHeaders(headerValues, spec);
19
19
  for (const name of Object.keys(headerValues)) {
20
20
  if (IDEMPOTENCY_HEADERS.has(name)) {
21
- headerValues[name] = requireIdempotencyKey(headerValues[name]);
21
+ headerValues[name] = isIssuance(spec.method, spec.path) ? requireIssuanceKey(headerValues[name]) : requireIdempotencyKey(headerValues[name]);
22
22
  }
23
23
  }
24
24
  if (spec.bodyRequired && params.body === undefined) {
@@ -57,7 +57,9 @@ export function inputSchema(spec) {
57
57
  }
58
58
  for (const param of spec.headerParams) {
59
59
  const field = HEADER_FIELDS[param.name] ?? param.name;
60
- properties[field] = { type: "string", description: param.name };
60
+ properties[field] = { type: "string", description: param.name,
61
+ ...(isIssuance(spec.method, spec.path) && IDEMPOTENCY_HEADERS.has(param.name) ? { minLength: 1, maxLength: 200, pattern: "^[A-Za-z0-9][A-Za-z0-9_.:-]{0,199}$" } : {}),
62
+ };
61
63
  if (param.required)
62
64
  required.push(field);
63
65
  }
package/dist/version.d.ts CHANGED
@@ -1,3 +1,3 @@
1
1
  /** Published version. Keep this equal to package.json. */
2
- export declare const SDK_VERSION = "0.2.12";
3
- export declare const USER_AGENT = "@carrierllc/sdk/0.2.12";
2
+ export declare const SDK_VERSION = "0.2.14";
3
+ export declare const USER_AGENT = "@carrierllc/sdk/0.2.14";
package/dist/version.js CHANGED
@@ -1,3 +1,3 @@
1
1
  /** Published version. Keep this equal to package.json. */
2
- export const SDK_VERSION = "0.2.12";
2
+ export const SDK_VERSION = "0.2.14";
3
3
  export const USER_AGENT = `@carrierllc/sdk/${SDK_VERSION}`;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@carrierllc/sdk",
3
- "version": "0.2.12",
3
+ "version": "0.2.14",
4
4
  "description": "TypeScript client for the Carrier API, MCP server, and CLI.",
5
5
  "license": "MIT",
6
6
  "type": "module",