@mitumba/sdk 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,166 @@
1
+ # @mitumba/sdk
2
+
3
+ The official isomorphic TypeScript SDK for the Mitumba marketplace platform.
4
+
5
+ [![CI](https://github.com/Mitumba-Ltd/mitumba-sdk/actions/workflows/ci.yml/badge.svg)](https://github.com/Mitumba-Ltd/mitumba-sdk/actions/workflows/ci.yml)
6
+ [![npm version](https://img.shields.io/npm/v/@mitumba/sdk.svg)](https://www.npmjs.com/package/@mitumba/sdk)
7
+
8
+ ## Features
9
+
10
+ - **Perfectly Typed**: 100% written in strict TypeScript. Zero `any` types.
11
+ - **Isomorphic**: Runs natively in Node.js, Cloudflare Workers, Next.js, and the Browser using the native `fetch` API.
12
+ - **Zero Dependencies**: Lightweight and extremely fast.
13
+ - **Auto-Token Rotation**: Automatically handles refresh tokens under the hood.
14
+
15
+ ## Installation
16
+
17
+ ```bash
18
+ npm install @mitumba/sdk
19
+ # or
20
+ yarn add @mitumba/sdk
21
+ # or
22
+ pnpm add @mitumba/sdk
23
+ ```
24
+
25
+ ## Quick Start
26
+
27
+ Initialize the client with the environment's base URL:
28
+
29
+ ```typescript
30
+ import { MitumbaClient } from '@mitumba/sdk'
31
+
32
+ const mitumba = new MitumbaClient({
33
+ baseUrl: 'https://api.mitumba.stanl.ink'
34
+ })
35
+ ```
36
+
37
+ If you already have a token (e.g. from local storage), pass it to the client:
38
+
39
+ ```typescript
40
+ mitumba.setToken('eyJhbGciOiJIUzI1...', 'my-refresh-token-hex')
41
+ ```
42
+
43
+ ## API Overview
44
+
45
+ The SDK is split into 5 core modules representing the marketplace domains.
46
+
47
+ ### 1. Auth Module (`mitumba.auth`)
48
+
49
+ Mitumba supports dual-mode authentication: Email + Password, or Phone OTP (SMS). The SDK strongly types both inputs.
50
+
51
+ ```typescript
52
+ // Email Login
53
+ const tokens = await mitumba.auth.login({
54
+ email: 'user@example.com',
55
+ password: 'password123'
56
+ })
57
+
58
+ // OTP Login
59
+ const { message } = await mitumba.auth.login({ phone: '+254700000000' })
60
+ const tokens = await mitumba.auth.verifyOtp({ phone: '+254700000000', code: '123456' })
61
+
62
+ // Provide the tokens to the client to authenticate future requests
63
+ mitumba.setToken(tokens.access_token, tokens.refresh_token)
64
+ ```
65
+
66
+ ### 2. Listings Module (`mitumba.listings`)
67
+
68
+ Browse, search, and manage inventory.
69
+
70
+ ```typescript
71
+ // Fetch the marketplace feed with filters
72
+ const feed = await mitumba.listings.getFeed({
73
+ city_id: 'nbi_01',
74
+ condition: 'like_new',
75
+ sort: 'recency'
76
+ })
77
+
78
+ // Create a new listing (requires seller role)
79
+ const listing = await mitumba.listings.create({
80
+ title: 'Vintage Denim Jacket',
81
+ category_id: 'cat_outerwear',
82
+ city_id: 'nbi_01',
83
+ price: 2500,
84
+ condition: 'good'
85
+ })
86
+ ```
87
+
88
+ ### 3. Search Module (`mitumba.search`)
89
+
90
+ AI-powered full-text search.
91
+
92
+ ```typescript
93
+ // Search with ranking
94
+ const results = await mitumba.search.search({
95
+ q: 'vintage jacket',
96
+ sort: 'relevance'
97
+ })
98
+
99
+ // Get trending search terms
100
+ const trending = await mitumba.search.getTrending('nbi_01')
101
+ ```
102
+
103
+ ### 4. Orders & Pay Modules (`mitumba.orders`, `mitumba.pay`)
104
+
105
+ End-to-end checkout and M-Pesa integration.
106
+
107
+ ```typescript
108
+ // 1. Create an order
109
+ const { order_id, total } = await mitumba.orders.create({ listing_id: 'lst_123' })
110
+
111
+ // 2. Initiate M-Pesa STK Push
112
+ await mitumba.pay.initiateStkPush({ order_id, phone: '+254700000000' })
113
+
114
+ // 3. Poll for payment status
115
+ const status = await mitumba.pay.getStatus(order_id)
116
+ ```
117
+
118
+ ### 5. Vazi Module (`mitumba.vazi`)
119
+
120
+ AI-assembled outfit feeds.
121
+
122
+ ```typescript
123
+ // Browse the curated outfits feed
124
+ const feed = await mitumba.vazi.getFeed({ limit: 10, offset: 0 })
125
+
126
+ // Get a full outfit built around a specific seed item
127
+ const { outfits } = await mitumba.vazi.completeOutfit('lst_123')
128
+ ```
129
+
130
+ ## Error Handling
131
+
132
+ All API errors are wrapped in a standard `APIError` object.
133
+
134
+ ```typescript
135
+ import { APIError } from '@mitumba/sdk'
136
+
137
+ try {
138
+ await mitumba.auth.login({ email: 'bad', password: 'bad' })
139
+ } catch (error) {
140
+ if (error instanceof APIError) {
141
+ console.error(error.code) // e.g. "invalid_credentials"
142
+ console.error(error.status) // e.g. 401
143
+ console.error(error.message) // "Wrong email or password"
144
+ }
145
+ }
146
+ ```
147
+
148
+ ## License
149
+
150
+ MIT
151
+
152
+ ---
153
+
154
+ ## Contributing & Releasing
155
+
156
+ We use [Changesets](https://github.com/changesets/changesets) for automated versioning and changelog generation.
157
+
158
+ When submitting a PR that requires a package version bump, run:
159
+
160
+ ```bash
161
+ npx changeset
162
+ ```
163
+
164
+ Select the appropriate version bump (`patch`, `minor`, `major`) and provide a description of your changes. Commit the generated markdown file along with your PR.
165
+
166
+ When your PR is merged, the automated workflow will handle updating the version, aggregating the changelog, and publishing to NPM.
@@ -0,0 +1,456 @@
1
+ interface AuthTokens {
2
+ access_token: string;
3
+ refresh_token: string;
4
+ expires_in: number;
5
+ }
6
+ interface MessageResponse {
7
+ message: string;
8
+ }
9
+ interface EmailRegisterInput {
10
+ email: string;
11
+ password: string;
12
+ display_name?: string;
13
+ }
14
+ interface PhoneRegisterInput {
15
+ phone: string;
16
+ }
17
+ type RegisterInput = EmailRegisterInput | PhoneRegisterInput;
18
+ interface EmailLoginInput {
19
+ email: string;
20
+ password: string;
21
+ }
22
+ interface PhoneLoginInput {
23
+ phone: string;
24
+ }
25
+ type LoginInput = EmailLoginInput | PhoneLoginInput;
26
+ interface SendOtpInput {
27
+ phone: string;
28
+ }
29
+ interface VerifyOtpInput {
30
+ phone: string;
31
+ code: string;
32
+ }
33
+
34
+ declare const CONDITIONS: readonly ["new", "like_new", "good", "fair"];
35
+ type Condition = typeof CONDITIONS[number];
36
+ declare const LISTING_STATUSES: readonly ["draft", "active", "sold", "removed"];
37
+ type ListingStatus = typeof LISTING_STATUSES[number];
38
+ interface ListingImage {
39
+ id: string;
40
+ listing_id: string;
41
+ url: string;
42
+ position: number;
43
+ created_at: string;
44
+ }
45
+ interface SellerProfile {
46
+ id: string;
47
+ sti_score: number;
48
+ verification_status: string;
49
+ seller_type: 'individual' | 'bale';
50
+ }
51
+ interface Listing {
52
+ id: string;
53
+ seller_id: string;
54
+ title: string;
55
+ description: string | null;
56
+ category_id: string;
57
+ city_id: string;
58
+ price: number;
59
+ condition: Condition;
60
+ status: ListingStatus;
61
+ photo_verified: boolean;
62
+ vazi_eligible: boolean;
63
+ created_at: string;
64
+ updated_at: string;
65
+ sti_score: number;
66
+ verification_status: string;
67
+ seller_type: 'individual' | 'bale';
68
+ images?: ListingImage[];
69
+ }
70
+ interface ListingsFeedParams {
71
+ city_id?: string;
72
+ category_id?: string;
73
+ min_price?: number;
74
+ max_price?: number;
75
+ condition?: Condition;
76
+ sort?: 'recency' | 'price_asc' | 'price_desc';
77
+ page?: number;
78
+ page_size?: number;
79
+ }
80
+ interface CreateListingInput {
81
+ title: string;
82
+ description?: string;
83
+ category_id: string;
84
+ city_id: string;
85
+ price: number;
86
+ condition: Condition;
87
+ }
88
+ type UpdateListingInput = Partial<CreateListingInput>;
89
+ interface Category {
90
+ id: string;
91
+ name: string;
92
+ slug: string;
93
+ }
94
+ interface City {
95
+ id: string;
96
+ name: string;
97
+ delivery_fee: number;
98
+ }
99
+ interface SellerStorefront {
100
+ seller: SellerProfile;
101
+ listings: Listing[];
102
+ total: number;
103
+ page: number;
104
+ page_size: number;
105
+ has_more: boolean;
106
+ }
107
+ interface PresignImageResponse {
108
+ upload_url: string;
109
+ image_id: string;
110
+ }
111
+
112
+ interface SearchParams {
113
+ q: string;
114
+ city_id?: string;
115
+ category_id?: string;
116
+ min_price?: number;
117
+ max_price?: number;
118
+ condition?: Condition;
119
+ sort?: 'relevance' | 'recency' | 'price_asc' | 'price_desc' | 'sti';
120
+ page?: number;
121
+ page_size?: number;
122
+ }
123
+ interface SearchResult extends Omit<Listing, 'images'> {
124
+ rank: number;
125
+ }
126
+ interface TrendingTerm {
127
+ term: string;
128
+ count: number;
129
+ }
130
+
131
+ declare const ORDER_STATUSES: readonly ["created", "payment_pending", "paid", "seller_confirmed", "shipped", "delivered", "completed", "cancelled", "disputed"];
132
+ type OrderStatus = typeof ORDER_STATUSES[number];
133
+ interface OrderEvent {
134
+ id: string;
135
+ order_id: string;
136
+ actor: string;
137
+ old_status: string;
138
+ new_status: string;
139
+ note: string | null;
140
+ created_at: string;
141
+ }
142
+ interface Order {
143
+ id: string;
144
+ buyer_id: string;
145
+ seller_id: string;
146
+ listing_id: string;
147
+ amount: number;
148
+ delivery_fee: number;
149
+ total: number;
150
+ status: OrderStatus;
151
+ city_id: string;
152
+ created_at: string;
153
+ updated_at: string;
154
+ events?: OrderEvent[];
155
+ }
156
+ interface CreateOrderInput {
157
+ listing_id: string;
158
+ }
159
+ interface TransitionOrderInput {
160
+ status: OrderStatus;
161
+ note?: string;
162
+ }
163
+ interface OrderHistoryParams {
164
+ role?: 'buyer' | 'seller';
165
+ page?: number;
166
+ }
167
+
168
+ interface StkPushInput {
169
+ order_id: string;
170
+ phone: string;
171
+ }
172
+ interface StkPushResponse {
173
+ payment_id: string;
174
+ provider: string;
175
+ }
176
+ declare const PAYMENT_STATUSES: readonly ["initiated", "funded", "failed", "refunded", "cancelled"];
177
+ type PaymentStatus = typeof PAYMENT_STATUSES[number];
178
+ interface PaymentStatusResponse {
179
+ id: string;
180
+ status: PaymentStatus;
181
+ total: number;
182
+ }
183
+
184
+ declare const GARMENT_TYPES: readonly ["top", "bottom", "shoes", "accessory", "dress", "outerwear", "bag", "kids"];
185
+ type GarmentType = typeof GARMENT_TYPES[number];
186
+ interface VAZIOutfitItem {
187
+ listing_id: string;
188
+ garment_type: GarmentType;
189
+ price_kes: number;
190
+ seller_id: string;
191
+ seller_sti: number;
192
+ seller_city: string;
193
+ image_url: string | null;
194
+ is_seed: boolean;
195
+ final_score: number;
196
+ }
197
+ interface VAZIOutfit {
198
+ id: string;
199
+ name: string;
200
+ items: VAZIOutfitItem[];
201
+ total_price_kes: number;
202
+ sellers_count: number;
203
+ is_multi_city: boolean;
204
+ assembled_at: string;
205
+ }
206
+ interface VaziFeedParams {
207
+ limit?: number;
208
+ offset?: number;
209
+ }
210
+ interface VaziFeedResponse {
211
+ outfits: VAZIOutfit[];
212
+ total: number;
213
+ limit: number;
214
+ offset: number;
215
+ }
216
+
217
+ interface MitumbaClientConfig {
218
+ baseUrl: string;
219
+ debug?: boolean;
220
+ maxRetries?: number;
221
+ /**
222
+ * Optional access token for authenticated requests.
223
+ */
224
+ token?: string;
225
+ /**
226
+ * Optional refresh token. If provided, the client can automatically refresh
227
+ * the access token when encountering a 401 response.
228
+ */
229
+ refreshToken?: string;
230
+ /**
231
+ * Callback invoked when the token is automatically refreshed.
232
+ * Useful for persisting the new tokens.
233
+ */
234
+ onTokenRefresh?: (tokens: {
235
+ token: string;
236
+ refreshToken: string;
237
+ }) => void;
238
+ }
239
+ interface APIErrorResponse {
240
+ error: string;
241
+ message?: string;
242
+ details?: unknown;
243
+ }
244
+ interface PaginatedResponse<T> {
245
+ data: T[];
246
+ total: number;
247
+ page: number;
248
+ page_size: number;
249
+ has_more: boolean;
250
+ }
251
+
252
+ interface RequestOptions {
253
+ signal?: AbortSignal;
254
+ }
255
+
256
+ declare class APIError extends Error {
257
+ readonly code: string;
258
+ readonly status: number;
259
+ readonly details?: unknown;
260
+ constructor(status: number, data: APIErrorResponse);
261
+ }
262
+ declare class APIClient {
263
+ private config;
264
+ private isRefreshing;
265
+ private refreshPromise;
266
+ constructor(config: MitumbaClientConfig);
267
+ setToken(token: string, refreshToken?: string): void;
268
+ clearToken(): void;
269
+ private request;
270
+ private handleTokenRefresh;
271
+ get<T>(path: string, params?: Record<string, string | number | boolean | undefined>, options?: RequestOptions): Promise<T>;
272
+ post<T>(path: string, body?: unknown, options?: RequestOptions): Promise<T>;
273
+ put<T>(path: string, body?: unknown, options?: RequestOptions): Promise<T>;
274
+ patch<T>(path: string, body?: unknown, options?: RequestOptions): Promise<T>;
275
+ delete<T>(path: string, options?: RequestOptions): Promise<T>;
276
+ }
277
+
278
+ declare class AuthModule {
279
+ private readonly client;
280
+ constructor(client: APIClient);
281
+ /**
282
+ * Register a new account.
283
+ * If using EmailRegisterInput, returns AuthTokens.
284
+ * If using PhoneRegisterInput, returns MessageResponse (OTP sent).
285
+ */
286
+ register(input: RegisterInput, options?: RequestOptions): Promise<AuthTokens | MessageResponse>;
287
+ /**
288
+ * Log in to an existing account.
289
+ * If using EmailLoginInput, returns AuthTokens.
290
+ * If using PhoneLoginInput, returns MessageResponse (OTP sent).
291
+ */
292
+ login(input: LoginInput, options?: RequestOptions): Promise<AuthTokens | MessageResponse>;
293
+ /**
294
+ * Send an OTP code to a phone number.
295
+ */
296
+ sendOtp(input: SendOtpInput, options?: RequestOptions): Promise<MessageResponse>;
297
+ /**
298
+ * Verify an OTP code.
299
+ */
300
+ verifyOtp(input: VerifyOtpInput, options?: RequestOptions): Promise<AuthTokens>;
301
+ /**
302
+ * Refresh the access token using a refresh token.
303
+ */
304
+ refresh(refreshToken: string, options?: RequestOptions): Promise<AuthTokens>;
305
+ /**
306
+ * Revoke the refresh token and log out.
307
+ */
308
+ logout(refreshToken: string, options?: RequestOptions): Promise<{
309
+ ok: boolean;
310
+ }>;
311
+ }
312
+
313
+ declare class ListingsModule {
314
+ private readonly client;
315
+ constructor(client: APIClient);
316
+ /**
317
+ * Browse the marketplace feed with optional filters.
318
+ */
319
+ getFeed(params?: ListingsFeedParams, options?: RequestOptions): Promise<PaginatedResponse<Listing>>;
320
+ /**
321
+ * Get full details of a single listing, including its images.
322
+ */
323
+ getById(id: string, options?: RequestOptions): Promise<Listing>;
324
+ /**
325
+ * Create a new listing (requires seller role).
326
+ */
327
+ create(input: CreateListingInput, options?: RequestOptions): Promise<Listing>;
328
+ /**
329
+ * Update an existing listing.
330
+ */
331
+ update(id: string, input: UpdateListingInput, options?: RequestOptions): Promise<Listing>;
332
+ /**
333
+ * Change the status of a listing.
334
+ */
335
+ updateStatus(id: string, status: ListingStatus, options?: RequestOptions): Promise<Listing>;
336
+ /**
337
+ * Soft delete a listing (sets status to 'removed').
338
+ */
339
+ delete(id: string, options?: RequestOptions): Promise<{
340
+ ok: boolean;
341
+ }>;
342
+ /**
343
+ * Get a seller's public storefront.
344
+ */
345
+ getSellerStorefront(sellerId: string, params?: {
346
+ page?: number;
347
+ page_size?: number;
348
+ }, options?: RequestOptions): Promise<SellerStorefront>;
349
+ /**
350
+ * List all supported categories.
351
+ */
352
+ getCategories(options?: RequestOptions): Promise<Category[]>;
353
+ /**
354
+ * List all supported cities.
355
+ */
356
+ getCities(options?: RequestOptions): Promise<City[]>;
357
+ /**
358
+ * Get a presigned upload URL for a listing image.
359
+ * Index should be between 0 and 9.
360
+ */
361
+ presignImage(listingId: string, index: number, options?: RequestOptions): Promise<PresignImageResponse>;
362
+ }
363
+
364
+ declare class SearchModule {
365
+ private readonly client;
366
+ constructor(client: APIClient);
367
+ /**
368
+ * Perform a full-text search with optional filters.
369
+ */
370
+ search(params: SearchParams, options?: RequestOptions): Promise<PaginatedResponse<SearchResult>>;
371
+ /**
372
+ * Get trending search terms.
373
+ */
374
+ getTrending(cityId?: string, options?: RequestOptions): Promise<{
375
+ terms: TrendingTerm[];
376
+ }>;
377
+ }
378
+
379
+ declare class OrdersModule {
380
+ private readonly client;
381
+ constructor(client: APIClient);
382
+ /**
383
+ * Create a new order from a listing.
384
+ */
385
+ create(input: CreateOrderInput, options?: RequestOptions): Promise<{
386
+ order_id: string;
387
+ total: number;
388
+ delivery_fee: number;
389
+ }>;
390
+ /**
391
+ * Get full details of an order, including its event timeline.
392
+ */
393
+ getById(id: string, options?: RequestOptions): Promise<Order>;
394
+ /**
395
+ * Transition the status of an order.
396
+ */
397
+ transition(id: string, input: TransitionOrderInput, options?: RequestOptions): Promise<Order>;
398
+ /**
399
+ * Get the order history for the current authenticated user.
400
+ */
401
+ getHistory(params?: OrderHistoryParams, options?: RequestOptions): Promise<{
402
+ data: Order[];
403
+ page: number;
404
+ page_size: number;
405
+ }>;
406
+ }
407
+
408
+ declare class PayModule {
409
+ private readonly client;
410
+ constructor(client: APIClient);
411
+ /**
412
+ * Initiate an M-Pesa STK Push payment for an order.
413
+ */
414
+ initiateStkPush(input: StkPushInput, options?: RequestOptions): Promise<StkPushResponse>;
415
+ /**
416
+ * Poll for the current status of a payment by its order ID.
417
+ */
418
+ getStatus(orderId: string, options?: RequestOptions): Promise<PaymentStatusResponse>;
419
+ }
420
+
421
+ declare class VaziModule {
422
+ private readonly client;
423
+ constructor(client: APIClient);
424
+ /**
425
+ * Browse the AI-curated outfit feed.
426
+ */
427
+ getFeed(params?: VaziFeedParams, options?: RequestOptions): Promise<VaziFeedResponse>;
428
+ /**
429
+ * Get a complete outfit built around a specific seed listing.
430
+ */
431
+ completeOutfit(listingId: string, options?: RequestOptions): Promise<{
432
+ outfits: VAZIOutfit[];
433
+ }>;
434
+ }
435
+
436
+ declare class MitumbaClient {
437
+ readonly api: APIClient;
438
+ readonly auth: AuthModule;
439
+ readonly listings: ListingsModule;
440
+ readonly search: SearchModule;
441
+ readonly orders: OrdersModule;
442
+ readonly pay: PayModule;
443
+ readonly vazi: VaziModule;
444
+ constructor(config: MitumbaClientConfig);
445
+ /**
446
+ * Set the access token for authenticated requests.
447
+ * Optionally pass a refresh token to enable automatic token rotation.
448
+ */
449
+ setToken(token: string, refreshToken?: string): void;
450
+ /**
451
+ * Clear the current tokens.
452
+ */
453
+ clearToken(): void;
454
+ }
455
+
456
+ export { APIClient, APIError, type APIErrorResponse, AuthModule, type AuthTokens, CONDITIONS, type Category, type City, type Condition, type CreateListingInput, type CreateOrderInput, type EmailLoginInput, type EmailRegisterInput, GARMENT_TYPES, type GarmentType, LISTING_STATUSES, type Listing, type ListingImage, type ListingStatus, type ListingsFeedParams, ListingsModule, type LoginInput, type MessageResponse, MitumbaClient, type MitumbaClientConfig, ORDER_STATUSES, type Order, type OrderEvent, type OrderHistoryParams, type OrderStatus, OrdersModule, PAYMENT_STATUSES, type PaginatedResponse, PayModule, type PaymentStatus, type PaymentStatusResponse, type PhoneLoginInput, type PhoneRegisterInput, type PresignImageResponse, type RegisterInput, type RequestOptions, SearchModule, type SearchParams, type SearchResult, type SellerProfile, type SellerStorefront, type SendOtpInput, type StkPushInput, type StkPushResponse, type TransitionOrderInput, type TrendingTerm, type UpdateListingInput, type VAZIOutfit, type VAZIOutfitItem, type VaziFeedParams, type VaziFeedResponse, VaziModule, type VerifyOtpInput };