@open-mercato/sync-akeneo 0.6.6-develop.5523.1.e223ca1915 → 0.6.6-develop.5536.1.7cfc9c28a1

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.
@@ -137,6 +137,25 @@ function coerceCredentials(credentials) {
137
137
  function sleep(ms) {
138
138
  return new Promise((resolve) => setTimeout(resolve, ms));
139
139
  }
140
+ const DEFAULT_AKENEO_REQUEST_TIMEOUT_MS = 3e4;
141
+ const DEFAULT_AKENEO_MAX_RATE_LIMIT_RETRIES = 5;
142
+ const DEFAULT_AKENEO_RETRY_AFTER_CAP_MS = 6e4;
143
+ function resolvePositiveIntEnv(raw, fallback) {
144
+ const parsed = Number.parseInt(raw ?? "", 10);
145
+ return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
146
+ }
147
+ function resolveAkeneoRequestTimeoutMs(env = process.env) {
148
+ return resolvePositiveIntEnv(env.OM_INTEGRATION_AKENEO_REQUEST_TIMEOUT_MS, DEFAULT_AKENEO_REQUEST_TIMEOUT_MS);
149
+ }
150
+ function resolveAkeneoMaxRateLimitRetries(env = process.env) {
151
+ return resolvePositiveIntEnv(env.OM_INTEGRATION_AKENEO_MAX_RATE_LIMIT_RETRIES, DEFAULT_AKENEO_MAX_RATE_LIMIT_RETRIES);
152
+ }
153
+ function clampAkeneoRetryAfterMs(retryAfterHeader, env = process.env) {
154
+ const retryAfter = Number(retryAfterHeader ?? "1");
155
+ const requestedMs = Number.isFinite(retryAfter) ? retryAfter * 1e3 : 1e3;
156
+ const cap = resolvePositiveIntEnv(env.OM_INTEGRATION_AKENEO_RETRY_AFTER_CAP_MS, DEFAULT_AKENEO_RETRY_AFTER_CAP_MS);
157
+ return Math.min(Math.max(requestedMs, 0), cap);
158
+ }
140
159
  function createAkeneoClient(credentialsInput) {
141
160
  const credentials = coerceCredentials(credentialsInput);
142
161
  const akeneoBaseUrl = new URL(`${credentials.apiUrl}/`);
@@ -162,6 +181,7 @@ function createAkeneoClient(credentialsInput) {
162
181
  async function acquirePasswordGrantToken() {
163
182
  const response = await fetch(tokenEndpointUrl, {
164
183
  method: "POST",
184
+ signal: AbortSignal.timeout(resolveAkeneoRequestTimeoutMs()),
165
185
  headers: {
166
186
  accept: "application/json",
167
187
  "content-type": "application/json"
@@ -192,6 +212,7 @@ function createAkeneoClient(credentialsInput) {
192
212
  if (!current.refreshToken) return acquirePasswordGrantToken();
193
213
  const response = await fetch(tokenEndpointUrl, {
194
214
  method: "POST",
215
+ signal: AbortSignal.timeout(resolveAkeneoRequestTimeoutMs()),
195
216
  headers: {
196
217
  accept: "application/json",
197
218
  "content-type": "application/json"
@@ -221,11 +242,12 @@ function createAkeneoClient(credentialsInput) {
221
242
  tokenState = tokenState ? await refreshAccessToken(tokenState) : await acquirePasswordGrantToken();
222
243
  return tokenState.accessToken;
223
244
  }
224
- async function request(pathOrUrl, init = {}, retried = false) {
245
+ async function request(pathOrUrl, init = {}, retried = false, rateLimitAttempts = 0) {
225
246
  const url = resolveAkeneoRequestUrl(pathOrUrl);
226
247
  const token = await ensureToken();
227
248
  const response = await fetch(url, {
228
249
  ...init,
250
+ signal: init.signal ?? AbortSignal.timeout(resolveAkeneoRequestTimeoutMs()),
229
251
  headers: {
230
252
  accept: "application/json",
231
253
  authorization: `Bearer ${token}`,
@@ -234,12 +256,15 @@ function createAkeneoClient(credentialsInput) {
234
256
  });
235
257
  if (response.status === 401 && !retried) {
236
258
  await ensureToken(true);
237
- return request(pathOrUrl, init, true);
259
+ return request(pathOrUrl, init, true, rateLimitAttempts);
238
260
  }
239
261
  if (response.status === 429) {
240
- const retryAfter = Number(response.headers.get("retry-after") ?? "1");
241
- await sleep(Number.isFinite(retryAfter) ? retryAfter * 1e3 : 1e3);
242
- return request(pathOrUrl, init, retried);
262
+ if (rateLimitAttempts >= resolveAkeneoMaxRateLimitRetries()) {
263
+ const body = await response.text();
264
+ throw new Error(`Akeneo request failed (429): ${body}`);
265
+ }
266
+ await sleep(clampAkeneoRetryAfterMs(response.headers.get("retry-after")));
267
+ return request(pathOrUrl, init, retried, rateLimitAttempts + 1);
243
268
  }
244
269
  if (!response.ok) {
245
270
  const body = await response.text();
@@ -250,11 +275,12 @@ function createAkeneoClient(credentialsInput) {
250
275
  }
251
276
  return response.json();
252
277
  }
253
- async function requestBinary(pathOrUrl, init = {}, retried = false) {
278
+ async function requestBinary(pathOrUrl, init = {}, retried = false, rateLimitAttempts = 0) {
254
279
  const url = resolveAkeneoRequestUrl(pathOrUrl);
255
280
  const token = await ensureToken();
256
281
  const response = await fetch(url, {
257
282
  ...init,
283
+ signal: init.signal ?? AbortSignal.timeout(resolveAkeneoRequestTimeoutMs()),
258
284
  headers: {
259
285
  authorization: `Bearer ${token}`,
260
286
  ...init.headers
@@ -262,12 +288,15 @@ function createAkeneoClient(credentialsInput) {
262
288
  });
263
289
  if (response.status === 401 && !retried) {
264
290
  await ensureToken(true);
265
- return requestBinary(pathOrUrl, init, true);
291
+ return requestBinary(pathOrUrl, init, true, rateLimitAttempts);
266
292
  }
267
293
  if (response.status === 429) {
268
- const retryAfter = Number(response.headers.get("retry-after") ?? "1");
269
- await sleep(Number.isFinite(retryAfter) ? retryAfter * 1e3 : 1e3);
270
- return requestBinary(pathOrUrl, init, retried);
294
+ if (rateLimitAttempts >= resolveAkeneoMaxRateLimitRetries()) {
295
+ const body = await response.text();
296
+ throw new Error(`Akeneo request failed (429): ${body}`);
297
+ }
298
+ await sleep(clampAkeneoRetryAfterMs(response.headers.get("retry-after")));
299
+ return requestBinary(pathOrUrl, init, retried, rateLimitAttempts + 1);
271
300
  }
272
301
  if (!response.ok) {
273
302
  const body = await response.text();
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../src/modules/sync_akeneo/lib/client.ts"],
4
- "sourcesContent": ["import { dedupeStrings, labelFromLocalizedRecord, safeRecord, type AkeneoAttribute, type AkeneoAttributeOption, type AkeneoCategory, type AkeneoChannel, type AkeneoCredentialShape, type AkeneoFamily, type AkeneoFamilyVariant, type AkeneoLocale, type AkeneoProduct, type AkeneoProductModel } from './shared'\n\ntype TokenState = {\n accessToken: string\n refreshToken?: string | null\n expiresAt: number\n}\n\ntype AkeneoListResponse<T> = {\n _embedded?: {\n items?: T[]\n }\n _links?: {\n next?: {\n href?: string\n }\n }\n items_count?: number\n}\n\ntype AkeneoMediaFile = {\n code: string\n original_filename?: string | null\n mime_type?: string | null\n size?: number | null\n extension?: string | null\n _links?: {\n download?: {\n href?: string\n }\n }\n}\n\ntype AkeneoClient = ReturnType<typeof createAkeneoClient>\n\nconst DEFAULT_ALLOWED_AKENEO_HOST_PATTERNS = ['*.cloud.akeneo.com', '*.akeneo.cloud'] as const\nconst ALLOWED_AKENEO_HOSTS_ENV_KEYS = [\n 'OM_INTEGRATION_AKENEO_ALLOWED_HOSTS',\n 'OPENMERCATO_AKENEO_ALLOWED_HOSTS',\n 'AKENEO_ALLOWED_HOSTS',\n] as const\n\nfunction normalizeBaseUrl(url: string): string {\n return url.trim().replace(/\\/+$/g, '')\n}\n\nfunction readAllowedAkeneoHostPatterns(env: NodeJS.ProcessEnv = process.env): string[] {\n for (const key of ALLOWED_AKENEO_HOSTS_ENV_KEYS) {\n const raw = env[key]\n if (typeof raw !== 'string') continue\n const patterns = raw\n .split(',')\n .map((entry) => entry.trim().toLowerCase())\n .filter((entry) => entry.length > 0)\n if (patterns.length > 0) {\n return patterns\n }\n }\n\n return [...DEFAULT_ALLOWED_AKENEO_HOST_PATTERNS]\n}\n\nfunction matchesAkeneoHostPattern(hostname: string, pattern: string): boolean {\n if (pattern.startsWith('*.')) {\n const suffix = pattern.slice(2)\n return hostname === suffix || hostname.endsWith(`.${suffix}`)\n }\n\n return hostname === pattern\n}\n\nexport function validateAkeneoApiUrl(rawUrl: string, env: NodeJS.ProcessEnv = process.env): string {\n let parsed: URL\n try {\n parsed = new URL(rawUrl.trim())\n } catch {\n throw new Error('Akeneo URL must be a valid absolute URL')\n }\n\n if (parsed.protocol !== 'https:') {\n throw new Error('Akeneo URL must use https')\n }\n if (parsed.username || parsed.password) {\n throw new Error('Akeneo URL must not include embedded credentials')\n }\n if (parsed.port && parsed.port !== '443') {\n throw new Error('Akeneo URL must not use a custom port')\n }\n if (parsed.pathname !== '/' && parsed.pathname !== '') {\n throw new Error('Akeneo URL must not include a path')\n }\n if (parsed.search || parsed.hash) {\n throw new Error('Akeneo URL must not include query parameters or fragments')\n }\n\n const hostname = parsed.hostname.toLowerCase()\n const allowedPatterns = readAllowedAkeneoHostPatterns(env)\n if (!allowedPatterns.some((pattern) => matchesAkeneoHostPattern(hostname, pattern))) {\n throw new Error(`Akeneo URL host is not allowed: ${hostname}`)\n }\n\n return normalizeBaseUrl(parsed.origin)\n}\n\nexport function normalizeAkeneoDateTime(value: string | null | undefined): string | null {\n if (typeof value !== 'string' || value.trim().length === 0) return null\n const trimmed = value.trim()\n if (/^\\d{4}-\\d{2}-\\d{2} \\d{2}:\\d{2}:\\d{2}$/.test(trimmed)) {\n return trimmed\n }\n\n const parsed = new Date(trimmed)\n if (Number.isNaN(parsed.getTime())) {\n return null\n }\n\n const year = parsed.getUTCFullYear()\n const month = String(parsed.getUTCMonth() + 1).padStart(2, '0')\n const day = String(parsed.getUTCDate()).padStart(2, '0')\n const hours = String(parsed.getUTCHours()).padStart(2, '0')\n const minutes = String(parsed.getUTCMinutes()).padStart(2, '0')\n const seconds = String(parsed.getUTCSeconds()).padStart(2, '0')\n return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`\n}\n\nexport function encodeAkeneoPathParam(value: string): string {\n return value\n .split('/')\n .map((segment) => encodeURIComponent(segment))\n .join('/')\n}\n\nexport function sanitizeAkeneoProductNextUrl(nextUrl: string): string {\n let url: URL\n try {\n url = new URL(nextUrl)\n } catch {\n return nextUrl\n }\n\n const rawSearch = url.searchParams.get('search')\n if (!rawSearch) {\n return url.toString()\n }\n\n try {\n const parsed = JSON.parse(rawSearch) as Record<string, unknown>\n const updatedFilters = Array.isArray(parsed.updated) ? parsed.updated : []\n const sanitizedUpdated = updatedFilters\n .map((entry) => {\n if (!entry || typeof entry !== 'object') return null\n const record = entry as Record<string, unknown>\n const normalizedValue = normalizeAkeneoDateTime(\n typeof record.value === 'string' ? record.value : null,\n )\n if (!normalizedValue) return null\n return {\n ...record,\n value: normalizedValue,\n }\n })\n .filter((entry) => entry !== null) as Record<string, unknown>[]\n\n if (sanitizedUpdated.length === 0) {\n delete parsed.updated\n } else {\n parsed.updated = sanitizedUpdated\n }\n\n if (Object.keys(parsed).length === 0) {\n url.searchParams.delete('search')\n } else {\n url.searchParams.set('search', JSON.stringify(parsed))\n }\n return url.toString()\n } catch {\n return nextUrl\n }\n}\n\nfunction coerceCredentials(credentials: Record<string, unknown>): AkeneoCredentialShape {\n const apiUrl = typeof credentials.apiUrl === 'string' ? credentials.apiUrl.trim() : ''\n const clientId = typeof credentials.clientId === 'string' ? credentials.clientId.trim() : ''\n const clientSecret = typeof credentials.clientSecret === 'string' ? credentials.clientSecret : ''\n const username = typeof credentials.username === 'string' ? credentials.username.trim() : ''\n const password = typeof credentials.password === 'string' ? credentials.password : ''\n if (!apiUrl || !clientId || !clientSecret || !username || !password) {\n throw new Error('Akeneo credentials are incomplete')\n }\n return {\n apiUrl: validateAkeneoApiUrl(apiUrl),\n clientId,\n clientSecret,\n username,\n password,\n }\n}\n\nfunction sleep(ms: number): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, ms))\n}\n\nexport function createAkeneoClient(credentialsInput: Record<string, unknown>) {\n const credentials = coerceCredentials(credentialsInput)\n const akeneoBaseUrl = new URL(`${credentials.apiUrl}/`)\n const tokenEndpointUrl = new URL('/api/oauth/v1/token', akeneoBaseUrl).toString()\n let tokenState: TokenState | null = null\n let lastAttributeOptionRequestAt = 0\n const familyCache = new Map<string, Promise<AkeneoFamily | null>>()\n const familyVariantCache = new Map<string, Promise<AkeneoFamilyVariant | null>>()\n const productModelCache = new Map<string, Promise<AkeneoProductModel | null>>()\n const attributeCache = new Map<string, Promise<AkeneoAttribute | null>>()\n const categoryCache = new Map<string, Promise<AkeneoCategory | null>>()\n const mediaFileCache = new Map<string, Promise<AkeneoMediaFile | null>>()\n\n function resolveAkeneoRequestUrl(pathOrUrl: string): string {\n const resolved = new URL(pathOrUrl, akeneoBaseUrl)\n if (resolved.origin !== akeneoBaseUrl.origin) {\n throw new Error(`Akeneo request URL must stay on the configured host: ${resolved.origin}`)\n }\n return resolved.toString()\n }\n\n function normalizeAkeneoNextUrl(nextUrl: string): string {\n return resolveAkeneoRequestUrl(nextUrl)\n }\n\n async function acquirePasswordGrantToken(): Promise<TokenState> {\n const response = await fetch(tokenEndpointUrl, {\n method: 'POST',\n headers: {\n accept: 'application/json',\n 'content-type': 'application/json',\n },\n body: JSON.stringify({\n grant_type: 'password',\n client_id: credentials.clientId,\n client_secret: credentials.clientSecret,\n username: credentials.username,\n password: credentials.password,\n }),\n })\n\n if (!response.ok) {\n const message = await response.text()\n throw new Error(`Akeneo authentication failed (${response.status}): ${message}`)\n }\n\n const payload = await response.json() as {\n access_token?: string\n refresh_token?: string\n expires_in?: number\n }\n if (!payload.access_token) {\n throw new Error('Akeneo authentication did not return an access token')\n }\n\n return {\n accessToken: payload.access_token,\n refreshToken: payload.refresh_token ?? null,\n expiresAt: Date.now() + ((payload.expires_in ?? 3600) * 1000) - 10_000,\n }\n }\n\n async function refreshAccessToken(current: TokenState): Promise<TokenState> {\n if (!current.refreshToken) return acquirePasswordGrantToken()\n const response = await fetch(tokenEndpointUrl, {\n method: 'POST',\n headers: {\n accept: 'application/json',\n 'content-type': 'application/json',\n },\n body: JSON.stringify({\n grant_type: 'refresh_token',\n client_id: credentials.clientId,\n client_secret: credentials.clientSecret,\n refresh_token: current.refreshToken,\n }),\n })\n\n if (!response.ok) {\n return acquirePasswordGrantToken()\n }\n\n const payload = await response.json() as {\n access_token?: string\n refresh_token?: string\n expires_in?: number\n }\n if (!payload.access_token) return acquirePasswordGrantToken()\n return {\n accessToken: payload.access_token,\n refreshToken: payload.refresh_token ?? current.refreshToken ?? null,\n expiresAt: Date.now() + ((payload.expires_in ?? 3600) * 1000) - 10_000,\n }\n }\n\n async function ensureToken(forceRefresh = false): Promise<string> {\n if (!forceRefresh && tokenState && tokenState.expiresAt > Date.now()) {\n return tokenState.accessToken\n }\n tokenState = tokenState ? await refreshAccessToken(tokenState) : await acquirePasswordGrantToken()\n return tokenState.accessToken\n }\n\n async function request<T>(\n pathOrUrl: string,\n init: RequestInit = {},\n retried = false,\n ): Promise<T> {\n const url = resolveAkeneoRequestUrl(pathOrUrl)\n const token = await ensureToken()\n\n const response = await fetch(url, {\n ...init,\n headers: {\n accept: 'application/json',\n authorization: `Bearer ${token}`,\n ...init.headers,\n },\n })\n\n if (response.status === 401 && !retried) {\n await ensureToken(true)\n return request<T>(pathOrUrl, init, true)\n }\n\n if (response.status === 429) {\n const retryAfter = Number(response.headers.get('retry-after') ?? '1')\n await sleep(Number.isFinite(retryAfter) ? retryAfter * 1000 : 1000)\n return request<T>(pathOrUrl, init, retried)\n }\n\n if (!response.ok) {\n const body = await response.text()\n throw new Error(`Akeneo request failed (${response.status}): ${body}`)\n }\n\n if (response.status === 204) {\n return null as T\n }\n\n return response.json() as Promise<T>\n }\n\n async function requestBinary(\n pathOrUrl: string,\n init: RequestInit = {},\n retried = false,\n ): Promise<{\n buffer: Buffer\n contentType: string | null\n contentLength: number | null\n }> {\n const url = resolveAkeneoRequestUrl(pathOrUrl)\n const token = await ensureToken()\n\n const response = await fetch(url, {\n ...init,\n headers: {\n authorization: `Bearer ${token}`,\n ...init.headers,\n },\n })\n\n if (response.status === 401 && !retried) {\n await ensureToken(true)\n return requestBinary(pathOrUrl, init, true)\n }\n\n if (response.status === 429) {\n const retryAfter = Number(response.headers.get('retry-after') ?? '1')\n await sleep(Number.isFinite(retryAfter) ? retryAfter * 1000 : 1000)\n return requestBinary(pathOrUrl, init, retried)\n }\n\n if (!response.ok) {\n const body = await response.text()\n throw new Error(`Akeneo request failed (${response.status}): ${body}`)\n }\n\n const arrayBuffer = await response.arrayBuffer()\n return {\n buffer: Buffer.from(arrayBuffer),\n contentType: response.headers.get('content-type'),\n contentLength: Number(response.headers.get('content-length') ?? '') || arrayBuffer.byteLength || null,\n }\n }\n\n function buildUrl(path: string, params: Record<string, string | number | boolean | undefined | null>): string {\n const url = new URL(resolveAkeneoRequestUrl(path))\n for (const [key, value] of Object.entries(params)) {\n if (value === undefined || value === null || value === '') continue\n url.searchParams.set(key, String(value))\n }\n return url.toString()\n }\n\n async function readList<T>(pathOrUrl: string, params?: Record<string, string | number | boolean | undefined | null>): Promise<{\n items: T[]\n nextUrl: string | null\n totalEstimate: number | null\n }> {\n const payload = await request<AkeneoListResponse<T>>(\n params ? buildUrl(pathOrUrl, params) : pathOrUrl,\n )\n return {\n items: Array.isArray(payload?._embedded?.items) ? payload._embedded.items : [],\n nextUrl: typeof payload?._links?.next?.href === 'string'\n ? normalizeAkeneoNextUrl(payload._links.next.href)\n : null,\n totalEstimate: typeof payload?.items_count === 'number' ? payload.items_count : null,\n }\n }\n\n async function countProducts(updatedAfter?: string | null): Promise<number | null> {\n const params: Record<string, string | number | boolean | undefined | null> = {\n limit: 1,\n pagination_type: 'page',\n with_count: true,\n }\n const normalizedUpdatedAfter = normalizeAkeneoDateTime(updatedAfter)\n if (normalizedUpdatedAfter) {\n params.search = JSON.stringify({\n updated: [\n {\n operator: '>',\n value: normalizedUpdatedAfter,\n },\n ],\n })\n }\n\n const page = await readList<AkeneoProduct>('/api/rest/v1/products-uuid', params)\n return page.totalEstimate\n }\n\n async function getSystemProbe(): Promise<{ version: string | null }> {\n try {\n const result = await request<Record<string, unknown>>('/api/rest/v1/system-information')\n return {\n version: typeof result.pim_version === 'string' ? result.pim_version : null,\n }\n } catch {\n const attrs = await readList<AkeneoAttribute>('/api/rest/v1/attributes', {\n limit: 1,\n pagination_type: 'page',\n })\n return { version: attrs.items.length >= 0 ? null : null }\n }\n }\n\n async function listProducts(options: {\n nextUrl?: string | null\n batchSize: number\n updatedAfter?: string | null\n }): Promise<{ items: AkeneoProduct[]; nextUrl: string | null; totalEstimate: number | null }> {\n if (options.nextUrl) {\n const page = await readList<AkeneoProduct>(normalizeAkeneoNextUrl(sanitizeAkeneoProductNextUrl(options.nextUrl)))\n return {\n ...page,\n nextUrl: page.nextUrl ? normalizeAkeneoNextUrl(sanitizeAkeneoProductNextUrl(page.nextUrl)) : null,\n }\n }\n const params: Record<string, string | number | boolean | undefined | null> = {\n limit: Math.min(Math.max(options.batchSize, 1), 100),\n pagination_type: 'search_after',\n with_count: false,\n }\n const normalizedUpdatedAfter = normalizeAkeneoDateTime(options.updatedAfter)\n if (normalizedUpdatedAfter) {\n params.search = JSON.stringify({\n updated: [\n {\n operator: '>',\n value: normalizedUpdatedAfter,\n },\n ],\n })\n }\n const [page, totalEstimate] = await Promise.all([\n readList<AkeneoProduct>('/api/rest/v1/products-uuid', params),\n countProducts(normalizedUpdatedAfter).catch(() => null),\n ])\n return {\n ...page,\n totalEstimate: totalEstimate ?? page.totalEstimate,\n nextUrl: page.nextUrl ? normalizeAkeneoNextUrl(sanitizeAkeneoProductNextUrl(page.nextUrl)) : null,\n }\n }\n\n async function listCategories(nextUrl?: string | null, batchSize = 100): Promise<{ items: AkeneoCategory[]; nextUrl: string | null; totalEstimate: number | null }> {\n if (nextUrl) return readList<AkeneoCategory>(nextUrl)\n return readList<AkeneoCategory>('/api/rest/v1/categories', {\n limit: Math.min(Math.max(batchSize, 1), 100),\n pagination_type: 'page',\n with_count: true,\n })\n }\n\n async function listAttributes(nextUrl?: string | null, batchSize = 100): Promise<{ items: AkeneoAttribute[]; nextUrl: string | null; totalEstimate: number | null }> {\n if (nextUrl) return readList<AkeneoAttribute>(nextUrl)\n return readList<AkeneoAttribute>('/api/rest/v1/attributes', {\n limit: Math.min(Math.max(batchSize, 1), 100),\n pagination_type: 'page',\n with_count: true,\n })\n }\n\n async function listFamilies(nextUrl?: string | null, batchSize = 100): Promise<{ items: AkeneoFamily[]; nextUrl: string | null; totalEstimate: number | null }> {\n if (nextUrl) return readList<AkeneoFamily>(nextUrl)\n return readList<AkeneoFamily>('/api/rest/v1/families', {\n limit: Math.min(Math.max(batchSize, 1), 100),\n pagination_type: 'page',\n with_count: true,\n })\n }\n\n async function listFamilyVariants(\n familyCode: string,\n nextUrl?: string | null,\n batchSize = 100,\n ): Promise<{ items: AkeneoFamilyVariant[]; nextUrl: string | null; totalEstimate: number | null }> {\n if (nextUrl) return readList<AkeneoFamilyVariant>(nextUrl)\n return readList<AkeneoFamilyVariant>(`/api/rest/v1/families/${encodeURIComponent(familyCode)}/variants`, {\n limit: Math.min(Math.max(batchSize, 1), 100),\n with_count: false,\n })\n }\n\n async function listChannels(): Promise<AkeneoChannel[]> {\n const response = await readList<AkeneoChannel>('/api/rest/v1/channels', {\n limit: 100,\n pagination_type: 'page',\n with_count: true,\n })\n return response.items\n }\n\n async function listLocales(): Promise<AkeneoLocale[]> {\n const response = await readList<AkeneoLocale>('/api/rest/v1/locales', {\n limit: 100,\n pagination_type: 'page',\n with_count: true,\n })\n return response.items\n }\n\n async function getCategory(code: string): Promise<AkeneoCategory | null> {\n if (!categoryCache.has(code)) {\n categoryCache.set(code, request<AkeneoCategory>(`/api/rest/v1/categories/${encodeURIComponent(code)}`).catch(() => null))\n }\n return categoryCache.get(code) ?? null\n }\n\n async function getAttribute(code: string): Promise<AkeneoAttribute | null> {\n if (!attributeCache.has(code)) {\n attributeCache.set(code, request<AkeneoAttribute>(`/api/rest/v1/attributes/${encodeURIComponent(code)}`).catch(() => null))\n }\n return attributeCache.get(code) ?? null\n }\n\n async function listAttributeOptions(attributeCode: string): Promise<AkeneoAttributeOption[]> {\n const options: AkeneoAttributeOption[] = []\n let nextUrl: string | null | undefined = null\n do {\n const now = Date.now()\n const waitMs = Math.max(0, 350 - (now - lastAttributeOptionRequestAt))\n if (waitMs > 0) await sleep(waitMs)\n lastAttributeOptionRequestAt = Date.now()\n const page: { items: AkeneoAttributeOption[]; nextUrl: string | null; totalEstimate: number | null } = await readList<AkeneoAttributeOption>(\n nextUrl ?? `/api/rest/v1/attributes/${encodeURIComponent(attributeCode)}/options`,\n nextUrl\n ? undefined\n : {\n limit: 100,\n pagination_type: 'page',\n with_count: true,\n },\n )\n options.push(...page.items)\n nextUrl = page.nextUrl\n } while (nextUrl)\n return options\n }\n\n async function getFamily(code: string): Promise<AkeneoFamily | null> {\n if (!familyCache.has(code)) {\n familyCache.set(code, request<AkeneoFamily>(`/api/rest/v1/families/${encodeURIComponent(code)}`).catch(() => null))\n }\n return familyCache.get(code) ?? null\n }\n\n async function getFamilyVariant(familyCode: string, familyVariantCode: string): Promise<AkeneoFamilyVariant | null> {\n const cacheKey = `${familyCode}:${familyVariantCode}`\n if (!familyVariantCache.has(cacheKey)) {\n familyVariantCache.set(\n cacheKey,\n request<AkeneoFamilyVariant>(`/api/rest/v1/families/${encodeURIComponent(familyCode)}/variants/${encodeURIComponent(familyVariantCode)}`).catch(() => null),\n )\n }\n return familyVariantCache.get(cacheKey) ?? null\n }\n\n async function getProductModel(code: string): Promise<AkeneoProductModel | null> {\n if (!productModelCache.has(code)) {\n productModelCache.set(code, request<AkeneoProductModel>(`/api/rest/v1/product-models/${encodeURIComponent(code)}`).catch(() => null))\n }\n return productModelCache.get(code) ?? null\n }\n\n async function getMediaFile(code: string): Promise<AkeneoMediaFile | null> {\n if (!mediaFileCache.has(code)) {\n mediaFileCache.set(code, request<AkeneoMediaFile>(`/api/rest/v1/media-files/${encodeAkeneoPathParam(code)}`).catch(() => null))\n }\n return mediaFileCache.get(code) ?? null\n }\n\n async function downloadMediaFile(codeOrUrl: string): Promise<{\n buffer: Buffer\n contentType: string | null\n contentLength: number | null\n fileName: string | null\n code: string | null\n }> {\n if (codeOrUrl.startsWith('http://') || codeOrUrl.startsWith('https://') || codeOrUrl.startsWith('/')) {\n const binary = await requestBinary(codeOrUrl)\n return {\n ...binary,\n fileName: null,\n code: null,\n }\n }\n\n const mediaFile = await getMediaFile(codeOrUrl)\n if (!mediaFile) {\n throw new Error(`Akeneo media file ${codeOrUrl} was not found`)\n }\n const downloadHref = mediaFile._links?.download?.href\n const binary = await requestBinary(downloadHref || `/api/rest/v1/media-files/${encodeAkeneoPathParam(codeOrUrl)}/download`)\n return {\n ...binary,\n fileName: typeof mediaFile.original_filename === 'string' && mediaFile.original_filename.trim().length > 0\n ? mediaFile.original_filename.trim()\n : null,\n code: mediaFile.code ?? codeOrUrl,\n }\n }\n\n async function collectDiscoveryData(): Promise<{\n locales: Array<{ code: string; label: string; enabled: boolean }>\n channels: Array<{ code: string; label: string; locales: string[] }>\n attributes: Array<{ code: string; type: string; label: string; localizable: boolean; scopable: boolean; group?: string; metricFamily?: string }>\n families: Array<{ code: string; label: string; attributeCount: number }>\n familyVariants: Array<{ familyCode: string; code: string; label: string; axes: string[]; attributes: string[] }>\n version: string | null\n }> {\n const [locales, channels, attributes, families, probe] = await Promise.all([\n listLocales().catch(() => []),\n listChannels().catch(() => []),\n listAttributes(null, 100),\n listFamilies(null, 100),\n getSystemProbe().catch(() => ({ version: null })),\n ])\n\n const familyVariants: Array<{ familyCode: string; code: string; label: string; axes: string[]; attributes: string[] }> = []\n for (const family of families.items) {\n let nextUrl: string | null = null\n do {\n const page: { items: AkeneoFamilyVariant[]; nextUrl: string | null; totalEstimate: number | null } = await listFamilyVariants(family.code, nextUrl, 100).catch(() => ({\n items: [] as AkeneoFamilyVariant[],\n nextUrl: null,\n totalEstimate: null,\n }))\n familyVariants.push(...page.items.map((familyVariant) => ({\n familyCode: family.code,\n code: familyVariant.code,\n label: labelFromLocalizedRecord(familyVariant.labels ?? null, null, familyVariant.code),\n axes: dedupeStrings(familyVariant.variant_attribute_sets?.flatMap((set: { axes?: string[] }) => Array.isArray(set.axes) ? set.axes : []) ?? []),\n attributes: dedupeStrings(familyVariant.variant_attribute_sets?.flatMap((set: { attributes?: string[] }) => Array.isArray(set.attributes) ? set.attributes : []) ?? []),\n })))\n nextUrl = page.nextUrl\n } while (nextUrl)\n }\n\n return {\n locales: locales.map((locale) => ({\n code: locale.code,\n label: labelFromLocalizedRecord(locale.labels ?? null, null, locale.code),\n enabled: locale.enabled ?? false,\n })),\n channels: channels.map((channel) => ({\n code: channel.code,\n label: labelFromLocalizedRecord(channel.labels ?? null, null, channel.code),\n locales: dedupeStrings(channel.locales ?? []),\n })),\n attributes: attributes.items.map((attribute) => ({\n code: attribute.code,\n type: attribute.type,\n label: labelFromLocalizedRecord(attribute.labels ?? null, null, attribute.code),\n localizable: Boolean(attribute.localizable),\n scopable: Boolean(attribute.scopable),\n group: typeof attribute.group === 'string' && attribute.group.trim().length > 0 ? attribute.group.trim() : undefined,\n metricFamily: typeof attribute.metric_family === 'string' && attribute.metric_family.trim().length > 0\n ? attribute.metric_family.trim()\n : undefined,\n })),\n families: families.items.map((family) => ({\n code: family.code,\n label: labelFromLocalizedRecord(family.labels ?? null, null, family.code),\n attributeCount: Array.isArray(family.attributes) ? family.attributes.length : 0,\n })),\n familyVariants,\n version: probe.version,\n }\n }\n\n return {\n credentials,\n getSystemProbe,\n collectDiscoveryData,\n listProducts,\n listCategories,\n listAttributes,\n listFamilies,\n listFamilyVariants,\n listChannels,\n listLocales,\n getCategory,\n getAttribute,\n listAttributeOptions,\n getFamily,\n getFamilyVariant,\n getProductModel,\n getMediaFile,\n downloadMediaFile,\n }\n}\n\nexport type { AkeneoClient }\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import { dedupeStrings, labelFromLocalizedRecord, safeRecord, type AkeneoAttribute, type AkeneoAttributeOption, type AkeneoCategory, type AkeneoChannel, type AkeneoCredentialShape, type AkeneoFamily, type AkeneoFamilyVariant, type AkeneoLocale, type AkeneoProduct, type AkeneoProductModel } from './shared'\n\ntype TokenState = {\n accessToken: string\n refreshToken?: string | null\n expiresAt: number\n}\n\ntype AkeneoListResponse<T> = {\n _embedded?: {\n items?: T[]\n }\n _links?: {\n next?: {\n href?: string\n }\n }\n items_count?: number\n}\n\ntype AkeneoMediaFile = {\n code: string\n original_filename?: string | null\n mime_type?: string | null\n size?: number | null\n extension?: string | null\n _links?: {\n download?: {\n href?: string\n }\n }\n}\n\ntype AkeneoClient = ReturnType<typeof createAkeneoClient>\n\nconst DEFAULT_ALLOWED_AKENEO_HOST_PATTERNS = ['*.cloud.akeneo.com', '*.akeneo.cloud'] as const\nconst ALLOWED_AKENEO_HOSTS_ENV_KEYS = [\n 'OM_INTEGRATION_AKENEO_ALLOWED_HOSTS',\n 'OPENMERCATO_AKENEO_ALLOWED_HOSTS',\n 'AKENEO_ALLOWED_HOSTS',\n] as const\n\nfunction normalizeBaseUrl(url: string): string {\n return url.trim().replace(/\\/+$/g, '')\n}\n\nfunction readAllowedAkeneoHostPatterns(env: NodeJS.ProcessEnv = process.env): string[] {\n for (const key of ALLOWED_AKENEO_HOSTS_ENV_KEYS) {\n const raw = env[key]\n if (typeof raw !== 'string') continue\n const patterns = raw\n .split(',')\n .map((entry) => entry.trim().toLowerCase())\n .filter((entry) => entry.length > 0)\n if (patterns.length > 0) {\n return patterns\n }\n }\n\n return [...DEFAULT_ALLOWED_AKENEO_HOST_PATTERNS]\n}\n\nfunction matchesAkeneoHostPattern(hostname: string, pattern: string): boolean {\n if (pattern.startsWith('*.')) {\n const suffix = pattern.slice(2)\n return hostname === suffix || hostname.endsWith(`.${suffix}`)\n }\n\n return hostname === pattern\n}\n\nexport function validateAkeneoApiUrl(rawUrl: string, env: NodeJS.ProcessEnv = process.env): string {\n let parsed: URL\n try {\n parsed = new URL(rawUrl.trim())\n } catch {\n throw new Error('Akeneo URL must be a valid absolute URL')\n }\n\n if (parsed.protocol !== 'https:') {\n throw new Error('Akeneo URL must use https')\n }\n if (parsed.username || parsed.password) {\n throw new Error('Akeneo URL must not include embedded credentials')\n }\n if (parsed.port && parsed.port !== '443') {\n throw new Error('Akeneo URL must not use a custom port')\n }\n if (parsed.pathname !== '/' && parsed.pathname !== '') {\n throw new Error('Akeneo URL must not include a path')\n }\n if (parsed.search || parsed.hash) {\n throw new Error('Akeneo URL must not include query parameters or fragments')\n }\n\n const hostname = parsed.hostname.toLowerCase()\n const allowedPatterns = readAllowedAkeneoHostPatterns(env)\n if (!allowedPatterns.some((pattern) => matchesAkeneoHostPattern(hostname, pattern))) {\n throw new Error(`Akeneo URL host is not allowed: ${hostname}`)\n }\n\n return normalizeBaseUrl(parsed.origin)\n}\n\nexport function normalizeAkeneoDateTime(value: string | null | undefined): string | null {\n if (typeof value !== 'string' || value.trim().length === 0) return null\n const trimmed = value.trim()\n if (/^\\d{4}-\\d{2}-\\d{2} \\d{2}:\\d{2}:\\d{2}$/.test(trimmed)) {\n return trimmed\n }\n\n const parsed = new Date(trimmed)\n if (Number.isNaN(parsed.getTime())) {\n return null\n }\n\n const year = parsed.getUTCFullYear()\n const month = String(parsed.getUTCMonth() + 1).padStart(2, '0')\n const day = String(parsed.getUTCDate()).padStart(2, '0')\n const hours = String(parsed.getUTCHours()).padStart(2, '0')\n const minutes = String(parsed.getUTCMinutes()).padStart(2, '0')\n const seconds = String(parsed.getUTCSeconds()).padStart(2, '0')\n return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`\n}\n\nexport function encodeAkeneoPathParam(value: string): string {\n return value\n .split('/')\n .map((segment) => encodeURIComponent(segment))\n .join('/')\n}\n\nexport function sanitizeAkeneoProductNextUrl(nextUrl: string): string {\n let url: URL\n try {\n url = new URL(nextUrl)\n } catch {\n return nextUrl\n }\n\n const rawSearch = url.searchParams.get('search')\n if (!rawSearch) {\n return url.toString()\n }\n\n try {\n const parsed = JSON.parse(rawSearch) as Record<string, unknown>\n const updatedFilters = Array.isArray(parsed.updated) ? parsed.updated : []\n const sanitizedUpdated = updatedFilters\n .map((entry) => {\n if (!entry || typeof entry !== 'object') return null\n const record = entry as Record<string, unknown>\n const normalizedValue = normalizeAkeneoDateTime(\n typeof record.value === 'string' ? record.value : null,\n )\n if (!normalizedValue) return null\n return {\n ...record,\n value: normalizedValue,\n }\n })\n .filter((entry) => entry !== null) as Record<string, unknown>[]\n\n if (sanitizedUpdated.length === 0) {\n delete parsed.updated\n } else {\n parsed.updated = sanitizedUpdated\n }\n\n if (Object.keys(parsed).length === 0) {\n url.searchParams.delete('search')\n } else {\n url.searchParams.set('search', JSON.stringify(parsed))\n }\n return url.toString()\n } catch {\n return nextUrl\n }\n}\n\nfunction coerceCredentials(credentials: Record<string, unknown>): AkeneoCredentialShape {\n const apiUrl = typeof credentials.apiUrl === 'string' ? credentials.apiUrl.trim() : ''\n const clientId = typeof credentials.clientId === 'string' ? credentials.clientId.trim() : ''\n const clientSecret = typeof credentials.clientSecret === 'string' ? credentials.clientSecret : ''\n const username = typeof credentials.username === 'string' ? credentials.username.trim() : ''\n const password = typeof credentials.password === 'string' ? credentials.password : ''\n if (!apiUrl || !clientId || !clientSecret || !username || !password) {\n throw new Error('Akeneo credentials are incomplete')\n }\n return {\n apiUrl: validateAkeneoApiUrl(apiUrl),\n clientId,\n clientSecret,\n username,\n password,\n }\n}\n\nfunction sleep(ms: number): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, ms))\n}\n\nconst DEFAULT_AKENEO_REQUEST_TIMEOUT_MS = 30_000\nconst DEFAULT_AKENEO_MAX_RATE_LIMIT_RETRIES = 5\nconst DEFAULT_AKENEO_RETRY_AFTER_CAP_MS = 60_000\n\nfunction resolvePositiveIntEnv(raw: string | undefined, fallback: number): number {\n const parsed = Number.parseInt(raw ?? '', 10)\n return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback\n}\n\nfunction resolveAkeneoRequestTimeoutMs(env: NodeJS.ProcessEnv = process.env): number {\n return resolvePositiveIntEnv(env.OM_INTEGRATION_AKENEO_REQUEST_TIMEOUT_MS, DEFAULT_AKENEO_REQUEST_TIMEOUT_MS)\n}\n\nfunction resolveAkeneoMaxRateLimitRetries(env: NodeJS.ProcessEnv = process.env): number {\n return resolvePositiveIntEnv(env.OM_INTEGRATION_AKENEO_MAX_RATE_LIMIT_RETRIES, DEFAULT_AKENEO_MAX_RATE_LIMIT_RETRIES)\n}\n\nfunction clampAkeneoRetryAfterMs(retryAfterHeader: string | null, env: NodeJS.ProcessEnv = process.env): number {\n const retryAfter = Number(retryAfterHeader ?? '1')\n const requestedMs = Number.isFinite(retryAfter) ? retryAfter * 1000 : 1000\n const cap = resolvePositiveIntEnv(env.OM_INTEGRATION_AKENEO_RETRY_AFTER_CAP_MS, DEFAULT_AKENEO_RETRY_AFTER_CAP_MS)\n return Math.min(Math.max(requestedMs, 0), cap)\n}\n\nexport function createAkeneoClient(credentialsInput: Record<string, unknown>) {\n const credentials = coerceCredentials(credentialsInput)\n const akeneoBaseUrl = new URL(`${credentials.apiUrl}/`)\n const tokenEndpointUrl = new URL('/api/oauth/v1/token', akeneoBaseUrl).toString()\n let tokenState: TokenState | null = null\n let lastAttributeOptionRequestAt = 0\n const familyCache = new Map<string, Promise<AkeneoFamily | null>>()\n const familyVariantCache = new Map<string, Promise<AkeneoFamilyVariant | null>>()\n const productModelCache = new Map<string, Promise<AkeneoProductModel | null>>()\n const attributeCache = new Map<string, Promise<AkeneoAttribute | null>>()\n const categoryCache = new Map<string, Promise<AkeneoCategory | null>>()\n const mediaFileCache = new Map<string, Promise<AkeneoMediaFile | null>>()\n\n function resolveAkeneoRequestUrl(pathOrUrl: string): string {\n const resolved = new URL(pathOrUrl, akeneoBaseUrl)\n if (resolved.origin !== akeneoBaseUrl.origin) {\n throw new Error(`Akeneo request URL must stay on the configured host: ${resolved.origin}`)\n }\n return resolved.toString()\n }\n\n function normalizeAkeneoNextUrl(nextUrl: string): string {\n return resolveAkeneoRequestUrl(nextUrl)\n }\n\n async function acquirePasswordGrantToken(): Promise<TokenState> {\n const response = await fetch(tokenEndpointUrl, {\n method: 'POST',\n signal: AbortSignal.timeout(resolveAkeneoRequestTimeoutMs()),\n headers: {\n accept: 'application/json',\n 'content-type': 'application/json',\n },\n body: JSON.stringify({\n grant_type: 'password',\n client_id: credentials.clientId,\n client_secret: credentials.clientSecret,\n username: credentials.username,\n password: credentials.password,\n }),\n })\n\n if (!response.ok) {\n const message = await response.text()\n throw new Error(`Akeneo authentication failed (${response.status}): ${message}`)\n }\n\n const payload = await response.json() as {\n access_token?: string\n refresh_token?: string\n expires_in?: number\n }\n if (!payload.access_token) {\n throw new Error('Akeneo authentication did not return an access token')\n }\n\n return {\n accessToken: payload.access_token,\n refreshToken: payload.refresh_token ?? null,\n expiresAt: Date.now() + ((payload.expires_in ?? 3600) * 1000) - 10_000,\n }\n }\n\n async function refreshAccessToken(current: TokenState): Promise<TokenState> {\n if (!current.refreshToken) return acquirePasswordGrantToken()\n const response = await fetch(tokenEndpointUrl, {\n method: 'POST',\n signal: AbortSignal.timeout(resolveAkeneoRequestTimeoutMs()),\n headers: {\n accept: 'application/json',\n 'content-type': 'application/json',\n },\n body: JSON.stringify({\n grant_type: 'refresh_token',\n client_id: credentials.clientId,\n client_secret: credentials.clientSecret,\n refresh_token: current.refreshToken,\n }),\n })\n\n if (!response.ok) {\n return acquirePasswordGrantToken()\n }\n\n const payload = await response.json() as {\n access_token?: string\n refresh_token?: string\n expires_in?: number\n }\n if (!payload.access_token) return acquirePasswordGrantToken()\n return {\n accessToken: payload.access_token,\n refreshToken: payload.refresh_token ?? current.refreshToken ?? null,\n expiresAt: Date.now() + ((payload.expires_in ?? 3600) * 1000) - 10_000,\n }\n }\n\n async function ensureToken(forceRefresh = false): Promise<string> {\n if (!forceRefresh && tokenState && tokenState.expiresAt > Date.now()) {\n return tokenState.accessToken\n }\n tokenState = tokenState ? await refreshAccessToken(tokenState) : await acquirePasswordGrantToken()\n return tokenState.accessToken\n }\n\n async function request<T>(\n pathOrUrl: string,\n init: RequestInit = {},\n retried = false,\n rateLimitAttempts = 0,\n ): Promise<T> {\n const url = resolveAkeneoRequestUrl(pathOrUrl)\n const token = await ensureToken()\n\n const response = await fetch(url, {\n ...init,\n signal: init.signal ?? AbortSignal.timeout(resolveAkeneoRequestTimeoutMs()),\n headers: {\n accept: 'application/json',\n authorization: `Bearer ${token}`,\n ...init.headers,\n },\n })\n\n if (response.status === 401 && !retried) {\n await ensureToken(true)\n return request<T>(pathOrUrl, init, true, rateLimitAttempts)\n }\n\n if (response.status === 429) {\n if (rateLimitAttempts >= resolveAkeneoMaxRateLimitRetries()) {\n const body = await response.text()\n throw new Error(`Akeneo request failed (429): ${body}`)\n }\n await sleep(clampAkeneoRetryAfterMs(response.headers.get('retry-after')))\n return request<T>(pathOrUrl, init, retried, rateLimitAttempts + 1)\n }\n\n if (!response.ok) {\n const body = await response.text()\n throw new Error(`Akeneo request failed (${response.status}): ${body}`)\n }\n\n if (response.status === 204) {\n return null as T\n }\n\n return response.json() as Promise<T>\n }\n\n async function requestBinary(\n pathOrUrl: string,\n init: RequestInit = {},\n retried = false,\n rateLimitAttempts = 0,\n ): Promise<{\n buffer: Buffer\n contentType: string | null\n contentLength: number | null\n }> {\n const url = resolveAkeneoRequestUrl(pathOrUrl)\n const token = await ensureToken()\n\n const response = await fetch(url, {\n ...init,\n signal: init.signal ?? AbortSignal.timeout(resolveAkeneoRequestTimeoutMs()),\n headers: {\n authorization: `Bearer ${token}`,\n ...init.headers,\n },\n })\n\n if (response.status === 401 && !retried) {\n await ensureToken(true)\n return requestBinary(pathOrUrl, init, true, rateLimitAttempts)\n }\n\n if (response.status === 429) {\n if (rateLimitAttempts >= resolveAkeneoMaxRateLimitRetries()) {\n const body = await response.text()\n throw new Error(`Akeneo request failed (429): ${body}`)\n }\n await sleep(clampAkeneoRetryAfterMs(response.headers.get('retry-after')))\n return requestBinary(pathOrUrl, init, retried, rateLimitAttempts + 1)\n }\n\n if (!response.ok) {\n const body = await response.text()\n throw new Error(`Akeneo request failed (${response.status}): ${body}`)\n }\n\n const arrayBuffer = await response.arrayBuffer()\n return {\n buffer: Buffer.from(arrayBuffer),\n contentType: response.headers.get('content-type'),\n contentLength: Number(response.headers.get('content-length') ?? '') || arrayBuffer.byteLength || null,\n }\n }\n\n function buildUrl(path: string, params: Record<string, string | number | boolean | undefined | null>): string {\n const url = new URL(resolveAkeneoRequestUrl(path))\n for (const [key, value] of Object.entries(params)) {\n if (value === undefined || value === null || value === '') continue\n url.searchParams.set(key, String(value))\n }\n return url.toString()\n }\n\n async function readList<T>(pathOrUrl: string, params?: Record<string, string | number | boolean | undefined | null>): Promise<{\n items: T[]\n nextUrl: string | null\n totalEstimate: number | null\n }> {\n const payload = await request<AkeneoListResponse<T>>(\n params ? buildUrl(pathOrUrl, params) : pathOrUrl,\n )\n return {\n items: Array.isArray(payload?._embedded?.items) ? payload._embedded.items : [],\n nextUrl: typeof payload?._links?.next?.href === 'string'\n ? normalizeAkeneoNextUrl(payload._links.next.href)\n : null,\n totalEstimate: typeof payload?.items_count === 'number' ? payload.items_count : null,\n }\n }\n\n async function countProducts(updatedAfter?: string | null): Promise<number | null> {\n const params: Record<string, string | number | boolean | undefined | null> = {\n limit: 1,\n pagination_type: 'page',\n with_count: true,\n }\n const normalizedUpdatedAfter = normalizeAkeneoDateTime(updatedAfter)\n if (normalizedUpdatedAfter) {\n params.search = JSON.stringify({\n updated: [\n {\n operator: '>',\n value: normalizedUpdatedAfter,\n },\n ],\n })\n }\n\n const page = await readList<AkeneoProduct>('/api/rest/v1/products-uuid', params)\n return page.totalEstimate\n }\n\n async function getSystemProbe(): Promise<{ version: string | null }> {\n try {\n const result = await request<Record<string, unknown>>('/api/rest/v1/system-information')\n return {\n version: typeof result.pim_version === 'string' ? result.pim_version : null,\n }\n } catch {\n const attrs = await readList<AkeneoAttribute>('/api/rest/v1/attributes', {\n limit: 1,\n pagination_type: 'page',\n })\n return { version: attrs.items.length >= 0 ? null : null }\n }\n }\n\n async function listProducts(options: {\n nextUrl?: string | null\n batchSize: number\n updatedAfter?: string | null\n }): Promise<{ items: AkeneoProduct[]; nextUrl: string | null; totalEstimate: number | null }> {\n if (options.nextUrl) {\n const page = await readList<AkeneoProduct>(normalizeAkeneoNextUrl(sanitizeAkeneoProductNextUrl(options.nextUrl)))\n return {\n ...page,\n nextUrl: page.nextUrl ? normalizeAkeneoNextUrl(sanitizeAkeneoProductNextUrl(page.nextUrl)) : null,\n }\n }\n const params: Record<string, string | number | boolean | undefined | null> = {\n limit: Math.min(Math.max(options.batchSize, 1), 100),\n pagination_type: 'search_after',\n with_count: false,\n }\n const normalizedUpdatedAfter = normalizeAkeneoDateTime(options.updatedAfter)\n if (normalizedUpdatedAfter) {\n params.search = JSON.stringify({\n updated: [\n {\n operator: '>',\n value: normalizedUpdatedAfter,\n },\n ],\n })\n }\n const [page, totalEstimate] = await Promise.all([\n readList<AkeneoProduct>('/api/rest/v1/products-uuid', params),\n countProducts(normalizedUpdatedAfter).catch(() => null),\n ])\n return {\n ...page,\n totalEstimate: totalEstimate ?? page.totalEstimate,\n nextUrl: page.nextUrl ? normalizeAkeneoNextUrl(sanitizeAkeneoProductNextUrl(page.nextUrl)) : null,\n }\n }\n\n async function listCategories(nextUrl?: string | null, batchSize = 100): Promise<{ items: AkeneoCategory[]; nextUrl: string | null; totalEstimate: number | null }> {\n if (nextUrl) return readList<AkeneoCategory>(nextUrl)\n return readList<AkeneoCategory>('/api/rest/v1/categories', {\n limit: Math.min(Math.max(batchSize, 1), 100),\n pagination_type: 'page',\n with_count: true,\n })\n }\n\n async function listAttributes(nextUrl?: string | null, batchSize = 100): Promise<{ items: AkeneoAttribute[]; nextUrl: string | null; totalEstimate: number | null }> {\n if (nextUrl) return readList<AkeneoAttribute>(nextUrl)\n return readList<AkeneoAttribute>('/api/rest/v1/attributes', {\n limit: Math.min(Math.max(batchSize, 1), 100),\n pagination_type: 'page',\n with_count: true,\n })\n }\n\n async function listFamilies(nextUrl?: string | null, batchSize = 100): Promise<{ items: AkeneoFamily[]; nextUrl: string | null; totalEstimate: number | null }> {\n if (nextUrl) return readList<AkeneoFamily>(nextUrl)\n return readList<AkeneoFamily>('/api/rest/v1/families', {\n limit: Math.min(Math.max(batchSize, 1), 100),\n pagination_type: 'page',\n with_count: true,\n })\n }\n\n async function listFamilyVariants(\n familyCode: string,\n nextUrl?: string | null,\n batchSize = 100,\n ): Promise<{ items: AkeneoFamilyVariant[]; nextUrl: string | null; totalEstimate: number | null }> {\n if (nextUrl) return readList<AkeneoFamilyVariant>(nextUrl)\n return readList<AkeneoFamilyVariant>(`/api/rest/v1/families/${encodeURIComponent(familyCode)}/variants`, {\n limit: Math.min(Math.max(batchSize, 1), 100),\n with_count: false,\n })\n }\n\n async function listChannels(): Promise<AkeneoChannel[]> {\n const response = await readList<AkeneoChannel>('/api/rest/v1/channels', {\n limit: 100,\n pagination_type: 'page',\n with_count: true,\n })\n return response.items\n }\n\n async function listLocales(): Promise<AkeneoLocale[]> {\n const response = await readList<AkeneoLocale>('/api/rest/v1/locales', {\n limit: 100,\n pagination_type: 'page',\n with_count: true,\n })\n return response.items\n }\n\n async function getCategory(code: string): Promise<AkeneoCategory | null> {\n if (!categoryCache.has(code)) {\n categoryCache.set(code, request<AkeneoCategory>(`/api/rest/v1/categories/${encodeURIComponent(code)}`).catch(() => null))\n }\n return categoryCache.get(code) ?? null\n }\n\n async function getAttribute(code: string): Promise<AkeneoAttribute | null> {\n if (!attributeCache.has(code)) {\n attributeCache.set(code, request<AkeneoAttribute>(`/api/rest/v1/attributes/${encodeURIComponent(code)}`).catch(() => null))\n }\n return attributeCache.get(code) ?? null\n }\n\n async function listAttributeOptions(attributeCode: string): Promise<AkeneoAttributeOption[]> {\n const options: AkeneoAttributeOption[] = []\n let nextUrl: string | null | undefined = null\n do {\n const now = Date.now()\n const waitMs = Math.max(0, 350 - (now - lastAttributeOptionRequestAt))\n if (waitMs > 0) await sleep(waitMs)\n lastAttributeOptionRequestAt = Date.now()\n const page: { items: AkeneoAttributeOption[]; nextUrl: string | null; totalEstimate: number | null } = await readList<AkeneoAttributeOption>(\n nextUrl ?? `/api/rest/v1/attributes/${encodeURIComponent(attributeCode)}/options`,\n nextUrl\n ? undefined\n : {\n limit: 100,\n pagination_type: 'page',\n with_count: true,\n },\n )\n options.push(...page.items)\n nextUrl = page.nextUrl\n } while (nextUrl)\n return options\n }\n\n async function getFamily(code: string): Promise<AkeneoFamily | null> {\n if (!familyCache.has(code)) {\n familyCache.set(code, request<AkeneoFamily>(`/api/rest/v1/families/${encodeURIComponent(code)}`).catch(() => null))\n }\n return familyCache.get(code) ?? null\n }\n\n async function getFamilyVariant(familyCode: string, familyVariantCode: string): Promise<AkeneoFamilyVariant | null> {\n const cacheKey = `${familyCode}:${familyVariantCode}`\n if (!familyVariantCache.has(cacheKey)) {\n familyVariantCache.set(\n cacheKey,\n request<AkeneoFamilyVariant>(`/api/rest/v1/families/${encodeURIComponent(familyCode)}/variants/${encodeURIComponent(familyVariantCode)}`).catch(() => null),\n )\n }\n return familyVariantCache.get(cacheKey) ?? null\n }\n\n async function getProductModel(code: string): Promise<AkeneoProductModel | null> {\n if (!productModelCache.has(code)) {\n productModelCache.set(code, request<AkeneoProductModel>(`/api/rest/v1/product-models/${encodeURIComponent(code)}`).catch(() => null))\n }\n return productModelCache.get(code) ?? null\n }\n\n async function getMediaFile(code: string): Promise<AkeneoMediaFile | null> {\n if (!mediaFileCache.has(code)) {\n mediaFileCache.set(code, request<AkeneoMediaFile>(`/api/rest/v1/media-files/${encodeAkeneoPathParam(code)}`).catch(() => null))\n }\n return mediaFileCache.get(code) ?? null\n }\n\n async function downloadMediaFile(codeOrUrl: string): Promise<{\n buffer: Buffer\n contentType: string | null\n contentLength: number | null\n fileName: string | null\n code: string | null\n }> {\n if (codeOrUrl.startsWith('http://') || codeOrUrl.startsWith('https://') || codeOrUrl.startsWith('/')) {\n const binary = await requestBinary(codeOrUrl)\n return {\n ...binary,\n fileName: null,\n code: null,\n }\n }\n\n const mediaFile = await getMediaFile(codeOrUrl)\n if (!mediaFile) {\n throw new Error(`Akeneo media file ${codeOrUrl} was not found`)\n }\n const downloadHref = mediaFile._links?.download?.href\n const binary = await requestBinary(downloadHref || `/api/rest/v1/media-files/${encodeAkeneoPathParam(codeOrUrl)}/download`)\n return {\n ...binary,\n fileName: typeof mediaFile.original_filename === 'string' && mediaFile.original_filename.trim().length > 0\n ? mediaFile.original_filename.trim()\n : null,\n code: mediaFile.code ?? codeOrUrl,\n }\n }\n\n async function collectDiscoveryData(): Promise<{\n locales: Array<{ code: string; label: string; enabled: boolean }>\n channels: Array<{ code: string; label: string; locales: string[] }>\n attributes: Array<{ code: string; type: string; label: string; localizable: boolean; scopable: boolean; group?: string; metricFamily?: string }>\n families: Array<{ code: string; label: string; attributeCount: number }>\n familyVariants: Array<{ familyCode: string; code: string; label: string; axes: string[]; attributes: string[] }>\n version: string | null\n }> {\n const [locales, channels, attributes, families, probe] = await Promise.all([\n listLocales().catch(() => []),\n listChannels().catch(() => []),\n listAttributes(null, 100),\n listFamilies(null, 100),\n getSystemProbe().catch(() => ({ version: null })),\n ])\n\n const familyVariants: Array<{ familyCode: string; code: string; label: string; axes: string[]; attributes: string[] }> = []\n for (const family of families.items) {\n let nextUrl: string | null = null\n do {\n const page: { items: AkeneoFamilyVariant[]; nextUrl: string | null; totalEstimate: number | null } = await listFamilyVariants(family.code, nextUrl, 100).catch(() => ({\n items: [] as AkeneoFamilyVariant[],\n nextUrl: null,\n totalEstimate: null,\n }))\n familyVariants.push(...page.items.map((familyVariant) => ({\n familyCode: family.code,\n code: familyVariant.code,\n label: labelFromLocalizedRecord(familyVariant.labels ?? null, null, familyVariant.code),\n axes: dedupeStrings(familyVariant.variant_attribute_sets?.flatMap((set: { axes?: string[] }) => Array.isArray(set.axes) ? set.axes : []) ?? []),\n attributes: dedupeStrings(familyVariant.variant_attribute_sets?.flatMap((set: { attributes?: string[] }) => Array.isArray(set.attributes) ? set.attributes : []) ?? []),\n })))\n nextUrl = page.nextUrl\n } while (nextUrl)\n }\n\n return {\n locales: locales.map((locale) => ({\n code: locale.code,\n label: labelFromLocalizedRecord(locale.labels ?? null, null, locale.code),\n enabled: locale.enabled ?? false,\n })),\n channels: channels.map((channel) => ({\n code: channel.code,\n label: labelFromLocalizedRecord(channel.labels ?? null, null, channel.code),\n locales: dedupeStrings(channel.locales ?? []),\n })),\n attributes: attributes.items.map((attribute) => ({\n code: attribute.code,\n type: attribute.type,\n label: labelFromLocalizedRecord(attribute.labels ?? null, null, attribute.code),\n localizable: Boolean(attribute.localizable),\n scopable: Boolean(attribute.scopable),\n group: typeof attribute.group === 'string' && attribute.group.trim().length > 0 ? attribute.group.trim() : undefined,\n metricFamily: typeof attribute.metric_family === 'string' && attribute.metric_family.trim().length > 0\n ? attribute.metric_family.trim()\n : undefined,\n })),\n families: families.items.map((family) => ({\n code: family.code,\n label: labelFromLocalizedRecord(family.labels ?? null, null, family.code),\n attributeCount: Array.isArray(family.attributes) ? family.attributes.length : 0,\n })),\n familyVariants,\n version: probe.version,\n }\n }\n\n return {\n credentials,\n getSystemProbe,\n collectDiscoveryData,\n listProducts,\n listCategories,\n listAttributes,\n listFamilies,\n listFamilyVariants,\n listChannels,\n listLocales,\n getCategory,\n getAttribute,\n listAttributeOptions,\n getFamily,\n getFamilyVariant,\n getProductModel,\n getMediaFile,\n downloadMediaFile,\n }\n}\n\nexport type { AkeneoClient }\n"],
5
+ "mappings": "AAAA,SAAS,eAAe,gCAAgR;AAmCxS,MAAM,uCAAuC,CAAC,sBAAsB,gBAAgB;AACpF,MAAM,gCAAgC;AAAA,EACpC;AAAA,EACA;AAAA,EACA;AACF;AAEA,SAAS,iBAAiB,KAAqB;AAC7C,SAAO,IAAI,KAAK,EAAE,QAAQ,SAAS,EAAE;AACvC;AAEA,SAAS,8BAA8B,MAAyB,QAAQ,KAAe;AACrF,aAAW,OAAO,+BAA+B;AAC/C,UAAM,MAAM,IAAI,GAAG;AACnB,QAAI,OAAO,QAAQ,SAAU;AAC7B,UAAM,WAAW,IACd,MAAM,GAAG,EACT,IAAI,CAAC,UAAU,MAAM,KAAK,EAAE,YAAY,CAAC,EACzC,OAAO,CAAC,UAAU,MAAM,SAAS,CAAC;AACrC,QAAI,SAAS,SAAS,GAAG;AACvB,aAAO;AAAA,IACT;AAAA,EACF;AAEA,SAAO,CAAC,GAAG,oCAAoC;AACjD;AAEA,SAAS,yBAAyB,UAAkB,SAA0B;AAC5E,MAAI,QAAQ,WAAW,IAAI,GAAG;AAC5B,UAAM,SAAS,QAAQ,MAAM,CAAC;AAC9B,WAAO,aAAa,UAAU,SAAS,SAAS,IAAI,MAAM,EAAE;AAAA,EAC9D;AAEA,SAAO,aAAa;AACtB;AAEO,SAAS,qBAAqB,QAAgB,MAAyB,QAAQ,KAAa;AACjG,MAAI;AACJ,MAAI;AACF,aAAS,IAAI,IAAI,OAAO,KAAK,CAAC;AAAA,EAChC,QAAQ;AACN,UAAM,IAAI,MAAM,yCAAyC;AAAA,EAC3D;AAEA,MAAI,OAAO,aAAa,UAAU;AAChC,UAAM,IAAI,MAAM,2BAA2B;AAAA,EAC7C;AACA,MAAI,OAAO,YAAY,OAAO,UAAU;AACtC,UAAM,IAAI,MAAM,kDAAkD;AAAA,EACpE;AACA,MAAI,OAAO,QAAQ,OAAO,SAAS,OAAO;AACxC,UAAM,IAAI,MAAM,uCAAuC;AAAA,EACzD;AACA,MAAI,OAAO,aAAa,OAAO,OAAO,aAAa,IAAI;AACrD,UAAM,IAAI,MAAM,oCAAoC;AAAA,EACtD;AACA,MAAI,OAAO,UAAU,OAAO,MAAM;AAChC,UAAM,IAAI,MAAM,2DAA2D;AAAA,EAC7E;AAEA,QAAM,WAAW,OAAO,SAAS,YAAY;AAC7C,QAAM,kBAAkB,8BAA8B,GAAG;AACzD,MAAI,CAAC,gBAAgB,KAAK,CAAC,YAAY,yBAAyB,UAAU,OAAO,CAAC,GAAG;AACnF,UAAM,IAAI,MAAM,mCAAmC,QAAQ,EAAE;AAAA,EAC/D;AAEA,SAAO,iBAAiB,OAAO,MAAM;AACvC;AAEO,SAAS,wBAAwB,OAAiD;AACvF,MAAI,OAAO,UAAU,YAAY,MAAM,KAAK,EAAE,WAAW,EAAG,QAAO;AACnE,QAAM,UAAU,MAAM,KAAK;AAC3B,MAAI,wCAAwC,KAAK,OAAO,GAAG;AACzD,WAAO;AAAA,EACT;AAEA,QAAM,SAAS,IAAI,KAAK,OAAO;AAC/B,MAAI,OAAO,MAAM,OAAO,QAAQ,CAAC,GAAG;AAClC,WAAO;AAAA,EACT;AAEA,QAAM,OAAO,OAAO,eAAe;AACnC,QAAM,QAAQ,OAAO,OAAO,YAAY,IAAI,CAAC,EAAE,SAAS,GAAG,GAAG;AAC9D,QAAM,MAAM,OAAO,OAAO,WAAW,CAAC,EAAE,SAAS,GAAG,GAAG;AACvD,QAAM,QAAQ,OAAO,OAAO,YAAY,CAAC,EAAE,SAAS,GAAG,GAAG;AAC1D,QAAM,UAAU,OAAO,OAAO,cAAc,CAAC,EAAE,SAAS,GAAG,GAAG;AAC9D,QAAM,UAAU,OAAO,OAAO,cAAc,CAAC,EAAE,SAAS,GAAG,GAAG;AAC9D,SAAO,GAAG,IAAI,IAAI,KAAK,IAAI,GAAG,IAAI,KAAK,IAAI,OAAO,IAAI,OAAO;AAC/D;AAEO,SAAS,sBAAsB,OAAuB;AAC3D,SAAO,MACJ,MAAM,GAAG,EACT,IAAI,CAAC,YAAY,mBAAmB,OAAO,CAAC,EAC5C,KAAK,GAAG;AACb;AAEO,SAAS,6BAA6B,SAAyB;AACpE,MAAI;AACJ,MAAI;AACF,UAAM,IAAI,IAAI,OAAO;AAAA,EACvB,QAAQ;AACN,WAAO;AAAA,EACT;AAEA,QAAM,YAAY,IAAI,aAAa,IAAI,QAAQ;AAC/C,MAAI,CAAC,WAAW;AACd,WAAO,IAAI,SAAS;AAAA,EACtB;AAEA,MAAI;AACF,UAAM,SAAS,KAAK,MAAM,SAAS;AACnC,UAAM,iBAAiB,MAAM,QAAQ,OAAO,OAAO,IAAI,OAAO,UAAU,CAAC;AACzE,UAAM,mBAAmB,eACtB,IAAI,CAAC,UAAU;AACd,UAAI,CAAC,SAAS,OAAO,UAAU,SAAU,QAAO;AAChD,YAAM,SAAS;AACf,YAAM,kBAAkB;AAAA,QACtB,OAAO,OAAO,UAAU,WAAW,OAAO,QAAQ;AAAA,MACpD;AACA,UAAI,CAAC,gBAAiB,QAAO;AAC7B,aAAO;AAAA,QACL,GAAG;AAAA,QACH,OAAO;AAAA,MACT;AAAA,IACF,CAAC,EACA,OAAO,CAAC,UAAU,UAAU,IAAI;AAEnC,QAAI,iBAAiB,WAAW,GAAG;AACjC,aAAO,OAAO;AAAA,IAChB,OAAO;AACL,aAAO,UAAU;AAAA,IACnB;AAEA,QAAI,OAAO,KAAK,MAAM,EAAE,WAAW,GAAG;AACpC,UAAI,aAAa,OAAO,QAAQ;AAAA,IAClC,OAAO;AACL,UAAI,aAAa,IAAI,UAAU,KAAK,UAAU,MAAM,CAAC;AAAA,IACvD;AACA,WAAO,IAAI,SAAS;AAAA,EACtB,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAEA,SAAS,kBAAkB,aAA6D;AACtF,QAAM,SAAS,OAAO,YAAY,WAAW,WAAW,YAAY,OAAO,KAAK,IAAI;AACpF,QAAM,WAAW,OAAO,YAAY,aAAa,WAAW,YAAY,SAAS,KAAK,IAAI;AAC1F,QAAM,eAAe,OAAO,YAAY,iBAAiB,WAAW,YAAY,eAAe;AAC/F,QAAM,WAAW,OAAO,YAAY,aAAa,WAAW,YAAY,SAAS,KAAK,IAAI;AAC1F,QAAM,WAAW,OAAO,YAAY,aAAa,WAAW,YAAY,WAAW;AACnF,MAAI,CAAC,UAAU,CAAC,YAAY,CAAC,gBAAgB,CAAC,YAAY,CAAC,UAAU;AACnE,UAAM,IAAI,MAAM,mCAAmC;AAAA,EACrD;AACA,SAAO;AAAA,IACL,QAAQ,qBAAqB,MAAM;AAAA,IACnC;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EACF;AACF;AAEA,SAAS,MAAM,IAA2B;AACxC,SAAO,IAAI,QAAQ,CAAC,YAAY,WAAW,SAAS,EAAE,CAAC;AACzD;AAEA,MAAM,oCAAoC;AAC1C,MAAM,wCAAwC;AAC9C,MAAM,oCAAoC;AAE1C,SAAS,sBAAsB,KAAyB,UAA0B;AAChF,QAAM,SAAS,OAAO,SAAS,OAAO,IAAI,EAAE;AAC5C,SAAO,OAAO,SAAS,MAAM,KAAK,SAAS,IAAI,SAAS;AAC1D;AAEA,SAAS,8BAA8B,MAAyB,QAAQ,KAAa;AACnF,SAAO,sBAAsB,IAAI,0CAA0C,iCAAiC;AAC9G;AAEA,SAAS,iCAAiC,MAAyB,QAAQ,KAAa;AACtF,SAAO,sBAAsB,IAAI,8CAA8C,qCAAqC;AACtH;AAEA,SAAS,wBAAwB,kBAAiC,MAAyB,QAAQ,KAAa;AAC9G,QAAM,aAAa,OAAO,oBAAoB,GAAG;AACjD,QAAM,cAAc,OAAO,SAAS,UAAU,IAAI,aAAa,MAAO;AACtE,QAAM,MAAM,sBAAsB,IAAI,0CAA0C,iCAAiC;AACjH,SAAO,KAAK,IAAI,KAAK,IAAI,aAAa,CAAC,GAAG,GAAG;AAC/C;AAEO,SAAS,mBAAmB,kBAA2C;AAC5E,QAAM,cAAc,kBAAkB,gBAAgB;AACtD,QAAM,gBAAgB,IAAI,IAAI,GAAG,YAAY,MAAM,GAAG;AACtD,QAAM,mBAAmB,IAAI,IAAI,uBAAuB,aAAa,EAAE,SAAS;AAChF,MAAI,aAAgC;AACpC,MAAI,+BAA+B;AACnC,QAAM,cAAc,oBAAI,IAA0C;AAClE,QAAM,qBAAqB,oBAAI,IAAiD;AAChF,QAAM,oBAAoB,oBAAI,IAAgD;AAC9E,QAAM,iBAAiB,oBAAI,IAA6C;AACxE,QAAM,gBAAgB,oBAAI,IAA4C;AACtE,QAAM,iBAAiB,oBAAI,IAA6C;AAExE,WAAS,wBAAwB,WAA2B;AAC1D,UAAM,WAAW,IAAI,IAAI,WAAW,aAAa;AACjD,QAAI,SAAS,WAAW,cAAc,QAAQ;AAC5C,YAAM,IAAI,MAAM,wDAAwD,SAAS,MAAM,EAAE;AAAA,IAC3F;AACA,WAAO,SAAS,SAAS;AAAA,EAC3B;AAEA,WAAS,uBAAuB,SAAyB;AACvD,WAAO,wBAAwB,OAAO;AAAA,EACxC;AAEA,iBAAe,4BAAiD;AAC9D,UAAM,WAAW,MAAM,MAAM,kBAAkB;AAAA,MAC7C,QAAQ;AAAA,MACR,QAAQ,YAAY,QAAQ,8BAA8B,CAAC;AAAA,MAC3D,SAAS;AAAA,QACP,QAAQ;AAAA,QACR,gBAAgB;AAAA,MAClB;AAAA,MACA,MAAM,KAAK,UAAU;AAAA,QACnB,YAAY;AAAA,QACZ,WAAW,YAAY;AAAA,QACvB,eAAe,YAAY;AAAA,QAC3B,UAAU,YAAY;AAAA,QACtB,UAAU,YAAY;AAAA,MACxB,CAAC;AAAA,IACH,CAAC;AAED,QAAI,CAAC,SAAS,IAAI;AAChB,YAAM,UAAU,MAAM,SAAS,KAAK;AACpC,YAAM,IAAI,MAAM,iCAAiC,SAAS,MAAM,MAAM,OAAO,EAAE;AAAA,IACjF;AAEA,UAAM,UAAU,MAAM,SAAS,KAAK;AAKpC,QAAI,CAAC,QAAQ,cAAc;AACzB,YAAM,IAAI,MAAM,sDAAsD;AAAA,IACxE;AAEA,WAAO;AAAA,MACL,aAAa,QAAQ;AAAA,MACrB,cAAc,QAAQ,iBAAiB;AAAA,MACvC,WAAW,KAAK,IAAI,KAAM,QAAQ,cAAc,QAAQ,MAAQ;AAAA,IAClE;AAAA,EACF;AAEA,iBAAe,mBAAmB,SAA0C;AAC1E,QAAI,CAAC,QAAQ,aAAc,QAAO,0BAA0B;AAC5D,UAAM,WAAW,MAAM,MAAM,kBAAkB;AAAA,MAC7C,QAAQ;AAAA,MACR,QAAQ,YAAY,QAAQ,8BAA8B,CAAC;AAAA,MAC3D,SAAS;AAAA,QACP,QAAQ;AAAA,QACR,gBAAgB;AAAA,MAClB;AAAA,MACA,MAAM,KAAK,UAAU;AAAA,QACnB,YAAY;AAAA,QACZ,WAAW,YAAY;AAAA,QACvB,eAAe,YAAY;AAAA,QAC3B,eAAe,QAAQ;AAAA,MACzB,CAAC;AAAA,IACH,CAAC;AAED,QAAI,CAAC,SAAS,IAAI;AAChB,aAAO,0BAA0B;AAAA,IACnC;AAEA,UAAM,UAAU,MAAM,SAAS,KAAK;AAKpC,QAAI,CAAC,QAAQ,aAAc,QAAO,0BAA0B;AAC5D,WAAO;AAAA,MACL,aAAa,QAAQ;AAAA,MACrB,cAAc,QAAQ,iBAAiB,QAAQ,gBAAgB;AAAA,MAC/D,WAAW,KAAK,IAAI,KAAM,QAAQ,cAAc,QAAQ,MAAQ;AAAA,IAClE;AAAA,EACF;AAEA,iBAAe,YAAY,eAAe,OAAwB;AAChE,QAAI,CAAC,gBAAgB,cAAc,WAAW,YAAY,KAAK,IAAI,GAAG;AACpE,aAAO,WAAW;AAAA,IACpB;AACA,iBAAa,aAAa,MAAM,mBAAmB,UAAU,IAAI,MAAM,0BAA0B;AACjG,WAAO,WAAW;AAAA,EACpB;AAEA,iBAAe,QACb,WACA,OAAoB,CAAC,GACrB,UAAU,OACV,oBAAoB,GACR;AACZ,UAAM,MAAM,wBAAwB,SAAS;AAC7C,UAAM,QAAQ,MAAM,YAAY;AAEhC,UAAM,WAAW,MAAM,MAAM,KAAK;AAAA,MAChC,GAAG;AAAA,MACH,QAAQ,KAAK,UAAU,YAAY,QAAQ,8BAA8B,CAAC;AAAA,MAC1E,SAAS;AAAA,QACP,QAAQ;AAAA,QACR,eAAe,UAAU,KAAK;AAAA,QAC9B,GAAG,KAAK;AAAA,MACV;AAAA,IACF,CAAC;AAED,QAAI,SAAS,WAAW,OAAO,CAAC,SAAS;AACvC,YAAM,YAAY,IAAI;AACtB,aAAO,QAAW,WAAW,MAAM,MAAM,iBAAiB;AAAA,IAC5D;AAEA,QAAI,SAAS,WAAW,KAAK;AAC3B,UAAI,qBAAqB,iCAAiC,GAAG;AAC3D,cAAM,OAAO,MAAM,SAAS,KAAK;AACjC,cAAM,IAAI,MAAM,gCAAgC,IAAI,EAAE;AAAA,MACxD;AACA,YAAM,MAAM,wBAAwB,SAAS,QAAQ,IAAI,aAAa,CAAC,CAAC;AACxE,aAAO,QAAW,WAAW,MAAM,SAAS,oBAAoB,CAAC;AAAA,IACnE;AAEA,QAAI,CAAC,SAAS,IAAI;AAChB,YAAM,OAAO,MAAM,SAAS,KAAK;AACjC,YAAM,IAAI,MAAM,0BAA0B,SAAS,MAAM,MAAM,IAAI,EAAE;AAAA,IACvE;AAEA,QAAI,SAAS,WAAW,KAAK;AAC3B,aAAO;AAAA,IACT;AAEA,WAAO,SAAS,KAAK;AAAA,EACvB;AAEA,iBAAe,cACb,WACA,OAAoB,CAAC,GACrB,UAAU,OACV,oBAAoB,GAKnB;AACD,UAAM,MAAM,wBAAwB,SAAS;AAC7C,UAAM,QAAQ,MAAM,YAAY;AAEhC,UAAM,WAAW,MAAM,MAAM,KAAK;AAAA,MAChC,GAAG;AAAA,MACH,QAAQ,KAAK,UAAU,YAAY,QAAQ,8BAA8B,CAAC;AAAA,MAC1E,SAAS;AAAA,QACP,eAAe,UAAU,KAAK;AAAA,QAC9B,GAAG,KAAK;AAAA,MACV;AAAA,IACF,CAAC;AAED,QAAI,SAAS,WAAW,OAAO,CAAC,SAAS;AACvC,YAAM,YAAY,IAAI;AACtB,aAAO,cAAc,WAAW,MAAM,MAAM,iBAAiB;AAAA,IAC/D;AAEA,QAAI,SAAS,WAAW,KAAK;AAC3B,UAAI,qBAAqB,iCAAiC,GAAG;AAC3D,cAAM,OAAO,MAAM,SAAS,KAAK;AACjC,cAAM,IAAI,MAAM,gCAAgC,IAAI,EAAE;AAAA,MACxD;AACA,YAAM,MAAM,wBAAwB,SAAS,QAAQ,IAAI,aAAa,CAAC,CAAC;AACxE,aAAO,cAAc,WAAW,MAAM,SAAS,oBAAoB,CAAC;AAAA,IACtE;AAEA,QAAI,CAAC,SAAS,IAAI;AAChB,YAAM,OAAO,MAAM,SAAS,KAAK;AACjC,YAAM,IAAI,MAAM,0BAA0B,SAAS,MAAM,MAAM,IAAI,EAAE;AAAA,IACvE;AAEA,UAAM,cAAc,MAAM,SAAS,YAAY;AAC/C,WAAO;AAAA,MACL,QAAQ,OAAO,KAAK,WAAW;AAAA,MAC/B,aAAa,SAAS,QAAQ,IAAI,cAAc;AAAA,MAChD,eAAe,OAAO,SAAS,QAAQ,IAAI,gBAAgB,KAAK,EAAE,KAAK,YAAY,cAAc;AAAA,IACnG;AAAA,EACF;AAEA,WAAS,SAAS,MAAc,QAA8E;AAC5G,UAAM,MAAM,IAAI,IAAI,wBAAwB,IAAI,CAAC;AACjD,eAAW,CAAC,KAAK,KAAK,KAAK,OAAO,QAAQ,MAAM,GAAG;AACjD,UAAI,UAAU,UAAa,UAAU,QAAQ,UAAU,GAAI;AAC3D,UAAI,aAAa,IAAI,KAAK,OAAO,KAAK,CAAC;AAAA,IACzC;AACA,WAAO,IAAI,SAAS;AAAA,EACtB;AAEA,iBAAe,SAAY,WAAmB,QAI3C;AACD,UAAM,UAAU,MAAM;AAAA,MACpB,SAAS,SAAS,WAAW,MAAM,IAAI;AAAA,IACzC;AACA,WAAO;AAAA,MACL,OAAO,MAAM,QAAQ,SAAS,WAAW,KAAK,IAAI,QAAQ,UAAU,QAAQ,CAAC;AAAA,MAC7E,SAAS,OAAO,SAAS,QAAQ,MAAM,SAAS,WAC5C,uBAAuB,QAAQ,OAAO,KAAK,IAAI,IAC/C;AAAA,MACJ,eAAe,OAAO,SAAS,gBAAgB,WAAW,QAAQ,cAAc;AAAA,IAClF;AAAA,EACF;AAEA,iBAAe,cAAc,cAAsD;AACjF,UAAM,SAAuE;AAAA,MAC3E,OAAO;AAAA,MACP,iBAAiB;AAAA,MACjB,YAAY;AAAA,IACd;AACA,UAAM,yBAAyB,wBAAwB,YAAY;AACnE,QAAI,wBAAwB;AAC1B,aAAO,SAAS,KAAK,UAAU;AAAA,QAC7B,SAAS;AAAA,UACP;AAAA,YACE,UAAU;AAAA,YACV,OAAO;AAAA,UACT;AAAA,QACF;AAAA,MACF,CAAC;AAAA,IACH;AAEA,UAAM,OAAO,MAAM,SAAwB,8BAA8B,MAAM;AAC/E,WAAO,KAAK;AAAA,EACd;AAEA,iBAAe,iBAAsD;AACnE,QAAI;AACF,YAAM,SAAS,MAAM,QAAiC,iCAAiC;AACvF,aAAO;AAAA,QACL,SAAS,OAAO,OAAO,gBAAgB,WAAW,OAAO,cAAc;AAAA,MACzE;AAAA,IACF,QAAQ;AACN,YAAM,QAAQ,MAAM,SAA0B,2BAA2B;AAAA,QACvE,OAAO;AAAA,QACP,iBAAiB;AAAA,MACnB,CAAC;AACD,aAAO,EAAE,SAAS,MAAM,MAAM,UAAU,IAAI,OAAO,KAAK;AAAA,IAC1D;AAAA,EACF;AAEA,iBAAe,aAAa,SAIkE;AAC5F,QAAI,QAAQ,SAAS;AACnB,YAAMA,QAAO,MAAM,SAAwB,uBAAuB,6BAA6B,QAAQ,OAAO,CAAC,CAAC;AAChH,aAAO;AAAA,QACL,GAAGA;AAAA,QACH,SAASA,MAAK,UAAU,uBAAuB,6BAA6BA,MAAK,OAAO,CAAC,IAAI;AAAA,MAC/F;AAAA,IACF;AACA,UAAM,SAAuE;AAAA,MAC3E,OAAO,KAAK,IAAI,KAAK,IAAI,QAAQ,WAAW,CAAC,GAAG,GAAG;AAAA,MACnD,iBAAiB;AAAA,MACjB,YAAY;AAAA,IACd;AACA,UAAM,yBAAyB,wBAAwB,QAAQ,YAAY;AAC3E,QAAI,wBAAwB;AAC1B,aAAO,SAAS,KAAK,UAAU;AAAA,QAC7B,SAAS;AAAA,UACP;AAAA,YACE,UAAU;AAAA,YACV,OAAO;AAAA,UACT;AAAA,QACF;AAAA,MACF,CAAC;AAAA,IACH;AACA,UAAM,CAAC,MAAM,aAAa,IAAI,MAAM,QAAQ,IAAI;AAAA,MAC9C,SAAwB,8BAA8B,MAAM;AAAA,MAC5D,cAAc,sBAAsB,EAAE,MAAM,MAAM,IAAI;AAAA,IACxD,CAAC;AACD,WAAO;AAAA,MACL,GAAG;AAAA,MACH,eAAe,iBAAiB,KAAK;AAAA,MACrC,SAAS,KAAK,UAAU,uBAAuB,6BAA6B,KAAK,OAAO,CAAC,IAAI;AAAA,IAC/F;AAAA,EACF;AAEA,iBAAe,eAAe,SAAyB,YAAY,KAAiG;AAClK,QAAI,QAAS,QAAO,SAAyB,OAAO;AACpD,WAAO,SAAyB,2BAA2B;AAAA,MACzD,OAAO,KAAK,IAAI,KAAK,IAAI,WAAW,CAAC,GAAG,GAAG;AAAA,MAC3C,iBAAiB;AAAA,MACjB,YAAY;AAAA,IACd,CAAC;AAAA,EACH;AAEA,iBAAe,eAAe,SAAyB,YAAY,KAAkG;AACnK,QAAI,QAAS,QAAO,SAA0B,OAAO;AACrD,WAAO,SAA0B,2BAA2B;AAAA,MAC1D,OAAO,KAAK,IAAI,KAAK,IAAI,WAAW,CAAC,GAAG,GAAG;AAAA,MAC3C,iBAAiB;AAAA,MACjB,YAAY;AAAA,IACd,CAAC;AAAA,EACH;AAEA,iBAAe,aAAa,SAAyB,YAAY,KAA+F;AAC9J,QAAI,QAAS,QAAO,SAAuB,OAAO;AAClD,WAAO,SAAuB,yBAAyB;AAAA,MACrD,OAAO,KAAK,IAAI,KAAK,IAAI,WAAW,CAAC,GAAG,GAAG;AAAA,MAC3C,iBAAiB;AAAA,MACjB,YAAY;AAAA,IACd,CAAC;AAAA,EACH;AAEA,iBAAe,mBACb,YACA,SACA,YAAY,KACqF;AACjG,QAAI,QAAS,QAAO,SAA8B,OAAO;AACzD,WAAO,SAA8B,yBAAyB,mBAAmB,UAAU,CAAC,aAAa;AAAA,MACvG,OAAO,KAAK,IAAI,KAAK,IAAI,WAAW,CAAC,GAAG,GAAG;AAAA,MAC3C,YAAY;AAAA,IACd,CAAC;AAAA,EACH;AAEA,iBAAe,eAAyC;AACtD,UAAM,WAAW,MAAM,SAAwB,yBAAyB;AAAA,MACtE,OAAO;AAAA,MACP,iBAAiB;AAAA,MACjB,YAAY;AAAA,IACd,CAAC;AACD,WAAO,SAAS;AAAA,EAClB;AAEA,iBAAe,cAAuC;AACpD,UAAM,WAAW,MAAM,SAAuB,wBAAwB;AAAA,MACpE,OAAO;AAAA,MACP,iBAAiB;AAAA,MACjB,YAAY;AAAA,IACd,CAAC;AACD,WAAO,SAAS;AAAA,EAClB;AAEA,iBAAe,YAAY,MAA8C;AACvE,QAAI,CAAC,cAAc,IAAI,IAAI,GAAG;AAC5B,oBAAc,IAAI,MAAM,QAAwB,2BAA2B,mBAAmB,IAAI,CAAC,EAAE,EAAE,MAAM,MAAM,IAAI,CAAC;AAAA,IAC1H;AACA,WAAO,cAAc,IAAI,IAAI,KAAK;AAAA,EACpC;AAEA,iBAAe,aAAa,MAA+C;AACzE,QAAI,CAAC,eAAe,IAAI,IAAI,GAAG;AAC7B,qBAAe,IAAI,MAAM,QAAyB,2BAA2B,mBAAmB,IAAI,CAAC,EAAE,EAAE,MAAM,MAAM,IAAI,CAAC;AAAA,IAC5H;AACA,WAAO,eAAe,IAAI,IAAI,KAAK;AAAA,EACrC;AAEA,iBAAe,qBAAqB,eAAyD;AAC3F,UAAM,UAAmC,CAAC;AAC1C,QAAI,UAAqC;AACzC,OAAG;AACD,YAAM,MAAM,KAAK,IAAI;AACrB,YAAM,SAAS,KAAK,IAAI,GAAG,OAAO,MAAM,6BAA6B;AACrE,UAAI,SAAS,EAAG,OAAM,MAAM,MAAM;AAClC,qCAA+B,KAAK,IAAI;AACxC,YAAM,OAAiG,MAAM;AAAA,QAC3G,WAAW,2BAA2B,mBAAmB,aAAa,CAAC;AAAA,QACvE,UACI,SACA;AAAA,UACE,OAAO;AAAA,UACP,iBAAiB;AAAA,UACjB,YAAY;AAAA,QACd;AAAA,MACN;AACA,cAAQ,KAAK,GAAG,KAAK,KAAK;AAC1B,gBAAU,KAAK;AAAA,IACjB,SAAS;AACT,WAAO;AAAA,EACT;AAEA,iBAAe,UAAU,MAA4C;AACnE,QAAI,CAAC,YAAY,IAAI,IAAI,GAAG;AAC1B,kBAAY,IAAI,MAAM,QAAsB,yBAAyB,mBAAmB,IAAI,CAAC,EAAE,EAAE,MAAM,MAAM,IAAI,CAAC;AAAA,IACpH;AACA,WAAO,YAAY,IAAI,IAAI,KAAK;AAAA,EAClC;AAEA,iBAAe,iBAAiB,YAAoB,mBAAgE;AAClH,UAAM,WAAW,GAAG,UAAU,IAAI,iBAAiB;AACnD,QAAI,CAAC,mBAAmB,IAAI,QAAQ,GAAG;AACrC,yBAAmB;AAAA,QACjB;AAAA,QACA,QAA6B,yBAAyB,mBAAmB,UAAU,CAAC,aAAa,mBAAmB,iBAAiB,CAAC,EAAE,EAAE,MAAM,MAAM,IAAI;AAAA,MAC5J;AAAA,IACF;AACA,WAAO,mBAAmB,IAAI,QAAQ,KAAK;AAAA,EAC7C;AAEA,iBAAe,gBAAgB,MAAkD;AAC/E,QAAI,CAAC,kBAAkB,IAAI,IAAI,GAAG;AAChC,wBAAkB,IAAI,MAAM,QAA4B,+BAA+B,mBAAmB,IAAI,CAAC,EAAE,EAAE,MAAM,MAAM,IAAI,CAAC;AAAA,IACtI;AACA,WAAO,kBAAkB,IAAI,IAAI,KAAK;AAAA,EACxC;AAEA,iBAAe,aAAa,MAA+C;AACzE,QAAI,CAAC,eAAe,IAAI,IAAI,GAAG;AAC7B,qBAAe,IAAI,MAAM,QAAyB,4BAA4B,sBAAsB,IAAI,CAAC,EAAE,EAAE,MAAM,MAAM,IAAI,CAAC;AAAA,IAChI;AACA,WAAO,eAAe,IAAI,IAAI,KAAK;AAAA,EACrC;AAEA,iBAAe,kBAAkB,WAM9B;AACD,QAAI,UAAU,WAAW,SAAS,KAAK,UAAU,WAAW,UAAU,KAAK,UAAU,WAAW,GAAG,GAAG;AACpG,YAAMC,UAAS,MAAM,cAAc,SAAS;AAC5C,aAAO;AAAA,QACL,GAAGA;AAAA,QACH,UAAU;AAAA,QACV,MAAM;AAAA,MACR;AAAA,IACF;AAEA,UAAM,YAAY,MAAM,aAAa,SAAS;AAC9C,QAAI,CAAC,WAAW;AACd,YAAM,IAAI,MAAM,qBAAqB,SAAS,gBAAgB;AAAA,IAChE;AACA,UAAM,eAAe,UAAU,QAAQ,UAAU;AACjD,UAAM,SAAS,MAAM,cAAc,gBAAgB,4BAA4B,sBAAsB,SAAS,CAAC,WAAW;AAC1H,WAAO;AAAA,MACL,GAAG;AAAA,MACH,UAAU,OAAO,UAAU,sBAAsB,YAAY,UAAU,kBAAkB,KAAK,EAAE,SAAS,IACrG,UAAU,kBAAkB,KAAK,IACjC;AAAA,MACJ,MAAM,UAAU,QAAQ;AAAA,IAC1B;AAAA,EACF;AAEA,iBAAe,uBAOZ;AACD,UAAM,CAAC,SAAS,UAAU,YAAY,UAAU,KAAK,IAAI,MAAM,QAAQ,IAAI;AAAA,MACzE,YAAY,EAAE,MAAM,MAAM,CAAC,CAAC;AAAA,MAC5B,aAAa,EAAE,MAAM,MAAM,CAAC,CAAC;AAAA,MAC7B,eAAe,MAAM,GAAG;AAAA,MACxB,aAAa,MAAM,GAAG;AAAA,MACtB,eAAe,EAAE,MAAM,OAAO,EAAE,SAAS,KAAK,EAAE;AAAA,IAClD,CAAC;AAED,UAAM,iBAAmH,CAAC;AAC1H,eAAW,UAAU,SAAS,OAAO;AACnC,UAAI,UAAyB;AAC7B,SAAG;AACD,cAAM,OAA+F,MAAM,mBAAmB,OAAO,MAAM,SAAS,GAAG,EAAE,MAAM,OAAO;AAAA,UACpK,OAAO,CAAC;AAAA,UACR,SAAS;AAAA,UACT,eAAe;AAAA,QACjB,EAAE;AACF,uBAAe,KAAK,GAAG,KAAK,MAAM,IAAI,CAAC,mBAAmB;AAAA,UACxD,YAAY,OAAO;AAAA,UACnB,MAAM,cAAc;AAAA,UACpB,OAAO,yBAAyB,cAAc,UAAU,MAAM,MAAM,cAAc,IAAI;AAAA,UACtF,MAAM,cAAc,cAAc,wBAAwB,QAAQ,CAAC,QAA6B,MAAM,QAAQ,IAAI,IAAI,IAAI,IAAI,OAAO,CAAC,CAAC,KAAK,CAAC,CAAC;AAAA,UAC9I,YAAY,cAAc,cAAc,wBAAwB,QAAQ,CAAC,QAAmC,MAAM,QAAQ,IAAI,UAAU,IAAI,IAAI,aAAa,CAAC,CAAC,KAAK,CAAC,CAAC;AAAA,QACxK,EAAE,CAAC;AACH,kBAAU,KAAK;AAAA,MACjB,SAAS;AAAA,IACX;AAEA,WAAO;AAAA,MACL,SAAS,QAAQ,IAAI,CAAC,YAAY;AAAA,QAChC,MAAM,OAAO;AAAA,QACb,OAAO,yBAAyB,OAAO,UAAU,MAAM,MAAM,OAAO,IAAI;AAAA,QACxE,SAAS,OAAO,WAAW;AAAA,MAC7B,EAAE;AAAA,MACF,UAAU,SAAS,IAAI,CAAC,aAAa;AAAA,QACnC,MAAM,QAAQ;AAAA,QACd,OAAO,yBAAyB,QAAQ,UAAU,MAAM,MAAM,QAAQ,IAAI;AAAA,QAC1E,SAAS,cAAc,QAAQ,WAAW,CAAC,CAAC;AAAA,MAC9C,EAAE;AAAA,MACF,YAAY,WAAW,MAAM,IAAI,CAAC,eAAe;AAAA,QAC/C,MAAM,UAAU;AAAA,QAChB,MAAM,UAAU;AAAA,QAChB,OAAO,yBAAyB,UAAU,UAAU,MAAM,MAAM,UAAU,IAAI;AAAA,QAC9E,aAAa,QAAQ,UAAU,WAAW;AAAA,QAC1C,UAAU,QAAQ,UAAU,QAAQ;AAAA,QACpC,OAAO,OAAO,UAAU,UAAU,YAAY,UAAU,MAAM,KAAK,EAAE,SAAS,IAAI,UAAU,MAAM,KAAK,IAAI;AAAA,QAC3G,cAAc,OAAO,UAAU,kBAAkB,YAAY,UAAU,cAAc,KAAK,EAAE,SAAS,IACjG,UAAU,cAAc,KAAK,IAC7B;AAAA,MACN,EAAE;AAAA,MACF,UAAU,SAAS,MAAM,IAAI,CAAC,YAAY;AAAA,QACxC,MAAM,OAAO;AAAA,QACb,OAAO,yBAAyB,OAAO,UAAU,MAAM,MAAM,OAAO,IAAI;AAAA,QACxE,gBAAgB,MAAM,QAAQ,OAAO,UAAU,IAAI,OAAO,WAAW,SAAS;AAAA,MAChF,EAAE;AAAA,MACF;AAAA,MACA,SAAS,MAAM;AAAA,IACjB;AAAA,EACF;AAEA,SAAO;AAAA,IACL;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EACF;AACF;",
6
6
  "names": ["page", "binary"]
7
7
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@open-mercato/sync-akeneo",
3
- "version": "0.6.6-develop.5523.1.e223ca1915",
3
+ "version": "0.6.6-develop.5536.1.7cfc9c28a1",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "scripts": {
@@ -52,18 +52,18 @@
52
52
  }
53
53
  },
54
54
  "dependencies": {
55
- "@open-mercato/core": "0.6.6-develop.5523.1.e223ca1915",
56
- "@open-mercato/ui": "0.6.6-develop.5523.1.e223ca1915",
55
+ "@open-mercato/core": "0.6.6-develop.5536.1.7cfc9c28a1",
56
+ "@open-mercato/ui": "0.6.6-develop.5536.1.7cfc9c28a1",
57
57
  "node-html-markdown": "^2.0.0"
58
58
  },
59
59
  "peerDependencies": {
60
60
  "@mikro-orm/postgresql": "^7.0.14",
61
- "@open-mercato/shared": "0.6.6-develop.5523.1.e223ca1915",
61
+ "@open-mercato/shared": "0.6.6-develop.5536.1.7cfc9c28a1",
62
62
  "react": "^19.0.0",
63
63
  "react-dom": "^19.0.0"
64
64
  },
65
65
  "devDependencies": {
66
- "@open-mercato/shared": "0.6.6-develop.5523.1.e223ca1915",
66
+ "@open-mercato/shared": "0.6.6-develop.5536.1.7cfc9c28a1",
67
67
  "@types/jest": "^30.0.0",
68
68
  "@types/react": "^19.2.17",
69
69
  "@types/react-dom": "^19.2.3",
@@ -220,3 +220,89 @@ describe('akeneo client security', () => {
220
220
  }))
221
221
  })
222
222
  })
223
+
224
+ describe('akeneo client resilience (issue #2976)', () => {
225
+ const originalFetch = global.fetch
226
+ const originalSetTimeout = global.setTimeout
227
+ const originalMaxRetries = process.env.OM_INTEGRATION_AKENEO_MAX_RATE_LIMIT_RETRIES
228
+ const originalRetryCap = process.env.OM_INTEGRATION_AKENEO_RETRY_AFTER_CAP_MS
229
+ let capturedDelays: number[]
230
+
231
+ function tokenResponse(): Response {
232
+ return jsonResponse({ access_token: 'token-123', expires_in: 3600 })
233
+ }
234
+
235
+ beforeEach(() => {
236
+ global.fetch = jest.fn() as typeof fetch
237
+ capturedDelays = []
238
+ // Fire backoff/retry-after sleeps synchronously so the retry loop runs
239
+ // without real timers while we assert the requested wait durations.
240
+ global.setTimeout = ((callback: () => void, ms?: number) => {
241
+ capturedDelays.push(typeof ms === 'number' ? ms : 0)
242
+ callback()
243
+ return 0 as unknown as ReturnType<typeof setTimeout>
244
+ }) as unknown as typeof global.setTimeout
245
+ })
246
+
247
+ afterEach(() => {
248
+ global.fetch = originalFetch
249
+ global.setTimeout = originalSetTimeout
250
+ if (originalMaxRetries === undefined) delete process.env.OM_INTEGRATION_AKENEO_MAX_RATE_LIMIT_RETRIES
251
+ else process.env.OM_INTEGRATION_AKENEO_MAX_RATE_LIMIT_RETRIES = originalMaxRetries
252
+ if (originalRetryCap === undefined) delete process.env.OM_INTEGRATION_AKENEO_RETRY_AFTER_CAP_MS
253
+ else process.env.OM_INTEGRATION_AKENEO_RETRY_AFTER_CAP_MS = originalRetryCap
254
+ jest.restoreAllMocks()
255
+ })
256
+
257
+ it('caps 429 retries and surfaces the existing error shape instead of looping forever', async () => {
258
+ process.env.OM_INTEGRATION_AKENEO_MAX_RATE_LIMIT_RETRIES = '2'
259
+ const fetchMock = global.fetch as jest.MockedFunction<typeof fetch>
260
+ fetchMock.mockImplementation(async (input) => {
261
+ if (String(input).endsWith('/api/oauth/v1/token')) return tokenResponse()
262
+ return new Response('rate limited', { status: 429, headers: { 'retry-after': '1' } })
263
+ })
264
+
265
+ const client = createAkeneoClient(validCredentials)
266
+ await expect(client.listChannels()).rejects.toThrow('Akeneo request failed (429)')
267
+
268
+ // 1 token + (maxRetries + 1) request attempts = 1 + 3 = 4 fetches; 2 sleeps.
269
+ expect(fetchMock).toHaveBeenCalledTimes(4)
270
+ expect(capturedDelays).toHaveLength(2)
271
+ })
272
+
273
+ it('clamps a hostile retry-after header to the configured ceiling', async () => {
274
+ process.env.OM_INTEGRATION_AKENEO_RETRY_AFTER_CAP_MS = '60000'
275
+ const fetchMock = global.fetch as jest.MockedFunction<typeof fetch>
276
+ let listCalls = 0
277
+ fetchMock.mockImplementation(async (input) => {
278
+ if (String(input).endsWith('/api/oauth/v1/token')) return tokenResponse()
279
+ listCalls += 1
280
+ if (listCalls === 1) {
281
+ // retry-after: ~31 years — must be clamped, not slept verbatim.
282
+ return new Response('', { status: 429, headers: { 'retry-after': '999999999' } })
283
+ }
284
+ return jsonResponse({ _embedded: { items: [{ code: 'web' }] }, _links: {}, items_count: 1 })
285
+ })
286
+
287
+ const client = createAkeneoClient(validCredentials)
288
+ const channels = await client.listChannels()
289
+
290
+ expect(channels).toHaveLength(1)
291
+ expect(capturedDelays).toEqual([60000])
292
+ })
293
+
294
+ it('attaches an AbortSignal timeout to authenticated requests', async () => {
295
+ const fetchMock = global.fetch as jest.MockedFunction<typeof fetch>
296
+ fetchMock
297
+ .mockResolvedValueOnce(tokenResponse())
298
+ .mockResolvedValueOnce(jsonResponse({ _embedded: { items: [] }, _links: {}, items_count: 0 }))
299
+
300
+ const client = createAkeneoClient(validCredentials)
301
+ await client.listChannels()
302
+
303
+ const tokenInit = fetchMock.mock.calls[0][1] as RequestInit
304
+ const requestInit = fetchMock.mock.calls[1][1] as RequestInit
305
+ expect(tokenInit.signal).toBeInstanceOf(AbortSignal)
306
+ expect(requestInit.signal).toBeInstanceOf(AbortSignal)
307
+ })
308
+ })
@@ -200,6 +200,30 @@ function sleep(ms: number): Promise<void> {
200
200
  return new Promise((resolve) => setTimeout(resolve, ms))
201
201
  }
202
202
 
203
+ const DEFAULT_AKENEO_REQUEST_TIMEOUT_MS = 30_000
204
+ const DEFAULT_AKENEO_MAX_RATE_LIMIT_RETRIES = 5
205
+ const DEFAULT_AKENEO_RETRY_AFTER_CAP_MS = 60_000
206
+
207
+ function resolvePositiveIntEnv(raw: string | undefined, fallback: number): number {
208
+ const parsed = Number.parseInt(raw ?? '', 10)
209
+ return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback
210
+ }
211
+
212
+ function resolveAkeneoRequestTimeoutMs(env: NodeJS.ProcessEnv = process.env): number {
213
+ return resolvePositiveIntEnv(env.OM_INTEGRATION_AKENEO_REQUEST_TIMEOUT_MS, DEFAULT_AKENEO_REQUEST_TIMEOUT_MS)
214
+ }
215
+
216
+ function resolveAkeneoMaxRateLimitRetries(env: NodeJS.ProcessEnv = process.env): number {
217
+ return resolvePositiveIntEnv(env.OM_INTEGRATION_AKENEO_MAX_RATE_LIMIT_RETRIES, DEFAULT_AKENEO_MAX_RATE_LIMIT_RETRIES)
218
+ }
219
+
220
+ function clampAkeneoRetryAfterMs(retryAfterHeader: string | null, env: NodeJS.ProcessEnv = process.env): number {
221
+ const retryAfter = Number(retryAfterHeader ?? '1')
222
+ const requestedMs = Number.isFinite(retryAfter) ? retryAfter * 1000 : 1000
223
+ const cap = resolvePositiveIntEnv(env.OM_INTEGRATION_AKENEO_RETRY_AFTER_CAP_MS, DEFAULT_AKENEO_RETRY_AFTER_CAP_MS)
224
+ return Math.min(Math.max(requestedMs, 0), cap)
225
+ }
226
+
203
227
  export function createAkeneoClient(credentialsInput: Record<string, unknown>) {
204
228
  const credentials = coerceCredentials(credentialsInput)
205
229
  const akeneoBaseUrl = new URL(`${credentials.apiUrl}/`)
@@ -228,6 +252,7 @@ export function createAkeneoClient(credentialsInput: Record<string, unknown>) {
228
252
  async function acquirePasswordGrantToken(): Promise<TokenState> {
229
253
  const response = await fetch(tokenEndpointUrl, {
230
254
  method: 'POST',
255
+ signal: AbortSignal.timeout(resolveAkeneoRequestTimeoutMs()),
231
256
  headers: {
232
257
  accept: 'application/json',
233
258
  'content-type': 'application/json',
@@ -266,6 +291,7 @@ export function createAkeneoClient(credentialsInput: Record<string, unknown>) {
266
291
  if (!current.refreshToken) return acquirePasswordGrantToken()
267
292
  const response = await fetch(tokenEndpointUrl, {
268
293
  method: 'POST',
294
+ signal: AbortSignal.timeout(resolveAkeneoRequestTimeoutMs()),
269
295
  headers: {
270
296
  accept: 'application/json',
271
297
  'content-type': 'application/json',
@@ -307,12 +333,14 @@ export function createAkeneoClient(credentialsInput: Record<string, unknown>) {
307
333
  pathOrUrl: string,
308
334
  init: RequestInit = {},
309
335
  retried = false,
336
+ rateLimitAttempts = 0,
310
337
  ): Promise<T> {
311
338
  const url = resolveAkeneoRequestUrl(pathOrUrl)
312
339
  const token = await ensureToken()
313
340
 
314
341
  const response = await fetch(url, {
315
342
  ...init,
343
+ signal: init.signal ?? AbortSignal.timeout(resolveAkeneoRequestTimeoutMs()),
316
344
  headers: {
317
345
  accept: 'application/json',
318
346
  authorization: `Bearer ${token}`,
@@ -322,13 +350,16 @@ export function createAkeneoClient(credentialsInput: Record<string, unknown>) {
322
350
 
323
351
  if (response.status === 401 && !retried) {
324
352
  await ensureToken(true)
325
- return request<T>(pathOrUrl, init, true)
353
+ return request<T>(pathOrUrl, init, true, rateLimitAttempts)
326
354
  }
327
355
 
328
356
  if (response.status === 429) {
329
- const retryAfter = Number(response.headers.get('retry-after') ?? '1')
330
- await sleep(Number.isFinite(retryAfter) ? retryAfter * 1000 : 1000)
331
- return request<T>(pathOrUrl, init, retried)
357
+ if (rateLimitAttempts >= resolveAkeneoMaxRateLimitRetries()) {
358
+ const body = await response.text()
359
+ throw new Error(`Akeneo request failed (429): ${body}`)
360
+ }
361
+ await sleep(clampAkeneoRetryAfterMs(response.headers.get('retry-after')))
362
+ return request<T>(pathOrUrl, init, retried, rateLimitAttempts + 1)
332
363
  }
333
364
 
334
365
  if (!response.ok) {
@@ -347,6 +378,7 @@ export function createAkeneoClient(credentialsInput: Record<string, unknown>) {
347
378
  pathOrUrl: string,
348
379
  init: RequestInit = {},
349
380
  retried = false,
381
+ rateLimitAttempts = 0,
350
382
  ): Promise<{
351
383
  buffer: Buffer
352
384
  contentType: string | null
@@ -357,6 +389,7 @@ export function createAkeneoClient(credentialsInput: Record<string, unknown>) {
357
389
 
358
390
  const response = await fetch(url, {
359
391
  ...init,
392
+ signal: init.signal ?? AbortSignal.timeout(resolveAkeneoRequestTimeoutMs()),
360
393
  headers: {
361
394
  authorization: `Bearer ${token}`,
362
395
  ...init.headers,
@@ -365,13 +398,16 @@ export function createAkeneoClient(credentialsInput: Record<string, unknown>) {
365
398
 
366
399
  if (response.status === 401 && !retried) {
367
400
  await ensureToken(true)
368
- return requestBinary(pathOrUrl, init, true)
401
+ return requestBinary(pathOrUrl, init, true, rateLimitAttempts)
369
402
  }
370
403
 
371
404
  if (response.status === 429) {
372
- const retryAfter = Number(response.headers.get('retry-after') ?? '1')
373
- await sleep(Number.isFinite(retryAfter) ? retryAfter * 1000 : 1000)
374
- return requestBinary(pathOrUrl, init, retried)
405
+ if (rateLimitAttempts >= resolveAkeneoMaxRateLimitRetries()) {
406
+ const body = await response.text()
407
+ throw new Error(`Akeneo request failed (429): ${body}`)
408
+ }
409
+ await sleep(clampAkeneoRetryAfterMs(response.headers.get('retry-after')))
410
+ return requestBinary(pathOrUrl, init, retried, rateLimitAttempts + 1)
375
411
  }
376
412
 
377
413
  if (!response.ok) {