diff --git a/apps/cli/src/commands/publish-manifest.spec.ts b/apps/cli/src/commands/publish-manifest.spec.ts new file mode 100644 index 0000000..1b21342 --- /dev/null +++ b/apps/cli/src/commands/publish-manifest.spec.ts @@ -0,0 +1,82 @@ +import { mkdtempSync, readFileSync, writeFileSync } from 'node:fs'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; + +import { + createLocalRegistryBackend, + publishManifest, + rollbackManifest, +} from '@federation-wizards/core'; +import { stringify as stringifyYaml } from 'yaml'; + +function createWorkspace(): string { + const dir = mkdtempSync(join(tmpdir(), 'fwiz-cli-publish-')); + + const config = { + version: '1', + workspace: { type: 'plain' }, + hosts: [{ name: 'shell', port: 4200 }], + remotes: [{ name: 'checkout', port: 4201 }], + shared: { + react: { + singleton: true, + requiredVersion: '^19.0.0', + eager: false, + }, + }, + registry: { + type: 'http', + baseUrl: 'https://cdn.example.com', + prefix: 'mf', + }, + }; + + writeWorkspaceConfig(dir, config); + return dir; +} + +function writeWorkspaceConfig(dir: string, config: unknown): void { + writeFileSync( + join(dir, 'fwiz.config.yaml'), + `${stringifyYaml(config)}\n`, + 'utf8', + ); +} + +describe('fwiz publish-manifest command', () => { + it('publishes and rolls back a remote through core APIs', async () => { + const dir = createWorkspace(); + const backend = createLocalRegistryBackend(join(dir, '.registry')); + + const publishResult = await publishManifest( + { + cwd: dir, + version: '1.0.0', + remote: 'checkout', + }, + backend, + ); + + expect(publishResult.remotes[0]?.name).toBe('checkout'); + + const rollbackResult = await rollbackManifest( + { + cwd: dir, + remote: 'checkout', + version: '1.0.0', + dryRun: true, + }, + backend, + ); + + expect(rollbackResult.dryRun).toBe(true); + expect(rollbackResult.previousVersion).toBe('1.0.0'); + + const manifest = readFileSync( + join(dir, '.registry/mf/checkout/1.0.0/mf-manifest.json'), + 'utf8', + ); + + expect(manifest).toContain('"name": "checkout"'); + }); +}); diff --git a/apps/cli/src/commands/publish-manifest.ts b/apps/cli/src/commands/publish-manifest.ts new file mode 100644 index 0000000..3a2d2c1 --- /dev/null +++ b/apps/cli/src/commands/publish-manifest.ts @@ -0,0 +1,110 @@ +import type { Command } from 'commander'; + +import { + PublishManifestError, + publishManifest, + rollbackManifest, +} from '@federation-wizards/core'; + +export function registerPublishManifestCommand(program: Command): void { + program + .command('publish-manifest') + .description( + 'Generate versioned mf-manifest.json files and publish them to a central registry', + ) + .summary('Upload remote manifests and update remotes-registry.json') + .addHelpText( + 'after', + ` +Examples: + $ fwiz publish-manifest + $ fwiz publish-manifest --version 1.2.0 + $ fwiz publish-manifest --remote checkout + $ fwiz publish-manifest --dry-run + $ fwiz publish-manifest --rollback --remote checkout + $ fwiz publish-manifest --rollback --remote checkout --version 1.1.0 + +Requires a registry section in fwiz.config.yaml with either an HTTP PUT +endpoint or S3-compatible storage settings. +`, + ) + .option( + '--cwd ', + 'Workspace directory (defaults to current working directory)', + process.cwd(), + ) + .option('--dry-run', 'Generate manifests without uploading or updating registry') + .option('--version ', 'Manifest version to publish or roll back to') + .option('--remote ', 'Publish or roll back a single remote') + .option('--rollback', 'Roll back the selected remote to a previous version') + .action( + async (options: { + cwd: string; + dryRun?: boolean; + version?: string; + remote?: string; + rollback?: boolean; + }) => { + try { + if (options.rollback) { + if (!options.remote) { + console.error('--remote is required when using --rollback.'); + process.exitCode = 1; + return; + } + + const result = await rollbackManifest({ + cwd: options.cwd, + remote: options.remote, + version: options.version, + dryRun: options.dryRun, + }); + + if (result.dryRun) { + console.log( + `[dry-run] Would roll back ${result.remote} from ${result.currentVersion} to ${result.previousVersion}.`, + ); + return; + } + + console.log( + `Rolled back ${result.remote} from ${result.currentVersion} to ${result.previousVersion}.`, + ); + console.log(`Registry: ${result.registryUrl}`); + return; + } + + const result = await publishManifest({ + cwd: options.cwd, + version: options.version, + remote: options.remote, + dryRun: options.dryRun, + }); + + if (result.dryRun) { + console.log( + `[dry-run] Would publish version ${result.version} for ${result.remotes.length} remote(s).`, + ); + } else { + console.log( + `Published version ${result.version} for ${result.remotes.length} remote(s).`, + ); + } + + for (const remote of result.remotes) { + console.log(` - ${remote.name}: ${remote.manifestUrl}`); + } + + console.log(`Registry: ${result.registryUrl}`); + } catch (error) { + const message = + error instanceof PublishManifestError || + error instanceof Error + ? error.message + : String(error); + console.error(message); + process.exitCode = 1; + } + }, + ); +} diff --git a/apps/cli/src/program.ts b/apps/cli/src/program.ts index 1537071..2db7010 100644 --- a/apps/cli/src/program.ts +++ b/apps/cli/src/program.ts @@ -1,6 +1,7 @@ import { Command } from 'commander'; import { registerInitCommand } from './commands/init.js'; +import { registerPublishManifestCommand } from './commands/publish-manifest.js'; export function createProgram(): Command { const program = new Command(); @@ -11,6 +12,7 @@ export function createProgram(): Command { .version('0.0.1'); registerInitCommand(program); + registerPublishManifestCommand(program); return program; } diff --git a/libs/core/package.json b/libs/core/package.json index 5f66821..b7f2c15 100644 --- a/libs/core/package.json +++ b/libs/core/package.json @@ -16,6 +16,7 @@ } }, "dependencies": { + "@aws-sdk/client-s3": "^3.848.0", "joi": "^18.2.1", "tslib": "^2.3.0", "yaml": "^2.9.0" diff --git a/libs/core/src/index.ts b/libs/core/src/index.ts index 3485403..067afa9 100644 --- a/libs/core/src/index.ts +++ b/libs/core/src/index.ts @@ -5,3 +5,5 @@ export * from './lib/config/init.js'; export * from './lib/config/load.js'; export * from './lib/workspace/detect.js'; export * from './lib/workspace/patch.js'; +export * from './lib/manifest/generate.js'; +export * from './lib/registry/index.js'; diff --git a/libs/core/src/lib/config/schema.spec.ts b/libs/core/src/lib/config/schema.spec.ts new file mode 100644 index 0000000..b6f08f4 --- /dev/null +++ b/libs/core/src/lib/config/schema.spec.ts @@ -0,0 +1,41 @@ +import { describe, expect, it } from 'vitest'; + +import { createDefaultConfig } from './defaults.js'; +import { validateFwizConfig } from './schema.js'; + +describe('validateFwizConfig registry section', () => { + it('accepts an HTTP registry configuration', () => { + const config = { + ...createDefaultConfig({ type: 'plain', appProjects: [] }), + registry: { + type: 'http', + baseUrl: 'https://cdn.example.com/mf', + prefix: 'assets', + }, + }; + + const result = validateFwizConfig(config); + + expect(result.valid).toBe(true); + if (result.valid) { + expect(result.value.registry?.type).toBe('http'); + } + }); + + it('requires bucket for S3 registry configuration', () => { + const config = { + ...createDefaultConfig({ type: 'plain', appProjects: [] }), + registry: { + type: 's3', + baseUrl: 'https://cdn.example.com', + }, + }; + + const result = validateFwizConfig(config); + + expect(result.valid).toBe(false); + if (!result.valid) { + expect(result.warnings.join(' ')).toMatch(/bucket/i); + } + }); +}); diff --git a/libs/core/src/lib/config/schema.ts b/libs/core/src/lib/config/schema.ts index 1cc0169..2f6fde3 100644 --- a/libs/core/src/lib/config/schema.ts +++ b/libs/core/src/lib/config/schema.ts @@ -20,6 +20,35 @@ const remoteSchema = Joi.object({ port: Joi.number().integer().min(1).max(65535).required(), }); +const registrySchema = Joi.object({ + type: Joi.string().valid('s3', 'http').required(), + baseUrl: Joi.string().uri().required(), + prefix: Joi.string().optional(), + remotesRegistryKey: Joi.string().optional(), + uploadBaseUrl: Joi.string().uri().optional(), + headers: Joi.object().pattern(Joi.string(), Joi.string()).optional(), + bucket: Joi.string().when('type', { + is: 's3', + then: Joi.required(), + otherwise: Joi.forbidden(), + }), + region: Joi.string().when('type', { + is: 's3', + then: Joi.optional(), + otherwise: Joi.forbidden(), + }), + endpoint: Joi.string().uri().when('type', { + is: 's3', + then: Joi.optional(), + otherwise: Joi.forbidden(), + }), + forcePathStyle: Joi.boolean().when('type', { + is: 's3', + then: Joi.optional(), + otherwise: Joi.forbidden(), + }), +}); + export const fwizConfigSchema = Joi.object({ version: Joi.string().required(), workspace: Joi.object({ @@ -28,6 +57,7 @@ export const fwizConfigSchema = Joi.object({ hosts: Joi.array().items(hostSchema).min(1).required(), remotes: Joi.array().items(remoteSchema).required(), shared: Joi.object().pattern(Joi.string(), sharedDependencySchema).required(), + registry: registrySchema.optional(), }); export function validateFwizConfig( diff --git a/libs/core/src/lib/config/types.ts b/libs/core/src/lib/config/types.ts index c82bfe8..7edf4d8 100644 --- a/libs/core/src/lib/config/types.ts +++ b/libs/core/src/lib/config/types.ts @@ -18,6 +18,21 @@ export interface RemoteConfig { port: number; } +export type RegistryBackendType = 's3' | 'http'; + +export interface RegistryConfig { + type: RegistryBackendType; + baseUrl: string; + prefix?: string; + remotesRegistryKey?: string; + uploadBaseUrl?: string; + headers?: Record; + bucket?: string; + region?: string; + endpoint?: string; + forcePathStyle?: boolean; +} + export interface FwizConfig { version: string; workspace: { @@ -26,6 +41,7 @@ export interface FwizConfig { hosts: HostConfig[]; remotes: RemoteConfig[]; shared: Record; + registry?: RegistryConfig; } export interface WorkspaceInfo { diff --git a/libs/core/src/lib/manifest/generate.spec.ts b/libs/core/src/lib/manifest/generate.spec.ts new file mode 100644 index 0000000..bf2badb --- /dev/null +++ b/libs/core/src/lib/manifest/generate.spec.ts @@ -0,0 +1,35 @@ +import { describe, expect, it } from 'vitest'; + +import { generateMfManifest } from './generate.js'; + +describe('generateMfManifest', () => { + it('generates a versioned mf-manifest.json shape for a remote', () => { + const manifest = generateMfManifest({ + remote: { name: 'checkout', project: 'checkout', port: 4201 }, + version: '1.2.0', + shared: { + react: { + singleton: true, + requiredVersion: '^19.0.0', + eager: false, + }, + }, + registry: { + type: 'http', + baseUrl: 'https://cdn.example.com/assets', + prefix: 'mf', + }, + buildId: 'test-build-id', + }); + + expect(manifest.id).toBe('test-build-id'); + expect(manifest.name).toBe('checkout'); + expect(manifest.metaData.buildInfo.buildVersion).toBe('1.2.0'); + expect(manifest.metaData.publicPath).toBe( + 'https://cdn.example.com/assets/mf/checkout/1.2.0/', + ); + expect(manifest.shared).toHaveLength(1); + expect(manifest.exposes[0]?.name).toBe('./Module'); + expect(manifest.remotes).toEqual([]); + }); +}); diff --git a/libs/core/src/lib/manifest/generate.ts b/libs/core/src/lib/manifest/generate.ts new file mode 100644 index 0000000..9e25851 --- /dev/null +++ b/libs/core/src/lib/manifest/generate.ts @@ -0,0 +1,80 @@ +import { createHash } from 'node:crypto'; + +import type { + RegistryConfig, + RemoteConfig, + SharedDependencyConfig, +} from '../config/types.js'; +import { getManifestStorageKey, getPublicUrl } from '../registry/paths.js'; +import type { MfManifest } from '../registry/types.js'; + +const EMPTY_ASSETS = { + js: { sync: [] as string[], async: [] as string[] }, + css: { sync: [] as string[], async: [] as string[] }, +}; + +export interface GenerateManifestOptions { + remote: RemoteConfig; + version: string; + shared: Record; + registry: RegistryConfig; + buildId?: string; +} + +function createBuildId(remoteName: string, version: string): string { + return createHash('sha256') + .update(`${remoteName}:${version}`) + .digest('hex') + .slice(0, 16); +} + +function toManifestShared( + shared: Record, +): MfManifest['shared'] { + return Object.entries(shared).map(([name, config]) => ({ + id: name, + name, + version: config.requiredVersion, + singleton: config.singleton, + requiredVersion: config.requiredVersion, + assets: EMPTY_ASSETS, + })); +} + +export function generateMfManifest(options: GenerateManifestOptions): MfManifest { + const { remote, version, shared, registry } = options; + const buildId = options.buildId ?? createBuildId(remote.name, version); + const storageKey = getManifestStorageKey(registry, remote.name, version); + const publicPath = `${getPublicUrl(registry, storageKey).replace(/\/mf-manifest\.json$/, '')}/`; + const remoteEntryName = 'remoteEntry.js'; + + return { + id: buildId, + name: remote.name, + metaData: { + name: remote.name, + globalName: remote.name, + type: 'app', + buildInfo: { + buildVersion: version, + buildName: remote.project ?? remote.name, + }, + remoteEntry: { + path: remoteEntryName, + name: remoteEntryName, + type: 'var', + }, + publicPath, + }, + shared: toManifestShared(shared), + remotes: [], + exposes: [ + { + id: `${remote.name}:./Module`, + name: './Module', + path: './src/index', + assets: EMPTY_ASSETS, + }, + ], + }; +} diff --git a/libs/core/src/lib/registry/backends/factory.ts b/libs/core/src/lib/registry/backends/factory.ts new file mode 100644 index 0000000..d2c7530 --- /dev/null +++ b/libs/core/src/lib/registry/backends/factory.ts @@ -0,0 +1,19 @@ +import type { RegistryConfig } from '../../config/types.js'; +import { createHttpRegistryBackend } from './http.js'; +import { createS3RegistryBackend } from './s3.js'; +import type { RegistryBackend } from './types.js'; + +export function createRegistryBackend( + registry: RegistryConfig, +): RegistryBackend { + switch (registry.type) { + case 'http': + return createHttpRegistryBackend(registry); + case 's3': + return createS3RegistryBackend(registry); + default: { + const exhaustive: never = registry.type; + throw new Error(`Unsupported registry type: ${exhaustive}`); + } + } +} diff --git a/libs/core/src/lib/registry/backends/http.spec.ts b/libs/core/src/lib/registry/backends/http.spec.ts new file mode 100644 index 0000000..5b174ef --- /dev/null +++ b/libs/core/src/lib/registry/backends/http.spec.ts @@ -0,0 +1,96 @@ +import { createServer, type IncomingMessage, type ServerResponse } from 'node:http'; +import { AddressInfo } from 'node:net'; + +import { afterEach, describe, expect, it } from 'vitest'; + +import { createHttpRegistryBackend } from './http.js'; + +const servers: Array> = []; + +function startTestServer( + handler: (request: IncomingMessage, response: ServerResponse) => void, +): Promise { + return new Promise((resolve, reject) => { + const server = createServer(handler); + servers.push(server); + + server.listen(0, '127.0.0.1', () => { + const address = server.address() as AddressInfo; + resolve(`http://127.0.0.1:${address.port}`); + }); + + server.on('error', reject); + }); +} + +afterEach(async () => { + await Promise.all( + servers.splice(0).map( + (server) => + new Promise((resolve, reject) => { + server.close((error) => { + if (error) { + reject(error); + return; + } + + resolve(); + }); + }), + ), + ); +}); + +describe('createHttpRegistryBackend', () => { + it('uploads and reads objects via HTTP PUT/GET', async () => { + const store = new Map(); + const baseUrl = await startTestServer((request, response) => { + const url = new URL(request.url ?? '/', 'http://localhost'); + const key = url.pathname.replace(/^\//, ''); + + if (request.method === 'PUT') { + const chunks: Buffer[] = []; + + request.on('data', (chunk) => { + chunks.push(Buffer.from(chunk)); + }); + + request.on('end', () => { + store.set(key, Buffer.concat(chunks).toString('utf8')); + response.writeHead(200); + response.end('ok'); + }); + return; + } + + if (request.method === 'GET') { + const value = store.get(key); + + if (!value) { + response.writeHead(404); + response.end('not found'); + return; + } + + response.writeHead(200, { 'content-type': 'application/json' }); + response.end(value); + return; + } + + response.writeHead(405); + response.end('method not allowed'); + }); + + const backend = createHttpRegistryBackend({ + type: 'http', + baseUrl, + prefix: 'mf', + }); + + await backend.put('mf/checkout/1.0.0/mf-manifest.json', '{"name":"checkout"}\n'); + const value = await backend.get('mf/checkout/1.0.0/mf-manifest.json'); + + expect(value).toBe('{"name":"checkout"}\n'); + expect(await backend.get('mf/missing.json')).toBeNull(); + }); +}); diff --git a/libs/core/src/lib/registry/backends/http.ts b/libs/core/src/lib/registry/backends/http.ts new file mode 100644 index 0000000..31bd353 --- /dev/null +++ b/libs/core/src/lib/registry/backends/http.ts @@ -0,0 +1,92 @@ +import type { RegistryConfig } from '../../config/types.js'; +import { joinRegistryPath } from '../paths.js'; +import type { RegistryBackend } from './types.js'; + +function trimSlashes(value: string): string { + return value.replace(/^\/+|\/+$/g, ''); +} + +function resolveUploadBaseUrl(registry: RegistryConfig): string { + const base = registry.uploadBaseUrl ?? registry.baseUrl; + return base.replace(/\/+$/, ''); +} + +function resolveObjectUrl(registry: RegistryConfig, key: string): string { + const uploadBase = resolveUploadBaseUrl(registry); + const normalizedKey = trimSlashes(key); + return `${uploadBase}/${normalizedKey}`; +} + +export function createHttpRegistryBackend( + registry: RegistryConfig, +): RegistryBackend { + const headers = registry.headers ?? {}; + + return { + async get(key: string): Promise { + const url = resolveObjectUrl(registry, key); + const response = await fetch(url, { + method: 'GET', + headers, + }); + + if (response.status === 404) { + return null; + } + + if (!response.ok) { + throw new Error( + `HTTP registry GET failed for ${url}: ${response.status} ${response.statusText}`, + ); + } + + return response.text(); + }, + + async put( + key: string, + body: string, + contentType = 'application/json', + ): Promise { + const url = resolveObjectUrl(registry, key); + const response = await fetch(url, { + method: 'PUT', + headers: { + 'content-type': contentType, + ...headers, + }, + body, + }); + + if (!response.ok) { + throw new Error( + `HTTP registry PUT failed for ${url}: ${response.status} ${response.statusText}`, + ); + } + }, + }; +} + +export function createHttpRegistryBackendFromBaseUrl( + baseUrl: string, + options: { + prefix?: string; + headers?: Record; + } = {}, +): RegistryBackend { + return createHttpRegistryBackend({ + type: 'http', + baseUrl, + uploadBaseUrl: baseUrl, + prefix: options.prefix, + headers: options.headers, + }); +} + +export function getHttpRegistryPublicUrl( + registry: RegistryConfig, + key: string, +): string { + const publicBase = registry.baseUrl.replace(/\/+$/, ''); + return joinRegistryPath(publicBase, trimSlashes(key)); +} diff --git a/libs/core/src/lib/registry/backends/local.ts b/libs/core/src/lib/registry/backends/local.ts new file mode 100644 index 0000000..e1e7ef2 --- /dev/null +++ b/libs/core/src/lib/registry/backends/local.ts @@ -0,0 +1,24 @@ +import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'; +import { dirname, join } from 'node:path'; + +import type { RegistryBackend } from './types.js'; + +export function createLocalRegistryBackend(rootDir: string): RegistryBackend { + return { + async get(key: string): Promise { + const filePath = join(rootDir, key); + + if (!existsSync(filePath)) { + return null; + } + + return readFileSync(filePath, 'utf8'); + }, + + async put(key: string, body: string): Promise { + const filePath = join(rootDir, key); + mkdirSync(dirname(filePath), { recursive: true }); + writeFileSync(filePath, body, 'utf8'); + }, + }; +} diff --git a/libs/core/src/lib/registry/backends/s3.spec.ts b/libs/core/src/lib/registry/backends/s3.spec.ts new file mode 100644 index 0000000..afba5ab --- /dev/null +++ b/libs/core/src/lib/registry/backends/s3.spec.ts @@ -0,0 +1,100 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest'; + +const send = vi.fn(); + +vi.mock('@aws-sdk/client-s3', () => { + class S3Client { + send = send; + } + + class GetObjectCommand { + input: unknown; + + constructor(input: unknown) { + this.input = input; + } + } + + class PutObjectCommand { + input: unknown; + + constructor(input: unknown) { + this.input = input; + } + } + + return { + S3Client, + GetObjectCommand, + PutObjectCommand, + }; +}); + +import { createS3RegistryBackend } from './s3.js'; + +describe('createS3RegistryBackend', () => { + beforeEach(() => { + send.mockReset(); + }); + + it('uploads objects with PutObjectCommand', async () => { + send.mockResolvedValueOnce({}); + + const backend = createS3RegistryBackend({ + type: 's3', + baseUrl: 'https://cdn.example.com', + bucket: 'mf-assets', + region: 'us-east-1', + }); + + await backend.put('checkout/1.0.0/mf-manifest.json', '{"name":"checkout"}\n'); + + expect(send).toHaveBeenCalledTimes(1); + expect(send.mock.calls[0]?.[0]).toMatchObject({ + input: { + Bucket: 'mf-assets', + Key: 'checkout/1.0.0/mf-manifest.json', + Body: '{"name":"checkout"}\n', + ContentType: 'application/json', + }, + }); + }); + + it('reads objects with GetObjectCommand', async () => { + send.mockResolvedValueOnce({ + Body: { + transformToString: async () => '{"name":"checkout"}\n', + }, + }); + + const backend = createS3RegistryBackend({ + type: 's3', + baseUrl: 'https://cdn.example.com', + bucket: 'mf-assets', + }); + + const value = await backend.get('checkout/1.0.0/mf-manifest.json'); + + expect(value).toBe('{"name":"checkout"}\n'); + expect(send.mock.calls[0]?.[0]).toMatchObject({ + input: { + Bucket: 'mf-assets', + Key: 'checkout/1.0.0/mf-manifest.json', + }, + }); + }); + + it('returns null when the object does not exist', async () => { + const error = new Error('not found'); + error.name = 'NoSuchKey'; + send.mockRejectedValueOnce(error); + + const backend = createS3RegistryBackend({ + type: 's3', + baseUrl: 'https://cdn.example.com', + bucket: 'mf-assets', + }); + + await expect(backend.get('missing.json')).resolves.toBeNull(); + }); +}); diff --git a/libs/core/src/lib/registry/backends/s3.ts b/libs/core/src/lib/registry/backends/s3.ts new file mode 100644 index 0000000..fb170ec --- /dev/null +++ b/libs/core/src/lib/registry/backends/s3.ts @@ -0,0 +1,89 @@ +import { + GetObjectCommand, + PutObjectCommand, + S3Client, +} from '@aws-sdk/client-s3'; + +import type { RegistryConfig } from '../../config/types.js'; +import type { RegistryBackend } from './types.js'; + +function readBody(body: unknown): Promise { + if (!body) { + return Promise.resolve(''); + } + + if (typeof body === 'string') { + return Promise.resolve(body); + } + + if (body instanceof Uint8Array) { + return Promise.resolve(new TextDecoder().decode(body)); + } + + if (typeof (body as { transformToString?: () => Promise }).transformToString === 'function') { + return (body as { transformToString: () => Promise }).transformToString(); + } + + return Promise.resolve(String(body)); +} + +export function createS3RegistryBackend(registry: RegistryConfig): RegistryBackend { + if (!registry.bucket) { + throw new Error('S3 registry requires a bucket.'); + } + + const client = new S3Client({ + region: registry.region ?? process.env.AWS_REGION ?? 'us-east-1', + endpoint: registry.endpoint, + forcePathStyle: registry.forcePathStyle, + credentials: + process.env.AWS_ACCESS_KEY_ID && process.env.AWS_SECRET_ACCESS_KEY + ? { + accessKeyId: process.env.AWS_ACCESS_KEY_ID, + secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY, + sessionToken: process.env.AWS_SESSION_TOKEN, + } + : undefined, + }); + + return { + async get(key: string): Promise { + try { + const response = await client.send( + new GetObjectCommand({ + Bucket: registry.bucket, + Key: key, + }), + ); + + return readBody(response.Body); + } catch (error) { + if ( + error && + typeof error === 'object' && + 'name' in error && + (error.name === 'NoSuchKey' || error.name === 'NotFound') + ) { + return null; + } + + throw error; + } + }, + + async put( + key: string, + body: string, + contentType = 'application/json', + ): Promise { + await client.send( + new PutObjectCommand({ + Bucket: registry.bucket, + Key: key, + Body: body, + ContentType: contentType, + }), + ); + }, + }; +} diff --git a/libs/core/src/lib/registry/backends/types.ts b/libs/core/src/lib/registry/backends/types.ts new file mode 100644 index 0000000..b5b81d9 --- /dev/null +++ b/libs/core/src/lib/registry/backends/types.ts @@ -0,0 +1,4 @@ +export interface RegistryBackend { + get(key: string): Promise; + put(key: string, body: string, contentType?: string): Promise; +} diff --git a/libs/core/src/lib/registry/index.ts b/libs/core/src/lib/registry/index.ts new file mode 100644 index 0000000..f9723c9 --- /dev/null +++ b/libs/core/src/lib/registry/index.ts @@ -0,0 +1,10 @@ +export * from './types.js'; +export * from './paths.js'; +export * from './publish.js'; +export * from './rollback.js'; +export * from './registry-io.js'; +export * from './backends/types.js'; +export * from './backends/factory.js'; +export * from './backends/http.js'; +export * from './backends/s3.js'; +export * from './backends/local.js'; diff --git a/libs/core/src/lib/registry/paths.ts b/libs/core/src/lib/registry/paths.ts new file mode 100644 index 0000000..fdc177c --- /dev/null +++ b/libs/core/src/lib/registry/paths.ts @@ -0,0 +1,40 @@ +import type { RegistryConfig } from '../config/types.js'; + +function trimSlashes(value: string): string { + return value.replace(/^\/+|\/+$/g, ''); +} + +export function joinRegistryPath(...segments: string[]): string { + return segments + .map((segment) => trimSlashes(segment)) + .filter((segment) => segment.length > 0) + .join('/'); +} + +export function getRegistryPrefix(registry: RegistryConfig): string { + return trimSlashes(registry.prefix ?? ''); +} + +export function getRemotesRegistryKey(registry: RegistryConfig): string { + return registry.remotesRegistryKey ?? 'remotes-registry.json'; +} + +export function getManifestStorageKey( + registry: RegistryConfig, + remoteName: string, + version: string, +): string { + const prefix = getRegistryPrefix(registry); + return joinRegistryPath(prefix, remoteName, version, 'mf-manifest.json'); +} + +export function getRemotesRegistryStorageKey(registry: RegistryConfig): string { + const prefix = getRegistryPrefix(registry); + return joinRegistryPath(prefix, getRemotesRegistryKey(registry)); +} + +export function getPublicUrl(registry: RegistryConfig, storageKey: string): string { + const baseUrl = registry.baseUrl.replace(/\/+$/, ''); + const key = trimSlashes(storageKey); + return `${baseUrl}/${key}`; +} diff --git a/libs/core/src/lib/registry/publish.spec.ts b/libs/core/src/lib/registry/publish.spec.ts new file mode 100644 index 0000000..c18e6d9 --- /dev/null +++ b/libs/core/src/lib/registry/publish.spec.ts @@ -0,0 +1,76 @@ +import { readFileSync } from 'node:fs'; +import { join } from 'node:path'; + +import { describe, expect, it } from 'vitest'; + +import { createLocalRegistryBackend } from './backends/local.js'; +import { publishManifest } from './publish.js'; +import { createTestWorkspace } from './test-helpers.js'; +import type { RemotesRegistry } from './types.js'; + +describe('publishManifest', () => { + it('publishes manifests and updates remotes-registry.json in a local round-trip', async () => { + const registryDir = createTestWorkspace({ + type: 'http', + baseUrl: 'https://cdn.example.com', + prefix: 'mf', + }); + const backend = createLocalRegistryBackend(join(registryDir, '.registry')); + + const result = await publishManifest( + { + cwd: registryDir, + version: '2.0.0', + remote: 'checkout', + }, + backend, + ); + + expect(result.dryRun).toBe(false); + expect(result.version).toBe('2.0.0'); + expect(result.remotes).toHaveLength(1); + expect(result.remotes[0]?.manifestUrl).toBe( + 'https://cdn.example.com/mf/checkout/2.0.0/mf-manifest.json', + ); + + const manifest = JSON.parse( + readFileSync( + join(registryDir, '.registry/mf/checkout/2.0.0/mf-manifest.json'), + 'utf8', + ), + ) as { name: string; metaData: { buildInfo: { buildVersion: string } } }; + + expect(manifest.name).toBe('checkout'); + expect(manifest.metaData.buildInfo.buildVersion).toBe('2.0.0'); + + const remotesRegistry = JSON.parse( + readFileSync(join(registryDir, '.registry/mf/remotes-registry.json'), 'utf8'), + ) as RemotesRegistry; + + expect(remotesRegistry.remotes.checkout?.current).toBe('2.0.0'); + expect( + remotesRegistry.remotes.checkout?.versions['2.0.0']?.manifestUrl, + ).toBe('https://cdn.example.com/mf/checkout/2.0.0/mf-manifest.json'); + }); + + it('supports dry-run without writing files', async () => { + const registryDir = createTestWorkspace({ + type: 'http', + baseUrl: 'https://cdn.example.com', + }); + const backend = createLocalRegistryBackend(join(registryDir, '.registry')); + + const result = await publishManifest( + { + cwd: registryDir, + version: '1.0.0', + dryRun: true, + }, + backend, + ); + + expect(result.dryRun).toBe(true); + expect(result.registryUpdated).toBe(false); + expect(result.remotes[0]?.uploaded).toBe(false); + }); +}); diff --git a/libs/core/src/lib/registry/publish.ts b/libs/core/src/lib/registry/publish.ts new file mode 100644 index 0000000..98512e4 --- /dev/null +++ b/libs/core/src/lib/registry/publish.ts @@ -0,0 +1,200 @@ +import { readFileSync } from 'node:fs'; +import { join } from 'node:path'; + +import { generateMfManifest } from '../manifest/generate.js'; +import { FwizConfigError, loadFwizConfig } from '../config/load.js'; +import type { RemoteConfig } from '../config/types.js'; +import { createRegistryBackend } from './backends/factory.js'; +import type { RegistryBackend } from './backends/types.js'; +import { + getManifestStorageKey, + getPublicUrl, + getRemotesRegistryStorageKey, +} from './paths.js'; +import { loadRemotesRegistry, saveRemotesRegistry } from './registry-io.js'; +import type { + PublishManifestOptions, + PublishManifestResult, + PublishedRemoteResult, +} from './types.js'; + +export class PublishManifestError extends Error { + constructor(message: string) { + super(message); + this.name = 'PublishManifestError'; + } +} + +function resolveVersion(cwd: string, explicitVersion?: string): string { + if (explicitVersion) { + return explicitVersion; + } + + try { + const packageJson = JSON.parse( + readFileSync(join(cwd, 'package.json'), 'utf8'), + ) as { version?: string }; + + if (packageJson.version) { + return packageJson.version; + } + } catch { + // Fall back to timestamp-based version below. + } + + return `0.0.0-${Date.now()}`; +} + +function selectRemotes( + remotes: RemoteConfig[], + remoteName?: string, +): RemoteConfig[] { + if (!remoteName) { + return remotes; + } + + const selected = remotes.filter((remote) => remote.name === remoteName); + + if (selected.length === 0) { + throw new PublishManifestError( + `Remote "${remoteName}" is not defined in fwiz.config.yaml.`, + ); + } + + return selected; +} + +function requireRegistryConfig( + config: ReturnType, +): NonNullable['registry']> { + if (!config.registry) { + throw new PublishManifestError( + 'fwiz.config.yaml is missing a registry section. Add registry.type, registry.baseUrl, and backend-specific settings before publishing.', + ); + } + + return config.registry; +} + +async function publishRemoteManifest( + backend: RegistryBackend, + options: { + remote: RemoteConfig; + version: string; + config: ReturnType; + dryRun: boolean; + }, +): Promise { + const registry = options.config.registry!; + const manifest = generateMfManifest({ + remote: options.remote, + version: options.version, + shared: options.config.shared, + registry, + }); + const storageKey = getManifestStorageKey( + registry, + options.remote.name, + options.version, + ); + const manifestUrl = getPublicUrl(registry, storageKey); + const payload = `${JSON.stringify(manifest, null, 2)}\n`; + + if (!options.dryRun) { + await backend.put(storageKey, payload); + } + + return { + name: options.remote.name, + manifestUrl, + storageKey, + uploaded: !options.dryRun, + }; +} + +function updateRegistryForPublish( + remotesRegistry: Awaited>, + publishedRemotes: PublishedRemoteResult[], + version: string, +): Awaited> { + const next = structuredClone(remotesRegistry); + + for (const published of publishedRemotes) { + const existing = next.remotes[published.name] ?? { + current: version, + versions: {}, + }; + + existing.current = version; + existing.versions[version] = { + manifestUrl: published.manifestUrl, + publishedAt: new Date().toISOString(), + }; + next.remotes[published.name] = existing; + } + + return next; +} + +export async function publishManifest( + options: PublishManifestOptions, + backendOverride?: RegistryBackend, +): Promise { + const config = loadFwizConfig(options.cwd); + const registry = requireRegistryConfig(config); + const backend = backendOverride ?? createRegistryBackend(registry); + const version = resolveVersion(options.cwd, options.version); + const dryRun = options.dryRun ?? false; + const remotes = selectRemotes(config.remotes, options.remote); + + if (remotes.length === 0) { + throw new PublishManifestError( + 'No remotes configured in fwiz.config.yaml.', + ); + } + + const publishedRemotes: PublishedRemoteResult[] = []; + + for (const remote of remotes) { + publishedRemotes.push( + await publishRemoteManifest(backend, { + remote, + version, + config, + dryRun, + }), + ); + } + + const currentRegistry = await loadRemotesRegistry(backend, registry); + const nextRegistry = updateRegistryForPublish( + currentRegistry, + publishedRemotes, + version, + ); + + const registryStorageKey = getRemotesRegistryStorageKey(registry); + let registryUrl = getPublicUrl(registry, registryStorageKey); + + if (!dryRun) { + registryUrl = await saveRemotesRegistry(backend, registry, nextRegistry); + } + + return { + dryRun, + version, + remotes: publishedRemotes, + registryUpdated: !dryRun, + registryUrl, + }; +} + +export function assertRegistryConfigured(cwd: string): void { + const config = loadFwizConfig(cwd); + + if (!config.registry) { + throw new FwizConfigError( + 'fwiz.config.yaml is missing a registry section required for publish-manifest.', + ); + } +} diff --git a/libs/core/src/lib/registry/registry-io.ts b/libs/core/src/lib/registry/registry-io.ts new file mode 100644 index 0000000..ed3fab9 --- /dev/null +++ b/libs/core/src/lib/registry/registry-io.ts @@ -0,0 +1,55 @@ +import type { RegistryConfig } from '../config/types.js'; +import type { RegistryBackend } from './backends/types.js'; +import { + getPublicUrl, + getRemotesRegistryStorageKey, +} from './paths.js'; +import type { RemotesRegistry } from './types.js'; + +const EMPTY_REGISTRY: RemotesRegistry = { + version: '1', + updatedAt: new Date(0).toISOString(), + remotes: {}, +}; + +export function parseRemotesRegistry(raw: string | null): RemotesRegistry { + if (!raw) { + return structuredClone(EMPTY_REGISTRY); + } + + const parsed = JSON.parse(raw) as RemotesRegistry; + + return { + version: parsed.version ?? '1', + updatedAt: parsed.updatedAt ?? new Date().toISOString(), + remotes: parsed.remotes ?? {}, + }; +} + +export async function loadRemotesRegistry( + backend: RegistryBackend, + registry: RegistryConfig, +): Promise { + const key = getRemotesRegistryStorageKey(registry); + const raw = await backend.get(key); + return parseRemotesRegistry(raw); +} + +export async function saveRemotesRegistry( + backend: RegistryBackend, + registry: RegistryConfig, + remotesRegistry: RemotesRegistry, +): Promise { + const key = getRemotesRegistryStorageKey(registry); + const payload = JSON.stringify( + { + ...remotesRegistry, + updatedAt: new Date().toISOString(), + }, + null, + 2, + ); + + await backend.put(key, `${payload}\n`); + return getPublicUrl(registry, key); +} diff --git a/libs/core/src/lib/registry/rollback.spec.ts b/libs/core/src/lib/registry/rollback.spec.ts new file mode 100644 index 0000000..c94041d --- /dev/null +++ b/libs/core/src/lib/registry/rollback.spec.ts @@ -0,0 +1,84 @@ +import { readFileSync } from 'node:fs'; +import { join } from 'node:path'; + +import { describe, expect, it } from 'vitest'; + +import { createLocalRegistryBackend } from './backends/local.js'; +import { publishManifest } from './publish.js'; +import { rollbackManifest } from './rollback.js'; +import { createTestWorkspace } from './test-helpers.js'; +import type { RemotesRegistry } from './types.js'; + +describe('rollbackManifest', () => { + it('rolls back to the previous published version', async () => { + const registryDir = createTestWorkspace({ + type: 'http', + baseUrl: 'https://cdn.example.com', + prefix: 'mf', + }); + const backend = createLocalRegistryBackend(join(registryDir, '.registry')); + + await publishManifest( + { cwd: registryDir, version: '1.0.0', remote: 'checkout' }, + backend, + ); + await publishManifest( + { cwd: registryDir, version: '2.0.0', remote: 'checkout' }, + backend, + ); + + const result = await rollbackManifest( + { cwd: registryDir, remote: 'checkout' }, + backend, + ); + + expect(result.rolledBack).toBe(true); + expect(result.currentVersion).toBe('2.0.0'); + expect(result.previousVersion).toBe('1.0.0'); + + const remotesRegistry = JSON.parse( + readFileSync(join(registryDir, '.registry/mf/remotes-registry.json'), 'utf8'), + ) as RemotesRegistry; + + expect(remotesRegistry.remotes.checkout?.current).toBe('1.0.0'); + expect(remotesRegistry.remotes.checkout?.versions['2.0.0']).toBeDefined(); + }); + + it('rolls back to an explicit version', async () => { + const registryDir = createTestWorkspace({ + type: 'http', + baseUrl: 'https://cdn.example.com', + }); + const backend = createLocalRegistryBackend(join(registryDir, '.registry')); + + await publishManifest( + { cwd: registryDir, version: '1.0.0', remote: 'checkout' }, + backend, + ); + await publishManifest( + { cwd: registryDir, version: '2.0.0', remote: 'checkout' }, + backend, + ); + await publishManifest( + { cwd: registryDir, version: '3.0.0', remote: 'checkout' }, + backend, + ); + + const result = await rollbackManifest( + { + cwd: registryDir, + remote: 'checkout', + version: '1.0.0', + }, + backend, + ); + + expect(result.previousVersion).toBe('1.0.0'); + + const remotesRegistry = JSON.parse( + readFileSync(join(registryDir, '.registry/remotes-registry.json'), 'utf8'), + ) as RemotesRegistry; + + expect(remotesRegistry.remotes.checkout?.current).toBe('1.0.0'); + }); +}); diff --git a/libs/core/src/lib/registry/rollback.ts b/libs/core/src/lib/registry/rollback.ts new file mode 100644 index 0000000..ff25163 --- /dev/null +++ b/libs/core/src/lib/registry/rollback.ts @@ -0,0 +1,98 @@ +import { loadFwizConfig } from '../config/load.js'; +import { createRegistryBackend } from './backends/factory.js'; +import type { RegistryBackend } from './backends/types.js'; +import { getPublicUrl, getRemotesRegistryStorageKey } from './paths.js'; +import { PublishManifestError } from './publish.js'; +import { loadRemotesRegistry, saveRemotesRegistry } from './registry-io.js'; +import type { + RollbackManifestOptions, + RollbackManifestResult, +} from './types.js'; + +function sortedPublishedVersions( + versions: Record, +): string[] { + return Object.entries(versions) + .sort((left, right) => + right[1].publishedAt.localeCompare(left[1].publishedAt), + ) + .map(([version]) => version); +} + +function resolveRollbackTarget( + currentVersion: string, + versions: Record, + explicitVersion?: string, +): string { + if (explicitVersion) { + if (!versions[explicitVersion]) { + throw new PublishManifestError( + `Version "${explicitVersion}" is not published for this remote.`, + ); + } + + return explicitVersion; + } + + const ordered = sortedPublishedVersions(versions).filter( + (version) => version !== currentVersion, + ); + + if (ordered.length === 0) { + throw new PublishManifestError( + 'No previous version is available to roll back to.', + ); + } + + return ordered[0]; +} + +export async function rollbackManifest( + options: RollbackManifestOptions, + backendOverride?: RegistryBackend, +): Promise { + const config = loadFwizConfig(options.cwd); + + if (!config.registry) { + throw new PublishManifestError( + 'fwiz.config.yaml is missing a registry section required for rollback.', + ); + } + + const registry = config.registry; + const backend = backendOverride ?? createRegistryBackend(registry); + const dryRun = options.dryRun ?? false; + const remotesRegistry = await loadRemotesRegistry(backend, registry); + const remoteEntry = remotesRegistry.remotes[options.remote]; + + if (!remoteEntry) { + throw new PublishManifestError( + `Remote "${options.remote}" has no published versions in the registry.`, + ); + } + + const previousVersion = resolveRollbackTarget( + remoteEntry.current, + remoteEntry.versions, + options.version, + ); + const currentVersion = remoteEntry.current; + + if (!dryRun) { + const nextRegistry = structuredClone(remotesRegistry); + nextRegistry.remotes[options.remote] = { + ...remoteEntry, + current: previousVersion, + }; + await saveRemotesRegistry(backend, registry, nextRegistry); + } + + return { + dryRun, + remote: options.remote, + previousVersion, + currentVersion, + rolledBack: !dryRun, + registryUrl: getPublicUrl(registry, getRemotesRegistryStorageKey(registry)), + }; +} diff --git a/libs/core/src/lib/registry/test-helpers.ts b/libs/core/src/lib/registry/test-helpers.ts new file mode 100644 index 0000000..8107940 --- /dev/null +++ b/libs/core/src/lib/registry/test-helpers.ts @@ -0,0 +1,32 @@ +import { mkdtempSync, writeFileSync } from 'node:fs'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; + +import { stringify as stringifyYaml } from 'yaml'; + +import { createDefaultConfig } from '../config/defaults.js'; +import { FWIZ_CONFIG_FILENAME } from '../config/init.js'; +import type { FwizConfig, RegistryConfig } from '../config/types.js'; + +export function createTestWorkspace( + registry: RegistryConfig, + overrides: Partial = {}, +): string { + const dir = mkdtempSync(join(tmpdir(), 'fwiz-registry-')); + const config = { + ...createDefaultConfig({ + type: 'plain', + appProjects: ['shell', 'checkout'], + }), + registry, + ...overrides, + } satisfies FwizConfig; + + writeFileSync( + join(dir, FWIZ_CONFIG_FILENAME), + `${stringifyYaml(config)}\n`, + 'utf8', + ); + + return dir; +} diff --git a/libs/core/src/lib/registry/types.ts b/libs/core/src/lib/registry/types.ts new file mode 100644 index 0000000..89c902b --- /dev/null +++ b/libs/core/src/lib/registry/types.ts @@ -0,0 +1,102 @@ +export interface MfManifestResourceInfo { + path: string; + name: string; + type: string; +} + +export interface MfManifestMetaData { + name: string; + globalName: string; + type: string; + buildInfo: { + buildVersion: string; + buildName: string; + }; + remoteEntry: MfManifestResourceInfo; + publicPath: string; +} + +export interface MfManifestShared { + id: string; + name: string; + version: string; + singleton: boolean; + requiredVersion: string; + assets: { + js: { sync: string[]; async: string[] }; + css: { sync: string[]; async: string[] }; + }; +} + +export interface MfManifestExpose { + id: string; + name: string; + path: string; + assets: { + js: { sync: string[]; async: string[] }; + css: { sync: string[]; async: string[] }; + }; +} + +export interface MfManifest { + id: string; + name: string; + metaData: MfManifestMetaData; + shared: MfManifestShared[]; + remotes: []; + exposes: MfManifestExpose[]; +} + +export interface RemoteVersionEntry { + manifestUrl: string; + publishedAt: string; +} + +export interface RemoteRegistryEntry { + current: string; + versions: Record; +} + +export interface RemotesRegistry { + version: string; + updatedAt: string; + remotes: Record; +} + +export interface PublishedRemoteResult { + name: string; + manifestUrl: string; + storageKey: string; + uploaded: boolean; +} + +export interface PublishManifestResult { + dryRun: boolean; + version: string; + remotes: PublishedRemoteResult[]; + registryUpdated: boolean; + registryUrl: string; +} + +export interface RollbackManifestResult { + dryRun: boolean; + remote: string; + previousVersion: string; + currentVersion: string; + rolledBack: boolean; + registryUrl: string; +} + +export interface PublishManifestOptions { + cwd: string; + version?: string; + remote?: string; + dryRun?: boolean; +} + +export interface RollbackManifestOptions { + cwd: string; + remote: string; + version?: string; + dryRun?: boolean; +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2639d3c..fcd359b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -177,6 +177,9 @@ importers: libs/core: dependencies: + '@aws-sdk/client-s3': + specifier: ^3.848.0 + version: 3.1067.0 joi: specifier: ^18.2.1 version: 18.2.1 @@ -210,6 +213,109 @@ importers: packages: + '@aws-crypto/crc32@5.2.0': + resolution: {integrity: sha512-nLbCWqQNgUiwwtFsen1AdzAtvuLRsQS8rYgMuxCrdKf9kOssamGLuPwyTY9wyYblNr9+1XM8v6zoDTPPSIeANg==} + engines: {node: '>=16.0.0'} + + '@aws-crypto/crc32c@5.2.0': + resolution: {integrity: sha512-+iWb8qaHLYKrNvGRbiYRHSdKRWhto5XlZUEBwDjYNf+ly5SVYG6zEoYIdxvf5R3zyeP16w4PLBn3rH1xc74Rag==} + + '@aws-crypto/sha1-browser@5.2.0': + resolution: {integrity: sha512-OH6lveCFfcDjX4dbAvCFSYUjJZjDr/3XJ3xHtjn3Oj5b9RjojQo8npoLeA/bNwkOkrSQ0wgrHzXk4tDRxGKJeg==} + + '@aws-crypto/sha256-browser@5.2.0': + resolution: {integrity: sha512-AXfN/lGotSQwu6HNcEsIASo7kWXZ5HYWvfOmSNKDsEqC4OashTp8alTmaz+F7TC2L083SFv5RdB+qU3Vs1kZqw==} + + '@aws-crypto/sha256-js@5.2.0': + resolution: {integrity: sha512-FFQQyu7edu4ufvIZ+OadFpHHOt+eSTBaYaki44c+akjg7qZg9oOQeLlk77F6tSYqjDAFClrHJk9tMf0HdVyOvA==} + engines: {node: '>=16.0.0'} + + '@aws-crypto/supports-web-crypto@5.2.0': + resolution: {integrity: sha512-iAvUotm021kM33eCdNfwIN//F77/IADDSs58i+MDaOqFrVjZo9bAal0NK7HurRuWLLpF1iLX7gbWrjHjeo+YFg==} + + '@aws-crypto/util@5.2.0': + resolution: {integrity: sha512-4RkU9EsI6ZpBve5fseQlGNUWKMa1RLPQ1dnjnQoe07ldfIzcsGb5hC5W0Dm7u423KWzawlrpbjXBrXCEv9zazQ==} + + '@aws-sdk/checksums@3.1000.5': + resolution: {integrity: sha512-zOXUUnilC6lgCsQtp77p/QNPmRlTES9Xi6tlDwbR6kfC/kz5PCzZckgHWm5z+8DskdwuMAbFDq61x3zr10GEEQ==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/client-s3@3.1067.0': + resolution: {integrity: sha512-3f64o9YWzwJ9WzMIC4JlUQiMOm7R/EtkIDyFdj8yaQXuh8SR9ezz2R32UMpvTlVMtpoPan3Uj8oveAHr2UeExw==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/core@3.974.20': + resolution: {integrity: sha512-7sDi2B2N3mc3nf1nz6FyEx/FCrJ1N1QnBmraHHQNabFaeAh2IaOOLml48/rHOD1bICHgTRkbBgNTvUzEr5Z35g==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/credential-provider-env@3.972.46': + resolution: {integrity: sha512-+GPXVS2srMOlH74S+SmC1gVuP2TvUZ0siuC0onKO93q+udP+M72dmY8wJfVQ5CX9z/9X5A1HHwz5yRIGBtskvQ==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/credential-provider-http@3.972.48': + resolution: {integrity: sha512-fA5loSdlocacRxyUXtpoHSMuk5rsIKRDzQYVMnMxjcmFeZshaJlJ8lymy/hYKji6sne/UmNGj5pxuEs6kq/Qcg==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/credential-provider-ini@3.972.53': + resolution: {integrity: sha512-ZfdhIOR41q8TcWEnUac+gCOb+O2LBWdHLmjedXpXz4IEFW2ppNuFcm6p0sMTavpM+zD5TYfpH5Gp7guRyqSgsQ==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/credential-provider-login@3.972.52': + resolution: {integrity: sha512-9hu2oR0qH7Fst5Tzdx+UWxm+w5zCXtErTLtOOW5hwwQc170CLwOeniRxyFY6s9mHfGEfC5zFukNBdKBwJR8mhQ==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/credential-provider-node@3.972.55': + resolution: {integrity: sha512-zMGLa/dhESVqmCD7mmIFFKSwSFrJGScvCXcjvBZEVOOMauFS5JRQvLTMukFpMEFWiV6dTAlsen2ATDBulLPtbg==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/credential-provider-process@3.972.46': + resolution: {integrity: sha512-VUoNFBIjWrUN8NbFiQiuxQEgFjvziAlBRPK+ddh27aj65gk0BYu6bLZnrdrNZwpW6vAihtSUtEMQ1PUJ32QRPA==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/credential-provider-sso@3.972.52': + resolution: {integrity: sha512-nb2/n4o/HQf+FVpVbZe9vCTFngmuDoIsltMgLAtjixaKzvzhB4J8WSDFyWgnErgLHk55ctWH+I4PU+LIHhyffg==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/credential-provider-web-identity@3.972.52': + resolution: {integrity: sha512-lKj6aRSGbqLmpYmM24bY7a1Xmfcq2vkE3hv8CSPYfc1yCu0BPu/XEJ1L4Fm61MsU6ULLNSG8UGsffNoFUBjESA==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/middleware-flexible-checksums@3.974.30': + resolution: {integrity: sha512-OaIhub+3yTgfFWPzKO8OzOZFIMUoJaiS5v67y3spQg7SoULGoMx4jKVBbE+uhnzkiZXQ+rEDS0RqrK4/aD1yJw==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/middleware-sdk-s3@3.972.51': + resolution: {integrity: sha512-keQgcIUTcHL0Qn7guhsuLaxQU36r9norCrxgaPH4DNCwon4TPtXdI/UdYuycl9vj3Dlwc3YR1dfL3U+6iIwJ6w==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/nested-clients@3.997.20': + resolution: {integrity: sha512-IYJuLpXp2DEILVQpQOy0PMpkftv0AHEOCn52o0atyOaumA0CdWQ3klPyXdViGYLbNpESsVFMVybvHUeZAuiGxA==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/signature-v4-multi-region@3.996.34': + resolution: {integrity: sha512-mx1L5qlumSOt/nKM3BFaHE2HVkWwz0i4Bw0pyYO42FfX/FeLlo8YI6csC0gSPprEk6fTIqI+CZN9RwUwKd5krQ==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/token-providers@3.1066.0': + resolution: {integrity: sha512-UqEUJq7dqa44hneLDUcX7UJy95cg8YqEWyakRpvIPnrNS3Mq+UlQHgCDGu5pvwAPtlIW4qcYbvW6reG6++FyvA==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/types@3.973.12': + resolution: {integrity: sha512-43ajd1NF0RMgX5k0hxCNUyEdrtFUsb2aHT2QvpktSC/2Eyb2Jr/JPVqdp0XIoaHWikZJq5tNWSLO6kB5q2eMCA==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/util-locate-window@3.965.7': + resolution: {integrity: sha512-M0D6oIpohdNHjc7udzTHEQyot0+0iuA36jc2I9Hps+f/GtKi2HO/pyijQnCnNcwZqLB5+rtn81z3eZK/GyjAmA==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/xml-builder@3.972.29': + resolution: {integrity: sha512-fk0niuGFxfi8yIJuMVM4mhwObkiQSuwZFj3tAPrLVx64Pk3BkrEIpqjzHKY4hKoEBUD6Jg/S74Zj9jy+5F3DnQ==} + engines: {node: '>=20.0.0'} + + '@aws/lambda-invoke-store@0.2.4': + resolution: {integrity: sha512-iY8yvjE0y651BixKNPgmv1WrQc+GZ142sb0z4gYnChDDY2YqI4P/jsSopBWrKfAt7LOJAkOXt7rC/hms+WclQQ==} + engines: {node: '>=18.0.0'} + '@babel/code-frame@7.27.1': resolution: {integrity: sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==} engines: {node: '>=6.9.0'} @@ -1906,6 +2012,9 @@ packages: '@napi-rs/wasm-runtime@1.0.7': resolution: {integrity: sha512-SeDnOO0Tk7Okiq6DbXmmBODgOAb9dp9gjlphokTUxmt8U3liIP1ZsozBahH69j/RJv+Rfs6IwUKHTgQYJ/HBAw==} + '@nodable/entities@2.2.0': + resolution: {integrity: sha512-9uGyhaQavEUMC8AIddIjau4NsnsXhou+j5sBAGojCM1oxmQpVKTWR/9JxABD6UAv12vpIms55fPZKFQEhG6uBg==} + '@nodelib/fs.scandir@2.1.5': resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} engines: {node: '>= 8'} @@ -2368,6 +2477,42 @@ packages: '@sinonjs/fake-timers@13.0.5': resolution: {integrity: sha512-36/hTbH2uaWuGVERyC6da9YwGWnzUZXuPro/F2LfsdOsLnCojz/iSH8MxUt/FD2S5XBSVPhmArFUXcpCQ2Hkiw==} + '@smithy/core@3.24.6': + resolution: {integrity: sha512-wBXDRup6UU97VKyaiRo8AssnfStPtG0oAAfpq/bC0a1YYau8pM86YB4kM6ccoVi1mS8l/UHbn9oDM+7uozr/ug==} + engines: {node: '>=18.0.0'} + + '@smithy/credential-provider-imds@4.3.8': + resolution: {integrity: sha512-5cAM+KZC02sTqDt6NaLXyu50M/GNMd1eTzDVR8Lb0BBsVtu7RWHo47VPPEEv1vt3Yub6uzr+M5FHC+GtoT0USg==} + engines: {node: '>=18.0.0'} + + '@smithy/fetch-http-handler@5.4.6': + resolution: {integrity: sha512-FEwEYJ1jlBKdhe9TPzfghEi1bP55ZeEImlDkEa62bBBYzUcnB6RUCyuiS2mqKt6ZVjUbBgcNhzfIctH+Hevx9g==} + engines: {node: '>=18.0.0'} + + '@smithy/is-array-buffer@2.2.0': + resolution: {integrity: sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA==} + engines: {node: '>=14.0.0'} + + '@smithy/node-http-handler@4.7.7': + resolution: {integrity: sha512-ZAFvHXrEk6K180EVhmZVg8GU5pUH5BSFqRs27JW3j1qEFx9YyYwWFx17x/MHcjALYimGAji7qEOlF1++be+G5A==} + engines: {node: '>=18.0.0'} + + '@smithy/signature-v4@5.4.6': + resolution: {integrity: sha512-Ojg4B6oIDlIr1R86xCDJt1zJWnYa0VINmqdjfe9qxWjdRivHalZ3iSlQgVqYbW0MdpFOC5XfHEWsnbmdnpIILQ==} + engines: {node: '>=18.0.0'} + + '@smithy/types@4.14.3': + resolution: {integrity: sha512-YupL0ZWmFtJexUN2cHzkvvF/b9pKrtAIfT1o7/oY/Ppu8IYeZ+lDPM5vZdQJaSeA132dJCqojjGC9NhXeF71VQ==} + engines: {node: '>=18.0.0'} + + '@smithy/util-buffer-from@2.2.0': + resolution: {integrity: sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA==} + engines: {node: '>=14.0.0'} + + '@smithy/util-utf8@2.3.0': + resolution: {integrity: sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==} + engines: {node: '>=14.0.0'} + '@standard-schema/spec@1.1.0': resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} @@ -3118,6 +3263,9 @@ packages: resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==} engines: {node: '>= 8'} + anynum@1.0.0: + resolution: {integrity: sha512-xjR9/zBVnUOP6ztMIIgShjsxui80nQUQH+5xJnvrYLs+90bF25/KJqaAi8mk+B4RDtX1Nspi6fmp4YTEts8SfA==} + arch@3.0.0: resolution: {integrity: sha512-AmIAC+Wtm2AU8lGfTtHsw0Y9Qtftx2YXEEtiBP10xFUtMOA+sHHx6OAddyL52mUKh1vsXQ6/w1mVDptZCyUt4Q==} @@ -3335,6 +3483,9 @@ packages: boolbase@1.0.0: resolution: {integrity: sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==} + bowser@2.14.1: + resolution: {integrity: sha512-tzPjzCxygAKWFOJP011oxFHs57HzIhOEracIgAePE4pqB3LikALKnSzUyU4MGs9/iCEUuHlAJTjTc5M+u7YEGg==} + brace-expansion@1.1.12: resolution: {integrity: sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==} @@ -4170,6 +4321,13 @@ packages: fast-uri@3.1.0: resolution: {integrity: sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==} + fast-xml-builder@1.2.0: + resolution: {integrity: sha512-00aAWieqff+ZJhsXA4g1g7M8k+7AYoMUUHF+/zFb5U6Uv/P0Vl4QZo84/IcufzYalLuEj9928bXN9PbbFzMF0Q==} + + fast-xml-parser@5.7.3: + resolution: {integrity: sha512-C0AaNuC+mscy6vrAQKAc/rMq+zAPHodfHGZu4sGVehvAQt/JLG1O5zEcYcXSY5zSqr4YVgxsB+pHXTq0i7eDlg==} + hasBin: true + fastq@1.20.1: resolution: {integrity: sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==} @@ -5511,6 +5669,10 @@ packages: resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} engines: {node: '>=8'} + path-expression-matcher@1.5.0: + resolution: {integrity: sha512-cbrerZV+6rvdQrrD+iGMcZFEiiSrbv9Tfdkvnusy6y0x0GKBXREFg/Y65GhIfm0tnLntThhzCnfKwp1WRjeCyQ==} + engines: {node: '>=14.0.0'} + path-is-absolute@1.0.1: resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==} engines: {node: '>=0.10.0'} @@ -6300,6 +6462,9 @@ packages: resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} engines: {node: '>=8'} + strnum@2.4.0: + resolution: {integrity: sha512-sHrVyWWdq28RbhjuJdZsA1SnGRJV6NiXbk6AXBxDOsgAcA+lmpUZCYjOdLBxkXMwis6RRe7dlZt4VlIWFVzkmg==} + strtok3@10.3.4: resolution: {integrity: sha512-KIy5nylvC5le1OdaaoCJ07L+8iQzJHGH6pWDuzS+d07Cu7n1MZ2x26P8ZKIWfbK02+XIL8Mp4RkWeqdUCrDMfg==} engines: {node: '>=18'} @@ -6853,6 +7018,10 @@ packages: resolution: {integrity: sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw==} engines: {node: '>=12'} + xml-naming@0.1.0: + resolution: {integrity: sha512-k8KO9hrMyNk6tUWqUfkTEZbezRRpONVOzUTnc97VnCvyj6Tf9lyUR9EDAIeiVLv56jsMcoXEwjW8Kv5yPY52lw==} + engines: {node: '>=16.0.0'} + xmlchars@2.2.0: resolution: {integrity: sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==} @@ -6894,6 +7063,236 @@ packages: snapshots: + '@aws-crypto/crc32@5.2.0': + dependencies: + '@aws-crypto/util': 5.2.0 + '@aws-sdk/types': 3.973.12 + tslib: 2.8.1 + + '@aws-crypto/crc32c@5.2.0': + dependencies: + '@aws-crypto/util': 5.2.0 + '@aws-sdk/types': 3.973.12 + tslib: 2.8.1 + + '@aws-crypto/sha1-browser@5.2.0': + dependencies: + '@aws-crypto/supports-web-crypto': 5.2.0 + '@aws-crypto/util': 5.2.0 + '@aws-sdk/types': 3.973.12 + '@aws-sdk/util-locate-window': 3.965.7 + '@smithy/util-utf8': 2.3.0 + tslib: 2.8.1 + + '@aws-crypto/sha256-browser@5.2.0': + dependencies: + '@aws-crypto/sha256-js': 5.2.0 + '@aws-crypto/supports-web-crypto': 5.2.0 + '@aws-crypto/util': 5.2.0 + '@aws-sdk/types': 3.973.12 + '@aws-sdk/util-locate-window': 3.965.7 + '@smithy/util-utf8': 2.3.0 + tslib: 2.8.1 + + '@aws-crypto/sha256-js@5.2.0': + dependencies: + '@aws-crypto/util': 5.2.0 + '@aws-sdk/types': 3.973.12 + tslib: 2.8.1 + + '@aws-crypto/supports-web-crypto@5.2.0': + dependencies: + tslib: 2.8.1 + + '@aws-crypto/util@5.2.0': + dependencies: + '@aws-sdk/types': 3.973.12 + '@smithy/util-utf8': 2.3.0 + tslib: 2.8.1 + + '@aws-sdk/checksums@3.1000.5': + dependencies: + '@aws-crypto/crc32': 5.2.0 + '@aws-crypto/crc32c': 5.2.0 + '@aws-crypto/util': 5.2.0 + '@aws-sdk/core': 3.974.20 + '@aws-sdk/types': 3.973.12 + '@smithy/core': 3.24.6 + '@smithy/types': 4.14.3 + tslib: 2.8.1 + + '@aws-sdk/client-s3@3.1067.0': + dependencies: + '@aws-crypto/sha1-browser': 5.2.0 + '@aws-crypto/sha256-browser': 5.2.0 + '@aws-crypto/sha256-js': 5.2.0 + '@aws-sdk/core': 3.974.20 + '@aws-sdk/credential-provider-node': 3.972.55 + '@aws-sdk/middleware-flexible-checksums': 3.974.30 + '@aws-sdk/middleware-sdk-s3': 3.972.51 + '@aws-sdk/signature-v4-multi-region': 3.996.34 + '@aws-sdk/types': 3.973.12 + '@smithy/core': 3.24.6 + '@smithy/fetch-http-handler': 5.4.6 + '@smithy/node-http-handler': 4.7.7 + '@smithy/types': 4.14.3 + tslib: 2.8.1 + + '@aws-sdk/core@3.974.20': + dependencies: + '@aws-sdk/types': 3.973.12 + '@aws-sdk/xml-builder': 3.972.29 + '@aws/lambda-invoke-store': 0.2.4 + '@smithy/core': 3.24.6 + '@smithy/signature-v4': 5.4.6 + '@smithy/types': 4.14.3 + bowser: 2.14.1 + tslib: 2.8.1 + + '@aws-sdk/credential-provider-env@3.972.46': + dependencies: + '@aws-sdk/core': 3.974.20 + '@aws-sdk/types': 3.973.12 + '@smithy/core': 3.24.6 + '@smithy/types': 4.14.3 + tslib: 2.8.1 + + '@aws-sdk/credential-provider-http@3.972.48': + dependencies: + '@aws-sdk/core': 3.974.20 + '@aws-sdk/types': 3.973.12 + '@smithy/core': 3.24.6 + '@smithy/fetch-http-handler': 5.4.6 + '@smithy/node-http-handler': 4.7.7 + '@smithy/types': 4.14.3 + tslib: 2.8.1 + + '@aws-sdk/credential-provider-ini@3.972.53': + dependencies: + '@aws-sdk/core': 3.974.20 + '@aws-sdk/credential-provider-env': 3.972.46 + '@aws-sdk/credential-provider-http': 3.972.48 + '@aws-sdk/credential-provider-login': 3.972.52 + '@aws-sdk/credential-provider-process': 3.972.46 + '@aws-sdk/credential-provider-sso': 3.972.52 + '@aws-sdk/credential-provider-web-identity': 3.972.52 + '@aws-sdk/nested-clients': 3.997.20 + '@aws-sdk/types': 3.973.12 + '@smithy/core': 3.24.6 + '@smithy/credential-provider-imds': 4.3.8 + '@smithy/types': 4.14.3 + tslib: 2.8.1 + + '@aws-sdk/credential-provider-login@3.972.52': + dependencies: + '@aws-sdk/core': 3.974.20 + '@aws-sdk/nested-clients': 3.997.20 + '@aws-sdk/types': 3.973.12 + '@smithy/core': 3.24.6 + '@smithy/types': 4.14.3 + tslib: 2.8.1 + + '@aws-sdk/credential-provider-node@3.972.55': + dependencies: + '@aws-sdk/credential-provider-env': 3.972.46 + '@aws-sdk/credential-provider-http': 3.972.48 + '@aws-sdk/credential-provider-ini': 3.972.53 + '@aws-sdk/credential-provider-process': 3.972.46 + '@aws-sdk/credential-provider-sso': 3.972.52 + '@aws-sdk/credential-provider-web-identity': 3.972.52 + '@aws-sdk/types': 3.973.12 + '@smithy/core': 3.24.6 + '@smithy/credential-provider-imds': 4.3.8 + '@smithy/types': 4.14.3 + tslib: 2.8.1 + + '@aws-sdk/credential-provider-process@3.972.46': + dependencies: + '@aws-sdk/core': 3.974.20 + '@aws-sdk/types': 3.973.12 + '@smithy/core': 3.24.6 + '@smithy/types': 4.14.3 + tslib: 2.8.1 + + '@aws-sdk/credential-provider-sso@3.972.52': + dependencies: + '@aws-sdk/core': 3.974.20 + '@aws-sdk/nested-clients': 3.997.20 + '@aws-sdk/token-providers': 3.1066.0 + '@aws-sdk/types': 3.973.12 + '@smithy/core': 3.24.6 + '@smithy/types': 4.14.3 + tslib: 2.8.1 + + '@aws-sdk/credential-provider-web-identity@3.972.52': + dependencies: + '@aws-sdk/core': 3.974.20 + '@aws-sdk/nested-clients': 3.997.20 + '@aws-sdk/types': 3.973.12 + '@smithy/core': 3.24.6 + '@smithy/types': 4.14.3 + tslib: 2.8.1 + + '@aws-sdk/middleware-flexible-checksums@3.974.30': + dependencies: + '@aws-sdk/checksums': 3.1000.5 + tslib: 2.8.1 + + '@aws-sdk/middleware-sdk-s3@3.972.51': + dependencies: + '@aws-sdk/core': 3.974.20 + '@aws-sdk/signature-v4-multi-region': 3.996.34 + '@aws-sdk/types': 3.973.12 + '@smithy/core': 3.24.6 + '@smithy/types': 4.14.3 + tslib: 2.8.1 + + '@aws-sdk/nested-clients@3.997.20': + dependencies: + '@aws-crypto/sha256-browser': 5.2.0 + '@aws-crypto/sha256-js': 5.2.0 + '@aws-sdk/core': 3.974.20 + '@aws-sdk/signature-v4-multi-region': 3.996.34 + '@aws-sdk/types': 3.973.12 + '@smithy/core': 3.24.6 + '@smithy/fetch-http-handler': 5.4.6 + '@smithy/node-http-handler': 4.7.7 + '@smithy/types': 4.14.3 + tslib: 2.8.1 + + '@aws-sdk/signature-v4-multi-region@3.996.34': + dependencies: + '@aws-sdk/types': 3.973.12 + '@smithy/signature-v4': 5.4.6 + '@smithy/types': 4.14.3 + tslib: 2.8.1 + + '@aws-sdk/token-providers@3.1066.0': + dependencies: + '@aws-sdk/core': 3.974.20 + '@aws-sdk/nested-clients': 3.997.20 + '@aws-sdk/types': 3.973.12 + '@smithy/core': 3.24.6 + '@smithy/types': 4.14.3 + tslib: 2.8.1 + + '@aws-sdk/types@3.973.12': + dependencies: + '@smithy/types': 4.14.3 + tslib: 2.8.1 + + '@aws-sdk/util-locate-window@3.965.7': + dependencies: + tslib: 2.8.1 + + '@aws-sdk/xml-builder@3.972.29': + dependencies: + '@smithy/types': 4.14.3 + fast-xml-parser: 5.7.3 + tslib: 2.8.1 + + '@aws/lambda-invoke-store@0.2.4': {} + '@babel/code-frame@7.27.1': dependencies: '@babel/helper-validator-identifier': 7.28.5 @@ -8877,6 +9276,8 @@ snapshots: '@tybys/wasm-util': 0.10.1 optional: true + '@nodable/entities@2.2.0': {} + '@nodelib/fs.scandir@2.1.5': dependencies: '@nodelib/fs.stat': 2.0.5 @@ -9512,6 +9913,54 @@ snapshots: dependencies: '@sinonjs/commons': 3.0.1 + '@smithy/core@3.24.6': + dependencies: + '@aws-crypto/crc32': 5.2.0 + '@smithy/types': 4.14.3 + tslib: 2.8.1 + + '@smithy/credential-provider-imds@4.3.8': + dependencies: + '@smithy/core': 3.24.6 + '@smithy/types': 4.14.3 + tslib: 2.8.1 + + '@smithy/fetch-http-handler@5.4.6': + dependencies: + '@smithy/core': 3.24.6 + '@smithy/types': 4.14.3 + tslib: 2.8.1 + + '@smithy/is-array-buffer@2.2.0': + dependencies: + tslib: 2.8.1 + + '@smithy/node-http-handler@4.7.7': + dependencies: + '@smithy/core': 3.24.6 + '@smithy/types': 4.14.3 + tslib: 2.8.1 + + '@smithy/signature-v4@5.4.6': + dependencies: + '@smithy/core': 3.24.6 + '@smithy/types': 4.14.3 + tslib: 2.8.1 + + '@smithy/types@4.14.3': + dependencies: + tslib: 2.8.1 + + '@smithy/util-buffer-from@2.2.0': + dependencies: + '@smithy/is-array-buffer': 2.2.0 + tslib: 2.8.1 + + '@smithy/util-utf8@2.3.0': + dependencies: + '@smithy/util-buffer-from': 2.2.0 + tslib: 2.8.1 + '@standard-schema/spec@1.1.0': {} '@svgr/babel-plugin-add-jsx-attribute@8.0.0(@babel/core@7.28.5)': @@ -10361,6 +10810,8 @@ snapshots: normalize-path: 3.0.0 picomatch: 2.3.1 + anynum@1.0.0: {} + arch@3.0.0: {} arg@4.1.3: {} @@ -10645,6 +11096,8 @@ snapshots: boolbase@1.0.0: {} + bowser@2.14.1: {} + brace-expansion@1.1.12: dependencies: balanced-match: 1.0.2 @@ -11373,7 +11826,6 @@ snapshots: '@esbuild/win32-arm64': 0.28.1 '@esbuild/win32-ia32': 0.28.1 '@esbuild/win32-x64': 0.28.1 - optional: true escalade@3.2.0: {} @@ -11679,6 +12131,18 @@ snapshots: fast-uri@3.1.0: {} + fast-xml-builder@1.2.0: + dependencies: + path-expression-matcher: 1.5.0 + xml-naming: 0.1.0 + + fast-xml-parser@5.7.3: + dependencies: + '@nodable/entities': 2.2.0 + fast-xml-builder: 1.2.0 + path-expression-matcher: 1.5.0 + strnum: 2.4.0 + fastq@1.20.1: dependencies: reusify: 1.1.0 @@ -13298,6 +13762,8 @@ snapshots: path-exists@4.0.0: {} + path-expression-matcher@1.5.0: {} + path-is-absolute@1.0.1: {} path-key@3.1.1: {} @@ -14158,6 +14624,10 @@ snapshots: strip-json-comments@3.1.1: {} + strnum@2.4.0: + dependencies: + anynum: 1.0.0 + strtok3@10.3.4: dependencies: '@tokenizer/token': 0.3.0 @@ -14380,7 +14850,6 @@ snapshots: esbuild: 0.28.1 optionalDependencies: fsevents: 2.3.3 - optional: true type-check@0.4.0: dependencies: @@ -14755,6 +15224,8 @@ snapshots: xml-name-validator@4.0.0: {} + xml-naming@0.1.0: {} + xmlchars@2.2.0: {} y18n@5.0.8: {}