diff --git a/packages/sdks/compatibility-layer/README.md b/packages/sdks/compatibility-layer/README.md new file mode 100644 index 00000000..1bd27731 --- /dev/null +++ b/packages/sdks/compatibility-layer/README.md @@ -0,0 +1,354 @@ +# @epcc-sdk/compatibility-layer + +A compatibility layer that enables the new TypeScript SDKs to use the old JS SDK's battle-tested authentication, retry, and throttle logic. + +## Overview + +This package bridges the new `@epcc-sdk/*` TypeScript SDKs with the existing `@elasticpath/js-sdk` (old JS SDK), allowing both to share: + +- **Authentication state** - Single source of truth for access tokens +- **Retry logic** - Exponential backoff with jitter on 401/429 errors +- **Request throttling** - Rate limiting to prevent API overload +- **Token storage** - Shared localStorage/cookie storage keys + +**Zero changes to the old JS SDK are required.** The compatibility layer works entirely by: + +1. Replicating the retry and throttle logic from the old SDK +2. Reading/writing to the same storage keys the old SDK uses +3. Using browser storage events to detect when either SDK updates tokens +4. Injecting a custom fetch function into the new SDKs + +## Installation + +```bash +pnpm add @epcc-sdk/compatibility-layer +``` + +## Quick Start + +### Basic Usage + +```typescript +import { client } from "@epcc-sdk/sdks-shopper" +import { getByContextAllProducts } from "@epcc-sdk/sdks-shopper" +import { createBridgedClient } from "@epcc-sdk/compatibility-layer" + +// Configure the client with compatibility layer +const { client: shopperClient, auth } = createBridgedClient(client, { + baseUrl: "https://api.elasticpath.com", + clientId: "your-client-id", + legacyStorageKey: "epCredentials", // Share with old SDK + retry: { maxAttempts: 4 }, + throttle: { enabled: true }, +}) + +// Use the type-safe SDK operations +const { data } = await getByContextAllProducts({ client: shopperClient }) +``` + +### Sharing Auth with Old SDK + +```typescript +import { gateway } from "@elasticpath/js-sdk" +import { client } from "@epcc-sdk/sdks-shopper" +import { createBridgedClient } from "@epcc-sdk/compatibility-layer" + +// Existing old SDK setup +const legacySdk = gateway({ + client_id: "your-client-id", + host: "api.elasticpath.com", +}) + +// Bridge new SDK to share auth state +const { client: shopperClient, auth } = createBridgedClient(client, { + baseUrl: "https://api.elasticpath.com", + clientId: "your-client-id", + legacyStorageKey: "epCredentials", // Same key old SDK uses +}) + +// Both SDKs now share the same token storage +// When old SDK authenticates, new SDK automatically picks up the token +await legacySdk.Products.All() + +// New SDK uses the shared token +const { data } = await getByContextAllProducts({ client: shopperClient }) +``` + +### Multiple Clients (Commerce Manager Pattern) + +```typescript +import { clientRegistry } from "@epcc-sdk/compatibility-layer" + +// Admin client with client_credentials +const admin = clientRegistry.getOrCreate({ + name: "admin", + authType: "client_credentials", + baseUrl: "https://api.elasticpath.com", + clientId: process.env.ADMIN_CLIENT_ID, + clientSecret: process.env.ADMIN_CLIENT_SECRET, + storage: "memory", // Don't persist admin tokens +}) + +// Shopper client with implicit grant +const shopper = clientRegistry.getOrCreate({ + name: "shopper", + authType: "implicit", + baseUrl: "https://api.elasticpath.com", + clientId: process.env.STOREFRONT_CLIENT_ID, + storage: "localStorage", +}) + +// Password-based client (manual token setting) +const user = clientRegistry.getOrCreate({ + name: "user", + authType: "password", + baseUrl: "https://api.elasticpath.com", + clientId: process.env.CLIENT_ID, + storage: "localStorage", +}) + +// Set token after external authentication +user.auth.setToken({ + access_token: "token-from-login", + refresh_token: "refresh-token", + expires: Math.floor(Date.now() / 1000) + 3600, +}) +``` + +## API Reference + +### `createBridgedClient(client, config)` + +Configures a `@hey-api/client-fetch` client with the compatibility layer. + +```typescript +function createBridgedClient( + client: T, + config: BridgeConfig +): BridgedClient +``` + +#### Parameters + +| Parameter | Type | Description | +|-----------|------|-------------| +| `client` | `HeyApiClient` | The SDK client to configure | +| `config` | `BridgeConfig` | Configuration options | + +#### BridgeConfig + +| Option | Type | Default | Description | +|--------|------|---------|-------------| +| `baseUrl` | `string` | Required | API base URL | +| `clientId` | `string` | Required | OAuth client ID | +| `clientSecret` | `string` | - | Client secret for `client_credentials` | +| `storage` | `StorageAdapter \| 'localStorage' \| 'cookie' \| 'memory'` | `'localStorage'` | Token storage backend | +| `legacyStorageKey` | `string` | `'epCredentials'` | Storage key for sharing with old SDK | +| `tokenProvider` | `TokenProvider` | Auto | Custom token provider function | +| `retry` | `Partial` | See below | Retry configuration | +| `throttle` | `Partial` | See below | Throttle configuration | +| `headers` | `Record` | - | Default headers | +| `leewaySec` | `number` | `60` | Token expiration leeway | + +#### RetryConfig + +| Option | Type | Default | Description | +|--------|------|---------|-------------| +| `maxAttempts` | `number` | `4` | Maximum retry attempts | +| `baseDelay` | `number` | `1000` | Base delay in ms | +| `jitter` | `number` | `500` | Random jitter in ms | +| `reauth` | `boolean` | `true` | Re-authenticate on 401 | + +#### ThrottleConfig + +| Option | Type | Default | Description | +|--------|------|---------|-------------| +| `enabled` | `boolean` | `false` | Enable throttling | +| `limit` | `number` | `3` | Requests per interval | +| `interval` | `number` | `125` | Interval in ms | + +#### Returns + +```typescript +interface BridgedClient { + client: T // Configured SDK client + auth: SharedAuthStateInterface // Auth state for manual management +} +``` + +### `createBridgedFetch(config)` + +Creates a bridged fetch function without configuring a specific client. + +```typescript +function createBridgedFetch(config: BridgeConfig): { + fetch: typeof fetch + auth: SharedAuthState +} +``` + +### `ClientRegistry` + +Manages multiple client instances for different auth contexts. + +```typescript +class ClientRegistry { + getOrCreate(config: ClientInstanceConfig): ClientInstance + get(name: string): ClientInstance | undefined + has(name: string): boolean + remove(name: string): boolean + names(): string[] + clear(): void +} +``` + +#### ClientInstanceConfig + +| Option | Type | Description | +|--------|------|-------------| +| `name` | `string` | Unique client name | +| `authType` | `'implicit' \| 'client_credentials' \| 'password' \| 'sso' \| 'jwt'` | Authentication type | +| `baseUrl` | `string` | API base URL | +| `clientId` | `string` | OAuth client ID | +| `clientSecret` | `string` | Required for `client_credentials` | +| `storage` | `StorageAdapter \| string` | Token storage | +| `legacyStorageKey` | `string` | Storage key for old SDK sharing | + +### `SharedAuthState` + +Manages authentication state with promise deduplication. + +```typescript +class SharedAuthState { + getValidAccessToken(): Promise // Get valid token, refresh if needed + refresh(): Promise // Force token refresh + setToken(tokenData: TokenData): void // Set token manually + clear(): void // Clear credentials + getSnapshot(): string | undefined // Get current token + isExpired(): boolean // Check if token is expired + subscribe(callback: () => void): () => void // Subscribe to changes + getCredentials(): TokenData | undefined // Get full credentials + dispose(): void // Cleanup subscriptions +} +``` + +### `createLegacyStorageBridge(options)` + +Creates a storage adapter that bridges to old SDK's storage location. + +```typescript +function createLegacyStorageBridge(options?: { + key?: string // Custom storage key + name?: string // Gateway name (uses ${name}_ep_credentials) + backend?: 'localStorage' | 'cookie' | 'memory' + cookie?: CookieOptions +}): StorageAdapter +``` + +## How Token Sharing Works + +``` +┌─────────────────┐ ┌─────────────────┐ +│ Old JS SDK │ │ New SDK + │ +│ │ │ Compat Layer │ +└────────┬────────┘ └────────┬────────┘ + │ │ + │ authenticate() │ getValidAccessToken() + │ stores token │ reads token + ▼ ▼ + ┌─────────────────────────────────────────────────┐ + │ localStorage['epCredentials'] │ + │ { access_token, expires, client_id, ... } │ + └─────────────────────────────────────────────────┘ + │ + │ storage event fires + │ when value changes + ▼ + New SDK automatically + reloads credentials +``` + +1. **Shared Storage Key**: Both SDKs read/write to the same localStorage key (`epCredentials`) +2. **Storage Events**: Browser automatically notifies listeners when localStorage changes +3. **Compatible Format**: Tokens are stored in JSON format compatible with old SDK: + ```json + { + "client_id": "your-client-id", + "access_token": "...", + "expires": 1234567890, + "expires_in": 3600, + "token_type": "Bearer" + } + ``` + +## Retry Logic + +The retry logic matches the old SDK's behavior: + +- **Exponential backoff**: `attempt * baseDelay + random(0, jitter)` +- **401 Handling**: Refresh token and retry (when `reauth: true`) +- **429 Handling**: Retry with backoff (rate limiting) +- **Max attempts**: Configurable, default 4 + +```typescript +// Example: Attempt delays with default config (baseDelay=1000, jitter=500) +// Attempt 1: 1000-1500ms +// Attempt 2: 2000-2500ms +// Attempt 3: 3000-3500ms +// Attempt 4: 4000-4500ms (final attempt) +``` + +## SSR Support + +The compatibility layer supports server-side rendering: + +```typescript +// Use memory storage for SSR +const { client, auth } = createBridgedClient(shopperClient, { + baseUrl: process.env.EPCC_ENDPOINT_URL, + clientId: process.env.EPCC_CLIENT_ID, + storage: "memory", // No localStorage on server +}) + +// For Next.js with cookies +import { createLegacyStorageBridge } from "@epcc-sdk/compatibility-layer" + +const storage = createLegacyStorageBridge({ + backend: "cookie", + cookie: { + secure: true, + sameSite: "Lax", + }, +}) +``` + +## Custom Token Providers + +For custom authentication flows (SSO, password with refresh, etc.): + +```typescript +const { client, auth } = createBridgedClient(shopperClient, { + baseUrl: "https://api.elasticpath.com", + clientId: "your-client-id", + tokenProvider: async ({ current }) => { + // Custom logic to get token + const response = await fetch("/api/auth/token", { + method: "POST", + body: JSON.stringify({ currentToken: current }), + }) + return response.json() + }, +}) +``` + +## Testing + +```bash +pnpm test # Run tests +pnpm test:watch # Watch mode +pnpm test:coverage # With coverage +``` + +## License + +MIT diff --git a/packages/sdks/compatibility-layer/package.json b/packages/sdks/compatibility-layer/package.json new file mode 100644 index 00000000..d3719fb9 --- /dev/null +++ b/packages/sdks/compatibility-layer/package.json @@ -0,0 +1,65 @@ +{ + "name": "@epcc-sdk/compatibility-layer", + "version": "0.0.1", + "description": "Compatibility layer enabling new TypeScript SDKs to use old JS SDK auth, retry, and throttle logic", + "type": "module", + "main": "./dist/index.cjs", + "module": "./dist/index.mjs", + "types": "./dist/index.d.ts", + "exports": { + ".": { + "import": { + "types": "./dist/index.d.ts", + "default": "./dist/index.mjs" + }, + "require": { + "types": "./dist/index.d.cts", + "default": "./dist/index.cjs" + } + }, + "./package.json": "./package.json" + }, + "scripts": { + "build": "tsup", + "test": "vitest run", + "test:watch": "vitest", + "test:coverage": "vitest run --coverage", + "test:integration": "vitest run --config vitest.integration.config.ts" + }, + "files": [ + "dist/**" + ], + "keywords": [ + "elasticpath", + "sdk", + "compatibility", + "auth", + "retry" + ], + "devDependencies": { + "@elasticpath/js-sdk": "^5.0.0", + "@epcc-sdk/sdks-shopper": "workspace:*", + "@epcc-sdk/sdks-pxm": "workspace:*", + "esbuild-fix-imports-plugin": "^1.0.19", + "esbuild-plugin-file-path-extensions": "^1.0.0", + "jsdom": "^26.1.0", + "msw": "^2.7.0", + "tsup": "^8.4.0", + "typescript": "^5.5.3", + "vitest": "3.1.4" + }, + "dependencies": { + "throttled-queue": "^2.1.4" + }, + "peerDependencies": { + "@hey-api/client-fetch": ">=0.6.0" + }, + "peerDependenciesMeta": { + "@hey-api/client-fetch": { + "optional": true + } + }, + "publishConfig": { + "access": "public" + } +} diff --git a/packages/sdks/compatibility-layer/pnpm-lock.yaml b/packages/sdks/compatibility-layer/pnpm-lock.yaml new file mode 100644 index 00000000..e11684d8 --- /dev/null +++ b/packages/sdks/compatibility-layer/pnpm-lock.yaml @@ -0,0 +1,2865 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + dependencies: + '@hey-api/client-fetch': + specifier: '>=0.6.0' + version: 0.13.1(@hey-api/openapi-ts@0.90.10(typescript@5.9.3)) + throttled-queue: + specifier: ^2.1.4 + version: 2.1.4 + devDependencies: + '@elasticpath/js-sdk': + specifier: ^5.0.0 + version: 5.0.0 + '@epcc-sdk/sdks-pxm': + specifier: workspace:* + version: link:../pim + '@epcc-sdk/sdks-shopper': + specifier: workspace:* + version: link:../shopper + esbuild-fix-imports-plugin: + specifier: ^1.0.19 + version: 1.0.23 + esbuild-plugin-file-path-extensions: + specifier: ^1.0.0 + version: 1.0.0 + jsdom: + specifier: ^26.1.0 + version: 26.1.0 + msw: + specifier: ^2.7.0 + version: 2.12.7(typescript@5.9.3) + tsup: + specifier: ^8.4.0 + version: 8.5.1(jiti@2.6.1)(postcss@8.5.6)(typescript@5.9.3) + typescript: + specifier: ^5.5.3 + version: 5.9.3 + vitest: + specifier: 3.1.4 + version: 3.1.4(jiti@2.6.1)(jsdom@26.1.0)(msw@2.12.7(typescript@5.9.3)) + +packages: + + '@asamuzakjp/css-color@3.2.0': + resolution: {integrity: sha512-K1A6z8tS3XsmCMM86xoWdn7Fkdn9m6RSVtocUrJYIwZnFVkng/PvkEoWtOWmP+Scc6saYWHWZYbndEEXxl24jw==} + + '@csstools/color-helpers@5.1.0': + resolution: {integrity: sha512-S11EXWJyy0Mz5SYvRmY8nJYTFFd1LCNV+7cXyAgQtOOuzb4EsgfqDufL+9esx72/eLhsRdGZwaldu/h+E4t4BA==} + engines: {node: '>=18'} + + '@csstools/css-calc@2.1.4': + resolution: {integrity: sha512-3N8oaj+0juUw/1H3YwmDDJXCgTB1gKU6Hc/bB502u9zR0q2vd786XJH9QfrKIEgFlZmhZiq6epXl4rHqhzsIgQ==} + engines: {node: '>=18'} + peerDependencies: + '@csstools/css-parser-algorithms': ^3.0.5 + '@csstools/css-tokenizer': ^3.0.4 + + '@csstools/css-color-parser@3.1.0': + resolution: {integrity: sha512-nbtKwh3a6xNVIp/VRuXV64yTKnb1IjTAEEh3irzS+HkKjAOYLTGNb9pmVNntZ8iVBHcWDA2Dof0QtPgFI1BaTA==} + engines: {node: '>=18'} + peerDependencies: + '@csstools/css-parser-algorithms': ^3.0.5 + '@csstools/css-tokenizer': ^3.0.4 + + '@csstools/css-parser-algorithms@3.0.5': + resolution: {integrity: sha512-DaDeUkXZKjdGhgYaHNJTV9pV7Y9B3b644jCLs9Upc3VeNGg6LWARAT6O+Q+/COo+2gg/bM5rhpMAtf70WqfBdQ==} + engines: {node: '>=18'} + peerDependencies: + '@csstools/css-tokenizer': ^3.0.4 + + '@csstools/css-tokenizer@3.0.4': + resolution: {integrity: sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==} + engines: {node: '>=18'} + + '@elasticpath/js-sdk@5.0.0': + resolution: {integrity: sha512-fPtFrolcaQyjHD4NhWyPXV8NMXYWSA3oSlKYvWbbhkSuHsy9mDyDX1EBWmhgKrAXhydxCnngqjJpf4QrxlWSEw==} + + '@esbuild/aix-ppc64@0.25.12': + resolution: {integrity: sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + + '@esbuild/aix-ppc64@0.27.2': + resolution: {integrity: sha512-GZMB+a0mOMZs4MpDbj8RJp4cw+w1WV5NYD6xzgvzUJ5Ek2jerwfO2eADyI6ExDSUED+1X8aMbegahsJi+8mgpw==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + + '@esbuild/android-arm64@0.25.12': + resolution: {integrity: sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm64@0.27.2': + resolution: {integrity: sha512-pvz8ZZ7ot/RBphf8fv60ljmaoydPU12VuXHImtAs0XhLLw+EXBi2BLe3OYSBslR4rryHvweW5gmkKFwTiFy6KA==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm@0.25.12': + resolution: {integrity: sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + + '@esbuild/android-arm@0.27.2': + resolution: {integrity: sha512-DVNI8jlPa7Ujbr1yjU2PfUSRtAUZPG9I1RwW4F4xFB1Imiu2on0ADiI/c3td+KmDtVKNbi+nffGDQMfcIMkwIA==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + + '@esbuild/android-x64@0.25.12': + resolution: {integrity: sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + + '@esbuild/android-x64@0.27.2': + resolution: {integrity: sha512-z8Ank4Byh4TJJOh4wpz8g2vDy75zFL0TlZlkUkEwYXuPSgX8yzep596n6mT7905kA9uHZsf/o2OJZubl2l3M7A==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + + '@esbuild/darwin-arm64@0.25.12': + resolution: {integrity: sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-arm64@0.27.2': + resolution: {integrity: sha512-davCD2Zc80nzDVRwXTcQP/28fiJbcOwvdolL0sOiOsbwBa72kegmVU0Wrh1MYrbuCL98Omp5dVhQFWRKR2ZAlg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-x64@0.25.12': + resolution: {integrity: sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + + '@esbuild/darwin-x64@0.27.2': + resolution: {integrity: sha512-ZxtijOmlQCBWGwbVmwOF/UCzuGIbUkqB1faQRf5akQmxRJ1ujusWsb3CVfk/9iZKr2L5SMU5wPBi1UWbvL+VQA==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + + '@esbuild/freebsd-arm64@0.25.12': + resolution: {integrity: sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-arm64@0.27.2': + resolution: {integrity: sha512-lS/9CN+rgqQ9czogxlMcBMGd+l8Q3Nj1MFQwBZJyoEKI50XGxwuzznYdwcav6lpOGv5BqaZXqvBSiB/kJ5op+g==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.25.12': + resolution: {integrity: sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.27.2': + resolution: {integrity: sha512-tAfqtNYb4YgPnJlEFu4c212HYjQWSO/w/h/lQaBK7RbwGIkBOuNKQI9tqWzx7Wtp7bTPaGC6MJvWI608P3wXYA==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + + '@esbuild/linux-arm64@0.25.12': + resolution: {integrity: sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm64@0.27.2': + resolution: {integrity: sha512-hYxN8pr66NsCCiRFkHUAsxylNOcAQaxSSkHMMjcpx0si13t1LHFphxJZUiGwojB1a/Hd5OiPIqDdXONia6bhTw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm@0.25.12': + resolution: {integrity: sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-arm@0.27.2': + resolution: {integrity: sha512-vWfq4GaIMP9AIe4yj1ZUW18RDhx6EPQKjwe7n8BbIecFtCQG4CfHGaHuh7fdfq+y3LIA2vGS/o9ZBGVxIDi9hw==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-ia32@0.25.12': + resolution: {integrity: sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-ia32@0.27.2': + resolution: {integrity: sha512-MJt5BRRSScPDwG2hLelYhAAKh9imjHK5+NE/tvnRLbIqUWa+0E9N4WNMjmp/kXXPHZGqPLxggwVhz7QP8CTR8w==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-loong64@0.25.12': + resolution: {integrity: sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-loong64@0.27.2': + resolution: {integrity: sha512-lugyF1atnAT463aO6KPshVCJK5NgRnU4yb3FUumyVz+cGvZbontBgzeGFO1nF+dPueHD367a2ZXe1NtUkAjOtg==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-mips64el@0.25.12': + resolution: {integrity: sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-mips64el@0.27.2': + resolution: {integrity: sha512-nlP2I6ArEBewvJ2gjrrkESEZkB5mIoaTswuqNFRv/WYd+ATtUpe9Y09RnJvgvdag7he0OWgEZWhviS1OTOKixw==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-ppc64@0.25.12': + resolution: {integrity: sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-ppc64@0.27.2': + resolution: {integrity: sha512-C92gnpey7tUQONqg1n6dKVbx3vphKtTHJaNG2Ok9lGwbZil6DrfyecMsp9CrmXGQJmZ7iiVXvvZH6Ml5hL6XdQ==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-riscv64@0.25.12': + resolution: {integrity: sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-riscv64@0.27.2': + resolution: {integrity: sha512-B5BOmojNtUyN8AXlK0QJyvjEZkWwy/FKvakkTDCziX95AowLZKR6aCDhG7LeF7uMCXEJqwa8Bejz5LTPYm8AvA==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-s390x@0.25.12': + resolution: {integrity: sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-s390x@0.27.2': + resolution: {integrity: sha512-p4bm9+wsPwup5Z8f4EpfN63qNagQ47Ua2znaqGH6bqLlmJ4bx97Y9JdqxgGZ6Y8xVTixUnEkoKSHcpRlDnNr5w==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-x64@0.25.12': + resolution: {integrity: sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + + '@esbuild/linux-x64@0.27.2': + resolution: {integrity: sha512-uwp2Tip5aPmH+NRUwTcfLb+W32WXjpFejTIOWZFw/v7/KnpCDKG66u4DLcurQpiYTiYwQ9B7KOeMJvLCu/OvbA==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + + '@esbuild/netbsd-arm64@0.25.12': + resolution: {integrity: sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + + '@esbuild/netbsd-arm64@0.27.2': + resolution: {integrity: sha512-Kj6DiBlwXrPsCRDeRvGAUb/LNrBASrfqAIok+xB0LxK8CHqxZ037viF13ugfsIpePH93mX7xfJp97cyDuTZ3cw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.25.12': + resolution: {integrity: sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.27.2': + resolution: {integrity: sha512-HwGDZ0VLVBY3Y+Nw0JexZy9o/nUAWq9MlV7cahpaXKW6TOzfVno3y3/M8Ga8u8Yr7GldLOov27xiCnqRZf0tCA==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-arm64@0.25.12': + resolution: {integrity: sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + + '@esbuild/openbsd-arm64@0.27.2': + resolution: {integrity: sha512-DNIHH2BPQ5551A7oSHD0CKbwIA/Ox7+78/AWkbS5QoRzaqlev2uFayfSxq68EkonB+IKjiuxBFoV8ESJy8bOHA==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.25.12': + resolution: {integrity: sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.27.2': + resolution: {integrity: sha512-/it7w9Nb7+0KFIzjalNJVR5bOzA9Vay+yIPLVHfIQYG/j+j9VTH84aNB8ExGKPU4AzfaEvN9/V4HV+F+vo8OEg==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openharmony-arm64@0.25.12': + resolution: {integrity: sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + + '@esbuild/openharmony-arm64@0.27.2': + resolution: {integrity: sha512-LRBbCmiU51IXfeXk59csuX/aSaToeG7w48nMwA6049Y4J4+VbWALAuXcs+qcD04rHDuSCSRKdmY63sruDS5qag==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + + '@esbuild/sunos-x64@0.25.12': + resolution: {integrity: sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + + '@esbuild/sunos-x64@0.27.2': + resolution: {integrity: sha512-kMtx1yqJHTmqaqHPAzKCAkDaKsffmXkPHThSfRwZGyuqyIeBvf08KSsYXl+abf5HDAPMJIPnbBfXvP2ZC2TfHg==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + + '@esbuild/win32-arm64@0.25.12': + resolution: {integrity: sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-arm64@0.27.2': + resolution: {integrity: sha512-Yaf78O/B3Kkh+nKABUF++bvJv5Ijoy9AN1ww904rOXZFLWVc5OLOfL56W+C8F9xn5JQZa3UX6m+IktJnIb1Jjg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-ia32@0.25.12': + resolution: {integrity: sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-ia32@0.27.2': + resolution: {integrity: sha512-Iuws0kxo4yusk7sw70Xa2E2imZU5HoixzxfGCdxwBdhiDgt9vX9VUCBhqcwY7/uh//78A1hMkkROMJq9l27oLQ==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-x64@0.25.12': + resolution: {integrity: sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + + '@esbuild/win32-x64@0.27.2': + resolution: {integrity: sha512-sRdU18mcKf7F+YgheI/zGf5alZatMUTKj/jNS6l744f9u3WFu4v7twcUI9vu4mknF4Y9aDlblIie0IM+5xxaqQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + + '@hey-api/client-fetch@0.13.1': + resolution: {integrity: sha512-29jBRYNdxVGlx5oewFgOrkulZckpIpBIRHth3uHFn1PrL2ucMy52FvWOY3U3dVx2go1Z3kUmMi6lr07iOpUqqA==} + deprecated: Starting with v0.73.0, this package is bundled directly inside @hey-api/openapi-ts. + peerDependencies: + '@hey-api/openapi-ts': < 2 + + '@hey-api/codegen-core@0.5.5': + resolution: {integrity: sha512-f2ZHucnA2wBGAY8ipB4wn/mrEYW+WUxU2huJmUvfDO6AE2vfILSHeF3wCO39Pz4wUYPoAWZByaauftLrOfC12Q==} + engines: {node: '>=20.19.0'} + peerDependencies: + typescript: '>=5.5.3' + + '@hey-api/json-schema-ref-parser@1.2.2': + resolution: {integrity: sha512-oS+5yAdwnK20lSeFO1d53Ku+yaGCsY8PcrmSq2GtSs3bsBfRnHAbpPKSVzQcaxAOrzj5NB+f34WhZglVrNayBA==} + engines: {node: '>= 16'} + + '@hey-api/openapi-ts@0.90.10': + resolution: {integrity: sha512-o0wlFxuLt1bcyIV/ZH8DQ1wrgODTnUYj/VfCHOOYgXUQlLp9Dm2PjihOz+WYrZLowhqUhSKeJRArOGzvLuOTsg==} + engines: {node: '>=20.19.0'} + hasBin: true + peerDependencies: + typescript: '>=5.5.3' + + '@hey-api/types@0.1.2': + resolution: {integrity: sha512-uNNtiVAWL7XNrV/tFXx7GLY9lwaaDazx1173cGW3+UEaw4RUPsHEmiB4DSpcjNxMIcrctfz2sGKLnVx5PBG2RA==} + + '@inquirer/ansi@1.0.2': + resolution: {integrity: sha512-S8qNSZiYzFd0wAcyG5AXCvUHC5Sr7xpZ9wZ2py9XR88jUz8wooStVx5M6dRzczbBWjic9NP7+rY0Xi7qqK/aMQ==} + engines: {node: '>=18'} + + '@inquirer/confirm@5.1.21': + resolution: {integrity: sha512-KR8edRkIsUayMXV+o3Gv+q4jlhENF9nMYUZs9PA2HzrXeHI8M5uDag70U7RJn9yyiMZSbtF5/UexBtAVtZGSbQ==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/core@10.3.2': + resolution: {integrity: sha512-43RTuEbfP8MbKzedNqBrlhhNKVwoK//vUFNW3Q3vZ88BLcrs4kYpGg+B2mm5p2K/HfygoCxuKwJJiv8PbGmE0A==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/figures@1.0.15': + resolution: {integrity: sha512-t2IEY+unGHOzAaVM5Xx6DEWKeXlDDcNPeDyUpsRc6CUhBfU3VQOEl+Vssh7VNp1dR8MdUJBWhuObjXCsVpjN5g==} + engines: {node: '>=18'} + + '@inquirer/type@3.0.10': + resolution: {integrity: sha512-BvziSRxfz5Ov8ch0z/n3oijRSEcEsHnhggm4xFZe93DHcUCTlutlq9Ox4SVENAfcRD22UQq7T/atg9Wr3k09eA==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@jridgewell/gen-mapping@0.3.13': + resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} + + '@jridgewell/resolve-uri@3.1.2': + resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} + engines: {node: '>=6.0.0'} + + '@jridgewell/sourcemap-codec@1.5.5': + resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} + + '@jridgewell/trace-mapping@0.3.31': + resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} + + '@jsdevtools/ono@7.1.3': + resolution: {integrity: sha512-4JQNk+3mVzK3xh2rqd6RB4J46qUR19azEHBneZyTZM+c456qOrbbM/5xcR8huNCCcbVt7+UmizG6GuUvPvKUYg==} + + '@mswjs/interceptors@0.40.0': + resolution: {integrity: sha512-EFd6cVbHsgLa6wa4RljGj6Wk75qoHxUSyc5asLyyPSyuhIcdS2Q3Phw6ImS1q+CkALthJRShiYfKANcQMuMqsQ==} + engines: {node: '>=18'} + + '@open-draft/deferred-promise@2.2.0': + resolution: {integrity: sha512-CecwLWx3rhxVQF6V4bAgPS5t+So2sTbPgAzafKkVizyi7tlwpcFpdFqq+wqF2OwNBmqFuu6tOyouTuxgpMfzmA==} + + '@open-draft/logger@0.3.0': + resolution: {integrity: sha512-X2g45fzhxH238HKO4xbSr7+wBS8Fvw6ixhTDuvLd5mqh6bJJCFAPwU9mPDxbcrRtfxv4u5IHCEH77BmxvXmmxQ==} + + '@open-draft/until@2.1.0': + resolution: {integrity: sha512-U69T3ItWHvLwGg5eJ0n3I62nWuE6ilHlmz7zM0npLBRvPRd7e6NYmg54vvRtP5mZG7kZqZCFVdsTWo7BPtBujg==} + + '@rollup/rollup-android-arm-eabi@4.57.0': + resolution: {integrity: sha512-tPgXB6cDTndIe1ah7u6amCI1T0SsnlOuKgg10Xh3uizJk4e5M1JGaUMk7J4ciuAUcFpbOiNhm2XIjP9ON0dUqA==} + cpu: [arm] + os: [android] + + '@rollup/rollup-android-arm64@4.57.0': + resolution: {integrity: sha512-sa4LyseLLXr1onr97StkU1Nb7fWcg6niokTwEVNOO7awaKaoRObQ54+V/hrF/BP1noMEaaAW6Fg2d/CfLiq3Mg==} + cpu: [arm64] + os: [android] + + '@rollup/rollup-darwin-arm64@4.57.0': + resolution: {integrity: sha512-/NNIj9A7yLjKdmkx5dC2XQ9DmjIECpGpwHoGmA5E1AhU0fuICSqSWScPhN1yLCkEdkCwJIDu2xIeLPs60MNIVg==} + cpu: [arm64] + os: [darwin] + + '@rollup/rollup-darwin-x64@4.57.0': + resolution: {integrity: sha512-xoh8abqgPrPYPr7pTYipqnUi1V3em56JzE/HgDgitTqZBZ3yKCWI+7KUkceM6tNweyUKYru1UMi7FC060RyKwA==} + cpu: [x64] + os: [darwin] + + '@rollup/rollup-freebsd-arm64@4.57.0': + resolution: {integrity: sha512-PCkMh7fNahWSbA0OTUQ2OpYHpjZZr0hPr8lId8twD7a7SeWrvT3xJVyza+dQwXSSq4yEQTMoXgNOfMCsn8584g==} + cpu: [arm64] + os: [freebsd] + + '@rollup/rollup-freebsd-x64@4.57.0': + resolution: {integrity: sha512-1j3stGx+qbhXql4OCDZhnK7b01s6rBKNybfsX+TNrEe9JNq4DLi1yGiR1xW+nL+FNVvI4D02PUnl6gJ/2y6WJA==} + cpu: [x64] + os: [freebsd] + + '@rollup/rollup-linux-arm-gnueabihf@4.57.0': + resolution: {integrity: sha512-eyrr5W08Ms9uM0mLcKfM/Uzx7hjhz2bcjv8P2uynfj0yU8GGPdz8iYrBPhiLOZqahoAMB8ZiolRZPbbU2MAi6Q==} + cpu: [arm] + os: [linux] + + '@rollup/rollup-linux-arm-musleabihf@4.57.0': + resolution: {integrity: sha512-Xds90ITXJCNyX9pDhqf85MKWUI4lqjiPAipJ8OLp8xqI2Ehk+TCVhF9rvOoN8xTbcafow3QOThkNnrM33uCFQA==} + cpu: [arm] + os: [linux] + + '@rollup/rollup-linux-arm64-gnu@4.57.0': + resolution: {integrity: sha512-Xws2KA4CLvZmXjy46SQaXSejuKPhwVdaNinldoYfqruZBaJHqVo6hnRa8SDo9z7PBW5x84SH64+izmldCgbezw==} + cpu: [arm64] + os: [linux] + + '@rollup/rollup-linux-arm64-musl@4.57.0': + resolution: {integrity: sha512-hrKXKbX5FdaRJj7lTMusmvKbhMJSGWJ+w++4KmjiDhpTgNlhYobMvKfDoIWecy4O60K6yA4SnztGuNTQF+Lplw==} + cpu: [arm64] + os: [linux] + + '@rollup/rollup-linux-loong64-gnu@4.57.0': + resolution: {integrity: sha512-6A+nccfSDGKsPm00d3xKcrsBcbqzCTAukjwWK6rbuAnB2bHaL3r9720HBVZ/no7+FhZLz/U3GwwZZEh6tOSI8Q==} + cpu: [loong64] + os: [linux] + + '@rollup/rollup-linux-loong64-musl@4.57.0': + resolution: {integrity: sha512-4P1VyYUe6XAJtQH1Hh99THxr0GKMMwIXsRNOceLrJnaHTDgk1FTcTimDgneRJPvB3LqDQxUmroBclQ1S0cIJwQ==} + cpu: [loong64] + os: [linux] + + '@rollup/rollup-linux-ppc64-gnu@4.57.0': + resolution: {integrity: sha512-8Vv6pLuIZCMcgXre6c3nOPhE0gjz1+nZP6T+hwWjr7sVH8k0jRkH+XnfjjOTglyMBdSKBPPz54/y1gToSKwrSQ==} + cpu: [ppc64] + os: [linux] + + '@rollup/rollup-linux-ppc64-musl@4.57.0': + resolution: {integrity: sha512-r1te1M0Sm2TBVD/RxBPC6RZVwNqUTwJTA7w+C/IW5v9Ssu6xmxWEi+iJQlpBhtUiT1raJ5b48pI8tBvEjEFnFA==} + cpu: [ppc64] + os: [linux] + + '@rollup/rollup-linux-riscv64-gnu@4.57.0': + resolution: {integrity: sha512-say0uMU/RaPm3CDQLxUUTF2oNWL8ysvHkAjcCzV2znxBr23kFfaxocS9qJm+NdkRhF8wtdEEAJuYcLPhSPbjuQ==} + cpu: [riscv64] + os: [linux] + + '@rollup/rollup-linux-riscv64-musl@4.57.0': + resolution: {integrity: sha512-/MU7/HizQGsnBREtRpcSbSV1zfkoxSTR7wLsRmBPQ8FwUj5sykrP1MyJTvsxP5KBq9SyE6kH8UQQQwa0ASeoQQ==} + cpu: [riscv64] + os: [linux] + + '@rollup/rollup-linux-s390x-gnu@4.57.0': + resolution: {integrity: sha512-Q9eh+gUGILIHEaJf66aF6a414jQbDnn29zeu0eX3dHMuysnhTvsUvZTCAyZ6tJhUjnvzBKE4FtuaYxutxRZpOg==} + cpu: [s390x] + os: [linux] + + '@rollup/rollup-linux-x64-gnu@4.57.0': + resolution: {integrity: sha512-OR5p5yG5OKSxHReWmwvM0P+VTPMwoBS45PXTMYaskKQqybkS3Kmugq1W+YbNWArF8/s7jQScgzXUhArzEQ7x0A==} + cpu: [x64] + os: [linux] + + '@rollup/rollup-linux-x64-musl@4.57.0': + resolution: {integrity: sha512-XeatKzo4lHDsVEbm1XDHZlhYZZSQYym6dg2X/Ko0kSFgio+KXLsxwJQprnR48GvdIKDOpqWqssC3iBCjoMcMpw==} + cpu: [x64] + os: [linux] + + '@rollup/rollup-openbsd-x64@4.57.0': + resolution: {integrity: sha512-Lu71y78F5qOfYmubYLHPcJm74GZLU6UJ4THkf/a1K7Tz2ycwC2VUbsqbJAXaR6Bx70SRdlVrt2+n5l7F0agTUw==} + cpu: [x64] + os: [openbsd] + + '@rollup/rollup-openharmony-arm64@4.57.0': + resolution: {integrity: sha512-v5xwKDWcu7qhAEcsUubiav7r+48Uk/ENWdr82MBZZRIm7zThSxCIVDfb3ZeRRq9yqk+oIzMdDo6fCcA5DHfMyA==} + cpu: [arm64] + os: [openharmony] + + '@rollup/rollup-win32-arm64-msvc@4.57.0': + resolution: {integrity: sha512-XnaaaSMGSI6Wk8F4KK3QP7GfuuhjGchElsVerCplUuxRIzdvZ7hRBpLR0omCmw+kI2RFJB80nenhOoGXlJ5TfQ==} + cpu: [arm64] + os: [win32] + + '@rollup/rollup-win32-ia32-msvc@4.57.0': + resolution: {integrity: sha512-3K1lP+3BXY4t4VihLw5MEg6IZD3ojSYzqzBG571W3kNQe4G4CcFpSUQVgurYgib5d+YaCjeFow8QivWp8vuSvA==} + cpu: [ia32] + os: [win32] + + '@rollup/rollup-win32-x64-gnu@4.57.0': + resolution: {integrity: sha512-MDk610P/vJGc5L5ImE4k5s+GZT3en0KoK1MKPXCRgzmksAMk79j4h3k1IerxTNqwDLxsGxStEZVBqG0gIqZqoA==} + cpu: [x64] + os: [win32] + + '@rollup/rollup-win32-x64-msvc@4.57.0': + resolution: {integrity: sha512-Zv7v6q6aV+VslnpwzqKAmrk5JdVkLUzok2208ZXGipjb+msxBr/fJPZyeEXiFgH7k62Ak0SLIfxQRZQvTuf7rQ==} + cpu: [x64] + os: [win32] + + '@types/estree@1.0.8': + resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} + + '@types/json-schema@7.0.15': + resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} + + '@types/statuses@2.0.6': + resolution: {integrity: sha512-xMAgYwceFhRA2zY+XbEA7mxYbA093wdiW8Vu6gZPGWy9cmOyU9XesH1tNcEWsKFd5Vzrqx5T3D38PWx1FIIXkA==} + + '@vitest/expect@3.1.4': + resolution: {integrity: sha512-xkD/ljeliyaClDYqHPNCiJ0plY5YIcM0OlRiZizLhlPmpXWpxnGMyTZXOHFhFeG7w9P5PBeL4IdtJ/HeQwTbQA==} + + '@vitest/mocker@3.1.4': + resolution: {integrity: sha512-8IJ3CvwtSw/EFXqWFL8aCMu+YyYXG2WUSrQbViOZkWTKTVicVwZ/YiEZDSqD00kX+v/+W+OnxhNWoeVKorHygA==} + peerDependencies: + msw: ^2.4.9 + vite: ^5.0.0 || ^6.0.0 + peerDependenciesMeta: + msw: + optional: true + vite: + optional: true + + '@vitest/pretty-format@3.1.4': + resolution: {integrity: sha512-cqv9H9GvAEoTaoq+cYqUTCGscUjKqlJZC7PRwY5FMySVj5J+xOm1KQcCiYHJOEzOKRUhLH4R2pTwvFlWCEScsg==} + + '@vitest/pretty-format@3.2.4': + resolution: {integrity: sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA==} + + '@vitest/runner@3.1.4': + resolution: {integrity: sha512-djTeF1/vt985I/wpKVFBMWUlk/I7mb5hmD5oP8K9ACRmVXgKTae3TUOtXAEBfslNKPzUQvnKhNd34nnRSYgLNQ==} + + '@vitest/snapshot@3.1.4': + resolution: {integrity: sha512-JPHf68DvuO7vilmvwdPr9TS0SuuIzHvxeaCkxYcCD4jTk67XwL45ZhEHFKIuCm8CYstgI6LZ4XbwD6ANrwMpFg==} + + '@vitest/spy@3.1.4': + resolution: {integrity: sha512-Xg1bXhu+vtPXIodYN369M86K8shGLouNjoVI78g8iAq2rFoHFdajNvJJ5A/9bPMFcfQqdaCpOgWKEoMQg/s0Yg==} + + '@vitest/utils@3.1.4': + resolution: {integrity: sha512-yriMuO1cfFhmiGc8ataN51+9ooHRuURdfAZfwFd3usWynjzpLslZdYnRegTv32qdgtJTsj15FoeZe2g15fY1gg==} + + acorn@8.15.0: + resolution: {integrity: sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==} + engines: {node: '>=0.4.0'} + hasBin: true + + agent-base@7.1.4: + resolution: {integrity: sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==} + engines: {node: '>= 14'} + + ansi-colors@4.1.3: + resolution: {integrity: sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==} + engines: {node: '>=6'} + + ansi-regex@5.0.1: + resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} + engines: {node: '>=8'} + + ansi-styles@4.3.0: + resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} + engines: {node: '>=8'} + + any-promise@1.3.0: + resolution: {integrity: sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==} + + argparse@2.0.1: + resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + + assertion-error@2.0.1: + resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} + engines: {node: '>=12'} + + asynckit@0.4.0: + resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} + + bundle-name@4.1.0: + resolution: {integrity: sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==} + engines: {node: '>=18'} + + bundle-require@5.1.0: + resolution: {integrity: sha512-3WrrOuZiyaaZPWiEt4G3+IffISVC9HYlWueJEBWED4ZH4aIAC2PnkdnuRrR94M+w6yGWn4AglWtJtBI8YqvgoA==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + peerDependencies: + esbuild: '>=0.18' + + c12@3.3.3: + resolution: {integrity: sha512-750hTRvgBy5kcMNPdh95Qo+XUBeGo8C7nsKSmedDmaQI+E0r82DwHeM6vBewDe4rGFbnxoa4V9pw+sPh5+Iz8Q==} + peerDependencies: + magicast: '*' + peerDependenciesMeta: + magicast: + optional: true + + cac@6.7.14: + resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} + engines: {node: '>=8'} + + call-bind-apply-helpers@1.0.2: + resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} + engines: {node: '>= 0.4'} + + chai@5.3.3: + resolution: {integrity: sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==} + engines: {node: '>=18'} + + check-error@2.1.3: + resolution: {integrity: sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==} + engines: {node: '>= 16'} + + chokidar@4.0.3: + resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==} + engines: {node: '>= 14.16.0'} + + chokidar@5.0.0: + resolution: {integrity: sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==} + engines: {node: '>= 20.19.0'} + + citty@0.1.6: + resolution: {integrity: sha512-tskPPKEs8D2KPafUypv2gxwJP8h/OaJmC82QQGGDQcHvXX43xF2VDACcJVmZ0EuSxkpO9Kc4MlrA3q0+FG58AQ==} + + citty@0.2.0: + resolution: {integrity: sha512-8csy5IBFI2ex2hTVpaHN2j+LNE199AgiI7y4dMintrr8i0lQiFn+0AWMZrWdHKIgMOer65f8IThysYhoReqjWA==} + + cli-width@4.1.0: + resolution: {integrity: sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==} + engines: {node: '>= 12'} + + cliui@8.0.1: + resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==} + engines: {node: '>=12'} + + color-convert@2.0.1: + resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} + engines: {node: '>=7.0.0'} + + color-name@1.1.4: + resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + + color-support@1.1.3: + resolution: {integrity: sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==} + hasBin: true + + combined-stream@1.0.8: + resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} + engines: {node: '>= 0.8'} + + commander@14.0.2: + resolution: {integrity: sha512-TywoWNNRbhoD0BXs1P3ZEScW8W5iKrnbithIl0YH+uCmBd0QpPOA8yc82DS3BIE5Ma6FnBVUsJ7wVUDz4dvOWQ==} + engines: {node: '>=20'} + + commander@4.1.1: + resolution: {integrity: sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==} + engines: {node: '>= 6'} + + confbox@0.1.8: + resolution: {integrity: sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==} + + confbox@0.2.2: + resolution: {integrity: sha512-1NB+BKqhtNipMsov4xI/NnhCKp9XG9NamYp5PVm9klAT0fsrNPjaFICsCFhNhwZJKNh7zB/3q8qXz0E9oaMNtQ==} + + consola@3.4.2: + resolution: {integrity: sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==} + engines: {node: ^14.18.0 || >=16.10.0} + + cookie@1.1.1: + resolution: {integrity: sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==} + engines: {node: '>=18'} + + cross-fetch@3.2.0: + resolution: {integrity: sha512-Q+xVJLoGOeIMXZmbUK4HYk+69cQH6LudR0Vu/pRm2YlU/hDV9CiS0gKUMaWY5f2NeUH9C1nV3bsTlCo0FsTV1Q==} + + cssstyle@4.6.0: + resolution: {integrity: sha512-2z+rWdzbbSZv6/rhtvzvqeZQHrBaqgogqt85sqFNbabZOuFbCVFb8kPeEtZjiKkbrm395irpNKiYeFeLiQnFPg==} + engines: {node: '>=18'} + + data-urls@5.0.0: + resolution: {integrity: sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==} + engines: {node: '>=18'} + + debug@4.4.3: + resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + decimal.js@10.6.0: + resolution: {integrity: sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==} + + deep-eql@5.0.2: + resolution: {integrity: sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==} + engines: {node: '>=6'} + + default-browser-id@5.0.1: + resolution: {integrity: sha512-x1VCxdX4t+8wVfd1so/9w+vQ4vx7lKd2Qp5tDRutErwmR85OgmfX7RlLRMWafRMY7hbEiXIbudNrjOAPa/hL8Q==} + engines: {node: '>=18'} + + default-browser@5.4.0: + resolution: {integrity: sha512-XDuvSq38Hr1MdN47EDvYtx3U0MTqpCEn+F6ft8z2vYDzMrvQhVp0ui9oQdqW3MvK3vqUETglt1tVGgjLuJ5izg==} + engines: {node: '>=18'} + + define-lazy-prop@3.0.0: + resolution: {integrity: sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==} + engines: {node: '>=12'} + + defu@6.1.4: + resolution: {integrity: sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==} + + delayed-stream@1.0.0: + resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} + engines: {node: '>=0.4.0'} + + destr@2.0.5: + resolution: {integrity: sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA==} + + dotenv@17.2.3: + resolution: {integrity: sha512-JVUnt+DUIzu87TABbhPmNfVdBDt18BLOWjMUFJMSi/Qqg7NTYtabbvSNJGOJ7afbRuv9D/lngizHtP7QyLQ+9w==} + engines: {node: '>=12'} + + dunder-proto@1.0.1: + resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} + engines: {node: '>= 0.4'} + + emoji-regex@8.0.0: + resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} + + entities@6.0.1: + resolution: {integrity: sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==} + engines: {node: '>=0.12'} + + es-define-property@1.0.1: + resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} + engines: {node: '>= 0.4'} + + es-errors@1.3.0: + resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} + engines: {node: '>= 0.4'} + + es-module-lexer@1.7.0: + resolution: {integrity: sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==} + + es-object-atoms@1.1.1: + resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==} + engines: {node: '>= 0.4'} + + es-set-tostringtag@2.1.0: + resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==} + engines: {node: '>= 0.4'} + + es6-promise@4.2.8: + resolution: {integrity: sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w==} + + esbuild-fix-imports-plugin@1.0.23: + resolution: {integrity: sha512-zDn2Mq3OnW9qNm9FrHDeE0FePgIRIaH9EWpHOGsJZFPfyPSNqzvCK/x9EZJ5eHEEyXe8ZGdb5CjDpzqkyAqcCg==} + + esbuild-plugin-file-path-extensions@1.0.0: + resolution: {integrity: sha512-v5LpSkml+CbsC0+xAaETEGDECdvKp1wKkD4aXMdI4zLjXP0EYfK4GjGhphumt4N+kjR3A8Q+DIkpgxX1XTqO4Q==} + engines: {node: '>=v14.0.0', npm: '>=7.0.0'} + + esbuild@0.25.12: + resolution: {integrity: sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==} + engines: {node: '>=18'} + hasBin: true + + esbuild@0.27.2: + resolution: {integrity: sha512-HyNQImnsOC7X9PMNaCIeAm4ISCQXs5a5YasTXVliKv4uuBo1dKrG0A+uQS8M5eXjVMnLg3WgXaKvprHlFJQffw==} + engines: {node: '>=18'} + hasBin: true + + escalade@3.2.0: + resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} + engines: {node: '>=6'} + + estree-walker@3.0.3: + resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} + + expect-type@1.3.0: + resolution: {integrity: sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==} + engines: {node: '>=12.0.0'} + + exsolve@1.0.8: + resolution: {integrity: sha512-LmDxfWXwcTArk8fUEnOfSZpHOJ6zOMUJKOtFLFqJLoKJetuQG874Uc7/Kki7zFLzYybmZhp1M7+98pfMqeX8yA==} + + fdir@6.5.0: + resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} + engines: {node: '>=12.0.0'} + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + + fix-dts-default-cjs-exports@1.0.1: + resolution: {integrity: sha512-pVIECanWFC61Hzl2+oOCtoJ3F17kglZC/6N94eRWycFgBH35hHx0Li604ZIzhseh97mf2p0cv7vVrOZGoqhlEg==} + + form-data@4.0.5: + resolution: {integrity: sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==} + engines: {node: '>= 6'} + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + function-bind@1.1.2: + resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + + get-caller-file@2.0.5: + resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} + engines: {node: 6.* || 8.* || >= 10.*} + + get-intrinsic@1.3.0: + resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} + engines: {node: '>= 0.4'} + + get-proto@1.0.1: + resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} + engines: {node: '>= 0.4'} + + giget@2.0.0: + resolution: {integrity: sha512-L5bGsVkxJbJgdnwyuheIunkGatUF/zssUoxxjACCseZYAVbaqdh9Tsmmlkl8vYan09H7sbvKt4pS8GqKLBrEzA==} + hasBin: true + + gopd@1.2.0: + resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} + engines: {node: '>= 0.4'} + + graceful-fs@4.2.11: + resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + + graphql@16.12.0: + resolution: {integrity: sha512-DKKrynuQRne0PNpEbzuEdHlYOMksHSUI8Zc9Unei5gTsMNA2/vMpoMz/yKba50pejK56qj98qM0SjYxAKi13gQ==} + engines: {node: ^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0} + + has-symbols@1.1.0: + resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} + engines: {node: '>= 0.4'} + + has-tostringtag@1.0.2: + resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==} + engines: {node: '>= 0.4'} + + hasown@2.0.2: + resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} + engines: {node: '>= 0.4'} + + headers-polyfill@4.0.3: + resolution: {integrity: sha512-IScLbePpkvO846sIwOtOTDjutRMWdXdJmXdMvk6gCBHxFO8d+QKOQedyZSxFTTFYRSmlgSTDtXqqq4pcenBXLQ==} + + html-encoding-sniffer@4.0.0: + resolution: {integrity: sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==} + engines: {node: '>=18'} + + http-proxy-agent@7.0.2: + resolution: {integrity: sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==} + engines: {node: '>= 14'} + + https-proxy-agent@7.0.6: + resolution: {integrity: sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==} + engines: {node: '>= 14'} + + iconv-lite@0.6.3: + resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} + engines: {node: '>=0.10.0'} + + imurmurhash@0.1.4: + resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} + engines: {node: '>=0.8.19'} + + inflected@2.1.0: + resolution: {integrity: sha512-hAEKNxvHf2Iq3H60oMBHkB4wl5jn3TPF3+fXek/sRwAB5gP9xWs4r7aweSF95f99HFoz69pnZTcu8f0SIHV18w==} + + is-docker@3.0.0: + resolution: {integrity: sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + hasBin: true + + is-fullwidth-code-point@3.0.0: + resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} + engines: {node: '>=8'} + + is-in-ssh@1.0.0: + resolution: {integrity: sha512-jYa6Q9rH90kR1vKB6NM7qqd1mge3Fx4Dhw5TVlK1MUBqhEOuCagrEHMevNuCcbECmXZ0ThXkRm+Ymr51HwEPAw==} + engines: {node: '>=20'} + + is-inside-container@1.0.0: + resolution: {integrity: sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==} + engines: {node: '>=14.16'} + hasBin: true + + is-node-process@1.2.0: + resolution: {integrity: sha512-Vg4o6/fqPxIjtxgUH5QLJhwZ7gW5diGCVlXpuUfELC62CuxM1iHcRe51f2W1FDy04Ai4KJkagKjx3XaqyfRKXw==} + + is-potential-custom-element-name@1.0.1: + resolution: {integrity: sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==} + + is-wsl@3.1.0: + resolution: {integrity: sha512-UcVfVfaK4Sc4m7X3dUSoHoozQGBEFeDC+zVo06t98xe8CzHSZZBekNXH+tu0NalHolcJ/QAGqS46Hef7QXBIMw==} + engines: {node: '>=16'} + + jiti@2.6.1: + resolution: {integrity: sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==} + hasBin: true + + joycon@3.1.1: + resolution: {integrity: sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==} + engines: {node: '>=10'} + + js-cookie@3.0.5: + resolution: {integrity: sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw==} + engines: {node: '>=14'} + + js-yaml@4.1.1: + resolution: {integrity: sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==} + hasBin: true + + jsdom@26.1.0: + resolution: {integrity: sha512-Cvc9WUhxSMEo4McES3P7oK3QaXldCfNWp7pl2NNeiIFlCoLr3kfq9kb1fxftiwk1FLV7CvpvDfonxtzUDeSOPg==} + engines: {node: '>=18'} + peerDependencies: + canvas: ^3.0.0 + peerDependenciesMeta: + canvas: + optional: true + + lilconfig@3.1.3: + resolution: {integrity: sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==} + engines: {node: '>=14'} + + lines-and-columns@1.2.4: + resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} + + load-tsconfig@0.2.5: + resolution: {integrity: sha512-IXO6OCs9yg8tMKzfPZ1YmheJbZCiEsnBdcB03l0OcfK9prKnJb96siuHCr5Fl37/yo9DnKU+TLpxzTUspw9shg==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + lodash@4.17.23: + resolution: {integrity: sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==} + + loupe@3.2.1: + resolution: {integrity: sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==} + + lru-cache@10.4.3: + resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} + + magic-string@0.30.21: + resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + + math-intrinsics@1.1.0: + resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} + engines: {node: '>= 0.4'} + + mime-db@1.52.0: + resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} + engines: {node: '>= 0.6'} + + mime-types@2.1.35: + resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} + engines: {node: '>= 0.6'} + + mlly@1.8.0: + resolution: {integrity: sha512-l8D9ODSRWLe2KHJSifWGwBqpTZXIXTeo8mlKjY+E2HAakaTeNpqAyBZ8GSqLzHgw4XmHmC8whvpjJNMbFZN7/g==} + + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + + msw@2.12.7: + resolution: {integrity: sha512-retd5i3xCZDVWMYjHEVuKTmhqY8lSsxujjVrZiGbbdoxxIBg5S7rCuYy/YQpfrTYIxpd/o0Kyb/3H+1udBMoYg==} + engines: {node: '>=18'} + hasBin: true + peerDependencies: + typescript: '>= 4.8.x' + peerDependenciesMeta: + typescript: + optional: true + + mute-stream@2.0.0: + resolution: {integrity: sha512-WWdIxpyjEn+FhQJQQv9aQAYlHoNVdzIzUySNV1gHUPDSdZJ3yZn7pAAbQcV7B56Mvu881q9FZV+0Vx2xC44VWA==} + engines: {node: ^18.17.0 || >=20.5.0} + + mz@2.7.0: + resolution: {integrity: sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==} + + nanoid@3.3.11: + resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + + node-fetch-native@1.6.7: + resolution: {integrity: sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q==} + + node-fetch@2.7.0: + resolution: {integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==} + engines: {node: 4.x || >=6.0.0} + peerDependencies: + encoding: ^0.1.0 + peerDependenciesMeta: + encoding: + optional: true + + node-localstorage@2.2.1: + resolution: {integrity: sha512-vv8fJuOUCCvSPjDjBLlMqYMHob4aGjkmrkaE42/mZr0VT+ZAU10jRF8oTnX9+pgU9/vYJ8P7YT3Vd6ajkmzSCw==} + engines: {node: '>=0.12'} + + nwsapi@2.2.23: + resolution: {integrity: sha512-7wfH4sLbt4M0gCDzGE6vzQBo0bfTKjU7Sfpqy/7gs1qBfYz2vEJH6vXcBKpO3+6Yu1telwd0t9HpyOoLEQQbIQ==} + + nypm@0.6.4: + resolution: {integrity: sha512-1TvCKjZyyklN+JJj2TS3P4uSQEInrM/HkkuSXsEzm1ApPgBffOn8gFguNnZf07r/1X6vlryfIqMUkJKQMzlZiw==} + engines: {node: '>=18'} + hasBin: true + + object-assign@4.1.1: + resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} + engines: {node: '>=0.10.0'} + + ohash@2.0.11: + resolution: {integrity: sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ==} + + open@11.0.0: + resolution: {integrity: sha512-smsWv2LzFjP03xmvFoJ331ss6h+jixfA4UUV/Bsiyuu4YJPfN+FIQGOIiv4w9/+MoHkfkJ22UIaQWRVFRfH6Vw==} + engines: {node: '>=20'} + + outvariant@1.4.3: + resolution: {integrity: sha512-+Sl2UErvtsoajRDKCE5/dBz4DIvHXQQnAxtQTF04OJxY0+DyZXSo5P5Bb7XYWOh81syohlYL24hbDwxedPUJCA==} + + parse5@7.3.0: + resolution: {integrity: sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==} + + path-to-regexp@6.3.0: + resolution: {integrity: sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==} + + pathe@2.0.3: + resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} + + pathval@2.0.1: + resolution: {integrity: sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==} + engines: {node: '>= 14.16'} + + perfect-debounce@2.1.0: + resolution: {integrity: sha512-LjgdTytVFXeUgtHZr9WYViYSM/g8MkcTPYDlPa3cDqMirHjKiSZPYd6DoL7pK8AJQr+uWkQvCjHNdiMqsrJs+g==} + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + picomatch@4.0.3: + resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==} + engines: {node: '>=12'} + + pirates@4.0.7: + resolution: {integrity: sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==} + engines: {node: '>= 6'} + + pkg-types@1.3.1: + resolution: {integrity: sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==} + + pkg-types@2.3.0: + resolution: {integrity: sha512-SIqCzDRg0s9npO5XQ3tNZioRY1uK06lA41ynBC1YmFTmnY6FjUjVt6s4LoADmwoig1qqD0oK8h1p/8mlMx8Oig==} + + postcss-load-config@6.0.1: + resolution: {integrity: sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==} + engines: {node: '>= 18'} + peerDependencies: + jiti: '>=1.21.0' + postcss: '>=8.0.9' + tsx: ^4.8.1 + yaml: ^2.4.2 + peerDependenciesMeta: + jiti: + optional: true + postcss: + optional: true + tsx: + optional: true + yaml: + optional: true + + postcss@8.5.6: + resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==} + engines: {node: ^10 || ^12 || >=14} + + powershell-utils@0.1.0: + resolution: {integrity: sha512-dM0jVuXJPsDN6DvRpea484tCUaMiXWjuCn++HGTqUWzGDjv5tZkEZldAJ/UMlqRYGFrD/etByo4/xOuC/snX2A==} + engines: {node: '>=20'} + + punycode@2.3.1: + resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} + engines: {node: '>=6'} + + rc9@2.1.2: + resolution: {integrity: sha512-btXCnMmRIBINM2LDZoEmOogIZU7Qe7zn4BpomSKZ/ykbLObuBdvG+mFq11DL6fjH1DRwHhrlgtYWG96bJiC7Cg==} + + readdirp@4.1.2: + resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==} + engines: {node: '>= 14.18.0'} + + readdirp@5.0.0: + resolution: {integrity: sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ==} + engines: {node: '>= 20.19.0'} + + require-directory@2.1.1: + resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} + engines: {node: '>=0.10.0'} + + resolve-from@5.0.0: + resolution: {integrity: sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==} + engines: {node: '>=8'} + + rettime@0.7.0: + resolution: {integrity: sha512-LPRKoHnLKd/r3dVxcwO7vhCW+orkOGj9ViueosEBK6ie89CijnfRlhaDhHq/3Hxu4CkWQtxwlBG0mzTQY6uQjw==} + + rollup@4.57.0: + resolution: {integrity: sha512-e5lPJi/aui4TO1LpAXIRLySmwXSE8k3b9zoGfd42p67wzxog4WHjiZF3M2uheQih4DGyc25QEV4yRBbpueNiUA==} + engines: {node: '>=18.0.0', npm: '>=8.0.0'} + hasBin: true + + rrweb-cssom@0.8.0: + resolution: {integrity: sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw==} + + run-applescript@7.1.0: + resolution: {integrity: sha512-DPe5pVFaAsinSaV6QjQ6gdiedWDcRCbUuiQfQa2wmWV7+xC9bGulGI8+TdRmoFkAPaBXk8CrAbnlY2ISniJ47Q==} + engines: {node: '>=18'} + + safer-buffer@2.1.2: + resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + + saxes@6.0.0: + resolution: {integrity: sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==} + engines: {node: '>=v12.22.7'} + + semver@7.7.3: + resolution: {integrity: sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==} + engines: {node: '>=10'} + hasBin: true + + siginfo@2.0.0: + resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} + + signal-exit@4.1.0: + resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} + engines: {node: '>=14'} + + slide@1.1.6: + resolution: {integrity: sha512-NwrtjCg+lZoqhFU8fOwl4ay2ei8PaqCBOUV3/ektPY9trO1yQ1oXEfmHAhKArUVUr/hOHvy5f6AdP17dCM0zMw==} + + source-map-js@1.2.1: + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + engines: {node: '>=0.10.0'} + + source-map@0.7.6: + resolution: {integrity: sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==} + engines: {node: '>= 12'} + + stackback@0.0.2: + resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} + + statuses@2.0.2: + resolution: {integrity: sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==} + engines: {node: '>= 0.8'} + + std-env@3.10.0: + resolution: {integrity: sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==} + + strict-event-emitter@0.5.1: + resolution: {integrity: sha512-vMgjE/GGEPEFnhFub6pa4FmJBRBVOLpIII2hvCZ8Kzb7K0hlHo7mQv6xYrBvCL2LtAIBwFUK8wvuJgTVSQ5MFQ==} + + string-width@4.2.3: + resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} + engines: {node: '>=8'} + + strip-ansi@6.0.1: + resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} + engines: {node: '>=8'} + + sucrase@3.35.1: + resolution: {integrity: sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==} + engines: {node: '>=16 || 14 >=14.17'} + hasBin: true + + symbol-tree@3.2.4: + resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==} + + tagged-tag@1.0.0: + resolution: {integrity: sha512-yEFYrVhod+hdNyx7g5Bnkkb0G6si8HJurOoOEgC8B/O0uXLHlaey/65KRv6cuWBNhBgHKAROVpc7QyYqE5gFng==} + engines: {node: '>=20'} + + thenify-all@1.6.0: + resolution: {integrity: sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==} + engines: {node: '>=0.8'} + + thenify@3.3.1: + resolution: {integrity: sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==} + + throttled-queue@2.1.4: + resolution: {integrity: sha512-YGdk8sdmr4ge3g+doFj/7RLF5kLM+Mi7DEciu9PHxnMJZMeVuZeTj31g4VE7ekUffx/IdbvrtOCiz62afg0mkg==} + + tinybench@2.9.0: + resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} + + tinyexec@0.3.2: + resolution: {integrity: sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==} + + tinyexec@1.0.2: + resolution: {integrity: sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg==} + engines: {node: '>=18'} + + tinyglobby@0.2.15: + resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==} + engines: {node: '>=12.0.0'} + + tinypool@1.1.1: + resolution: {integrity: sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==} + engines: {node: ^18.0.0 || >=20.0.0} + + tinyrainbow@2.0.0: + resolution: {integrity: sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==} + engines: {node: '>=14.0.0'} + + tinyspy@3.0.2: + resolution: {integrity: sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q==} + engines: {node: '>=14.0.0'} + + tldts-core@6.1.86: + resolution: {integrity: sha512-Je6p7pkk+KMzMv2XXKmAE3McmolOQFdxkKw0R8EYNr7sELW46JqnNeTX8ybPiQgvg1ymCoF8LXs5fzFaZvJPTA==} + + tldts-core@7.0.19: + resolution: {integrity: sha512-lJX2dEWx0SGH4O6p+7FPwYmJ/bu1JbcGJ8RLaG9b7liIgZ85itUVEPbMtWRVrde/0fnDPEPHW10ZsKW3kVsE9A==} + + tldts@6.1.86: + resolution: {integrity: sha512-WMi/OQ2axVTf/ykqCQgXiIct+mSQDFdH2fkwhPwgEwvJ1kSzZRiinb0zF2Xb8u4+OqPChmyI6MEu4EezNJz+FQ==} + hasBin: true + + tldts@7.0.19: + resolution: {integrity: sha512-8PWx8tvC4jDB39BQw1m4x8y5MH1BcQ5xHeL2n7UVFulMPH/3Q0uiamahFJ3lXA0zO2SUyRXuVVbWSDmstlt9YA==} + hasBin: true + + tough-cookie@5.1.2: + resolution: {integrity: sha512-FVDYdxtnj0G6Qm/DhNPSb8Ju59ULcup3tuJxkFb5K8Bv2pUXILbf0xZWU8PX8Ov19OXljbUyveOFwRMwkXzO+A==} + engines: {node: '>=16'} + + tough-cookie@6.0.0: + resolution: {integrity: sha512-kXuRi1mtaKMrsLUxz3sQYvVl37B0Ns6MzfrtV5DvJceE9bPyspOqk9xxv7XbZWcfLWbFmm997vl83qUWVJA64w==} + engines: {node: '>=16'} + + tr46@0.0.3: + resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==} + + tr46@5.1.1: + resolution: {integrity: sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==} + engines: {node: '>=18'} + + tree-kill@1.2.2: + resolution: {integrity: sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==} + hasBin: true + + ts-interface-checker@0.1.13: + resolution: {integrity: sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==} + + tsup@8.5.1: + resolution: {integrity: sha512-xtgkqwdhpKWr3tKPmCkvYmS9xnQK3m3XgxZHwSUjvfTjp7YfXe5tT3GgWi0F2N+ZSMsOeWeZFh7ZZFg5iPhing==} + engines: {node: '>=18'} + hasBin: true + peerDependencies: + '@microsoft/api-extractor': ^7.36.0 + '@swc/core': ^1 + postcss: ^8.4.12 + typescript: '>=4.5.0' + peerDependenciesMeta: + '@microsoft/api-extractor': + optional: true + '@swc/core': + optional: true + postcss: + optional: true + typescript: + optional: true + + type-fest@5.4.2: + resolution: {integrity: sha512-FLEenlVYf7Zcd34ISMLo3ZzRE1gRjY1nMDTp+bQRBiPsaKyIW8K3Zr99ioHDUgA9OGuGGJPyYpNcffGmBhJfGg==} + engines: {node: '>=20'} + + typescript@5.9.3: + resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} + engines: {node: '>=14.17'} + hasBin: true + + ufo@1.6.3: + resolution: {integrity: sha512-yDJTmhydvl5lJzBmy/hyOAA0d+aqCBuwl818haVdYCRrWV84o7YyeVm4QlVHStqNrrJSTb6jKuFAVqAFsr+K3Q==} + + until-async@3.0.2: + resolution: {integrity: sha512-IiSk4HlzAMqTUseHHe3VhIGyuFmN90zMTpD3Z3y8jeQbzLIq500MVM7Jq2vUAnTKAFPJrqwkzr6PoTcPhGcOiw==} + + vite-node@3.1.4: + resolution: {integrity: sha512-6enNwYnpyDo4hEgytbmc6mYWHXDHYEn0D1/rw4Q+tnHUGtKTJsn8T1YkX6Q18wI5LCrS8CTYlBaiCqxOy2kvUA==} + engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} + hasBin: true + + vite@6.4.1: + resolution: {integrity: sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==} + engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} + hasBin: true + peerDependencies: + '@types/node': ^18.0.0 || ^20.0.0 || >=22.0.0 + jiti: '>=1.21.0' + less: '*' + lightningcss: ^1.21.0 + sass: '*' + sass-embedded: '*' + stylus: '*' + sugarss: '*' + terser: ^5.16.0 + tsx: ^4.8.1 + yaml: ^2.4.2 + peerDependenciesMeta: + '@types/node': + optional: true + jiti: + optional: true + less: + optional: true + lightningcss: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + tsx: + optional: true + yaml: + optional: true + + vitest@3.1.4: + resolution: {integrity: sha512-Ta56rT7uWxCSJXlBtKgIlApJnT6e6IGmTYxYcmxjJ4ujuZDI59GUQgVDObXXJujOmPDBYXHK1qmaGtneu6TNIQ==} + engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} + hasBin: true + peerDependencies: + '@edge-runtime/vm': '*' + '@types/debug': ^4.1.12 + '@types/node': ^18.0.0 || ^20.0.0 || >=22.0.0 + '@vitest/browser': 3.1.4 + '@vitest/ui': 3.1.4 + happy-dom: '*' + jsdom: '*' + peerDependenciesMeta: + '@edge-runtime/vm': + optional: true + '@types/debug': + optional: true + '@types/node': + optional: true + '@vitest/browser': + optional: true + '@vitest/ui': + optional: true + happy-dom: + optional: true + jsdom: + optional: true + + w3c-xmlserializer@5.0.0: + resolution: {integrity: sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==} + engines: {node: '>=18'} + + webidl-conversions@3.0.1: + resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==} + + webidl-conversions@7.0.0: + resolution: {integrity: sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==} + engines: {node: '>=12'} + + whatwg-encoding@3.1.1: + resolution: {integrity: sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==} + engines: {node: '>=18'} + deprecated: Use @exodus/bytes instead for a more spec-conformant and faster implementation + + whatwg-mimetype@4.0.0: + resolution: {integrity: sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==} + engines: {node: '>=18'} + + whatwg-url@14.2.0: + resolution: {integrity: sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==} + engines: {node: '>=18'} + + whatwg-url@5.0.0: + resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==} + + why-is-node-running@2.3.0: + resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==} + engines: {node: '>=8'} + hasBin: true + + wrap-ansi@6.2.0: + resolution: {integrity: sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==} + engines: {node: '>=8'} + + wrap-ansi@7.0.0: + resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} + engines: {node: '>=10'} + + write-file-atomic@1.3.4: + resolution: {integrity: sha512-SdrHoC/yVBPpV0Xq/mUZQIpW2sWXAShb/V4pomcJXh92RuaO+f3UTWItiR3Px+pLnV2PvC2/bfn5cwr5X6Vfxw==} + + ws@8.19.0: + resolution: {integrity: sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==} + engines: {node: '>=10.0.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: '>=5.0.2' + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + + wsl-utils@0.3.1: + resolution: {integrity: sha512-g/eziiSUNBSsdDJtCLB8bdYEUMj4jR7AGeUo96p/3dTafgjHhpF4RiCFPiRILwjQoDXx5MqkBr4fwWtR3Ky4Wg==} + engines: {node: '>=20'} + + xml-name-validator@5.0.0: + resolution: {integrity: sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==} + engines: {node: '>=18'} + + xmlchars@2.2.0: + resolution: {integrity: sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==} + + y18n@5.0.8: + resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} + engines: {node: '>=10'} + + yargs-parser@21.1.1: + resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==} + engines: {node: '>=12'} + + yargs@17.7.2: + resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==} + engines: {node: '>=12'} + + yoctocolors-cjs@2.1.3: + resolution: {integrity: sha512-U/PBtDf35ff0D8X8D0jfdzHYEPFxAI7jJlxZXwCSez5M3190m+QobIfh+sWDWSHMCWWJN2AWamkegn6vr6YBTw==} + engines: {node: '>=18'} + +snapshots: + + '@asamuzakjp/css-color@3.2.0': + dependencies: + '@csstools/css-calc': 2.1.4(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) + '@csstools/css-color-parser': 3.1.0(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) + '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) + '@csstools/css-tokenizer': 3.0.4 + lru-cache: 10.4.3 + + '@csstools/color-helpers@5.1.0': {} + + '@csstools/css-calc@2.1.4(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4)': + dependencies: + '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) + '@csstools/css-tokenizer': 3.0.4 + + '@csstools/css-color-parser@3.1.0(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4)': + dependencies: + '@csstools/color-helpers': 5.1.0 + '@csstools/css-calc': 2.1.4(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) + '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) + '@csstools/css-tokenizer': 3.0.4 + + '@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4)': + dependencies: + '@csstools/css-tokenizer': 3.0.4 + + '@csstools/css-tokenizer@3.0.4': {} + + '@elasticpath/js-sdk@5.0.0': + dependencies: + cross-fetch: 3.2.0 + es6-promise: 4.2.8 + form-data: 4.0.5 + inflected: 2.1.0 + js-cookie: 3.0.5 + node-localstorage: 2.2.1 + throttled-queue: 2.1.4 + transitivePeerDependencies: + - encoding + + '@esbuild/aix-ppc64@0.25.12': + optional: true + + '@esbuild/aix-ppc64@0.27.2': + optional: true + + '@esbuild/android-arm64@0.25.12': + optional: true + + '@esbuild/android-arm64@0.27.2': + optional: true + + '@esbuild/android-arm@0.25.12': + optional: true + + '@esbuild/android-arm@0.27.2': + optional: true + + '@esbuild/android-x64@0.25.12': + optional: true + + '@esbuild/android-x64@0.27.2': + optional: true + + '@esbuild/darwin-arm64@0.25.12': + optional: true + + '@esbuild/darwin-arm64@0.27.2': + optional: true + + '@esbuild/darwin-x64@0.25.12': + optional: true + + '@esbuild/darwin-x64@0.27.2': + optional: true + + '@esbuild/freebsd-arm64@0.25.12': + optional: true + + '@esbuild/freebsd-arm64@0.27.2': + optional: true + + '@esbuild/freebsd-x64@0.25.12': + optional: true + + '@esbuild/freebsd-x64@0.27.2': + optional: true + + '@esbuild/linux-arm64@0.25.12': + optional: true + + '@esbuild/linux-arm64@0.27.2': + optional: true + + '@esbuild/linux-arm@0.25.12': + optional: true + + '@esbuild/linux-arm@0.27.2': + optional: true + + '@esbuild/linux-ia32@0.25.12': + optional: true + + '@esbuild/linux-ia32@0.27.2': + optional: true + + '@esbuild/linux-loong64@0.25.12': + optional: true + + '@esbuild/linux-loong64@0.27.2': + optional: true + + '@esbuild/linux-mips64el@0.25.12': + optional: true + + '@esbuild/linux-mips64el@0.27.2': + optional: true + + '@esbuild/linux-ppc64@0.25.12': + optional: true + + '@esbuild/linux-ppc64@0.27.2': + optional: true + + '@esbuild/linux-riscv64@0.25.12': + optional: true + + '@esbuild/linux-riscv64@0.27.2': + optional: true + + '@esbuild/linux-s390x@0.25.12': + optional: true + + '@esbuild/linux-s390x@0.27.2': + optional: true + + '@esbuild/linux-x64@0.25.12': + optional: true + + '@esbuild/linux-x64@0.27.2': + optional: true + + '@esbuild/netbsd-arm64@0.25.12': + optional: true + + '@esbuild/netbsd-arm64@0.27.2': + optional: true + + '@esbuild/netbsd-x64@0.25.12': + optional: true + + '@esbuild/netbsd-x64@0.27.2': + optional: true + + '@esbuild/openbsd-arm64@0.25.12': + optional: true + + '@esbuild/openbsd-arm64@0.27.2': + optional: true + + '@esbuild/openbsd-x64@0.25.12': + optional: true + + '@esbuild/openbsd-x64@0.27.2': + optional: true + + '@esbuild/openharmony-arm64@0.25.12': + optional: true + + '@esbuild/openharmony-arm64@0.27.2': + optional: true + + '@esbuild/sunos-x64@0.25.12': + optional: true + + '@esbuild/sunos-x64@0.27.2': + optional: true + + '@esbuild/win32-arm64@0.25.12': + optional: true + + '@esbuild/win32-arm64@0.27.2': + optional: true + + '@esbuild/win32-ia32@0.25.12': + optional: true + + '@esbuild/win32-ia32@0.27.2': + optional: true + + '@esbuild/win32-x64@0.25.12': + optional: true + + '@esbuild/win32-x64@0.27.2': + optional: true + + '@hey-api/client-fetch@0.13.1(@hey-api/openapi-ts@0.90.10(typescript@5.9.3))': + dependencies: + '@hey-api/openapi-ts': 0.90.10(typescript@5.9.3) + + '@hey-api/codegen-core@0.5.5(typescript@5.9.3)': + dependencies: + '@hey-api/types': 0.1.2 + ansi-colors: 4.1.3 + c12: 3.3.3 + color-support: 1.1.3 + typescript: 5.9.3 + transitivePeerDependencies: + - magicast + + '@hey-api/json-schema-ref-parser@1.2.2': + dependencies: + '@jsdevtools/ono': 7.1.3 + '@types/json-schema': 7.0.15 + js-yaml: 4.1.1 + lodash: 4.17.23 + + '@hey-api/openapi-ts@0.90.10(typescript@5.9.3)': + dependencies: + '@hey-api/codegen-core': 0.5.5(typescript@5.9.3) + '@hey-api/json-schema-ref-parser': 1.2.2 + '@hey-api/types': 0.1.2 + ansi-colors: 4.1.3 + color-support: 1.1.3 + commander: 14.0.2 + open: 11.0.0 + semver: 7.7.3 + typescript: 5.9.3 + transitivePeerDependencies: + - magicast + + '@hey-api/types@0.1.2': {} + + '@inquirer/ansi@1.0.2': {} + + '@inquirer/confirm@5.1.21': + dependencies: + '@inquirer/core': 10.3.2 + '@inquirer/type': 3.0.10 + + '@inquirer/core@10.3.2': + dependencies: + '@inquirer/ansi': 1.0.2 + '@inquirer/figures': 1.0.15 + '@inquirer/type': 3.0.10 + cli-width: 4.1.0 + mute-stream: 2.0.0 + signal-exit: 4.1.0 + wrap-ansi: 6.2.0 + yoctocolors-cjs: 2.1.3 + + '@inquirer/figures@1.0.15': {} + + '@inquirer/type@3.0.10': {} + + '@jridgewell/gen-mapping@0.3.13': + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/resolve-uri@3.1.2': {} + + '@jridgewell/sourcemap-codec@1.5.5': {} + + '@jridgewell/trace-mapping@0.3.31': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.5.5 + + '@jsdevtools/ono@7.1.3': {} + + '@mswjs/interceptors@0.40.0': + dependencies: + '@open-draft/deferred-promise': 2.2.0 + '@open-draft/logger': 0.3.0 + '@open-draft/until': 2.1.0 + is-node-process: 1.2.0 + outvariant: 1.4.3 + strict-event-emitter: 0.5.1 + + '@open-draft/deferred-promise@2.2.0': {} + + '@open-draft/logger@0.3.0': + dependencies: + is-node-process: 1.2.0 + outvariant: 1.4.3 + + '@open-draft/until@2.1.0': {} + + '@rollup/rollup-android-arm-eabi@4.57.0': + optional: true + + '@rollup/rollup-android-arm64@4.57.0': + optional: true + + '@rollup/rollup-darwin-arm64@4.57.0': + optional: true + + '@rollup/rollup-darwin-x64@4.57.0': + optional: true + + '@rollup/rollup-freebsd-arm64@4.57.0': + optional: true + + '@rollup/rollup-freebsd-x64@4.57.0': + optional: true + + '@rollup/rollup-linux-arm-gnueabihf@4.57.0': + optional: true + + '@rollup/rollup-linux-arm-musleabihf@4.57.0': + optional: true + + '@rollup/rollup-linux-arm64-gnu@4.57.0': + optional: true + + '@rollup/rollup-linux-arm64-musl@4.57.0': + optional: true + + '@rollup/rollup-linux-loong64-gnu@4.57.0': + optional: true + + '@rollup/rollup-linux-loong64-musl@4.57.0': + optional: true + + '@rollup/rollup-linux-ppc64-gnu@4.57.0': + optional: true + + '@rollup/rollup-linux-ppc64-musl@4.57.0': + optional: true + + '@rollup/rollup-linux-riscv64-gnu@4.57.0': + optional: true + + '@rollup/rollup-linux-riscv64-musl@4.57.0': + optional: true + + '@rollup/rollup-linux-s390x-gnu@4.57.0': + optional: true + + '@rollup/rollup-linux-x64-gnu@4.57.0': + optional: true + + '@rollup/rollup-linux-x64-musl@4.57.0': + optional: true + + '@rollup/rollup-openbsd-x64@4.57.0': + optional: true + + '@rollup/rollup-openharmony-arm64@4.57.0': + optional: true + + '@rollup/rollup-win32-arm64-msvc@4.57.0': + optional: true + + '@rollup/rollup-win32-ia32-msvc@4.57.0': + optional: true + + '@rollup/rollup-win32-x64-gnu@4.57.0': + optional: true + + '@rollup/rollup-win32-x64-msvc@4.57.0': + optional: true + + '@types/estree@1.0.8': {} + + '@types/json-schema@7.0.15': {} + + '@types/statuses@2.0.6': {} + + '@vitest/expect@3.1.4': + dependencies: + '@vitest/spy': 3.1.4 + '@vitest/utils': 3.1.4 + chai: 5.3.3 + tinyrainbow: 2.0.0 + + '@vitest/mocker@3.1.4(msw@2.12.7(typescript@5.9.3))(vite@6.4.1(jiti@2.6.1))': + dependencies: + '@vitest/spy': 3.1.4 + estree-walker: 3.0.3 + magic-string: 0.30.21 + optionalDependencies: + msw: 2.12.7(typescript@5.9.3) + vite: 6.4.1(jiti@2.6.1) + + '@vitest/pretty-format@3.1.4': + dependencies: + tinyrainbow: 2.0.0 + + '@vitest/pretty-format@3.2.4': + dependencies: + tinyrainbow: 2.0.0 + + '@vitest/runner@3.1.4': + dependencies: + '@vitest/utils': 3.1.4 + pathe: 2.0.3 + + '@vitest/snapshot@3.1.4': + dependencies: + '@vitest/pretty-format': 3.1.4 + magic-string: 0.30.21 + pathe: 2.0.3 + + '@vitest/spy@3.1.4': + dependencies: + tinyspy: 3.0.2 + + '@vitest/utils@3.1.4': + dependencies: + '@vitest/pretty-format': 3.1.4 + loupe: 3.2.1 + tinyrainbow: 2.0.0 + + acorn@8.15.0: {} + + agent-base@7.1.4: {} + + ansi-colors@4.1.3: {} + + ansi-regex@5.0.1: {} + + ansi-styles@4.3.0: + dependencies: + color-convert: 2.0.1 + + any-promise@1.3.0: {} + + argparse@2.0.1: {} + + assertion-error@2.0.1: {} + + asynckit@0.4.0: {} + + bundle-name@4.1.0: + dependencies: + run-applescript: 7.1.0 + + bundle-require@5.1.0(esbuild@0.27.2): + dependencies: + esbuild: 0.27.2 + load-tsconfig: 0.2.5 + + c12@3.3.3: + dependencies: + chokidar: 5.0.0 + confbox: 0.2.2 + defu: 6.1.4 + dotenv: 17.2.3 + exsolve: 1.0.8 + giget: 2.0.0 + jiti: 2.6.1 + ohash: 2.0.11 + pathe: 2.0.3 + perfect-debounce: 2.1.0 + pkg-types: 2.3.0 + rc9: 2.1.2 + + cac@6.7.14: {} + + call-bind-apply-helpers@1.0.2: + dependencies: + es-errors: 1.3.0 + function-bind: 1.1.2 + + chai@5.3.3: + dependencies: + assertion-error: 2.0.1 + check-error: 2.1.3 + deep-eql: 5.0.2 + loupe: 3.2.1 + pathval: 2.0.1 + + check-error@2.1.3: {} + + chokidar@4.0.3: + dependencies: + readdirp: 4.1.2 + + chokidar@5.0.0: + dependencies: + readdirp: 5.0.0 + + citty@0.1.6: + dependencies: + consola: 3.4.2 + + citty@0.2.0: {} + + cli-width@4.1.0: {} + + cliui@8.0.1: + dependencies: + string-width: 4.2.3 + strip-ansi: 6.0.1 + wrap-ansi: 7.0.0 + + color-convert@2.0.1: + dependencies: + color-name: 1.1.4 + + color-name@1.1.4: {} + + color-support@1.1.3: {} + + combined-stream@1.0.8: + dependencies: + delayed-stream: 1.0.0 + + commander@14.0.2: {} + + commander@4.1.1: {} + + confbox@0.1.8: {} + + confbox@0.2.2: {} + + consola@3.4.2: {} + + cookie@1.1.1: {} + + cross-fetch@3.2.0: + dependencies: + node-fetch: 2.7.0 + transitivePeerDependencies: + - encoding + + cssstyle@4.6.0: + dependencies: + '@asamuzakjp/css-color': 3.2.0 + rrweb-cssom: 0.8.0 + + data-urls@5.0.0: + dependencies: + whatwg-mimetype: 4.0.0 + whatwg-url: 14.2.0 + + debug@4.4.3: + dependencies: + ms: 2.1.3 + + decimal.js@10.6.0: {} + + deep-eql@5.0.2: {} + + default-browser-id@5.0.1: {} + + default-browser@5.4.0: + dependencies: + bundle-name: 4.1.0 + default-browser-id: 5.0.1 + + define-lazy-prop@3.0.0: {} + + defu@6.1.4: {} + + delayed-stream@1.0.0: {} + + destr@2.0.5: {} + + dotenv@17.2.3: {} + + dunder-proto@1.0.1: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-errors: 1.3.0 + gopd: 1.2.0 + + emoji-regex@8.0.0: {} + + entities@6.0.1: {} + + es-define-property@1.0.1: {} + + es-errors@1.3.0: {} + + es-module-lexer@1.7.0: {} + + es-object-atoms@1.1.1: + dependencies: + es-errors: 1.3.0 + + es-set-tostringtag@2.1.0: + dependencies: + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + has-tostringtag: 1.0.2 + hasown: 2.0.2 + + es6-promise@4.2.8: {} + + esbuild-fix-imports-plugin@1.0.23: {} + + esbuild-plugin-file-path-extensions@1.0.0: {} + + esbuild@0.25.12: + optionalDependencies: + '@esbuild/aix-ppc64': 0.25.12 + '@esbuild/android-arm': 0.25.12 + '@esbuild/android-arm64': 0.25.12 + '@esbuild/android-x64': 0.25.12 + '@esbuild/darwin-arm64': 0.25.12 + '@esbuild/darwin-x64': 0.25.12 + '@esbuild/freebsd-arm64': 0.25.12 + '@esbuild/freebsd-x64': 0.25.12 + '@esbuild/linux-arm': 0.25.12 + '@esbuild/linux-arm64': 0.25.12 + '@esbuild/linux-ia32': 0.25.12 + '@esbuild/linux-loong64': 0.25.12 + '@esbuild/linux-mips64el': 0.25.12 + '@esbuild/linux-ppc64': 0.25.12 + '@esbuild/linux-riscv64': 0.25.12 + '@esbuild/linux-s390x': 0.25.12 + '@esbuild/linux-x64': 0.25.12 + '@esbuild/netbsd-arm64': 0.25.12 + '@esbuild/netbsd-x64': 0.25.12 + '@esbuild/openbsd-arm64': 0.25.12 + '@esbuild/openbsd-x64': 0.25.12 + '@esbuild/openharmony-arm64': 0.25.12 + '@esbuild/sunos-x64': 0.25.12 + '@esbuild/win32-arm64': 0.25.12 + '@esbuild/win32-ia32': 0.25.12 + '@esbuild/win32-x64': 0.25.12 + + esbuild@0.27.2: + optionalDependencies: + '@esbuild/aix-ppc64': 0.27.2 + '@esbuild/android-arm': 0.27.2 + '@esbuild/android-arm64': 0.27.2 + '@esbuild/android-x64': 0.27.2 + '@esbuild/darwin-arm64': 0.27.2 + '@esbuild/darwin-x64': 0.27.2 + '@esbuild/freebsd-arm64': 0.27.2 + '@esbuild/freebsd-x64': 0.27.2 + '@esbuild/linux-arm': 0.27.2 + '@esbuild/linux-arm64': 0.27.2 + '@esbuild/linux-ia32': 0.27.2 + '@esbuild/linux-loong64': 0.27.2 + '@esbuild/linux-mips64el': 0.27.2 + '@esbuild/linux-ppc64': 0.27.2 + '@esbuild/linux-riscv64': 0.27.2 + '@esbuild/linux-s390x': 0.27.2 + '@esbuild/linux-x64': 0.27.2 + '@esbuild/netbsd-arm64': 0.27.2 + '@esbuild/netbsd-x64': 0.27.2 + '@esbuild/openbsd-arm64': 0.27.2 + '@esbuild/openbsd-x64': 0.27.2 + '@esbuild/openharmony-arm64': 0.27.2 + '@esbuild/sunos-x64': 0.27.2 + '@esbuild/win32-arm64': 0.27.2 + '@esbuild/win32-ia32': 0.27.2 + '@esbuild/win32-x64': 0.27.2 + + escalade@3.2.0: {} + + estree-walker@3.0.3: + dependencies: + '@types/estree': 1.0.8 + + expect-type@1.3.0: {} + + exsolve@1.0.8: {} + + fdir@6.5.0(picomatch@4.0.3): + optionalDependencies: + picomatch: 4.0.3 + + fix-dts-default-cjs-exports@1.0.1: + dependencies: + magic-string: 0.30.21 + mlly: 1.8.0 + rollup: 4.57.0 + + form-data@4.0.5: + dependencies: + asynckit: 0.4.0 + combined-stream: 1.0.8 + es-set-tostringtag: 2.1.0 + hasown: 2.0.2 + mime-types: 2.1.35 + + fsevents@2.3.3: + optional: true + + function-bind@1.1.2: {} + + get-caller-file@2.0.5: {} + + get-intrinsic@1.3.0: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-define-property: 1.0.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + function-bind: 1.1.2 + get-proto: 1.0.1 + gopd: 1.2.0 + has-symbols: 1.1.0 + hasown: 2.0.2 + math-intrinsics: 1.1.0 + + get-proto@1.0.1: + dependencies: + dunder-proto: 1.0.1 + es-object-atoms: 1.1.1 + + giget@2.0.0: + dependencies: + citty: 0.1.6 + consola: 3.4.2 + defu: 6.1.4 + node-fetch-native: 1.6.7 + nypm: 0.6.4 + pathe: 2.0.3 + + gopd@1.2.0: {} + + graceful-fs@4.2.11: {} + + graphql@16.12.0: {} + + has-symbols@1.1.0: {} + + has-tostringtag@1.0.2: + dependencies: + has-symbols: 1.1.0 + + hasown@2.0.2: + dependencies: + function-bind: 1.1.2 + + headers-polyfill@4.0.3: {} + + html-encoding-sniffer@4.0.0: + dependencies: + whatwg-encoding: 3.1.1 + + http-proxy-agent@7.0.2: + dependencies: + agent-base: 7.1.4 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + + https-proxy-agent@7.0.6: + dependencies: + agent-base: 7.1.4 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + + iconv-lite@0.6.3: + dependencies: + safer-buffer: 2.1.2 + + imurmurhash@0.1.4: {} + + inflected@2.1.0: {} + + is-docker@3.0.0: {} + + is-fullwidth-code-point@3.0.0: {} + + is-in-ssh@1.0.0: {} + + is-inside-container@1.0.0: + dependencies: + is-docker: 3.0.0 + + is-node-process@1.2.0: {} + + is-potential-custom-element-name@1.0.1: {} + + is-wsl@3.1.0: + dependencies: + is-inside-container: 1.0.0 + + jiti@2.6.1: {} + + joycon@3.1.1: {} + + js-cookie@3.0.5: {} + + js-yaml@4.1.1: + dependencies: + argparse: 2.0.1 + + jsdom@26.1.0: + dependencies: + cssstyle: 4.6.0 + data-urls: 5.0.0 + decimal.js: 10.6.0 + html-encoding-sniffer: 4.0.0 + http-proxy-agent: 7.0.2 + https-proxy-agent: 7.0.6 + is-potential-custom-element-name: 1.0.1 + nwsapi: 2.2.23 + parse5: 7.3.0 + rrweb-cssom: 0.8.0 + saxes: 6.0.0 + symbol-tree: 3.2.4 + tough-cookie: 5.1.2 + w3c-xmlserializer: 5.0.0 + webidl-conversions: 7.0.0 + whatwg-encoding: 3.1.1 + whatwg-mimetype: 4.0.0 + whatwg-url: 14.2.0 + ws: 8.19.0 + xml-name-validator: 5.0.0 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + + lilconfig@3.1.3: {} + + lines-and-columns@1.2.4: {} + + load-tsconfig@0.2.5: {} + + lodash@4.17.23: {} + + loupe@3.2.1: {} + + lru-cache@10.4.3: {} + + magic-string@0.30.21: + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + + math-intrinsics@1.1.0: {} + + mime-db@1.52.0: {} + + mime-types@2.1.35: + dependencies: + mime-db: 1.52.0 + + mlly@1.8.0: + dependencies: + acorn: 8.15.0 + pathe: 2.0.3 + pkg-types: 1.3.1 + ufo: 1.6.3 + + ms@2.1.3: {} + + msw@2.12.7(typescript@5.9.3): + dependencies: + '@inquirer/confirm': 5.1.21 + '@mswjs/interceptors': 0.40.0 + '@open-draft/deferred-promise': 2.2.0 + '@types/statuses': 2.0.6 + cookie: 1.1.1 + graphql: 16.12.0 + headers-polyfill: 4.0.3 + is-node-process: 1.2.0 + outvariant: 1.4.3 + path-to-regexp: 6.3.0 + picocolors: 1.1.1 + rettime: 0.7.0 + statuses: 2.0.2 + strict-event-emitter: 0.5.1 + tough-cookie: 6.0.0 + type-fest: 5.4.2 + until-async: 3.0.2 + yargs: 17.7.2 + optionalDependencies: + typescript: 5.9.3 + transitivePeerDependencies: + - '@types/node' + + mute-stream@2.0.0: {} + + mz@2.7.0: + dependencies: + any-promise: 1.3.0 + object-assign: 4.1.1 + thenify-all: 1.6.0 + + nanoid@3.3.11: {} + + node-fetch-native@1.6.7: {} + + node-fetch@2.7.0: + dependencies: + whatwg-url: 5.0.0 + + node-localstorage@2.2.1: + dependencies: + write-file-atomic: 1.3.4 + + nwsapi@2.2.23: {} + + nypm@0.6.4: + dependencies: + citty: 0.2.0 + pathe: 2.0.3 + tinyexec: 1.0.2 + + object-assign@4.1.1: {} + + ohash@2.0.11: {} + + open@11.0.0: + dependencies: + default-browser: 5.4.0 + define-lazy-prop: 3.0.0 + is-in-ssh: 1.0.0 + is-inside-container: 1.0.0 + powershell-utils: 0.1.0 + wsl-utils: 0.3.1 + + outvariant@1.4.3: {} + + parse5@7.3.0: + dependencies: + entities: 6.0.1 + + path-to-regexp@6.3.0: {} + + pathe@2.0.3: {} + + pathval@2.0.1: {} + + perfect-debounce@2.1.0: {} + + picocolors@1.1.1: {} + + picomatch@4.0.3: {} + + pirates@4.0.7: {} + + pkg-types@1.3.1: + dependencies: + confbox: 0.1.8 + mlly: 1.8.0 + pathe: 2.0.3 + + pkg-types@2.3.0: + dependencies: + confbox: 0.2.2 + exsolve: 1.0.8 + pathe: 2.0.3 + + postcss-load-config@6.0.1(jiti@2.6.1)(postcss@8.5.6): + dependencies: + lilconfig: 3.1.3 + optionalDependencies: + jiti: 2.6.1 + postcss: 8.5.6 + + postcss@8.5.6: + dependencies: + nanoid: 3.3.11 + picocolors: 1.1.1 + source-map-js: 1.2.1 + + powershell-utils@0.1.0: {} + + punycode@2.3.1: {} + + rc9@2.1.2: + dependencies: + defu: 6.1.4 + destr: 2.0.5 + + readdirp@4.1.2: {} + + readdirp@5.0.0: {} + + require-directory@2.1.1: {} + + resolve-from@5.0.0: {} + + rettime@0.7.0: {} + + rollup@4.57.0: + dependencies: + '@types/estree': 1.0.8 + optionalDependencies: + '@rollup/rollup-android-arm-eabi': 4.57.0 + '@rollup/rollup-android-arm64': 4.57.0 + '@rollup/rollup-darwin-arm64': 4.57.0 + '@rollup/rollup-darwin-x64': 4.57.0 + '@rollup/rollup-freebsd-arm64': 4.57.0 + '@rollup/rollup-freebsd-x64': 4.57.0 + '@rollup/rollup-linux-arm-gnueabihf': 4.57.0 + '@rollup/rollup-linux-arm-musleabihf': 4.57.0 + '@rollup/rollup-linux-arm64-gnu': 4.57.0 + '@rollup/rollup-linux-arm64-musl': 4.57.0 + '@rollup/rollup-linux-loong64-gnu': 4.57.0 + '@rollup/rollup-linux-loong64-musl': 4.57.0 + '@rollup/rollup-linux-ppc64-gnu': 4.57.0 + '@rollup/rollup-linux-ppc64-musl': 4.57.0 + '@rollup/rollup-linux-riscv64-gnu': 4.57.0 + '@rollup/rollup-linux-riscv64-musl': 4.57.0 + '@rollup/rollup-linux-s390x-gnu': 4.57.0 + '@rollup/rollup-linux-x64-gnu': 4.57.0 + '@rollup/rollup-linux-x64-musl': 4.57.0 + '@rollup/rollup-openbsd-x64': 4.57.0 + '@rollup/rollup-openharmony-arm64': 4.57.0 + '@rollup/rollup-win32-arm64-msvc': 4.57.0 + '@rollup/rollup-win32-ia32-msvc': 4.57.0 + '@rollup/rollup-win32-x64-gnu': 4.57.0 + '@rollup/rollup-win32-x64-msvc': 4.57.0 + fsevents: 2.3.3 + + rrweb-cssom@0.8.0: {} + + run-applescript@7.1.0: {} + + safer-buffer@2.1.2: {} + + saxes@6.0.0: + dependencies: + xmlchars: 2.2.0 + + semver@7.7.3: {} + + siginfo@2.0.0: {} + + signal-exit@4.1.0: {} + + slide@1.1.6: {} + + source-map-js@1.2.1: {} + + source-map@0.7.6: {} + + stackback@0.0.2: {} + + statuses@2.0.2: {} + + std-env@3.10.0: {} + + strict-event-emitter@0.5.1: {} + + string-width@4.2.3: + dependencies: + emoji-regex: 8.0.0 + is-fullwidth-code-point: 3.0.0 + strip-ansi: 6.0.1 + + strip-ansi@6.0.1: + dependencies: + ansi-regex: 5.0.1 + + sucrase@3.35.1: + dependencies: + '@jridgewell/gen-mapping': 0.3.13 + commander: 4.1.1 + lines-and-columns: 1.2.4 + mz: 2.7.0 + pirates: 4.0.7 + tinyglobby: 0.2.15 + ts-interface-checker: 0.1.13 + + symbol-tree@3.2.4: {} + + tagged-tag@1.0.0: {} + + thenify-all@1.6.0: + dependencies: + thenify: 3.3.1 + + thenify@3.3.1: + dependencies: + any-promise: 1.3.0 + + throttled-queue@2.1.4: {} + + tinybench@2.9.0: {} + + tinyexec@0.3.2: {} + + tinyexec@1.0.2: {} + + tinyglobby@0.2.15: + dependencies: + fdir: 6.5.0(picomatch@4.0.3) + picomatch: 4.0.3 + + tinypool@1.1.1: {} + + tinyrainbow@2.0.0: {} + + tinyspy@3.0.2: {} + + tldts-core@6.1.86: {} + + tldts-core@7.0.19: {} + + tldts@6.1.86: + dependencies: + tldts-core: 6.1.86 + + tldts@7.0.19: + dependencies: + tldts-core: 7.0.19 + + tough-cookie@5.1.2: + dependencies: + tldts: 6.1.86 + + tough-cookie@6.0.0: + dependencies: + tldts: 7.0.19 + + tr46@0.0.3: {} + + tr46@5.1.1: + dependencies: + punycode: 2.3.1 + + tree-kill@1.2.2: {} + + ts-interface-checker@0.1.13: {} + + tsup@8.5.1(jiti@2.6.1)(postcss@8.5.6)(typescript@5.9.3): + dependencies: + bundle-require: 5.1.0(esbuild@0.27.2) + cac: 6.7.14 + chokidar: 4.0.3 + consola: 3.4.2 + debug: 4.4.3 + esbuild: 0.27.2 + fix-dts-default-cjs-exports: 1.0.1 + joycon: 3.1.1 + picocolors: 1.1.1 + postcss-load-config: 6.0.1(jiti@2.6.1)(postcss@8.5.6) + resolve-from: 5.0.0 + rollup: 4.57.0 + source-map: 0.7.6 + sucrase: 3.35.1 + tinyexec: 0.3.2 + tinyglobby: 0.2.15 + tree-kill: 1.2.2 + optionalDependencies: + postcss: 8.5.6 + typescript: 5.9.3 + transitivePeerDependencies: + - jiti + - supports-color + - tsx + - yaml + + type-fest@5.4.2: + dependencies: + tagged-tag: 1.0.0 + + typescript@5.9.3: {} + + ufo@1.6.3: {} + + until-async@3.0.2: {} + + vite-node@3.1.4(jiti@2.6.1): + dependencies: + cac: 6.7.14 + debug: 4.4.3 + es-module-lexer: 1.7.0 + pathe: 2.0.3 + vite: 6.4.1(jiti@2.6.1) + transitivePeerDependencies: + - '@types/node' + - jiti + - less + - lightningcss + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + - tsx + - yaml + + vite@6.4.1(jiti@2.6.1): + dependencies: + esbuild: 0.25.12 + fdir: 6.5.0(picomatch@4.0.3) + picomatch: 4.0.3 + postcss: 8.5.6 + rollup: 4.57.0 + tinyglobby: 0.2.15 + optionalDependencies: + fsevents: 2.3.3 + jiti: 2.6.1 + + vitest@3.1.4(jiti@2.6.1)(jsdom@26.1.0)(msw@2.12.7(typescript@5.9.3)): + dependencies: + '@vitest/expect': 3.1.4 + '@vitest/mocker': 3.1.4(msw@2.12.7(typescript@5.9.3))(vite@6.4.1(jiti@2.6.1)) + '@vitest/pretty-format': 3.2.4 + '@vitest/runner': 3.1.4 + '@vitest/snapshot': 3.1.4 + '@vitest/spy': 3.1.4 + '@vitest/utils': 3.1.4 + chai: 5.3.3 + debug: 4.4.3 + expect-type: 1.3.0 + magic-string: 0.30.21 + pathe: 2.0.3 + std-env: 3.10.0 + tinybench: 2.9.0 + tinyexec: 0.3.2 + tinyglobby: 0.2.15 + tinypool: 1.1.1 + tinyrainbow: 2.0.0 + vite: 6.4.1(jiti@2.6.1) + vite-node: 3.1.4(jiti@2.6.1) + why-is-node-running: 2.3.0 + optionalDependencies: + jsdom: 26.1.0 + transitivePeerDependencies: + - jiti + - less + - lightningcss + - msw + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + - tsx + - yaml + + w3c-xmlserializer@5.0.0: + dependencies: + xml-name-validator: 5.0.0 + + webidl-conversions@3.0.1: {} + + webidl-conversions@7.0.0: {} + + whatwg-encoding@3.1.1: + dependencies: + iconv-lite: 0.6.3 + + whatwg-mimetype@4.0.0: {} + + whatwg-url@14.2.0: + dependencies: + tr46: 5.1.1 + webidl-conversions: 7.0.0 + + whatwg-url@5.0.0: + dependencies: + tr46: 0.0.3 + webidl-conversions: 3.0.1 + + why-is-node-running@2.3.0: + dependencies: + siginfo: 2.0.0 + stackback: 0.0.2 + + wrap-ansi@6.2.0: + dependencies: + ansi-styles: 4.3.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + + wrap-ansi@7.0.0: + dependencies: + ansi-styles: 4.3.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + + write-file-atomic@1.3.4: + dependencies: + graceful-fs: 4.2.11 + imurmurhash: 0.1.4 + slide: 1.1.6 + + ws@8.19.0: {} + + wsl-utils@0.3.1: + dependencies: + is-wsl: 3.1.0 + powershell-utils: 0.1.0 + + xml-name-validator@5.0.0: {} + + xmlchars@2.2.0: {} + + y18n@5.0.8: {} + + yargs-parser@21.1.1: {} + + yargs@17.7.2: + dependencies: + cliui: 8.0.1 + escalade: 3.2.0 + get-caller-file: 2.0.5 + require-directory: 2.1.1 + string-width: 4.2.3 + y18n: 5.0.8 + yargs-parser: 21.1.1 + + yoctocolors-cjs@2.1.3: {} diff --git a/packages/sdks/compatibility-layer/src/__integration__/bridged-shopper.test.ts b/packages/sdks/compatibility-layer/src/__integration__/bridged-shopper.test.ts new file mode 100644 index 00000000..e8186e4a --- /dev/null +++ b/packages/sdks/compatibility-layer/src/__integration__/bridged-shopper.test.ts @@ -0,0 +1,276 @@ +import { describe, it, expect, beforeAll, afterAll, beforeEach } from "vitest" +import { client as shopperClient } from "@epcc-sdk/sdks-shopper" +import { createBridgedClient, createLegacyStorageBridge } from "../index" +import { + setupMswServer, + teardownMswServer, + resetMswHandlers, + getRequestCounts, + setReturn401, + setReturn429, + mockTokenData, + mockCatalogProducts, +} from "./setup" + +describe("Bridged Shopper Client Integration", () => { + beforeAll(() => { + setupMswServer() + }) + + afterAll(() => { + teardownMswServer() + }) + + beforeEach(() => { + resetMswHandlers() + }) + + describe("Basic API calls", () => { + it("should automatically authenticate and make API call", async () => { + const storage = createLegacyStorageBridge({ backend: "memory" }) + + const { auth } = createBridgedClient(shopperClient, { + baseUrl: "https://api.elasticpath.com", + clientId: "test-client-id", + storage, + }) + + // Make request - should trigger authentication automatically + const response = await fetch("https://api.elasticpath.com/catalog/products", { + headers: { + Authorization: `Bearer ${await auth.getValidAccessToken()}`, + }, + }) + + expect(response.ok).toBe(true) + const data = await response.json() + expect(data).toEqual(mockCatalogProducts) + + // Should have authenticated + const { authRequestCount } = getRequestCounts() + expect(authRequestCount).toBe(1) + + // Token should be stored + expect(auth.getSnapshot()).toBe(mockTokenData.access_token) + }) + + it("should reuse existing token for subsequent calls", async () => { + const storage = createLegacyStorageBridge({ backend: "memory" }) + + const { auth } = createBridgedClient(shopperClient, { + baseUrl: "https://api.elasticpath.com", + clientId: "test-client-id", + storage, + }) + + // First call - should authenticate + const token1 = await auth.getValidAccessToken() + + // Second call - should reuse token + const token2 = await auth.getValidAccessToken() + + expect(token1).toBe(token2) + + // Should only have authenticated once + const { authRequestCount } = getRequestCounts() + expect(authRequestCount).toBe(1) + }) + }) + + describe("Retry on 401", () => { + it("should retry with fresh token on 401 response", async () => { + const storage = createLegacyStorageBridge({ backend: "memory" }) + + // Pre-populate with a token + storage.set( + JSON.stringify({ + access_token: "expired-token", + expires: Math.floor(Date.now() / 1000) + 3600, // Not actually expired + }) + ) + + // Set up MSW to return 401 once, then succeed + setReturn401(true, 1) + + const { auth } = createBridgedClient(shopperClient, { + baseUrl: "https://api.elasticpath.com", + clientId: "test-client-id", + storage, + retry: { maxAttempts: 3, reauth: true }, + }) + + // Use the bridged fetch to make a request + const token = await auth.getValidAccessToken() + const response = await globalThis.fetch( + "https://api.elasticpath.com/catalog/products", + { + headers: { + Authorization: `Bearer ${token}`, + }, + } + ) + + // Note: The retry happens at fetch level, not at auth level + // This test verifies the auth system works, the retry-on-401 is tested + // in the fetch-with-retry unit tests + expect(response.status).toBe(401) // First request gets 401 + }) + + it("should trigger token refresh on explicit refresh call", async () => { + const storage = createLegacyStorageBridge({ backend: "memory" }) + + const { auth } = createBridgedClient(shopperClient, { + baseUrl: "https://api.elasticpath.com", + clientId: "test-client-id", + storage, + }) + + // Get initial token + const token1 = await auth.getValidAccessToken() + expect(token1).toBe(mockTokenData.access_token) + + const { authRequestCount: count1 } = getRequestCounts() + expect(count1).toBe(1) + + // Force refresh + const token2 = await auth.refresh() + expect(token2).toBe(mockTokenData.access_token) + + // Should have made another auth request + const { authRequestCount: count2 } = getRequestCounts() + expect(count2).toBe(2) + }) + }) + + describe("Rate limiting handling", () => { + it("should handle 429 responses gracefully", async () => { + const storage = createLegacyStorageBridge({ backend: "memory" }) + + setReturn429(true) + + const { auth } = createBridgedClient(shopperClient, { + baseUrl: "https://api.elasticpath.com", + clientId: "test-client-id", + storage, + }) + + const token = await auth.getValidAccessToken() + + // First request should get 429, but we're just testing auth works + const response = await globalThis.fetch( + "https://api.elasticpath.com/catalog/products", + { + headers: { + Authorization: `Bearer ${token}`, + }, + } + ) + + // MSW handler returns 429 once then succeeds + expect(response.status).toBe(429) + }) + }) + + describe("Throttling", () => { + it("should throttle rapid requests when enabled", async () => { + const storage = createLegacyStorageBridge({ backend: "memory" }) + + const { auth } = createBridgedClient(shopperClient, { + baseUrl: "https://api.elasticpath.com", + clientId: "test-client-id", + storage, + throttle: { enabled: true, limit: 2, interval: 100 }, + }) + + const token = await auth.getValidAccessToken() + + // Make multiple rapid requests + await Promise.all([ + fetch("https://api.elasticpath.com/catalog/products", { + headers: { Authorization: `Bearer ${token}` }, + }), + fetch("https://api.elasticpath.com/catalog/products", { + headers: { Authorization: `Bearer ${token}` }, + }), + fetch("https://api.elasticpath.com/catalog/products", { + headers: { Authorization: `Bearer ${token}` }, + }), + fetch("https://api.elasticpath.com/catalog/products", { + headers: { Authorization: `Bearer ${token}` }, + }), + ]) + + // All requests should complete + const { productRequestCount } = getRequestCounts() + expect(productRequestCount).toBe(4) + + // Should have taken some time due to throttling + // With limit=2 and interval=100ms, 4 requests should take at least 100ms + // But we don't enforce exact timing in tests due to CI variability + }) + }) + + describe("Headers configuration", () => { + it("should include custom headers in requests", async () => { + const storage = createLegacyStorageBridge({ backend: "memory" }) + + const { auth } = createBridgedClient(shopperClient, { + baseUrl: "https://api.elasticpath.com", + clientId: "test-client-id", + storage, + headers: { + "X-Custom-Header": "test-value", + "X-Moltin-Currency": "USD", + }, + }) + + // The headers are configured on the client + // We just verify the client was configured without errors + const token = await auth.getValidAccessToken() + expect(token).toBe(mockTokenData.access_token) + }) + }) + + describe("Storage persistence", () => { + it("should persist token to storage after authentication", async () => { + const storage = createLegacyStorageBridge({ backend: "memory" }) + + const { auth } = createBridgedClient(shopperClient, { + baseUrl: "https://api.elasticpath.com", + clientId: "test-client-id", + storage, + }) + + // Trigger authentication + await auth.getValidAccessToken() + + // Check storage was updated + const stored = storage.get() + expect(stored).toBeDefined() + + const parsed = JSON.parse(stored!) + expect(parsed.access_token).toBe(mockTokenData.access_token) + }) + + it("should clear token from storage on clear()", async () => { + const storage = createLegacyStorageBridge({ backend: "memory" }) + + const { auth } = createBridgedClient(shopperClient, { + baseUrl: "https://api.elasticpath.com", + clientId: "test-client-id", + storage, + }) + + // Authenticate first + await auth.getValidAccessToken() + expect(storage.get()).toBeDefined() + + // Clear + auth.clear() + + // Storage should be empty (memory adapter returns undefined, not null) + expect(storage.get()).toBeFalsy() + expect(auth.getSnapshot()).toBeUndefined() + }) + }) +}) diff --git a/packages/sdks/compatibility-layer/src/__integration__/handlers.ts b/packages/sdks/compatibility-layer/src/__integration__/handlers.ts new file mode 100644 index 00000000..6948507f --- /dev/null +++ b/packages/sdks/compatibility-layer/src/__integration__/handlers.ts @@ -0,0 +1,233 @@ +import { http, HttpResponse } from "msw" + +const BASE_URL = "https://api.elasticpath.com" + +// Track request counts for testing retry behavior +let authRequestCount = 0 +let productRequestCount = 0 + +export function resetRequestCounts() { + authRequestCount = 0 + productRequestCount = 0 +} + +export function getRequestCounts() { + return { authRequestCount, productRequestCount } +} + +// Mock token data +export const mockTokenData = { + access_token: "mock-access-token-12345", + token_type: "Bearer", + expires_in: 3600, + expires: Math.floor(Date.now() / 1000) + 3600, + identifier: "implicit", + client_id: "test-client-id", +} + +export const mockClientCredentialsToken = { + access_token: "mock-admin-token-67890", + token_type: "Bearer", + expires_in: 3600, + expires: Math.floor(Date.now() / 1000) + 3600, + identifier: "client_credentials", + client_id: "admin-client-id", +} + +// Mock product data (old SDK format - v2 API) +export const mockProductsV2 = { + data: [ + { + id: "product-1", + type: "product", + name: "Test Product 1", + slug: "test-product-1", + sku: "TP001", + description: "A test product", + status: "live", + commodity_type: "physical", + }, + { + id: "product-2", + type: "product", + name: "Test Product 2", + slug: "test-product-2", + sku: "TP002", + description: "Another test product", + status: "live", + commodity_type: "physical", + }, + ], + meta: { + results: { + total: 2, + }, + }, +} + +// Mock product data (new SDK format - catalog API) +export const mockCatalogProducts = { + data: [ + { + id: "catalog-product-1", + type: "product", + attributes: { + name: "Catalog Product 1", + slug: "catalog-product-1", + sku: "CP001", + description: "A catalog product", + status: "live", + }, + }, + ], + meta: { + results: { + total: 1, + }, + }, +} + +// Mock PIM products +export const mockPimProducts = { + data: [ + { + id: "pim-product-1", + type: "product", + attributes: { + name: "PIM Product 1", + commodity_type: "physical", + sku: "PIM001", + status: "live", + }, + }, + ], + meta: { + results: { + total: 1, + }, + }, +} + +// State for controlling test scenarios +let shouldReturn401 = false +let shouldReturn429 = false +let return401Count = 0 + +export function setReturn401(value: boolean, count = 1) { + shouldReturn401 = value + return401Count = count +} + +export function setReturn429(value: boolean) { + shouldReturn429 = value +} + +export const handlers = [ + // OAuth token endpoint - implicit and client_credentials + http.post(`${BASE_URL}/oauth/access_token`, async ({ request }) => { + authRequestCount++ + + const body = await request.text() + const params = new URLSearchParams(body) + const grantType = params.get("grant_type") + + if (grantType === "client_credentials") { + return HttpResponse.json(mockClientCredentialsToken) + } + + // Default to implicit grant + return HttpResponse.json(mockTokenData) + }), + + // Old SDK products endpoint (v2) + http.get(`${BASE_URL}/v2/products`, ({ request }) => { + productRequestCount++ + + // Check for 401 scenario + if (shouldReturn401 && return401Count > 0) { + return401Count-- + if (return401Count === 0) { + shouldReturn401 = false + } + return new HttpResponse(JSON.stringify({ errors: [{ status: 401 }] }), { + status: 401, + }) + } + + // Check for 429 scenario + if (shouldReturn429) { + shouldReturn429 = false + return new HttpResponse(JSON.stringify({ errors: [{ status: 429 }] }), { + status: 429, + }) + } + + // Verify authorization header + const authHeader = request.headers.get("Authorization") + if (!authHeader || !authHeader.startsWith("Bearer ")) { + return new HttpResponse( + JSON.stringify({ errors: [{ status: 401, detail: "Missing auth" }] }), + { status: 401 } + ) + } + + return HttpResponse.json(mockProductsV2) + }), + + // New SDK catalog products endpoint + http.get(`${BASE_URL}/catalog/products`, ({ request }) => { + productRequestCount++ + + // Check for 401 scenario + if (shouldReturn401 && return401Count > 0) { + return401Count-- + if (return401Count === 0) { + shouldReturn401 = false + } + return new HttpResponse(JSON.stringify({ errors: [{ status: 401 }] }), { + status: 401, + }) + } + + // Check for 429 scenario + if (shouldReturn429) { + shouldReturn429 = false + return new HttpResponse(JSON.stringify({ errors: [{ status: 429 }] }), { + status: 429, + }) + } + + // Verify authorization header + const authHeader = request.headers.get("Authorization") + if (!authHeader || !authHeader.startsWith("Bearer ")) { + return new HttpResponse( + JSON.stringify({ errors: [{ status: 401, detail: "Missing auth" }] }), + { status: 401 } + ) + } + + return HttpResponse.json(mockCatalogProducts) + }), + + // PIM products endpoint + http.get(`${BASE_URL}/pcm/products`, ({ request }) => { + productRequestCount++ + + // Verify authorization header + const authHeader = request.headers.get("Authorization") + if (!authHeader || !authHeader.startsWith("Bearer ")) { + return new HttpResponse( + JSON.stringify({ errors: [{ status: 401, detail: "Missing auth" }] }), + { status: 401 } + ) + } + + return HttpResponse.json(mockPimProducts) + }), + + // Catch-all for debugging + http.all(`${BASE_URL}/*`, ({ request }) => { + console.log(`Unhandled request: ${request.method} ${request.url}`) + return new HttpResponse(null, { status: 404 }) + }), +] diff --git a/packages/sdks/compatibility-layer/src/__integration__/multi-client.test.ts b/packages/sdks/compatibility-layer/src/__integration__/multi-client.test.ts new file mode 100644 index 00000000..a4721891 --- /dev/null +++ b/packages/sdks/compatibility-layer/src/__integration__/multi-client.test.ts @@ -0,0 +1,307 @@ +import { describe, it, expect, beforeAll, afterAll, beforeEach } from "vitest" +import { client as shopperClient } from "@epcc-sdk/sdks-shopper" +import { client as pimClient } from "@epcc-sdk/sdks-pxm" +import { + createBridgedClient, + createLegacyStorageBridge, + clientRegistry, +} from "../index" +import { + setupMswServer, + teardownMswServer, + resetMswHandlers, + getRequestCounts, + mockTokenData, + mockClientCredentialsToken, +} from "./setup" + +describe("Multi-Client Integration", () => { + beforeAll(() => { + setupMswServer() + }) + + afterAll(() => { + teardownMswServer() + }) + + beforeEach(() => { + resetMswHandlers() + // Clear the client registry between tests + clientRegistry.remove("shopper") + clientRegistry.remove("admin") + clientRegistry.remove("pim") + }) + + describe("Independent client instances", () => { + it("should maintain separate auth states for shopper and PIM clients", async () => { + // Create shopper client (implicit grant) + const shopperStorage = createLegacyStorageBridge({ + backend: "memory", + name: "shopper", + }) + const { auth: shopperAuth } = createBridgedClient(shopperClient, { + baseUrl: "https://api.elasticpath.com", + clientId: "shopper-client-id", + storage: shopperStorage, + }) + + // Create PIM client (client_credentials grant) + const pimStorage = createLegacyStorageBridge({ + backend: "memory", + name: "pim", + }) + const { auth: pimAuth } = createBridgedClient(pimClient, { + baseUrl: "https://api.elasticpath.com", + clientId: "admin-client-id", + clientSecret: "admin-secret", + storage: pimStorage, + }) + + // Get tokens for both clients + const shopperToken = await shopperAuth.getValidAccessToken() + const pimToken = await pimAuth.getValidAccessToken() + + // Tokens should be different (implicit vs client_credentials) + expect(shopperToken).toBe(mockTokenData.access_token) + expect(pimToken).toBe(mockClientCredentialsToken.access_token) + expect(shopperToken).not.toBe(pimToken) + + // Both should have made auth requests + const { authRequestCount } = getRequestCounts() + expect(authRequestCount).toBe(2) + }) + + it("should not affect other clients when one refreshes", async () => { + const shopperStorage = createLegacyStorageBridge({ + backend: "memory", + name: "shopper", + }) + const { auth: shopperAuth } = createBridgedClient(shopperClient, { + baseUrl: "https://api.elasticpath.com", + clientId: "shopper-client-id", + storage: shopperStorage, + }) + + const pimStorage = createLegacyStorageBridge({ + backend: "memory", + name: "pim", + }) + const { auth: pimAuth } = createBridgedClient(pimClient, { + baseUrl: "https://api.elasticpath.com", + clientId: "admin-client-id", + clientSecret: "admin-secret", + storage: pimStorage, + }) + + // Initial auth for both + await shopperAuth.getValidAccessToken() + await pimAuth.getValidAccessToken() + + const { authRequestCount: initialCount } = getRequestCounts() + expect(initialCount).toBe(2) + + // Refresh only shopper + await shopperAuth.refresh() + + const { authRequestCount: afterRefresh } = getRequestCounts() + expect(afterRefresh).toBe(3) // Only one additional request + + // PIM auth should still have its token + expect(pimAuth.getSnapshot()).toBe(mockClientCredentialsToken.access_token) + }) + }) + + describe("Client registry", () => { + it("should create and manage multiple named clients", async () => { + // Create shopper client via registry + const { auth: shopperAuth } = clientRegistry.getOrCreate({ + name: "shopper", + authType: "implicit", + baseUrl: "https://api.elasticpath.com", + clientId: "shopper-client-id", + storage: "memory", + }) + + // Create admin client via registry + const { auth: adminAuth } = clientRegistry.getOrCreate({ + name: "admin", + authType: "client_credentials", + baseUrl: "https://api.elasticpath.com", + clientId: "admin-client-id", + clientSecret: "admin-secret", + storage: "memory", + }) + + // Get tokens + await shopperAuth.getValidAccessToken() + await adminAuth.getValidAccessToken() + + // Should have both auth requests + const { authRequestCount } = getRequestCounts() + expect(authRequestCount).toBe(2) + }) + + it("should return same client instance for same name", async () => { + const { fetch: fetch1, auth: auth1 } = clientRegistry.getOrCreate({ + name: "shopper", + authType: "implicit", + baseUrl: "https://api.elasticpath.com", + clientId: "shopper-client-id", + storage: "memory", + }) + + const { fetch: fetch2, auth: auth2 } = clientRegistry.getOrCreate({ + name: "shopper", + authType: "implicit", + baseUrl: "https://api.elasticpath.com", + clientId: "shopper-client-id", + storage: "memory", + }) + + // Should be same instance + expect(fetch1).toBe(fetch2) + expect(auth1).toBe(auth2) + }) + + it("should remove client from registry", async () => { + const { auth: auth1 } = clientRegistry.getOrCreate({ + name: "temp-client", + authType: "implicit", + baseUrl: "https://api.elasticpath.com", + clientId: "temp-client-id", + storage: "memory", + }) + + await auth1.getValidAccessToken() + expect(auth1.getSnapshot()).toBe(mockTokenData.access_token) + + // Remove client + clientRegistry.remove("temp-client") + + // Getting client with same name should create new instance + const { auth: auth2 } = clientRegistry.getOrCreate({ + name: "temp-client", + authType: "implicit", + baseUrl: "https://api.elasticpath.com", + clientId: "temp-client-id", + storage: "memory", + }) + + // New instance should not have token + expect(auth2.getSnapshot()).toBeUndefined() + expect(auth1).not.toBe(auth2) + }) + }) + + describe("Concurrent requests across clients", () => { + it("should handle concurrent requests from multiple clients", async () => { + const shopperStorage = createLegacyStorageBridge({ + backend: "memory", + name: "shopper", + }) + const { auth: shopperAuth } = createBridgedClient(shopperClient, { + baseUrl: "https://api.elasticpath.com", + clientId: "shopper-client-id", + storage: shopperStorage, + }) + + const pimStorage = createLegacyStorageBridge({ + backend: "memory", + name: "pim", + }) + const { auth: pimAuth } = createBridgedClient(pimClient, { + baseUrl: "https://api.elasticpath.com", + clientId: "admin-client-id", + clientSecret: "admin-secret", + storage: pimStorage, + }) + + // Make concurrent requests from both clients + const [shopperToken, pimToken, shopperToken2, pimToken2] = + await Promise.all([ + shopperAuth.getValidAccessToken(), + pimAuth.getValidAccessToken(), + shopperAuth.getValidAccessToken(), + pimAuth.getValidAccessToken(), + ]) + + // All shopper requests should get same token + expect(shopperToken).toBe(shopperToken2) + expect(shopperToken).toBe(mockTokenData.access_token) + + // All PIM requests should get same token + expect(pimToken).toBe(pimToken2) + expect(pimToken).toBe(mockClientCredentialsToken.access_token) + + // Should only have 2 auth requests (one per client, deduplicated) + const { authRequestCount } = getRequestCounts() + expect(authRequestCount).toBe(2) + }) + }) + + describe("Different storage backends", () => { + it("should support memory storage for server-side clients", async () => { + const { auth } = createBridgedClient(pimClient, { + baseUrl: "https://api.elasticpath.com", + clientId: "admin-client-id", + clientSecret: "admin-secret", + storage: "memory", + }) + + const token = await auth.getValidAccessToken() + expect(token).toBe(mockClientCredentialsToken.access_token) + }) + + it("should support custom storage adapter", async () => { + const customStorage: Record = {} + const adapter = { + get: () => customStorage["token"] ?? null, + set: (value: string) => { + customStorage["token"] = value + }, + delete: () => { + customStorage["token"] = null + }, + } + + const { auth } = createBridgedClient(shopperClient, { + baseUrl: "https://api.elasticpath.com", + clientId: "test-client-id", + storage: adapter, + }) + + await auth.getValidAccessToken() + + // Token should be stored in custom storage + expect(customStorage["token"]).toBeDefined() + const parsed = JSON.parse(customStorage["token"]!) + expect(parsed.access_token).toBe(mockTokenData.access_token) + }) + }) + + describe("Token provider customization", () => { + it("should support custom token provider for SSO", async () => { + const customTokenProvider = async () => ({ + access_token: "custom-sso-token", + expires: Math.floor(Date.now() / 1000) + 3600, + token_type: "Bearer" as const, + }) + + const { auth } = createBridgedClient(shopperClient, { + baseUrl: "https://api.elasticpath.com", + clientId: "test-client-id", + storage: "memory", + tokenProvider: customTokenProvider, + }) + + const token = await auth.getValidAccessToken() + + // Should use custom token provider + expect(token).toBe("custom-sso-token") + + // Should NOT have made any auth requests to the server + const { authRequestCount } = getRequestCounts() + expect(authRequestCount).toBe(0) + }) + }) +}) diff --git a/packages/sdks/compatibility-layer/src/__integration__/setup.ts b/packages/sdks/compatibility-layer/src/__integration__/setup.ts new file mode 100644 index 00000000..ab8e212c --- /dev/null +++ b/packages/sdks/compatibility-layer/src/__integration__/setup.ts @@ -0,0 +1,37 @@ +import { setupServer } from "msw/node" +import { handlers, resetRequestCounts } from "./handlers" + +// Create MSW server for Node.js environment +export const server = setupServer(...handlers) + +// Setup function to be called in beforeAll +export function setupMswServer() { + // Start the server before all tests + server.listen({ + onUnhandledRequest: "warn", + }) +} + +// Cleanup function to be called in afterAll +export function teardownMswServer() { + server.close() +} + +// Reset handlers between tests +export function resetMswHandlers() { + server.resetHandlers() + resetRequestCounts() +} + +// Re-export utilities from handlers +export { + resetRequestCounts, + getRequestCounts, + setReturn401, + setReturn429, + mockTokenData, + mockClientCredentialsToken, + mockProductsV2, + mockCatalogProducts, + mockPimProducts, +} from "./handlers" diff --git a/packages/sdks/compatibility-layer/src/__integration__/token-sharing.test.ts b/packages/sdks/compatibility-layer/src/__integration__/token-sharing.test.ts new file mode 100644 index 00000000..37ded9c1 --- /dev/null +++ b/packages/sdks/compatibility-layer/src/__integration__/token-sharing.test.ts @@ -0,0 +1,305 @@ +import { describe, it, expect, beforeAll, afterAll, beforeEach } from "vitest" +import { client as shopperClient } from "@epcc-sdk/sdks-shopper" +import { createBridgedClient, createLegacyStorageBridge } from "../index" +import { + setupMswServer, + teardownMswServer, + resetMswHandlers, + mockTokenData, + getRequestCounts, +} from "./setup" + +describe("Token Sharing Integration", () => { + // Use in-memory storage that mimics localStorage for testing + let storageData: Record = {} + + const mockLocalStorage = { + getItem: (key: string) => storageData[key] ?? null, + setItem: (key: string, value: string) => { + storageData[key] = value + }, + removeItem: (key: string) => { + delete storageData[key] + }, + clear: () => { + storageData = {} + }, + } + + beforeAll(() => { + // Setup MSW server + setupMswServer() + + // Mock localStorage globally + Object.defineProperty(globalThis, "localStorage", { + value: mockLocalStorage, + writable: true, + }) + }) + + afterAll(() => { + teardownMswServer() + }) + + beforeEach(() => { + resetMswHandlers() + storageData = {} + }) + + describe("Old SDK stores token, New SDK reads it", () => { + it("should allow new SDK to use token stored by old SDK", async () => { + // Simulate old SDK storing a token + const oldSdkCredentials = { + client_id: "test-client-id", + access_token: mockTokenData.access_token, + expires: mockTokenData.expires, + expires_in: mockTokenData.expires_in, + identifier: "implicit", + token_type: "Bearer", + } + storageData["epCredentials"] = JSON.stringify(oldSdkCredentials) + + // Create bridged client that reads from the same storage + const storage = createLegacyStorageBridge({ + backend: "memory", + }) + // Manually set the storage to mimic the shared localStorage + storage.set(JSON.stringify(oldSdkCredentials)) + + const { auth } = createBridgedClient(shopperClient, { + baseUrl: "https://api.elasticpath.com", + clientId: "test-client-id", + storage, + }) + + // New SDK should be able to get the token without making an auth request + const token = await auth.getValidAccessToken() + + expect(token).toBe(mockTokenData.access_token) + }) + + it("should share the same storage key format", async () => { + // Old SDK uses 'epCredentials' by default + const storage = createLegacyStorageBridge({ + key: "epCredentials", + backend: "memory", + }) + + // The storage adapter should use the same key + storage.set(JSON.stringify({ access_token: "shared-token", expires: Date.now() / 1000 + 3600 })) + + const storedValue = storage.get() + expect(storedValue).toBeDefined() + + const parsed = JSON.parse(storedValue!) + expect(parsed.access_token).toBe("shared-token") + }) + }) + + describe("Token format compatibility", () => { + it("should handle old SDK token format with all fields", () => { + const oldSdkFormat = { + client_id: "test-client-id", + access_token: "old-format-token", + expires: Math.floor(Date.now() / 1000) + 3600, + expires_in: 3600, + identifier: "implicit", + token_type: "Bearer", + } + + const storage = createLegacyStorageBridge({ backend: "memory" }) + storage.set(JSON.stringify(oldSdkFormat)) + + const { auth } = createBridgedClient(shopperClient, { + baseUrl: "https://api.elasticpath.com", + clientId: "test-client-id", + storage, + }) + + // Should be able to read the token + expect(auth.getSnapshot()).toBe("old-format-token") + expect(auth.isExpired()).toBe(false) + }) + + it("should handle minimal token format", () => { + const minimalFormat = { + access_token: "minimal-token", + } + + const storage = createLegacyStorageBridge({ backend: "memory" }) + storage.set(JSON.stringify(minimalFormat)) + + const { auth } = createBridgedClient(shopperClient, { + baseUrl: "https://api.elasticpath.com", + clientId: "test-client-id", + storage, + }) + + expect(auth.getSnapshot()).toBe("minimal-token") + // Without expires, should be considered expired + expect(auth.isExpired()).toBe(true) + }) + + it("should write tokens in format compatible with old SDK", async () => { + const storage = createLegacyStorageBridge({ backend: "memory" }) + + const { auth } = createBridgedClient(shopperClient, { + baseUrl: "https://api.elasticpath.com", + clientId: "test-client-id", + storage, + }) + + // Trigger a token refresh which will write to storage + await auth.refresh() + + const storedValue = storage.get() + expect(storedValue).toBeDefined() + + const parsed = JSON.parse(storedValue!) + + // Should have the fields old SDK expects + expect(parsed.access_token).toBeDefined() + expect(parsed.expires).toBeDefined() + expect(typeof parsed.expires).toBe("number") + }) + }) + + describe("Named gateway support", () => { + it("should support named storage keys like old SDK", () => { + // Old SDK with name: gateway({ name: 'myapp', ... }) + // stores to 'myapp_ep_credentials' + const namedStorage = createLegacyStorageBridge({ + name: "myapp", + backend: "memory", + }) + + namedStorage.set(JSON.stringify({ + access_token: "named-token", + expires: Math.floor(Date.now() / 1000) + 3600, + })) + + const { auth } = createBridgedClient(shopperClient, { + baseUrl: "https://api.elasticpath.com", + clientId: "test-client-id", + storage: namedStorage, + }) + + expect(auth.getSnapshot()).toBe("named-token") + }) + }) + + describe("Concurrent token refresh deduplication", () => { + it("should only make one auth request for concurrent refreshes", async () => { + const storage = createLegacyStorageBridge({ backend: "memory" }) + + const { auth } = createBridgedClient(shopperClient, { + baseUrl: "https://api.elasticpath.com", + clientId: "test-client-id", + storage, + }) + + // Trigger multiple concurrent refreshes + const [token1, token2, token3] = await Promise.all([ + auth.refresh(), + auth.refresh(), + auth.refresh(), + ]) + + // All should return the same token + expect(token1).toBe(token2) + expect(token2).toBe(token3) + + // Should only have made one auth request + const { authRequestCount } = getRequestCounts() + expect(authRequestCount).toBe(1) + }) + + it("should handle concurrent re-auth from multiple clients sharing same storage", async () => { + // Create a SHARED storage adapter (simulating old SDK + new SDK sharing localStorage) + const sharedStorage = createLegacyStorageBridge({ backend: "memory" }) + + // Pre-populate with an EXPIRED token + sharedStorage.set( + JSON.stringify({ + access_token: "expired-token", + expires: Math.floor(Date.now() / 1000) - 100, // Expired 100 seconds ago + }) + ) + + // Create TWO bridged clients sharing the SAME storage + // This simulates old SDK and new SDK both detecting expired token + const { auth: client1Auth } = createBridgedClient(shopperClient, { + baseUrl: "https://api.elasticpath.com", + clientId: "test-client-id", + storage: sharedStorage, + }) + + const { auth: client2Auth } = createBridgedClient(shopperClient, { + baseUrl: "https://api.elasticpath.com", + clientId: "test-client-id", + storage: sharedStorage, + }) + + // Both clients detect expired token and try to refresh concurrently + const [token1, token2] = await Promise.all([ + client1Auth.getValidAccessToken(), + client2Auth.getValidAccessToken(), + ]) + + // Both should end up with the same valid token + expect(token1).toBe(mockTokenData.access_token) + expect(token2).toBe(mockTokenData.access_token) + + // Ideally only 1-2 auth requests (each client has its own deduplication) + // Since they're separate SharedAuthState instances, each may make one request + // but they should both end up reading the same token from storage + const { authRequestCount } = getRequestCounts() + expect(authRequestCount).toBeLessThanOrEqual(2) + + // Verify storage has the new token + const storedValue = sharedStorage.get() + const parsed = JSON.parse(storedValue!) + expect(parsed.access_token).toBe(mockTokenData.access_token) + }) + + it("should allow second client to use token refreshed by first client", async () => { + const sharedStorage = createLegacyStorageBridge({ backend: "memory" }) + + // Start with expired token + sharedStorage.set( + JSON.stringify({ + access_token: "old-expired-token", + expires: Math.floor(Date.now() / 1000) - 100, + }) + ) + + // First client refreshes the token + const { auth: client1Auth } = createBridgedClient(shopperClient, { + baseUrl: "https://api.elasticpath.com", + clientId: "test-client-id", + storage: sharedStorage, + }) + + const token1 = await client1Auth.getValidAccessToken() + expect(token1).toBe(mockTokenData.access_token) + + const { authRequestCount: countAfterFirst } = getRequestCounts() + expect(countAfterFirst).toBe(1) + + // Second client should read the refreshed token from storage + // without making another auth request + const { auth: client2Auth } = createBridgedClient(shopperClient, { + baseUrl: "https://api.elasticpath.com", + clientId: "test-client-id", + storage: sharedStorage, + }) + + const token2 = await client2Auth.getValidAccessToken() + expect(token2).toBe(mockTokenData.access_token) + + // Should NOT have made another auth request + const { authRequestCount: countAfterSecond } = getRequestCounts() + expect(countAfterSecond).toBe(1) + }) + }) +}) diff --git a/packages/sdks/compatibility-layer/src/auth/legacy-storage-bridge.test.ts b/packages/sdks/compatibility-layer/src/auth/legacy-storage-bridge.test.ts new file mode 100644 index 00000000..5f15da1b --- /dev/null +++ b/packages/sdks/compatibility-layer/src/auth/legacy-storage-bridge.test.ts @@ -0,0 +1,234 @@ +import { describe, it, expect, vi, beforeEach, afterEach } from "vitest" +import { + createLegacyStorageBridge, + storageAdapters, +} from "./legacy-storage-bridge" + +describe("createLegacyStorageBridge", () => { + describe("key resolution", () => { + it("should use default key when no options provided", () => { + const storage = createLegacyStorageBridge({ backend: "memory" }) + + storage.set("test-value") + + // Memory adapter stores internally, but we can verify it works + expect(storage.get()).toBe("test-value") + }) + + it("should use custom key when provided", () => { + const storage = createLegacyStorageBridge({ + key: "custom_key", + backend: "memory", + }) + + storage.set("test-value") + expect(storage.get()).toBe("test-value") + }) + + it("should use name pattern when name is provided", () => { + const storage = createLegacyStorageBridge({ + name: "myapp", + backend: "memory", + }) + + // Should use key pattern: ${name}_ep_credentials + storage.set("test-value") + expect(storage.get()).toBe("test-value") + }) + }) + + describe("memory backend", () => { + it("should store and retrieve values", () => { + const storage = createLegacyStorageBridge({ backend: "memory" }) + + storage.set("test-value") + expect(storage.get()).toBe("test-value") + }) + + it("should clear values when set to undefined", () => { + const storage = createLegacyStorageBridge({ backend: "memory" }) + + storage.set("test-value") + storage.set(undefined) + + expect(storage.get()).toBeUndefined() + }) + + it("should support subscriptions", () => { + const storage = createLegacyStorageBridge({ backend: "memory" }) + const callback = vi.fn() + + const unsubscribe = storage.subscribe!(callback) + + storage.set("new-value") + expect(callback).toHaveBeenCalledTimes(1) + + unsubscribe() + storage.set("another-value") + expect(callback).toHaveBeenCalledTimes(1) // Should not be called again + }) + }) + + describe("localStorage backend", () => { + const mockLocalStorage: Record = {} + + beforeEach(() => { + // Clear mock storage + Object.keys(mockLocalStorage).forEach( + (key) => delete mockLocalStorage[key] + ) + + // Mock localStorage + vi.stubGlobal("window", { + localStorage: { + getItem: vi.fn((key: string) => mockLocalStorage[key] ?? null), + setItem: vi.fn((key: string, value: string) => { + mockLocalStorage[key] = value + }), + removeItem: vi.fn((key: string) => { + delete mockLocalStorage[key] + }), + }, + addEventListener: vi.fn(), + removeEventListener: vi.fn(), + }) + }) + + afterEach(() => { + vi.unstubAllGlobals() + }) + + it("should store and retrieve values from localStorage", () => { + const storage = createLegacyStorageBridge({ backend: "localStorage" }) + + storage.set("test-value") + expect(window.localStorage.setItem).toHaveBeenCalledWith( + "epCredentials", + "test-value" + ) + }) + + it("should remove value when set to undefined", () => { + const storage = createLegacyStorageBridge({ backend: "localStorage" }) + + storage.set(undefined) + expect(window.localStorage.removeItem).toHaveBeenCalledWith( + "epCredentials" + ) + }) + + it("should subscribe to storage events", () => { + const storage = createLegacyStorageBridge({ backend: "localStorage" }) + const callback = vi.fn() + + storage.subscribe!(callback) + + expect(window.addEventListener).toHaveBeenCalledWith( + "storage", + expect.any(Function) + ) + }) + + it("should unsubscribe from storage events", () => { + const storage = createLegacyStorageBridge({ backend: "localStorage" }) + const callback = vi.fn() + + const unsubscribe = storage.subscribe!(callback) + unsubscribe() + + expect(window.removeEventListener).toHaveBeenCalledWith( + "storage", + expect.any(Function) + ) + }) + }) + + describe("cookie backend", () => { + beforeEach(() => { + vi.stubGlobal("document", { + cookie: "", + }) + }) + + afterEach(() => { + vi.unstubAllGlobals() + }) + + it("should write cookies with proper format", () => { + const storage = createLegacyStorageBridge({ + backend: "cookie", + cookie: { secure: true }, + }) + + storage.set("test-value") + + expect(document.cookie).toContain("epCredentials=test-value") + expect(document.cookie).toContain("Secure") + }) + + it("should read cookies", () => { + Object.defineProperty(document, "cookie", { + get: () => "epCredentials=stored-value; other=cookie", + set: vi.fn(), + configurable: true, + }) + + const storage = createLegacyStorageBridge({ backend: "cookie" }) + + expect(storage.get()).toBe("stored-value") + }) + + it("should clear cookies by setting Max-Age=0", () => { + let cookieValue = "" + Object.defineProperty(document, "cookie", { + get: () => cookieValue, + set: (value: string) => { + cookieValue = value + }, + configurable: true, + }) + + const storage = createLegacyStorageBridge({ backend: "cookie" }) + + storage.set(undefined) + + expect(cookieValue).toContain("Max-Age=0") + }) + + it("should not have subscribe method", () => { + const storage = createLegacyStorageBridge({ backend: "cookie" }) + + // Cookie adapter doesn't support reliable cross-tab change detection + expect(storage.subscribe).toBeUndefined() + }) + }) + + describe("error handling", () => { + it("should throw on unknown backend", () => { + expect(() => + createLegacyStorageBridge({ backend: "unknown" as any }) + ).toThrow("Unknown storage backend: unknown") + }) + }) +}) + +describe("storageAdapters", () => { + it("should export localStorage adapter factory", () => { + expect(typeof storageAdapters.localStorage).toBe("function") + }) + + it("should export cookie adapter factory", () => { + expect(typeof storageAdapters.cookie).toBe("function") + }) + + it("should export memory adapter factory", () => { + expect(typeof storageAdapters.memory).toBe("function") + }) + + describe("memory adapter", () => { + it("should accept initial value", () => { + const adapter = storageAdapters.memory("initial-value") + expect(adapter.get()).toBe("initial-value") + }) + }) +}) diff --git a/packages/sdks/compatibility-layer/src/auth/legacy-storage-bridge.ts b/packages/sdks/compatibility-layer/src/auth/legacy-storage-bridge.ts new file mode 100644 index 00000000..f00ad206 --- /dev/null +++ b/packages/sdks/compatibility-layer/src/auth/legacy-storage-bridge.ts @@ -0,0 +1,206 @@ +import type { StorageAdapter } from "../types" +import { DEFAULT_LEGACY_STORAGE_KEY } from "../types" + +export interface LegacyStorageBridgeOptions { + /** + * Storage key to use. + * If name is provided, uses `${name}_ep_credentials` (matching old SDK). + * Otherwise uses 'epCredentials' (old SDK default). + */ + key?: string + /** + * Named gateway configuration (old SDK pattern). + * If provided, key becomes `${name}_ep_credentials`. + */ + name?: string + /** + * Storage backend to use. + * - 'localStorage': Browser localStorage (default) + * - 'cookie': JS-readable cookies + * - 'memory': In-memory storage (for SSR/tests) + */ + backend?: "localStorage" | "cookie" | "memory" + /** + * Cookie options (only used if backend is 'cookie') + */ + cookie?: { + path?: string + domain?: string + sameSite?: "Lax" | "Strict" | "None" + secure?: boolean + maxAge?: number + } +} + +/** + * Resolve the storage key for old SDK compatibility. + * Matches: ep-js-sdk/src/utils/helpers.js resolveCredentialsStorageKey + */ +function resolveStorageKey(options: LegacyStorageBridgeOptions): string { + if (options.key) { + return options.key + } + if (options.name) { + return `${options.name}_ep_credentials` + } + return DEFAULT_LEGACY_STORAGE_KEY +} + +/** + * Create a localStorage-based storage adapter. + * Includes cross-tab sync via 'storage' event. + */ +function createLocalStorageAdapter(key: string): StorageAdapter { + const subscribers = new Set<() => void>() + + const onStorageEvent = (e: StorageEvent) => { + if (e.key === key) { + subscribers.forEach((fn) => fn()) + } + } + + const safeGet = (): string | undefined => { + try { + if (typeof window === "undefined" || !("localStorage" in window)) { + return undefined + } + return window.localStorage.getItem(key) ?? undefined + } catch { + return undefined + } + } + + const safeSet = (value?: string): void => { + try { + if (typeof window === "undefined" || !("localStorage" in window)) { + return + } + if (!value) { + window.localStorage.removeItem(key) + } else { + window.localStorage.setItem(key, value) + } + } catch { + // Storage may be disabled or full + } + } + + return { + get: safeGet, + set: safeSet, + subscribe(cb) { + subscribers.add(cb) + if (typeof window !== "undefined") { + window.addEventListener("storage", onStorageEvent) + } + return () => { + subscribers.delete(cb) + if (subscribers.size === 0 && typeof window !== "undefined") { + window.removeEventListener("storage", onStorageEvent) + } + } + }, + } +} + +/** + * Create a cookie-based storage adapter. + * Note: These are JS-readable cookies, not httpOnly. + */ +function createCookieAdapter( + key: string, + options: LegacyStorageBridgeOptions["cookie"] = {} +): StorageAdapter { + const { path = "/", sameSite = "Lax", domain, secure, maxAge } = options + + const read = (): string | undefined => { + if (typeof document === "undefined") return undefined + const raw = document.cookie + .split("; ") + .find((c) => c.startsWith(key + "=")) + if (!raw) return undefined + try { + return decodeURIComponent(raw.split("=").slice(1).join("=")) + } catch { + return raw.split("=").slice(1).join("=") + } + } + + const write = (value: string): void => { + if (typeof document === "undefined") return + let cookie = `${key}=${encodeURIComponent(value)}; Path=${path}; SameSite=${sameSite}` + if (domain) cookie += `; Domain=${domain}` + if (secure) cookie += `; Secure` + if (maxAge) cookie += `; Max-Age=${maxAge}` + document.cookie = cookie + } + + const clear = (): void => { + if (typeof document === "undefined") return + document.cookie = `${key}=; Max-Age=0; Path=${path}` + } + + return { + get: read, + set: (value) => (value ? write(value) : clear()), + // No reliable cookie change event across tabs + } +} + +/** + * Create an in-memory storage adapter. + * Useful for SSR and testing. + */ +function createMemoryAdapter(initialValue?: string): StorageAdapter { + let value = initialValue + const subscribers = new Set<() => void>() + + return { + get: () => value, + set: (v) => { + value = v + subscribers.forEach((fn) => fn()) + }, + subscribe(cb) { + subscribers.add(cb) + return () => subscribers.delete(cb) + }, + } +} + +/** + * Create a storage adapter that bridges to old SDK's storage location. + * + * This allows the new SDKs to read/write tokens from the same storage + * location that the old SDK uses, enabling seamless token sharing. + * + * The storage key format matches the old SDK: + * - Default: 'epCredentials' + * - Named: '${name}_ep_credentials' + */ +export function createLegacyStorageBridge( + options: LegacyStorageBridgeOptions = {} +): StorageAdapter { + const key = resolveStorageKey(options) + const backend = options.backend ?? "localStorage" + + switch (backend) { + case "localStorage": + return createLocalStorageAdapter(key) + case "cookie": + return createCookieAdapter(key, options.cookie) + case "memory": + return createMemoryAdapter() + default: + throw new Error(`Unknown storage backend: ${backend}`) + } +} + +/** + * Storage adapter factories for direct use. + */ +export const storageAdapters = { + localStorage: createLocalStorageAdapter, + cookie: createCookieAdapter, + memory: createMemoryAdapter, +} diff --git a/packages/sdks/compatibility-layer/src/auth/shared-auth-state.test.ts b/packages/sdks/compatibility-layer/src/auth/shared-auth-state.test.ts new file mode 100644 index 00000000..634b69f4 --- /dev/null +++ b/packages/sdks/compatibility-layer/src/auth/shared-auth-state.test.ts @@ -0,0 +1,363 @@ +import { describe, it, expect, vi, beforeEach, afterEach } from "vitest" +import { SharedAuthState } from "./shared-auth-state" +import type { StorageAdapter, TokenData, TokenProvider } from "../types" + +describe("SharedAuthState", () => { + let mockStorage: StorageAdapter + let mockTokenProvider: TokenProvider + let storedValue: string | undefined + + beforeEach(() => { + storedValue = undefined + mockStorage = { + get: vi.fn(() => storedValue), + set: vi.fn((value) => { + storedValue = value + }), + subscribe: vi.fn().mockReturnValue(() => {}), + } + + mockTokenProvider = vi.fn().mockResolvedValue({ + access_token: "new-token", + expires: Math.floor(Date.now() / 1000) + 3600, + }) + }) + + afterEach(() => { + vi.clearAllMocks() + }) + + describe("constructor", () => { + it("should load credentials from storage on initialization", () => { + const tokenData: TokenData = { + access_token: "stored-token", + expires: Math.floor(Date.now() / 1000) + 3600, + } + storedValue = JSON.stringify(tokenData) + + const authState = new SharedAuthState({ + storage: mockStorage, + tokenProvider: mockTokenProvider, + }) + + expect(authState.getSnapshot()).toBe("stored-token") + }) + + it("should handle legacy plain token format", () => { + storedValue = "plain-token" + + const authState = new SharedAuthState({ + storage: mockStorage, + tokenProvider: mockTokenProvider, + }) + + expect(authState.getSnapshot()).toBe("plain-token") + }) + + it("should subscribe to storage changes", () => { + new SharedAuthState({ + storage: mockStorage, + tokenProvider: mockTokenProvider, + }) + + expect(mockStorage.subscribe).toHaveBeenCalled() + }) + }) + + describe("isExpired", () => { + it("should return true when no credentials", () => { + const authState = new SharedAuthState({ + storage: mockStorage, + tokenProvider: mockTokenProvider, + }) + + expect(authState.isExpired()).toBe(true) + }) + + it("should return false when token is not expired", () => { + const tokenData: TokenData = { + access_token: "valid-token", + expires: Math.floor(Date.now() / 1000) + 3600, // 1 hour from now + } + storedValue = JSON.stringify(tokenData) + + const authState = new SharedAuthState({ + storage: mockStorage, + tokenProvider: mockTokenProvider, + }) + + expect(authState.isExpired()).toBe(false) + }) + + it("should return true when token is expired", () => { + const tokenData: TokenData = { + access_token: "expired-token", + expires: Math.floor(Date.now() / 1000) - 100, // 100 seconds ago + } + storedValue = JSON.stringify(tokenData) + + const authState = new SharedAuthState({ + storage: mockStorage, + tokenProvider: mockTokenProvider, + }) + + expect(authState.isExpired()).toBe(true) + }) + + it("should respect leeway setting", () => { + const tokenData: TokenData = { + access_token: "almost-expired-token", + expires: Math.floor(Date.now() / 1000) + 30, // 30 seconds from now + } + storedValue = JSON.stringify(tokenData) + + const authState = new SharedAuthState({ + storage: mockStorage, + tokenProvider: mockTokenProvider, + leewaySec: 60, // 60 second leeway + }) + + // Token expires in 30s but leeway is 60s, so should be considered expired + expect(authState.isExpired()).toBe(true) + }) + + it("should consider token expired when no expires field present", () => { + // Token without an expires field should be considered expired + // (EP tokens always have an expires field, this handles edge cases) + const tokenData: TokenData = { + access_token: "token-without-expires", + } + storedValue = JSON.stringify(tokenData) + + const authState = new SharedAuthState({ + storage: mockStorage, + tokenProvider: mockTokenProvider, + }) + + expect(authState.isExpired()).toBe(true) + }) + }) + + describe("getValidAccessToken", () => { + it("should return cached token if valid", async () => { + const tokenData: TokenData = { + access_token: "valid-token", + expires: Math.floor(Date.now() / 1000) + 3600, + } + storedValue = JSON.stringify(tokenData) + + const authState = new SharedAuthState({ + storage: mockStorage, + tokenProvider: mockTokenProvider, + }) + + const token = await authState.getValidAccessToken() + + expect(token).toBe("valid-token") + expect(mockTokenProvider).not.toHaveBeenCalled() + }) + + it("should refresh if token is expired", async () => { + const expiredTokenData: TokenData = { + access_token: "expired-token", + expires: Math.floor(Date.now() / 1000) - 100, + } + storedValue = JSON.stringify(expiredTokenData) + + const authState = new SharedAuthState({ + storage: mockStorage, + tokenProvider: mockTokenProvider, + }) + + const token = await authState.getValidAccessToken() + + expect(token).toBe("new-token") + expect(mockTokenProvider).toHaveBeenCalledTimes(1) + }) + }) + + describe("refresh", () => { + it("should call token provider and store result", async () => { + const authState = new SharedAuthState({ + storage: mockStorage, + tokenProvider: mockTokenProvider, + }) + + const token = await authState.refresh() + + expect(token).toBe("new-token") + expect(mockTokenProvider).toHaveBeenCalledTimes(1) + expect(mockStorage.set).toHaveBeenCalledTimes(1) + + // Verify the stored value contains the expected token + const storedValue = (mockStorage.set as ReturnType).mock.calls[0][0] + const parsed = JSON.parse(storedValue) + expect(parsed.access_token).toBe("new-token") + expect(typeof parsed.expires).toBe("number") + }) + + it("should deduplicate concurrent refresh calls", async () => { + const authState = new SharedAuthState({ + storage: mockStorage, + tokenProvider: mockTokenProvider, + }) + + // Call refresh multiple times concurrently + const [token1, token2, token3] = await Promise.all([ + authState.refresh(), + authState.refresh(), + authState.refresh(), + ]) + + expect(token1).toBe("new-token") + expect(token2).toBe("new-token") + expect(token3).toBe("new-token") + // Should only call provider once due to deduplication + expect(mockTokenProvider).toHaveBeenCalledTimes(1) + }) + + it("should pass current token to provider", async () => { + const tokenData: TokenData = { + access_token: "current-token", + expires: Math.floor(Date.now() / 1000) + 3600, + } + storedValue = JSON.stringify(tokenData) + + const authState = new SharedAuthState({ + storage: mockStorage, + tokenProvider: mockTokenProvider, + }) + + await authState.refresh() + + expect(mockTokenProvider).toHaveBeenCalledWith({ + current: "current-token", + }) + }) + }) + + describe("setToken", () => { + it("should set token and store in storage", () => { + const authState = new SharedAuthState({ + storage: mockStorage, + tokenProvider: mockTokenProvider, + }) + + const tokenData: TokenData = { + access_token: "manual-token", + expires: Math.floor(Date.now() / 1000) + 3600, + } + + authState.setToken(tokenData) + + expect(authState.getSnapshot()).toBe("manual-token") + expect(mockStorage.set).toHaveBeenCalledWith(JSON.stringify(tokenData)) + }) + + it("should notify subscribers", () => { + const authState = new SharedAuthState({ + storage: mockStorage, + tokenProvider: mockTokenProvider, + }) + + const subscriber = vi.fn() + authState.subscribe(subscriber) + + authState.setToken({ access_token: "new-token" }) + + expect(subscriber).toHaveBeenCalled() + }) + }) + + describe("clear", () => { + it("should clear credentials and storage", () => { + const tokenData: TokenData = { + access_token: "token-to-clear", + expires: Math.floor(Date.now() / 1000) + 3600, + } + storedValue = JSON.stringify(tokenData) + + const authState = new SharedAuthState({ + storage: mockStorage, + tokenProvider: mockTokenProvider, + }) + + authState.clear() + + expect(authState.getSnapshot()).toBeUndefined() + expect(mockStorage.set).toHaveBeenCalledWith(undefined) + }) + }) + + describe("subscribe", () => { + it("should call subscriber on auth changes", () => { + const authState = new SharedAuthState({ + storage: mockStorage, + tokenProvider: mockTokenProvider, + }) + + const subscriber = vi.fn() + authState.subscribe(subscriber) + + authState.setToken({ access_token: "new-token" }) + + expect(subscriber).toHaveBeenCalledTimes(1) + }) + + it("should return unsubscribe function", () => { + const authState = new SharedAuthState({ + storage: mockStorage, + tokenProvider: mockTokenProvider, + }) + + const subscriber = vi.fn() + const unsubscribe = authState.subscribe(subscriber) + + unsubscribe() + authState.setToken({ access_token: "new-token" }) + + expect(subscriber).not.toHaveBeenCalled() + }) + }) + + describe("getCredentials", () => { + it("should return full credentials object", () => { + const tokenData: TokenData = { + access_token: "token", + expires: 12345, + client_id: "client-123", + } + storedValue = JSON.stringify(tokenData) + + const authState = new SharedAuthState({ + storage: mockStorage, + tokenProvider: mockTokenProvider, + }) + + expect(authState.getCredentials()).toEqual(tokenData) + }) + }) + + describe("dispose", () => { + it("should unsubscribe from storage and clear subscribers", () => { + const unsubscribeFn = vi.fn() + mockStorage.subscribe = vi.fn().mockReturnValue(unsubscribeFn) + + const authState = new SharedAuthState({ + storage: mockStorage, + tokenProvider: mockTokenProvider, + }) + + const subscriber = vi.fn() + authState.subscribe(subscriber) + + authState.dispose() + + expect(unsubscribeFn).toHaveBeenCalled() + + // Subscriber should not be called after dispose + authState.setToken({ access_token: "test" }) + expect(subscriber).not.toHaveBeenCalled() + }) + }) +}) diff --git a/packages/sdks/compatibility-layer/src/auth/shared-auth-state.ts b/packages/sdks/compatibility-layer/src/auth/shared-auth-state.ts new file mode 100644 index 00000000..e1ec6718 --- /dev/null +++ b/packages/sdks/compatibility-layer/src/auth/shared-auth-state.ts @@ -0,0 +1,192 @@ +import type { + StorageAdapter, + TokenData, + TokenProvider, + SharedAuthStateInterface, +} from "../types" + +export interface SharedAuthStateOptions { + /** Storage adapter for persisting tokens */ + storage: StorageAdapter + /** Function to obtain new tokens */ + tokenProvider: TokenProvider + /** Leeway in seconds for expiration check (default: 60) */ + leewaySec?: number +} + +/** + * SharedAuthState manages authentication state across SDKs. + * + * Key features: + * - Promise deduplication for concurrent refresh requests + * - Cross-tab synchronization via storage events + * - Compatible with old SDK's credential format + * + * Based on patterns from: + * - New SDK: packages/sdks/shopper/src/auth/kit.ts + * - Commerce Manager: authentication.service.ts (isRefreshing pattern) + */ +export class SharedAuthState implements SharedAuthStateInterface { + private storage: StorageAdapter + private tokenProvider: TokenProvider + private credentials: TokenData | undefined + private refreshPromise: Promise | undefined + private leewaySec: number + private subscribers: Set<() => void> = new Set() + private unsubscribeStorage?: () => void + + constructor(options: SharedAuthStateOptions) { + this.storage = options.storage + this.tokenProvider = options.tokenProvider + this.leewaySec = options.leewaySec ?? 60 + + // Load initial credentials from storage + this.loadFromStorage() + + // Subscribe to external storage changes (e.g., other tabs, old SDK updates) + if (this.storage.subscribe) { + this.unsubscribeStorage = this.storage.subscribe(() => { + this.loadFromStorage() + }) + } + } + + /** + * Load credentials from storage. + * Handles both new format (JSON object) and legacy format (plain token string). + */ + private loadFromStorage(): void { + const storedData = this.storage.get() + if (storedData) { + try { + const parsed = JSON.parse(storedData) + if (parsed.access_token) { + this.credentials = parsed + } + } catch { + // Not JSON, assume it's a legacy plain token + this.credentials = { access_token: storedData } + } + } else { + this.credentials = undefined + } + this.notifySubscribers() + } + + /** + * Notify all subscribers of auth state change. + */ + private notifySubscribers(): void { + this.subscribers.forEach((cb) => cb()) + } + + /** + * Check if the current token is expired. + */ + isExpired(): boolean { + if (!this.credentials?.access_token) return true + + const now = Math.floor(Date.now() / 1000) + + // Use the expires field if available (absolute timestamp) + if (this.credentials.expires) { + return now >= this.credentials.expires - this.leewaySec + } + + // If we can't determine expiration, consider it expired + return true + } + + /** + * Get a valid access token, refreshing if necessary. + */ + async getValidAccessToken(): Promise { + if (this.credentials?.access_token && !this.isExpired()) { + return this.credentials.access_token + } + return this.refresh() + } + + /** + * Force a token refresh. + * Deduplicates concurrent refresh calls (promise caching pattern). + */ + async refresh(): Promise { + // If already refreshing, return the existing promise + if (this.refreshPromise) { + return this.refreshPromise + } + + this.refreshPromise = (async () => { + try { + const tokenData = await this.tokenProvider({ + current: this.credentials?.access_token, + }) + this.credentials = tokenData + // Store in format compatible with old SDK + this.storage.set(JSON.stringify(tokenData)) + this.notifySubscribers() + return tokenData.access_token + } finally { + this.refreshPromise = undefined + } + })() + + return this.refreshPromise + } + + /** + * Manually set token data. + * Use this for external auth integration (e.g., password flow, SSO). + */ + setToken(tokenData: TokenData): void { + this.credentials = tokenData + this.storage.set(JSON.stringify(tokenData)) + this.notifySubscribers() + } + + /** + * Clear stored credentials. + */ + clear(): void { + this.credentials = undefined + this.storage.set(undefined) + this.notifySubscribers() + } + + /** + * Get current token without validation. + */ + getSnapshot(): string | undefined { + return this.credentials?.access_token + } + + /** + * Subscribe to auth state changes. + * Returns an unsubscribe function. + */ + subscribe(callback: () => void): () => void { + this.subscribers.add(callback) + return () => { + this.subscribers.delete(callback) + } + } + + /** + * Get the full credentials object. + */ + getCredentials(): TokenData | undefined { + return this.credentials + } + + /** + * Cleanup subscriptions. + * Call this when disposing of the auth state. + */ + dispose(): void { + if (this.unsubscribeStorage) { + this.unsubscribeStorage() + } + this.subscribers.clear() + } +} diff --git a/packages/sdks/compatibility-layer/src/client/client-registry.test.ts b/packages/sdks/compatibility-layer/src/client/client-registry.test.ts new file mode 100644 index 00000000..ad28a230 --- /dev/null +++ b/packages/sdks/compatibility-layer/src/client/client-registry.test.ts @@ -0,0 +1,316 @@ +import { describe, it, expect, vi, beforeEach, afterEach } from "vitest" +import { ClientRegistry, clientRegistry } from "./client-registry" + +describe("ClientRegistry", () => { + let registry: ClientRegistry + + beforeEach(() => { + registry = new ClientRegistry() + + // Mock global fetch for token providers + vi.stubGlobal( + "fetch", + vi.fn().mockResolvedValue( + new Response( + JSON.stringify({ + access_token: "test-token", + expires: Math.floor(Date.now() / 1000) + 3600, + }), + { status: 200 } + ) + ) + ) + }) + + afterEach(() => { + registry.clear() + vi.unstubAllGlobals() + vi.clearAllMocks() + }) + + describe("getOrCreate", () => { + it("should create a new client instance", () => { + const instance = registry.getOrCreate({ + name: "test-client", + authType: "implicit", + baseUrl: "https://api.test.com", + clientId: "test-client-id", + storage: "memory", + }) + + expect(instance).toBeDefined() + expect(instance.fetch).toBeDefined() + expect(instance.auth).toBeDefined() + expect(instance.config.name).toBe("test-client") + }) + + it("should return existing client on subsequent calls", () => { + const config = { + name: "test-client", + authType: "implicit" as const, + baseUrl: "https://api.test.com", + clientId: "test-client-id", + storage: "memory" as const, + } + + const instance1 = registry.getOrCreate(config) + const instance2 = registry.getOrCreate(config) + + expect(instance1).toBe(instance2) + }) + + it("should create separate instances for different names", () => { + const instance1 = registry.getOrCreate({ + name: "client-1", + authType: "implicit", + baseUrl: "https://api.test.com", + clientId: "client-id-1", + storage: "memory", + }) + + const instance2 = registry.getOrCreate({ + name: "client-2", + authType: "implicit", + baseUrl: "https://api.test.com", + clientId: "client-id-2", + storage: "memory", + }) + + expect(instance1).not.toBe(instance2) + expect(instance1.config.name).toBe("client-1") + expect(instance2.config.name).toBe("client-2") + }) + + it("should support client_credentials auth type", () => { + const instance = registry.getOrCreate({ + name: "admin-client", + authType: "client_credentials", + baseUrl: "https://api.test.com", + clientId: "admin-client-id", + clientSecret: "admin-secret", + storage: "memory", + }) + + expect(instance).toBeDefined() + }) + + it("should throw for client_credentials without secret", () => { + expect(() => + registry.getOrCreate({ + name: "admin-client", + authType: "client_credentials", + baseUrl: "https://api.test.com", + clientId: "admin-client-id", + storage: "memory", + }) + ).toThrow("Client secret required") + }) + + it("should support password auth type (manual token)", async () => { + const instance = registry.getOrCreate({ + name: "password-client", + authType: "password", + baseUrl: "https://api.test.com", + clientId: "client-id", + storage: "memory", + }) + + // Should throw when trying to auto-refresh + await expect(instance.auth.refresh()).rejects.toThrow( + "requires manual token setting" + ) + + // But should work with manual token + instance.auth.setToken({ + access_token: "manual-token", + expires: Math.floor(Date.now() / 1000) + 3600, + }) + + expect(instance.auth.getSnapshot()).toBe("manual-token") + }) + + it("should support sso auth type (manual token)", async () => { + const instance = registry.getOrCreate({ + name: "sso-client", + authType: "sso", + baseUrl: "https://api.test.com", + clientId: "client-id", + storage: "memory", + }) + + await expect(instance.auth.refresh()).rejects.toThrow( + "requires manual token setting" + ) + }) + + it("should support jwt auth type (manual token)", async () => { + const instance = registry.getOrCreate({ + name: "jwt-client", + authType: "jwt", + baseUrl: "https://api.test.com", + clientId: "client-id", + storage: "memory", + }) + + await expect(instance.auth.refresh()).rejects.toThrow( + "requires manual token setting" + ) + }) + }) + + describe("get", () => { + it("should return undefined for non-existent client", () => { + expect(registry.get("non-existent")).toBeUndefined() + }) + + it("should return existing client", () => { + registry.getOrCreate({ + name: "existing-client", + authType: "implicit", + baseUrl: "https://api.test.com", + clientId: "client-id", + storage: "memory", + }) + + const instance = registry.get("existing-client") + expect(instance).toBeDefined() + expect(instance?.config.name).toBe("existing-client") + }) + }) + + describe("has", () => { + it("should return false for non-existent client", () => { + expect(registry.has("non-existent")).toBe(false) + }) + + it("should return true for existing client", () => { + registry.getOrCreate({ + name: "existing-client", + authType: "implicit", + baseUrl: "https://api.test.com", + clientId: "client-id", + storage: "memory", + }) + + expect(registry.has("existing-client")).toBe(true) + }) + }) + + describe("remove", () => { + it("should return false for non-existent client", () => { + expect(registry.remove("non-existent")).toBe(false) + }) + + it("should remove and dispose client", () => { + const instance = registry.getOrCreate({ + name: "client-to-remove", + authType: "implicit", + baseUrl: "https://api.test.com", + clientId: "client-id", + storage: "memory", + }) + + const disposeSpy = vi.spyOn(instance.auth, "dispose") + + const result = registry.remove("client-to-remove") + + expect(result).toBe(true) + expect(disposeSpy).toHaveBeenCalled() + expect(registry.has("client-to-remove")).toBe(false) + }) + }) + + describe("names", () => { + it("should return empty array when no clients", () => { + expect(registry.names()).toEqual([]) + }) + + it("should return all client names", () => { + registry.getOrCreate({ + name: "client-1", + authType: "implicit", + baseUrl: "https://api.test.com", + clientId: "client-id-1", + storage: "memory", + }) + + registry.getOrCreate({ + name: "client-2", + authType: "implicit", + baseUrl: "https://api.test.com", + clientId: "client-id-2", + storage: "memory", + }) + + expect(registry.names()).toContain("client-1") + expect(registry.names()).toContain("client-2") + expect(registry.names()).toHaveLength(2) + }) + }) + + describe("clear", () => { + it("should remove all clients and dispose them", () => { + const instance1 = registry.getOrCreate({ + name: "client-1", + authType: "implicit", + baseUrl: "https://api.test.com", + clientId: "client-id-1", + storage: "memory", + }) + + const instance2 = registry.getOrCreate({ + name: "client-2", + authType: "implicit", + baseUrl: "https://api.test.com", + clientId: "client-id-2", + storage: "memory", + }) + + const disposeSpy1 = vi.spyOn(instance1.auth, "dispose") + const disposeSpy2 = vi.spyOn(instance2.auth, "dispose") + + registry.clear() + + expect(disposeSpy1).toHaveBeenCalled() + expect(disposeSpy2).toHaveBeenCalled() + expect(registry.names()).toEqual([]) + }) + }) +}) + +describe("clientRegistry (singleton)", () => { + afterEach(() => { + clientRegistry.clear() + vi.unstubAllGlobals() + }) + + it("should be a global singleton instance", () => { + expect(clientRegistry).toBeInstanceOf(ClientRegistry) + }) + + it("should work as expected", () => { + vi.stubGlobal( + "fetch", + vi.fn().mockResolvedValue( + new Response( + JSON.stringify({ + access_token: "test-token", + expires: Math.floor(Date.now() / 1000) + 3600, + }), + { status: 200 } + ) + ) + ) + + const instance = clientRegistry.getOrCreate({ + name: "singleton-client", + authType: "implicit", + baseUrl: "https://api.test.com", + clientId: "client-id", + storage: "memory", + }) + + expect(instance).toBeDefined() + expect(clientRegistry.has("singleton-client")).toBe(true) + }) +}) diff --git a/packages/sdks/compatibility-layer/src/client/client-registry.ts b/packages/sdks/compatibility-layer/src/client/client-registry.ts new file mode 100644 index 00000000..c2cf71ee --- /dev/null +++ b/packages/sdks/compatibility-layer/src/client/client-registry.ts @@ -0,0 +1,282 @@ +import type { BridgeConfig, StorageAdapter, TokenProvider } from "../types" +import { SharedAuthState } from "../auth/shared-auth-state" +import { createLegacyStorageBridge } from "../auth/legacy-storage-bridge" +import { createBridgedFetch } from "./create-bridged-client" + +/** + * Auth type for client configuration. + */ +export type AuthType = + | "implicit" + | "client_credentials" + | "password" + | "sso" + | "jwt" + +/** + * Configuration for a named client instance. + */ +export interface ClientInstanceConfig { + /** Unique name for this client instance */ + name: string + /** Authentication type */ + authType: AuthType + /** Base URL for API calls */ + baseUrl: string + /** Client ID for authentication */ + clientId: string + /** Client secret (required for client_credentials) */ + clientSecret?: string + /** Storage adapter or shorthand */ + storage?: StorageAdapter | "localStorage" | "cookie" | "memory" + /** Legacy storage key (if sharing with old SDK) */ + legacyStorageKey?: string + /** Default headers */ + headers?: Record + /** Retry configuration */ + retry?: BridgeConfig["retry"] + /** Throttle configuration */ + throttle?: BridgeConfig["throttle"] + /** Leeway for token expiration */ + leewaySec?: number +} + +/** + * Result of creating/getting a client instance. + */ +export interface ClientInstance { + /** The bridged fetch function */ + fetch: typeof fetch + /** Shared auth state for token management */ + auth: SharedAuthState + /** Configuration used */ + config: ClientInstanceConfig +} + +/** + * Registry for managing multiple client instances. + * + * This supports Commerce Manager's pattern of having separate clients + * for different auth contexts (admin, shopper, preview, etc.). + * + * @example + * ```typescript + * const registry = new ClientRegistry() + * + * // Create admin client with client_credentials + * const admin = registry.getOrCreate({ + * name: 'admin', + * authType: 'client_credentials', + * baseUrl: 'https://api.elasticpath.com', + * clientId: 'xxx', + * clientSecret: 'yyy', + * storage: 'memory', // Don't persist admin tokens + * }) + * + * // Create shopper client with implicit grant + * const shopper = registry.getOrCreate({ + * name: 'shopper', + * authType: 'implicit', + * baseUrl: 'https://api.elasticpath.com', + * clientId: 'zzz', + * storage: 'localStorage', + * }) + * ``` + */ +export class ClientRegistry { + private clients = new Map() + + /** + * Get an existing client or create a new one. + */ + getOrCreate(config: ClientInstanceConfig): ClientInstance { + const existing = this.clients.get(config.name) + if (existing) { + return existing + } + + const instance = this.createInstance(config) + this.clients.set(config.name, instance) + return instance + } + + /** + * Get an existing client by name. + */ + get(name: string): ClientInstance | undefined { + return this.clients.get(name) + } + + /** + * Check if a client exists. + */ + has(name: string): boolean { + return this.clients.has(name) + } + + /** + * Remove and dispose a client instance. + */ + remove(name: string): boolean { + const instance = this.clients.get(name) + if (instance) { + instance.auth.dispose() + this.clients.delete(name) + return true + } + return false + } + + /** + * Get all client names. + */ + names(): string[] { + return Array.from(this.clients.keys()) + } + + /** + * Remove all clients. + */ + clear(): void { + for (const instance of this.clients.values()) { + instance.auth.dispose() + } + this.clients.clear() + } + + /** + * Create a client instance from config. + */ + private createInstance(config: ClientInstanceConfig): ClientInstance { + const storage = this.resolveStorage(config) + const tokenProvider = this.createTokenProvider(config) + + const { fetch, auth } = createBridgedFetch({ + baseUrl: config.baseUrl, + clientId: config.clientId, + clientSecret: config.clientSecret, + storage, + tokenProvider, + headers: config.headers, + retry: config.retry, + throttle: config.throttle, + leewaySec: config.leewaySec, + }) + + return { fetch, auth, config } + } + + /** + * Resolve storage for the client. + */ + private resolveStorage(config: ClientInstanceConfig): StorageAdapter { + if (typeof config.storage === "object" && config.storage !== null) { + return config.storage + } + + const backend = config.storage ?? "localStorage" + return createLegacyStorageBridge({ + key: config.legacyStorageKey, + name: config.name, + backend, + }) + } + + /** + * Create token provider based on auth type. + */ + private createTokenProvider(config: ClientInstanceConfig): TokenProvider { + switch (config.authType) { + case "implicit": + return this.createImplicitTokenProvider(config) + case "client_credentials": + return this.createClientCredentialsTokenProvider(config) + case "password": + case "sso": + case "jwt": + // These require external token setting + return this.createManualTokenProvider(config) + default: + throw new Error(`Unknown auth type: ${config.authType}`) + } + } + + /** + * Create token provider for implicit grant. + */ + private createImplicitTokenProvider( + config: ClientInstanceConfig + ): TokenProvider { + return async () => { + // Use globalThis.fetch to avoid circular dependencies with the bridged fetch + const response = await globalThis.fetch(`${config.baseUrl}/oauth/access_token`, { + method: "POST", + headers: { "Content-Type": "application/x-www-form-urlencoded" }, + body: new URLSearchParams({ + grant_type: "implicit", + client_id: config.clientId, + }).toString(), + }) + + if (!response.ok) { + throw new Error(`Implicit token fetch failed: ${response.status}`) + } + + return response.json() + } + } + + /** + * Create token provider for client_credentials grant. + */ + private createClientCredentialsTokenProvider( + config: ClientInstanceConfig + ): TokenProvider { + if (!config.clientSecret) { + throw new Error( + `Client secret required for client_credentials auth type (client: ${config.name})` + ) + } + + return async () => { + // Use globalThis.fetch to avoid circular dependencies with the bridged fetch + const response = await globalThis.fetch(`${config.baseUrl}/oauth/access_token`, { + method: "POST", + headers: { "Content-Type": "application/x-www-form-urlencoded" }, + body: new URLSearchParams({ + grant_type: "client_credentials", + client_id: config.clientId, + client_secret: config.clientSecret!, + }).toString(), + }) + + if (!response.ok) { + throw new Error( + `Client credentials token fetch failed: ${response.status}` + ) + } + + return response.json() + } + } + + /** + * Create token provider that throws (requires manual token setting). + */ + private createManualTokenProvider( + config: ClientInstanceConfig + ): TokenProvider { + return async () => { + throw new Error( + `Auth type '${config.authType}' requires manual token setting via auth.setToken(). ` + + `Use the auth object returned from getOrCreate() to set tokens after external authentication.` + ) + } + } +} + +/** + * Global client registry singleton. + * Use this for simple cases where a single registry is sufficient. + */ +export const clientRegistry = new ClientRegistry() diff --git a/packages/sdks/compatibility-layer/src/client/create-bridged-client.test.ts b/packages/sdks/compatibility-layer/src/client/create-bridged-client.test.ts new file mode 100644 index 00000000..2c790178 --- /dev/null +++ b/packages/sdks/compatibility-layer/src/client/create-bridged-client.test.ts @@ -0,0 +1,242 @@ +import { describe, it, expect, vi, beforeEach, afterEach } from "vitest" +import { createBridgedClient, createBridgedFetch } from "./create-bridged-client" +import type { StorageAdapter } from "../types" + +describe("createBridgedClient", () => { + let mockClient: { setConfig: ReturnType } + let mockStorage: StorageAdapter + + beforeEach(() => { + mockClient = { + setConfig: vi.fn(), + } + + mockStorage = { + get: vi.fn().mockReturnValue(undefined), + set: vi.fn(), + subscribe: vi.fn().mockReturnValue(() => {}), + } + + // Mock global fetch for token provider + vi.stubGlobal( + "fetch", + vi.fn().mockResolvedValue( + new Response( + JSON.stringify({ + access_token: "test-token", + expires: Math.floor(Date.now() / 1000) + 3600, + }), + { status: 200 } + ) + ) + ) + }) + + afterEach(() => { + vi.unstubAllGlobals() + vi.clearAllMocks() + }) + + it("should configure client with baseUrl and headers", () => { + const { client } = createBridgedClient(mockClient, { + baseUrl: "https://api.test.com", + clientId: "test-client-id", + storage: mockStorage, + headers: { "X-Custom-Header": "value" }, + }) + + expect(client.setConfig).toHaveBeenCalledWith({ + baseUrl: "https://api.test.com", + headers: { "X-Custom-Header": "value" }, + fetch: expect.any(Function), + }) + }) + + it("should return auth state", () => { + const { auth } = createBridgedClient(mockClient, { + baseUrl: "https://api.test.com", + clientId: "test-client-id", + storage: mockStorage, + }) + + expect(auth).toBeDefined() + expect(auth.getValidAccessToken).toBeDefined() + expect(auth.refresh).toBeDefined() + expect(auth.setToken).toBeDefined() + expect(auth.clear).toBeDefined() + }) + + it("should use provided storage adapter", () => { + createBridgedClient(mockClient, { + baseUrl: "https://api.test.com", + clientId: "test-client-id", + storage: mockStorage, + }) + + // Storage subscribe should be called during auth state initialization + expect(mockStorage.subscribe).toHaveBeenCalled() + }) + + it("should use custom token provider when provided", async () => { + const customTokenProvider = vi.fn().mockResolvedValue({ + access_token: "custom-token", + expires: Math.floor(Date.now() / 1000) + 3600, + }) + + const { auth } = createBridgedClient(mockClient, { + baseUrl: "https://api.test.com", + clientId: "test-client-id", + storage: mockStorage, + tokenProvider: customTokenProvider, + }) + + const token = await auth.refresh() + + expect(customTokenProvider).toHaveBeenCalled() + expect(token).toBe("custom-token") + }) + + it("should use default token provider for implicit grant", async () => { + const { auth } = createBridgedClient(mockClient, { + baseUrl: "https://api.test.com", + clientId: "test-client-id", + storage: mockStorage, + }) + + await auth.refresh() + + expect(fetch).toHaveBeenCalledWith( + "https://api.test.com/oauth/access_token", + expect.objectContaining({ + method: "POST", + body: expect.stringContaining("grant_type=implicit"), + }) + ) + }) + + it("should use client_credentials grant when clientSecret is provided", async () => { + const { auth } = createBridgedClient(mockClient, { + baseUrl: "https://api.test.com", + clientId: "test-client-id", + clientSecret: "test-secret", + storage: mockStorage, + }) + + await auth.refresh() + + expect(fetch).toHaveBeenCalledWith( + "https://api.test.com/oauth/access_token", + expect.objectContaining({ + method: "POST", + body: expect.stringContaining("grant_type=client_credentials"), + }) + ) + }) + + it("should apply retry configuration", () => { + createBridgedClient(mockClient, { + baseUrl: "https://api.test.com", + clientId: "test-client-id", + storage: mockStorage, + retry: { + maxAttempts: 5, + baseDelay: 2000, + }, + }) + + // The retry config is applied internally to the fetch wrapper + expect(mockClient.setConfig).toHaveBeenCalledWith( + expect.objectContaining({ + fetch: expect.any(Function), + }) + ) + }) + + it("should apply throttle configuration", () => { + createBridgedClient(mockClient, { + baseUrl: "https://api.test.com", + clientId: "test-client-id", + storage: mockStorage, + throttle: { + enabled: true, + limit: 5, + interval: 200, + }, + }) + + expect(mockClient.setConfig).toHaveBeenCalledWith( + expect.objectContaining({ + fetch: expect.any(Function), + }) + ) + }) +}) + +describe("createBridgedFetch", () => { + let mockStorage: StorageAdapter + + beforeEach(() => { + mockStorage = { + get: vi.fn().mockReturnValue(undefined), + set: vi.fn(), + subscribe: vi.fn().mockReturnValue(() => {}), + } + + vi.stubGlobal( + "fetch", + vi.fn().mockResolvedValue( + new Response( + JSON.stringify({ + access_token: "test-token", + expires: Math.floor(Date.now() / 1000) + 3600, + }), + { status: 200 } + ) + ) + ) + }) + + afterEach(() => { + vi.unstubAllGlobals() + vi.clearAllMocks() + }) + + it("should return fetch function and auth state", () => { + const result = createBridgedFetch({ + baseUrl: "https://api.test.com", + clientId: "test-client-id", + storage: mockStorage, + }) + + expect(result.fetch).toBeDefined() + expect(typeof result.fetch).toBe("function") + expect(result.auth).toBeDefined() + }) + + it("should create fetch that adds authorization header", async () => { + // Pre-populate storage with a valid token + const tokenData = { + access_token: "existing-token", + expires: Math.floor(Date.now() / 1000) + 3600, + } + mockStorage.get = vi.fn().mockReturnValue(JSON.stringify(tokenData)) + + const mockFetchFn = vi.fn().mockResolvedValue( + new Response("{}", { status: 200 }) + ) + vi.stubGlobal("fetch", mockFetchFn) + + const { fetch: bridgedFetch } = createBridgedFetch({ + baseUrl: "https://api.test.com", + clientId: "test-client-id", + storage: mockStorage, + }) + + await bridgedFetch("https://api.test.com/products") + + const calledRequest = mockFetchFn.mock.calls[0][0] as Request + expect(calledRequest.headers.get("Authorization")).toBe( + "Bearer existing-token" + ) + }) +}) diff --git a/packages/sdks/compatibility-layer/src/client/create-bridged-client.ts b/packages/sdks/compatibility-layer/src/client/create-bridged-client.ts new file mode 100644 index 00000000..47226665 --- /dev/null +++ b/packages/sdks/compatibility-layer/src/client/create-bridged-client.ts @@ -0,0 +1,223 @@ +import type { + BridgeConfig, + BridgedClient, + StorageAdapter, + TokenData, + TokenProvider, + RetryConfig, + ThrottleConfig, +} from "../types" +import { + DEFAULT_RETRY_CONFIG, + DEFAULT_THROTTLE_CONFIG, + DEFAULT_LEGACY_STORAGE_KEY, +} from "../types" +import { SharedAuthState } from "../auth/shared-auth-state" +import { createLegacyStorageBridge } from "../auth/legacy-storage-bridge" +import { createFetchWithRetry } from "../fetch/fetch-with-retry" +import { createThrottledFetch } from "../fetch/throttle" + +/** + * Client interface that matches @hey-api/client-fetch client structure. + * We use a minimal interface to avoid hard dependency on the package. + */ +interface HeyApiClient { + setConfig(config: { + baseUrl?: string + headers?: HeadersInit | Record + fetch?: typeof fetch + }): void +} + +/** + * Resolve storage option to a StorageAdapter. + */ +function resolveStorage( + storage: BridgeConfig["storage"], + legacyStorageKey?: string +): StorageAdapter { + if (typeof storage === "object" && storage !== null) { + // Already a StorageAdapter + return storage + } + + // Use legacy storage bridge with the specified backend + return createLegacyStorageBridge({ + key: legacyStorageKey, + backend: storage ?? "localStorage", + }) +} + +/** + * Create a default token provider that fetches implicit grant tokens. + * This matches the old SDK's createAuthRequest function. + */ +function createDefaultTokenProvider( + baseUrl: string, + clientId: string, + clientSecret?: string, + baseFetch: typeof fetch = globalThis.fetch +): TokenProvider { + return async (): Promise => { + const grantType = clientSecret ? "client_credentials" : "implicit" + + const body = new URLSearchParams({ + grant_type: grantType, + client_id: clientId, + }) + + if (clientSecret) { + body.append("client_secret", clientSecret) + } + + const response = await baseFetch(`${baseUrl}/oauth/access_token`, { + method: "POST", + headers: { + "Content-Type": "application/x-www-form-urlencoded", + }, + body: body.toString(), + }) + + if (!response.ok) { + const error = await response.json().catch(() => ({})) + throw new Error( + `Authentication failed: ${response.status} ${JSON.stringify(error)}` + ) + } + + const tokenData: TokenData = await response.json() + return tokenData + } +} + +/** + * Compose the final fetch function with retry and throttle logic. + */ +function composeFetch( + authState: SharedAuthState, + retryConfig: Partial, + throttleConfig: Partial, + baseFetch: typeof fetch = globalThis.fetch +): typeof fetch { + // Start with base fetch + let composedFetch = baseFetch + + // Add throttling if enabled + const fullThrottleConfig = { ...DEFAULT_THROTTLE_CONFIG, ...throttleConfig } + if (fullThrottleConfig.enabled) { + composedFetch = createThrottledFetch(composedFetch, fullThrottleConfig) + } + + // Add retry logic (wraps the possibly-throttled fetch) + const fullRetryConfig = { ...DEFAULT_RETRY_CONFIG, ...retryConfig } + composedFetch = createFetchWithRetry(fullRetryConfig, authState, composedFetch) + + return composedFetch +} + +/** + * Create a bridged client that uses old SDK's auth, retry, and throttle logic. + * + * This function configures a @hey-api/client-fetch client to use: + * - Shared auth state with the old SDK (via localStorage events) + * - Retry logic matching the old SDK (exponential backoff, 401/429 handling) + * - Optional throttling matching the old SDK + * + * @example + * ```typescript + * import { client } from '@epcc-sdk/sdks-shopper' + * import { createBridgedClient } from '@epcc-sdk/compatibility-layer' + * + * const { client: shopperClient, auth } = createBridgedClient(client, { + * baseUrl: 'https://api.elasticpath.com', + * clientId: 'your-client-id', + * legacyStorageKey: 'epCredentials', // Share with old SDK + * retry: { maxAttempts: 4 }, + * throttle: { enabled: true }, + * }) + * ``` + */ +export function createBridgedClient( + client: T, + config: BridgeConfig +): BridgedClient { + // Resolve storage + const storage = resolveStorage( + config.storage, + config.legacyStorageKey ?? DEFAULT_LEGACY_STORAGE_KEY + ) + + // Create or use provided token provider + const tokenProvider = + config.tokenProvider ?? + createDefaultTokenProvider( + config.baseUrl, + config.clientId, + config.clientSecret + ) + + // Create shared auth state + const authState = new SharedAuthState({ + storage, + tokenProvider, + leewaySec: config.leewaySec, + }) + + // Compose fetch with retry and throttle + const bridgedFetch = composeFetch( + authState, + config.retry ?? {}, + config.throttle ?? {} + ) + + // Configure the client + client.setConfig({ + baseUrl: config.baseUrl, + headers: config.headers, + fetch: bridgedFetch, + }) + + return { + client, + auth: authState, + } +} + +/** + * Create a bridged fetch function without configuring a specific client. + * Use this if you need the fetch function directly. + */ +export function createBridgedFetch(config: BridgeConfig): { + fetch: typeof fetch + auth: SharedAuthState +} { + const storage = resolveStorage( + config.storage, + config.legacyStorageKey ?? DEFAULT_LEGACY_STORAGE_KEY + ) + + const tokenProvider = + config.tokenProvider ?? + createDefaultTokenProvider( + config.baseUrl, + config.clientId, + config.clientSecret + ) + + const authState = new SharedAuthState({ + storage, + tokenProvider, + leewaySec: config.leewaySec, + }) + + const bridgedFetch = composeFetch( + authState, + config.retry ?? {}, + config.throttle ?? {} + ) + + return { + fetch: bridgedFetch, + auth: authState, + } +} diff --git a/packages/sdks/compatibility-layer/src/fetch/fetch-with-retry.test.ts b/packages/sdks/compatibility-layer/src/fetch/fetch-with-retry.test.ts new file mode 100644 index 00000000..c6d7c78e --- /dev/null +++ b/packages/sdks/compatibility-layer/src/fetch/fetch-with-retry.test.ts @@ -0,0 +1,208 @@ +import { describe, it, expect, vi, beforeEach, afterEach } from "vitest" +import { fetchWithRetry, createFetchWithRetry } from "./fetch-with-retry" +import type { SharedAuthStateInterface } from "../types" + +describe("fetchWithRetry", () => { + let mockFetch: ReturnType + let mockAuthState: SharedAuthStateInterface + + beforeEach(() => { + mockFetch = vi.fn() + mockAuthState = { + getValidAccessToken: vi.fn().mockResolvedValue("test-token"), + refresh: vi.fn().mockResolvedValue("new-token"), + setToken: vi.fn(), + clear: vi.fn(), + getSnapshot: vi.fn().mockReturnValue("test-token"), + isExpired: vi.fn().mockReturnValue(false), + subscribe: vi.fn().mockReturnValue(() => {}), + } + vi.useFakeTimers() + }) + + afterEach(() => { + vi.useRealTimers() + vi.clearAllMocks() + }) + + it("should return response on successful request", async () => { + const mockResponse = new Response(JSON.stringify({ data: "test" }), { + status: 200, + }) + mockFetch.mockResolvedValueOnce(mockResponse) + + const request = new Request("https://api.test.com/products") + const promise = fetchWithRetry(request, {}, mockAuthState, mockFetch) + + await vi.runAllTimersAsync() + const response = await promise + + expect(response.status).toBe(200) + expect(mockFetch).toHaveBeenCalledTimes(1) + }) + + it("should add Authorization header from auth state", async () => { + const mockResponse = new Response("{}", { status: 200 }) + mockFetch.mockResolvedValueOnce(mockResponse) + + const request = new Request("https://api.test.com/products") + const promise = fetchWithRetry(request, {}, mockAuthState, mockFetch) + + await vi.runAllTimersAsync() + await promise + + const calledRequest = mockFetch.mock.calls[0][0] as Request + expect(calledRequest.headers.get("Authorization")).toBe("Bearer test-token") + }) + + it("should not add Authorization header for OAuth endpoints", async () => { + const mockResponse = new Response("{}", { status: 200 }) + mockFetch.mockResolvedValueOnce(mockResponse) + + const request = new Request("https://api.test.com/oauth/access_token") + const promise = fetchWithRetry(request, {}, mockAuthState, mockFetch) + + await vi.runAllTimersAsync() + await promise + + const calledRequest = mockFetch.mock.calls[0][0] as Request + expect(calledRequest.headers.get("Authorization")).toBeNull() + }) + + it("should retry on 401 with token refresh", async () => { + const unauthorizedResponse = new Response("{}", { status: 401 }) + const successResponse = new Response("{}", { status: 200 }) + + mockFetch + .mockResolvedValueOnce(unauthorizedResponse) + .mockResolvedValueOnce(successResponse) + + const request = new Request("https://api.test.com/products") + const config = { maxAttempts: 4, baseDelay: 100, jitter: 0, reauth: true } + + const promise = fetchWithRetry(request, config, mockAuthState, mockFetch) + + // Let first request complete + await vi.advanceTimersByTimeAsync(0) + // Let refresh and retry timer complete + await vi.advanceTimersByTimeAsync(200) + + const response = await promise + + expect(mockAuthState.refresh).toHaveBeenCalledTimes(1) + expect(mockFetch).toHaveBeenCalledTimes(2) + expect(response.status).toBe(200) + }) + + it("should retry on 429 with backoff", async () => { + const rateLimitResponse = new Response("{}", { status: 429 }) + const successResponse = new Response("{}", { status: 200 }) + + mockFetch + .mockResolvedValueOnce(rateLimitResponse) + .mockResolvedValueOnce(successResponse) + + const request = new Request("https://api.test.com/products") + const config = { maxAttempts: 4, baseDelay: 100, jitter: 0, reauth: true } + + const promise = fetchWithRetry(request, config, mockAuthState, mockFetch) + + // Let first request complete + await vi.advanceTimersByTimeAsync(0) + // Let retry timer complete (attempt 1 * 100ms baseDelay) + await vi.advanceTimersByTimeAsync(200) + + const response = await promise + + expect(mockAuthState.refresh).not.toHaveBeenCalled() + expect(mockFetch).toHaveBeenCalledTimes(2) + expect(response.status).toBe(200) + }) + + it("should stop retrying after max attempts", async () => { + const rateLimitResponse = new Response("{}", { status: 429 }) + + mockFetch.mockResolvedValue(rateLimitResponse) + + const request = new Request("https://api.test.com/products") + const config = { maxAttempts: 3, baseDelay: 10, jitter: 0, reauth: true } + + const promise = fetchWithRetry(request, config, mockAuthState, mockFetch) + + // Run through all retry timers + await vi.runAllTimersAsync() + + const response = await promise + + expect(mockFetch).toHaveBeenCalledTimes(3) + expect(response.status).toBe(429) + }) + + it("should not retry on other error statuses", async () => { + const errorResponse = new Response("{}", { status: 500 }) + mockFetch.mockResolvedValueOnce(errorResponse) + + const request = new Request("https://api.test.com/products") + const promise = fetchWithRetry(request, {}, mockAuthState, mockFetch) + + await vi.runAllTimersAsync() + const response = await promise + + expect(mockFetch).toHaveBeenCalledTimes(1) + expect(response.status).toBe(500) + }) + + it("should not retry 401 when reauth is disabled", async () => { + const unauthorizedResponse = new Response("{}", { status: 401 }) + mockFetch.mockResolvedValueOnce(unauthorizedResponse) + + const request = new Request("https://api.test.com/products") + const config = { reauth: false } + + const promise = fetchWithRetry(request, config, mockAuthState, mockFetch) + + await vi.runAllTimersAsync() + const response = await promise + + expect(mockAuthState.refresh).not.toHaveBeenCalled() + expect(mockFetch).toHaveBeenCalledTimes(1) + expect(response.status).toBe(401) + }) + + it("should work without auth state", async () => { + const mockResponse = new Response("{}", { status: 200 }) + mockFetch.mockResolvedValueOnce(mockResponse) + + const request = new Request("https://api.test.com/products") + const promise = fetchWithRetry(request, {}, undefined, mockFetch) + + await vi.runAllTimersAsync() + const response = await promise + + expect(response.status).toBe(200) + const calledRequest = mockFetch.mock.calls[0][0] as Request + expect(calledRequest.headers.get("Authorization")).toBeNull() + }) +}) + +describe("createFetchWithRetry", () => { + it("should create a fetch function with retry logic", async () => { + const mockFetch = vi.fn().mockResolvedValue(new Response("{}", { status: 200 })) + const mockAuthState: SharedAuthStateInterface = { + getValidAccessToken: vi.fn().mockResolvedValue("token"), + refresh: vi.fn(), + setToken: vi.fn(), + clear: vi.fn(), + getSnapshot: vi.fn(), + isExpired: vi.fn(), + subscribe: vi.fn().mockReturnValue(() => {}), + } + + const retryFetch = createFetchWithRetry({}, mockAuthState, mockFetch) + + const response = await retryFetch("https://api.test.com/products") + + expect(response.status).toBe(200) + expect(mockFetch).toHaveBeenCalledTimes(1) + }) +}) diff --git a/packages/sdks/compatibility-layer/src/fetch/fetch-with-retry.ts b/packages/sdks/compatibility-layer/src/fetch/fetch-with-retry.ts new file mode 100644 index 00000000..df98dc0b --- /dev/null +++ b/packages/sdks/compatibility-layer/src/fetch/fetch-with-retry.ts @@ -0,0 +1,145 @@ +import type { RetryConfig, SharedAuthStateInterface } from "../types" +import { DEFAULT_RETRY_CONFIG } from "../types" + +/** + * Calculate delay for retry attempt using exponential backoff with jitter. + * Formula: attempt * baseDelay + random(0, jitter) + * + * This matches the old SDK's retry delay calculation from: + * ep-js-sdk/src/factories/request.js line 86 + */ +function calculateDelay(attempt: number, config: RetryConfig): number { + return attempt * config.baseDelay + Math.floor(Math.random() * config.jitter) +} + +/** + * Sleep for the specified duration. + */ +function sleep(ms: number): Promise { + return new Promise((resolve) => setTimeout(resolve, ms)) +} + +/** + * Check if the URL is an OAuth endpoint (to skip auth injection). + */ +function isOAuthEndpoint(url: string): boolean { + return url.includes("/oauth/") +} + +/** + * Fetch with retry logic ported from old SDK. + * + * Implements: + * - Exponential backoff with jitter on retries + * - 401 handling: re-authenticate and retry + * - 429 handling: retry with backoff (rate limiting) + * - Configurable max attempts + * + * Based on ep-js-sdk/src/factories/request.js lines 54-118 + */ +export async function fetchWithRetry( + request: Request, + config: Partial, + authState: SharedAuthStateInterface | undefined, + baseFetch: typeof fetch, + attempt = 1 +): Promise { + const fullConfig: RetryConfig = { ...DEFAULT_RETRY_CONFIG, ...config } + + // Clone request for potential retry (body can only be read once) + const requestClone = request.clone() + + // Skip auth injection for OAuth endpoints to prevent infinite loops + const skipAuth = isOAuthEndpoint(request.url) + + // Add auth token if not an OAuth request and we have auth state + if (!skipAuth && authState) { + try { + const token = await authState.getValidAccessToken() + if (token && !request.headers.has("Authorization")) { + // Request headers are immutable, so we need to create a new request + const newHeaders = new Headers(request.headers) + newHeaders.set("Authorization", `Bearer ${token}`) + request = new Request(request, { headers: newHeaders }) + } + } catch { + // If token fetch fails, continue without auth + // The request will likely fail with 401 which triggers reauth + } + } + + let response: Response + try { + response = await baseFetch(request) + } catch (error) { + // Network error - don't retry on network failures + throw error + } + + // Success - return response + if (response.ok) { + return response + } + + // Check if we've exhausted retries + if (attempt >= fullConfig.maxAttempts) { + return response + } + + // Handle 401 - re-authenticate and retry + if (response.status === 401 && fullConfig.reauth && !skipAuth && authState) { + try { + // Force token refresh + await authState.refresh() + + // Calculate delay and wait + const retryDelay = calculateDelay(attempt, fullConfig) + await sleep(retryDelay) + + // Retry with new token + return fetchWithRetry( + requestClone, + fullConfig, + authState, + baseFetch, + attempt + 1 + ) + } catch { + // Auth refresh failed, return original 401 response + return response + } + } + + // Handle 429 - rate limited, retry with backoff + if (response.status === 429) { + const retryDelay = calculateDelay(attempt, fullConfig) + await sleep(retryDelay) + + // Retry without re-auth (just rate limited) + return fetchWithRetry( + requestClone, + fullConfig, + authState, + baseFetch, + attempt + 1 + ) + } + + // Other errors - don't retry + return response +} + +/** + * Create a fetch function with retry logic. + * This can be injected into @hey-api/client-fetch via client.setConfig({ fetch: ... }) + */ +export function createFetchWithRetry( + config: Partial, + authState: SharedAuthStateInterface | undefined, + baseFetch: typeof fetch = globalThis.fetch +): typeof fetch { + return (input: RequestInfo | URL, init?: RequestInit): Promise => { + const request = new Request(input, init) + return fetchWithRetry(request, config, authState, baseFetch) + } +} diff --git a/packages/sdks/compatibility-layer/src/fetch/throttle.test.ts b/packages/sdks/compatibility-layer/src/fetch/throttle.test.ts new file mode 100644 index 00000000..4f438877 --- /dev/null +++ b/packages/sdks/compatibility-layer/src/fetch/throttle.test.ts @@ -0,0 +1,136 @@ +import { describe, it, expect, vi, beforeEach, afterEach } from "vitest" +import { + createThrottledFetch, + createIsolatedThrottledFetch, + resetGlobalThrottleQueue, +} from "./throttle" + +describe("createThrottledFetch", () => { + let mockFetch: ReturnType + + beforeEach(() => { + mockFetch = vi.fn().mockResolvedValue(new Response("{}", { status: 200 })) + resetGlobalThrottleQueue() + }) + + afterEach(() => { + vi.clearAllMocks() + resetGlobalThrottleQueue() + }) + + it("should return base fetch when throttling is disabled", async () => { + const throttledFetch = createThrottledFetch(mockFetch, { enabled: false }) + + await throttledFetch("https://api.test.com/products") + + expect(mockFetch).toHaveBeenCalledTimes(1) + // When disabled, returns base fetch directly which is called with just the URL + expect(mockFetch).toHaveBeenCalledWith("https://api.test.com/products") + }) + + it("should throttle requests when enabled", async () => { + const throttledFetch = createThrottledFetch(mockFetch, { + enabled: true, + limit: 2, + interval: 100, + }) + + // Make 4 requests + const promises = [ + throttledFetch("https://api.test.com/1"), + throttledFetch("https://api.test.com/2"), + throttledFetch("https://api.test.com/3"), + throttledFetch("https://api.test.com/4"), + ] + + // Wait for all to complete + await Promise.all(promises) + + expect(mockFetch).toHaveBeenCalledTimes(4) + }) + + it("should pass init options to fetch", async () => { + const throttledFetch = createThrottledFetch(mockFetch, { enabled: true }) + + const init = { method: "POST", body: JSON.stringify({ data: "test" }) } + await throttledFetch("https://api.test.com/products", init) + + expect(mockFetch).toHaveBeenCalledWith("https://api.test.com/products", init) + }) +}) + +describe("createIsolatedThrottledFetch", () => { + let mockFetch: ReturnType + + beforeEach(() => { + mockFetch = vi.fn().mockResolvedValue(new Response("{}", { status: 200 })) + }) + + afterEach(() => { + vi.clearAllMocks() + }) + + it("should return base fetch when throttling is disabled", async () => { + const throttledFetch = createIsolatedThrottledFetch(mockFetch, { + enabled: false, + }) + + await throttledFetch("https://api.test.com/products") + + expect(mockFetch).toHaveBeenCalledTimes(1) + }) + + it("should use separate queue from global throttle", async () => { + // Create two isolated throttled fetches + const throttledFetch1 = createIsolatedThrottledFetch(mockFetch, { + enabled: true, + limit: 1, + interval: 100, + }) + + const throttledFetch2 = createIsolatedThrottledFetch(mockFetch, { + enabled: true, + limit: 1, + interval: 100, + }) + + // Both should be able to make requests independently + const promises = [ + throttledFetch1("https://api.test.com/1"), + throttledFetch2("https://api.test.com/2"), + ] + + await Promise.all(promises) + + expect(mockFetch).toHaveBeenCalledTimes(2) + }) +}) + +describe("resetGlobalThrottleQueue", () => { + it("should reset the global throttle queue", async () => { + const mockFetch = vi.fn().mockResolvedValue(new Response("{}", { status: 200 })) + + // Create throttled fetch with specific config + const throttledFetch1 = createThrottledFetch(mockFetch, { + enabled: true, + limit: 5, + interval: 1000, + }) + + await throttledFetch1("https://api.test.com/1") + + // Reset the queue + resetGlobalThrottleQueue() + + // Create new throttled fetch - should create new queue + const throttledFetch2 = createThrottledFetch(mockFetch, { + enabled: true, + limit: 1, + interval: 100, + }) + + await throttledFetch2("https://api.test.com/2") + + expect(mockFetch).toHaveBeenCalledTimes(2) + }) +}) diff --git a/packages/sdks/compatibility-layer/src/fetch/throttle.ts b/packages/sdks/compatibility-layer/src/fetch/throttle.ts new file mode 100644 index 00000000..b322c0c4 --- /dev/null +++ b/packages/sdks/compatibility-layer/src/fetch/throttle.ts @@ -0,0 +1,85 @@ +import throttledQueue from "throttled-queue" +import type { ThrottleConfig } from "../types" +import { DEFAULT_THROTTLE_CONFIG } from "../types" + +/** + * Cached throttle queue instance. + * Using a module-level cache matches the old SDK's behavior. + */ +let globalThrottleQueue: + | ((fn: () => Promise) => Promise) + | undefined + +/** + * Create a throttled fetch function. + * + * This matches the old SDK's throttle implementation from: + * ep-js-sdk/src/utils/throttle.js + * + * The throttle queue limits requests to `limit` requests per `interval` milliseconds. + * Requests beyond the limit are queued and executed when a slot becomes available. + * + * **Important:** This function uses a global throttle queue. The queue is created with + * the first configuration passed to this function, and subsequent calls with different + * configurations will reuse the same queue with the original configuration. If you need + * separate throttle queues with different configurations, use `createIsolatedThrottledFetch` + * instead. + */ +export function createThrottledFetch( + baseFetch: typeof fetch, + config: Partial = {} +): typeof fetch { + const fullConfig: ThrottleConfig = { ...DEFAULT_THROTTLE_CONFIG, ...config } + + // If throttling is disabled, return the base fetch + if (!fullConfig.enabled) { + return baseFetch + } + + // Initialize the throttle queue if not already done + if (globalThrottleQueue === undefined) { + globalThrottleQueue = throttledQueue(fullConfig.limit, fullConfig.interval) + } + + return async ( + input: RequestInfo | URL, + init?: RequestInit + ): Promise => { + // Queue the fetch request + return globalThrottleQueue!(() => baseFetch(input, init)) + } +} + +/** + * Create a throttled fetch with a dedicated queue (not shared globally). + * Use this when you need separate throttle queues for different clients. + */ +export function createIsolatedThrottledFetch( + baseFetch: typeof fetch, + config: Partial = {} +): typeof fetch { + const fullConfig: ThrottleConfig = { ...DEFAULT_THROTTLE_CONFIG, ...config } + + // If throttling is disabled, return the base fetch + if (!fullConfig.enabled) { + return baseFetch + } + + // Create a dedicated throttle queue for this instance + const queue = throttledQueue(fullConfig.limit, fullConfig.interval) + + return async ( + input: RequestInfo | URL, + init?: RequestInit + ): Promise => { + return queue(() => baseFetch(input, init)) + } +} + +/** + * Reset the global throttle queue. + * Useful for testing or when configuration changes. + */ +export function resetGlobalThrottleQueue(): void { + globalThrottleQueue = undefined +} diff --git a/packages/sdks/compatibility-layer/src/index.ts b/packages/sdks/compatibility-layer/src/index.ts new file mode 100644 index 00000000..5b0f4e26 --- /dev/null +++ b/packages/sdks/compatibility-layer/src/index.ts @@ -0,0 +1,100 @@ +/** + * @epcc-sdk/compatibility-layer + * + * Compatibility layer enabling new TypeScript SDKs to use the old JS SDK's + * battle-tested auth, retry, and throttle logic. + * + * Zero changes to old JS SDK required. This layer works by: + * - Replicating retry and throttle logic + * - Reading from the same storage keys the old SDK uses + * - Using browser storage events to detect token updates + * - Injecting custom fetch into new SDKs + * + * @example Basic Usage + * ```typescript + * import { client } from '@epcc-sdk/sdks-shopper' + * import { createBridgedClient } from '@epcc-sdk/compatibility-layer' + * + * const { client: shopperClient, auth } = createBridgedClient(client, { + * baseUrl: 'https://api.elasticpath.com', + * clientId: 'your-client-id', + * legacyStorageKey: 'epCredentials', // Share with old SDK + * }) + * ``` + * + * @example Multiple Clients (Commerce Manager pattern) + * ```typescript + * import { clientRegistry } from '@epcc-sdk/compatibility-layer' + * + * const admin = clientRegistry.getOrCreate({ + * name: 'admin', + * authType: 'client_credentials', + * baseUrl: 'https://api.elasticpath.com', + * clientId: 'xxx', + * clientSecret: 'yyy', + * }) + * + * const shopper = clientRegistry.getOrCreate({ + * name: 'shopper', + * authType: 'implicit', + * baseUrl: 'https://api.elasticpath.com', + * clientId: 'zzz', + * }) + * ``` + */ + +// Types +export type { + TokenData, + StorageAdapter, + TokenProvider, + RetryConfig, + ThrottleConfig, + BridgeConfig, + BridgedClient, + SharedAuthStateInterface, +} from "./types" + +export { + DEFAULT_RETRY_CONFIG, + DEFAULT_THROTTLE_CONFIG, + DEFAULT_LEGACY_STORAGE_KEY, +} from "./types" + +// Auth +export { SharedAuthState } from "./auth/shared-auth-state" +export type { SharedAuthStateOptions } from "./auth/shared-auth-state" + +export { + createLegacyStorageBridge, + storageAdapters, +} from "./auth/legacy-storage-bridge" +export type { LegacyStorageBridgeOptions } from "./auth/legacy-storage-bridge" + +// Fetch utilities +export { + fetchWithRetry, + createFetchWithRetry, +} from "./fetch/fetch-with-retry" + +export { + createThrottledFetch, + createIsolatedThrottledFetch, + resetGlobalThrottleQueue, +} from "./fetch/throttle" + +// Client utilities +export { + createBridgedClient, + createBridgedFetch, +} from "./client/create-bridged-client" + +export { + ClientRegistry, + clientRegistry, +} from "./client/client-registry" +export type { + AuthType, + ClientInstanceConfig, + ClientInstance, +} from "./client/client-registry" diff --git a/packages/sdks/compatibility-layer/src/types.ts b/packages/sdks/compatibility-layer/src/types.ts new file mode 100644 index 00000000..d30bf1d7 --- /dev/null +++ b/packages/sdks/compatibility-layer/src/types.ts @@ -0,0 +1,144 @@ +/** + * Token data structure compatible with both old and new SDK formats. + * This matches the old SDK's credential storage format. + */ +export interface TokenData { + /** The client ID used to obtain this token */ + client_id?: string + /** The access token */ + access_token: string + /** Unix timestamp when the token expires */ + expires?: number + /** Token lifetime in seconds (from OAuth response) */ + expires_in?: number + /** Grant type identifier (e.g., 'implicit', 'client_credentials') */ + identifier?: string + /** Token type (usually 'Bearer') */ + token_type?: string + /** Refresh token if available (for password/SSO flows) */ + refresh_token?: string +} + +/** + * Storage adapter interface compatible with new SDK's storage system. + * Allows pluggable storage backends (localStorage, cookies, memory). + */ +export interface StorageAdapter { + /** Return the current stored value or undefined if none. */ + get(): string | undefined + /** Persist or remove the value. */ + set(value?: string): void + /** Optional: subscribe to external changes (e.g., other tabs). Returns an unsubscribe function. */ + subscribe?(cb: () => void): () => void +} + +/** + * Function type for providing tokens. + * Used by SharedAuthState to obtain new tokens when needed. + */ +export type TokenProvider = (opts: { + /** Current token (if any) for refresh flows */ + current?: string +}) => Promise + +/** + * Configuration for retry behavior. + * Matches the old SDK's retry configuration. + */ +export interface RetryConfig { + /** Maximum number of retry attempts (default: 4) */ + maxAttempts: number + /** Base delay in milliseconds (default: 1000) */ + baseDelay: number + /** Random jitter in milliseconds (default: 500) */ + jitter: number + /** Whether to re-authenticate on 401 (default: true) */ + reauth: boolean +} + +/** + * Configuration for request throttling. + * Matches the old SDK's throttle configuration. + */ +export interface ThrottleConfig { + /** Whether throttling is enabled */ + enabled: boolean + /** Number of requests allowed per interval (default: 3) */ + limit: number + /** Interval in milliseconds (default: 125) */ + interval: number +} + +/** + * Configuration for creating a bridged client. + */ +export interface BridgeConfig { + /** Base URL for API calls (e.g., 'https://api.elasticpath.com') */ + baseUrl: string + /** Client ID for authentication */ + clientId: string + /** Client secret for client_credentials flow (optional) */ + clientSecret?: string + /** Storage adapter or shorthand name */ + storage?: StorageAdapter | "localStorage" | "cookie" | "memory" + /** Legacy storage key to read from (default: 'epCredentials') */ + legacyStorageKey?: string + /** Custom token provider function (for custom auth flows) */ + tokenProvider?: TokenProvider + /** Retry configuration */ + retry?: Partial + /** Throttle configuration */ + throttle?: Partial + /** Default headers to include in all requests */ + headers?: Record + /** Leeway in seconds for token expiration check (default: 60) */ + leewaySec?: number +} + +/** + * Result of creating a bridged client. + */ +export interface BridgedClient { + /** The configured client */ + client: T + /** Shared auth state for manual token management */ + auth: SharedAuthStateInterface +} + +/** + * Interface for SharedAuthState (for type references without circular deps). + */ +export interface SharedAuthStateInterface { + /** Get a valid access token, refreshing if necessary */ + getValidAccessToken(): Promise + /** Force a token refresh */ + refresh(): Promise + /** Manually set token data (for external auth integration) */ + setToken(tokenData: TokenData): void + /** Clear stored credentials */ + clear(): void + /** Get current token without validation */ + getSnapshot(): string | undefined + /** Check if current token is expired */ + isExpired(): boolean + /** Subscribe to auth state changes */ + subscribe(callback: () => void): () => void +} + +/** + * Default configuration values matching old SDK. + */ +export const DEFAULT_RETRY_CONFIG: RetryConfig = { + maxAttempts: 4, + baseDelay: 1000, + jitter: 500, + reauth: true, +} + +export const DEFAULT_THROTTLE_CONFIG: ThrottleConfig = { + enabled: false, + limit: 3, + interval: 125, +} + +export const DEFAULT_LEGACY_STORAGE_KEY = "epCredentials" diff --git a/packages/sdks/compatibility-layer/tsconfig.json b/packages/sdks/compatibility-layer/tsconfig.json new file mode 100644 index 00000000..b8e089f9 --- /dev/null +++ b/packages/sdks/compatibility-layer/tsconfig.json @@ -0,0 +1,25 @@ +{ + "compilerOptions": { + "target": "ES2020", + "useDefineForClassFields": true, + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "module": "ESNext", + "skipLibCheck": true, + "outDir": "dist", + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true + }, + "include": ["src"], + "references": [{ "path": "./tsconfig.node.json" }] +} diff --git a/packages/sdks/compatibility-layer/tsconfig.node.json b/packages/sdks/compatibility-layer/tsconfig.node.json new file mode 100644 index 00000000..6aebd3c2 --- /dev/null +++ b/packages/sdks/compatibility-layer/tsconfig.node.json @@ -0,0 +1,13 @@ +{ + "compilerOptions": { + "composite": true, + "skipLibCheck": true, + "module": "ESNext", + "moduleResolution": "bundler", + "allowSyntheticDefaultImports": true, + "strict": true, + "outDir": "dist", + "rootDir": "src" + }, + "include": ["src"] +} diff --git a/packages/sdks/compatibility-layer/tsup.config.ts b/packages/sdks/compatibility-layer/tsup.config.ts new file mode 100644 index 00000000..1b815dec --- /dev/null +++ b/packages/sdks/compatibility-layer/tsup.config.ts @@ -0,0 +1,34 @@ +import { defineConfig } from "tsup" +import { + fixAliasPlugin, + fixFolderImportsPlugin, + fixExtensionsPlugin, +} from "esbuild-fix-imports-plugin" + +export default defineConfig({ + entry: ["src/**/*.ts", "!src/**/*.test.ts"], + format: ["esm", "cjs"], + dts: true, + splitting: false, + sourcemap: true, + clean: true, + treeshake: false, + bundle: false, + esbuildPlugins: [ + fixAliasPlugin(), + fixFolderImportsPlugin(), + fixExtensionsPlugin(), + ], + external: ["@hey-api/client-fetch", "throttled-queue"], + outDir: "dist", + outExtension(ctx) { + return { + dts: ".d.ts", + js: ctx.format === "cjs" ? ".cjs" : ".mjs", + } + }, + target: "es2020", + esbuildOptions(options) { + options.preserveSymlinks = true + }, +}) diff --git a/packages/sdks/compatibility-layer/vitest.config.ts b/packages/sdks/compatibility-layer/vitest.config.ts new file mode 100644 index 00000000..86a85577 --- /dev/null +++ b/packages/sdks/compatibility-layer/vitest.config.ts @@ -0,0 +1,16 @@ +import { defineConfig } from "vitest/config" + +export default defineConfig({ + test: { + globals: true, + environment: "jsdom", + include: ["src/**/*.test.ts"], + exclude: ["src/__integration__/**"], // Integration tests have separate config + coverage: { + provider: "v8", + reporter: ["text", "json", "html"], + include: ["src/**/*.ts"], + exclude: ["src/**/*.test.ts", "src/__integration__/**", "src/index.ts"], + }, + }, +}) diff --git a/packages/sdks/compatibility-layer/vitest.integration.config.ts b/packages/sdks/compatibility-layer/vitest.integration.config.ts new file mode 100644 index 00000000..e73d4669 --- /dev/null +++ b/packages/sdks/compatibility-layer/vitest.integration.config.ts @@ -0,0 +1,21 @@ +import { defineConfig } from "vitest/config" + +export default defineConfig({ + test: { + globals: true, + environment: "node", // Use node for MSW server + include: ["src/__integration__/**/*.test.ts"], + testTimeout: 10000, // Allow more time for integration tests + hookTimeout: 10000, + coverage: { + provider: "v8", + reporter: ["text", "json", "html"], + include: ["src/**/*.ts"], + exclude: [ + "src/**/*.test.ts", + "src/__integration__/**", + "src/index.ts", + ], + }, + }, +})