@mitumbadotafrica/logistics 0.0.0-stage → 0.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs ADDED
@@ -0,0 +1,221 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __reExport = (target, mod, secondTarget) => (__copyProps(target, mod, "default"), secondTarget && __copyProps(secondTarget, mod, "default"));
19
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
20
+
21
+ // src/index.ts
22
+ var index_exports = {};
23
+ __export(index_exports, {
24
+ DEFAULT_BASE_URL: () => DEFAULT_BASE_URL,
25
+ HttpClient: () => HttpClient,
26
+ LogisticsError: () => LogisticsError,
27
+ MitumbaLogistics: () => MitumbaLogistics,
28
+ pollParcel: () => pollParcel
29
+ });
30
+ module.exports = __toCommonJS(index_exports);
31
+
32
+ // src/errors.ts
33
+ var LogisticsError = class extends Error {
34
+ constructor(status, body) {
35
+ super(body.message ?? body.error);
36
+ this.name = "LogisticsError";
37
+ this.code = body.error;
38
+ this.status = status;
39
+ this.details = body.details;
40
+ this.reason = body.reason;
41
+ }
42
+ /** True for a temporary backend condition (503), e.g. no carriers configured. */
43
+ get isTemporary() {
44
+ return this.status === 503;
45
+ }
46
+ };
47
+
48
+ // src/config.ts
49
+ var DEFAULT_BASE_URL = "https://api.mitumba.africa";
50
+
51
+ // src/http.ts
52
+ var RETRYABLE_METHODS = /* @__PURE__ */ new Set(["GET"]);
53
+ var HttpClient = class {
54
+ constructor(config = {}) {
55
+ this.baseUrl = stripTrailingSlashes(config.baseUrl ?? DEFAULT_BASE_URL);
56
+ this.maxRetries = config.maxRetries ?? 3;
57
+ this.fetchImpl = config.fetch ?? globalThis.fetch;
58
+ this.debug = config.debug ?? false;
59
+ this.tokenSource = config.token;
60
+ if (typeof this.fetchImpl !== "function") {
61
+ throw new Error(
62
+ "No fetch implementation available. Provide `fetch` in the config for this runtime."
63
+ );
64
+ }
65
+ }
66
+ async resolveToken() {
67
+ if (typeof this.tokenSource === "function") return this.tokenSource();
68
+ return this.tokenSource;
69
+ }
70
+ async get(path, options) {
71
+ return this.request("GET", path, void 0, options);
72
+ }
73
+ async post(path, body, options) {
74
+ return this.request("POST", path, body, options);
75
+ }
76
+ async request(method, path, body, options) {
77
+ const url = `${this.baseUrl}${path.startsWith("/") ? path : `/${path}`}`;
78
+ const token = options?.token ?? await this.resolveToken();
79
+ const headers = new Headers();
80
+ if (body !== void 0) headers.set("Content-Type", "application/json");
81
+ if (token) headers.set("Authorization", `Bearer ${token}`);
82
+ const init = {
83
+ method,
84
+ headers,
85
+ body: body !== void 0 ? JSON.stringify(body) : void 0,
86
+ signal: options?.signal
87
+ };
88
+ const canRetry = RETRYABLE_METHODS.has(method);
89
+ let attempt = 0;
90
+ let response;
91
+ for (; ; ) {
92
+ if (this.debug) console.log(`[mitumba-logistics] ${method} ${url} (attempt ${attempt + 1})`);
93
+ try {
94
+ response = await this.fetchImpl(url, init);
95
+ } catch (err) {
96
+ if (err instanceof Error && err.name === "AbortError") throw err;
97
+ if (attempt < this.maxRetries) {
98
+ await backoff(++attempt);
99
+ continue;
100
+ }
101
+ throw new LogisticsError(0, {
102
+ error: "network_error",
103
+ message: err instanceof Error ? err.message : "Network request failed"
104
+ });
105
+ }
106
+ if (response.status >= 500 && canRetry && attempt < this.maxRetries) {
107
+ await backoff(++attempt);
108
+ continue;
109
+ }
110
+ break;
111
+ }
112
+ if (!response.ok) {
113
+ let errBody;
114
+ try {
115
+ errBody = await response.json();
116
+ } catch {
117
+ errBody = { error: "unknown_error", message: response.statusText };
118
+ }
119
+ throw new LogisticsError(response.status, errBody);
120
+ }
121
+ if (response.status === 204) return void 0;
122
+ return await response.json();
123
+ }
124
+ };
125
+ function backoff(attempt) {
126
+ const ms = Math.min(2 ** attempt * 100, 2e3);
127
+ return new Promise((resolve) => setTimeout(resolve, ms));
128
+ }
129
+ function stripTrailingSlashes(value) {
130
+ let end = value.length;
131
+ while (end > 0 && value.charCodeAt(end - 1) === 47) end--;
132
+ return value.slice(0, end);
133
+ }
134
+
135
+ // src/client.ts
136
+ var MitumbaLogistics = class {
137
+ constructor(config = {}) {
138
+ this.http = new HttpClient(config);
139
+ }
140
+ /** Public market discovery: markets, currencies, fulfilment methods. */
141
+ async getMarkets(options) {
142
+ return this.http.get("/logistics/markets", options);
143
+ }
144
+ /**
145
+ * Route and price a parcel for the chosen method. Returns the chosen (cheapest)
146
+ * option plus alternatives; each option carries its persisted `quote_id` to
147
+ * pass to {@link createParcel}.
148
+ */
149
+ async quote(input, options) {
150
+ return this.http.post("/logistics/quotes", input, options);
151
+ }
152
+ /** Book the carrier chosen by a prior quote and create the parcel. */
153
+ async createParcel(input, options) {
154
+ return this.http.post("/logistics/parcels", input, options);
155
+ }
156
+ /**
157
+ * Buyer-facing parcel view: the normalized status plus the dual-surface
158
+ * carrier tracking passthrough. The collection code is never returned — only
159
+ * whether one exists.
160
+ */
161
+ async getParcel(parcelId, options) {
162
+ return this.http.get(`/logistics/parcels/${encodeURIComponent(parcelId)}`, options);
163
+ }
164
+ };
165
+
166
+ // src/polling.ts
167
+ var import_logistics_contract = require("@mitumbadotafrica/logistics-contract");
168
+ async function pollParcel(client, parcelId, options = {}) {
169
+ const deadlineMs = options.deadlineMs ?? 6e5;
170
+ const intervalMs = options.intervalMs ?? 15e3;
171
+ const jitter = options.jitter ?? 0.2;
172
+ const stopOnAction = options.stopOnActionRequired ?? true;
173
+ const startedAt = Date.now();
174
+ let last = null;
175
+ for (; ; ) {
176
+ if (options.signal?.aborted) return last;
177
+ const reqOptions = options.signal ? { signal: options.signal } : {};
178
+ last = await client.getParcel(parcelId, reqOptions);
179
+ options.onUpdate?.(last);
180
+ if ((0, import_logistics_contract.isTerminalParcelStatus)(last.status)) return last;
181
+ if (stopOnAction && last.status === "action_required") return last;
182
+ if (Date.now() - startedAt >= deadlineMs) {
183
+ options.onTimeout?.(last);
184
+ return last;
185
+ }
186
+ const wait = applyJitter(intervalMs, jitter);
187
+ const remaining = deadlineMs - (Date.now() - startedAt);
188
+ await sleep(Math.min(wait, Math.max(remaining, 0)), options.signal);
189
+ }
190
+ }
191
+ function applyJitter(baseMs, jitter) {
192
+ if (jitter <= 0) return baseMs;
193
+ const delta = baseMs * jitter;
194
+ return Math.round(baseMs - delta + Math.random() * 2 * delta);
195
+ }
196
+ function sleep(ms, signal) {
197
+ return new Promise((resolve) => {
198
+ if (ms <= 0) return resolve();
199
+ const timer = setTimeout(() => {
200
+ signal?.removeEventListener("abort", onAbort);
201
+ resolve();
202
+ }, ms);
203
+ const onAbort = () => {
204
+ clearTimeout(timer);
205
+ resolve();
206
+ };
207
+ signal?.addEventListener("abort", onAbort, { once: true });
208
+ });
209
+ }
210
+
211
+ // src/index.ts
212
+ __reExport(index_exports, require("@mitumbadotafrica/logistics-contract"), module.exports);
213
+ // Annotate the CommonJS export names for ESM import in node:
214
+ 0 && (module.exports = {
215
+ DEFAULT_BASE_URL,
216
+ HttpClient,
217
+ LogisticsError,
218
+ MitumbaLogistics,
219
+ pollParcel,
220
+ ...require("@mitumbadotafrica/logistics-contract")
221
+ });
@@ -0,0 +1,149 @@
1
+ import { MarketsResponse, QuoteRequest, QuoteResponse, CreateParcelResponse, ParcelView, LogisticsErrorBody } from '@mitumbadotafrica/logistics-contract';
2
+ export * from '@mitumbadotafrica/logistics-contract';
3
+
4
+ /** Canonical public API base. Override for staging/self-hosting. */
5
+ declare const DEFAULT_BASE_URL = "https://api.mitumba.africa";
6
+ interface MitumbaLogisticsConfig {
7
+ /** API base URL. Defaults to {@link DEFAULT_BASE_URL}. */
8
+ baseUrl?: string;
9
+ /**
10
+ * A bearer token, or a (possibly async) function returning one. The core
11
+ * client is auth-agnostic: it attaches the token as `Authorization: Bearer
12
+ * <token>`. Token acquisition and refresh are the host application's job.
13
+ */
14
+ token?: string | (() => string | undefined | Promise<string | undefined>);
15
+ /** Max retries on network errors and 5xx responses. Default 3. */
16
+ maxRetries?: number;
17
+ /** Custom fetch implementation (e.g. for RN or tests). Defaults to global fetch. */
18
+ fetch?: typeof fetch;
19
+ /** Log request/response lines to the console. Default false. */
20
+ debug?: boolean;
21
+ }
22
+ interface RequestOptions {
23
+ /** Abort signal to cancel the in-flight request. */
24
+ signal?: AbortSignal;
25
+ /** Per-request bearer override, sent instead of the client's configured token. */
26
+ token?: string;
27
+ }
28
+
29
+ /**
30
+ * Input to book a parcel against a prior quote. The sender is Mitumba's masked
31
+ * identity, supplied by the caller (the orders flow) — never a seller identity.
32
+ * `weight_grams` is the real parcel weight (the carrier books and bills by it).
33
+ */
34
+ interface CreateParcelInput {
35
+ quote_id: string;
36
+ weight_grams: number;
37
+ sender: ShipmentPartyInput;
38
+ recipient: ShipmentPartyInput;
39
+ }
40
+ interface ShipmentPartyInput {
41
+ name: string;
42
+ phone: string;
43
+ city_id?: string | null;
44
+ node_id?: string;
45
+ address_line?: string;
46
+ point?: {
47
+ latitude: number;
48
+ longitude: number;
49
+ };
50
+ }
51
+ /**
52
+ * The headless, carrier-neutral Mitumba Logistics client. Knows the shape of the
53
+ * HTTP contract (via @mitumbadotafrica/logistics-contract) and nothing about how parcels
54
+ * are booked, routed, or tracked. No carrier implementations, no secrets.
55
+ *
56
+ * The buyer picks a fulfilment METHOD in {@link quote}; the backend chooses the
57
+ * carrier. Carrier identity returned in responses is observable metadata — never
58
+ * something the client switches on to drive the flow.
59
+ */
60
+ declare class MitumbaLogistics {
61
+ private readonly http;
62
+ constructor(config?: MitumbaLogisticsConfig);
63
+ /** Public market discovery: markets, currencies, fulfilment methods. */
64
+ getMarkets(options?: RequestOptions): Promise<MarketsResponse>;
65
+ /**
66
+ * Route and price a parcel for the chosen method. Returns the chosen (cheapest)
67
+ * option plus alternatives; each option carries its persisted `quote_id` to
68
+ * pass to {@link createParcel}.
69
+ */
70
+ quote(input: QuoteRequest, options?: RequestOptions): Promise<QuoteResponse>;
71
+ /** Book the carrier chosen by a prior quote and create the parcel. */
72
+ createParcel(input: CreateParcelInput, options?: RequestOptions): Promise<CreateParcelResponse>;
73
+ /**
74
+ * Buyer-facing parcel view: the normalized status plus the dual-surface
75
+ * carrier tracking passthrough. The collection code is never returned — only
76
+ * whether one exists.
77
+ */
78
+ getParcel(parcelId: string, options?: RequestOptions): Promise<ParcelView>;
79
+ }
80
+
81
+ /**
82
+ * Minimal, framework-agnostic HTTP client. Token acquisition is the host's job
83
+ * (see {@link MitumbaLogisticsConfig.token}). Retries network errors and 5xx
84
+ * with exponential backoff; GET is idempotent-safe to retry, POST is only
85
+ * retried on network errors (never after a server response).
86
+ */
87
+ declare class HttpClient {
88
+ private readonly baseUrl;
89
+ private readonly maxRetries;
90
+ private readonly fetchImpl;
91
+ private readonly debug;
92
+ private readonly tokenSource;
93
+ constructor(config?: MitumbaLogisticsConfig);
94
+ private resolveToken;
95
+ get<T>(path: string, options?: RequestOptions): Promise<T>;
96
+ post<T>(path: string, body: unknown, options?: RequestOptions): Promise<T>;
97
+ private request;
98
+ }
99
+
100
+ /**
101
+ * Thrown for any non-2xx API response and for network/transport failures.
102
+ * `code` is the machine-readable error code from the API envelope (`{ error }`).
103
+ * `status` is 0 for network-level failures. `reason` carries the backend's
104
+ * sub-reason where present (e.g. `no_route` → `not_serviceable`).
105
+ */
106
+ declare class LogisticsError extends Error {
107
+ readonly code: string;
108
+ readonly status: number;
109
+ readonly details?: unknown;
110
+ readonly reason?: string;
111
+ constructor(status: number, body: LogisticsErrorBody);
112
+ /** True for a temporary backend condition (503), e.g. no carriers configured. */
113
+ get isTemporary(): boolean;
114
+ }
115
+
116
+ interface PollParcelOptions {
117
+ /** Fired on every successful status read. */
118
+ onUpdate?: (parcel: ParcelView) => void;
119
+ /**
120
+ * Client-side safety deadline (ms). When exceeded, polling stops and
121
+ * `onTimeout` fires. This is "still in transit" — it must NOT be treated as a
122
+ * delivery failure. Default 600000 (10 min); parcels are long-lived, so hosts
123
+ * typically poll a view open on screen rather than to completion.
124
+ */
125
+ deadlineMs?: number;
126
+ onTimeout?: (last: ParcelView | null) => void;
127
+ /** Abort signal to stop polling. */
128
+ signal?: AbortSignal;
129
+ /** Base interval between reads (ms). Default 15000. */
130
+ intervalMs?: number;
131
+ /** Fractional jitter applied to each interval. Default 0.2. */
132
+ jitter?: number;
133
+ /**
134
+ * Stop as soon as the parcel needs the buyer (status `action_required`), so the
135
+ * host can surface the NextAction. Default true. When false, polling continues
136
+ * through an action_required park (it is non-terminal and resumable).
137
+ */
138
+ stopOnActionRequired?: boolean;
139
+ }
140
+ /**
141
+ * Poll a parcel's status until it reaches a terminal state (delivered, returned,
142
+ * cancelled, lost), needs buyer action, the deadline passes, or the signal
143
+ * aborts. Keeps at most one request in flight. Critically, an UNKNOWN status is
144
+ * treated as non-terminal and keeps polling — elapsed client time never invents
145
+ * a terminal outcome. Resolves with the last view read.
146
+ */
147
+ declare function pollParcel(client: MitumbaLogistics, parcelId: string, options?: PollParcelOptions): Promise<ParcelView | null>;
148
+
149
+ export { type CreateParcelInput, DEFAULT_BASE_URL, HttpClient, LogisticsError, MitumbaLogistics, type MitumbaLogisticsConfig, type PollParcelOptions, type RequestOptions, type ShipmentPartyInput, pollParcel };
@@ -0,0 +1,149 @@
1
+ import { MarketsResponse, QuoteRequest, QuoteResponse, CreateParcelResponse, ParcelView, LogisticsErrorBody } from '@mitumbadotafrica/logistics-contract';
2
+ export * from '@mitumbadotafrica/logistics-contract';
3
+
4
+ /** Canonical public API base. Override for staging/self-hosting. */
5
+ declare const DEFAULT_BASE_URL = "https://api.mitumba.africa";
6
+ interface MitumbaLogisticsConfig {
7
+ /** API base URL. Defaults to {@link DEFAULT_BASE_URL}. */
8
+ baseUrl?: string;
9
+ /**
10
+ * A bearer token, or a (possibly async) function returning one. The core
11
+ * client is auth-agnostic: it attaches the token as `Authorization: Bearer
12
+ * <token>`. Token acquisition and refresh are the host application's job.
13
+ */
14
+ token?: string | (() => string | undefined | Promise<string | undefined>);
15
+ /** Max retries on network errors and 5xx responses. Default 3. */
16
+ maxRetries?: number;
17
+ /** Custom fetch implementation (e.g. for RN or tests). Defaults to global fetch. */
18
+ fetch?: typeof fetch;
19
+ /** Log request/response lines to the console. Default false. */
20
+ debug?: boolean;
21
+ }
22
+ interface RequestOptions {
23
+ /** Abort signal to cancel the in-flight request. */
24
+ signal?: AbortSignal;
25
+ /** Per-request bearer override, sent instead of the client's configured token. */
26
+ token?: string;
27
+ }
28
+
29
+ /**
30
+ * Input to book a parcel against a prior quote. The sender is Mitumba's masked
31
+ * identity, supplied by the caller (the orders flow) — never a seller identity.
32
+ * `weight_grams` is the real parcel weight (the carrier books and bills by it).
33
+ */
34
+ interface CreateParcelInput {
35
+ quote_id: string;
36
+ weight_grams: number;
37
+ sender: ShipmentPartyInput;
38
+ recipient: ShipmentPartyInput;
39
+ }
40
+ interface ShipmentPartyInput {
41
+ name: string;
42
+ phone: string;
43
+ city_id?: string | null;
44
+ node_id?: string;
45
+ address_line?: string;
46
+ point?: {
47
+ latitude: number;
48
+ longitude: number;
49
+ };
50
+ }
51
+ /**
52
+ * The headless, carrier-neutral Mitumba Logistics client. Knows the shape of the
53
+ * HTTP contract (via @mitumbadotafrica/logistics-contract) and nothing about how parcels
54
+ * are booked, routed, or tracked. No carrier implementations, no secrets.
55
+ *
56
+ * The buyer picks a fulfilment METHOD in {@link quote}; the backend chooses the
57
+ * carrier. Carrier identity returned in responses is observable metadata — never
58
+ * something the client switches on to drive the flow.
59
+ */
60
+ declare class MitumbaLogistics {
61
+ private readonly http;
62
+ constructor(config?: MitumbaLogisticsConfig);
63
+ /** Public market discovery: markets, currencies, fulfilment methods. */
64
+ getMarkets(options?: RequestOptions): Promise<MarketsResponse>;
65
+ /**
66
+ * Route and price a parcel for the chosen method. Returns the chosen (cheapest)
67
+ * option plus alternatives; each option carries its persisted `quote_id` to
68
+ * pass to {@link createParcel}.
69
+ */
70
+ quote(input: QuoteRequest, options?: RequestOptions): Promise<QuoteResponse>;
71
+ /** Book the carrier chosen by a prior quote and create the parcel. */
72
+ createParcel(input: CreateParcelInput, options?: RequestOptions): Promise<CreateParcelResponse>;
73
+ /**
74
+ * Buyer-facing parcel view: the normalized status plus the dual-surface
75
+ * carrier tracking passthrough. The collection code is never returned — only
76
+ * whether one exists.
77
+ */
78
+ getParcel(parcelId: string, options?: RequestOptions): Promise<ParcelView>;
79
+ }
80
+
81
+ /**
82
+ * Minimal, framework-agnostic HTTP client. Token acquisition is the host's job
83
+ * (see {@link MitumbaLogisticsConfig.token}). Retries network errors and 5xx
84
+ * with exponential backoff; GET is idempotent-safe to retry, POST is only
85
+ * retried on network errors (never after a server response).
86
+ */
87
+ declare class HttpClient {
88
+ private readonly baseUrl;
89
+ private readonly maxRetries;
90
+ private readonly fetchImpl;
91
+ private readonly debug;
92
+ private readonly tokenSource;
93
+ constructor(config?: MitumbaLogisticsConfig);
94
+ private resolveToken;
95
+ get<T>(path: string, options?: RequestOptions): Promise<T>;
96
+ post<T>(path: string, body: unknown, options?: RequestOptions): Promise<T>;
97
+ private request;
98
+ }
99
+
100
+ /**
101
+ * Thrown for any non-2xx API response and for network/transport failures.
102
+ * `code` is the machine-readable error code from the API envelope (`{ error }`).
103
+ * `status` is 0 for network-level failures. `reason` carries the backend's
104
+ * sub-reason where present (e.g. `no_route` → `not_serviceable`).
105
+ */
106
+ declare class LogisticsError extends Error {
107
+ readonly code: string;
108
+ readonly status: number;
109
+ readonly details?: unknown;
110
+ readonly reason?: string;
111
+ constructor(status: number, body: LogisticsErrorBody);
112
+ /** True for a temporary backend condition (503), e.g. no carriers configured. */
113
+ get isTemporary(): boolean;
114
+ }
115
+
116
+ interface PollParcelOptions {
117
+ /** Fired on every successful status read. */
118
+ onUpdate?: (parcel: ParcelView) => void;
119
+ /**
120
+ * Client-side safety deadline (ms). When exceeded, polling stops and
121
+ * `onTimeout` fires. This is "still in transit" — it must NOT be treated as a
122
+ * delivery failure. Default 600000 (10 min); parcels are long-lived, so hosts
123
+ * typically poll a view open on screen rather than to completion.
124
+ */
125
+ deadlineMs?: number;
126
+ onTimeout?: (last: ParcelView | null) => void;
127
+ /** Abort signal to stop polling. */
128
+ signal?: AbortSignal;
129
+ /** Base interval between reads (ms). Default 15000. */
130
+ intervalMs?: number;
131
+ /** Fractional jitter applied to each interval. Default 0.2. */
132
+ jitter?: number;
133
+ /**
134
+ * Stop as soon as the parcel needs the buyer (status `action_required`), so the
135
+ * host can surface the NextAction. Default true. When false, polling continues
136
+ * through an action_required park (it is non-terminal and resumable).
137
+ */
138
+ stopOnActionRequired?: boolean;
139
+ }
140
+ /**
141
+ * Poll a parcel's status until it reaches a terminal state (delivered, returned,
142
+ * cancelled, lost), needs buyer action, the deadline passes, or the signal
143
+ * aborts. Keeps at most one request in flight. Critically, an UNKNOWN status is
144
+ * treated as non-terminal and keeps polling — elapsed client time never invents
145
+ * a terminal outcome. Resolves with the last view read.
146
+ */
147
+ declare function pollParcel(client: MitumbaLogistics, parcelId: string, options?: PollParcelOptions): Promise<ParcelView | null>;
148
+
149
+ export { type CreateParcelInput, DEFAULT_BASE_URL, HttpClient, LogisticsError, MitumbaLogistics, type MitumbaLogisticsConfig, type PollParcelOptions, type RequestOptions, type ShipmentPartyInput, pollParcel };
package/dist/index.js ADDED
@@ -0,0 +1,188 @@
1
+ // src/errors.ts
2
+ var LogisticsError = class extends Error {
3
+ constructor(status, body) {
4
+ super(body.message ?? body.error);
5
+ this.name = "LogisticsError";
6
+ this.code = body.error;
7
+ this.status = status;
8
+ this.details = body.details;
9
+ this.reason = body.reason;
10
+ }
11
+ /** True for a temporary backend condition (503), e.g. no carriers configured. */
12
+ get isTemporary() {
13
+ return this.status === 503;
14
+ }
15
+ };
16
+
17
+ // src/config.ts
18
+ var DEFAULT_BASE_URL = "https://api.mitumba.africa";
19
+
20
+ // src/http.ts
21
+ var RETRYABLE_METHODS = /* @__PURE__ */ new Set(["GET"]);
22
+ var HttpClient = class {
23
+ constructor(config = {}) {
24
+ this.baseUrl = stripTrailingSlashes(config.baseUrl ?? DEFAULT_BASE_URL);
25
+ this.maxRetries = config.maxRetries ?? 3;
26
+ this.fetchImpl = config.fetch ?? globalThis.fetch;
27
+ this.debug = config.debug ?? false;
28
+ this.tokenSource = config.token;
29
+ if (typeof this.fetchImpl !== "function") {
30
+ throw new Error(
31
+ "No fetch implementation available. Provide `fetch` in the config for this runtime."
32
+ );
33
+ }
34
+ }
35
+ async resolveToken() {
36
+ if (typeof this.tokenSource === "function") return this.tokenSource();
37
+ return this.tokenSource;
38
+ }
39
+ async get(path, options) {
40
+ return this.request("GET", path, void 0, options);
41
+ }
42
+ async post(path, body, options) {
43
+ return this.request("POST", path, body, options);
44
+ }
45
+ async request(method, path, body, options) {
46
+ const url = `${this.baseUrl}${path.startsWith("/") ? path : `/${path}`}`;
47
+ const token = options?.token ?? await this.resolveToken();
48
+ const headers = new Headers();
49
+ if (body !== void 0) headers.set("Content-Type", "application/json");
50
+ if (token) headers.set("Authorization", `Bearer ${token}`);
51
+ const init = {
52
+ method,
53
+ headers,
54
+ body: body !== void 0 ? JSON.stringify(body) : void 0,
55
+ signal: options?.signal
56
+ };
57
+ const canRetry = RETRYABLE_METHODS.has(method);
58
+ let attempt = 0;
59
+ let response;
60
+ for (; ; ) {
61
+ if (this.debug) console.log(`[mitumba-logistics] ${method} ${url} (attempt ${attempt + 1})`);
62
+ try {
63
+ response = await this.fetchImpl(url, init);
64
+ } catch (err) {
65
+ if (err instanceof Error && err.name === "AbortError") throw err;
66
+ if (attempt < this.maxRetries) {
67
+ await backoff(++attempt);
68
+ continue;
69
+ }
70
+ throw new LogisticsError(0, {
71
+ error: "network_error",
72
+ message: err instanceof Error ? err.message : "Network request failed"
73
+ });
74
+ }
75
+ if (response.status >= 500 && canRetry && attempt < this.maxRetries) {
76
+ await backoff(++attempt);
77
+ continue;
78
+ }
79
+ break;
80
+ }
81
+ if (!response.ok) {
82
+ let errBody;
83
+ try {
84
+ errBody = await response.json();
85
+ } catch {
86
+ errBody = { error: "unknown_error", message: response.statusText };
87
+ }
88
+ throw new LogisticsError(response.status, errBody);
89
+ }
90
+ if (response.status === 204) return void 0;
91
+ return await response.json();
92
+ }
93
+ };
94
+ function backoff(attempt) {
95
+ const ms = Math.min(2 ** attempt * 100, 2e3);
96
+ return new Promise((resolve) => setTimeout(resolve, ms));
97
+ }
98
+ function stripTrailingSlashes(value) {
99
+ let end = value.length;
100
+ while (end > 0 && value.charCodeAt(end - 1) === 47) end--;
101
+ return value.slice(0, end);
102
+ }
103
+
104
+ // src/client.ts
105
+ var MitumbaLogistics = class {
106
+ constructor(config = {}) {
107
+ this.http = new HttpClient(config);
108
+ }
109
+ /** Public market discovery: markets, currencies, fulfilment methods. */
110
+ async getMarkets(options) {
111
+ return this.http.get("/logistics/markets", options);
112
+ }
113
+ /**
114
+ * Route and price a parcel for the chosen method. Returns the chosen (cheapest)
115
+ * option plus alternatives; each option carries its persisted `quote_id` to
116
+ * pass to {@link createParcel}.
117
+ */
118
+ async quote(input, options) {
119
+ return this.http.post("/logistics/quotes", input, options);
120
+ }
121
+ /** Book the carrier chosen by a prior quote and create the parcel. */
122
+ async createParcel(input, options) {
123
+ return this.http.post("/logistics/parcels", input, options);
124
+ }
125
+ /**
126
+ * Buyer-facing parcel view: the normalized status plus the dual-surface
127
+ * carrier tracking passthrough. The collection code is never returned — only
128
+ * whether one exists.
129
+ */
130
+ async getParcel(parcelId, options) {
131
+ return this.http.get(`/logistics/parcels/${encodeURIComponent(parcelId)}`, options);
132
+ }
133
+ };
134
+
135
+ // src/polling.ts
136
+ import { isTerminalParcelStatus } from "@mitumbadotafrica/logistics-contract";
137
+ async function pollParcel(client, parcelId, options = {}) {
138
+ const deadlineMs = options.deadlineMs ?? 6e5;
139
+ const intervalMs = options.intervalMs ?? 15e3;
140
+ const jitter = options.jitter ?? 0.2;
141
+ const stopOnAction = options.stopOnActionRequired ?? true;
142
+ const startedAt = Date.now();
143
+ let last = null;
144
+ for (; ; ) {
145
+ if (options.signal?.aborted) return last;
146
+ const reqOptions = options.signal ? { signal: options.signal } : {};
147
+ last = await client.getParcel(parcelId, reqOptions);
148
+ options.onUpdate?.(last);
149
+ if (isTerminalParcelStatus(last.status)) return last;
150
+ if (stopOnAction && last.status === "action_required") return last;
151
+ if (Date.now() - startedAt >= deadlineMs) {
152
+ options.onTimeout?.(last);
153
+ return last;
154
+ }
155
+ const wait = applyJitter(intervalMs, jitter);
156
+ const remaining = deadlineMs - (Date.now() - startedAt);
157
+ await sleep(Math.min(wait, Math.max(remaining, 0)), options.signal);
158
+ }
159
+ }
160
+ function applyJitter(baseMs, jitter) {
161
+ if (jitter <= 0) return baseMs;
162
+ const delta = baseMs * jitter;
163
+ return Math.round(baseMs - delta + Math.random() * 2 * delta);
164
+ }
165
+ function sleep(ms, signal) {
166
+ return new Promise((resolve) => {
167
+ if (ms <= 0) return resolve();
168
+ const timer = setTimeout(() => {
169
+ signal?.removeEventListener("abort", onAbort);
170
+ resolve();
171
+ }, ms);
172
+ const onAbort = () => {
173
+ clearTimeout(timer);
174
+ resolve();
175
+ };
176
+ signal?.addEventListener("abort", onAbort, { once: true });
177
+ });
178
+ }
179
+
180
+ // src/index.ts
181
+ export * from "@mitumbadotafrica/logistics-contract";
182
+ export {
183
+ DEFAULT_BASE_URL,
184
+ HttpClient,
185
+ LogisticsError,
186
+ MitumbaLogistics,
187
+ pollParcel
188
+ };
package/package.json CHANGED
@@ -1,6 +1,58 @@
1
1
  {
2
2
  "name": "@mitumbadotafrica/logistics",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.0.0",
4
+ "description": "Asset-light carrier-orchestration SDK — carrier-neutral headless client for Kenyan couriers. Zero UI, zero carrier knowledge.",
5
+ "license": "MIT",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "git+https://github.com/Mitumba-Ltd/mitumba-logistics.git",
9
+ "directory": "packages/core"
10
+ },
11
+ "homepage": "https://github.com/Mitumba-Ltd/mitumba-logistics/tree/main/packages/core#readme",
12
+ "bugs": {
13
+ "url": "https://github.com/Mitumba-Ltd/mitumba-logistics/issues"
14
+ },
15
+ "keywords": [
16
+ "logistics",
17
+ "shipping",
18
+ "courier",
19
+ "kenya",
20
+ "africa",
21
+ "pickup-mtaani",
22
+ "sdk"
23
+ ],
24
+ "type": "module",
25
+ "main": "./dist/index.cjs",
26
+ "module": "./dist/index.js",
27
+ "types": "./dist/index.d.ts",
28
+ "exports": {
29
+ ".": {
30
+ "types": "./dist/index.d.ts",
31
+ "import": "./dist/index.js",
32
+ "require": "./dist/index.cjs"
33
+ }
34
+ },
35
+ "files": [
36
+ "dist"
37
+ ],
38
+ "sideEffects": false,
39
+ "publishConfig": {
40
+ "access": "public",
41
+ "provenance": true
42
+ },
43
+ "scripts": {
44
+ "build": "tsup src/index.ts --format esm,cjs --dts --clean",
45
+ "dev": "tsup src/index.ts --format esm,cjs --dts --watch",
46
+ "typecheck": "tsc --noEmit",
47
+ "test": "vitest run",
48
+ "lint": "eslint src",
49
+ "clean": "rimraf dist .turbo"
50
+ },
51
+ "dependencies": {
52
+ "@mitumbadotafrica/logistics-contract": "0.0.0"
53
+ },
54
+ "devDependencies": {
55
+ "rimraf": "^6.0.1",
56
+ "vitest": "^2.1.1"
57
+ }
58
+ }
package/README.md DELETED
@@ -1,3 +0,0 @@
1
- # Temporary Holding Version
2
-
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.