diff --git a/.talismanrc b/.talismanrc index 8b5afad01..9d0b9a22d 100644 --- a/.talismanrc +++ b/.talismanrc @@ -6,5 +6,5 @@ fileignoreconfig: - filename: packages/contentstack-import-setup/test/unit/login-handler.test.ts checksum: 2df43be75a96e1a3a00dd256628e191909aa9d7f192b672b56cb3772e074958c - filename: pnpm-lock.yaml - checksum: eae269c31146f807a80978acb68b83b761742b6d8e82116b6af48dcc0156f085 + checksum: 2e1116588e19b058f0d6a5efa28f54c6a3af11b0f0d8c442ed9866a90b77b713 version: "" diff --git a/packages/contentstack-asset-management/src/export/assets.ts b/packages/contentstack-asset-management/src/export/assets.ts index 662da831c..a955e62be 100644 --- a/packages/contentstack-asset-management/src/export/assets.ts +++ b/packages/contentstack-asset-management/src/export/assets.ts @@ -2,12 +2,13 @@ import { resolve as pResolve } from 'node:path'; import { Readable } from 'node:stream'; import { mkdir, writeFile } from 'node:fs/promises'; import chunk from 'lodash/chunk'; -import { configHandler, log, FsUtility } from '@contentstack/cli-utilities'; +import { log, FsUtility } from '@contentstack/cli-utilities'; import type { CSAssetsAPIConfig, LinkedWorkspace } from '../types/cs-assets-api'; import type { ExportContext } from '../types/export-types'; import { CSAssetsExportAdapter } from './base'; -import { writeStreamToFile, getArrayFromResponse } from '../utils/export-helpers'; +import { writeStreamToFile, getArrayFromResponse, getSecuredAssetAuth, SecuredAssetAuthError } from '../utils/export-helpers'; +import type { SecuredAssetAuth } from '../utils/export-helpers'; import { forEachChunkedJsonStore } from '../utils/chunked-json-reader'; import { withRetry, RetryableHttpError, isRetryableStatus, parseRetryAfterMs } from '../utils/retry'; import type { CustomPromiseHandler } from '../utils/cs-assets-api-adapter'; @@ -93,7 +94,6 @@ export default class ExportAssets extends CSAssetsExportAdapter { await mkdir(filesDir, { recursive: true }); const securedAssets = this.exportContext.securedAssets ?? false; - const authtoken = securedAssets ? configHandler.get('authtoken') : null; log.debug( `Asset downloads: securedAssets=${securedAssets}, concurrency=${this.downloadAssetsBatchConcurrency}, expected=${expectedDownloads}`, this.exportContext.context, @@ -102,6 +102,9 @@ export default class ExportAssets extends CSAssetsExportAdapter { let downloadOk = 0; let downloadFail = 0; + // Set when a 401 persists after a forced token refresh — from then on, skip the network + // entirely and abort the phase, instead of individually failing every remaining asset. + let authFailure: SecuredAssetAuthError | null = null; await forEachChunkedJsonStore( assetsDir, @@ -136,6 +139,17 @@ export default class ExportAssets extends CSAssetsExportAdapter { async (records) => { const valid = records.filter((asset) => this.isDownloadable(asset)); if (valid.length === 0) return; + const chunkAuthFailure = authFailure; + if (chunkAuthFailure) { + // Auth already failed hard — count the remaining downloadables without hitting the network. + for (const rec of valid) { + downloadFail += 1; + this.tick(false, `asset: ${rec.filename ?? rec.file_name ?? rec.uid ?? 'asset'}`, chunkAuthFailure.message); + } + return; + } + // Resolve per chunk so long download phases pick up proactively refreshed OAuth tokens. + let auth: SecuredAssetAuth = securedAssets ? await getSecuredAssetAuth() : {}; const apiBatches = chunk(valid, this.downloadAssetsBatchConcurrency); const promisifyHandler: CustomPromiseHandler = async ({ index, batchIndex }) => { const asset = apiBatches[batchIndex][index] as AssetRecord; @@ -143,18 +157,42 @@ export default class ExportAssets extends CSAssetsExportAdapter { const url = asset.url as string; const filename = asset.filename ?? asset.file_name ?? 'asset'; if (!url || !uid) return; + const knownAuthFailure = authFailure as SecuredAssetAuthError | null; + if (knownAuthFailure) { + downloadFail += 1; + this.tick(false, `asset: ${filename}`, knownAuthFailure.message); + return; + } try { const separator = url.includes('?') ? '&' : '?'; - const downloadUrl = securedAssets && authtoken ? `${url}${separator}authtoken=${authtoken}` : url; + const doFetch = () => + fetch( + securedAssets && auth.authtoken ? `${url}${separator}authtoken=${auth.authtoken}` : url, + securedAssets && auth.headers ? { headers: auth.headers } : undefined, + ); // Binary GET is idempotent — retry transient failures with backoff. const response = await withRetry( async () => { let resp: Response; try { - resp = await fetch(downloadUrl); + resp = await doFetch(); } catch (e) { throw new RetryableHttpError(`download network error: ${(e as Error)?.message ?? String(e)}`); } + if (securedAssets && resp.status === 401) { + // Token expired or was revoked mid-run — force one refresh (deduped upstream) + // and refetch. A second 401 means auth is unrecoverable: abort the phase. + auth = await getSecuredAssetAuth(true); + try { + resp = await doFetch(); + } catch (e) { + throw new RetryableHttpError(`download network error: ${(e as Error)?.message ?? String(e)}`); + } + if (resp.status === 401) { + authFailure = new SecuredAssetAuthError(resp.status); + throw authFailure; + } + } if (!resp.ok) { if (isRetryableStatus(resp.status)) { throw new RetryableHttpError(`HTTP ${resp.status}`, resp.status, parseRetryAfterMs(resp.headers.get('retry-after'))); @@ -191,6 +229,16 @@ export default class ExportAssets extends CSAssetsExportAdapter { }, ); + const terminalAuthFailure = authFailure as SecuredAssetAuthError | null; + if (terminalAuthFailure) { + // Fail the space loudly — a "completed with errors" summary would bury the real cause. + log.error( + `Aborted asset downloads for space ${spaceUid}: ${terminalAuthFailure.message}`, + this.exportContext.context, + ); + throw terminalAuthFailure; + } + log.info( downloadFail === 0 ? `Finished downloading ${downloadOk} asset file(s) for space ${spaceUid}` diff --git a/packages/contentstack-asset-management/src/query-export/cs-assets-query-exporter.ts b/packages/contentstack-asset-management/src/query-export/cs-assets-query-exporter.ts index eda828159..92e93d5f3 100644 --- a/packages/contentstack-asset-management/src/query-export/cs-assets-query-exporter.ts +++ b/packages/contentstack-asset-management/src/query-export/cs-assets-query-exporter.ts @@ -1,7 +1,7 @@ import { resolve as pResolve } from 'node:path'; import { mkdir, writeFile } from 'node:fs/promises'; import { Readable } from 'node:stream'; -import { log, handleAndLogError, configHandler } from '@contentstack/cli-utilities'; +import { log, handleAndLogError } from '@contentstack/cli-utilities'; import type { CsAssetsQueryExportOptions, CSAssetsAPIConfig, LinkedWorkspace } from '../types/cs-assets-api'; import type { ExportContext } from '../types/export-types'; @@ -9,7 +9,8 @@ import ExportAssetTypes from '../export/asset-types'; import ExportFields from '../export/fields'; import { CSAssetsExportAdapter } from '../export/base'; import chunk from 'lodash/chunk'; -import { getAssetItems, writeStreamToFile } from '../utils/export-helpers'; +import { getAssetItems, writeStreamToFile, getSecuredAssetAuth, SecuredAssetAuthError } from '../utils/export-helpers'; +import type { SecuredAssetAuth } from '../utils/export-helpers'; import { withRetry, RetryableHttpError, isRetryableStatus, parseRetryAfterMs } from '../utils/retry'; import type { CustomPromiseHandler } from '../utils/cs-assets-api-adapter'; @@ -223,7 +224,25 @@ class QueryExportWorkspaceAdapter extends CSAssetsExportAdapter { await mkdir(filesDir, { recursive: true }); const securedAssets = this.exportContext.securedAssets ?? false; - const authtoken = securedAssets ? configHandler.get('authtoken') : null; + // Set when a 401 persists after a forced token refresh — from then on, skip the network + // entirely and abort the phase, instead of individually failing every remaining asset. + let authFailure: SecuredAssetAuthError | null = null; + // OAuth → Authorization: Bearer header; basic auth → ?authtoken= query param. + // Resolved lazily once per sequential batch (handlers within a batch share one resolve), so + // long download runs keep picking up proactively refreshed OAuth tokens. + let auth: SecuredAssetAuth = {}; + let authBatchIndex = -1; + let authResolve: Promise = Promise.resolve(); + const ensureAuthForBatch = (batchIndex: number): Promise => { + if (!securedAssets) return Promise.resolve(); + if (batchIndex !== authBatchIndex) { + authBatchIndex = batchIndex; + authResolve = getSecuredAssetAuth().then((resolved) => { + auth = resolved; + }); + } + return authResolve; + }; const apiBatches = chunk(downloadable, this.downloadAssetsBatchConcurrency); const promisifyHandler: CustomPromiseHandler = async ({ index, batchIndex }) => { @@ -231,18 +250,38 @@ class QueryExportWorkspaceAdapter extends CSAssetsExportAdapter { const uid = String(asset.uid ?? asset._uid); const url = String(asset.url); const filename = String(asset.filename ?? asset.file_name ?? 'asset'); + if (authFailure) return; // auth failed hard — don't hit the network for remaining assets try { + await ensureAuthForBatch(batchIndex); const separator = url.includes('?') ? '&' : '?'; - const downloadUrl = securedAssets && authtoken ? `${url}${separator}authtoken=${authtoken}` : url; + const doFetch = () => + fetch( + securedAssets && auth.authtoken ? `${url}${separator}authtoken=${auth.authtoken}` : url, + securedAssets && auth.headers ? { headers: auth.headers } : undefined, + ); // Binary GET is idempotent — retry transient failures with backoff. const response = await withRetry( async () => { let resp: Response; try { - resp = await fetch(downloadUrl); + resp = await doFetch(); } catch (e) { throw new RetryableHttpError(`download network error: ${(e as Error)?.message ?? String(e)}`); } + if (securedAssets && resp.status === 401) { + // Token expired or was revoked mid-run — force one refresh (deduped upstream) + // and refetch. A second 401 means auth is unrecoverable: abort the phase. + auth = await getSecuredAssetAuth(true); + try { + resp = await doFetch(); + } catch (e) { + throw new RetryableHttpError(`download network error: ${(e as Error)?.message ?? String(e)}`); + } + if (resp.status === 401) { + authFailure = new SecuredAssetAuthError(resp.status); + throw authFailure; + } + } if (!resp.ok) { if (isRetryableStatus(resp.status)) { throw new RetryableHttpError(`HTTP ${resp.status}`, resp.status, parseRetryAfterMs(resp.headers.get('retry-after'))); @@ -265,5 +304,15 @@ class QueryExportWorkspaceAdapter extends CSAssetsExportAdapter { }; await this.makeConcurrentCall({ apiBatches, module: 'asset downloads' }, promisifyHandler); + + const terminalAuthFailure = authFailure as SecuredAssetAuthError | null; + if (terminalAuthFailure) { + // Fail the space loudly — silently skipping the rest would look like a successful export. + log.error( + `Aborted asset downloads for space ${spaceUid}: ${terminalAuthFailure.message}`, + this.exportContext.context, + ); + throw terminalAuthFailure; + } } } diff --git a/packages/contentstack-asset-management/src/utils/export-helpers.ts b/packages/contentstack-asset-management/src/utils/export-helpers.ts index 252b33652..330f706d1 100644 --- a/packages/contentstack-asset-management/src/utils/export-helpers.ts +++ b/packages/contentstack-asset-management/src/utils/export-helpers.ts @@ -1,4 +1,51 @@ import { createWriteStream } from 'node:fs'; +import { authHandler, authenticationHandler, configHandler } from '@contentstack/cli-utilities'; + +export interface SecuredAssetAuth { + /** OAuth: header to attach to the download fetch (value is already "Bearer "). */ + headers?: Record; + /** Basic auth: token to append as ?authtoken= (existing behavior). */ + authtoken?: string; +} + +/** + * Terminal auth failure for secured asset downloads: the server kept rejecting the token even + * after a forced refresh. Download loops throw this to abort the whole phase instead of failing + * every remaining asset individually. + */ +export class SecuredAssetAuthError extends Error { + readonly status: number; + + constructor(status: number) { + super( + `Secured asset download authentication failed (HTTP ${status}) even after refreshing credentials. ` + + 'Please log in again (csdx auth:login) and re-run the export.', + ); + this.name = 'SecuredAssetAuthError'; + this.status = status; + } +} + +/** + * Resolve auth for secured asset binary downloads. + * OAuth → Authorization: Bearer header (getAuthDetails handles proactive expiry refresh). + * Basic → authtoken query param (existing behavior). + * + * Pass `forceRefresh` after a 401: the server rejected a token that is still inside its local + * expiry window (revoked/invalidated), so force a refresh — concurrent callers are deduped by + * authHandler's in-flight refresh promise. No-op for basic auth, which cannot be refreshed. + */ +export async function getSecuredAssetAuth(forceRefresh = false): Promise { + if (forceRefresh && authenticationHandler.isOauthEnabled) { + await authHandler.compareOAuthExpiry(true); + } + await authenticationHandler.getAuthDetails(); + if (authenticationHandler.isOauthEnabled) { + return { headers: { authorization: authenticationHandler.accessToken } }; + } + const authtoken = configHandler.get('authtoken'); + return authtoken ? { authtoken } : {}; +} export function getArrayFromResponse(data: unknown, arrayKey: string): unknown[] { if (Array.isArray(data)) return data; diff --git a/packages/contentstack-asset-management/src/utils/index.ts b/packages/contentstack-asset-management/src/utils/index.ts index b78e6bf0a..44064543f 100644 --- a/packages/contentstack-asset-management/src/utils/index.ts +++ b/packages/contentstack-asset-management/src/utils/index.ts @@ -5,8 +5,11 @@ export { getArrayFromResponse, getAssetItems, getReadableStreamFromDownloadResponse, + getSecuredAssetAuth, + SecuredAssetAuthError, writeStreamToFile, } from './export-helpers'; +export type { SecuredAssetAuth } from './export-helpers'; export { chunkArray, runInBatches } from './concurrent-batch'; export { withRetry, RetryableHttpError, isRetryableStatus, parseRetryAfterMs } from './retry'; export { detectAssetManagementExportFromContentDir } from './detect-asset-management-export'; diff --git a/packages/contentstack-asset-management/test/unit/export/assets.test.ts b/packages/contentstack-asset-management/test/unit/export/assets.test.ts index ae930da8b..1665f44ef 100644 --- a/packages/contentstack-asset-management/test/unit/export/assets.test.ts +++ b/packages/contentstack-asset-management/test/unit/export/assets.test.ts @@ -1,10 +1,10 @@ import { expect } from 'chai'; import sinon from 'sinon'; -import { configHandler } from '@contentstack/cli-utilities'; import ExportAssets from '../../../src/export/assets'; import { CSAssetsExportAdapter } from '../../../src/export/base'; import * as chunkedJsonReader from '../../../src/utils/chunked-json-reader'; +import * as exportHelpers from '../../../src/utils/export-helpers'; import * as retryModule from '../../../src/utils/retry'; import type { CSAssetsAPIConfig, LinkedWorkspace } from '../../../src/types/cs-assets-api'; @@ -209,7 +209,7 @@ describe('ExportAssets', () => { }); it('should append authtoken to URL when securedAssets is true', async () => { - sinon.stub(configHandler, 'get').returns('my-auth-token'); + sinon.stub(exportHelpers, 'getSecuredAssetAuth').resolves({ authtoken: 'my-auth-token' }); wireStreaming([{ uid: 'a1', url: 'https://cdn.example.com/a1.png', filename: 'img.png' }] as any); fetchStub.callsFake(async () => makeFetchResponse() as any); @@ -220,7 +220,7 @@ describe('ExportAssets', () => { }); it('should use "&" separator when URL already contains "?"', async () => { - sinon.stub(configHandler, 'get').returns('my-token'); + sinon.stub(exportHelpers, 'getSecuredAssetAuth').resolves({ authtoken: 'my-token' }); wireStreaming([{ uid: 'a1', url: 'https://cdn.example.com/a1?v=1', filename: 'img.png' }] as any); fetchStub.callsFake(async () => makeFetchResponse() as any); @@ -258,4 +258,87 @@ describe('ExportAssets', () => { expect(assetTicks[0].args[2]).to.equal('No response body'); }); }); + + describe('secured asset downloads', () => { + const securedContext: ExportContext = { ...exportContext, securedAssets: true }; + const asset = { uid: 'a1', url: 'https://cdn.example.com/a1.png', filename: 'img.png' }; + const make401 = () => ({ ok: false, status: 401, headers: { get: (): string | null => null } }); + + it('should send the Authorization header (and no authtoken param) for OAuth', async () => { + wireStreaming([asset] as any); + sinon.stub(exportHelpers, 'getSecuredAssetAuth').resolves({ headers: { authorization: 'Bearer oauth-token' } }); + fetchStub.callsFake(async () => makeFetchResponse() as any); + + const exporter = new ExportAssets(apiConfig, securedContext); + await exporter.start(workspace, spaceDir); + + const [url, init] = fetchStub.firstCall.args; + expect(url).to.equal(asset.url); + expect(init).to.deep.equal({ headers: { authorization: 'Bearer oauth-token' } }); + }); + + it('should append ?authtoken= (and no headers) for basic auth', async () => { + wireStreaming([asset] as any); + sinon.stub(exportHelpers, 'getSecuredAssetAuth').resolves({ authtoken: 'basic-token' }); + fetchStub.callsFake(async () => makeFetchResponse() as any); + + const exporter = new ExportAssets(apiConfig, securedContext); + await exporter.start(workspace, spaceDir); + + const [url, init] = fetchStub.firstCall.args; + expect(url).to.equal(`${asset.url}?authtoken=basic-token`); + expect(init).to.be.undefined; + }); + + it('should not resolve auth or attach credentials for unsecured exports', async () => { + wireStreaming([asset] as any); + const authStub = sinon.stub(exportHelpers, 'getSecuredAssetAuth'); + fetchStub.callsFake(async () => makeFetchResponse() as any); + + const exporter = new ExportAssets(apiConfig, exportContext); + await exporter.start(workspace, spaceDir); + + expect(authStub.called).to.be.false; + const [url, init] = fetchStub.firstCall.args; + expect(url).to.equal(asset.url); + expect(init).to.be.undefined; + }); + + it('should force-refresh once on 401 and succeed with the fresh token', async () => { + wireStreaming([asset] as any); + const authStub = sinon + .stub(exportHelpers, 'getSecuredAssetAuth') + .callsFake(async (force?: boolean) => + force ? { headers: { authorization: 'Bearer fresh' } } : { headers: { authorization: 'Bearer stale' } }, + ); + fetchStub.callsFake(async (_url: any, init: any) => + init?.headers?.authorization === 'Bearer fresh' ? (makeFetchResponse() as any) : (make401() as any), + ); + + const exporter = new ExportAssets(apiConfig, securedContext); + await exporter.start(workspace, spaceDir); + + expect(authStub.calledWith(true)).to.be.true; + expect(fetchStub.callCount).to.equal(2); + const tickStub = (CSAssetsExportAdapter.prototype as any).tick as sinon.SinonStub; + const assetTicks = tickStub.getCalls().filter((c) => String(c.args[1]).startsWith('asset:')); + expect(assetTicks).to.have.length(1); + expect(assetTicks[0].args[0]).to.be.true; + }); + + it('should abort the space with SecuredAssetAuthError when 401 persists after refresh', async () => { + wireStreaming(assetItems); + sinon.stub(exportHelpers, 'getSecuredAssetAuth').resolves({ headers: { authorization: 'Bearer bad' } }); + fetchStub.callsFake(async () => make401() as any); + + const exporter = new ExportAssets(apiConfig, securedContext); + try { + await exporter.start(workspace, spaceDir); + expect.fail('should have thrown'); + } catch (e: any) { + expect(e).to.be.instanceOf(exportHelpers.SecuredAssetAuthError); + expect(e.message).to.include('401'); + } + }); + }); }); diff --git a/packages/contentstack-asset-management/test/unit/query-export/cs-assets-query-exporter.test.ts b/packages/contentstack-asset-management/test/unit/query-export/cs-assets-query-exporter.test.ts index 87e5dbe6a..801a679d8 100644 --- a/packages/contentstack-asset-management/test/unit/query-export/cs-assets-query-exporter.test.ts +++ b/packages/contentstack-asset-management/test/unit/query-export/cs-assets-query-exporter.test.ts @@ -10,6 +10,7 @@ import ExportAssetTypes from '../../../src/export/asset-types'; import ExportFields from '../../../src/export/fields'; import { CSAssetsExportAdapter } from '../../../src/export/base'; import { CSAssetsAdapter } from '../../../src/utils/cs-assets-api-adapter'; +import * as exportHelpers from '../../../src/utils/export-helpers'; import * as retryModule from '../../../src/utils/retry'; import type { CsAssetsQueryExportOptions } from '../../../src/types/cs-assets-api'; @@ -17,6 +18,7 @@ import type { CsAssetsQueryExportOptions } from '../../../src/types/cs-assets-ap describe('CsAssetsQueryExporter', () => { let exportDir: string; let searchAssetsStub: sinon.SinonStub; + let fetchStub: sinon.SinonStub; const baseOptions: CsAssetsQueryExportOptions = { linkedWorkspaces: [{ uid: 'main', space_uid: 'space-1', is_default: true }], exportDir: '', @@ -47,19 +49,22 @@ describe('CsAssetsQueryExporter', () => { }); sinon.stub(CSAssetsExportAdapter.prototype as any, 'writeItemsToChunkedJson').resolves(); // Downloads now run through makeConcurrentCall; fake it by invoking the - // promisifyHandler synchronously over each element of every apiBatch. + // promisifyHandler over each element of every apiBatch. Mirror the real + // implementation's Promise.allSettled semantics: handler rejections are swallowed. sinon.stub(CSAssetsAdapter.prototype, 'makeConcurrentCall').callsFake(async (env: any, handler: any) => { const batches = env?.apiBatches ?? []; for (let batchIndex = 0; batchIndex < batches.length; batchIndex++) { + const batchPromises: Array> = []; for (let index = 0; index < batches[batchIndex].length; index++) { - if (handler) await handler({ index, batchIndex, isLastRequest: false }); + if (handler) batchPromises.push(handler({ index, batchIndex, isLastRequest: false })); } + await Promise.allSettled(batchPromises); } }); // Run the download retry wrapper inline (single attempt, no backoff) and serve a fake binary // so download attempts don't hit the network or wait on real retry delays. sinon.stub(retryModule, 'withRetry').callsFake(async (fn: () => Promise) => fn()); - sinon.stub(globalThis, 'fetch').callsFake( + fetchStub = sinon.stub(globalThis, 'fetch').callsFake( async () => ({ ok: true, @@ -116,6 +121,97 @@ describe('CsAssetsQueryExporter', () => { const folders = JSON.parse(await fs.readFile(foldersPath, 'utf-8')); expect(folders).to.be.an('array').that.is.empty; }); + + describe('secured asset downloads', () => { + const securedOptions: CsAssetsQueryExportOptions = { ...baseOptions, securedAssets: true }; + const make401 = () => ({ ok: false, status: 401, headers: { get: (): string | null => null } }); + + it('should send the Authorization header (and no authtoken param) for OAuth', async () => { + securedOptions.exportDir = exportDir; + const authStub = sinon + .stub(exportHelpers, 'getSecuredAssetAuth') + .resolves({ headers: { authorization: 'Bearer oauth-token' } }); + + const exporter = new CsAssetsQueryExporter(securedOptions); + await exporter.export(['asset-1', 'asset-2']); + + expect(authStub.called).to.be.true; + expect(fetchStub.called).to.be.true; + const [url, init] = fetchStub.firstCall.args; + expect(url).to.equal('https://cdn.example.com/a1.png'); + expect(init).to.deep.equal({ headers: { authorization: 'Bearer oauth-token' } }); + }); + + it('should append ?authtoken= (and no headers) for basic auth', async () => { + securedOptions.exportDir = exportDir; + sinon.stub(exportHelpers, 'getSecuredAssetAuth').resolves({ authtoken: 'basic-token' }); + + const exporter = new CsAssetsQueryExporter(securedOptions); + await exporter.export(['asset-1']); + + const [url, init] = fetchStub.firstCall.args; + expect(url).to.equal('https://cdn.example.com/a1.png?authtoken=basic-token'); + expect(init).to.be.undefined; + }); + + it('should not resolve auth or attach credentials for unsecured exports', async () => { + const authStub = sinon.stub(exportHelpers, 'getSecuredAssetAuth'); + + const exporter = new CsAssetsQueryExporter(baseOptions); + await exporter.export(['asset-1']); + + expect(authStub.called).to.be.false; + const [url, init] = fetchStub.firstCall.args; + expect(url).to.equal('https://cdn.example.com/a1.png'); + expect(init).to.be.undefined; + }); + + it('should force-refresh once on 401 and succeed with the fresh token', async () => { + securedOptions.exportDir = exportDir; + const authStub = sinon + .stub(exportHelpers, 'getSecuredAssetAuth') + .callsFake(async (force?: boolean) => + force ? { headers: { authorization: 'Bearer fresh' } } : { headers: { authorization: 'Bearer stale' } }, + ); + fetchStub.callsFake(async (_url: any, init: any) => + init?.headers?.authorization === 'Bearer fresh' + ? ({ + ok: true, + status: 200, + body: new ReadableStream({ + start(controller) { + controller.enqueue(new TextEncoder().encode('x')); + controller.close(); + }, + }), + } as any) + : (make401() as any), + ); + + const exporter = new CsAssetsQueryExporter(securedOptions); + await exporter.export(['asset-1']); + + expect(authStub.calledWith(true)).to.be.true; + }); + + it('should stop downloading after a 401 persists post-refresh instead of failing every asset', async () => { + securedOptions.exportDir = exportDir; + const authStub = sinon + .stub(exportHelpers, 'getSecuredAssetAuth') + .resolves({ headers: { authorization: 'Bearer bad' } }); + fetchStub.callsFake(async () => make401() as any); + + // export() marks the space as failed and resolves (per-space isolation) — the key + // behavior is that later batches are never fetched once auth failed hard. + // Concurrency 1 → one asset per batch, so the abort is observable on the second asset. + const exporter = new CsAssetsQueryExporter({ ...securedOptions, downloadAssetsConcurrency: 1 }); + await exporter.export(['asset-1', 'asset-2']); + + // asset-1: initial fetch + post-refresh fetch = 2; asset-2: skipped entirely. + expect(fetchStub.callCount).to.equal(2); + expect(authStub.calledWith(true)).to.be.true; + }); + }); }); describe('CSAssetsAdapter.searchAssets', () => { diff --git a/packages/contentstack-asset-management/test/unit/utils/export-helpers.test.ts b/packages/contentstack-asset-management/test/unit/utils/export-helpers.test.ts index adcd20896..a3fbc5ed0 100644 --- a/packages/contentstack-asset-management/test/unit/utils/export-helpers.test.ts +++ b/packages/contentstack-asset-management/test/unit/utils/export-helpers.test.ts @@ -1,10 +1,13 @@ import { expect } from 'chai'; +import sinon from 'sinon'; import { PassThrough } from 'node:stream'; +import { authHandler, authenticationHandler, configHandler } from '@contentstack/cli-utilities'; import { getArrayFromResponse, getAssetItems, getReadableStreamFromDownloadResponse, + getSecuredAssetAuth, writeStreamToFile, } from '../../../src/utils/export-helpers'; @@ -96,6 +99,72 @@ describe('export-helpers', () => { }); }); + describe('getSecuredAssetAuth', () => { + afterEach(() => { + sinon.restore(); + }); + + it('should return an authorization header when OAuth is enabled', async () => { + sinon.stub(authenticationHandler, 'getAuthDetails').resolves(); + sinon.stub(authenticationHandler, 'isOauthEnabled').get(() => true); + sinon.stub(authenticationHandler, 'accessToken').get(() => 'Bearer oauth-token-123'); + + const auth = await getSecuredAssetAuth(); + expect(auth.headers).to.deep.equal({ authorization: 'Bearer oauth-token-123' }); + expect(auth.authtoken).to.be.undefined; + }); + + it('should return the authtoken for basic auth', async () => { + sinon.stub(authenticationHandler, 'getAuthDetails').resolves(); + sinon.stub(authenticationHandler, 'isOauthEnabled').get(() => false); + sinon.stub(configHandler, 'get').withArgs('authtoken').returns('basic-token-456'); + + const auth = await getSecuredAssetAuth(); + expect(auth.authtoken).to.equal('basic-token-456'); + expect(auth.headers).to.be.undefined; + }); + + it('should return an empty object when no authtoken is configured', async () => { + sinon.stub(authenticationHandler, 'getAuthDetails').resolves(); + sinon.stub(authenticationHandler, 'isOauthEnabled').get(() => false); + sinon.stub(configHandler, 'get').withArgs('authtoken').returns(undefined); + + const auth = await getSecuredAssetAuth(); + expect(auth).to.deep.equal({}); + }); + + it('should refresh auth details before resolving the token', async () => { + const getAuthDetailsStub = sinon.stub(authenticationHandler, 'getAuthDetails').resolves(); + sinon.stub(authenticationHandler, 'isOauthEnabled').get(() => true); + sinon.stub(authenticationHandler, 'accessToken').get(() => 'Bearer fresh-token'); + + await getSecuredAssetAuth(); + expect(getAuthDetailsStub.calledOnce).to.be.true; + }); + + it('should force an upstream token refresh when forceRefresh is set and OAuth is enabled', async () => { + const compareStub = sinon.stub(authHandler, 'compareOAuthExpiry').resolves(); + sinon.stub(authenticationHandler, 'getAuthDetails').resolves(); + sinon.stub(authenticationHandler, 'isOauthEnabled').get(() => true); + sinon.stub(authenticationHandler, 'accessToken').get(() => 'Bearer fresh'); + + const auth = await getSecuredAssetAuth(true); + expect(compareStub.calledOnceWith(true)).to.be.true; + expect(auth.headers).to.deep.equal({ authorization: 'Bearer fresh' }); + }); + + it('should not force a refresh for basic auth (cannot be refreshed)', async () => { + const compareStub = sinon.stub(authHandler, 'compareOAuthExpiry').resolves(); + sinon.stub(authenticationHandler, 'getAuthDetails').resolves(); + sinon.stub(authenticationHandler, 'isOauthEnabled').get(() => false); + sinon.stub(configHandler, 'get').withArgs('authtoken').returns('basic-token'); + + const auth = await getSecuredAssetAuth(true); + expect(compareStub.called).to.be.false; + expect(auth.authtoken).to.equal('basic-token'); + }); + }); + describe('writeStreamToFile', () => { it('should resolve when stream finishes writing', async () => { const source = new PassThrough(); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c8c990cac..b756676c8 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -393,7 +393,7 @@ importers: version: 2.0.0-beta.10(@types/node@20.19.43) '@contentstack/delivery-sdk': specifier: ^5.2.0 - version: 5.2.2 + version: 5.3.0 '@contentstack/management': specifier: ^1.30.2 version: 1.30.4(debug@4.4.3) @@ -1663,76 +1663,76 @@ packages: '@asamuzakjp/dom-selector@2.0.2': resolution: {integrity: sha512-x1KXOatwofR6ZAYzXRBL5wrdV0vwNxlTCK9NCuLqAzQYARqGcvFwiJA6A1ERuh+dgeA4Dxm3JBYictIes+SqUQ==} - '@aws-sdk/checksums@3.1000.16': - resolution: {integrity: sha512-EKnvkXSmz3IpA99tCNuI+dLFXyZyClSm8zns9sB/elvkU+MTuomAs6toJMPMBf98/fICG/urXDkzGz0/c3yyAQ==} + '@aws-sdk/checksums@3.1000.18': + resolution: {integrity: sha512-IImkbEyXdV6/uaF5r6Wkk+8718mQw1ll83j0a4a30R3JM/rHVFdWAiT4jtJpFjJiIwM/oJ6SxIxr0z2TaQUGqw==} engines: {node: '>=20.0.0'} - '@aws-sdk/client-cloudfront@3.1086.0': - resolution: {integrity: sha512-MbqWAu9sNRgnCvuTvVSMgsUD9g4DCvhmiXc5G77/Lq3ty6joCsk/VdQ8naqw/IqMZc3HSVZMTqVugI8yHtq4Rw==} + '@aws-sdk/client-cloudfront@3.1088.0': + resolution: {integrity: sha512-ljqqpLZZrkeWR5U4A42YstyQyRhbcZHfGnh28ovDALlZ24rq+yP2o+nV84OlObjxRoRYYSZUxNga0Qi8aXNJtg==} engines: {node: '>=20.0.0'} - '@aws-sdk/client-s3@3.1086.0': - resolution: {integrity: sha512-6+7mVMPKetZmmF2L1yRJ+rN9b1OwVgc5sju2mj8ixdxuGjtVZ0ekFlcWGBFMQT9gpFk55PPLBng/XRYO3qah5A==} + '@aws-sdk/client-s3@3.1088.0': + resolution: {integrity: sha512-9ryKKxnjtJRKLDI24P+2gQZki7k28BeV6gz1AySvFDWayhjAZuh4QZjCyx55tqR8Oz0IJxWutgJRO43dDOcXoQ==} engines: {node: '>=20.0.0'} - '@aws-sdk/core@3.975.1': - resolution: {integrity: sha512-8qh/6EYb7hl/ZwVfQufhbMEZs1gQIc7GbdrIf4eprQJ7cv042+74nE6l3YDfyWNzb9iPXb8fRyYSHkNIk5eE6Q==} + '@aws-sdk/core@3.975.3': + resolution: {integrity: sha512-7ur3kCKuvPLqlsZ2XlvnNBVQ7KkpSu6Y6dOTwSPHLrFpTEfZM8isLBJc4cgv96WB7GifeVM436mpycwxBd2vEA==} engines: {node: '>=20.0.0'} - '@aws-sdk/credential-provider-env@3.972.57': - resolution: {integrity: sha512-1RfJaF7SW1TOnvNGU7kaYjwUf5H3sfm+synGH1bHhRlqcnxCt3szebH3dmKEyY4tuGcbQ6ffzUT89cRitBV8OQ==} + '@aws-sdk/credential-provider-env@3.972.59': + resolution: {integrity: sha512-Ny5e4Mfh3QPmiAc0AiUe+cbTXDlxkU3Rc+EpWOfyWeWEy6yp7Fa1KmfNeCc+1a8by9zQ9gtohmiQUkMPScF3ng==} engines: {node: '>=20.0.0'} - '@aws-sdk/credential-provider-http@3.972.59': - resolution: {integrity: sha512-sRCkpTiFnCdQvuaRVjQ6SVoHu6i7RUpurVo1c4F81HWhPvUJ7Wdp5MNtSdX1O29CNXc8em3O5m52hCjVtAD9SA==} + '@aws-sdk/credential-provider-http@3.972.61': + resolution: {integrity: sha512-8jAjgStl5Ytq4+HF3X/9f+EmRinaRbGRRtQGktlPfBRVx73H+R1y48vIeXerQtYGFaUqkEp3fT6jP854rVO2yQ==} engines: {node: '>=20.0.0'} - '@aws-sdk/credential-provider-ini@3.973.1': - resolution: {integrity: sha512-6d8H6ZAh3ZPKZ6fe1nG2OWeZEZPtt9ravoD1dezPdPtsSkJRoxGAnFSHwKT3E/Te6fHE30zRzjV6TD12rvF6yQ==} + '@aws-sdk/credential-provider-ini@3.973.3': + resolution: {integrity: sha512-WpuqYX4gGkx++fCTSWE8+41JzkZVcrI50SH48Ml4CsG1pyuHKyMmpw/FixBHDrmjoQ553PmeCLa/fZIcst+WyA==} engines: {node: '>=20.0.0'} - '@aws-sdk/credential-provider-login@3.972.63': - resolution: {integrity: sha512-GREWRrMj0XnNKMaVa/Mauoaui26qBEHu71WWqXbwZOu/jFQOnPZjTf7u0KtGKC8VGa6VUs9kDWGgocrKNLS9vw==} + '@aws-sdk/credential-provider-login@3.972.65': + resolution: {integrity: sha512-xr9rgjYEdmC2Tpg2lwt9o+nOEaK9Qpd+dBjzrVCuWWyQfvhO91Ezu0Hh9ts2VUxOZxmS/k5T9msa34e4R1bnrQ==} engines: {node: '>=20.0.0'} - '@aws-sdk/credential-provider-node@3.972.67': - resolution: {integrity: sha512-oYlzWst56rlhhjbYnexwv5hVLYe1cW4liLObhDfxDLI4RAQzleMVHQgQgx7XsC4HKj4e3kjT8v9DId+Pi/dndw==} + '@aws-sdk/credential-provider-node@3.972.69': + resolution: {integrity: sha512-wbJGGesd0Tl18bmUcbj1xJ+e7CpuRJ6PIpMywLFuUttGy615lua87cJ0EA8pFpY/QgPuUXbnupWBtSPJ9tyZhg==} engines: {node: '>=20.0.0'} - '@aws-sdk/credential-provider-process@3.972.57': - resolution: {integrity: sha512-TiVQhuU0pbhIZAUZacbPHMyzrIdiH+lnx+PMY/Pu/b93dJrq3wdZwzUJ0TPpvNxaqbHsxJvQZW3/h/beLiKq7Q==} + '@aws-sdk/credential-provider-process@3.972.59': + resolution: {integrity: sha512-DlZF2/MhLlatDdlrIy3CUCpfdbLrKx+3SMjVo+WyHnPpwzkc/M3vwAHw4OVJf7DMvO+4vfRqSCMc/E9I1auN0g==} engines: {node: '>=20.0.0'} - '@aws-sdk/credential-provider-sso@3.973.1': - resolution: {integrity: sha512-3foTZUJ4821Ij60X7K3NJroygiZLnbBmarN+T//O2cjkISan90zElN3NBmgSlDrTQ7Gs6z/yO8V7h60QNcDZHQ==} + '@aws-sdk/credential-provider-sso@3.973.3': + resolution: {integrity: sha512-hmdDHoy2G5Es2e8IgelNMYUuSQI6uCIAKZMJ2u2PdKDhxvbk1uWD/g4+R7R5c/tJfKEB1+KjjWiaoCr/S+ZTiQ==} engines: {node: '>=20.0.0'} - '@aws-sdk/credential-provider-web-identity@3.972.63': - resolution: {integrity: sha512-8qZLFhM69eKcS37m459ctPR05Qimycm/74OPVioe6wNZabMT54GYhwBju0+J656RkMasNSawWQu+c8CmBe3TUQ==} + '@aws-sdk/credential-provider-web-identity@3.972.65': + resolution: {integrity: sha512-gHQb/Kt0chjk/JQDa/GJDqmAvEuVn8n7z10wK2h0LFM9TUDRkohgOO4aEF+s2sBLM0br7Cl5W6P7phgjrrJvLQ==} engines: {node: '>=20.0.0'} - '@aws-sdk/middleware-sdk-s3@3.972.62': - resolution: {integrity: sha512-k8JJwYXVYlOOjWnPZDThQS1xDFJgi5Dokt73qFlDtrZAbdcint5aIdjB9XgJAAQVP5OoqcefQmh1FYXiPpvsvw==} + '@aws-sdk/middleware-sdk-s3@3.972.64': + resolution: {integrity: sha512-RBi43anhDBUv+HCfxCOXwGOE7GmT4n7ChV04Mwr22RhXTNcamW/iWnJlOotDPCZSrJ4dEvhZSiWWQMwLX+ZhFA==} engines: {node: '>=20.0.0'} - '@aws-sdk/nested-clients@3.997.31': - resolution: {integrity: sha512-BDHTpwcsZHEBNEJzOg/B1BkFYJxAXY50dau/NyVWs3d51F0WgIUGSWZot/Os+N3KpDhXeaXnz37mWffAvduREw==} + '@aws-sdk/nested-clients@3.997.33': + resolution: {integrity: sha512-dVZOroI/r3/ENvqNGgjMPul+jjlz9GddfVusgTXlVjfZj5isibOxecLkGQbRPp8XOuX+RAfjXLFgPkD1JS5xrw==} engines: {node: '>=20.0.0'} - '@aws-sdk/signature-v4-multi-region@3.996.39': - resolution: {integrity: sha512-8+srXqYIF8KYMLC4FxMLEM5Ek7kUNibJu1R4m8/fUhhNYIZZz26oGtKkCr8I/HiG2fFQxBvaGgQZT4/mqRCSnA==} + '@aws-sdk/signature-v4-multi-region@3.996.41': + resolution: {integrity: sha512-QMUytg+FQMGouc8gHS00KoYih3+N6cqmVI/pQGOIo7Nr7OpQaiXjSYOuL+vsPZ1tymY4LAQ8MYcHJmws5LRxng==} engines: {node: '>=20.0.0'} - '@aws-sdk/token-providers@3.1083.0': - resolution: {integrity: sha512-s0woKnxuHrExLc5L2ArIH5BMkbonHPtt+5hSBM8oknp9M6QTuUmmAmJ2E0EdzCGONrO+8+ADPqvv6UX0nNcc7A==} + '@aws-sdk/token-providers@3.1088.0': + resolution: {integrity: sha512-4ObatWt2qpJg5FBk4LOOKrTQYzaqeewAtdO3r9ZO8lH9YqLtpTzLyIdy0mJ+nVdfYOnqISkKNfmzP22bNDhwyw==} engines: {node: '>=20.0.0'} - '@aws-sdk/types@3.974.0': - resolution: {integrity: sha512-QIBrw90CDm4O0UaIIzkU6DrFdeJzEb2Va5EPEVpyldj6sHJxB6cshhStJuhZxk3wR3PmjJlYsjPmY1kNb+KGBg==} + '@aws-sdk/types@3.974.2': + resolution: {integrity: sha512-3W6IUtSxFbH6X7Wb7DzGCV5QiFQsd0g8bOfntpmDxQlzBoKWUMBu/JPQR0DwkE+Hpnxd6db1tXbOwdeHddG6cA==} engines: {node: '>=20.0.0'} - '@aws-sdk/xml-builder@3.972.34': - resolution: {integrity: sha512-wHhWL1y7sN3enBA8POrPpQM5jCcmu2ozyhbRei4c8OjVcEaEs6yLucLa/pla457ggS/ysuy7bosagz3HaJkZXA==} + '@aws-sdk/xml-builder@3.972.36': + resolution: {integrity: sha512-RdGmS1GLrtaTOLE1ElSluMldNrpk9Emq6uYs8SS8iHlu5xTAmM9rRkM91o48+rIRryBtyO9t+uLYCoMG6jVMVA==} engines: {node: '>=20.0.0'} '@aws/lambda-invoke-store@0.3.0': @@ -2368,8 +2368,8 @@ packages: '@contentstack/core@1.4.1': resolution: {integrity: sha512-QfLa8WUwquWSwvF8EltLyzQTkeNE2I9b9PBkPe21w0d5PnHOagxFzDNCYN4VO/zuJ52sNtKLFIFUcvLsOPk9ww==} - '@contentstack/delivery-sdk@5.2.2': - resolution: {integrity: sha512-hSRYbeWAIq46nkg94LH10jFTmUUJOQF18fw6BTvhfFbK0q98m6QYTLY8BswpQYuhqcH6HNu3wGam+SKzGwSGkg==} + '@contentstack/delivery-sdk@5.3.0': + resolution: {integrity: sha512-jtEV/SOX4iqzQYM4zaWtGhttyl7sio09XMf28FiiJ/TkaneRQNBWK5u2B0CWKHxtxp/nEWYhH2sgUB/HdR56tw==} engines: {node: '>=18'} '@contentstack/json-rte-serializer@2.1.0': @@ -3373,24 +3373,24 @@ packages: Deprecated: no longer maintained and no longer used by Sinon packages. See https://github.com/sinonjs/nise/issues/243 for replacement details. - '@smithy/core@3.29.3': - resolution: {integrity: sha512-L+Ys6ecjk5vwPMAKHBpPKlJ3DkqwNcnfEISXBZIsVvWG/XKXfsAP8mwIYlTeLcd2ElHdesPI8OuOmJSFAPhm6A==} + '@smithy/core@3.29.4': + resolution: {integrity: sha512-G1GRglAabzEhqghJMBAd54FkRS7SAFGHEwbhcI9r+O+LIMuFsLyXkLZkCoFSgAglRu8s/URVXJB0hglq3ZipIg==} engines: {node: '>=18.0.0'} - '@smithy/credential-provider-imds@4.4.8': - resolution: {integrity: sha512-q9J7JTiXrAhB8sDp4px97uEPT7CwKH61Co78grdNQvU8QZAdiuaSRhP0tUVf2ogy36RZTrlMU1rBmDEH+cnkiA==} + '@smithy/credential-provider-imds@4.4.9': + resolution: {integrity: sha512-2nfV4qRKiYeXU4zD2vvSCfg5dfp/BuhrM73vt7q9gzBhxs4rbPxXY21wo+kyI3bRmXcEGRnCLTaW8O437jzHIg==} engines: {node: '>=18.0.0'} - '@smithy/fetch-http-handler@5.6.5': - resolution: {integrity: sha512-SuqeisTyPoiIPtIYru/sGxGyXzmZ+8nnFOhC+qRPglt06Ebd1yH//CDltZB2J/3WBNVhwfUaZ0EtHB3cm2X32g==} + '@smithy/fetch-http-handler@5.6.6': + resolution: {integrity: sha512-NHLgAlORUFZjn5ZfhYuyyKMlXA1WLYOdGxEhyNxrPpbJzoacGbl0chn1lN2KiZ8mpNVk0tV5607CSYlYs/OFgw==} engines: {node: '>=18.0.0'} - '@smithy/node-http-handler@4.9.5': - resolution: {integrity: sha512-bNqdxTQTxmLbomSmlkZFz8L6B/feQ2HHzw4L2zY7Ecp2XffYAZq2uzdWDdxJHJFbEvqd+SRuluJso0P8+xPdbw==} + '@smithy/node-http-handler@4.9.6': + resolution: {integrity: sha512-odd+HYx3OLcXRSEz0ZeF3JQdSYdK8QnRgA2N87cPW7coWIbKfRk7a9VQjfeWQLqnzrDLk23KMEn46p8N7M/JFg==} engines: {node: '>=18.0.0'} - '@smithy/signature-v4@5.6.4': - resolution: {integrity: sha512-B89bpf2t/y/wia6LZ+4JfHXYQT9PnVftsH05rgJKKIStS7r/4XSs9HOjtPoLtgcA6HCW9jVqX5DBbq7E0PAkiQ==} + '@smithy/signature-v4@5.6.5': + resolution: {integrity: sha512-MO5VEhwVl0BN7xVoVeNrZfiUFoQtqxUbgl6/RwOTlMMxCSjblG8twSrVTwz3J4w9WZxd2rBfBAUXjH77agspBg==} engines: {node: '>=18.0.0'} '@smithy/types@4.16.1': @@ -4483,8 +4483,8 @@ packages: resolution: {integrity: sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==} engines: {node: '>=10'} - caniuse-lite@1.0.30001805: - resolution: {integrity: sha512-52noaS3DubycKSXaU30TwPGIp+POyQSUVa5jBEq3vkRkY0kjyb3LQgvhU6WGyCcyXqVLWO0Cw0Q6BSdD0kUfVA==} + caniuse-lite@1.0.30001806: + resolution: {integrity: sha512-72Cuvd95zbSYPKq6Fhg8eDJRlzgWDf7/mtoZv6Qe/DYNCEBdNxoA3+rZAU2ZhGCpZlns3EssFavaZomckT5Uuw==} capital-case@1.0.4: resolution: {integrity: sha512-ds37W8CytHgwnhGGTi88pcPyR15qoNkOpYwmMMfnWqqWgESapLqvDx6huFjQ5vqWSn2Z06173XNA7LtMOeUh1A==} @@ -5117,8 +5117,8 @@ packages: engines: {node: '>=0.10.0'} hasBin: true - electron-to-chromium@1.5.389: - resolution: {integrity: sha512-cEto7aeOqBfU1D+c5py5pE+ooscKE75JifxLBdFUZsqAxRS6y7kebtxAZvICszSl05gPjYHDTjY+lXpyGvpJbg==} + electron-to-chromium@1.5.392: + resolution: {integrity: sha512-1yQq3VQCZRwsnYc67Oc+1fge6Lwtn0hzi6zmEVkB61Zx21kTbwJAW4dFLadl5Rc1tKhG/kSpYXnfiAhu0f0a1g==} elegant-spinner@1.0.1: resolution: {integrity: sha512-B+ZM+RXvRqQaAmkMlO/oSe5nMUOaUnyfGYCEHoR8wrXsZR2mA0XVibsxV1bvTwxdRWah1PkQqso2EzhILGHtEQ==} @@ -9152,8 +9152,8 @@ packages: resolution: {integrity: sha512-+QU2zd6OTD8XWIJCbffaiQeH9U73qIqafo1x6V1snCWYGJf6cVE0cDR4D8xRzcEnfI21IFrUPzPGtcPf8AC+Rw==} engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} - ws@8.21.0: - resolution: {integrity: sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==} + ws@8.21.1: + resolution: {integrity: sha512-+0NTnW77fFN/DjQi6k/Sq/Yvk4Sgajw7urW8V+asjXnRgDs9gyGkdb7EzgfhA4goXsRIZKE28fzIXBHEzhuiWw==} engines: {node: '>=10.0.0'} peerDependencies: bufferutil: ^4.0.1 @@ -9282,176 +9282,176 @@ snapshots: css-tree: 2.3.1 is-potential-custom-element-name: 1.0.1 - '@aws-sdk/checksums@3.1000.16': + '@aws-sdk/checksums@3.1000.18': dependencies: - '@aws-sdk/core': 3.975.1 - '@aws-sdk/types': 3.974.0 - '@smithy/core': 3.29.3 + '@aws-sdk/core': 3.975.3 + '@aws-sdk/types': 3.974.2 + '@smithy/core': 3.29.4 '@smithy/types': 4.16.1 tslib: 2.8.1 - '@aws-sdk/client-cloudfront@3.1086.0': + '@aws-sdk/client-cloudfront@3.1088.0': dependencies: - '@aws-sdk/core': 3.975.1 - '@aws-sdk/credential-provider-node': 3.972.67 - '@aws-sdk/types': 3.974.0 - '@smithy/core': 3.29.3 - '@smithy/fetch-http-handler': 5.6.5 - '@smithy/node-http-handler': 4.9.5 + '@aws-sdk/core': 3.975.3 + '@aws-sdk/credential-provider-node': 3.972.69 + '@aws-sdk/types': 3.974.2 + '@smithy/core': 3.29.4 + '@smithy/fetch-http-handler': 5.6.6 + '@smithy/node-http-handler': 4.9.6 '@smithy/types': 4.16.1 tslib: 2.8.1 - '@aws-sdk/client-s3@3.1086.0': - dependencies: - '@aws-sdk/checksums': 3.1000.16 - '@aws-sdk/core': 3.975.1 - '@aws-sdk/credential-provider-node': 3.972.67 - '@aws-sdk/middleware-sdk-s3': 3.972.62 - '@aws-sdk/signature-v4-multi-region': 3.996.39 - '@aws-sdk/types': 3.974.0 - '@smithy/core': 3.29.3 - '@smithy/fetch-http-handler': 5.6.5 - '@smithy/node-http-handler': 4.9.5 + '@aws-sdk/client-s3@3.1088.0': + dependencies: + '@aws-sdk/checksums': 3.1000.18 + '@aws-sdk/core': 3.975.3 + '@aws-sdk/credential-provider-node': 3.972.69 + '@aws-sdk/middleware-sdk-s3': 3.972.64 + '@aws-sdk/signature-v4-multi-region': 3.996.41 + '@aws-sdk/types': 3.974.2 + '@smithy/core': 3.29.4 + '@smithy/fetch-http-handler': 5.6.6 + '@smithy/node-http-handler': 4.9.6 '@smithy/types': 4.16.1 tslib: 2.8.1 - '@aws-sdk/core@3.975.1': + '@aws-sdk/core@3.975.3': dependencies: - '@aws-sdk/types': 3.974.0 - '@aws-sdk/xml-builder': 3.972.34 + '@aws-sdk/types': 3.974.2 + '@aws-sdk/xml-builder': 3.972.36 '@aws/lambda-invoke-store': 0.3.0 - '@smithy/core': 3.29.3 - '@smithy/signature-v4': 5.6.4 + '@smithy/core': 3.29.4 + '@smithy/signature-v4': 5.6.5 '@smithy/types': 4.16.1 bowser: 2.14.1 tslib: 2.8.1 - '@aws-sdk/credential-provider-env@3.972.57': + '@aws-sdk/credential-provider-env@3.972.59': dependencies: - '@aws-sdk/core': 3.975.1 - '@aws-sdk/types': 3.974.0 - '@smithy/core': 3.29.3 + '@aws-sdk/core': 3.975.3 + '@aws-sdk/types': 3.974.2 + '@smithy/core': 3.29.4 '@smithy/types': 4.16.1 tslib: 2.8.1 - '@aws-sdk/credential-provider-http@3.972.59': + '@aws-sdk/credential-provider-http@3.972.61': dependencies: - '@aws-sdk/core': 3.975.1 - '@aws-sdk/types': 3.974.0 - '@smithy/core': 3.29.3 - '@smithy/fetch-http-handler': 5.6.5 - '@smithy/node-http-handler': 4.9.5 + '@aws-sdk/core': 3.975.3 + '@aws-sdk/types': 3.974.2 + '@smithy/core': 3.29.4 + '@smithy/fetch-http-handler': 5.6.6 + '@smithy/node-http-handler': 4.9.6 '@smithy/types': 4.16.1 tslib: 2.8.1 - '@aws-sdk/credential-provider-ini@3.973.1': - dependencies: - '@aws-sdk/core': 3.975.1 - '@aws-sdk/credential-provider-env': 3.972.57 - '@aws-sdk/credential-provider-http': 3.972.59 - '@aws-sdk/credential-provider-login': 3.972.63 - '@aws-sdk/credential-provider-process': 3.972.57 - '@aws-sdk/credential-provider-sso': 3.973.1 - '@aws-sdk/credential-provider-web-identity': 3.972.63 - '@aws-sdk/nested-clients': 3.997.31 - '@aws-sdk/types': 3.974.0 - '@smithy/core': 3.29.3 - '@smithy/credential-provider-imds': 4.4.8 + '@aws-sdk/credential-provider-ini@3.973.3': + dependencies: + '@aws-sdk/core': 3.975.3 + '@aws-sdk/credential-provider-env': 3.972.59 + '@aws-sdk/credential-provider-http': 3.972.61 + '@aws-sdk/credential-provider-login': 3.972.65 + '@aws-sdk/credential-provider-process': 3.972.59 + '@aws-sdk/credential-provider-sso': 3.973.3 + '@aws-sdk/credential-provider-web-identity': 3.972.65 + '@aws-sdk/nested-clients': 3.997.33 + '@aws-sdk/types': 3.974.2 + '@smithy/core': 3.29.4 + '@smithy/credential-provider-imds': 4.4.9 '@smithy/types': 4.16.1 tslib: 2.8.1 - '@aws-sdk/credential-provider-login@3.972.63': + '@aws-sdk/credential-provider-login@3.972.65': dependencies: - '@aws-sdk/core': 3.975.1 - '@aws-sdk/nested-clients': 3.997.31 - '@aws-sdk/types': 3.974.0 - '@smithy/core': 3.29.3 + '@aws-sdk/core': 3.975.3 + '@aws-sdk/nested-clients': 3.997.33 + '@aws-sdk/types': 3.974.2 + '@smithy/core': 3.29.4 '@smithy/types': 4.16.1 tslib: 2.8.1 - '@aws-sdk/credential-provider-node@3.972.67': - dependencies: - '@aws-sdk/credential-provider-env': 3.972.57 - '@aws-sdk/credential-provider-http': 3.972.59 - '@aws-sdk/credential-provider-ini': 3.973.1 - '@aws-sdk/credential-provider-process': 3.972.57 - '@aws-sdk/credential-provider-sso': 3.973.1 - '@aws-sdk/credential-provider-web-identity': 3.972.63 - '@aws-sdk/types': 3.974.0 - '@smithy/core': 3.29.3 - '@smithy/credential-provider-imds': 4.4.8 + '@aws-sdk/credential-provider-node@3.972.69': + dependencies: + '@aws-sdk/credential-provider-env': 3.972.59 + '@aws-sdk/credential-provider-http': 3.972.61 + '@aws-sdk/credential-provider-ini': 3.973.3 + '@aws-sdk/credential-provider-process': 3.972.59 + '@aws-sdk/credential-provider-sso': 3.973.3 + '@aws-sdk/credential-provider-web-identity': 3.972.65 + '@aws-sdk/types': 3.974.2 + '@smithy/core': 3.29.4 + '@smithy/credential-provider-imds': 4.4.9 '@smithy/types': 4.16.1 tslib: 2.8.1 - '@aws-sdk/credential-provider-process@3.972.57': + '@aws-sdk/credential-provider-process@3.972.59': dependencies: - '@aws-sdk/core': 3.975.1 - '@aws-sdk/types': 3.974.0 - '@smithy/core': 3.29.3 + '@aws-sdk/core': 3.975.3 + '@aws-sdk/types': 3.974.2 + '@smithy/core': 3.29.4 '@smithy/types': 4.16.1 tslib: 2.8.1 - '@aws-sdk/credential-provider-sso@3.973.1': + '@aws-sdk/credential-provider-sso@3.973.3': dependencies: - '@aws-sdk/core': 3.975.1 - '@aws-sdk/nested-clients': 3.997.31 - '@aws-sdk/token-providers': 3.1083.0 - '@aws-sdk/types': 3.974.0 - '@smithy/core': 3.29.3 + '@aws-sdk/core': 3.975.3 + '@aws-sdk/nested-clients': 3.997.33 + '@aws-sdk/token-providers': 3.1088.0 + '@aws-sdk/types': 3.974.2 + '@smithy/core': 3.29.4 '@smithy/types': 4.16.1 tslib: 2.8.1 - '@aws-sdk/credential-provider-web-identity@3.972.63': + '@aws-sdk/credential-provider-web-identity@3.972.65': dependencies: - '@aws-sdk/core': 3.975.1 - '@aws-sdk/nested-clients': 3.997.31 - '@aws-sdk/types': 3.974.0 - '@smithy/core': 3.29.3 + '@aws-sdk/core': 3.975.3 + '@aws-sdk/nested-clients': 3.997.33 + '@aws-sdk/types': 3.974.2 + '@smithy/core': 3.29.4 '@smithy/types': 4.16.1 tslib: 2.8.1 - '@aws-sdk/middleware-sdk-s3@3.972.62': + '@aws-sdk/middleware-sdk-s3@3.972.64': dependencies: - '@aws-sdk/core': 3.975.1 - '@aws-sdk/signature-v4-multi-region': 3.996.39 - '@aws-sdk/types': 3.974.0 - '@smithy/core': 3.29.3 + '@aws-sdk/core': 3.975.3 + '@aws-sdk/signature-v4-multi-region': 3.996.41 + '@aws-sdk/types': 3.974.2 + '@smithy/core': 3.29.4 '@smithy/types': 4.16.1 tslib: 2.8.1 - '@aws-sdk/nested-clients@3.997.31': + '@aws-sdk/nested-clients@3.997.33': dependencies: - '@aws-sdk/core': 3.975.1 - '@aws-sdk/signature-v4-multi-region': 3.996.39 - '@aws-sdk/types': 3.974.0 - '@smithy/core': 3.29.3 - '@smithy/fetch-http-handler': 5.6.5 - '@smithy/node-http-handler': 4.9.5 + '@aws-sdk/core': 3.975.3 + '@aws-sdk/signature-v4-multi-region': 3.996.41 + '@aws-sdk/types': 3.974.2 + '@smithy/core': 3.29.4 + '@smithy/fetch-http-handler': 5.6.6 + '@smithy/node-http-handler': 4.9.6 '@smithy/types': 4.16.1 tslib: 2.8.1 - '@aws-sdk/signature-v4-multi-region@3.996.39': + '@aws-sdk/signature-v4-multi-region@3.996.41': dependencies: - '@aws-sdk/types': 3.974.0 - '@smithy/signature-v4': 5.6.4 + '@aws-sdk/types': 3.974.2 + '@smithy/signature-v4': 5.6.5 '@smithy/types': 4.16.1 tslib: 2.8.1 - '@aws-sdk/token-providers@3.1083.0': + '@aws-sdk/token-providers@3.1088.0': dependencies: - '@aws-sdk/core': 3.975.1 - '@aws-sdk/nested-clients': 3.997.31 - '@aws-sdk/types': 3.974.0 - '@smithy/core': 3.29.3 + '@aws-sdk/core': 3.975.3 + '@aws-sdk/nested-clients': 3.997.33 + '@aws-sdk/types': 3.974.2 + '@smithy/core': 3.29.4 '@smithy/types': 4.16.1 tslib: 2.8.1 - '@aws-sdk/types@3.974.0': + '@aws-sdk/types@3.974.2': dependencies: '@smithy/types': 4.16.1 tslib: 2.8.1 - '@aws-sdk/xml-builder@3.972.34': + '@aws-sdk/xml-builder@3.972.36': dependencies: '@smithy/types': 4.16.1 tslib: 2.8.1 @@ -10587,7 +10587,7 @@ snapshots: - debug - supports-color - '@contentstack/delivery-sdk@5.2.2': + '@contentstack/delivery-sdk@5.3.0': dependencies: '@contentstack/core': 1.4.1 '@contentstack/utils': 1.9.1 @@ -10638,7 +10638,7 @@ snapshots: '@contentstack/types-generator@3.10.2(graphql@16.14.2)': dependencies: - '@contentstack/delivery-sdk': 5.2.2 + '@contentstack/delivery-sdk': 5.3.0 '@gql2ts/from-schema': 2.0.0-4(graphql@16.14.2) async: 3.2.6 axios: 1.18.0 @@ -12281,32 +12281,32 @@ snapshots: '@sinonjs/text-encoding@0.7.3': {} - '@smithy/core@3.29.3': + '@smithy/core@3.29.4': dependencies: '@smithy/types': 4.16.1 tslib: 2.8.1 - '@smithy/credential-provider-imds@4.4.8': + '@smithy/credential-provider-imds@4.4.9': dependencies: - '@smithy/core': 3.29.3 + '@smithy/core': 3.29.4 '@smithy/types': 4.16.1 tslib: 2.8.1 - '@smithy/fetch-http-handler@5.6.5': + '@smithy/fetch-http-handler@5.6.6': dependencies: - '@smithy/core': 3.29.3 + '@smithy/core': 3.29.4 '@smithy/types': 4.16.1 tslib: 2.8.1 - '@smithy/node-http-handler@4.9.5': + '@smithy/node-http-handler@4.9.6': dependencies: - '@smithy/core': 3.29.3 + '@smithy/core': 3.29.4 '@smithy/types': 4.16.1 tslib: 2.8.1 - '@smithy/signature-v4@5.6.4': + '@smithy/signature-v4@5.6.5': dependencies: - '@smithy/core': 3.29.3 + '@smithy/core': 3.29.4 '@smithy/types': 4.16.1 tslib: 2.8.1 @@ -13825,8 +13825,8 @@ snapshots: browserslist@4.28.6: dependencies: baseline-browser-mapping: 2.10.43 - caniuse-lite: 1.0.30001805 - electron-to-chromium: 1.5.389 + caniuse-lite: 1.0.30001806 + electron-to-chromium: 1.5.392 node-releases: 2.0.51 update-browserslist-db: 1.2.3(browserslist@4.28.6) @@ -13911,7 +13911,7 @@ snapshots: camelcase@6.3.0: {} - caniuse-lite@1.0.30001805: {} + caniuse-lite@1.0.30001806: {} capital-case@1.0.4: dependencies: @@ -14584,7 +14584,7 @@ snapshots: dependencies: jake: 10.9.4 - electron-to-chromium@1.5.389: {} + electron-to-chromium@1.5.392: {} elegant-spinner@1.0.1: {} @@ -17318,7 +17318,7 @@ snapshots: whatwg-encoding: 3.1.1 whatwg-mimetype: 4.0.0 whatwg-url: 14.2.0 - ws: 8.21.0 + ws: 8.21.1 xml-name-validator: 5.0.0 transitivePeerDependencies: - bufferutil @@ -18017,8 +18017,8 @@ snapshots: oclif@4.23.27(@types/node@14.18.63): dependencies: - '@aws-sdk/client-cloudfront': 3.1086.0 - '@aws-sdk/client-s3': 3.1086.0 + '@aws-sdk/client-cloudfront': 3.1088.0 + '@aws-sdk/client-s3': 3.1088.0 '@inquirer/confirm': 3.2.0 '@inquirer/input': 2.3.0 '@inquirer/select': 2.5.0 @@ -18046,8 +18046,8 @@ snapshots: oclif@4.23.27(@types/node@18.19.130): dependencies: - '@aws-sdk/client-cloudfront': 3.1086.0 - '@aws-sdk/client-s3': 3.1086.0 + '@aws-sdk/client-cloudfront': 3.1088.0 + '@aws-sdk/client-s3': 3.1088.0 '@inquirer/confirm': 3.2.0 '@inquirer/input': 2.3.0 '@inquirer/select': 2.5.0 @@ -18075,8 +18075,8 @@ snapshots: oclif@4.23.27(@types/node@20.19.43): dependencies: - '@aws-sdk/client-cloudfront': 3.1086.0 - '@aws-sdk/client-s3': 3.1086.0 + '@aws-sdk/client-cloudfront': 3.1088.0 + '@aws-sdk/client-s3': 3.1088.0 '@inquirer/confirm': 3.2.0 '@inquirer/input': 2.3.0 '@inquirer/select': 2.5.0 @@ -18104,8 +18104,8 @@ snapshots: oclif@4.23.27(@types/node@22.20.1): dependencies: - '@aws-sdk/client-cloudfront': 3.1086.0 - '@aws-sdk/client-s3': 3.1086.0 + '@aws-sdk/client-cloudfront': 3.1088.0 + '@aws-sdk/client-s3': 3.1088.0 '@inquirer/confirm': 3.2.0 '@inquirer/input': 2.3.0 '@inquirer/select': 2.5.0 @@ -20209,7 +20209,7 @@ snapshots: imurmurhash: 0.1.4 signal-exit: 4.1.0 - ws@8.21.0: {} + ws@8.21.1: {} xdg-basedir@4.0.0: {}