@lonca/trendyol 0.1.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,59 @@
1
+ # @lonca/trendyol
2
+
3
+ Type-safe TypeScript SDK for the [Trendyol Marketplace API](https://developers.trendyol.com).
4
+
5
+ > ⚠️ **Alpha** — Only the `brands` resource is implemented in this initial scaffold. More endpoints (orders, products, inventory, webhooks) land in follow-up releases.
6
+
7
+ ## Install
8
+
9
+ ```bash
10
+ pnpm add @lonca/trendyol
11
+ ```
12
+
13
+ ## Quick start
14
+
15
+ ```ts
16
+ import { createTrendyolClient } from '@lonca/trendyol';
17
+ import { paginate } from '@lonca/core';
18
+
19
+ const client = createTrendyolClient({
20
+ sellerId: 12345,
21
+ apiKey: process.env.TRENDYOL_API_KEY!,
22
+ apiSecret: process.env.TRENDYOL_API_SECRET!,
23
+ env: 'stage', // or 'prod'
24
+ integratorName: 'MyCompany', // optional; defaults to 'SelfIntegration'
25
+ });
26
+
27
+ for await (const brand of paginate((p) => client.brands.list(p))) {
28
+ console.log(brand.id, brand.name);
29
+ }
30
+ ```
31
+
32
+ ## Authentication
33
+
34
+ Trendyol uses HTTP Basic Auth. Get your `sellerId`, `apiKey`, and `apiSecret` from the [Trendyol Partner Panel → Account Info → Integration Information](https://partner.trendyol.com/account/info?tab=integrationInformation) (master-user only).
35
+
36
+ **Production vs Stage** have different credentials. Stage also requires IP whitelisting — register your CI/server IP with Trendyol support.
37
+
38
+ ## Built-in robustness
39
+
40
+ - **Retry with exponential backoff** on 429 (respects `Retry-After`) and 5xx
41
+ - **Per-endpoint rate limiting** (token bucket) sized to Trendyol's documented limits
42
+ - **Structured errors** via `@lonca/core` (`AuthError`, `RateLimitError`, `NotFoundError`, `ServerError`, `ValidationError`, `NetworkError`, `TimeoutError`)
43
+ - **Correlation ID** auto-generated per request (`x-correlationid` header) for Trendyol-side log tracing
44
+ - **`AbortSignal` support** for cancellation throughout
45
+
46
+ ## Environments
47
+
48
+ | Env | Base URL | Notes |
49
+ | ------- | --------------------------------- | --------------------------------------------- |
50
+ | `prod` | `https://apigw.trendyol.com` | No IP whitelist |
51
+ | `stage` | `https://stageapigw.trendyol.com` | IP whitelist required — call Trendyol support |
52
+
53
+ ## Stability
54
+
55
+ `0.x` — alpha. Public APIs may change between minor versions until `1.0.0`.
56
+
57
+ ## License
58
+
59
+ MIT
package/dist/index.cjs ADDED
@@ -0,0 +1,249 @@
1
+ 'use strict';
2
+
3
+ var core = require('@lonca/core');
4
+ var crypto = require('crypto');
5
+
6
+ // src/resources/brands.ts
7
+ var DEFAULT_PAGE_SIZE = 1e3;
8
+ var BrandsResource = class {
9
+ constructor(transport, limiter) {
10
+ this.transport = transport;
11
+ this.limiter = limiter ?? new core.TokenBucketRateLimiter({ capacity: 50, intervalMs: 6e4 });
12
+ }
13
+ transport;
14
+ limiter;
15
+ /**
16
+ * List Trendyol brands, one page at a time.
17
+ *
18
+ * @example
19
+ * ```ts
20
+ * import { paginate } from '@lonca/core';
21
+ * for await (const brand of paginate((p) => client.brands.list(p))) {
22
+ * console.log(brand.id, brand.name);
23
+ * }
24
+ * ```
25
+ */
26
+ async list(params = {}) {
27
+ const page = params.cursor ? Number.parseInt(params.cursor, 10) : 0;
28
+ const size = params.limit ?? DEFAULT_PAGE_SIZE;
29
+ const data = await this.transport.request({
30
+ method: "GET",
31
+ path: "/sapigw/brands",
32
+ query: { page, size },
33
+ rateLimiter: this.limiter
34
+ });
35
+ const items = data.brands.map((b) => ({ id: String(b.id), name: b.name }));
36
+ const nextCursor = page + 1 < data.totalPages ? String(page + 1) : void 0;
37
+ return nextCursor !== void 0 ? { items, nextCursor } : { items };
38
+ }
39
+ };
40
+
41
+ // src/auth.ts
42
+ function buildAuthHeader(apiKey, apiSecret) {
43
+ const token = Buffer.from(`${apiKey}:${apiSecret}`, "utf8").toString("base64");
44
+ return `Basic ${token}`;
45
+ }
46
+ function buildUserAgent(sellerId, integratorName = "SelfIntegration") {
47
+ return `${sellerId} - ${integratorName}`;
48
+ }
49
+ function mapHttpError(status, body, retryAfterMs) {
50
+ const data = { body };
51
+ switch (status) {
52
+ case 401:
53
+ return new core.AuthError({
54
+ message: "Trendyol authentication failed (check apiKey / apiSecret)",
55
+ status,
56
+ data
57
+ });
58
+ case 403:
59
+ return new core.ValidationError({
60
+ message: "Trendyol forbidden (check User-Agent header or endpoint path)",
61
+ status,
62
+ data
63
+ });
64
+ case 404:
65
+ return new core.NotFoundError({ message: "Trendyol resource not found", status, data });
66
+ case 429:
67
+ return new core.RateLimitError({
68
+ message: "Trendyol rate limit exceeded",
69
+ status,
70
+ retryAfterMs,
71
+ data
72
+ });
73
+ case 500:
74
+ case 502:
75
+ case 503:
76
+ case 504:
77
+ return new core.ServerError({
78
+ message: `Trendyol server error (${status})`,
79
+ status,
80
+ data
81
+ });
82
+ default:
83
+ return new core.LoncaError({
84
+ code: "UNKNOWN",
85
+ message: `Trendyol unexpected response (${status})`,
86
+ status,
87
+ data,
88
+ retryable: false
89
+ });
90
+ }
91
+ }
92
+ function parseRetryAfter(header) {
93
+ if (!header) return void 0;
94
+ const seconds = Number(header);
95
+ if (!Number.isNaN(seconds)) return Math.max(0, seconds * 1e3);
96
+ const epoch = Date.parse(header);
97
+ if (!Number.isNaN(epoch)) return Math.max(0, epoch - Date.now());
98
+ return void 0;
99
+ }
100
+
101
+ // src/transport.ts
102
+ var BASE_URLS = {
103
+ prod: "https://apigw.trendyol.com",
104
+ stage: "https://stageapigw.trendyol.com"
105
+ };
106
+ var TrendyolTransport = class {
107
+ constructor(config) {
108
+ this.config = config;
109
+ this.baseUrl = BASE_URLS[config.env];
110
+ this.logger = config.logger ?? core.noopLogger;
111
+ this.timeoutMs = config.timeoutMs ?? 3e4;
112
+ this.fetchImpl = config.fetch ?? fetch;
113
+ }
114
+ config;
115
+ baseUrl;
116
+ logger;
117
+ timeoutMs;
118
+ fetchImpl;
119
+ async request(opts) {
120
+ return core.retry(
121
+ async (attempt) => {
122
+ if (opts.rateLimiter) await opts.rateLimiter.acquire(opts.signal);
123
+ const url = this.buildUrl(opts.path, opts.query);
124
+ const correlationId = crypto.randomUUID();
125
+ const headers = this.buildHeaders(correlationId);
126
+ const init = {
127
+ method: opts.method,
128
+ headers,
129
+ signal: this.composeSignal(opts.signal)
130
+ };
131
+ if (opts.body !== void 0 && opts.method !== "GET") {
132
+ init.body = JSON.stringify(opts.body);
133
+ }
134
+ this.logger.debug("trendyol.request", {
135
+ method: opts.method,
136
+ url,
137
+ correlationId,
138
+ attempt
139
+ });
140
+ let response;
141
+ try {
142
+ response = await this.fetchImpl(url, init);
143
+ } catch (err) {
144
+ if (err instanceof Error && err.name === "AbortError") {
145
+ throw new core.TimeoutError({
146
+ message: `Trendyol request timed out after ${this.timeoutMs}ms`,
147
+ cause: err
148
+ });
149
+ }
150
+ throw new core.NetworkError({
151
+ message: "Trendyol network failure",
152
+ cause: err
153
+ });
154
+ }
155
+ if (!response.ok) {
156
+ const body = await safeJson(response);
157
+ const retryAfterMs = parseRetryAfter(response.headers.get("retry-after"));
158
+ const error = mapHttpError(response.status, body, retryAfterMs);
159
+ this.logger.warn("trendyol.error", {
160
+ method: opts.method,
161
+ url,
162
+ correlationId,
163
+ status: response.status,
164
+ code: error.code,
165
+ retryable: error.retryable
166
+ });
167
+ throw error;
168
+ }
169
+ this.logger.debug("trendyol.response", {
170
+ correlationId,
171
+ status: response.status
172
+ });
173
+ if (response.status === 204) return void 0;
174
+ return await safeJson(response);
175
+ },
176
+ {
177
+ signal: opts.signal,
178
+ onRetry: (err, attempt, delay) => {
179
+ if (err instanceof core.LoncaError) {
180
+ this.logger.warn("trendyol.retry", {
181
+ attempt,
182
+ delayMs: delay,
183
+ code: err.code,
184
+ status: err.status
185
+ });
186
+ }
187
+ }
188
+ }
189
+ );
190
+ }
191
+ buildUrl(path, query) {
192
+ const url = new URL(path, this.baseUrl);
193
+ if (query) {
194
+ for (const [key, value] of Object.entries(query)) {
195
+ if (value === void 0) continue;
196
+ url.searchParams.set(key, String(value));
197
+ }
198
+ }
199
+ return url.toString();
200
+ }
201
+ buildHeaders(correlationId) {
202
+ const integratorName = this.config.integratorName ?? "SelfIntegration";
203
+ return {
204
+ Authorization: buildAuthHeader(this.config.apiKey, this.config.apiSecret),
205
+ "x-clientip": this.config.clientIp ?? "127.0.0.1",
206
+ "x-correlationid": correlationId,
207
+ "x-agentname": integratorName,
208
+ "User-Agent": buildUserAgent(this.config.sellerId, integratorName),
209
+ "Content-Type": "application/json",
210
+ Accept: "application/json"
211
+ };
212
+ }
213
+ composeSignal(external) {
214
+ const timeoutSignal = AbortSignal.timeout(this.timeoutMs);
215
+ if (!external) return timeoutSignal;
216
+ return AbortSignal.any([external, timeoutSignal]);
217
+ }
218
+ };
219
+ async function safeJson(response) {
220
+ const text = await response.text();
221
+ if (!text) return void 0;
222
+ try {
223
+ return JSON.parse(text);
224
+ } catch {
225
+ return text;
226
+ }
227
+ }
228
+
229
+ // src/client.ts
230
+ function createTrendyolClient(opts) {
231
+ const transport = new TrendyolTransport({
232
+ sellerId: opts.sellerId,
233
+ apiKey: opts.apiKey,
234
+ apiSecret: opts.apiSecret,
235
+ env: opts.env,
236
+ integratorName: opts.integratorName,
237
+ clientIp: opts.clientIp,
238
+ logger: opts.logger,
239
+ timeoutMs: opts.timeoutMs
240
+ });
241
+ return {
242
+ brands: new BrandsResource(transport)
243
+ };
244
+ }
245
+
246
+ exports.BrandsResource = BrandsResource;
247
+ exports.createTrendyolClient = createTrendyolClient;
248
+ //# sourceMappingURL=index.cjs.map
249
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/resources/brands.ts","../src/auth.ts","../src/errors.ts","../src/transport.ts","../src/client.ts"],"names":["TokenBucketRateLimiter","AuthError","ValidationError","NotFoundError","RateLimitError","ServerError","LoncaError","noopLogger","retry","randomUUID","TimeoutError","NetworkError"],"mappings":"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","file":"index.cjs","sourcesContent":["import { TokenBucketRateLimiter, type CursorPage, type CursorPaginationParams } from '@lonca/core';\nimport type { TrendyolTransport } from '../transport.js';\nimport type { Brand } from '../types/brand.js';\n\n/** Default page size when callers don't specify `limit`. Trendyol allows up to 1000. */\nconst DEFAULT_PAGE_SIZE = 1000;\n\ninterface TrendyolBrandListResponse {\n brands: Array<{ id: number; name: string }>;\n totalPages: number;\n totalElements: number;\n}\n\n/**\n * Trendyol brand-list endpoint group.\n *\n * Rate limit: 50 req/min (per Trendyol service limits).\n *\n * Trendyol uses page-based pagination internally; we expose the cursor-based\n * `CursorPage` shape from `@lonca/core` so callers can drive everything with\n * `paginate()` and stay consistent across Lonca SDKs.\n */\nexport class BrandsResource {\n private readonly limiter: TokenBucketRateLimiter;\n\n constructor(\n private readonly transport: TrendyolTransport,\n limiter?: TokenBucketRateLimiter,\n ) {\n this.limiter = limiter ?? new TokenBucketRateLimiter({ capacity: 50, intervalMs: 60_000 });\n }\n\n /**\n * List Trendyol brands, one page at a time.\n *\n * @example\n * ```ts\n * import { paginate } from '@lonca/core';\n * for await (const brand of paginate((p) => client.brands.list(p))) {\n * console.log(brand.id, brand.name);\n * }\n * ```\n */\n async list(params: CursorPaginationParams = {}): Promise<CursorPage<Brand>> {\n const page = params.cursor ? Number.parseInt(params.cursor, 10) : 0;\n const size = params.limit ?? DEFAULT_PAGE_SIZE;\n\n const data = await this.transport.request<TrendyolBrandListResponse>({\n method: 'GET',\n path: '/sapigw/brands',\n query: { page, size },\n rateLimiter: this.limiter,\n });\n\n const items: Brand[] = data.brands.map((b) => ({ id: String(b.id), name: b.name }));\n const nextCursor = page + 1 < data.totalPages ? String(page + 1) : undefined;\n\n return nextCursor !== undefined ? { items, nextCursor } : { items };\n }\n}\n","/** Build the HTTP Basic auth header value from Trendyol credentials. */\nexport function buildAuthHeader(apiKey: string, apiSecret: string): string {\n const token = Buffer.from(`${apiKey}:${apiSecret}`, 'utf8').toString('base64');\n return `Basic ${token}`;\n}\n\n/**\n * Build the `User-Agent` header value Trendyol expects.\n *\n * Per Trendyol's docs:\n * - Solo integrators: `\"<sellerId> - SelfIntegration\"`\n * - Integrator companies: `\"<sellerId> - <CompanyName>\"` (alphanumeric, max 30 chars)\n *\n * A missing or malformed `User-Agent` triggers HTTP 403.\n */\nexport function buildUserAgent(sellerId: number, integratorName = 'SelfIntegration'): string {\n return `${sellerId} - ${integratorName}`;\n}\n","import {\n AuthError,\n LoncaError,\n NotFoundError,\n RateLimitError,\n ServerError,\n ValidationError,\n} from '@lonca/core';\n\n/**\n * Map a Trendyol HTTP response status to a `@lonca/core` error.\n *\n * - `401` → `AuthError` (bad credentials)\n * - `403` → `ValidationError` (usually missing/malformed `User-Agent` header, or wrong endpoint path)\n * - `404` → `NotFoundError`\n * - `429` → `RateLimitError` (carries `retryAfterMs` if the server provided `Retry-After`)\n * - `5xx` → `ServerError` (retryable)\n * - other → `LoncaError` with code `UNKNOWN` (non-retryable)\n */\nexport function mapHttpError(status: number, body: unknown, retryAfterMs?: number): LoncaError {\n const data = { body } as Record<string, unknown>;\n switch (status) {\n case 401:\n return new AuthError({\n message: 'Trendyol authentication failed (check apiKey / apiSecret)',\n status,\n data,\n });\n case 403:\n return new ValidationError({\n message: 'Trendyol forbidden (check User-Agent header or endpoint path)',\n status,\n data,\n });\n case 404:\n return new NotFoundError({ message: 'Trendyol resource not found', status, data });\n case 429:\n return new RateLimitError({\n message: 'Trendyol rate limit exceeded',\n status,\n retryAfterMs,\n data,\n });\n case 500:\n case 502:\n case 503:\n case 504:\n return new ServerError({\n message: `Trendyol server error (${status})`,\n status,\n data,\n });\n default:\n return new LoncaError({\n code: 'UNKNOWN',\n message: `Trendyol unexpected response (${status})`,\n status,\n data,\n retryable: false,\n });\n }\n}\n\n/** Parse a `Retry-After` header value (seconds OR HTTP-date) into milliseconds. */\nexport function parseRetryAfter(header: string | null): number | undefined {\n if (!header) return undefined;\n const seconds = Number(header);\n if (!Number.isNaN(seconds)) return Math.max(0, seconds * 1000);\n const epoch = Date.parse(header);\n if (!Number.isNaN(epoch)) return Math.max(0, epoch - Date.now());\n return undefined;\n}\n","import { randomUUID } from 'node:crypto';\nimport {\n LoncaError,\n NetworkError,\n TimeoutError,\n retry,\n noopLogger,\n type Logger,\n type TokenBucketRateLimiter,\n} from '@lonca/core';\nimport { buildAuthHeader, buildUserAgent } from './auth.js';\nimport { mapHttpError, parseRetryAfter } from './errors.js';\n\nconst BASE_URLS = {\n prod: 'https://apigw.trendyol.com',\n stage: 'https://stageapigw.trendyol.com',\n} as const;\n\nexport type TrendyolEnvironment = keyof typeof BASE_URLS;\n\nexport interface TransportConfig {\n sellerId: number;\n apiKey: string;\n apiSecret: string;\n env: TrendyolEnvironment;\n integratorName?: string;\n clientIp?: string;\n logger?: Logger;\n /** Request timeout in ms. Default: 30_000. */\n timeoutMs?: number;\n /** Override the underlying `fetch` (tests inject a mock). */\n fetch?: typeof fetch;\n}\n\nexport interface RequestOptions {\n method: 'GET' | 'POST' | 'PUT' | 'DELETE';\n /** Path beginning with `/` (e.g., `/sapigw/brands`). */\n path: string;\n query?: Record<string, string | number | boolean | undefined>;\n body?: unknown;\n signal?: AbortSignal;\n /** Per-endpoint rate limiter; acquire one token before each attempt. */\n rateLimiter?: TokenBucketRateLimiter;\n}\n\nexport class TrendyolTransport {\n private readonly baseUrl: string;\n private readonly logger: Logger;\n private readonly timeoutMs: number;\n private readonly fetchImpl: typeof fetch;\n\n constructor(private readonly config: TransportConfig) {\n this.baseUrl = BASE_URLS[config.env];\n this.logger = config.logger ?? noopLogger;\n this.timeoutMs = config.timeoutMs ?? 30_000;\n this.fetchImpl = config.fetch ?? fetch;\n }\n\n async request<T>(opts: RequestOptions): Promise<T> {\n return retry(\n async (attempt) => {\n if (opts.rateLimiter) await opts.rateLimiter.acquire(opts.signal);\n\n const url = this.buildUrl(opts.path, opts.query);\n const correlationId = randomUUID();\n const headers = this.buildHeaders(correlationId);\n const init: RequestInit = {\n method: opts.method,\n headers,\n signal: this.composeSignal(opts.signal),\n };\n if (opts.body !== undefined && opts.method !== 'GET') {\n init.body = JSON.stringify(opts.body);\n }\n\n this.logger.debug('trendyol.request', {\n method: opts.method,\n url,\n correlationId,\n attempt,\n });\n\n let response: Response;\n try {\n response = await this.fetchImpl(url, init);\n } catch (err) {\n if (err instanceof Error && err.name === 'AbortError') {\n throw new TimeoutError({\n message: `Trendyol request timed out after ${this.timeoutMs}ms`,\n cause: err,\n });\n }\n throw new NetworkError({\n message: 'Trendyol network failure',\n cause: err,\n });\n }\n\n if (!response.ok) {\n const body = await safeJson(response);\n const retryAfterMs = parseRetryAfter(response.headers.get('retry-after'));\n const error = mapHttpError(response.status, body, retryAfterMs);\n this.logger.warn('trendyol.error', {\n method: opts.method,\n url,\n correlationId,\n status: response.status,\n code: error.code,\n retryable: error.retryable,\n });\n throw error;\n }\n\n this.logger.debug('trendyol.response', {\n correlationId,\n status: response.status,\n });\n\n if (response.status === 204) return undefined as T;\n return (await safeJson(response)) as T;\n },\n {\n signal: opts.signal,\n onRetry: (err, attempt, delay) => {\n if (err instanceof LoncaError) {\n this.logger.warn('trendyol.retry', {\n attempt,\n delayMs: delay,\n code: err.code,\n status: err.status,\n });\n }\n },\n },\n );\n }\n\n private buildUrl(path: string, query?: RequestOptions['query']): string {\n const url = new URL(path, this.baseUrl);\n if (query) {\n for (const [key, value] of Object.entries(query)) {\n if (value === undefined) continue;\n url.searchParams.set(key, String(value));\n }\n }\n return url.toString();\n }\n\n private buildHeaders(correlationId: string): Record<string, string> {\n const integratorName = this.config.integratorName ?? 'SelfIntegration';\n return {\n Authorization: buildAuthHeader(this.config.apiKey, this.config.apiSecret),\n 'x-clientip': this.config.clientIp ?? '127.0.0.1',\n 'x-correlationid': correlationId,\n 'x-agentname': integratorName,\n 'User-Agent': buildUserAgent(this.config.sellerId, integratorName),\n 'Content-Type': 'application/json',\n Accept: 'application/json',\n };\n }\n\n private composeSignal(external?: AbortSignal): AbortSignal {\n const timeoutSignal = AbortSignal.timeout(this.timeoutMs);\n if (!external) return timeoutSignal;\n return AbortSignal.any([external, timeoutSignal]);\n }\n}\n\nasync function safeJson(response: Response): Promise<unknown> {\n const text = await response.text();\n if (!text) return undefined;\n try {\n return JSON.parse(text);\n } catch {\n return text;\n }\n}\n","import type { Logger } from '@lonca/core';\nimport { BrandsResource } from './resources/brands.js';\nimport { TrendyolTransport, type TrendyolEnvironment } from './transport.js';\n\nexport interface CreateClientOptions {\n /** Trendyol seller (supplier) ID — visible in Partner Panel → Account Info. */\n sellerId: number;\n /** Trendyol API key. */\n apiKey: string;\n /** Trendyol API secret. */\n apiSecret: string;\n /** Which Trendyol environment to target. */\n env: TrendyolEnvironment;\n /**\n * Integrator company name to send in `User-Agent` / `x-agentname`.\n * Defaults to `'SelfIntegration'` if the seller owns the integration code.\n * Trendyol caps this at 30 alphanumeric characters.\n */\n integratorName?: string;\n /**\n * IPv4 address to send in `x-clientip`.\n * Defaults to `'127.0.0.1'` — Trendyol does not validate this against the\n * request origin, the header just has to be present and IPv4-shaped.\n */\n clientIp?: string;\n /** Optional structured logger (`@lonca/core` `Logger`). Defaults to no-op. */\n logger?: Logger;\n /** Request timeout in ms. Default: 30_000. */\n timeoutMs?: number;\n}\n\nexport interface TrendyolClient {\n brands: BrandsResource;\n}\n\n/**\n * Create a Trendyol Marketplace SDK client.\n *\n * @example\n * ```ts\n * import { createTrendyolClient } from '@lonca/trendyol';\n *\n * const client = createTrendyolClient({\n * sellerId: 12345,\n * apiKey: process.env.TRENDYOL_API_KEY!,\n * apiSecret: process.env.TRENDYOL_API_SECRET!,\n * env: 'stage',\n * });\n *\n * const page = await client.brands.list({ limit: 100 });\n * ```\n */\nexport function createTrendyolClient(opts: CreateClientOptions): TrendyolClient {\n const transport = new TrendyolTransport({\n sellerId: opts.sellerId,\n apiKey: opts.apiKey,\n apiSecret: opts.apiSecret,\n env: opts.env,\n integratorName: opts.integratorName,\n clientIp: opts.clientIp,\n logger: opts.logger,\n timeoutMs: opts.timeoutMs,\n });\n\n return {\n brands: new BrandsResource(transport),\n };\n}\n"]}
@@ -0,0 +1,130 @@
1
+ import { Logger, TokenBucketRateLimiter, CursorPaginationParams, CursorPage } from '@lonca/core';
2
+
3
+ declare const BASE_URLS: {
4
+ readonly prod: "https://apigw.trendyol.com";
5
+ readonly stage: "https://stageapigw.trendyol.com";
6
+ };
7
+ type TrendyolEnvironment = keyof typeof BASE_URLS;
8
+ interface TransportConfig {
9
+ sellerId: number;
10
+ apiKey: string;
11
+ apiSecret: string;
12
+ env: TrendyolEnvironment;
13
+ integratorName?: string;
14
+ clientIp?: string;
15
+ logger?: Logger;
16
+ /** Request timeout in ms. Default: 30_000. */
17
+ timeoutMs?: number;
18
+ /** Override the underlying `fetch` (tests inject a mock). */
19
+ fetch?: typeof fetch;
20
+ }
21
+ interface RequestOptions {
22
+ method: 'GET' | 'POST' | 'PUT' | 'DELETE';
23
+ /** Path beginning with `/` (e.g., `/sapigw/brands`). */
24
+ path: string;
25
+ query?: Record<string, string | number | boolean | undefined>;
26
+ body?: unknown;
27
+ signal?: AbortSignal;
28
+ /** Per-endpoint rate limiter; acquire one token before each attempt. */
29
+ rateLimiter?: TokenBucketRateLimiter;
30
+ }
31
+ declare class TrendyolTransport {
32
+ private readonly config;
33
+ private readonly baseUrl;
34
+ private readonly logger;
35
+ private readonly timeoutMs;
36
+ private readonly fetchImpl;
37
+ constructor(config: TransportConfig);
38
+ request<T>(opts: RequestOptions): Promise<T>;
39
+ private buildUrl;
40
+ private buildHeaders;
41
+ private composeSignal;
42
+ }
43
+
44
+ /**
45
+ * A Trendyol marketplace brand.
46
+ *
47
+ * Trendyol returns numeric IDs; we normalize to `string` to match the
48
+ * `@lonca/core` convention (string IDs across all Lonca SDKs).
49
+ */
50
+ interface Brand {
51
+ id: string;
52
+ name: string;
53
+ }
54
+
55
+ /**
56
+ * Trendyol brand-list endpoint group.
57
+ *
58
+ * Rate limit: 50 req/min (per Trendyol service limits).
59
+ *
60
+ * Trendyol uses page-based pagination internally; we expose the cursor-based
61
+ * `CursorPage` shape from `@lonca/core` so callers can drive everything with
62
+ * `paginate()` and stay consistent across Lonca SDKs.
63
+ */
64
+ declare class BrandsResource {
65
+ private readonly transport;
66
+ private readonly limiter;
67
+ constructor(transport: TrendyolTransport, limiter?: TokenBucketRateLimiter);
68
+ /**
69
+ * List Trendyol brands, one page at a time.
70
+ *
71
+ * @example
72
+ * ```ts
73
+ * import { paginate } from '@lonca/core';
74
+ * for await (const brand of paginate((p) => client.brands.list(p))) {
75
+ * console.log(brand.id, brand.name);
76
+ * }
77
+ * ```
78
+ */
79
+ list(params?: CursorPaginationParams): Promise<CursorPage<Brand>>;
80
+ }
81
+
82
+ interface CreateClientOptions {
83
+ /** Trendyol seller (supplier) ID — visible in Partner Panel → Account Info. */
84
+ sellerId: number;
85
+ /** Trendyol API key. */
86
+ apiKey: string;
87
+ /** Trendyol API secret. */
88
+ apiSecret: string;
89
+ /** Which Trendyol environment to target. */
90
+ env: TrendyolEnvironment;
91
+ /**
92
+ * Integrator company name to send in `User-Agent` / `x-agentname`.
93
+ * Defaults to `'SelfIntegration'` if the seller owns the integration code.
94
+ * Trendyol caps this at 30 alphanumeric characters.
95
+ */
96
+ integratorName?: string;
97
+ /**
98
+ * IPv4 address to send in `x-clientip`.
99
+ * Defaults to `'127.0.0.1'` — Trendyol does not validate this against the
100
+ * request origin, the header just has to be present and IPv4-shaped.
101
+ */
102
+ clientIp?: string;
103
+ /** Optional structured logger (`@lonca/core` `Logger`). Defaults to no-op. */
104
+ logger?: Logger;
105
+ /** Request timeout in ms. Default: 30_000. */
106
+ timeoutMs?: number;
107
+ }
108
+ interface TrendyolClient {
109
+ brands: BrandsResource;
110
+ }
111
+ /**
112
+ * Create a Trendyol Marketplace SDK client.
113
+ *
114
+ * @example
115
+ * ```ts
116
+ * import { createTrendyolClient } from '@lonca/trendyol';
117
+ *
118
+ * const client = createTrendyolClient({
119
+ * sellerId: 12345,
120
+ * apiKey: process.env.TRENDYOL_API_KEY!,
121
+ * apiSecret: process.env.TRENDYOL_API_SECRET!,
122
+ * env: 'stage',
123
+ * });
124
+ *
125
+ * const page = await client.brands.list({ limit: 100 });
126
+ * ```
127
+ */
128
+ declare function createTrendyolClient(opts: CreateClientOptions): TrendyolClient;
129
+
130
+ export { type Brand, BrandsResource, type CreateClientOptions, type TrendyolClient, type TrendyolEnvironment, createTrendyolClient };
@@ -0,0 +1,130 @@
1
+ import { Logger, TokenBucketRateLimiter, CursorPaginationParams, CursorPage } from '@lonca/core';
2
+
3
+ declare const BASE_URLS: {
4
+ readonly prod: "https://apigw.trendyol.com";
5
+ readonly stage: "https://stageapigw.trendyol.com";
6
+ };
7
+ type TrendyolEnvironment = keyof typeof BASE_URLS;
8
+ interface TransportConfig {
9
+ sellerId: number;
10
+ apiKey: string;
11
+ apiSecret: string;
12
+ env: TrendyolEnvironment;
13
+ integratorName?: string;
14
+ clientIp?: string;
15
+ logger?: Logger;
16
+ /** Request timeout in ms. Default: 30_000. */
17
+ timeoutMs?: number;
18
+ /** Override the underlying `fetch` (tests inject a mock). */
19
+ fetch?: typeof fetch;
20
+ }
21
+ interface RequestOptions {
22
+ method: 'GET' | 'POST' | 'PUT' | 'DELETE';
23
+ /** Path beginning with `/` (e.g., `/sapigw/brands`). */
24
+ path: string;
25
+ query?: Record<string, string | number | boolean | undefined>;
26
+ body?: unknown;
27
+ signal?: AbortSignal;
28
+ /** Per-endpoint rate limiter; acquire one token before each attempt. */
29
+ rateLimiter?: TokenBucketRateLimiter;
30
+ }
31
+ declare class TrendyolTransport {
32
+ private readonly config;
33
+ private readonly baseUrl;
34
+ private readonly logger;
35
+ private readonly timeoutMs;
36
+ private readonly fetchImpl;
37
+ constructor(config: TransportConfig);
38
+ request<T>(opts: RequestOptions): Promise<T>;
39
+ private buildUrl;
40
+ private buildHeaders;
41
+ private composeSignal;
42
+ }
43
+
44
+ /**
45
+ * A Trendyol marketplace brand.
46
+ *
47
+ * Trendyol returns numeric IDs; we normalize to `string` to match the
48
+ * `@lonca/core` convention (string IDs across all Lonca SDKs).
49
+ */
50
+ interface Brand {
51
+ id: string;
52
+ name: string;
53
+ }
54
+
55
+ /**
56
+ * Trendyol brand-list endpoint group.
57
+ *
58
+ * Rate limit: 50 req/min (per Trendyol service limits).
59
+ *
60
+ * Trendyol uses page-based pagination internally; we expose the cursor-based
61
+ * `CursorPage` shape from `@lonca/core` so callers can drive everything with
62
+ * `paginate()` and stay consistent across Lonca SDKs.
63
+ */
64
+ declare class BrandsResource {
65
+ private readonly transport;
66
+ private readonly limiter;
67
+ constructor(transport: TrendyolTransport, limiter?: TokenBucketRateLimiter);
68
+ /**
69
+ * List Trendyol brands, one page at a time.
70
+ *
71
+ * @example
72
+ * ```ts
73
+ * import { paginate } from '@lonca/core';
74
+ * for await (const brand of paginate((p) => client.brands.list(p))) {
75
+ * console.log(brand.id, brand.name);
76
+ * }
77
+ * ```
78
+ */
79
+ list(params?: CursorPaginationParams): Promise<CursorPage<Brand>>;
80
+ }
81
+
82
+ interface CreateClientOptions {
83
+ /** Trendyol seller (supplier) ID — visible in Partner Panel → Account Info. */
84
+ sellerId: number;
85
+ /** Trendyol API key. */
86
+ apiKey: string;
87
+ /** Trendyol API secret. */
88
+ apiSecret: string;
89
+ /** Which Trendyol environment to target. */
90
+ env: TrendyolEnvironment;
91
+ /**
92
+ * Integrator company name to send in `User-Agent` / `x-agentname`.
93
+ * Defaults to `'SelfIntegration'` if the seller owns the integration code.
94
+ * Trendyol caps this at 30 alphanumeric characters.
95
+ */
96
+ integratorName?: string;
97
+ /**
98
+ * IPv4 address to send in `x-clientip`.
99
+ * Defaults to `'127.0.0.1'` — Trendyol does not validate this against the
100
+ * request origin, the header just has to be present and IPv4-shaped.
101
+ */
102
+ clientIp?: string;
103
+ /** Optional structured logger (`@lonca/core` `Logger`). Defaults to no-op. */
104
+ logger?: Logger;
105
+ /** Request timeout in ms. Default: 30_000. */
106
+ timeoutMs?: number;
107
+ }
108
+ interface TrendyolClient {
109
+ brands: BrandsResource;
110
+ }
111
+ /**
112
+ * Create a Trendyol Marketplace SDK client.
113
+ *
114
+ * @example
115
+ * ```ts
116
+ * import { createTrendyolClient } from '@lonca/trendyol';
117
+ *
118
+ * const client = createTrendyolClient({
119
+ * sellerId: 12345,
120
+ * apiKey: process.env.TRENDYOL_API_KEY!,
121
+ * apiSecret: process.env.TRENDYOL_API_SECRET!,
122
+ * env: 'stage',
123
+ * });
124
+ *
125
+ * const page = await client.brands.list({ limit: 100 });
126
+ * ```
127
+ */
128
+ declare function createTrendyolClient(opts: CreateClientOptions): TrendyolClient;
129
+
130
+ export { type Brand, BrandsResource, type CreateClientOptions, type TrendyolClient, type TrendyolEnvironment, createTrendyolClient };
package/dist/index.js ADDED
@@ -0,0 +1,246 @@
1
+ import { TokenBucketRateLimiter, noopLogger, retry, TimeoutError, NetworkError, LoncaError, ServerError, RateLimitError, NotFoundError, ValidationError, AuthError } from '@lonca/core';
2
+ import { randomUUID } from 'crypto';
3
+
4
+ // src/resources/brands.ts
5
+ var DEFAULT_PAGE_SIZE = 1e3;
6
+ var BrandsResource = class {
7
+ constructor(transport, limiter) {
8
+ this.transport = transport;
9
+ this.limiter = limiter ?? new TokenBucketRateLimiter({ capacity: 50, intervalMs: 6e4 });
10
+ }
11
+ transport;
12
+ limiter;
13
+ /**
14
+ * List Trendyol brands, one page at a time.
15
+ *
16
+ * @example
17
+ * ```ts
18
+ * import { paginate } from '@lonca/core';
19
+ * for await (const brand of paginate((p) => client.brands.list(p))) {
20
+ * console.log(brand.id, brand.name);
21
+ * }
22
+ * ```
23
+ */
24
+ async list(params = {}) {
25
+ const page = params.cursor ? Number.parseInt(params.cursor, 10) : 0;
26
+ const size = params.limit ?? DEFAULT_PAGE_SIZE;
27
+ const data = await this.transport.request({
28
+ method: "GET",
29
+ path: "/sapigw/brands",
30
+ query: { page, size },
31
+ rateLimiter: this.limiter
32
+ });
33
+ const items = data.brands.map((b) => ({ id: String(b.id), name: b.name }));
34
+ const nextCursor = page + 1 < data.totalPages ? String(page + 1) : void 0;
35
+ return nextCursor !== void 0 ? { items, nextCursor } : { items };
36
+ }
37
+ };
38
+
39
+ // src/auth.ts
40
+ function buildAuthHeader(apiKey, apiSecret) {
41
+ const token = Buffer.from(`${apiKey}:${apiSecret}`, "utf8").toString("base64");
42
+ return `Basic ${token}`;
43
+ }
44
+ function buildUserAgent(sellerId, integratorName = "SelfIntegration") {
45
+ return `${sellerId} - ${integratorName}`;
46
+ }
47
+ function mapHttpError(status, body, retryAfterMs) {
48
+ const data = { body };
49
+ switch (status) {
50
+ case 401:
51
+ return new AuthError({
52
+ message: "Trendyol authentication failed (check apiKey / apiSecret)",
53
+ status,
54
+ data
55
+ });
56
+ case 403:
57
+ return new ValidationError({
58
+ message: "Trendyol forbidden (check User-Agent header or endpoint path)",
59
+ status,
60
+ data
61
+ });
62
+ case 404:
63
+ return new NotFoundError({ message: "Trendyol resource not found", status, data });
64
+ case 429:
65
+ return new RateLimitError({
66
+ message: "Trendyol rate limit exceeded",
67
+ status,
68
+ retryAfterMs,
69
+ data
70
+ });
71
+ case 500:
72
+ case 502:
73
+ case 503:
74
+ case 504:
75
+ return new ServerError({
76
+ message: `Trendyol server error (${status})`,
77
+ status,
78
+ data
79
+ });
80
+ default:
81
+ return new LoncaError({
82
+ code: "UNKNOWN",
83
+ message: `Trendyol unexpected response (${status})`,
84
+ status,
85
+ data,
86
+ retryable: false
87
+ });
88
+ }
89
+ }
90
+ function parseRetryAfter(header) {
91
+ if (!header) return void 0;
92
+ const seconds = Number(header);
93
+ if (!Number.isNaN(seconds)) return Math.max(0, seconds * 1e3);
94
+ const epoch = Date.parse(header);
95
+ if (!Number.isNaN(epoch)) return Math.max(0, epoch - Date.now());
96
+ return void 0;
97
+ }
98
+
99
+ // src/transport.ts
100
+ var BASE_URLS = {
101
+ prod: "https://apigw.trendyol.com",
102
+ stage: "https://stageapigw.trendyol.com"
103
+ };
104
+ var TrendyolTransport = class {
105
+ constructor(config) {
106
+ this.config = config;
107
+ this.baseUrl = BASE_URLS[config.env];
108
+ this.logger = config.logger ?? noopLogger;
109
+ this.timeoutMs = config.timeoutMs ?? 3e4;
110
+ this.fetchImpl = config.fetch ?? fetch;
111
+ }
112
+ config;
113
+ baseUrl;
114
+ logger;
115
+ timeoutMs;
116
+ fetchImpl;
117
+ async request(opts) {
118
+ return retry(
119
+ async (attempt) => {
120
+ if (opts.rateLimiter) await opts.rateLimiter.acquire(opts.signal);
121
+ const url = this.buildUrl(opts.path, opts.query);
122
+ const correlationId = randomUUID();
123
+ const headers = this.buildHeaders(correlationId);
124
+ const init = {
125
+ method: opts.method,
126
+ headers,
127
+ signal: this.composeSignal(opts.signal)
128
+ };
129
+ if (opts.body !== void 0 && opts.method !== "GET") {
130
+ init.body = JSON.stringify(opts.body);
131
+ }
132
+ this.logger.debug("trendyol.request", {
133
+ method: opts.method,
134
+ url,
135
+ correlationId,
136
+ attempt
137
+ });
138
+ let response;
139
+ try {
140
+ response = await this.fetchImpl(url, init);
141
+ } catch (err) {
142
+ if (err instanceof Error && err.name === "AbortError") {
143
+ throw new TimeoutError({
144
+ message: `Trendyol request timed out after ${this.timeoutMs}ms`,
145
+ cause: err
146
+ });
147
+ }
148
+ throw new NetworkError({
149
+ message: "Trendyol network failure",
150
+ cause: err
151
+ });
152
+ }
153
+ if (!response.ok) {
154
+ const body = await safeJson(response);
155
+ const retryAfterMs = parseRetryAfter(response.headers.get("retry-after"));
156
+ const error = mapHttpError(response.status, body, retryAfterMs);
157
+ this.logger.warn("trendyol.error", {
158
+ method: opts.method,
159
+ url,
160
+ correlationId,
161
+ status: response.status,
162
+ code: error.code,
163
+ retryable: error.retryable
164
+ });
165
+ throw error;
166
+ }
167
+ this.logger.debug("trendyol.response", {
168
+ correlationId,
169
+ status: response.status
170
+ });
171
+ if (response.status === 204) return void 0;
172
+ return await safeJson(response);
173
+ },
174
+ {
175
+ signal: opts.signal,
176
+ onRetry: (err, attempt, delay) => {
177
+ if (err instanceof LoncaError) {
178
+ this.logger.warn("trendyol.retry", {
179
+ attempt,
180
+ delayMs: delay,
181
+ code: err.code,
182
+ status: err.status
183
+ });
184
+ }
185
+ }
186
+ }
187
+ );
188
+ }
189
+ buildUrl(path, query) {
190
+ const url = new URL(path, this.baseUrl);
191
+ if (query) {
192
+ for (const [key, value] of Object.entries(query)) {
193
+ if (value === void 0) continue;
194
+ url.searchParams.set(key, String(value));
195
+ }
196
+ }
197
+ return url.toString();
198
+ }
199
+ buildHeaders(correlationId) {
200
+ const integratorName = this.config.integratorName ?? "SelfIntegration";
201
+ return {
202
+ Authorization: buildAuthHeader(this.config.apiKey, this.config.apiSecret),
203
+ "x-clientip": this.config.clientIp ?? "127.0.0.1",
204
+ "x-correlationid": correlationId,
205
+ "x-agentname": integratorName,
206
+ "User-Agent": buildUserAgent(this.config.sellerId, integratorName),
207
+ "Content-Type": "application/json",
208
+ Accept: "application/json"
209
+ };
210
+ }
211
+ composeSignal(external) {
212
+ const timeoutSignal = AbortSignal.timeout(this.timeoutMs);
213
+ if (!external) return timeoutSignal;
214
+ return AbortSignal.any([external, timeoutSignal]);
215
+ }
216
+ };
217
+ async function safeJson(response) {
218
+ const text = await response.text();
219
+ if (!text) return void 0;
220
+ try {
221
+ return JSON.parse(text);
222
+ } catch {
223
+ return text;
224
+ }
225
+ }
226
+
227
+ // src/client.ts
228
+ function createTrendyolClient(opts) {
229
+ const transport = new TrendyolTransport({
230
+ sellerId: opts.sellerId,
231
+ apiKey: opts.apiKey,
232
+ apiSecret: opts.apiSecret,
233
+ env: opts.env,
234
+ integratorName: opts.integratorName,
235
+ clientIp: opts.clientIp,
236
+ logger: opts.logger,
237
+ timeoutMs: opts.timeoutMs
238
+ });
239
+ return {
240
+ brands: new BrandsResource(transport)
241
+ };
242
+ }
243
+
244
+ export { BrandsResource, createTrendyolClient };
245
+ //# sourceMappingURL=index.js.map
246
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/resources/brands.ts","../src/auth.ts","../src/errors.ts","../src/transport.ts","../src/client.ts"],"names":["LoncaError"],"mappings":"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","file":"index.js","sourcesContent":["import { TokenBucketRateLimiter, type CursorPage, type CursorPaginationParams } from '@lonca/core';\nimport type { TrendyolTransport } from '../transport.js';\nimport type { Brand } from '../types/brand.js';\n\n/** Default page size when callers don't specify `limit`. Trendyol allows up to 1000. */\nconst DEFAULT_PAGE_SIZE = 1000;\n\ninterface TrendyolBrandListResponse {\n brands: Array<{ id: number; name: string }>;\n totalPages: number;\n totalElements: number;\n}\n\n/**\n * Trendyol brand-list endpoint group.\n *\n * Rate limit: 50 req/min (per Trendyol service limits).\n *\n * Trendyol uses page-based pagination internally; we expose the cursor-based\n * `CursorPage` shape from `@lonca/core` so callers can drive everything with\n * `paginate()` and stay consistent across Lonca SDKs.\n */\nexport class BrandsResource {\n private readonly limiter: TokenBucketRateLimiter;\n\n constructor(\n private readonly transport: TrendyolTransport,\n limiter?: TokenBucketRateLimiter,\n ) {\n this.limiter = limiter ?? new TokenBucketRateLimiter({ capacity: 50, intervalMs: 60_000 });\n }\n\n /**\n * List Trendyol brands, one page at a time.\n *\n * @example\n * ```ts\n * import { paginate } from '@lonca/core';\n * for await (const brand of paginate((p) => client.brands.list(p))) {\n * console.log(brand.id, brand.name);\n * }\n * ```\n */\n async list(params: CursorPaginationParams = {}): Promise<CursorPage<Brand>> {\n const page = params.cursor ? Number.parseInt(params.cursor, 10) : 0;\n const size = params.limit ?? DEFAULT_PAGE_SIZE;\n\n const data = await this.transport.request<TrendyolBrandListResponse>({\n method: 'GET',\n path: '/sapigw/brands',\n query: { page, size },\n rateLimiter: this.limiter,\n });\n\n const items: Brand[] = data.brands.map((b) => ({ id: String(b.id), name: b.name }));\n const nextCursor = page + 1 < data.totalPages ? String(page + 1) : undefined;\n\n return nextCursor !== undefined ? { items, nextCursor } : { items };\n }\n}\n","/** Build the HTTP Basic auth header value from Trendyol credentials. */\nexport function buildAuthHeader(apiKey: string, apiSecret: string): string {\n const token = Buffer.from(`${apiKey}:${apiSecret}`, 'utf8').toString('base64');\n return `Basic ${token}`;\n}\n\n/**\n * Build the `User-Agent` header value Trendyol expects.\n *\n * Per Trendyol's docs:\n * - Solo integrators: `\"<sellerId> - SelfIntegration\"`\n * - Integrator companies: `\"<sellerId> - <CompanyName>\"` (alphanumeric, max 30 chars)\n *\n * A missing or malformed `User-Agent` triggers HTTP 403.\n */\nexport function buildUserAgent(sellerId: number, integratorName = 'SelfIntegration'): string {\n return `${sellerId} - ${integratorName}`;\n}\n","import {\n AuthError,\n LoncaError,\n NotFoundError,\n RateLimitError,\n ServerError,\n ValidationError,\n} from '@lonca/core';\n\n/**\n * Map a Trendyol HTTP response status to a `@lonca/core` error.\n *\n * - `401` → `AuthError` (bad credentials)\n * - `403` → `ValidationError` (usually missing/malformed `User-Agent` header, or wrong endpoint path)\n * - `404` → `NotFoundError`\n * - `429` → `RateLimitError` (carries `retryAfterMs` if the server provided `Retry-After`)\n * - `5xx` → `ServerError` (retryable)\n * - other → `LoncaError` with code `UNKNOWN` (non-retryable)\n */\nexport function mapHttpError(status: number, body: unknown, retryAfterMs?: number): LoncaError {\n const data = { body } as Record<string, unknown>;\n switch (status) {\n case 401:\n return new AuthError({\n message: 'Trendyol authentication failed (check apiKey / apiSecret)',\n status,\n data,\n });\n case 403:\n return new ValidationError({\n message: 'Trendyol forbidden (check User-Agent header or endpoint path)',\n status,\n data,\n });\n case 404:\n return new NotFoundError({ message: 'Trendyol resource not found', status, data });\n case 429:\n return new RateLimitError({\n message: 'Trendyol rate limit exceeded',\n status,\n retryAfterMs,\n data,\n });\n case 500:\n case 502:\n case 503:\n case 504:\n return new ServerError({\n message: `Trendyol server error (${status})`,\n status,\n data,\n });\n default:\n return new LoncaError({\n code: 'UNKNOWN',\n message: `Trendyol unexpected response (${status})`,\n status,\n data,\n retryable: false,\n });\n }\n}\n\n/** Parse a `Retry-After` header value (seconds OR HTTP-date) into milliseconds. */\nexport function parseRetryAfter(header: string | null): number | undefined {\n if (!header) return undefined;\n const seconds = Number(header);\n if (!Number.isNaN(seconds)) return Math.max(0, seconds * 1000);\n const epoch = Date.parse(header);\n if (!Number.isNaN(epoch)) return Math.max(0, epoch - Date.now());\n return undefined;\n}\n","import { randomUUID } from 'node:crypto';\nimport {\n LoncaError,\n NetworkError,\n TimeoutError,\n retry,\n noopLogger,\n type Logger,\n type TokenBucketRateLimiter,\n} from '@lonca/core';\nimport { buildAuthHeader, buildUserAgent } from './auth.js';\nimport { mapHttpError, parseRetryAfter } from './errors.js';\n\nconst BASE_URLS = {\n prod: 'https://apigw.trendyol.com',\n stage: 'https://stageapigw.trendyol.com',\n} as const;\n\nexport type TrendyolEnvironment = keyof typeof BASE_URLS;\n\nexport interface TransportConfig {\n sellerId: number;\n apiKey: string;\n apiSecret: string;\n env: TrendyolEnvironment;\n integratorName?: string;\n clientIp?: string;\n logger?: Logger;\n /** Request timeout in ms. Default: 30_000. */\n timeoutMs?: number;\n /** Override the underlying `fetch` (tests inject a mock). */\n fetch?: typeof fetch;\n}\n\nexport interface RequestOptions {\n method: 'GET' | 'POST' | 'PUT' | 'DELETE';\n /** Path beginning with `/` (e.g., `/sapigw/brands`). */\n path: string;\n query?: Record<string, string | number | boolean | undefined>;\n body?: unknown;\n signal?: AbortSignal;\n /** Per-endpoint rate limiter; acquire one token before each attempt. */\n rateLimiter?: TokenBucketRateLimiter;\n}\n\nexport class TrendyolTransport {\n private readonly baseUrl: string;\n private readonly logger: Logger;\n private readonly timeoutMs: number;\n private readonly fetchImpl: typeof fetch;\n\n constructor(private readonly config: TransportConfig) {\n this.baseUrl = BASE_URLS[config.env];\n this.logger = config.logger ?? noopLogger;\n this.timeoutMs = config.timeoutMs ?? 30_000;\n this.fetchImpl = config.fetch ?? fetch;\n }\n\n async request<T>(opts: RequestOptions): Promise<T> {\n return retry(\n async (attempt) => {\n if (opts.rateLimiter) await opts.rateLimiter.acquire(opts.signal);\n\n const url = this.buildUrl(opts.path, opts.query);\n const correlationId = randomUUID();\n const headers = this.buildHeaders(correlationId);\n const init: RequestInit = {\n method: opts.method,\n headers,\n signal: this.composeSignal(opts.signal),\n };\n if (opts.body !== undefined && opts.method !== 'GET') {\n init.body = JSON.stringify(opts.body);\n }\n\n this.logger.debug('trendyol.request', {\n method: opts.method,\n url,\n correlationId,\n attempt,\n });\n\n let response: Response;\n try {\n response = await this.fetchImpl(url, init);\n } catch (err) {\n if (err instanceof Error && err.name === 'AbortError') {\n throw new TimeoutError({\n message: `Trendyol request timed out after ${this.timeoutMs}ms`,\n cause: err,\n });\n }\n throw new NetworkError({\n message: 'Trendyol network failure',\n cause: err,\n });\n }\n\n if (!response.ok) {\n const body = await safeJson(response);\n const retryAfterMs = parseRetryAfter(response.headers.get('retry-after'));\n const error = mapHttpError(response.status, body, retryAfterMs);\n this.logger.warn('trendyol.error', {\n method: opts.method,\n url,\n correlationId,\n status: response.status,\n code: error.code,\n retryable: error.retryable,\n });\n throw error;\n }\n\n this.logger.debug('trendyol.response', {\n correlationId,\n status: response.status,\n });\n\n if (response.status === 204) return undefined as T;\n return (await safeJson(response)) as T;\n },\n {\n signal: opts.signal,\n onRetry: (err, attempt, delay) => {\n if (err instanceof LoncaError) {\n this.logger.warn('trendyol.retry', {\n attempt,\n delayMs: delay,\n code: err.code,\n status: err.status,\n });\n }\n },\n },\n );\n }\n\n private buildUrl(path: string, query?: RequestOptions['query']): string {\n const url = new URL(path, this.baseUrl);\n if (query) {\n for (const [key, value] of Object.entries(query)) {\n if (value === undefined) continue;\n url.searchParams.set(key, String(value));\n }\n }\n return url.toString();\n }\n\n private buildHeaders(correlationId: string): Record<string, string> {\n const integratorName = this.config.integratorName ?? 'SelfIntegration';\n return {\n Authorization: buildAuthHeader(this.config.apiKey, this.config.apiSecret),\n 'x-clientip': this.config.clientIp ?? '127.0.0.1',\n 'x-correlationid': correlationId,\n 'x-agentname': integratorName,\n 'User-Agent': buildUserAgent(this.config.sellerId, integratorName),\n 'Content-Type': 'application/json',\n Accept: 'application/json',\n };\n }\n\n private composeSignal(external?: AbortSignal): AbortSignal {\n const timeoutSignal = AbortSignal.timeout(this.timeoutMs);\n if (!external) return timeoutSignal;\n return AbortSignal.any([external, timeoutSignal]);\n }\n}\n\nasync function safeJson(response: Response): Promise<unknown> {\n const text = await response.text();\n if (!text) return undefined;\n try {\n return JSON.parse(text);\n } catch {\n return text;\n }\n}\n","import type { Logger } from '@lonca/core';\nimport { BrandsResource } from './resources/brands.js';\nimport { TrendyolTransport, type TrendyolEnvironment } from './transport.js';\n\nexport interface CreateClientOptions {\n /** Trendyol seller (supplier) ID — visible in Partner Panel → Account Info. */\n sellerId: number;\n /** Trendyol API key. */\n apiKey: string;\n /** Trendyol API secret. */\n apiSecret: string;\n /** Which Trendyol environment to target. */\n env: TrendyolEnvironment;\n /**\n * Integrator company name to send in `User-Agent` / `x-agentname`.\n * Defaults to `'SelfIntegration'` if the seller owns the integration code.\n * Trendyol caps this at 30 alphanumeric characters.\n */\n integratorName?: string;\n /**\n * IPv4 address to send in `x-clientip`.\n * Defaults to `'127.0.0.1'` — Trendyol does not validate this against the\n * request origin, the header just has to be present and IPv4-shaped.\n */\n clientIp?: string;\n /** Optional structured logger (`@lonca/core` `Logger`). Defaults to no-op. */\n logger?: Logger;\n /** Request timeout in ms. Default: 30_000. */\n timeoutMs?: number;\n}\n\nexport interface TrendyolClient {\n brands: BrandsResource;\n}\n\n/**\n * Create a Trendyol Marketplace SDK client.\n *\n * @example\n * ```ts\n * import { createTrendyolClient } from '@lonca/trendyol';\n *\n * const client = createTrendyolClient({\n * sellerId: 12345,\n * apiKey: process.env.TRENDYOL_API_KEY!,\n * apiSecret: process.env.TRENDYOL_API_SECRET!,\n * env: 'stage',\n * });\n *\n * const page = await client.brands.list({ limit: 100 });\n * ```\n */\nexport function createTrendyolClient(opts: CreateClientOptions): TrendyolClient {\n const transport = new TrendyolTransport({\n sellerId: opts.sellerId,\n apiKey: opts.apiKey,\n apiSecret: opts.apiSecret,\n env: opts.env,\n integratorName: opts.integratorName,\n clientIp: opts.clientIp,\n logger: opts.logger,\n timeoutMs: opts.timeoutMs,\n });\n\n return {\n brands: new BrandsResource(transport),\n };\n}\n"]}
package/package.json ADDED
@@ -0,0 +1,63 @@
1
+ {
2
+ "name": "@lonca/trendyol",
3
+ "version": "0.1.0",
4
+ "description": "Type-safe TypeScript SDK for the Trendyol Marketplace API",
5
+ "license": "MIT",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "https://github.com/loncadev/lonca.git",
9
+ "directory": "sdks/trendyol"
10
+ },
11
+ "homepage": "https://github.com/loncadev/lonca/tree/main/sdks/trendyol#readme",
12
+ "bugs": "https://github.com/loncadev/lonca/issues",
13
+ "keywords": [
14
+ "trendyol",
15
+ "marketplace",
16
+ "e-commerce",
17
+ "turkey",
18
+ "sdk",
19
+ "api"
20
+ ],
21
+ "type": "module",
22
+ "main": "./dist/index.cjs",
23
+ "module": "./dist/index.js",
24
+ "types": "./dist/index.d.ts",
25
+ "exports": {
26
+ ".": {
27
+ "types": "./dist/index.d.ts",
28
+ "import": "./dist/index.js",
29
+ "require": "./dist/index.cjs"
30
+ },
31
+ "./package.json": "./package.json"
32
+ },
33
+ "files": [
34
+ "dist",
35
+ "README.md",
36
+ "LICENSE"
37
+ ],
38
+ "sideEffects": false,
39
+ "publishConfig": {
40
+ "access": "public"
41
+ },
42
+ "engines": {
43
+ "node": ">=22"
44
+ },
45
+ "scripts": {
46
+ "build": "tsup",
47
+ "dev": "tsup --watch",
48
+ "test": "vitest run",
49
+ "test:watch": "vitest",
50
+ "typecheck": "tsc --noEmit",
51
+ "lint": "eslint .",
52
+ "clean": "node -e \"require('fs').rmSync('dist',{recursive:true,force:true});require('fs').rmSync('.turbo',{recursive:true,force:true})\""
53
+ },
54
+ "dependencies": {
55
+ "@lonca/core": "workspace:^"
56
+ },
57
+ "devDependencies": {
58
+ "@types/node": "catalog:",
59
+ "tsup": "catalog:",
60
+ "typescript": "catalog:",
61
+ "vitest": "catalog:"
62
+ }
63
+ }