From e28332362fe6dea34152dec7f8c8cd64522f6c5e Mon Sep 17 00:00:00 2001 From: divyamagrawal06 Date: Wed, 6 May 2026 02:39:17 +0530 Subject: [PATCH 01/10] feat: add developer console and phase-1 auth/authz for sandbox lifecycle (OSEP-0006) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Implements OSEP-0006 Phase 1 (#348). Closes #348. Server — auth / identity - Config: add AuthConfig (mode: api_key_only|api_key_and_user), AuthzConfig (roles, scope keys), ConsoleConfig (optional SPA mount). Default is api_key_only — existing deployments are unaffected. - AuthMiddleware: dual-auth path for console; trusted-header user identity set by a reverse proxy. Missing required headers → 401 MISSING_TRUSTED_IDENTITY; never falls back to anonymous. - principal.py: Principal dataclass (source, subject, role, canonical_owner/team). canonicalize_scoped_value() maps arbitrary strings to deterministic Kubernetes label-safe tokens. Server — authorization and lifecycle - authorization.py: authorize_action() enforces read_only / operator / service_admin role matrix (OSEP-0006 Table 1); 403 on role or owner/team scope mismatch. - lifecycle_helpers.py: authorize_mutating_action() wraps authorize_action and emits a mutation_audit entry on denial (403s are always recorded). apply_reserved_metadata_for_create() injects access.owner / access.team on create; merge_list_scope_from_request() enforces scope on list. - All mutating routes (create/delete/pause/resume/renew) audit success, HTTPException error, 404 not_found, 403 forbidden, and bare Exception UNEXPECTED outcomes consistently. - Optional console SPA static mount in main.py (console.enabled). Console (console/) - Standalone React + TypeScript SPA (Vite, Tailwind, dark-default). - Pages: Sandbox List (filter by state/metadata), Detail (renew, endpoint, pause/resume, delete), Create (image, entrypoint, timeout, resources, env, metadata). - API client uses user-auth path — no server API key in browser code. - Role hint from VITE_UI_ROLE disables mutating buttons for read_only; server is always the enforcement point. - AuthHint renders an explicit misconfiguration banner on 401 MISSING_TRUSTED_IDENTITY instead of silently retrying. - Vite dev proxy injects trusted headers from VITE_DEV_IDENTITY_USER / VITE_DEV_IDENTITY_ROLES for local development. - Unit tests (vitest) and Playwright integration tests with reference screenshots under docs/public/images/console/. Spec - sandbox-lifecycle.yml: document dual auth modes, 401 trusted-header semantics, 403 INSUFFICIENT_ROLE / OUT_OF_SCOPE codes, reserved metadata keys for ownership scoping. Signed-off-by: divyamagrawal06 --- .gitignore | 5 + console/index.html | 13 + console/package-lock.json | 4304 +++++++++++++++++ console/package.json | 32 + console/postcss.config.js | 6 + console/src/App.tsx | 117 + console/src/api/client.ts | 139 + console/src/api/role.ts | 15 + console/src/components/AuthHint.tsx | 39 + console/src/main.tsx | 18 + console/src/pages/CreatePage.tsx | 171 + console/src/pages/DetailPage.tsx | 271 ++ console/src/pages/ListPage.tsx | 204 + console/src/tailwind.css | 3 + console/src/vite-env.d.ts | 10 + console/tailwind.config.js | 14 + console/tests/e2e/console.integration.spec.ts | 196 + console/tests/playwright.config.example.ts | 32 + console/tests/unit/client.test.ts | 66 + console/tests/unit/role.test.ts | 25 + console/tests/vitest-setup.ts | 1 + console/tsconfig.json | 20 + console/tsconfig.node.json | 10 + console/vite.config.ts | 31 + console/vitest.config.ts | 11 + docs/.vitepress/config.mts | 97 +- .../images/console/console-auth-error.png | Bin 0 -> 70587 bytes docs/public/images/console/console-create.png | Bin 0 -> 52115 bytes docs/public/images/console/console-detail.png | Bin 0 -> 55164 bytes docs/public/images/console/console-list.png | Bin 0 -> 63812 bytes server/opensandbox_server/api/lifecycle.py | 369 +- .../api/lifecycle_helpers.py | 115 + server/opensandbox_server/config.py | 103 + .../examples/example.config.toml | 22 + server/opensandbox_server/main.py | 15 + server/opensandbox_server/middleware/auth.py | 153 +- .../middleware/authorization.py | 137 + .../middleware/principal.py | 120 + server/tests/test_auth_trusted_header.py | 129 + server/tests/test_authorization.py | 83 + server/tests/test_config.py | 16 + server/tests/test_helpers.py | 111 +- server/tests/test_lifecycle_helpers.py | 84 + server/tests/test_principal.py | 63 + server/tests/test_routes_authorization.py | 180 + server/tests/test_routes_create_delete.py | 340 +- server/tests/test_routes_endpoint_behavior.py | 241 +- server/tests/test_routes_pause_resume.py | 209 +- server/tests/test_routes_renew_expiration.py | 246 +- specs/sandbox-lifecycle.yml | 55 +- 50 files changed, 7843 insertions(+), 798 deletions(-) create mode 100644 console/index.html create mode 100644 console/package-lock.json create mode 100644 console/package.json create mode 100644 console/postcss.config.js create mode 100644 console/src/App.tsx create mode 100644 console/src/api/client.ts create mode 100644 console/src/api/role.ts create mode 100644 console/src/components/AuthHint.tsx create mode 100644 console/src/main.tsx create mode 100644 console/src/pages/CreatePage.tsx create mode 100644 console/src/pages/DetailPage.tsx create mode 100644 console/src/pages/ListPage.tsx create mode 100644 console/src/tailwind.css create mode 100644 console/src/vite-env.d.ts create mode 100644 console/tailwind.config.js create mode 100644 console/tests/e2e/console.integration.spec.ts create mode 100644 console/tests/playwright.config.example.ts create mode 100644 console/tests/unit/client.test.ts create mode 100644 console/tests/unit/role.test.ts create mode 100644 console/tests/vitest-setup.ts create mode 100644 console/tsconfig.json create mode 100644 console/tsconfig.node.json create mode 100644 console/vite.config.ts create mode 100644 console/vitest.config.ts create mode 100644 docs/public/images/console/console-auth-error.png create mode 100644 docs/public/images/console/console-create.png create mode 100644 docs/public/images/console/console-detail.png create mode 100644 docs/public/images/console/console-list.png create mode 100644 server/opensandbox_server/api/lifecycle_helpers.py create mode 100644 server/opensandbox_server/middleware/authorization.py create mode 100644 server/opensandbox_server/middleware/principal.py create mode 100644 server/tests/test_auth_trusted_header.py create mode 100644 server/tests/test_authorization.py create mode 100644 server/tests/test_lifecycle_helpers.py create mode 100644 server/tests/test_principal.py create mode 100644 server/tests/test_routes_authorization.py diff --git a/.gitignore b/.gitignore index 999702192..eb94a1459 100644 --- a/.gitignore +++ b/.gitignore @@ -274,3 +274,8 @@ kubernetes/test/kind/gvisor/runsc kubernetes/test/kind/gvisor/containerd-shim-runsc-v1 bin/ obj/ +console/dist/ +console/output/ +console/playwright-report/ +console/tests/playwright-report/ +console/playwright.config.ts diff --git a/console/index.html b/console/index.html new file mode 100644 index 000000000..492f11d99 --- /dev/null +++ b/console/index.html @@ -0,0 +1,13 @@ + + + + + + + OpenSandbox · Developer Console + + +
+ + + diff --git a/console/package-lock.json b/console/package-lock.json new file mode 100644 index 000000000..7fce48358 --- /dev/null +++ b/console/package-lock.json @@ -0,0 +1,4304 @@ +{ + "name": "opensandbox-console", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "opensandbox-console", + "dependencies": { + "react": "^19.0.0", + "react-dom": "^19.0.0", + "react-router-dom": "^7.0.0" + }, + "devDependencies": { + "@playwright/test": "^1.55.0", + "@testing-library/jest-dom": "^6.6.0", + "@testing-library/react": "^16.0.0", + "@types/react": "^19.0.0", + "@types/react-dom": "^19.0.0", + "@vitejs/plugin-react": "^4.3.0", + "autoprefixer": "^10.4.21", + "jsdom": "^25.0.0", + "postcss": "^8.5.6", + "tailwindcss": "^3.4.17", + "typescript": "^5.6.0", + "vite": "^6.0.0", + "vitest": "^3.0.0" + } + }, + "node_modules/@adobe/css-tools": { + "version": "4.4.4", + "resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.4.4.tgz", + "integrity": "sha512-Elp+iwUx5rN5+Y8xLt5/GRoG20WGoDCQ/1Fb+1LiGtvwbDavuSk0jhD/eZdckHAuzcDzccnkv+rEjyWfRx18gg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@alloc/quick-lru": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", + "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@asamuzakjp/css-color": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-3.2.0.tgz", + "integrity": "sha512-K1A6z8tS3XsmCMM86xoWdn7Fkdn9m6RSVtocUrJYIwZnFVkng/PvkEoWtOWmP+Scc6saYWHWZYbndEEXxl24jw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@csstools/css-calc": "^2.1.3", + "@csstools/css-color-parser": "^3.0.9", + "@csstools/css-parser-algorithms": "^3.0.4", + "@csstools/css-tokenizer": "^3.0.3", + "lru-cache": "^10.4.3" + } + }, + "node_modules/@asamuzakjp/css-color/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/@babel/code-frame": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", + "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.28.5", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.0.tgz", + "integrity": "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz", + "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-compilation-targets": "^7.28.6", + "@babel/helper-module-transforms": "^7.28.6", + "@babel/helpers": "^7.28.6", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/traverse": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.29.1", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz", + "integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz", + "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.28.6", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz", + "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz", + "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.28.6", + "@babel/helper-validator-identifier": "^7.28.5", + "@babel/traverse": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.28.6.tgz", + "integrity": "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.2.tgz", + "integrity": "sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.28.6", + "@babel/types": "^7.29.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.2.tgz", + "integrity": "sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.0" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-self": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz", + "integrity": "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-source": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz", + "integrity": "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/runtime": { + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.2.tgz", + "integrity": "sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/template": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", + "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.28.6", + "@babel/parser": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz", + "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/types": "^7.29.0", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", + "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@csstools/color-helpers": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-5.1.0.tgz", + "integrity": "sha512-S11EXWJyy0Mz5SYvRmY8nJYTFFd1LCNV+7cXyAgQtOOuzb4EsgfqDufL+9esx72/eLhsRdGZwaldu/h+E4t4BA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "engines": { + "node": ">=18" + } + }, + "node_modules/@csstools/css-calc": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-2.1.4.tgz", + "integrity": "sha512-3N8oaj+0juUw/1H3YwmDDJXCgTB1gKU6Hc/bB502u9zR0q2vd786XJH9QfrKIEgFlZmhZiq6epXl4rHqhzsIgQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "node_modules/@csstools/css-color-parser": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-3.1.0.tgz", + "integrity": "sha512-nbtKwh3a6xNVIp/VRuXV64yTKnb1IjTAEEh3irzS+HkKjAOYLTGNb9pmVNntZ8iVBHcWDA2Dof0QtPgFI1BaTA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "dependencies": { + "@csstools/color-helpers": "^5.1.0", + "@csstools/css-calc": "^2.1.4" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "node_modules/@csstools/css-parser-algorithms": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-3.0.5.tgz", + "integrity": "sha512-DaDeUkXZKjdGhgYaHNJTV9pV7Y9B3b644jCLs9Upc3VeNGg6LWARAT6O+Q+/COo+2gg/bM5rhpMAtf70WqfBdQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "node_modules/@csstools/css-tokenizer": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-3.0.4.tgz", + "integrity": "sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", + "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz", + "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz", + "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz", + "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz", + "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz", + "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz", + "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz", + "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz", + "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz", + "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz", + "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz", + "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz", + "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz", + "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz", + "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz", + "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz", + "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz", + "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz", + "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz", + "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz", + "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz", + "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz", + "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz", + "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz", + "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", + "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@playwright/test": { + "version": "1.59.1", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.59.1.tgz", + "integrity": "sha512-PG6q63nQg5c9rIi4/Z5lR5IVF7yU5MqmKaPOe0HSc0O2cX1fPi96sUQu5j7eo4gKCkB2AnNGoWt7y4/Xx3Kcqg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright": "1.59.1" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-beta.27", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz", + "integrity": "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.2.tgz", + "integrity": "sha512-dnlp69efPPg6Uaw2dVqzWRfAWRnYVb1XJ8CyyhIbZeaq4CA5/mLeZ1IEt9QqQxmbdvagjLIm2ZL8BxXv5lH4Yw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.60.2.tgz", + "integrity": "sha512-OqZTwDRDchGRHHm/hwLOL7uVPB9aUvI0am/eQuWMNyFHf5PSEQmyEeYYheA0EPPKUO/l0uigCp+iaTjoLjVoHg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.60.2.tgz", + "integrity": "sha512-UwRE7CGpvSVEQS8gUMBe1uADWjNnVgP3Iusyda1nSRwNDCsRjnGc7w6El6WLQsXmZTbLZx9cecegumcitNfpmA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.60.2.tgz", + "integrity": "sha512-gjEtURKLCC5VXm1I+2i1u9OhxFsKAQJKTVB8WvDAHF+oZlq0GTVFOlTlO1q3AlCTE/DF32c16ESvfgqR7343/g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.60.2.tgz", + "integrity": "sha512-Bcl6CYDeAgE70cqZaMojOi/eK63h5Me97ZqAQoh77VPjMysA/4ORQBRGo3rRy45x4MzVlU9uZxs8Uwy7ZaKnBw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.60.2.tgz", + "integrity": "sha512-LU+TPda3mAE2QB0/Hp5VyeKJivpC6+tlOXd1VMoXV/YFMvk/MNk5iXeBfB4MQGRWyOYVJ01625vjkr0Az98OJQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.60.2.tgz", + "integrity": "sha512-2QxQrM+KQ7DAW4o22j+XZ6RKdxjLD7BOWTP0Bv0tmjdyhXSsr2Ul1oJDQqh9Zf5qOwTuTc7Ek83mOFaKnodPjg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.60.2.tgz", + "integrity": "sha512-TbziEu2DVsTEOPif2mKWkMeDMLoYjx95oESa9fkQQK7r/Orta0gnkcDpzwufEcAO2BLBsD7mZkXGFqEdMRRwfw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.60.2.tgz", + "integrity": "sha512-bO/rVDiDUuM2YfuCUwZ1t1cP+/yqjqz+Xf2VtkdppefuOFS2OSeAfgafaHNkFn0t02hEyXngZkxtGqXcXwO8Rg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.60.2.tgz", + "integrity": "sha512-hr26p7e93Rl0Za+JwW7EAnwAvKkehh12BU1Llm9Ykiibg4uIr2rbpxG9WCf56GuvidlTG9KiiQT/TXT1yAWxTA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.60.2.tgz", + "integrity": "sha512-pOjB/uSIyDt+ow3k/RcLvUAOGpysT2phDn7TTUB3n75SlIgZzM6NKAqlErPhoFU+npgY3/n+2HYIQVbF70P9/A==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.60.2.tgz", + "integrity": "sha512-2/w+q8jszv9Ww1c+6uJT3OwqhdmGP2/4T17cu8WuwyUuuaCDDJ2ojdyYwZzCxx0GcsZBhzi3HmH+J5pZNXnd+Q==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.60.2.tgz", + "integrity": "sha512-11+aL5vKheYgczxtPVVRhdptAM2H7fcDR5Gw4/bTcteuZBlH4oP9f5s9zYO9aGZvoGeBpqXI/9TZZihZ609wKw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.60.2.tgz", + "integrity": "sha512-i16fokAGK46IVZuV8LIIwMdtqhin9hfYkCh8pf8iC3QU3LpwL+1FSFGej+O7l3E/AoknL6Dclh2oTdnRMpTzFQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.60.2.tgz", + "integrity": "sha512-49FkKS6RGQoriDSK/6E2GkAsAuU5kETFCh7pG4yD/ylj9rKhTmO3elsnmBvRD4PgJPds5W2PkhC82aVwmUcJ7A==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.60.2.tgz", + "integrity": "sha512-mjYNkHPfGpUR00DuM1ZZIgs64Hpf4bWcz9Z41+4Q+pgDx73UwWdAYyf6EG/lRFldmdHHzgrYyge5akFUW0D3mQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.60.2.tgz", + "integrity": "sha512-ALyvJz965BQk8E9Al/JDKKDLH2kfKFLTGMlgkAbbYtZuJt9LU8DW3ZoDMCtQpXAltZxwBHevXz5u+gf0yA0YoA==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.60.2.tgz", + "integrity": "sha512-UQjrkIdWrKI626Du8lCQ6MJp/6V1LAo2bOK9OTu4mSn8GGXIkPXk/Vsp4bLHCd9Z9Iz2OTEaokUE90VweJgIYQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.60.2.tgz", + "integrity": "sha512-bTsRGj6VlSdn/XD4CGyzMnzaBs9bsRxy79eTqTCBsA8TMIEky7qg48aPkvJvFe1HyzQ5oMZdg7AnVlWQSKLTnw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.60.2.tgz", + "integrity": "sha512-6d4Z3534xitaA1FcMWP7mQPq5zGwBmGbhphh2DwaA1aNIXUu3KTOfwrWpbwI4/Gr0uANo7NTtaykFyO2hPuFLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.60.2.tgz", + "integrity": "sha512-NetAg5iO2uN7eB8zE5qrZ3CSil+7IJt4WDFLcC75Ymywq1VZVD6qJ6EvNLjZ3rEm6gB7XW5JdT60c6MN35Z85Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.60.2.tgz", + "integrity": "sha512-NCYhOotpgWZ5kdxCZsv6Iudx0wX8980Q/oW4pNFNihpBKsDbEA1zpkfxJGC0yugsUuyDZ7gL37dbzwhR0VI7pQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.60.2.tgz", + "integrity": "sha512-RXsaOqXxfoUBQoOgvmmijVxJnW2IGB0eoMO7F8FAjaj0UTywUO/luSqimWBJn04WNgUkeNhh7fs7pESXajWmkg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.60.2.tgz", + "integrity": "sha512-qdAzEULD+/hzObedtmV6iBpdL5TIbKVztGiK7O3/KYSf+HIzU257+MX1EXJcyIiDbMAqmbwaufcYPvyRryeZtA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.60.2.tgz", + "integrity": "sha512-Nd/SgG27WoA9e+/TdK74KnHz852TLa94ovOYySo/yMPuTmpckK/jIF2jSwS3g7ELSKXK13/cVdmg1Z/DaCWKxA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@testing-library/dom": { + "version": "10.4.1", + "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.1.tgz", + "integrity": "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/code-frame": "^7.10.4", + "@babel/runtime": "^7.12.5", + "@types/aria-query": "^5.0.1", + "aria-query": "5.3.0", + "dom-accessibility-api": "^0.5.9", + "lz-string": "^1.5.0", + "picocolors": "1.1.1", + "pretty-format": "^27.0.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@testing-library/jest-dom": { + "version": "6.9.1", + "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-6.9.1.tgz", + "integrity": "sha512-zIcONa+hVtVSSep9UT3jZ5rizo2BsxgyDYU7WFD5eICBE7no3881HGeb/QkGfsJs6JTkY1aQhT7rIPC7e+0nnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@adobe/css-tools": "^4.4.0", + "aria-query": "^5.0.0", + "css.escape": "^1.5.1", + "dom-accessibility-api": "^0.6.3", + "picocolors": "^1.1.1", + "redent": "^3.0.0" + }, + "engines": { + "node": ">=14", + "npm": ">=6", + "yarn": ">=1" + } + }, + "node_modules/@testing-library/jest-dom/node_modules/dom-accessibility-api": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.6.3.tgz", + "integrity": "sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@testing-library/react": { + "version": "16.3.2", + "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-16.3.2.tgz", + "integrity": "sha512-XU5/SytQM+ykqMnAnvB2umaJNIOsLF3PVv//1Ew4CTcpz0/BRyy/af40qqrt7SjKpDdT1saBMc42CUok5gaw+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.12.5" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@testing-library/dom": "^10.0.0", + "@types/react": "^18.0.0 || ^19.0.0", + "@types/react-dom": "^18.0.0 || ^19.0.0", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@types/aria-query": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", + "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.2" + } + }, + "node_modules/@types/chai": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" + } + }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/react": { + "version": "19.2.14", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz", + "integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==", + "dev": true, + "license": "MIT", + "dependencies": { + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "19.2.3", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz", + "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^19.2.0" + } + }, + "node_modules/@vitejs/plugin-react": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz", + "integrity": "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.28.0", + "@babel/plugin-transform-react-jsx-self": "^7.27.1", + "@babel/plugin-transform-react-jsx-source": "^7.27.1", + "@rolldown/pluginutils": "1.0.0-beta.27", + "@types/babel__core": "^7.20.5", + "react-refresh": "^0.17.0" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "peerDependencies": { + "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" + } + }, + "node_modules/@vitest/expect": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.2.4.tgz", + "integrity": "sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/chai": "^5.2.2", + "@vitest/spy": "3.2.4", + "@vitest/utils": "3.2.4", + "chai": "^5.2.0", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.2.4.tgz", + "integrity": "sha512-46ryTE9RZO/rfDd7pEqFl7etuyzekzEhUbTW3BvmeO/BcCMEgq59BKhek3dXDWgAj4oMK6OZi+vRr1wPW6qjEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "3.2.4", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.17" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/pretty-format": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.2.4.tgz", + "integrity": "sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.2.4.tgz", + "integrity": "sha512-oukfKT9Mk41LreEW09vt45f8wx7DordoWUZMYdY/cyAk7w5TWkTRCNZYF7sX7n2wB7jyGAl74OxgwhPgKaqDMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "3.2.4", + "pathe": "^2.0.3", + "strip-literal": "^3.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.2.4.tgz", + "integrity": "sha512-dEYtS7qQP2CjU27QBC5oUOxLE/v5eLkGqPE0ZKEIDGMs4vKWe7IjgLOeauHsR0D5YuuycGRO5oSRXnwnmA78fQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "3.2.4", + "magic-string": "^0.30.17", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.2.4.tgz", + "integrity": "sha512-vAfasCOe6AIK70iP5UD11Ac4siNUNJ9i/9PZ3NKx07sG6sUxeag1LWdNrMWeKKYBLlzuK+Gn65Yd5nyL6ds+nw==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyspy": "^4.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.2.4.tgz", + "integrity": "sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "3.2.4", + "loupe": "^3.1.4", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/any-promise": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", + "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", + "dev": true, + "license": "MIT" + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/anymatch/node_modules/picomatch": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/arg": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", + "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==", + "dev": true, + "license": "MIT" + }, + "node_modules/aria-query": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz", + "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "dequal": "^2.0.3" + } + }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/autoprefixer": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.5.0.tgz", + "integrity": "sha512-FMhOoZV4+qR6aTUALKX2rEqGG+oyATvwBt9IIzVR5rMa2HRWPkxf+P+PAJLD1I/H5/II+HuZcBJYEFBpq39ong==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/autoprefixer" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "browserslist": "^4.28.2", + "caniuse-lite": "^1.0.30001787", + "fraction.js": "^5.3.4", + "picocolors": "^1.1.1", + "postcss-value-parser": "^4.2.0" + }, + "bin": { + "autoprefixer": "bin/autoprefixer" + }, + "engines": { + "node": "^10 || ^12 || >=14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/baseline-browser-mapping": { + "version": "2.10.23", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.23.tgz", + "integrity": "sha512-xwVXGqevyKPsiuQdLj+dZMVjidjJV508TBqexND5HrF89cGdCYCJFB3qhcxRHSeMctdCfbR1jrxBajhDy7o29g==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.28.2", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.2.tgz", + "integrity": "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.10.12", + "caniuse-lite": "^1.0.30001782", + "electron-to-chromium": "^1.5.328", + "node-releases": "^2.0.36", + "update-browserslist-db": "^1.2.3" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/cac": { + "version": "6.7.14", + "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", + "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/camelcase-css": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz", + "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001791", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001791.tgz", + "integrity": "sha512-yk0l/YSrOnFZk3UROpDLQD9+kC1l4meK/wed583AXrzoarMGJcbRi2Q4RaUYbKxYAsZ8sWmaSa/DsLmdBeI1vQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/chai": { + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/chai/-/chai-5.3.3.tgz", + "integrity": "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "assertion-error": "^2.0.1", + "check-error": "^2.1.1", + "deep-eql": "^5.0.1", + "loupe": "^3.1.0", + "pathval": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/check-error": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.3.tgz", + "integrity": "sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 16" + } + }, + "node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/chokidar/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dev": true, + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/commander": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", + "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cookie": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz", + "integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/css.escape": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/css.escape/-/css.escape-1.5.1.tgz", + "integrity": "sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "dev": true, + "license": "MIT", + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/cssstyle": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-4.6.0.tgz", + "integrity": "sha512-2z+rWdzbbSZv6/rhtvzvqeZQHrBaqgogqt85sqFNbabZOuFbCVFb8kPeEtZjiKkbrm395irpNKiYeFeLiQnFPg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@asamuzakjp/css-color": "^3.2.0", + "rrweb-cssom": "^0.8.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/cssstyle/node_modules/rrweb-cssom": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.8.0.tgz", + "integrity": "sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw==", + "dev": true, + "license": "MIT" + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/data-urls": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-5.0.0.tgz", + "integrity": "sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==", + "dev": true, + "license": "MIT", + "dependencies": { + "whatwg-mimetype": "^4.0.0", + "whatwg-url": "^14.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decimal.js": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz", + "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==", + "dev": true, + "license": "MIT" + }, + "node_modules/deep-eql": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", + "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/didyoumean": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", + "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/dlv": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", + "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==", + "dev": true, + "license": "MIT" + }, + "node_modules/dom-accessibility-api": { + "version": "0.5.16", + "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", + "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.344", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.344.tgz", + "integrity": "sha512-4MxfbmNDm+KPh066EZy+eUnkcDPcZ35wNmOWzFuh/ijvHsve6kbLTLURy88uCNK5FbpN+yk2nQY6BYh1GEt+wg==", + "dev": true, + "license": "ISC" + }, + "node_modules/entities": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", + "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-module-lexer": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", + "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", + "dev": true, + "license": "MIT" + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/esbuild": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", + "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "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" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/expect-type": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz", + "integrity": "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/fast-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fastq": { + "version": "1.20.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz", + "integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==", + "dev": true, + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/form-data": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", + "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", + "dev": true, + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fraction.js": { + "version": "5.3.4", + "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-5.3.4.tgz", + "integrity": "sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/rawify" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "dev": true, + "license": "MIT", + "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" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.3.tgz", + "integrity": "sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/html-encoding-sniffer": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-4.0.0.tgz", + "integrity": "sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "whatwg-encoding": "^3.1.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/indent-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-core-module": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-potential-custom-element-name": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", + "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/jiti": { + "version": "1.21.7", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz", + "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==", + "dev": true, + "license": "MIT", + "bin": { + "jiti": "bin/jiti.js" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/jsdom": { + "version": "25.0.1", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-25.0.1.tgz", + "integrity": "sha512-8i7LzZj7BF8uplX+ZyOlIz86V6TAsSs+np6m1kpW9u0JWi4z/1t+FzcK1aek+ybTnAC4KhBL4uXCNT0wcUIeCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssstyle": "^4.1.0", + "data-urls": "^5.0.0", + "decimal.js": "^10.4.3", + "form-data": "^4.0.0", + "html-encoding-sniffer": "^4.0.0", + "http-proxy-agent": "^7.0.2", + "https-proxy-agent": "^7.0.5", + "is-potential-custom-element-name": "^1.0.1", + "nwsapi": "^2.2.12", + "parse5": "^7.1.2", + "rrweb-cssom": "^0.7.1", + "saxes": "^6.0.0", + "symbol-tree": "^3.2.4", + "tough-cookie": "^5.0.0", + "w3c-xmlserializer": "^5.0.0", + "webidl-conversions": "^7.0.0", + "whatwg-encoding": "^3.1.1", + "whatwg-mimetype": "^4.0.0", + "whatwg-url": "^14.0.0", + "ws": "^8.18.0", + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "canvas": "^2.11.2" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } + } + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/lilconfig": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", + "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antonk52" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true, + "license": "MIT" + }, + "node_modules/loupe": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.2.1.tgz", + "integrity": "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/lz-string": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz", + "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", + "dev": true, + "license": "MIT", + "peer": true, + "bin": { + "lz-string": "bin/bin.js" + } + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/micromatch/node_modules/picomatch": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/min-indent": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", + "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/mz": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", + "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0", + "object-assign": "^4.0.1", + "thenify-all": "^1.0.0" + } + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/node-releases": { + "version": "2.0.38", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.38.tgz", + "integrity": "sha512-3qT/88Y3FbH/Kx4szpQQ4HzUbVrHPKTLVpVocKiLfoYvw9XSGOX2FmD2d6DrXbVYyAQTF2HeF6My8jmzx7/CRw==", + "dev": true, + "license": "MIT" + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/nwsapi": { + "version": "2.2.23", + "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.23.tgz", + "integrity": "sha512-7wfH4sLbt4M0gCDzGE6vzQBo0bfTKjU7Sfpqy/7gs1qBfYz2vEJH6vXcBKpO3+6Yu1telwd0t9HpyOoLEQQbIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-hash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", + "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/parse5": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz", + "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "entities": "^6.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true, + "license": "MIT" + }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, + "node_modules/pathval": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.1.tgz", + "integrity": "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.16" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/pirates": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", + "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/playwright": { + "version": "1.59.1", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.59.1.tgz", + "integrity": "sha512-C8oWjPR3F81yljW9o5OxcWzfh6avkVwDD2VYdwIGqTkl+OGFISgypqzfu7dOe4QNLL2aqcWBmI3PMtLIK233lw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright-core": "1.59.1" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.59.1", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.59.1.tgz", + "integrity": "sha512-HBV/RJg81z5BiiZ9yPzIiClYV/QMsDCKUyogwH9p3MCP6IYjUFu/MActgYAvK0oWyV9NlwM3GLBjADyWgydVyg==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/playwright/node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/postcss": { + "version": "8.5.12", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.12.tgz", + "integrity": "sha512-W62t/Se6rA0Az3DfCL0AqJwXuKwBeYg6nOaIgzP+xZ7N5BFCI7DYi1qs6ygUYT6rvfi6t9k65UMLJC+PHZpDAA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-import": { + "version": "15.1.0", + "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz", + "integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==", + "dev": true, + "license": "MIT", + "dependencies": { + "postcss-value-parser": "^4.0.0", + "read-cache": "^1.0.0", + "resolve": "^1.1.7" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "postcss": "^8.0.0" + } + }, + "node_modules/postcss-js": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.1.0.tgz", + "integrity": "sha512-oIAOTqgIo7q2EOwbhb8UalYePMvYoIeRY2YKntdpFQXNosSu3vLrniGgmH9OKs/qAkfoj5oB3le/7mINW1LCfw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "camelcase-css": "^2.0.1" + }, + "engines": { + "node": "^12 || ^14 || >= 16" + }, + "peerDependencies": { + "postcss": "^8.4.21" + } + }, + "node_modules/postcss-load-config": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-6.0.1.tgz", + "integrity": "sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "lilconfig": "^3.1.1" + }, + "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 + } + } + }, + "node_modules/postcss-nested": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.2.0.tgz", + "integrity": "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "postcss-selector-parser": "^6.1.1" + }, + "engines": { + "node": ">=12.0" + }, + "peerDependencies": { + "postcss": "^8.2.14" + } + }, + "node_modules/postcss-selector-parser": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz", + "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/pretty-format": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz", + "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "ansi-regex": "^5.0.1", + "ansi-styles": "^5.0.0", + "react-is": "^17.0.1" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/react": { + "version": "19.2.5", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.5.tgz", + "integrity": "sha512-llUJLzz1zTUBrskt2pwZgLq59AemifIftw4aB7JxOqf1HY2FDaGDxgwpAPVzHU1kdWabH7FauP4i1oEeer2WCA==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.2.5", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.5.tgz", + "integrity": "sha512-J5bAZz+DXMMwW/wV3xzKke59Af6CHY7G4uYLN1OvBcKEsWOs4pQExj86BBKamxl/Ik5bx9whOrvBlSDfWzgSag==", + "license": "MIT", + "dependencies": { + "scheduler": "^0.27.0" + }, + "peerDependencies": { + "react": "^19.2.5" + } + }, + "node_modules/react-is": { + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", + "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/react-refresh": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz", + "integrity": "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-router": { + "version": "7.14.2", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.14.2.tgz", + "integrity": "sha512-yCqNne6I8IB6rVCH7XUvlBK7/QKyqypBFGv+8dj4QBFJiiRX+FG7/nkdAvGElyvVZ/HQP5N19wzteuTARXi5Gw==", + "license": "MIT", + "dependencies": { + "cookie": "^1.0.1", + "set-cookie-parser": "^2.6.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + } + } + }, + "node_modules/react-router-dom": { + "version": "7.14.2", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.14.2.tgz", + "integrity": "sha512-YZcM5ES8jJSM+KrJ9BdvHHqlnGTg5tH3sC5ChFRj4inosKctdyzBDhOyyHdGk597q2OT6NTrCA1OvB/YDwfekQ==", + "license": "MIT", + "dependencies": { + "react-router": "7.14.2" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + } + }, + "node_modules/read-cache": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", + "integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "pify": "^2.3.0" + } + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/readdirp/node_modules/picomatch": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/redent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz", + "integrity": "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "indent-string": "^4.0.0", + "strip-indent": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve": { + "version": "1.22.12", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.12.tgz", + "integrity": "sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "is-core-module": "^2.16.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rollup": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.2.tgz", + "integrity": "sha512-J9qZyW++QK/09NyN/zeO0dG/1GdGfyp9lV8ajHnRVLfo/uFsbji5mHnDgn/qYdUHyCkM2N+8VyspgZclfAh0eQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.60.2", + "@rollup/rollup-android-arm64": "4.60.2", + "@rollup/rollup-darwin-arm64": "4.60.2", + "@rollup/rollup-darwin-x64": "4.60.2", + "@rollup/rollup-freebsd-arm64": "4.60.2", + "@rollup/rollup-freebsd-x64": "4.60.2", + "@rollup/rollup-linux-arm-gnueabihf": "4.60.2", + "@rollup/rollup-linux-arm-musleabihf": "4.60.2", + "@rollup/rollup-linux-arm64-gnu": "4.60.2", + "@rollup/rollup-linux-arm64-musl": "4.60.2", + "@rollup/rollup-linux-loong64-gnu": "4.60.2", + "@rollup/rollup-linux-loong64-musl": "4.60.2", + "@rollup/rollup-linux-ppc64-gnu": "4.60.2", + "@rollup/rollup-linux-ppc64-musl": "4.60.2", + "@rollup/rollup-linux-riscv64-gnu": "4.60.2", + "@rollup/rollup-linux-riscv64-musl": "4.60.2", + "@rollup/rollup-linux-s390x-gnu": "4.60.2", + "@rollup/rollup-linux-x64-gnu": "4.60.2", + "@rollup/rollup-linux-x64-musl": "4.60.2", + "@rollup/rollup-openbsd-x64": "4.60.2", + "@rollup/rollup-openharmony-arm64": "4.60.2", + "@rollup/rollup-win32-arm64-msvc": "4.60.2", + "@rollup/rollup-win32-ia32-msvc": "4.60.2", + "@rollup/rollup-win32-x64-gnu": "4.60.2", + "@rollup/rollup-win32-x64-msvc": "4.60.2", + "fsevents": "~2.3.2" + } + }, + "node_modules/rrweb-cssom": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.7.1.tgz", + "integrity": "sha512-TrEMa7JGdVm0UThDJSx7ddw5nVm3UJS9o9CCIZ72B1vSyEZoziDqBYP3XIoi/12lKrJR8rE3jeFHMok2F/Mnsg==", + "dev": true, + "license": "MIT" + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true, + "license": "MIT" + }, + "node_modules/saxes": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", + "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==", + "dev": true, + "license": "ISC", + "dependencies": { + "xmlchars": "^2.2.0" + }, + "engines": { + "node": ">=v12.22.7" + } + }, + "node_modules/scheduler": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", + "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/set-cookie-parser": { + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz", + "integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==", + "license": "MIT" + }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/std-env": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", + "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", + "dev": true, + "license": "MIT" + }, + "node_modules/strip-indent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz", + "integrity": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "min-indent": "^1.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-literal": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-3.1.0.tgz", + "integrity": "sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "js-tokens": "^9.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/strip-literal/node_modules/js-tokens": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-9.0.1.tgz", + "integrity": "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/sucrase": { + "version": "3.35.1", + "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.1.tgz", + "integrity": "sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.2", + "commander": "^4.0.0", + "lines-and-columns": "^1.1.6", + "mz": "^2.7.0", + "pirates": "^4.0.1", + "tinyglobby": "^0.2.11", + "ts-interface-checker": "^0.1.9" + }, + "bin": { + "sucrase": "bin/sucrase", + "sucrase-node": "bin/sucrase-node" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/symbol-tree": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", + "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", + "dev": true, + "license": "MIT" + }, + "node_modules/tailwindcss": { + "version": "3.4.19", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.19.tgz", + "integrity": "sha512-3ofp+LL8E+pK/JuPLPggVAIaEuhvIz4qNcf3nA1Xn2o/7fb7s/TYpHhwGDv1ZU3PkBluUVaF8PyCHcm48cKLWQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@alloc/quick-lru": "^5.2.0", + "arg": "^5.0.2", + "chokidar": "^3.6.0", + "didyoumean": "^1.2.2", + "dlv": "^1.1.3", + "fast-glob": "^3.3.2", + "glob-parent": "^6.0.2", + "is-glob": "^4.0.3", + "jiti": "^1.21.7", + "lilconfig": "^3.1.3", + "micromatch": "^4.0.8", + "normalize-path": "^3.0.0", + "object-hash": "^3.0.0", + "picocolors": "^1.1.1", + "postcss": "^8.4.47", + "postcss-import": "^15.1.0", + "postcss-js": "^4.0.1", + "postcss-load-config": "^4.0.2 || ^5.0 || ^6.0", + "postcss-nested": "^6.2.0", + "postcss-selector-parser": "^6.1.2", + "resolve": "^1.22.8", + "sucrase": "^3.35.0" + }, + "bin": { + "tailwind": "lib/cli.js", + "tailwindcss": "lib/cli.js" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/thenify": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", + "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0" + } + }, + "node_modules/thenify-all": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", + "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "thenify": ">= 3.1.0 < 4" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz", + "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyglobby": { + "version": "0.2.16", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz", + "integrity": "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinypool": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.1.1.tgz", + "integrity": "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.0.0 || >=20.0.0" + } + }, + "node_modules/tinyrainbow": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-2.0.0.tgz", + "integrity": "sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tinyspy": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-4.0.4.tgz", + "integrity": "sha512-azl+t0z7pw/z958Gy9svOTuzqIk6xq+NSheJzn5MMWtWTFywIacg2wUlzKFGtt3cthx0r2SxMK0yzJOR0IES7Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tldts": { + "version": "6.1.86", + "resolved": "https://registry.npmjs.org/tldts/-/tldts-6.1.86.tgz", + "integrity": "sha512-WMi/OQ2axVTf/ykqCQgXiIct+mSQDFdH2fkwhPwgEwvJ1kSzZRiinb0zF2Xb8u4+OqPChmyI6MEu4EezNJz+FQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tldts-core": "^6.1.86" + }, + "bin": { + "tldts": "bin/cli.js" + } + }, + "node_modules/tldts-core": { + "version": "6.1.86", + "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-6.1.86.tgz", + "integrity": "sha512-Je6p7pkk+KMzMv2XXKmAE3McmolOQFdxkKw0R8EYNr7sELW46JqnNeTX8ybPiQgvg1ymCoF8LXs5fzFaZvJPTA==", + "dev": true, + "license": "MIT" + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/tough-cookie": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-5.1.2.tgz", + "integrity": "sha512-FVDYdxtnj0G6Qm/DhNPSb8Ju59ULcup3tuJxkFb5K8Bv2pUXILbf0xZWU8PX8Ov19OXljbUyveOFwRMwkXzO+A==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "tldts": "^6.1.32" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/tr46": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-5.1.1.tgz", + "integrity": "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==", + "dev": true, + "license": "MIT", + "dependencies": { + "punycode": "^2.3.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/ts-interface-checker": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", + "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true, + "license": "MIT" + }, + "node_modules/vite": { + "version": "6.4.2", + "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.2.tgz", + "integrity": "sha512-2N/55r4JDJ4gdrCvGgINMy+HH3iRpNIz8K6SFwVsA+JbQScLiC+clmAxBgwiSPgcG9U15QmvqCGWzMbqda5zGQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.25.0", + "fdir": "^6.4.4", + "picomatch": "^4.0.2", + "postcss": "^8.5.3", + "rollup": "^4.34.9", + "tinyglobby": "^0.2.13" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "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 + } + } + }, + "node_modules/vite-node": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.2.4.tgz", + "integrity": "sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cac": "^6.7.14", + "debug": "^4.4.1", + "es-module-lexer": "^1.7.0", + "pathe": "^2.0.3", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" + }, + "bin": { + "vite-node": "vite-node.mjs" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/vitest": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.2.4.tgz", + "integrity": "sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/chai": "^5.2.2", + "@vitest/expect": "3.2.4", + "@vitest/mocker": "3.2.4", + "@vitest/pretty-format": "^3.2.4", + "@vitest/runner": "3.2.4", + "@vitest/snapshot": "3.2.4", + "@vitest/spy": "3.2.4", + "@vitest/utils": "3.2.4", + "chai": "^5.2.0", + "debug": "^4.4.1", + "expect-type": "^1.2.1", + "magic-string": "^0.30.17", + "pathe": "^2.0.3", + "picomatch": "^4.0.2", + "std-env": "^3.9.0", + "tinybench": "^2.9.0", + "tinyexec": "^0.3.2", + "tinyglobby": "^0.2.14", + "tinypool": "^1.1.1", + "tinyrainbow": "^2.0.0", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0", + "vite-node": "3.2.4", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@types/debug": "^4.1.12", + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "@vitest/browser": "3.2.4", + "@vitest/ui": "3.2.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 + } + } + }, + "node_modules/w3c-xmlserializer": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz", + "integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/webidl-conversions": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", + "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/whatwg-encoding": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz", + "integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==", + "deprecated": "Use @exodus/bytes instead for a more spec-conformant and faster implementation", + "dev": true, + "license": "MIT", + "dependencies": { + "iconv-lite": "0.6.3" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/whatwg-mimetype": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz", + "integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/whatwg-url": { + "version": "14.2.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.2.0.tgz", + "integrity": "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "tr46": "^5.1.0", + "webidl-conversions": "^7.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/ws": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.20.0.tgz", + "integrity": "sha512-sAt8BhgNbzCtgGbt2OxmpuryO63ZoDk/sqaB/znQm94T4fCEsy/yV+7CdC1kJhOU9lboAEU7R3kquuycDoibVA==", + "dev": true, + "license": "MIT", + "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 + } + } + }, + "node_modules/xml-name-validator": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz", + "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/xmlchars": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", + "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", + "dev": true, + "license": "MIT" + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + } + } +} diff --git a/console/package.json b/console/package.json new file mode 100644 index 000000000..a791bd678 --- /dev/null +++ b/console/package.json @@ -0,0 +1,32 @@ +{ + "name": "opensandbox-console", + "private": true, + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc -b && vite build", + "preview": "vite preview", + "test": "vitest run", + "test:e2e": "playwright test --config tests/playwright.config.example.ts" + }, + "dependencies": { + "react": "^19.0.0", + "react-dom": "^19.0.0", + "react-router-dom": "^7.0.0" + }, + "devDependencies": { + "@testing-library/jest-dom": "^6.6.0", + "@testing-library/react": "^16.0.0", + "@types/react": "^19.0.0", + "@types/react-dom": "^19.0.0", + "@playwright/test": "^1.55.0", + "@vitejs/plugin-react": "^4.3.0", + "autoprefixer": "^10.4.21", + "jsdom": "^25.0.0", + "postcss": "^8.5.6", + "tailwindcss": "^3.4.17", + "typescript": "^5.6.0", + "vite": "^6.0.0", + "vitest": "^3.0.0" + } +} diff --git a/console/postcss.config.js b/console/postcss.config.js new file mode 100644 index 000000000..daedffd2b --- /dev/null +++ b/console/postcss.config.js @@ -0,0 +1,6 @@ +export default { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +}; diff --git a/console/src/App.tsx b/console/src/App.tsx new file mode 100644 index 000000000..790dbe0ff --- /dev/null +++ b/console/src/App.tsx @@ -0,0 +1,117 @@ +import { useEffect, useState } from "react"; +import { Link, NavLink, Route, Routes } from "react-router-dom"; +import { CreatePage } from "./pages/CreatePage"; +import { DetailPage } from "./pages/DetailPage"; +import { ListPage } from "./pages/ListPage"; + +function ConsoleNav() { + return ( + + ); +} + +export function App() { + const [dark, setDark] = useState(() => { + const saved = localStorage.getItem("os-console-theme"); + if (saved === "dark") return true; + if (saved === "light") return false; + return true; + }); + + useEffect(() => { + document.documentElement.classList.toggle("dark", dark); + localStorage.setItem("os-console-theme", dark ? "dark" : "light"); + }, [dark]); + + return ( +
+
+
+ + OpenSandbox logo + OpenSandbox + +
+ + +
+
+
+
+ + } /> + } /> + } /> + +
+
+ ); +} diff --git a/console/src/api/client.ts b/console/src/api/client.ts new file mode 100644 index 000000000..d5a50f62b --- /dev/null +++ b/console/src/api/client.ts @@ -0,0 +1,139 @@ +/** + * Lifecycle API client (user-auth path: no API key in the browser; proxy injects headers). + */ + +const API_PREFIX = (import.meta.env.VITE_API_PREFIX as string | undefined) ?? "/v1"; + +export type ApiErrorBody = { code: string; message: string }; + +export class ApiError extends Error { + constructor( + public status: number, + public body: ApiErrorBody, + ) { + super(body.message); + this.name = "ApiError"; + } +} + +export interface SandboxListItem { + id: string; + status: { state: string; reason?: string; message?: string }; + metadata?: Record; + image: { uri: string }; + expiresAt: string; + createdAt: string; + entrypoint: string[]; +} + +export interface ListResponse { + items: SandboxListItem[]; + pagination?: { page: number; pageSize: number; total: number }; +} + +async function parseJson(res: Response): Promise<{ data: unknown; text: string }> { + const text = await res.text(); + if (!text) { + return { data: null, text: "" }; + } + try { + return { data: JSON.parse(text) as unknown, text }; + } catch { + return { data: null, text }; + } +} + +export async function apiFetch(path: string, init?: RequestInit): Promise { + const res = await fetch(`${API_PREFIX}${path}`, { + ...init, + headers: { + "Content-Type": "application/json", + ...(init?.headers ?? {}), + }, + }); + if (res.status === 204 || res.status === 205) { + return null as T; + } + const parsed = await parseJson(res); + const data = parsed.data as Record | null; + if (!res.ok) { + const code = typeof data?.code === "string" ? data.code : "HTTP_ERROR"; + let message = typeof data?.message === "string" ? data.message : ""; + if (!message && parsed.text && parsed.text.length < 240) { + message = parsed.text; + } + if (!message && res.status === 500) { + message = "API proxy returned 500. Ensure the server is running on http://127.0.0.1:8080."; + } + if (!message) { + message = res.statusText || `HTTP ${res.status}`; + } + throw new ApiError(res.status, { code, message }); + } + return data as T; +} + +export function listSandboxes(params: { state?: string; metadata?: string; page?: number; pageSize?: number }) { + const q = new URLSearchParams(); + if (params.state) { + q.set("state", params.state); + } + if (params.metadata) { + q.set("metadata", params.metadata); + } + if (params.page != null) { + q.set("page", String(params.page)); + } + if (params.pageSize != null) { + q.set("pageSize", String(params.pageSize)); + } + const qs = q.toString(); + return apiFetch(`/sandboxes${qs ? `?${qs}` : ""}`); +} + +export function getSandbox(id: string) { + return apiFetch(`/sandboxes/${encodeURIComponent(id)}`); +} + +export interface CreatePayload { + image: { uri: string }; + timeout: number; + resourceLimits: Record; + entrypoint: string[]; + env?: Record | null; + metadata?: Record; +} + +export function createSandbox(body: CreatePayload) { + return apiFetch<{ + id: string; + status: { state: string }; + metadata?: Record; + expiresAt: string; + }>("/sandboxes", { method: "POST", body: JSON.stringify(body) }); +} + +export function deleteSandbox(id: string) { + return apiFetch(`/sandboxes/${encodeURIComponent(id)}`, { method: "DELETE" }); +} + +export function renewExpiration(id: string, expiresAt: string) { + return apiFetch<{ expiresAt: string }>(`/sandboxes/${encodeURIComponent(id)}/renew-expiration`, { + method: "POST", + body: JSON.stringify({ expiresAt }), + }); +} + +export function getEndpoint(id: string, port: number, useServerProxy = false) { + return apiFetch<{ endpoint: string }>( + `/sandboxes/${encodeURIComponent(id)}/endpoints/${port}?use_server_proxy=${useServerProxy ? "true" : "false"}`, + ); +} + +export function pauseSandbox(id: string) { + return apiFetch(`/sandboxes/${encodeURIComponent(id)}/pause`, { method: "POST" }); +} + +export function resumeSandbox(id: string) { + return apiFetch(`/sandboxes/${encodeURIComponent(id)}/resume`, { method: "POST" }); +} diff --git a/console/src/api/role.ts b/console/src/api/role.ts new file mode 100644 index 000000000..5c7039092 --- /dev/null +++ b/console/src/api/role.ts @@ -0,0 +1,15 @@ +/** + * Role hints for UI only (server enforces authorization). + * When using trusted headers, map X-OpenSandbox-Roles: operator | read_only. + */ +export function parseRoleFromEnv(): "operator" | "read_only" { + const r = (import.meta.env.VITE_UI_ROLE as string | undefined)?.toLowerCase() ?? "operator"; + if (r.includes("read")) { + return "read_only"; + } + return "operator"; +} + +export function canMutate(role: "operator" | "read_only"): boolean { + return role === "operator"; +} diff --git a/console/src/components/AuthHint.tsx b/console/src/components/AuthHint.tsx new file mode 100644 index 000000000..70998c2a4 --- /dev/null +++ b/console/src/components/AuthHint.tsx @@ -0,0 +1,39 @@ +import { ApiError } from "../api/client"; + +export function AuthHint({ error }: { error: unknown }) { + if (error instanceof ApiError && (error.status === 401 || error.body.code === "MISSING_TRUSTED_IDENTITY")) { + return ( +
+ Authentication required. This console expects the API to accept trusted identity headers + (for example X-OpenSandbox-User and X-OpenSandbox-Roles) when{" "} + auth.mode = "api_key_and_user" in the server. In local dev, set + VITE_DEV_IDENTITY_USER and start the Vite dev server so the proxy can add these headers, or + use a reverse proxy in front of the server. +
+ ); + } + return null; +} + +export function ErrorBanner({ message, code }: { message: string; code?: string }) { + return ( +
+ {code ? ( + + {code} + {": "} + + ) : null} + {message} +
+ ); +} + +export function K8sPauseNote() { + return ( +

+ Pause and resume are not supported on every runtime (for example, some Kubernetes setups return{" "} + 501). +

+ ); +} diff --git a/console/src/main.tsx b/console/src/main.tsx new file mode 100644 index 000000000..aaa97ef1b --- /dev/null +++ b/console/src/main.tsx @@ -0,0 +1,18 @@ +import { StrictMode } from "react"; +import { createRoot } from "react-dom/client"; +import { BrowserRouter } from "react-router-dom"; +import { App } from "./App"; +import "./tailwind.css"; + +const el = document.getElementById("root"); +const rawBase = import.meta.env.BASE_URL; +const routerBasename = rawBase === "/" ? undefined : rawBase.replace(/\/$/, ""); +if (el) { + createRoot(el).render( + + + + + , + ); +} diff --git a/console/src/pages/CreatePage.tsx b/console/src/pages/CreatePage.tsx new file mode 100644 index 000000000..12fe758ae --- /dev/null +++ b/console/src/pages/CreatePage.tsx @@ -0,0 +1,171 @@ +import { type FormEvent, useState } from "react"; +import { Link, useNavigate } from "react-router-dom"; +import { ApiError, createSandbox } from "../api/client"; +import { canMutate, parseRoleFromEnv } from "../api/role"; +import { AuthHint, ErrorBanner } from "../components/AuthHint"; + +export function CreatePage() { + const nav = useNavigate(); + const [image, setImage] = useState("python:3.11"); + const [timeout, setTimeoutSec] = useState(3600); + const [cpu, setCpu] = useState("500m"); + const [mem, setMem] = useState("512Mi"); + const [entrypoint, setEntrypoint] = useState("python3, -c, print(1)"); + const [err, setErr] = useState(null); + const [submitting, setSubmitting] = useState(false); + const role = parseRoleFromEnv(); + const mutate = canMutate(role); + + async function onSubmit(e: FormEvent) { + e.preventDefault(); + setErr(null); + setSubmitting(true); + const ep = entrypoint + .split(",") + .map((s) => s.trim()) + .filter(Boolean); + if (ep.length < 1) { + setErr(new Error("Entrypoint must have at least one part.")); + setSubmitting(false); + return; + } + try { + const res = await createSandbox({ + image: { uri: image }, + timeout, + resourceLimits: { cpu, memory: mem }, + entrypoint: ep, + env: { CONSOLE: "1" }, + }); + nav(`/sandboxes/${encodeURIComponent(res.id)}`); + } catch (ex) { + setErr(ex); + } finally { + setSubmitting(false); + } + } + + if (!mutate) { + return ( +
+
+ +
+
+ Read-only role. You cannot create sandboxes. Ask an operator to change your + X-OpenSandbox-Roles to operator (or set VITE_DEV_IDENTITY_ROLES=operator{" "} + for the dev UI hint). +
+
+ ); + } + + return ( +
+
+ +
+

Create sandbox

+

+ Reserved metadata for owner/team scope is injected on the server for user-authenticated requests. Do not expect + to set access.owner from the browser. +

+ + {err && !(err instanceof ApiError) ? : null} + {err instanceof ApiError ? : null} + +
+
+ + setImage(e.target.value)} + required + /> +
+
+ + setTimeoutSec(Number(e.target.value))} + required + /> +
+
+
+ + setCpu(e.target.value)} + /> +
+
+ + setMem(e.target.value)} + /> +
+
+
+ + setEntrypoint(e.target.value)} + required + /> +
+ +
+
+ ); +} diff --git a/console/src/pages/DetailPage.tsx b/console/src/pages/DetailPage.tsx new file mode 100644 index 000000000..89756ae42 --- /dev/null +++ b/console/src/pages/DetailPage.tsx @@ -0,0 +1,271 @@ +import { useCallback, useEffect, useState } from "react"; +import { Link, useNavigate, useParams } from "react-router-dom"; +import { + ApiError, + deleteSandbox, + getEndpoint, + getSandbox, + pauseSandbox, + renewExpiration, + resumeSandbox, + type SandboxListItem, +} from "../api/client"; +import { canMutate, parseRoleFromEnv } from "../api/role"; +import { AuthHint, ErrorBanner, K8sPauseNote } from "../components/AuthHint"; + +export function DetailPage() { + const { id: rawId } = useParams(); + const id = rawId ? decodeURIComponent(rawId) : ""; + const nav = useNavigate(); + const [box, setBox] = useState(null); + const [err, setErr] = useState(null); + const [endpoint, setEndpoint] = useState(null); + const [port, setPort] = useState("8080"); + const [renewAt, setRenewAt] = useState(""); + const [busy, setBusy] = useState(false); + const role = parseRoleFromEnv(); + const mutate = canMutate(role); + + const load = useCallback(async () => { + if (!id) { + return; + } + setErr(null); + try { + const s = await getSandbox(id); + setBox(s); + } catch (e) { + setErr(e); + } + }, [id]); + + useEffect(() => { + void load(); + }, [load]); + + async function onFetchEndpoint() { + if (!id) { + return; + } + setErr(null); + setEndpoint(null); + const p = Number(port); + if (!Number.isFinite(p) || p < 1 || p > 65535) { + setErr(new Error("Invalid port")); + return; + } + try { + const e = await getEndpoint(id, p, false); + setEndpoint(e.endpoint); + } catch (e) { + setErr(e); + } + } + + async function onRenew() { + if (!id || !renewAt) { + return; + } + setBusy(true); + setErr(null); + try { + await renewExpiration(id, renewAt); + await load(); + } catch (e) { + setErr(e); + } finally { + setBusy(false); + } + } + + async function onDelete() { + if (!id || !window.confirm("Delete this sandbox?")) { + return; + } + setBusy(true); + setErr(null); + try { + await deleteSandbox(id); + nav("/"); + } catch (e) { + setErr(e); + } finally { + setBusy(false); + } + } + + async function onPause() { + if (!id) { + return; + } + setBusy(true); + setErr(null); + try { + await pauseSandbox(id); + await load(); + } catch (e) { + setErr(e); + } finally { + setBusy(false); + } + } + + async function onResume() { + if (!id) { + return; + } + setBusy(true); + setErr(null); + try { + await resumeSandbox(id); + await load(); + } catch (e) { + setErr(e); + } finally { + setBusy(false); + } + } + + if (!id) { + return

Missing id.

; + } + + return ( +
+
+ +
+ + {err && !(err instanceof ApiError) ? : null} + {err instanceof ApiError && err.status !== 401 ? ( + + ) : null} + + {box && ( +
+

+ {box.id} + + {box.status.state} + +

+

Image: {box.image?.uri}

+

Expires: {box.expiresAt}

+ {box.metadata && Object.keys(box.metadata).length > 0 && ( +
+

+ Metadata +

+
{JSON.stringify(box.metadata, null, 2)}
+
+ )} +

+ entrypoint: {JSON.stringify(box.entrypoint)} +

+
+ )} + +
+

Get endpoint

+

Resolves a published port to a reachable host (per server ingress settings).

+
+ setPort(e.target.value)} + /> + +
+ {endpoint && ( +

+ {endpoint} +

+ )} +
+ + {mutate && ( +
+

Renew expiration

+
+ + setRenewAt(e.target.value)} + placeholder="2030-01-01T12:00:00Z" + /> +
+ +
+ )} + + {mutate && ( +
+

Lifecycle

+ +

+ + + +

+
+ )} + + {!mutate && ( +
+ Read-only. Your UI role is read_only; create, renew, delete, and pause are + hidden. The server is always authoritative. +
+ )} +
+ ); +} diff --git a/console/src/pages/ListPage.tsx b/console/src/pages/ListPage.tsx new file mode 100644 index 000000000..6b3e82f7e --- /dev/null +++ b/console/src/pages/ListPage.tsx @@ -0,0 +1,204 @@ +import { useCallback, useEffect, useState } from "react"; +import { Link } from "react-router-dom"; +import { ApiError, listSandboxes, type ListResponse } from "../api/client"; +import { AuthHint, ErrorBanner } from "../components/AuthHint"; +import { parseRoleFromEnv } from "../api/role"; + +const STATES = ["", "Running", "Pending", "Paused", "Stopping", "Terminated", "Failed"]; + +export function ListPage() { + const [data, setData] = useState(null); + const [err, setErr] = useState(null); + const [stateFilter, setStateFilter] = useState(""); + const [metaQuery, setMetaQuery] = useState(""); + const [page, setPage] = useState(1); + const [loading, setLoading] = useState(true); + const role = parseRoleFromEnv(); + + const load = useCallback(async () => { + setLoading(true); + setErr(null); + try { + const r = await listSandboxes({ + state: stateFilter || undefined, + metadata: metaQuery || undefined, + page, + pageSize: 20, + }); + setData(r); + } catch (e) { + setErr(e); + } finally { + setLoading(false); + } + }, [stateFilter, metaQuery, page]); + + useEffect(() => { + void load(); + }, [load]); + + return ( +
+
+
+

+ OpenSandbox Console +

+ +

+ Lifecycle operations for AI sandboxes. +

+

+ List, inspect, renew, and manage sandbox instances. +

+
+ + + Create sandbox + +
+
+
+

+ Sandboxes + + {data?.items?.length ?? 0} on page + +

+

+ UI role: {role} (server enforces real role) +

+

+ Current page: {page} {loading ? "· Loading..." : ""} +

+
+
+ + + {err && !(err instanceof ApiError) ? : null} + {err instanceof ApiError && err.status !== 401 ? ( + + ) : null} + +
+
+
+ + +
+
+ + { + setPage(1); + setMetaQuery(e.target.value); + }} + placeholder="e.g. project%3Ddemo" + /> +
+
+

+ Server-side owner/team scope (reserved metadata keys) applies automatically for console users; API key clients + are unchanged. +

+
+ + Page {page} + + +
+
+ + {loading && !data ?

Loading…

: null} + {data && ( +
+ + + + + + + + + + + {data.items?.map((s) => ( + + + + + + + ))} + +
IDStateImageExpires
+ {s.id} + + + {s.status.state} + + + {s.image?.uri} + + {s.expiresAt} +
+ {data.items?.length === 0 &&

No sandboxes match the filters.

} +
+ )} +
+ ); +} diff --git a/console/src/tailwind.css b/console/src/tailwind.css new file mode 100644 index 000000000..04b35af2a --- /dev/null +++ b/console/src/tailwind.css @@ -0,0 +1,3 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; diff --git a/console/src/vite-env.d.ts b/console/src/vite-env.d.ts new file mode 100644 index 000000000..e53817dee --- /dev/null +++ b/console/src/vite-env.d.ts @@ -0,0 +1,10 @@ +/// + +interface ImportMetaEnv { + readonly VITE_API_PREFIX: string; + readonly VITE_SANDBOX_HELP_RUNTIME_PAUSE: string; +} + +interface ImportMeta { + readonly env: ImportMetaEnv; +} diff --git a/console/tailwind.config.js b/console/tailwind.config.js new file mode 100644 index 000000000..6c318d9fd --- /dev/null +++ b/console/tailwind.config.js @@ -0,0 +1,14 @@ +/** @type {import('tailwindcss').Config} */ +export default { + darkMode: "class", + content: ["./index.html", "./src/**/*.{ts,tsx}"], + theme: { + extend: { + colors: { + "os-brand": "#2563eb", + "os-brand-dark": "#1d4ed8", + }, + }, + }, + plugins: [], +}; diff --git a/console/tests/e2e/console.integration.spec.ts b/console/tests/e2e/console.integration.spec.ts new file mode 100644 index 000000000..b6c4ae795 --- /dev/null +++ b/console/tests/e2e/console.integration.spec.ts @@ -0,0 +1,196 @@ +import { expect, test } from "@playwright/test"; +import { copyFileSync, mkdirSync } from "node:fs"; + +const shotsDir = "output/playwright"; +const docsShotsDir = "../docs/public/images/console"; + +test.beforeAll(() => { + mkdirSync(shotsDir, { recursive: true }); + mkdirSync(docsShotsDir, { recursive: true }); +}); + +test.beforeEach(async ({ page }) => { + await page.emulateMedia({ colorScheme: "dark" }); + + await page.route("**/v1/sandboxes?**", async (route) => { + const url = route.request().url(); + if (url.includes("state=Running")) { + await route.fulfill({ + status: 200, + contentType: "application/json", + body: JSON.stringify({ + items: [ + { + id: "sbx-running", + image: { uri: "python:3.11" }, + status: { state: "Running" }, + metadata: { "access.owner": "alice", project: "docs-demo" }, + entrypoint: ["python", "-V"], + expiresAt: "2030-01-01T00:00:00Z", + createdAt: "2029-12-31T00:00:00Z", + }, + ], + pagination: { page: 1, pageSize: 20, totalItems: 1, totalPages: 1, hasNextPage: false }, + }), + }); + return; + } + + await route.fulfill({ + status: 200, + contentType: "application/json", + body: JSON.stringify({ + items: [ + { + id: "sbx-001", + image: { uri: "python:3.11" }, + status: { state: "Running" }, + metadata: { "access.owner": "alice", project: "docs-demo" }, + entrypoint: ["python", "-V"], + expiresAt: "2030-01-01T00:00:00Z", + createdAt: "2029-12-31T00:00:00Z", + }, + { + id: "sbx-002", + image: { uri: "node:20" }, + status: { state: "Failed" }, + metadata: { "access.owner": "alice", project: "docs-demo" }, + entrypoint: ["node", "-v"], + expiresAt: "2030-01-01T00:00:00Z", + createdAt: "2029-12-31T00:00:00Z", + }, + ], + pagination: { page: 1, pageSize: 20, totalItems: 2, totalPages: 1, hasNextPage: false }, + }), + }); + }); + + await page.route("**/v1/sandboxes/sbx-001", async (route) => { + if (route.request().method() === "DELETE") { + await route.fulfill({ status: 204, body: "" }); + return; + } + await route.fulfill({ + status: 200, + contentType: "application/json", + body: JSON.stringify({ + id: "sbx-001", + image: { uri: "python:3.11" }, + status: { state: "Running" }, + metadata: { "access.owner": "alice", project: "docs-demo" }, + entrypoint: ["python", "-V"], + expiresAt: "2030-01-01T00:00:00Z", + createdAt: "2029-12-31T00:00:00Z", + }), + }); + }); + + await page.route("**/v1/sandboxes/sbx-001/endpoints/**", async (route) => { + await route.fulfill({ + status: 200, + contentType: "application/json", + body: JSON.stringify({ endpoint: "sandbox.example.com/sbx-001/8080" }), + }); + }); + + await page.route("**/v1/sandboxes/sbx-001/renew-expiration", async (route) => { + await route.fulfill({ + status: 200, + contentType: "application/json", + body: JSON.stringify({ expiresAt: "2030-01-02T00:00:00Z" }), + }); + }); + + await page.route("**/v1/sandboxes/sbx-001/pause", async (route) => { + await route.fulfill({ status: 202, body: "" }); + }); + await page.route("**/v1/sandboxes/sbx-001/resume", async (route) => { + await route.fulfill({ status: 202, body: "" }); + }); + + await page.route("**/v1/sandboxes", async (route) => { + if (route.request().method() === "POST") { + await route.fulfill({ + status: 202, + contentType: "application/json", + body: JSON.stringify({ + id: "sbx-created", + status: { state: "Pending" }, + metadata: { "access.owner": "alice" }, + expiresAt: "2030-01-01T00:00:00Z", + createdAt: "2029-12-31T00:00:00Z", + entrypoint: ["python", "-V"], + }), + }); + return; + } + await route.fallback(); + }); + + await page.route("**/v1/sandboxes/sbx-created", async (route) => { + await route.fulfill({ + status: 200, + contentType: "application/json", + body: JSON.stringify({ + id: "sbx-created", + image: { uri: "python:3.11" }, + status: { state: "Pending" }, + metadata: { "access.owner": "alice" }, + entrypoint: ["python", "-V"], + expiresAt: "2030-01-01T00:00:00Z", + createdAt: "2029-12-31T00:00:00Z", + }), + }); + }); +}); + +function saveScreenshot(tempPath: string, fileName: string) { + const finalPath = `${shotsDir}/${fileName}`; + const docsPath = `${docsShotsDir}/${fileName}`; + copyFileSync(tempPath, finalPath); + copyFileSync(tempPath, docsPath); +} + +test("list/detail/create lifecycle flows render and work", async ({ page }, testInfo) => { + await page.goto("/console/"); + await expect(page.getByRole("heading", { name: "OpenSandbox Console" })).toBeVisible(); + await expect(page.getByRole("link", { name: "sbx-001" })).toBeVisible(); + const listShot = testInfo.outputPath("console-list.png"); + await page.screenshot({ path: listShot, fullPage: true }); + saveScreenshot(listShot, "console-list.png"); + + await page.getByRole("link", { name: "sbx-001" }).click(); + await expect(page.getByRole("heading", { name: "sbx-001" })).toBeVisible(); + await page.fill('input[placeholder="2030-01-01T12:00:00Z"]', "2030-01-02T00:00:00Z"); + await page.getByRole("button", { name: "Renew" }).click(); + await page.fill("input", "8080"); + await page.getByRole("button", { name: "Get endpoint" }).click(); + await expect(page.getByText("sandbox.example.com/sbx-001/8080")).toBeVisible(); + const detailShot = testInfo.outputPath("console-detail.png"); + await page.screenshot({ path: detailShot, fullPage: true }); + saveScreenshot(detailShot, "console-detail.png"); + + await page.getByRole("link", { name: "Create" }).click(); + await page.getByRole("button", { name: "Create" }).click(); + await expect(page.getByRole("heading", { name: "sbx-created" })).toBeVisible(); + const createShot = testInfo.outputPath("console-create.png"); + await page.screenshot({ path: createShot, fullPage: true }); + saveScreenshot(createShot, "console-create.png"); +}); + +test("auth misconfiguration banner is shown on 401 missing trusted identity", async ({ page }, testInfo) => { + await page.unroute("**/v1/sandboxes?**"); + await page.route("**/v1/sandboxes?**", async (route) => { + await route.fulfill({ + status: 401, + contentType: "application/json", + body: JSON.stringify({ code: "MISSING_TRUSTED_IDENTITY", message: "missing trusted headers" }), + }); + }); + + await page.goto("/console/"); + await expect(page.getByText("Authentication required.")).toBeVisible(); + const authShot = testInfo.outputPath("console-auth-error.png"); + await page.screenshot({ path: authShot, fullPage: true }); + saveScreenshot(authShot, "console-auth-error.png"); +}); diff --git a/console/tests/playwright.config.example.ts b/console/tests/playwright.config.example.ts new file mode 100644 index 000000000..e39e85884 --- /dev/null +++ b/console/tests/playwright.config.example.ts @@ -0,0 +1,32 @@ +import { defineConfig, devices } from "@playwright/test"; + +/** + * Copy to `playwright.config.ts` at the console package root if you need local overrides. + * CI and default `npm run test:e2e` use this file via `--config`. + */ +export default defineConfig({ + testDir: "./e2e", + timeout: 60_000, + expect: { timeout: 10_000 }, + reporter: [["list"], ["html", { outputFolder: "playwright-report", open: "never" }]], + use: { + baseURL: "http://127.0.0.1:4173", + colorScheme: "dark", + trace: "on-first-retry", + screenshot: "only-on-failure", + video: "retain-on-failure", + }, + webServer: { + command: "npm run dev -- --host 127.0.0.1 --port 4173", + url: "http://127.0.0.1:4173/console/", + cwd: "..", + reuseExistingServer: true, + timeout: 120_000, + }, + projects: [ + { + name: "chromium", + use: { ...devices["Desktop Chrome"] }, + }, + ], +}); diff --git a/console/tests/unit/client.test.ts b/console/tests/unit/client.test.ts new file mode 100644 index 000000000..32622c15e --- /dev/null +++ b/console/tests/unit/client.test.ts @@ -0,0 +1,66 @@ +import { describe, expect, it, vi, beforeEach, afterEach } from "vitest"; +import { ApiError, apiFetch } from "../../src/api/client"; + +describe("apiFetch", () => { + beforeEach(() => { + vi.stubGlobal( + "fetch", + vi.fn(() => + Promise.resolve( + new Response(JSON.stringify({ items: [], pagination: { page: 1, pageSize: 20, total: 0 } }), { + status: 200, + headers: { "Content-Type": "application/json" }, + }), + ), + ), + ); + }); + afterEach(() => { + vi.unstubAllGlobals(); + }); + + it("parses json on success", async () => { + const r = await apiFetch<{ + items: unknown[]; + pagination: { page: number; pageSize: number; total: number }; + }>("/sandboxes"); + expect(r.items).toEqual([]); + }); + + it("throws ApiError on 401 with body", async () => { + vi.stubGlobal( + "fetch", + vi.fn(() => + Promise.resolve( + new Response(JSON.stringify({ code: "MISSING_TRUSTED_IDENTITY", message: "nope" }), { + status: 401, + headers: { "Content-Type": "application/json" }, + }), + ), + ), + ); + await expect(apiFetch("/x")).rejects.toMatchObject({ status: 401, body: { code: "MISSING_TRUSTED_IDENTITY" } }); + }); + + it("returns null for 204", async () => { + vi.stubGlobal("fetch", vi.fn(() => Promise.resolve(new Response(null, { status: 204 })))); + const r = await apiFetch("/sandboxes/abc"); + expect(r).toBeNull(); + }); + + it("shows helpful message for 500 without json body", async () => { + vi.stubGlobal("fetch", vi.fn(() => Promise.resolve(new Response("Internal Server Error", { status: 500 })))); + await expect(apiFetch("/x")).rejects.toMatchObject({ + status: 500, + body: { code: "HTTP_ERROR", message: "Internal Server Error" }, + }); + }); +}); + +describe("ApiError", () => { + it("exposes code and message", () => { + const e = new ApiError(403, { code: "INSUFFICIENT_ROLE", message: "no" }); + expect(e.status).toBe(403); + expect(e.body.code).toBe("INSUFFICIENT_ROLE"); + }); +}); diff --git a/console/tests/unit/role.test.ts b/console/tests/unit/role.test.ts new file mode 100644 index 000000000..8d2b58ae8 --- /dev/null +++ b/console/tests/unit/role.test.ts @@ -0,0 +1,25 @@ +import { describe, expect, it, vi, afterEach } from "vitest"; +import { canMutate, parseRoleFromEnv } from "../../src/api/role"; + +describe("parseRoleFromEnv", () => { + afterEach(() => { + vi.unstubAllEnvs(); + }); + it("defaults to operator", () => { + vi.stubEnv("VITE_UI_ROLE", undefined); + expect(parseRoleFromEnv()).toBe("operator"); + }); + it("detects read_only", () => { + vi.stubEnv("VITE_UI_ROLE", "read_only"); + expect(parseRoleFromEnv()).toBe("read_only"); + }); +}); + +describe("canMutate", () => { + it("operator can mutate", () => { + expect(canMutate("operator")).toBe(true); + }); + it("read_only cannot", () => { + expect(canMutate("read_only")).toBe(false); + }); +}); diff --git a/console/tests/vitest-setup.ts b/console/tests/vitest-setup.ts new file mode 100644 index 000000000..f149f27ae --- /dev/null +++ b/console/tests/vitest-setup.ts @@ -0,0 +1 @@ +import "@testing-library/jest-dom/vitest"; diff --git a/console/tsconfig.json b/console/tsconfig.json new file mode 100644 index 000000000..eaa417131 --- /dev/null +++ b/console/tsconfig.json @@ -0,0 +1,20 @@ +{ + "compilerOptions": { + "target": "ES2022", + "useDefineForClassFields": true, + "lib": ["ES2022", "DOM", "DOM.Iterable"], + "module": "ESNext", + "skipLibCheck": true, + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "isolatedModules": true, + "moduleDetection": "force", + "noEmit": true, + "jsx": "react-jsx", + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true + }, + "include": ["src"] +} diff --git a/console/tsconfig.node.json b/console/tsconfig.node.json new file mode 100644 index 000000000..0d7e4b872 --- /dev/null +++ b/console/tsconfig.node.json @@ -0,0 +1,10 @@ +{ + "compilerOptions": { + "target": "ES2022", + "lib": ["ES2023"], + "module": "ESNext", + "skipLibCheck": true, + "moduleResolution": "bundler" + }, + "include": ["vite.config.ts", "vitest.config.ts"] +} diff --git a/console/vite.config.ts b/console/vite.config.ts new file mode 100644 index 000000000..0078d02dd --- /dev/null +++ b/console/vite.config.ts @@ -0,0 +1,31 @@ +import { defineConfig, loadEnv } from "vite"; +import react from "@vitejs/plugin-react"; + +export default defineConfig(({ mode }) => { + const env = loadEnv(mode, process.cwd(), ""); + const user = env.VITE_DEV_IDENTITY_USER ?? ""; + const team = env.VITE_DEV_IDENTITY_TEAM ?? ""; + const roles = env.VITE_DEV_IDENTITY_ROLES ?? "operator"; + const proxy: Record = { + [env.VITE_API_BASE_PATH ?? "/v1"]: { + target: env.VITE_API_PROXY_TARGET ?? "http://127.0.0.1:8080", + changeOrigin: true, + configure(p) { + p.on("proxyReq", (proxyReq) => { + if (user) { + proxyReq.setHeader("X-OpenSandbox-User", user); + if (team) { + proxyReq.setHeader("X-OpenSandbox-Team", team); + } + proxyReq.setHeader("X-OpenSandbox-Roles", roles); + } + }); + }, + }, + }; + return { + base: env.VITE_BASE ?? "/console/", + plugins: [react()], + server: { proxy }, + }; +}); diff --git a/console/vitest.config.ts b/console/vitest.config.ts new file mode 100644 index 000000000..f104551e1 --- /dev/null +++ b/console/vitest.config.ts @@ -0,0 +1,11 @@ +import { defineConfig } from "vitest/config"; +import react from "@vitejs/plugin-react"; + +export default defineConfig({ + plugins: [react()], + test: { + environment: "jsdom", + setupFiles: ["./tests/vitest-setup.ts"], + include: ["tests/**/*.test.ts"], + }, +}); diff --git a/docs/.vitepress/config.mts b/docs/.vitepress/config.mts index 856b8990c..688e91e92 100644 --- a/docs/.vitepress/config.mts +++ b/docs/.vitepress/config.mts @@ -1,48 +1,49 @@ -import { defineConfig } from "vitepress"; -import { loadManifest } from "./scripts/docs-manifest.mjs"; - -const manifest = loadManifest(); -const docsBase = process.env.DOCS_BASE || "/"; - -export default defineConfig({ - title: "OpenSandbox", - description: "OpenSandbox documentation site for users and developers", - head: [["link", { rel: "icon", type: "image/svg+xml", href: "/favicon.svg" }]], - cleanUrls: true, - lastUpdated: true, - base: docsBase, - ignoreDeadLinks: [/^https?:\/\/localhost/, /\/README$/, /\/index$/, "./contributing"], - srcExclude: ["node_modules/**", "README_zh.md", "RELEASE_NOTE_TEMPLATE.md"], - rewrites: manifest.rewrites, - themeConfig: { - logo: "/assets/logo.svg", - search: { - provider: "local", - }, - socialLinks: [{ icon: "github", link: "https://github.com/alibaba/OpenSandbox" }], - nav: manifest.nav.en, - sidebar: { - ...manifest.sidebar.en, - ...manifest.sidebar.zh, - }, - outline: { - level: [2, 3], - }, - }, - locales: { - root: { - label: "English", - lang: "en-US", - themeConfig: { - nav: manifest.nav.en, - }, - }, - zh: { - label: "简体中文", - lang: "zh-CN", - themeConfig: { - nav: manifest.nav.zh, - }, - }, - }, -}); +import { defineConfig } from "vitepress"; +import { loadManifest } from "./scripts/docs-manifest.mjs"; + +const manifest = loadManifest(); +const docsBase = process.env.DOCS_BASE || "/"; + +export default defineConfig({ + title: "OpenSandbox", + description: "OpenSandbox documentation site for users and developers", + appearance: "dark", + head: [["link", { rel: "icon", type: "image/svg+xml", href: "/favicon.svg" }]], + cleanUrls: true, + lastUpdated: true, + base: docsBase, + ignoreDeadLinks: [/^https?:\/\/localhost/, /\/README$/, /\/index$/, "./contributing"], + srcExclude: ["node_modules/**", "README_zh.md", "RELEASE_NOTE_TEMPLATE.md"], + rewrites: manifest.rewrites, + themeConfig: { + logo: "/assets/logo.svg", + search: { + provider: "local", + }, + socialLinks: [{ icon: "github", link: "https://github.com/alibaba/OpenSandbox" }], + nav: manifest.nav.en, + sidebar: { + ...manifest.sidebar.en, + ...manifest.sidebar.zh, + }, + outline: { + level: [2, 3], + }, + }, + locales: { + root: { + label: "English", + lang: "en-US", + themeConfig: { + nav: manifest.nav.en, + }, + }, + zh: { + label: "简体中文", + lang: "zh-CN", + themeConfig: { + nav: manifest.nav.zh, + }, + }, + }, +}); diff --git a/docs/public/images/console/console-auth-error.png b/docs/public/images/console/console-auth-error.png new file mode 100644 index 0000000000000000000000000000000000000000..67727dfbe8109a52e507abbcfd244c03c1cce1f3 GIT binary patch literal 70587 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%0D$w5xZpAF z4?rTsF7qqCtO7-KeSHNcqB4rV%l=h1R2H_szt6j0tfhqx9A#doZjxPVON!a58u?XO zS(tCj_C|uxJml{$PF848tpJ#xCv}R8<|3bmTBCSI7siP>{B48(`qJ;wbn}}*sK){a zn{NIDdho4JQ9NHR?CWzaolmi92>+?;4z#=4Raeh{iJlujb7@|d_>1q~?7sc`KcSgi zvy*z^WZpHJ?4%k^dkPMR-g&=#T!ZtMIh3HMBML!dg(C-K0D!ejdY-G3Q>XKBk`H%Qs{?4*U0e>sE?O4fXMU%N2brLfUj{taQo3oVcMos;v}-&8;F z>Hl`#S4z{b!Rl3?TMe1WqI(~~V$!|~BtPHg_5K0(C{af0gZ%*_&lw%wwfwG4I+_^E zq&-b;`i3?FVB&%G|GPVXKdyWAT~^o~FZ%M)cM%a8qJ007?Rv#@9i7P-i$W(=W?aZi zg!cpRC(dB(_};7>b`zuI;=X$sj(dLqFFWje`LJC04mBnD%$$;9mIliKPoR|K^XzF{ zXXo*V{mY|kyB;UBebD9tKKY952>xU2yesr!jS>9p#*m#PB)hY*aHg@4II)1U`Gb!| zwxX}!*JAJQ(+_f2m|m+44!3{iel-U8asudS{TA?CC;O>x9N|~g3ALbG+d!W0t0j%f z$~UWJ6OGMDZr814ck33u35^$n>81Ox|LPoJeg!CD>wTdxe56XFF%Ns?gnYf%Sd()( z+&3|8bNF4Z&^oQ7Xl{t$i^}=(Xr9rbkewxgOd>a!rnqVFKGk#)Vr)tdOmw(?@>qq9Z(kkb*u>u*WJEYZS&Rl3)P7Vqsx8+R3ja zd%9MiHWp~)^&d}?%+;2Nt>tK(j8+WfUjrx86ZtSNN#*DI27Gcj->Ju5`8U`SjDj=b zp3-x!eoS)=G5JdV4R^xdVIU}gxp1^JqiV&|EPuavqOiI)>A4(D>qBXOV=_w?G6U9v z7~Y|xe2@#Q5M5H!J>W(K{SOP^7juh|PtH~)A;EzH(LPFm4)XJ7guzgU_`)dNG^L^; z75-GtP$F2!GD04D_)m6F`vDcH??R%dc#Z0DI+7Tu89$! zX6D)1i7DiUv?O(UzCA%V6bTZJCK6Lp9OSuWf9Hgnm?@X`#G3LPUrPDmB`SNuVA3*3H!Jn6 z?id@+57Nx%e#3*Bhrf$@m+7C~Qs!T3ff4#utif|9NzJT6owlO(k|>mboaog8UE5B) zP+Eok-NvYBny*98rqm;huh$*3v-z^coTYi*b(9*TDERTQBGd^;qWqWDk=`}Wbo^q< zv|7-G*zJo^u+HvsSWeYRtm%@eJ1Hyegj6B=T>U}CHN4okZxVSLoh~ZpBs>%RAyuE| zUzUeWd@JNKH!xu!e45|coz=E37QhxlCYP?z5gc~#nM3N{h*iIed}lNPDLyDM}HclEN7DwGn>BE!kN z9)&bNd|#r9uVMEP4E5?`t;S0lcsjyoS`r<0qy?VB@lk29Nq(IdkyRfrKX3dyC*wmb zB)}dVKG&TN^Z9OD&veO_)#NJh1Kwu7fdRKNXOFBMPuj&1;F$(^O9a zhoIi^M#NoW{fjwEt6If*>d*7-Q`EO?i}T-{ys(`GZnv!ZnbY=FC29)nlhZuz9SWcxNvEt^lu^HL9T2O)!I+LkVObY8QOBkdxbG(DN^mI?M`HH9& z9fy+(ycH@iv6BA{;#Ml%wlGgoovm^1Y5AnX-Ca6ex6LGbMu2~M;>=^ksv581DT?$) zwBYrI1l!8189j;cZkX$eJ5GL+SO4a$yR+SFBn|d zKA_V&$Fx^~nsB8bS@nlZeU-F%c2~E}--U^PP^`RhO_o3w+#fK$dwm!3ZExb z_1H=3AH4&oASOJ#!6>ZB+H*aVD5OTuzM3Eop1|hIScRK*xJfu&9H)xM^^<%)+BMiz zc37$Uz`+VLaqUyGT0;X($KPw6A|H7ohu;LB^ny9Ia*sY2y|()O$vM&A^x7Kgb*fg| z%e1XuYDY@SfS<<3M2`GeC}{^GFBFsu+rk zd+LVPa_hRmLmx$%X?XjS|PYRumfdMpMEMsVy?`?=LUfize2!qIwi>C@G@{lac zD24VuLV6K@hO{v0lzM%`juFMDNWQosW+Hec7daO-@MX95gEjn0P(vZUUbPsdr}r3i z7kd)D&T&8%IqV+4k2gB0U!x-=@e;+Sr`w+kJvFfTVpR2h%V4_=chJ>Ep!2iCusP={ zHe=ex^yhtb9bBIthJ{hC1^0|=o1ao$3W%cj=EZ4c)I9vkbzc5-93EyG{_kPF^lK0l za+`r(+SaQjstVjW0XewbtK7bkx4RO%pIsVO$uto=7|Ac3>UqK?^v<<|_CCd$6IHG4 zxI4G(TADDo+^srlzbDsxak9D*7@3v+27Bgai5Z#n%5KrC4?>XCA0DiK_^Vbdu{Pk^ z3nGuDp(aMWlqT@$&9&2EXONz$X#G1SHdfZ&jtKj}FkIQuhk&}$XMlM>okkCA-`{J}fyJ))RV)L~uv#p{Mlwd>zXKP$lldP z>Zc8Q{m^8KZryTCt;c%*&Rt}c+p0FxWKVh=NV}z+h1k(9#NxaD?$xwnbqiO~P?)Z# zbH?NPY?DIYFc+m49JdxsO@F4Un{#u`uD0jn1>QeZ zW^1*H2ONBt5f=r%eG{&3axQW#+y3$V0I8Hk;t?bN`Xl<1T1~nfY!>>*gt%P5nvXBQ z9(R%M?+J?gz|`RBvwB?B_(B{zD^O8Efw8*A+h_JH|5Q9##+)Q#%N08|^Pgwln&>38P@H5d_BhpQg=g;tAhf zM#S^i+Ub1xs`>i^$S)v7{$L)P1^&@j-kG>vhU7U%B=%9kC!;D;!;~aFZC2jOszOPI zGTo|Z30{^`p(|r*%HC^yyyy5=CAmI}&p19^bC?L^!rZ6}-4{HE^=q5b1}m@e5-F|M zp$ndBhW5{o!wsnB8(Y@(uDgm%w6FC{q$$a(3zohytF%sx0GYmW5W0VsTEQ<~ls~_c zo~wyod5e>V3b}PTr&Xb5EA-N6@s>RRnszrv@pyU{>|s zANL#5hB=B-a>5?ui*o(RQd_K=c01Kz=Y&|{Zq_V3_#0V9 z48~%Uf&3f`ZR$xz3ni~oKk8O;l*ouZq~;Ds@mzmzcv|@^l3P$8xVe%}OI(OCTj8i+ z`s0fYq*o)iWJ~FDo%aV`lKS2k1MG1jg|`=4n}=Qr4!YS9gbUYIM0G-Llu4jW2FVBX zWBHZHk;}xlGfMA(tb;0|vIq_*?c{7rTxT^B*AweKi+!{9v2*n=X?x_T0+sSQ-zrv% zgj}Rk?Ub**g)R-bP=EZuwCMSh(qVEk+MJK(sa@tK-^8} z^2TN*-2he^uX^A#?sqpEuF42i*RX_I>+_avoyqFes~K>)ANB`B@yjy=ro|sNbnQ}J zX&jEQGgRB3^6G2)clRhh>Jng?>f?Dy-vXWj{#RO$QPsHg3~xwbp|yiR(oOL zMm`riSeKke+_F%bnBvRJ{xwN(@5g9{6~oE+-9Xq_Rq67gTd&L29GsUfWo$Lnf@aIW z-g3&j{z}AQ6~%C=><%G{e`<6bfKKOSVh?Ai#1@5r@hn%Eh`ZAG$*lS)ySE&>Ki{%=)&rQ;ZFpV#!)Wf2@u4SV-;FVE{JBz8|6&}X;v}aJN(#S2M?j-ac-)#M2_X(Fbomtge1jmqXXc%Av0PQ40j|D=`8i zg8Nnu3?KXmd;HpWbTW`FWKe6mMKgJPP+a+WL+h+uUR_(4-0N4YcehFX59DZ9ze}Xt zD{UsaXB6%^Lh0dem$KY5eohELw6nMh%FS2pSAXtjHM|#Zdu8?8v9KA@UT_-wxWh0c zqcgoSi{nv+)Yz*Uq3!Q&9`7%FYg$`OS-;(0zZrEZ%>S7~_;c;Ynm2V?F10NqfO9~F z?u_4O>e;HbjEto9v4+1{iC>zU9-l+jhf;0WU8Ti!_RI@nR;LTjlrzKy^i{Usr|CO4 zoh(y47i4QlK+#w;9%0`TneOiGR0H%hjS*s~TDC|>X=RnSb4r9KISKhv{1jV#4xR0r zCz)QjSxS`rPG%bg4UaMi`*SbIo~$X26Xl4-=jnUcBO<;;Ll$bS4IM@mn{Bhk{)NKibBx1v56qdKnp#evFJUjp)oqCxKJTjvd_2mfU6aSUshe%uqhI5zJ>mXGFev}Q?F&>3>}Rl3qgBszJrsXSc*XY zIIizaX^y^5VO2&yW(6mlCIcr)AR;zIx5rcN@BmPtpT8W_e4)YQ^wdd)iFJgKXnBE@ zqj93YI>C}oduGw7xlRuHLxFYnaj6FmZgHnw&d_bi@k9pYdLRS{)^&8vuVa85aQ%96 z&a1u2az}12@~6_X5HZo-_aL3-(3Y=pHR}d?0@K0`50c3=$0Fi@e+KRq<%!joy%l>h z!Q~rpI38Pa;GKP-l_{Az3=wze+_j5;>5m6=75I|TZw9!xl}QP2~^6Br#oYLgtGo3+xtL%L7~+=Q-3#DSweXhr);Mz zRE#o|d5nKZm6yg$$ozbWH@e6bm*PgmKWzFLu7S?}QA~U%|im;4H!Ptdx~Uone9d=)2JE*A=EJ=;G(OJ_OI~1JLhLr|r?4 zOh|qy11@a`fuXYA_mK@h|H5r-UB?{u(0L|uhoFE&uI1Itwp7p}2yt@@@|(sID1C(z zxW*04PzTL)8WpDEGV+rP(!DRk26soCL}RM?H)ZuAF^RXRxj(+nZ;t#FNc6$crP*}z zs-OtTIKFV>DGuzAqy=_?15@5gR^oy%|KL5#wgcI!mepdh4AyS;YHj0&&O!|M+~Sv8 z%-^MiWhYMknOAK9@2NKg<0d5rHoE!f3(KGNn{f*a>)4Lmon^afCWORY@1rRfb)y1h zxaZj`&%3RMf_IC&X9II|UvD(y6-9tl1a4*@Bz}h=63xmJJI6gu`K@x3(wrqU!2_80 zb0U#v37vL~#V`1cxN-k-1$DzCm?*Q1f!o#vIllTt1pkBewTccK5Hu(hiu+z4A>}-L zIr#a3RHkVr%Q*WiP}?9Cku#E+p76x^W&Y$$7eguAIf3(GMC@a%8SbGnY3Hp`MG~g9 z;sGXv^lLGEnpHYolk(t!w!t1rI+9rtx|p*0#P)9{Us|A-z(!3ZJ!u1(_BPF1FEy=l zBYjjr9nC{heW3d)c6LE@CMf9Syv09$g?Y_G5qCX+Mp>rkm zybE`#mR(80N?XzkI&u{w@wmFdT{jKlvC@cR8$0erE;LT5W3bK2{j-D^+VZy6R94TX ze|fOcIPc{HX{O3yX3As4W|j0jk-+Pm1iX-&04!q7&%1i|El1}K)S&SWoca$ zO!?V&Qs9|vU5x>DW`Eh6ZjK>yv>76)fL zj2>1~6Sr8qy5kZ!$5Syv%@^v7=xmnS%bLqM1MsOQGI^QiVQN2y)3dx?R|VzjYC3}w zLXod)z8x_Uhm1T$Lx~)uVOp%;KDHWkWfL}h9>1)-JZ)yC`s92d(Y|(T`{`I=;Cb2> zLzfhm5KOI_g05V?_%HtNc#p=8X_BE+p6}@7RfBUNic{c5^9_1jH2MRy@47A}TxQ zT6JitS{fpJ&ra>#)LS;Gl4`g!w$1{|Es2W5_!#dg2jfj`N-v!|oZcU1_4Qp-E8dRi%1(y%b-$y>ZP%J1OatY4&ZB8OK2V@`I*Xe?`P%%SD~X{UX%E`yfX< z`jLSU!4c65LooriNWEV%Z0_18$Cbt1NnG7(dDMXhof!kpiBga-OcYzAN8m3xiQt3~ zVwWBz$9RAMKmWQef}5Cw%U?EX-*Da7^&8>c4Z)(zUVsmyG7dcx!I=En*eKgEz=v(w zw=x)IBb$-4y7uAtq?=23k7NHWR7KBIwg#5N@m38sI~TgVKo^Ep6lfL=BHU*3&_lr(iqMdxRM-4=pU4Vy2fd-S zeUUToCnbn?jp>OP1In9JRMHG%{3P7hGWgZlyEc7J7RUJ#T(qG3XRvdsKAX3ore>sb z*!72j9caM1otFros(K|!y_Ax`+T7wPhXyb~lKmsX>>={{kRdM3z@L3f-4QO7czoPc z0(EOphbE{;2K#V3ko)LEqcyEPD6W#0Z{wQ8LA^}U*yE4g4-&`HQIzXrRcrPqaUz2) zLraYb)QX|W%NIwK$5Z`HaHx6?6PN*H!t>1v6)%p(<>B!-9j<>?9O1h9UYKSweBNHO zZlfH;f4{?+t+B}tc5*bO$*`%|QpEj@0;5G*N?*u~{qHTvDxAQAJ!y-}$jb(? zh}@y$=VyfrrNPoCxuvIbpZhh#5?x1={H0AO%JYsy`2XzP#T<%0KX))>e?CVIjCK6e z*LljdJfa;sM*hx>=YYN=@L=Ay;Q{4CSo!eS@fkJ`@SXJd-7F)TMj#{GM#Jah5twnd zf`Km0w7AVvjXsC#+E-IieFg_BAxhA+=~FDyK4%&-GL}WszxV_p8grx5H)6l})(PX^ zJ6#A?2=HpPD8y0lQ3zn`n=r?F_KH_%z_e?ewPJAUSEz`~I_uRt%pM#$7;7NPKK0nb z^(@eYglSs$=G+MU2t%jNQC~qu-A4`>$6q|SAcjx()@;1`@0LzxZ$Z;~7Ro(rhP_#LB~3M>#Iqc}lZ$Lbn}O!zIUSsfRbHdQTiK!{WwT zt^z`9xW2Q1nzn^kv!QFI$BeSY;8h@wz_%|^Pk4;WT|a|*ZGPsc#W3J(&Z-qU{h*;rN&iBRHUOCM-3F$a7Zj!RoNi8bNjpWVWOgCk<*K6Ht za8sq+`=0L{5PNnFc%EIoO5a;#pM2bSmdefbvBS1zv3evy!v#Na^U3V6!JE6-cpylS z;Kd?r(%-o#NU7q>5_K*e>rL>MVTd#jEgu5&}yb#nULl>+lvp0u^RwQoG zSovT$@niwu+m|mP6K`Fr5fPS?IuMA#Ao!T{E=#Dt`>m}gNRLiLh;VX2&i!TfN27Iw zh{aRZ@ps+AbV>}^Vx10QN_t-%#fL+Q6%7s+&DTgRx&;{D{>1RB zjr1s(Zo;`>3{<*YTeY;>0pKdj5%vSOD+8@OiHxxrY>I~Yo`SVMxS>7p(a=s->M|iGceW?!r z!fnS{998(J;5h52QT*fyxpB)`-Yh@NzM1UiRC7U|3W!3vMXTnqWseh+`32F;$6 z8wh*T`0GpgVgQY<7Bu!_`q(8+pUUl*uHAfkPkaw&p9WK!`g%|hm8uE}O^cB2gv-aD zCWiNK(%a+uw~>d;83#O=N+b_DK4ObBSc#TpRPZ?fkWG`*p&V4Bm$Op~fsIdr1Z2-zN+R6bRDK54mJ**pVW zkSy!TjF2ADl51ma9J9Ua03`-3MN>tvDEP>@If*!$6uFIw+pwdTgct?*%=xG7oNAH9N47`M;r#qz)L=Gk0)teVYI$)PZ*AI%a*;<};f0t`veae$ z$u4hxM4RKqu!$j_%-B#%+q0|PLRV>v~q_tI@{So{%2v{>YWQ*z#$+VyJ}BIC(T1@ ztV5ZZEl$ui1vzuY2i9)^^29|&OFNsCcVP`oEi7m@<|KVIfqzXSfedQGgKH=maiE_{ z3bT$2R#hs|c8=o24k*g&dn-T$eg>jJ&I~Ddtess?DNVI8NKhWps1ItBSg<}-_>z`8UKu4qH+=;>Y;dyVa*@*yYicCgm zxBI}MdSh1Mx(;JDf~InVim=l3s;R9wkW|OfC9^9S&i}oa$~TElRh+HBnC3@_-xzpnMI zyX($#W*0pVw_(M`sJ^0u!74fe8UZhzT~qi)X0G^(#^XGKY?qHy2miwYqT(*r%%lJS zQ|bpRb9H=$+(=<10!9)g8G8&mDlEQcuZ&}@J*MQJc@o0=I);aP&`iVJBNBES&BCxjBV_|LK@(e5O=EF5-6qq%i!gi_1+dEU#*1< zYO0R5*Jovmj1-;B<_Ts`59em*>Mr6KEt?ZwZosaUv}fV;X+MCutdVcEG?u&*pUl-G@zbsAKCF8^qqDt?QV9A z1oo0I(L^An@g{b|xG^^8sZxPfP&r-qt0mR;!}1aLZ!)w;kgC6;36GS2k%uc8|y zS@vMR#|(1}2sifTTZNKuUd?7mj&0tJL|Rs)iSzz=K_maLuYiF*ESGeg5@=vqJfxGA z1S)Vm1LLawKiqv)SXANrEh?f&DJTd-ONdB!t4No$FobjuJ@g=;q?B}{boUG$(lOM~ z-Q6|7FmOhH|Npr<=i)riIX7qDz&zi~p8dss=iBRDYrQ7WpI=j2G|VZ^Z}(pjzF_xP z)tBFh+hEYNcq|d2FlwfZF14&{Fpo@(OEsFMGby6UyE9oI5oT9&^w@3emGj7sej}L3 z>725hJ!e+6Fdc1%G6}*@NJ!@NQKh00&WTRo^;nqbch!^lJLUHUJ|f0>c@d3 zbGhInLs5@@#4Alf4J|(=UrUO~U@%}9+d}DJY4urb*+B9shlc6e_=&q#K6`N~r`_El z?A@iRQ7Dh5HVmLxS7A`A*Ip5$zBwt2rVRCB+JT=JOahMZg<3!-JT#QW`@}Em9+CW( z87Px+-4e@rEm+IzdCiX?Isp$-qviJ%p2_p@H($+-W#i=Unp>X$>Yx903ZW*)5Yl!o z6gzK|URbc_+cp+yvcyh83Z^9Th22;SiYTrI`r3VUpEOQmHE^RJu;LY7XI>+}$LV-j z17z@jg~5d#GNfDI+(<6N5&Q(P-0%S`_7gMC zbM!wKSSKY`cShoe%W9tcdI!GI@v!{cpxv0LRoRMf8 zHl&fOsIJFFZYN8;`FxLcO7pV^%ZO?3Y+Mo)V>e(pm+J6CcU|T>o;9i)Hs#G)g?xXc^(-g95nY6k* zZl1X|R_PTQ+LNQ_a_*~E1AprVP|ok&w8WTt&6CU%u}@@*C{EU;rJw za*BBtJ@&<$iOEBgFNfFOP9fbPw|5-IQ^dhoxQ=w(EuJkDuBrL6&ga@`;wDE7vt0GztQ z3VbwGr!I3wNI=L-u@PpjX9WUHTm=9pOQygaE8ae!?ueZiFSjfbXKicCUN@%4#X(#q zCk}nRpOO<|W{>C_M$-YjAFfoyru65&Vy?ON0faP?5F5+=+xXa+yN5?yUq6OW{cgXm zR}T*lkBXA=aiSm7Fy@n()uAjL!#|>)Wr8?3ijX91f5yJ0eENvuK&%!(^S&qW7S6fhh<$Z-0|_!A`c|4 zLd^t8uN?giz^)$5fU}-JoL0SBPMez7tlq>#gpq&OV3#aQU35R8$8(fMQho^R7sTv9_n<__^Zgx zY!opL|8fhd+B~mI)0%rVkdYFT@c~S2zArY1dz^71mw2-VqhVeLyp>6j^^-b2`Q`Np znfhRRgFgy&P>$7Z`>TrJt+2C5u?-eW58H96^0+PSEVqZe^Fav#@Z%3;@W{jIgQy^R8p(2-7=1xl7!|5i{-vNkYytEWZj78k2s;>F9-&*kn}W3tl^Nfa-L%A zU4tw5{nNMv(F**GmDK4n3$m9#(dI!5>CFV6@x9Gy3H>%Tq+8E-kRJYJx)x^D9tCj0v`kPOa)_;K-J@<$TSF9l@Z&OkkK&&?8?P9 zV5i?QEh(mKtv0HXuN>Ec`{#;et}_6}=40$lNG?4Rc=Mo^fPtSXXyc+MXE!H1!0pks zKNi-@_as}=+g(-b4PMDNVns}tfkR3U-cvf)4O-YYo%Z5;Mn)F|&gp&NAMUAfvAe+` z;qgg54tBRhK3eLUGSj_P z6#sWvc-Wd;tJdw6j^<_ z$niJcio@|ztD{5Y#^*{_)GiDqRLD!nZ@*3KZI2o-@Rq(Pd&r^Ru)WzOW^#IJAA`kY zT>S~%lW@BZFzsSKwUmNEg-T(M9Fe{r2HI#*Atu0|jv|GrE*S60kWl%t8XGh?!RgnP zUBN)L(IPHfI%&FIvWU26&qKk|wW2QmXd>PrWKlk;kBPssnT>z{85Y*7f4UTE#y^f9 z*yd{Cnuy~EQdyTtxJ+V%3|rQl10+vGzRTEJFE2}n;sWRrNbPt+YVd7NEzba|jZDi5 zc2(AX1kXdO!^(i+ka>3<8IvU!yDB@12Z$eVp_9Oxd%uwxLmtTm`S`d!y`OWGLeAN^ z73+p?pyfu3NlE*S(|w-)`zP#v;amSo;3Xez8$hLiIp)?-N$ zON>lHMjiw9drivK=iR$fOnfdq0KI(Su)X!dqxI8#j0Or$y_4ayu(cbcym^@-Y8B`~ zQPQg(yp>u>omH3j)P~r3yO=-tB z{$`#exWxy>{G^i8Bu-$JWRgLR)?*GotNWQOZeO}yzT&Sk0uCHJ%e^|=zpwG|!Plq% zSu?t|=0zCHdUYY0aKWbY%7>tEKeULgpuW$zYaMy*XkYYC(8X@i%A_iuZ{w9_g%O&-JpaC&aEgOzdwJMXOT(Lk$Q{c)5) z3yRIdml)xlpg%xe6bHK*2^b=aN~p2Q6l%@A8ZNZ=Im6>kso>q9>B7&<>E{JNdpOB! zQZu^aeJr>E^G}_D2`WrhL=4G(T5ob5CbU-L`X&Qa<&?p=6=?*s#-PY*>w|P>3dwg5 zzS3g+)S&+~Gk(d*6dW97Y98bsXO5BYE2(1_Ec8})IA|(m?maMkC031fKqQc4V`TK} zuYDZPFg67yioAP;?$1zYXttfJ2T)W0;zE4zZSTqErGlwe-cq+|+*%)aDZU*gm>B&mxT~n19z-D4>i-TsX#{@Ba>JAYb*t)jvhI_96V^r@+-;S045L)a3Xq8m0!Ap>(OcH!M~XcvU;)paK%DefO`!f%1e@q&B@UiZW2 zqIAlns~AZW2$=8==ep>bKC7R4dfc5&?>hCOSIHJRerWH`3A-3$3&qR-Jr@=Q8?Rq8 z%@gOC$E{+Ty6(3S>6UMoU4h@rIi5UuGUW>l>iYP1dxDWk;q$d(eR}lFnZE84d0o*_ zY!KGiWEJP|dGGVj>sR-_O4GS?F-xLr?42OWmrfED>H*B`+hE9Xp$jGv@)fJrPjSJ) z``)ZO|39|-H7vC?f52JwY;klfM=a0!O+Y{f@O-%_+HYZQSSobXzc_YiA*pG*aoGti zJ!l~4Y-_)7JMbd`Sw>yKwFk>qElGk|*)&MN{O`gIGSKn7u~)BOc?@3)mMRb1cg%f9 zWZ}pEtg%1enkL=N3CK{nbkLFrz%Di5MWIeR3YRwHIg*LVSMo3*Cl`+TYHukL&Lz71(8aH74c<7$gRDx&DRWbB|KB4G!-0 zeVabT=a1E2iYk9+@Mh=`NFnCS{H)yD%UmP#APsoWCQu!@6QO^azikp$t( z;IoS%YEWJ|0t#NOX6e5dxcjscQ$<`C8SV^po@nIuQZH?ViZ*MsQF10f+md&V1nCQ9 zv=~k?inmyAznz!sftxUfw?V0j6v#Ty@T}k>?4@~!pQ&1GEvQk=lPCHis(U@wIWiJk zLLLDZUM7=N1`~@Z3ch1&3?TExXuMT(DXHp7wq2(kE%fg*A(LIB2Vnm|Zxs|G)2)JO ziy)0lBzCWb;~A~~dF$`LG{q2JoWHx^u{Z;qO1sNr%))8-$}~*mZ#Xd1o-#f1t49BI z4rqZcYhkw0#)`on+Ot0EcIVagX#uk|_5&n)D%M;Mlnp3Gy`XKsU*&$YT=|IznR^?p z;I?nTg>Yx_YojA2-U3t|M8XxMDVQDPPDT6KkQSXSQF8mWo-H;RV%c#->2A;Rngb?Qf3=$?Mkg>LsT0F`8)5^w@1ZTTX-1 z+E4q5-5}`*9XQ)CXWTw7Rj*Bn;f)ILPR!ZivrWKy4I!XzZBv53=SN%Y1V?hK5Dx}dGkF};pa{Ki{FqY59~ua^@JtSG-F`LQtuvsRtIrOubBYM; zy^rFwMGNfR%ktRWnDpAIbnW*#-q`AEX< z-TRHjUlOLg`YdKl7>%!7_^)%84=qmlMmQQs2m&v2g9!9BDj`JtM@A1Kzb(<(i|vEW z30nFD&!L;o&aN3+8#vWmnL> za*D}7WX8!&{_up_2N>w8-oz&lz#nZVAs9N?fZWZd+EkeLm+4fboJq%F8@a^=3H_Ro>9TIOpdei7r zL`KS>(4O0+L}2nEch_r^iBm*THE|&mSom6Yw%!Qcrblrx5K3r9{~Q*a@4lEVNU?vW z`+_G?pH}I{!m(&FCl&cu_QM1&pO$q&p9m23$xg*Gs!Pr%Mo%;wG^WdyiiU9-O$C12 zdJ1~%q|4HTG5y=BnruKej^iQ24b4B%?2r;~XeC1(w^uIhv~1+N!U>F4S*2#!p>gjG z=0<*OJ-u+J_yvN`vQr@m0hBJEnAa})G)d-ewqW4wb3N6!oHCinkpVax%wu*y97v)BgQof z<`nr}eIUSRZu;5l)Gv#m+dBK=vl(%N+bbUC<6Q~Ym3&T#joM({`h%^n+q@48!s7SC zE;*dpQRf)~iyUSirHi(@$iZA@F^^jukd2E-#o5W=xFc(f+I&}MH(O|K!)ybx*o;d~ zJ!Z#Y>MPaOa6{T#5!b5WkU2W@uGo!&nXXnv?=8o#Au%X;`s5~9*D;dQ_h;2+czDOr zKApbUo46+JQ&(uN+fWX;-LrN#m#5!i;acU2K@oB?8yR!r!Mdr1v^#zW|9w_cY1MJ- zl$d)Y2lM`uZT%VR^aI4n9vt*bPmQk#l58`l?_>^7a8XD)2iF>_?WeX}&0~X$FhKQJ z()&wZ^c=1<>V61cQnI8sYa(vjy05wh0lf?EtfLb$Dh6#6r`aHPSEnVF7M^={s9=`8 zPxU6NSs&_VPy2xu;fcPor|W4A&6}i2fR@zzvowGqEq&fAc?f_bzu)$5MoPVZ6N!paGM~^<&|;R={Rgj%J)yFd=N z1mVPM4Paecaos#NHnHTVC03L5&f2AuG<9-6cjf-i(S3+yNMQu5Pft;oDh z0bRCc@1iUu$TB_@hlM;@FMNXlTbj(9HH`kAUgfgw_)uAT5nv9BaeM~czM^uJsC-DV zsZLCm!>|Jb%Y4{d!(cxPpsne7up5RNzezzYh%9@@!iTzY-4D|1-NVXv(wh9Z- zTFVt=cTjci6r0PK-p}ga7eNG5ep>MPb(np$?$7h1zhaq$yPkLSdwMDhnR^& zJ=0THyj{s={Tpdu&ha}YLdZrLh(o9 z(^O?co)M1ehD!pbm*y)#fuHSc{vnXwbOB;Q$1-LDp>uX>P7Evul_c3h?bpe|shqAO zUNjnb*BP4YTNqY@x?RM5Ahs{vd?7x*sqEK$v5BdDgw6dEb&k^3JL^xl!4wgNtq?_OTqZh3%nHeg95=4a($1InJPOtI`SFPyLN88 zX@krU5d%B>0=3H9zf`B+WF1GTw=BjI&vEdJ8_nnLH$YIk14-&hdM$d%KHMB?Hml(xt8XD;yDV`|lg&zuK0*WNGTD z+_gdjOs`)<(^EksT0R0NuDvpK4};Dkkc>6pw+SdHu%lSNXfpzjKW!OxNc2!fInuBO zj40i2p^|Xj7!b09VIKaoC-_YkMbXeT_MxSS7xd1ny0LE!=-EVrQ_#{FknB{c9vR-=FHj!bHWu?kS0y7PF#Wdy>B^g%JaNGIq%=r*$lo{}e z+nVfy_H>)2cAUMGo3%DcfBS$2?7vXW*!qUOc+~15n+A`%)eJxA`pQS9GDOCPrpTuE z>PkUrocHtv^6SEYHanx;n6l_!GIkqf_b0`6B3^;{y#P&Ei9*VY-U(WQ2*zDY z&FR|wFZ@q?l$35pzUb%FSwSVzH5;twGv)aaCCce$lpoj3;zm=*t}9X!R2NrAKdf@a zIIYj<1nkCA`voaWb={TABmQV9{Mx40V|JZW3ybUXaKxRZpSsu?o%p31_VYI>mz||H z2e780Lj|o(})|+)43p~jG%Au zqgTf@iszAzAtnNBp_#bOHWKa|K>-%*y-Ksk>nm6FM$6Zocd=hi#ow~kGf=;Y8u!u%H4cvlt0ErZJ>J5wG+Aj%Dz zV~^&ue;=SZN_!(}*5NzFTrI9uRGt5TKwi#g#Kqc>g7CA_9nIeUdwCvgT}RX+D&e+daDFKh69JX?e)K@XGP9n6;@_l_N*T8oR1 znvcPaG)u9Srq_J*94)93*~1f_cBq(R)~pK)pV- z%oNs%)G1AE3H)|f`jepewEu!+!uw!@q&UnL=x#Z9eseSXagqG5U&h2!tVR1xJOx}H z7s9(}+1w{+`pCFy!mH~Y-k z#06q4wtly+f`Ukj5>t4Xkd5GPO{yy*%26W;fC5pv;9FXYp+axsIVr>GeqI<; z=bXGkdws;?dGGYy2b9YyymDeL!WL5q64d@;578kU-yf4f{_@1<>!E;MhL|}%;8dTn zMIxiK=`neQXxEMHz^BgxC5^nMM{huS^itOmg8GsZ;p8(pKXk>XCkFjcrI>Ke?*!Tx zeh%sxo;wM`5AhvbDvB-&a!`#~k?%tW8d_7;N8R<%w)9qcIvcXV<)<| zF5`bwnj&mx#kQ6M3d^{egDANd9P(Kg%{A#HM>IDvcLQAV&eKHrJ|G%yZX&ZU=r_(+ zp=G=_zAJ}p02B+cR}xx9hCwnqBTc=o zHmFluENN0`VGCtDgUGpnDIX$$dn~1HT>0hZXeq}K@B|l0raEw*H{ar@ z!jDK#Ql^wqbEUFj>s!1zSXiNssSZmPFe&exPJ+}Ksf#BewSK%ySV~nerG!y#yjQY) z+W&1gnSm{7O-HZL&`P;=m@!ulqrlpY0kv0$=$za}VG?Fot>FB7r33}qEt*9i2=dmc zQ=6;-75WBq6Jx51t3a!%fs%&%0!;gY@lc+AJ6X%mYg4L{_wk5>lvRxBXE9U{Q{hLW zz!fQ=FQD=cwI(^4#614vN5o0r7NDf)sl1|cIBwWkJPwgjcNCk_Y*LTmBt5HT8c=I*QhCcII~QeK zAXjB$D5>Bn6=eOP`G?Pkn&VkQGqzAu>gHW4O1_A_;?9r?7sT?Ce+i&vv4Uk?^f>G# zKtx_0Lyl26jB2Vzaq@0dZR-@dz4^e@HJGf1+!!igMVjH8p9Eco>(Nat}3!nJ3j^0Mb-+XSbXK1B%ITw!* za$(&^S1HWsFZ6pZaTBngk{sLba-tFv{=-`3!C_m*U3}vzstlQvbI3|SNj_n-mERT( z+7`>uF-4ajr!*QN?(4GJ3lfS>1)%|Ccr_%s6*@l(k7}a%7!7QqsT;m4rVQ)Cza5356S+7> zXMu0bRwy>Os&IB^_?;n%<~Pi+iD#-9{Hi)bZ;;lUMDVVvN6C2#8;3CMYGF9(23vP; zT27nnXQTIjf2!NDxsKxcvgsdM`&il&aVywQ}ms&{7ACUOjkx}Uq1-LM4 zoJqk|Y-<0_6k}IoS;>A%h-(5q@WN47P$yYU>>KaMQupMEeD)JcExg$xIsI16lnQ>d zqu(vR)M$n)@~Te8)cMg1fuxNK17bejPnQaN6%?d=n~OICyrNp$R)9*Y?J>@a+Ao8k z+ti1TNR0W0im74Xt556<%8L};TAlV`ixhD2U!iu7(>kgzB=~)&yP`Bnwwbse=)r+) zhHQ1kxcJ4C{8da&Lta0jhh#hv*<%N44~IXWrHGBS@b0_)aKS`-0xYFWny-!7+YgxD zE+m1QG$fXaG2Ff=SC`$soa@?^Pjft~_NDU~s+k$#gDzJ~zo>8&X5$hI*OJ@6fE^Oo?WGXN1j*ikjvxAfn#l2x}qtC4t;y0-ETO6*%>snKJi}f=+ z^|c?X>VE$Gj>1-UqU2^XYk4FI>__9cdHYqZZ&a&^zK^VSoF?~Xn$oD zM9v)gL&KTMG3Fv|^JIw4*7R+~e8pL{o`JlpoZ0RND^LqXmHQ)m8Uq#mJLNs`6kYq| z=t|3uZ%FsB?CG=*2||XTTUID#(b{n3=I?M7AiR{4Q~%U9yB9zzRVh@^BRe5}p0HLT zJLQptjsPBN3pl_N)K@Y*%nz;ux*m0%SEFsTeY={gz*EHwJz{C{j#rj&A|t~WE!E``YcHpH}I;D<+8_m0}xH^yZ_ zQ#sZ8jy3jff6bmScd`IxuUYK>N(OqmE9R*6d^Rdkp#JT@CalNrv2Gkfr^_MHw{)C2 zGOk(u7%&!deC7OcAMG$o#0iY5FgYIUmHTHNhH3ahF8!Z;5a$z2I%w>XG$tK{Nf`V) z{!b|zm=FEmE1UFx-{`Ib1fs^?Q5^O4zWGz}EL|rEv7Y_@s^w_q&m;~`e1@>2l+@C* zO#pht=1ySjd}g6v3i}}PpsYBJzgc0rxm_U9O+Q`ttZcJhi$)lnG{^ii%IYrZ*xPW^ zrp3%CFW?u(F86g|AqF^lw=b8YjHz>jv9`hU|n?3rQK8;q@{fO}>&_`!#&^ovhoX z9?AwXWV(kn8=EfYy^n82EBNSd_R2PZVhNQSXOG8m{7PqOX@9XYslTZZ_Yqc+1L5E4 zpThaaf%*I2LtekqJa!c}&!+CQ5*qJYR^$So=tdG<(@nAvw-8X@-2<7d6jbKsYqo#H zzt{b%jN@_6U2N*j;%tkqRq^E*UVqjc;;xG6>g}D~%1Guh2@cGDLpA||oT$hUSE<)3 zKnLE{9&?=G+HMS%);DgQU3b8@2hQ#tzJ0muae@)mJ#4*#522oK3MWfADawQ0RbR=HA38fO)mD&H_vb0}%v|gLUd>-uwVPJ)YOIAmLd!tj zT5Z>h_5*X}hLV8~45^8?*hhS%MoUp^+vxjE$-#?=X`Wnj* zh|I=QF}tVx5%V)!hI5kmZ5-U)I9%o)5f>_yre9t} z#c5h!+)gI>bVU+k^!?@a49>_W^@@{6+sQ21q5?h$qTWV*twk64xpwC86`Fw{jDNMy z_k=eIr0sc;<20(i2ZF< z;x_!QXuYD1{k^lT#`E-9WQL8u)e_Z*zR*9#S-TL^W!mYz77+x?&ckZC(+*-r z{4ld_NA+`?;@6@z9wW`p9XshS8u5uS3+k&hR@S@O)+(8@1IXIO$f_?{qW8pa$-4#d zIP&B+6R#l;XKy6a2~Q9PJLzlAu|7Cd8XU3LWT8BKuIY(g{d66Dg$k+CnYQ4_XreEx zY@8s}5Bes!tKHXVS9o!-AdtUu(?*h%01CUAy}|+GIf;VLM0pge@9-+h2#>W5#2nWy z>-LHmBT8oKNpE^{-eek@b~8DtPJMM40{GI1%e538+0Hg2On#MAMx@3TO?L`-?R*y+ zNi$nd7$xN95v<;yTR%Q(9hFegTUT2ZUP_zHI9z9|>xV~aYRvb|3ZZ&N06#;$zsq=W zIGuOgnz(qHdxDky;*5%>&jcTyoebzETzZVQ#l{R^&HQ9nY2pzWO?ZCebF_6eU=s(w zFZqZ1@Bn;pl=MVjoWv@{51qV8DUR_~)z!f`=iU!!KUwfl>jH0^x?!*2V*CozGiij*#zHd{TyCiNIh=E; zvJ~T9A|wcYxsWv-m&J@=*Ke7vcKtFNwQ%6kVz73>Fh2@2N1nRI8VY+}9#-jeX4@fE zmluqqYlZJ(>r^xOyjpDXC&Ni(lnt}`@ht(P?fRq8yL|QN->12!AtsR|$Wpj-=; zR1Q$03r^GJ%AMGOYEm@hY^`n&*a1^VMKt-a5~jgb9a5G7=rXHF*t2=KVCTT6wW*Ec z_sb;=#*T3ZnKhLv;njf4iJSK&Mb4vJ8+!FxD^sIo+xv?(lDtFd(^Wbi6IS4%(3{yu z-@duNmT>$5pq+=Btirbj;g}Ch&%L&vsk=+eWa1q<)EoVAAI|>qW9cpGzp((1$(qIT zrM^=c<+M5%v@*$J#J=zaNZsdbVEhT3NhLUgD?29W#CoP8%hy7`zU1u8X0ZTx$Zlp8 zWItTXg)A1e95&gXm1Dn&hsXYQ7pLao>)8G?ba&hQiLJbCk3FAL_G?Z_)VB`E%kj>p zwVSqAo7`2QZ&FazpDb>{b+W2V-h{e?yf%|0@PiZy!OSFXgD zMR8I;?Y$JE=NM*AKF^Q)b}y{I0@Nmtl!<><9ZJ|JSMbqwyZN$$%?;+noAxmr1b-ZH zCU!xHH_R^J3$E2pzsA~aa_@K*Wuu72H0@%#k2jR6hnbC<=#+smPoQip!oSKYqYCe^ z=-|CylH4)PhE<^@l!~0{&a=fJOXg7dCVFC^)-{qE5x=%)a8;(BO!3EXP>LfA*T*_@ z=zYBdg}w^KvmT9cNUd@M_V0qV86Pp5cf-TV5mMW;5tGJP)NV+TMJCXsZ=2f(MIxQv z?K`%)U(@w=7J+xCaegZJJLq{LnHrv&Bj$#cTjV3MSGP_!Guc(?0P z2Lqdc4Tj)6u_{aHO*#&j8P)vK%7ACnmZ(1*fn##qNWIB`R96OFrm%Gi`P0cqj zNf$o3Dl=_8>meOan!SxND(#w$%_-Sh=s?vg zii3fT@y6WMzAznuME8Xt35DJ|pO&2z`V8fvA|_%B_L-egvEj5iAt;K-K+}G`O>)!r zv$MgBVg0#z?)u0)&~ce#-YM3Xn-8W0yy90B-hR`JkZSoMV&QBxUoj8^Y_@lgdc-1n zj(515y*1X%YjDrs4yCcF#iFl)Xcf0}X;U-%4rOVZegNP-fX97j1_yy-7ZBm_QiHz zQ`70LN|3W`%5r-@V3Tlls$xxu{aR#rSD+wc%DqJWn$7^r;I}^D2y0?!g;& z&U_QLI>~^AcTCy6452==5iXEAl`U3#H}*PD(4C**#pe9_qD%bT0PoI>t_w_!WN7WG zUJlpsfY+q$gk0K2<9bwNrV3NYFDA6b>hZLLd}!rm@2N}9_&9sO=35$DQ4=1DOzYi& z!?l+kZ{6grYS(9qNq7Qkls-}RH3~VD+b6KEo#|}1va_*P5~d1{8owLMY-P?ijiGN0 zqbZS4Y58DF=Xgp$Gs)T*mscFuP?c2`w!Nwsc7hLn&QT<1<3?W4=y1Vbz}Mbn*w*#@ zMdPXhrnm?tRT_XP4ZlKV=->fH;SaN(E)MxP`fXeO&E$afLDbT|#_UDG;Jo1l`UU^- zu|doN9&2+v*m5zK=jPQ!o$FHT^85p!2S27CvwvLV_}+4tJJ@1-)^n#Mo2RNFGWQ10 z-?ro4s1MBN;1=|7Pn)j8MHsWg^XzGb+97qe$mH0;ejKI5kA)mJmlA%ZE%BeHp8SM- zK`#2OQUk=1Reo-_;*~bD6^O}_GrscpzqcjB@S;8~LQv~suc`T;4fEKe`%5^kn@dCr zk$i{fxhOxQ_h@tMbAa24uOrU6GBV<1&o9oN8kd5{+a)7&I6rZiE5S9~q;BYYi)Lxj z;G5=h)aT~o%_sBd=E~nj_QpyG^(42qjEL-dmNHVdE;hSA^Ac4D_y-84u{6EMwg~y8 z7KyM}QnMF?o{jnzxOL=H@@mn}Bk@dg@N`d*vW8dpPJ^E52BjnHnqiGB5U`W zm9?h{vuou>>zn*(l675%-;CO9Mr`)!-*12PvKj$hj>?CNYi^)l!mJGw(pS@WUyRwD zg*63k&1hpt`yNp{D?lNs$x&s}*wN4xaXZDKDJQWxEv_& z>vH|+k?|q~Fz83p^XaRK_Z*`9mK_;#tUm|%`)a$B%&msRTl;DMZa?`Re+B4u?q|uR zL@Kkg4fs+*-m4PLe+U)%RauNuC^kR zAXmj?Ko3UN*Q?+3#;s8<`sg{-#r0X!+mNkEo}BbHss74m8vHrR(*R?UP5V5H{h>6Y z{?pXvu`(ST=Ml7EaHdNG9!Dt#Ji8?ETP~TOt<^S|jWuic^bR5d9rhfeWqXd;vqJV0 z+3a=@Qu3@h9_ndHGvk4jr@77BvG~+44{`=((O14ut1#L6YJ8eZRH{z?K#P+4&+bFM z{k;#n88suuX&2k`Rowy|R0-94f*uD|X}{CS%kLT_WRY|#?i#0FZX}CaGaXKQ70cTs zxx27Ir+YMbU@Ai=g2c| zVPZ{3qhp?iPKxr2;iy)4nm~X|Rs0(DGLeU-!raEb(>^RBr>@I zwLUnFu&q;>P|a2FKyn3IydNIxo_%UIc#IF%PWLpIrLBXyqAu)Vc?s^{x_&&d&i^1y zw(v>q1q_-upMCXem1wn7;?Zt~RZDua6bDIG2C-~QoS4lyHzs5MSI>+yr8T%9=0!-f zqV&2~dW%Sga~IP_z>4^I^WX1l@Siy57l-HuT6luOQ)l>|#4z z=L;i^=(7R3*+_5IRoc&1L~vYC+z!gnZFln=auNuN`P5@Wd#< zX1NUc*vBUxSz&S(9OaJlJ+N-Ew@$&u2+W4GTFRd4eb2aA>(k9p%TnjKN3^kAK|a~% z<+9Cp>-wiOUlX>V&)}vIj8`!|{oqk$eK;(&gTO-W|g5 zmW1Yz*YXSVdI{Qh#8R|8E?bxxF|n24x;c4xt1fK%XX_v>{pC!E($~hdnGi$kV^c=f zly|(#pgbn+p@T^!W1N}&F*zqTrMq`ja%79DuUF8J;^{adLU!6A;jKW}O*0JjqiQCxc_{e4;#YXvmCqRZii@tfcIP zq}*8_{TkMsEAZ?7rB`iYHpl;oPhW*DuWA^NXt9-nd=;G)LlQToMlyZRIG55Mz7kcr zJ={06d8RZGG`s379?^)}?+&SGS-&YsHghY7$aqw@lj5XS7@EB?@a#{3O`h|_i->+5wZ?PslDM4W${{sOm{6Z|{=KLh3e zS1|wk51GFN`APVQ$h<^1hC5W&RwVUqK1OhlTILt6XVYuzr)#T6=e@ZF`iR77B>a5T z*7knwwJX@^XDB0aMdB4nNGG;!rk1LFoSNJnP%|gLv@lfzK^suvbL5A6#LtyHVgRr)t2t@Qr{JUfAb&G)z>?0!>r46gms%qrB_xcTI>zUxJX zgBXU%sUw+`%&HF(TE#x8XR&j{B=jph#d#pQXKihbsc0*|ZiZO}nHO`WrILa0M9A@QF>nbJl zs%mIrO6Yu@!BkN1>+g3&+1=Y&x9%O$r>K~yfV-c({yi%NivYbUka!eiZcvh3TmP)K z#WEd{%3QniPd=0Q4oeR}Xlxa!xnllnZA1#DJY({3UQJB`E(#Quqlvy)CZb9{<-PlB z|A0|Ke!$Yhi=eX)OL2V_DePFqS)Y?lom?*AtBB*x6-kh`tYuy)t zM%MV;=5iB>Iu}(N_?IUm)5Nka8<)%?gKdmohzG!pd-x=L`X|bSx+UgeWl}C`F7I!EvXu7}K z^M&s)fU`LIH6Qu1Cp8br%^K(u*9_Y9d~Ya{i;+r`?OL>M>1F_3#l1gVI0w)I1r`c? zNFIH#mM_yBG*FBG&{1Ju-znb39+Z!`Sb%`C%e4uXP&JrUZB;|d9J3@@VVpG{vd zFjQoVcdK1?7^G*vb&h#=*3VBpDCX6}m2Vifqn&g^bq5 zBDbj}QPo+_WEOVl%T;@h5uKTET~0IOmsQxwP{ma{M@oK<2;gnnUO>okZ-Jui0WlZ4 z#!3Yn`ejl#-V40CnQ_nqxM-Vkd_SIKW2jdXRQm%p8FV*UafXz-yE!8+093sEuk<5M zTV>%bUuy(mbVa3RMWs@Oa!H8iVKaB~K9_xOE|@qj{f(Eiq!h(7B+VgoxXSwcP98}* z`afuUtEjlTwp)-u0wGv{1eXL&f_rdx3&GvpwQzS0PT>T1cMXNR7f!H(!d(jMdf)H+ zPxnRlI5&N+7&X{?ud$ardp&c_XHKS)n<|E^7dCZS`gf=m($P!>(@fAAm$l288!=!< z9{XmJ*7AVqea25#CC*b8mR7Y|_v5u4G_kT%dwN2m&0iOyOAFRkTPFm(tz--awrKw1 z*JZ{wd%~dZz)XIKz%AN^tUkZT)~a72|MO%sjms_L)6-=#o!?l+#UZ!zCbL;S=#l?j zY(=a&&AOxQJQIZ6XSlT*N9{RVRv5z%=kzmaw(f<*i`&k~f}&cLz>l5f^HT0= z0H?+%8(Z{cyLc=@SqE~K*jIZV`H!8JBa<07WTM$D^lRUF~LkEDpyC zTwi$I@<%8guI3EozUjRTMj2L0-Ne86BwTqpwln&MYHzOWai7u=3ra8h))r_Ay@1yZ zBo-*>kceL28JZr6_42BRGR4cDpnxY&iCo}=#5dM;kaZc5J6RaW(ET3q^T zo3t|xp~<(uN$!HXl{F#irxm7M7lRQx;N8=k16G^Y5wft;?1?A|LF}xlrH$)_^JMF) z|3vW;^4C2JUn0ZvqvufAvgO()yh?W zw>8&y>Pmo|u_l>CW?l<2F>#MRuYglS?_!0G)t{Lc{$A2#g>~MD*Drie51ddGK(NR_ACcOu!)7W&~tN!#9 z)0uQ*7rM0<2WJ5ChppC_r$gWk>L z&NaR1`Iap6^VWR)+3jwtWU$Qz$l1A#SXBg$26>1(_mZ#ys-7Wfc0`rpao*Xr@NFqITibitD*V~I#dcNwbOng5xqZG;MF#shWw2$^T3?RGjIyIR zORIaiy93&xHmdo zJ8M2rt@A7#J?!npR;|K~jFHA%!jS0v3TZ%rc&09`!NpYaS6La&dS{W?YXYA^wl9$3 z#WcfGKfP8_zIMsDSc8KNe#&eQL!IgjM~V>sC9a;zwwBEDdAJE%`F{CG6#rum zG2xC3{R16GduEFp@TPiS2`M}J35>RuLEyXodruHwkoPQtcHEX$;@dJB<%5*Jo~E4Qvva!I zv%WlC%z45`#?om%HH0dR?V|7S9eas@b1}4O6x>BNCLUC z*Teyrfy`0BU0+*lkoYa;zZeec-KPrQ<*#E%9BgXY?AhUh5vOlL=b`(_lsz{ z2QSj?jw3ZqR+QM>Aus;$Oa8d~?|Oje_$wl=9IXn|WBY1!)xOpbk8rQvMwp%e z27y)F$%Abz%l}{j@fYod@Jxrys*y+@49~JK_a{-Fus{D}nt5pT?V_wcG2+RvsqG;4 z__ADe1+42FUd0TZ@VYpmYUXAdqL0P?`z%))^aN8-Io=?7;9-c zM%qUzs-+mF9b{u9Cpd|$8q3hdaS*VGlVzy!w!wsOqF)T$3lyR0onUQ~Qi^KUga2Y; zS$8UqYGv5x%F@!W46X9Hj$b;J#Q>X{P=k%}E_WAi_P!8~`Tp_wVo& z0-N`Tt4HU-@@;{FiVUQ}Ibq?jwC9H(KsJhQ1l~PloDEdd!^6_0B71OlXbEYAh6>y^ zb#bESA1;~$!xtvsqBfrdz5|svVbGKW&lYV@6iN|64S3hOFug}`e_J|#Q1&fkZa5yk zbQa~lxfTeLb67l?!xLw%Ql)~<{HhjVGF_L$4^Xs!ZqGGUpl(KMDKlt!v6x6*|0*<0 z-UABfb*7{H2sb$PL_|XHkJ(oY9r_dS?EPNlMJtGAsn&B4mXBufHd~&_ z!n1@Rgci)XgFsQZLs9%KE+%H2)Va5OOxeNF&)r=n=t6v`vZ`jD8eLWa<9Do6B(v9b zTOxcB@BSJVw;QObskyhbm#chhFo#RgI9seS#rQ@=zQcZ6e*4OSv{aTY>E0_s>YEkO z{wJKoFtwOAEnvHD{PSe`@BAIIA8P8VNvYiLQEL0JX?OU4=BueEjmp~LtEqEH9rGjb z89WlSNaLzUXjJs!Lf~xw;QBdf3cS3NW;K$>$7P%n6qG%@1UKb=-^p+)EKThVBGc9j z7^mq?Ge>gsP$w-;lx{{FFIW8LIT?o3cxcxyK=%C3)L$~LY74xyv@|R&V{ffuhqIsju;#@lDU!koMtKtGcxE`^PRKozh|vl&SLfIZ@IhZ| zNn2A%f^HENfv?RafRUFbrEfYS32jrWEJYo_Ijvr1B!?8R%8&mT()tfXwpX&C2RY`$J0Cr`%k^JV=iFKl16*iK zC`gEw;-AP3AMXF74$%MqdrJRro`{3kxCbZv?tQU5-v5Fdr6Wdx>iFs37e%;DNSIS8 znJ@fyeSL7yVP)$2AA@8a-U&?1&l5k1p4QdMB9lfk%hF{UwPNo6MKcjjSd=gv@gma0 zNgk;ZswQSutxP2ff8!WB*na;9m^C2Qmy}k-PqkK6ZIz+^H-00yJ7~#LJu4{Ce*0!Y zCD+6GFLcDYzeRrxIm%R1^3bmQZ`Wg~lxq2Cm%alyVsn50@CCbI@%5`3`DGq%0mzZ} z?~abZ+<^Z)k^S9#0VL(;(s;kPQa+_*AYVrBx~AO1X&tUK68Gs+FazIcaP{-|M>1d z|DO6{s;=rK+sE$yZ{((XBq6o`nruZP{qN0F8l|LC0YAlq@DcIg_|5N_MB~pAD*s3{ z!Y5nhkJ5raXs@E+jc$I%OWkah{Leit$)zg)^SD0Df;Qwhfq&cbm#B=y(CcB1PO=RN z|2~(C#Qg8|>~>b1|1|!8`yT&q#{2)>zWi?U{q4-dU}~qI_n%-3CoFMA@Ad%jj9`l9 z-@3)`7w@fF`m9--$fAre{ZlE%eO=BxjhX&<|ejvGl)7g6iOxSXk308}m- z?SG#(Xx%?zQ z(vv;ip1Ap31b$DX5N!$asuom9qr>ueTg8n4ZyUltn}}@N(WfID=9PJOum;3hiNj=n zuT}K{H>sZt{SgT%7tdWVe@v0Nv%ou8b|nAxtFz8IK`NmK)?8;QZVN90kod%fmrs`? z*Ob$^J*W^#^Wy9WVf@B0m8(gM5RMQ`s@*R0};$E^3aHn#`(n_Y&!uR*4d#{O%jO-EmS1G*kvMD-LH zD#G{e1zuH%Ab6iLH&0 zfbZ?HXqLOXiOUfg)hPGbd{)3Ltl~J$s7s7~cWLeOU>F#}gLD?2E6Cr_LBL>omB3|3 zN^6;B%}yi`3| z$`#)Km~~UqW*+#s;8a*Cwo##kCvg0rPh0y&K~PAbg@jYPSgErA2+1_3+rWRTZse9vV zSZXjQr-nP%k*qEJE zbL(i^cw>T@Q4KE8Ix56wPxbYA%7Bg{`rb!Au~ujA{w#q7Hc54iUE_x8#7G!XqeYF1 zAiaAjOQwRH6@CQNWmJ=@gMFL7g?s71GzO1wwEEFu`V+NWHo;48I{hCPT?tvd#cPRV zgHZYE5Jv3Pli9~BiwV--yr%BO2}|`oNv))RKyi9QDVxtO;%dZ6{TL@Sv#JGb<53CT z_TZakAl(Nub|9AxS7!r!PG9$RF*_)iF5o5eW zBMX&YHolk{!@8MrHt~KLAI2apVttz1DfSb(LURp7xYIMNKD~-`-tFm6%C!a|F8}iH z!u^f4P>n9PV0*&;lyPQGE5??}7SyiTJCM_j`y3NH*z*SDl7q4z8C+?H_);VsW~n={ zXYYkt>zFD|!*cp^V*LKM`;CuRwA~xq#nCEYxpvQ)A&e{3k%LJ!6gX?PNbr-XZpv_T zLEWmQu+Gj{L>kG0js8aA=VVA~3puB5UPZR>Wb+UM6DqwZF^|)wi^juGfLq;7%1O|N zai=1}BYaOW*R?0RNhWsZ0R_Hfo7HKBW*)kznd!b`v_{8=hGy`)zMgb1m#S~szT(Qi zI?nTR%}(P8Nu(__79B&4qBf1SJ`vgvrJFgu1h|5+t^a}jJQ={Zt^>avq!xfban zzMSqoYfSVTjom%pRO54cA9IdGCI5XqP5bNSsI*9u$p&MSnX!gp@@JFP0a>$2j&cS; zpajN%VO*2pj^vRMQ1tLFP)+2O$IhLZBzxF>r7{WiDcv%nLJs?{2hkWnmDpzzk}l!z;UGs8@NXI5E%S<0Bo z=nXNYrTU0nQS=b1y?z?+*I)%dS)g%Ub-vHeX24qhFtsgIRSKc={9{L3dh48plUZKX zkH1e0V#=6{$rCUA)be#<-JP8Jewi7g6!o&kXE(VRsT`i<^pmid6~YBrxr?UCeWeCAz5!f*(gH+?L%8tQVzb(F^U96d~s5O|?a`wafTRsaHO@p^NZ$hDU zX|8DEARCrSr5Qlz~Xzm!my0-`Nspoq|fOQe_AMief6GB7IxnA za`HW2{o!|+$IIeH22IFywwLDji5tPcu&GU0CAq%sGfOo+8dn~+6--sOK0ng z%i+F{M?eKX20SO(uHFvQuDdG8Y5DBMGQEQV@l$bI<*yKdVREk@68ooZcEdIsmd^Xh zA)1&^{WA@mU>Xi+uujG|d5DF2nXV20)FHV2XaT5S>L!aJSjKe^6&sJ+CN>}-wPS1b(MqidB$2<;|<2$xo0(S%3f?lUagH$FI;)`e^^ zCzNuqCK9Y;yPm5QWxB&GnnSt=vEbz;wFXr)FzWRQR_^HPW5R-J&t+pvyndz ztKHD*;&>Tgy?l7tKR$VIG78LBfj~hW4Qb+#)PW+QZj?&>1)e)jwzf@3t#5D!zt$L8 zm7JC7W&=X3rFzAN>AC(s*y)q3LV6=kOE7Eu{C?$T;UrAcfcp@AYCnXx=L(;~I@&2A zg2lxGn@_o}=gGTX{OriYmYC#Zz(5h3A#e}FhKxDWGBEG{$@E=gD~4hL7nPiGt!WWb z(Cm-TJJC0sL7LO->j?eq7(S;JH3`Vu8#?KsxfN8~>=v^5EgvhTXg6osL7}rN%;|hZ zP5yAo=yjy8<;9}Eve&5z!IP(zeWY4}S2PPj4A2t!JG|lnU`>k_qh)=WtSNfdoWc9m zJ!~p>jTLP1DDQ6oCr13sj~2vUHM@GO zcK%xyb}r-F>Lfw%Cn>G+}~R~XfES6dpX)48yfg;Zr( z6P_=q$%HE{SW4oT7xQveyw<(aE3+RMf?{z`{oMYC;66U47_6%;Uf zy0g;4Qyw*AfQjtav{saD`Sq1AaR^PWPK;fOFg=R)qgq)KxGu$^6u*+n0L z^G=6gXtq%!acTd)fWO8Dqo^EiwY676n9c~$8Zk)OsN z`wNuo`|a+sEz9yY`lgO4;zv#5B3P89-?piH8cQ;=@Ys?{7+R4n(kFOv#CVaWJE+ni zCBVcNFjN@O;aJ)?C)Zv95iYtP6IXa3#~y z@l?zwQa*+z;_oqEC8fGbz8=r$7Wqn@vZaC-gf@t7wqht|~0XpzdJDX})+YN<6@Do)ro?}E){fRV64ER zX!l!*C>qN3t2tva^1WXFcYx{CiEve;)ST8&61!ETjWY>Yb}TNk*(Zc9+6Kib7IYbo zkM*fUH(#)MFHQyHaD0j*4VFuyv!Z0$5=sL3h@$$>x zS~9!x`ulJ%2f>j~jn0P5(qD(l>lwM#z0venmtl0y8zep5j}GDpV0MPZrGd7c?Apd} z5te!KP6`EH!C^$~4Nc5{3zbU7Rt;+2P>95(r$V=C5t_XwEmj6I0TVhGLT}Do zMVKn&a7hC~2j$Y=q$khlu%-i{f9wutYtYrq4}V{r4%zNw8?>>L!OD*ux5^7}#WsA0 zKWGPys4Zt`=L7jzf?Dy-JqhQ!UVY2yKb|qJj|E#P%zly0t#n@lnxV}GjW%qhw@RE3 za7^Q_ z!J2RQnd&CzFK$MUfW43F=jrXl%o*a1yr*a zOR7Y=JJzaV7B?d{A)C^MdA@;kqW8N|<2DEL$NDJ@D(^_9iYj<&{(L&h*>myK;~+5obn>@)R|E_4-)Qxl$YWgkCRT1M%O&M&on zfpE?I4W&P!4%p6YrDMJ8k6Hb#$-08Gxf~C~2gZ7Y^5?tj`m{UsjoCM-B&7C2gXZ*49wL8A==du~20OW7dUzZRFNJt54R7 zRNMSj+~0zolUs$SU3(}8T4g5jk0otbU1F~2%`1{^%!#_8C`1NWsDBy{ncrx@WKioZ z&AjXm0ThCYW81L!gVcd_9jhg`p9J*{_n{Bz@;w3cICI7#V9w>(-NqgCF zI>iMoR}-rJ>h@dOR$kGLJZz9ovo-fLy1eM)<0OY2Oz0Q=)viJFZ(Z-gUQB(fK;_Zdsho;n;pzxpNXj=o|HKGqCJqv_s{LIHUd2aD` z)yf^ulBh6DSK1wXCtjmNj=m-;=`Q+0=qG~<;;$)trYm%nk$yC1B0SQU4?9CHU)!2( z7G;}FnW_1Vm-v|t=m-5Qw!f>lGN%Skuf;5{cT`HJpW%2i(&9{?aCN-QtsXj^by+)0 zLkis=Nc+w>} z$UQa9?tk%{lDHA+s}%QzrMje3{X{XFM7-3dN^;dPr+!Yevbhk!2z`rZ;9|+D;NNY) zNmM)-%1A~tuG~n8gFB8&P-V@~k+G1iX{(dxv49Eq_*;^9Z80D{4la-WR}Sp<5pS^@ z77z#=Jyl;O=YU`zvJ7TuLhXC`IY$0I&WFv08lxQ&^%1?`yFd6}gHsr}Z~Pv~TB-67_kWzJf3?d~C6}{&wG(@7r!5w?RW- z&S#g_vOOJpe5-!lUa39#z5A$RXHqOL9V8a3JOlzR?@kEUf&$44GioGF@`7Ynshqn3{=)Q2c>haspyFG!1e6m?93W88}<2RbcBZ=Q~arLl1UHpzexPitA~a( zP@KUweg>}<9O%#ehhE*2AA9)p=StRwJuc7chkM0Pr|IUfG(yuRnI-bnmt3rxkNf}O z5g=f_xq9_~Ml4_WM_41;`iqYd3jDlNWL5T*T%Y-K`W(uHQTB7~U~2zBJo3O%Q7VOP z8ukNXt^m+FJ8;m&7i#hMp~8BNm!E)By)cGPc2_@KT)yn)>6<*Tt8 zM2@;5rs(}3Y{Cl2WNyBYLnMO8khYpGJv6?>A}i7l9{{ZMi5cl6EZX?J4?7W!9d2fL zLPh8SjgI*&BefU4s-eR{yhZ4&im}Z1qf=BQYB*15WYSH)|I;2EV+BD@@Qi$+7zPN- z9rY;>FMuE3=W%s`hE4!E{8C4Yg&n+9Iud^kll>$W9xEq4{sYz*G&uzNYaTI;CVwQi zQE2*3e7QY;)UauD70XBSIFX&Lzr)6a&LpsObUr}OH+NN*bUE7F=YCUz{#P+2rzrtl ztB<;bq%E) zt0rq|X(u|zTG3qJ+NX8AZTOb0ANqNi)P6j+X0m`*Fpi`W?gz~WPlQ?TBN)a6woXt_ z`^$vmxEi8gjEgA?o!Ct*9b@1 zJ@VYnk_vTLho?jGr~QdszDXVN?=PF7P05JF-lF&ptfQGX&v?EL)bB-dMv?$%rzFyt z2TZ1pSx?YR6h=hQqRt;_S{XD@+w=3wjk9<<@sN4{W*FoPs?pCZX8SuCeHvU8(ZGr+lT|VJoqlmi1 zxv}0SBMpiAd!fS*Z3cC=ANhTFrgDVfZUNwhswXHK2pTF_XtQZod&0o*nan-xIolF4 zpXyBfym&d%?XkPHn$X?G_Cq^z)qNz0oEq4GT)UJd=Q#`1=i^G+*-X9IbUK&7hDuxj z%Z1}J-PeclGo#ei^IMGtrsyTkOy;Ik@i6>yvO^!0tKH0NEm<2lbYZ3!){!pMK4X1u z!1n6b7{%nfk98T*EFGqMVG?jV?=k$RN_89}v$;rKpN3==?xy73%W~=%&W&6>+4lLz zdt2={OsP9iFIM?P+ax3!;n-^)a$k%*JIS-evSEoeJlBl&iWQwv4{ImoiPiYTOeCxm z8HN1Ae~?=!?`K$Ui0IZ#0KJF1g%pYOw3&ekn3H`^kwYp5XlwabXSicQ{%>ltq3){p zU#(^qknuJqD~jAzUx|a}1QYpOCrKh5{aqfJrGkJ5HD^OcF_wG=G_1EcE;$jrMJN_* zW{h>Gj1&^CBqer?rZ1%hTV(^Z^`p~+IHyQGZVVHxSX4#CB{U* z;ay$W-8cripQF1VBTcIQW|lRS!v3+JWpqDpNWbp6Jv0OmNcrnWLk;#o-q7F9F1|&R1@!DihbI6p z-#pFsD&a=Rf_Gk%uh;U5^|dwG-xwowW{;-v?# z8*8%@NTq&qn(ki@cld#jiRF#m8d~0iSN>s{Hn}|zItDg#t z(j-W~1fl{t*|!*G7K&N)H_my)01=2xiDRacoV(8 zHt1Z5FI9as6zV)ZhLaXZr6)JB`e!DGGk;AhBMPgTMAL%&V!oZf)z$T2fX>L6h-peY z#9bG{G#7GvYX7+{hwV8o>YOwEK8qC)4>oQiLS+v~2~kA5H{EQXbkZjwYrNT!OlRQe z4mg{_#%_^ZM&Fib*s2m(maS}Jo9_hxyQ>uvrwE88_6;X^6g_F`H?-nUSR@4X;voms zntw%}q2n%i$sCs~nna!5pKW5;0gdCs3*EN6(oPfmDY^EF;-g=8St02|I%>!>E#iPI z{<{^_*67w0%OORZX&uZ2F0x^oXWfv2vON5Xs=Wrw^(@iT=}L4Pa7BBcYJK&_S=?C9#Vjmq>iNXVIU`tc7W2;JF+Q_f z{3sS8u&itXvNH{<@!Z(2))RfWii@Gvb`4AE7-#k+LJVc=Z)=NdpfmE-jA<%Vv*xSf zXZ4t_780mIL3;Ua7^zhU(5{q-TD_o`o}*`g2Cw>|`whLi&crg}DzX1jUl?or`^K!YH32{dTT@7AsoMZ9p?PcAnQ>{9RGKte5eZ zw4h(-gW{;^*s*J64ROg69{BlQ$ERItDyrjIL_O@4Z(8;xtKwQUp|cZ|F=JUD6jjW+ z3qv<4XD4p>TFPKKRTwbfIjXV=|Rg?an41Xbb8X$`H zFIlO#c~9Xj-RuHOE| z**Tmw462oZ002Mxog<6E31DPZd>s!axBVo-F*N^?2x|^ugAMkF!5#NOkg$q$-QUsC z*~pU@gv^Y{ervMK1{}cI7R6ZRL?LkVo-te*!TAGCNA&gl*q*|&Pjc^fbMi)@>_Xx3 zg)Nb|88tvmHvEr2wlI}&jh2;3o{;ZMr3MrIwPLbSRJqUf?eC7@1=lzFfh(?b4;O>h zx7}*c?&qlsV@(^|TJ1YW?7lCRh&W%Uje>{5g+IS%U3JR17o6laH=B@~0Lmty;!f;! zR%DH3a_@O$UuKTp@}(~Mw8NAwXg8+bjF~TSD`(wB!dd=?MQF;#6#Pyg+qB+&3dXiC z-bMA6w@r7EJfwpB^WpEayQbZ>v$%GtZOy-wc4zS!RXsP%d@s~mO*lve=7ce_k-|7M z-^QdLa`NhD<9JpwmYYJnt{n0k)&Ubm6I5~n6uMp795#$Bx|POBq9(whjE7jPWQGps z7r`(+Yh4n_G{NgGLhrXmWd}-@`(!C$8Y(`&QbEI)V;EwaYWkJOUlfKuWK0$nyU=zM z?0w<1;7-80ph-`!mJP$i$u@Kbq-Nk>g)U70Ais9amo1ByxH5lSb3%;3YkQCm9|;ur z&5^0n8e~U);qM0Doi!In9I5@4cf&Qjp*GMpa@)yTI(FXmq{)C~lz0hWA2rs2+7b;9 z-c-g&q32-nR%Y<_YXH<)ZUYy{YYXcfY1C2)2^H8+vOUA3uJ1lnV#*QFC~~a*Quf*6 zpI^|b!aN(omUA3H7|iOJy*aBDa5p9sHal=?dSvuqsXIlo2|pOZea@|w1%)5Y+WTd* zmvc0>lv%Ll>Cm?LR!W1+&H|r^8Yr?Vny0L6sd#zFT8DGI$J8lYaUbdu`m{t zv!5e;d6;-oe77WELz3uN{VM%Obw)fF9{B0nlu!!1_Ek}$km66GJy=peOtHqYi>NRF zGnKvbvu^fJA4!|-UkN`pY}QP_vq(%f5z(Or;dJ0Eo*$RmW&Y43;Rpz;VL~}hS1)y~ zoo(YI^`M)^&IZvhn8{kd|GqkGG5vE)lzw4%%B29XU*UUC?ew8M8~^Zn!nj@j-21oj z!@P1@>vBF1Vay$-eYw2REL5Dg&U;hO4V$9__eiFvs;>g7yKlNYr)%@2_bB!U+Z>vf zM)S)R-}T(5XGhYLHw-Ed_Ze$mQ`9NQL7w5CM9)Pj8n4bud5POVhBZ%bi(=)9Z)3^b zZa0nN4JuKp`97|!B3gp6^;<^m`lNYParhKNy@b&imgQPz+FjB`)NHe1RgTt^9rewHX4!d)y6 zJd&v^J(#=CVjm6Hk*pkY;V;HO{~;m=0&hQ`fcf8Ux7k5%#p6S^l-s&jmMU`VlLcXo+4j*Em}eT z4Uju7_;bo%%HoE^Ii-r%c;WzWW++s)%aTcwuFk%XxVT zK8vhR!)!2mjW{W|!eIKO=4-F64=DotQt}b2oohlk@0_?083?dWX6L(_Cr){%t)}4NIPKM_w{mrl$Dl zeCn&`4CS|1Wu6v){GA35&%_3E|n3?_R zoB1CsK=9%xBBf1bwek(^y#@9)Tgnb=c!MOG1J+b_bjso7khoiMLzaie=^4q<==nM9 zYjybX&Ryb`@3Kl@Trd&hx3{Ut)nQCrcUcgG1VLS3is?9vD7?n8F zdrID}gK;lpRU!Z?EY~6*tMYbS{YEjH>xMwC; z!K~dcb6VoPJ#fw4zT+>a#^s3wiK>Dgrne9xYPlNC{5bZM>oODy_u0yz;o%48RRRew z$@8L5=tp)>S@P=%Ek55VWlQa55BP&b_*soog~D($Ojd|=W*lW6_bW<4+-X$Ci|e|m z^FR3svxjo9Sp}6?C3dM6E&E-brO%kB%?aN!zL@M>U>gTgC}_eO=_595Sq0B#M%gku z6pH?QQUsXKt@fW22N)OTX%8RNTK<}!uLRZi?sqX@=Py(OJa?GbfE$YNsZtx4`}Q6? zm2-AgylA;qIRJx_^daC8|5u>KBy_VFApD$$a zTR#)~SrSV*uRQ5i@Y9Kw9PL6g`A0W~A<;gz&&ww|Ly=|*HE^j0?Y&f<;f^D(0d1;q z0k68!bR*);Go$_A8|OVSdlQys-pt`NaX!+4sx;qqJRxtavFBmRt!8vSBm^pp5&u^& z^X_UrWV*T4DloqDrGYnnS>MXG|y8*_IEzPcGx`#$PAsLg{6oXtC2@JiAY4}m9! zamw*(i*f27?s8@@{Kxd0fBZrb{=R+P^Uq%d$^X)qza##qLbnI|zl7yjA%uTRryO1* z8vJviKjnXQ8b|{Eb$tH!5mGUS=C&_Oih6D97xgo#&xZFmONA6w8g$YDNp@9Xmw|NA zWrCynIaCRp(D*e$?Ji!QgY1;}we!n_Gfg=9`iT&Th~OZeFb&D_@Y2(~xFwIeau__k z93kRl0M;qPo7KTCY!$V07!QK)3qmlY1jTz z>(`77i_UUaKR4T|Me#L`$_Jw>0Lm?c=My{)d_D@BbT?A-hJyd+BrPH?O_OZ zK=+4uNBMdmAc;(Zlnsua0|`z`PcN{^LoktSO2omu^1?iFx5h$^r7P&zS&|I3t8^$O zQKokIx2qKA9G~6^%k%f9`xN~S;{w~Xb3NUC?QgHP#?>d}veh9GurHA*=!z;o^SSm` z>Y*iT&EE4FSlX4}b>0FPQzMj6OR&sq`tGW@w;>&3$u;8dD)E~Pnp@8v;t9Dr$(QpS z(fZ76J;yziRYnR9w42xU-RS8OYD!%p_G`X0-z4*hOtTQ2KckeZp)^tBnk6$kcXBk9 zFqy4ed&V?suRD!rRS^s<_|cu@@czNg+Kp|OBkqPT$s{UJ*@Aof#QrjXMaK3qHe7uXKY6XsWUZJX%X zL@wL77n><5kJD>-fgF#&$x3^ym`+`7C+Cx#53Ju539 zF}|0>)VIW_UAKjuf>3yKn8K6SLT8BZgMcWCfMTS50e9u@S z1OQMzv7V-r-xui@=0k@&#-$>mBXALVISHL0EM!Xdx=Q%r1)ncAEBZ^>RJ;B@ zNPj13JJFzg(o(&u-2ByDXJw&ztKE)$;vOCwY^UGwn3_-uopo6qw{4u9d(0hs{a9Cw98|VG58=!v0x-s_i^1EuaWQiIARpd_lP>NqulGs7$ z)-TWa13qQfy{Psl0ZX{DW15Ri8~Celtihcm>j1y&caypC=kKTnmOpGfZ@d;H;vS-$ z$Yd@XnMgI}d>N>a@?2P#Z&Mv%edQDud9Z=ZrSfol>e*j2tq8cQk{{Lx=OYm-Asabp z+fMt^dXCvy=P~C@K7P#=cXfw(TG^=I1xjzvGYTHL$$H=tH(P2LotxkwePESlQV&zE zy1A~2VA8D?oSYdw%i2(R=(SYSygXrcb*_n@nzq_pteR4o#u{@V56fceCe-w#(2-T8 z0PWf{x!ok10sZ@Kw9L!wS{&B74H(}Yf8Kso`G?QU=kj>zCKev1p)q)WlTg>ZzBcCM zVB;bWYbbYv+2xV)Pewc8ELCEUQ&sNP8Wru9n^XObZlCqlK~=8qAHQ-L3>re$SlrT1 z&zdlsM?-@6y7GDHz3$V!5JpyDXDAU0DaI!n7VK79I}`;j`=1EITwq;OW+NmV?TWPr zE0%gH`^^QTr|He!&8_-XYw%82Sd$y2@mUfvZ43;rg7lU@yWa~H? zjR~yunngh(O)?r6D{yk%O3W8m*LERqI#cwUzYwA+g#{IhxyEyF7d=ygIv#vEl>}%s z9%|hU9v$My$`_f)J0T~P1lQ)?Mt48%$3&QLXBiWDxyqj0j@R?!l2C&DKE8k6Irng$ zN2)W}DaA{p1=Rplf|^nq&Bb)GGM7gHG)WjyNXi1mEr)K`Qr5Oty%CX#PZPxpRJEN8 z5oOlvnK!P+#THnEs(>K(>&k~cVtDz|S}el%V;}F|!ol0BS#CLH*MTYWxPxwMIcK`5 zcCbxP5b zHZPa_NB3jzH1T)o7UrWNr#9xeiM2W(Y_7MR*zvXRO87q#pxEUdGs zp3Io?DiqxQaz8E?yt3ltci+kDhZki8b& zx&orjf1liQ>cuOXqiJYKd}Zf^t*1DH-7FSx^xjH&KL;|uBh>OqW1k`q3)5!BpK*ARtRzXc4^mT%eGp!m6M_HL8MHB)kk=*JM@{G!A8C7Kn=LsuWRo$+vdR zstu0`@<_}AUqsi=hZA*X&sd7DhheBmLF`didZk0$DsD?5CG$RGr0QtzPe`MW$6t(T zefhp`>pn1nS3ThzE%Xl^H@Va$W_uz)Vfa{J68)}CL*gajFRYWF9tG|xz+OT%Gu7u* z+tV-ktT!G#x6xGCChs-u)4w)Qr~gt}F+uxycfIg0{&<(H*lPn}VIsXMDV?N(!CB<5 zgYO92-kGerk2zopDZ}E-0J6#iNrc&=WiX6W7 z_WcrdNuIL1Ta+&L-^ZUyN#IOeDQGBR4-Ny9^=ymN=sZZkkP)`0J zWUeC3WS+p^ri(#Ts=|Jg>)!UM!mWh;+y}iTN#228KDaIejS&q6n@+WL#7(JWwk|CO z@UQFqAEtWgec#Ig%HZvj-GGW2&q?gQflmlu^8j?iQkLU91I=^^syU)+e%uFZ07gz& zfN?)XB}Ha*h;Ewma;RvWe@u$Q$MD*~oqfW1H94&FA~bT|V@b}h!M~S-LDTJur*w1Q zR#x=XZ^O?up?Y-?6Q|w5o}|^3T2`uzaQdEbe-%TA-#%7ocDC(<9hA{R^X|rA1QR*@?_rc?>{|W8`a^lA(E63r z%oxe$=j|C|j9vO_X+U+D=0u18WWD$lk>xyfX0G>tjV_awgX>UTTi0 zy2uuVMTM|{B7-21H<@YRiV5C7>g9j7OsK-Zn9mvSuCN{?ep(U~D6Nd?>1=rPu-uUQ zA0yS8vun4G=m57}4n(DB>T?Ye0@(k6PEqn{&H*fT&xtC;!`XLwLi=k^oc!GluWf_* z_+(`?nEt~Tux&7lX~F=CU6He$)#2si-BiNAyWhwhD+JF#wUPkT^$L)jfCwm`53u)0 z0?q-dtmd}_l)S<9Cn=*}D|Db(M?`3p{Pc8f^uKIjU;VM_=4C6&&kP%$KgEA#s35TFw^h-I5}-h7DeozqROI zVm80|>Reh4gE5I+U!|oraVJms&5e^_V<+ET9{9Z%JG{x^ycy3^U3=oOh{&!?gU?c{ zwg0sc`M+uM%}vXqb37&GGKCy9@1Xa9FCy@6&f6Y}6 zTJ3=-*xzw)0~bQ|e*JqX|4Sz2U0?n;tM2}UjgqfQ}?FlVsjAhUgOvt`(5{cJ?!zyHOje7Nk~XfNN_#d z(KA_WAMq4&hPDJB8|cseGzj;<>w=-hz(5JFIC)y8THIJW0f<#j(w3b6`TpD`?(Xit zD?rxLUsyK=jkN{BPRe1NYfuw)dIvsw)(+v+I1m<&?;IKB z0Zs+-GszMMH1o8Pm@L*{2d+5P&jX1EqOXCB5}>bDKT{1VDk?7gCTgDl(p@F|-ruj> zl4f%!@^5P2T5orEE`hKh#-ea}dHIb$5`fzPpU1==e)pn09Sd;S4EIG&z`yAuLS9v{ zR}>xr5{`|I#)HF+?qo<)9ABG*(&(QOs$nk!4WDG+jo{aHj37|LhYtCB|EAM_c0}CW z^nZEQnBnB=ceKhUGS3v%aVdcdNbb52*hj}GB&61C3S@!3i#>DVr6n{6~TCy&dR z7xo-Ep!#)N@5OT8uA*ub^lN_cL%4*g@++h3T_p?S{P)BgOiAxrlYGY;w@&Q)Xvs8a z2Q4mk@*~YVzmtP?ZXNbF#w&&;rnfY02xlCYio18Pcb^@goKN?)G6^Mq^BHK?uPlfS z$WzD7X;e50&m>%8fS6cVS+83|ak-O~nz4+2Y1nly1}&fziX3;5C0ZCDhKrwaJ@%pN zp6nKQq$+3NuS>m0*1xl(%$|zXGX$-@-B|cp?K$xGLD9sdX6gIe-Tt@Pn*C&xH6M-A zjz7*P{l?Yq4oFpttG9H>l7yp)Q&kQu*T@tY)GKp(`qWQHUN>7WG-|(Ao|a z&x}6CJ{<0KUUB+#`t-2D=bU_-%-mP0kqr(T(6v15l_xxhenJnK zGDtnnFLv9z3G2isG8ruY>sQJxBEsyuhr`;h+k^BT)jkc$;hJW6W}Ye$lj? z*&s7Oy+F}afwS77kfyAguRr?eRA8KU7(&&sKmGuz;qFbKe~_0MDtDo0u*2${b*#LL z)GEaxmqowosLu9%D+ghu-Ov;NbNxO%$n+r`?)FOVLf=~jx_^?)0R6Vrr>#mFp=d6s zh9|M&NX!G()z2~MW7DXfa7f_Ei+aqFgf&?xGx|5jJED+>JJZ9YNBg5zw7xJZ>^H8N zdV$b}T8S}prsH70#KAHS@f*l%UEQu`4a+dWtgdV&@Kf)nV%tMS9^+iRGy%~R8VqNR zWn9Zap{g7qCGko$ezFdgEOi*|oQzRO&7wN#I6|LEI~wtPm-U@U`)$ZrFQp{WA(i8S zN>gOfGtjSUdLZw|?1VkH0g=t>NRL=nME)eFUzRbPr|g1+Q1QO%JJ&y|zT4~S*;Q75 zrvP+g=#SrUDI`SPzFqEjnPFI$nSN@P%>q#;UpaknN05L5W>Lp@r%PD|tmB%Wo&sKJz%`oqy6GDwQtR>P+<0w@Wy%r-i=OM!MQFv-Wg zELNlIk(?Q1`j{cSs+bVXbWCn?LPW)W+jPbW*?~G^}&x6Z7Ve8DsO8ys|j*3NtPX8qGx<7Sq&k zTZzGO{#H+7Y*Yo;JAeE*M>ZHTddI7L){oPh9%Snrd_E}!{gLIFGoAtSBk`XgVLQuz zlz_kU)*(Gn`oi#9Ps8ec5n=uiiQ1DvD+zZG^N90zZjP3xRv&iKFfAHYU)<9d84@Di ziWp^RKo=%-;?&}DM!n>Pc^jjBHA7flBp_foVScaxFaO{IeimNMmjPA-S>pb>sbb!H zKfa9$bdr&e-h{8~JzHBdy|Rx)jCgms`D{0U3!-7>t)P#V;H@sDOmwWzW+(1|>U`ynRN%x2YU!uyYIJz9!AZbrf(&HBU z(%WNSGSjB#`^#LV!}*@&w(|Tj z)FnoZHpi{#^nPc+JUWk@J3uil~v zTbfBatm0=*1q={VVz=~-8qV`g_f-TRw;m~TTl5KgRN6`Ve^mMCG>|1yZARxeWZJfE zJV!AYrA2B*pzo5C0Yk1Ij&l@>ToyUwFjk+2r#|zm-CG-`oYE{Rzecw+hnYi!* z%2s$$!KVH_${^`qz^}*C7zC<|`t;f4`4ygVmEZj6yUUw!ww8M!Ylg|u+p$88|GuN& z14$IwtTZ*G+*Ezi?TB{HKKsCfR5dJf^8Q092m&?Yay6F?W8i^aGowi3ejLz(h9#`- z#YTaIflSMHYEI*WdrpRKcQAVp=zm0xF358;R$OYuYcq=Nm>C&mK{X7O)z#In2L_s0 zGJz|lDHuU}^nTZ;)cjrt(|j=l0~`7V`ZxY?AOr*`d3rM%CE3fis+l%5UWeJ6L>ck- zK#R}CW#OFIfj%+_f7I34?(wB%*MmTgSl^6s->q-H&Pz9!Ee|g5CD*voZ;}15Gy}S$ z;wd4k_#Qh@LscG*y6P3>0vT?t#%F-P@?b2#=be1N0PgR1bw>R9CC1_y#Ui=WS-;-B<~I~WX@{%c&-A|C zH7A|C!{_D(czZt2|IkNB$tva(fbzAqJv?lcqCksG zTwGj$CLO`q!w0;f`PF^7$Gcz=_y=B42@aHtvbn$pHHp zx@mXdXF{m*j4vRv+vHNWwWUI6Bw~`5q(us@R}4Sd{dAfsIHBOL?sicSTtEyM`q=t` zqvJ;iKe5~3jXDa{TO~_$-@^x8rqi>RupbKJiR@!`#Z9a z5U<&xwfv3+)_F2>j3gZ5_FImNSwj>IFBDxY33VuF{J9b`ZC9ubygBc^_i`!YVWf=h zx0e-XO;^>I{UKo)*CWO8ccDYTp53w<nY>r27~&3~NOnT{gi z@n0L)!NGz4Ma1*0W<~{Ejb887tI>roc28^uq)wJ&oyj-jGlJh$`U zJsK$7?rL!Ze2bm}-qd2r{qVChHh*$ZbQIcndz5=vSy zq;(o$?%Q+w68t+ewqQL}(I@#Q1&I?;DStcV&lE-uY!nNo^NC=OFH~?a(*f{odZpXWmTW+j?c|Y`Z7|&*YwS zOJvZVDg0*dC#^W_b`zYZdL8I{w1yKz3zxDFfQf{_^F^zMtpb z?KwO^i?_k-^rz06*rcKBK)hOPf564VBgS}kc09t=Os}f90TQ;IVe1##^6<%V6jF~1 zt(jvPy05u&N<37Pf2cFWEm0hBu2_MLnSyzRc%4RA!USwvR#T+7QTdGu7&QKW&=Hf6 zXRi|@1v_YyC9|4zubQLjZNUatwUL6sFE4FVyTqA;KW23-u+m(Nv@<%2BR=Ljyf8z# z9TVOf6Em`g!L5iU3oTaLIu}=e>zhG;XO+FIgnaN`-S(lGa+KT8ctqI2p_D0XeAOIB z<9$(B#@pn#EKnldBx$>9tVPEjd>@|-FrnZVK|FQ57eEq8LkG7YRkg>NVV}qI_s~@| zhmp+Jap5=Fi^6S;aG`|&BwW&}my7H-iSBLC1ro8=+N%qa77`wJ zLS7?$YWA$=1t`c-{);&DYzk zx*xTVG#&YT^Tj4~jbHjV#Z%V#Mu_MI0cywui#MW$`|h zpkdWwKt`2mD%S%a2R!PbzZt(k-^8RI-&;me$9<#IqEb%(3rb>JaD~G|n#)9S=d-Ui zC9r9zK3%D<;K3Kfm0LKT_ds8P#p>s)O>L{hmd@b3vxO@SNwrrep7%h~jxwOMjH6(Q zB&`#JgQSHA`l!5-?^B5DQv+JmLB9DA1U;m9j-_NlZlznZ;q(Lf$k%=iKbVly zHj3I?%pa2d@1w){Xnk`-F0;?ZmHI2H%c}2o;D-rUYw^WLE0}zRg~|bym=$po$-q@t zKKn!R`5!Rp^O=a2!N0sO1TFTT`QTIYnKO~|24@`ke11^yo+0|O2pDf3d0Zz7{R&i0 zmw&c@ja7tTaW#@vZKu>#=TaD<;BhiN5F z<}?zoGG{b9M=~iIeWm6b4o8*!lnr2j{2nhP*LsWFjjWM~h!)z88a4s(i!m3h$h}1y(N| zwRxiqyD^CWQ?DAUyUh~(%-eCRpI21szK-bWC@hP<6k_W1_NQgA!68;XH=sV92)YHu zt{u4tVGY+C4c-BneobcMxp!g2$Mf)DD>CgcCp8_TzBxGq}feljq7N0^NhmuQLB?26c zGSA?l(sv46QfW`JkkwZ}fjcUY+Bjw|XUYe7rGWm%g((WSs@7C9MI~zJz#u2APYpt8 zHm`j@-HDfG3cw8-qz9?iMv?LuiU-6~8a6*#jR$f}g({Kqu%L`WP*k+d)nz^-+p;2hXTjQ^Td%rw-5l_Ufj88@4J-7ox+0w;syAW0gbUu$<_1Cb@ zw$OQxuMpD!8}<1Gc*Zj}2K=V>_gjTbe42(MRLZ2gupu@L>1iz)$2g!ZR9~d?0V5^B1c~cD^P`8DOCi$dDWZ@XsKvOy zm@%Y<~Ono5N{|*tk&Pv>5{_{%HW=khkjAqXJBFcgn?EN{^ zu{561^>eV}&uZeqpBd*y!5f>_xTW+C^nGI8c{#IfYYE_LA6jfu1sydY8y{@khI3~77`WjZA2r$zb&NAW* z_yDjzho71eiEkZ{QA^~;# zi?i{gjxP}kP{y+l3$X$*2{EO*ROf-T^ z9U{f|S{3Wwb|$xIraT+|Sl@zUcmue;XT)05@H9>Zwf}z1$K~+2?z^aIQ`j~&hj@S& zE}Uti2<5%@A4W-ZKBD&u{BfQ)F3D>?e00LS6RWY^5Vjqm#ayPV<(4ErMy}vz?v<|9 zqw}5E=e(3~HpFwiJJZp( zk3PL`ubyvv#Z=7j2yXF%d+j+ARiI%ZXQo~p+N{hat#(jqWAes7Y+@?cV;EV+^b^Fq z4K9cUzZeV-WsJjdlos?|b z3hwDt!U`uVII74et%tlm1?k{EtQNB_7QTYaY))*Drtu?Y^^%bVFF)lyOBJsE(;I#g z$gp4YGEf_%^0*cH%z|5D4RFYSZ6q%Bk(J1bke$j{CL z_4yt5di27teY)BRkz=V*2i#@ia^x$gy3P|-=Lp>jw^x2{7&6Qjdz~V%1{v&+V9VAA zMGnp<^F&BvV;;M+aQo1RRq}u$FXxWG6fk*mO)pqO(VDrzaMfZ?&7(xez80?Kf8t_^-GEp-m)+NdgKk=$XwqX2|q+*KlA!nyv#kV{( zKcOb-!G5K6?t3*6SHI^JTa%*SXL zEaJC_;U$uBO%Q8cH_q%fc~|nB?H#YL7jZz9YlW{q9(c6Zx6~Z;HC$IdVA#{KsoMMd zC1%SvzA;^^sP#!7L89Pq{R(%jKka7m6xO5f%kP@LJSYSny4+!b@9UiU| zM{LRzO<#2`RJ3#y={&#%Ys-9`F8y1@EBDo&ksXPxkdajX=J%(Y+{j@J*u&G%9Aw0B zeUO{7as-k8*+n4PyT-tq?pIi|jN)Cf*H z89Ij>hv4Bwb6FhMH_~=Gt&x}Omh?WlJ1YImillZr$)0EgO+9POa+KfQTx1^F%Qb`jng29ZMSH7{|*sMj(Mq-&L?Lza-2Fnlgn0mLpRoXFPu2qD% zyC_el^0b?N6n^9Cb=ek#7ST4mHojFgI8&GNh{n}* z=BCPkHZ!dmlD4Sax>mWN_t#G`_O3ogX16ZKiqxu5{WQ&VP5X6{Q>LT~Hg$o~tNL;+ zy{WQ=6$2fvr^n{qFZs$J`iIwcIr7Hvm1oF_=M;)eKmSrBw*PYz^SV8m1OX(L7j)-| zPlq!1n*>b1d7)9(#PCg+9#czkKj(?w-c%~x5}#$|%#+Cnsao=oEvGMKc{1Jr@d{B%=Tv&C_EZaWxfwLH-FvAqNRVexGt<@_FjW$YW zhvMx?9=$TjBd&3rM*Z!IN|&k^jzU+Rdq{Cc7wvPKmm7nyx|GpUr_LCQTC!rS(g~7J z9DTiE;Xxj+qEzQ;Gtshhd7u)lC6rcD=D2P$L^9jG&;wpHV9X(^DA9_2_BOFBM*d7T z4y25*x#?$6gqT)5tNuzHwNODZ`%L38uK^68#rT$)B&wuGAknO|79D9Xf9HhQ6$Iq4 zbY7KK6~1@_d9(U-xQiRAOT1%RiZU~B>(DqipfI&revqYpNMfs8z`MCJ-3n9_b$B1c zgkN|@Zl9OASg(HC{`_ooBo6efC<)@lWh#?sTaS2ACZV)>JcrC7X4oH$i4VClJB3Wi zw-$?f35~Oo%`(A^ik|bwr_-1e^WuTMTW#HEOTxrH7T+judp873AZ62JGs8>%Q*R+;oLZ`qh|3%e2Dy-{+;H9f`Or zp7xNLeJ<|Qk{R*SR6KHnNayo@E~1_SUE$fy2G-Kh5!zmsydYD{?+-P6Y|B24@>%M% zE2moCRDZ7JNE5J3d*MwCp&V%Fsb1d-=*>S0c1$Ph{rs3XFm%#FQ6<1!in-jp&;}n8 zld4*xp3R42$;EH+QfoKmXZKF>&^Ix8d2}A8a=MXqFN}2x_5K=0%t-2tJ6(5PjE*;t zP)VoWv7ogRSN=0t(D6kr<)wHH;qIM=@#Z~7Sm4Bsxg7P3mrHsizjCNZS-Tfv4pT?n zA_XeP(e`LsxJpQ;v-wJU$0Grmqwcc3+*8{kYOStz(k@s1xby9s!}i5Nh^}zs6o2eX z@kFRL%wxJkJjus*tn#u`j6EaH!hz+dD7&tQP0t5>W%odz;;OtlSh6zygs8-TKm->4 z#4L<)yT7^2RVMU>iuNF;M;%+?$aSB8&nh4BWn!zIYoBT6Q%>EToNi9r{3ik=EH3qj zgK5Ygk3G`3GilR83Rm3evl5)T_@bk?wH0Wov*L-?-G0``vu^dBDos$4ahiT;d|^LC zqaU~CIM3JAv%DETNB!G1f0~J9q^KekScIvck#D%A537@xwT8HGeQVIR@yjh4Y@2|+ zme78YK5Y^5twciMO~a@p$PHgVP^0*a@9

pH>Y*XB^iSCwb_RJ7M`AH*@8ta~{Pt zQA5Jll6r&nQoQrd=;=oTPRnk4vI@!UiGkg-!nLge3_qS-{z6HQo`{={wrtg(NQrrD z%~d%R{%GXxU~We@WD(%UIL=TZDQp@QcQ};4**ysgr>-kncDUk55Nn(lax>Zui0`9Dr_Mq!zW z3jjW1^+WG=Lsof__s71?L~qB=iid(b#{>QNB!kxriCKETswHo;b*YNowT~xeDyAJa z{rpI*2vp;cA05~~sxxzX)}r{z88n ztFkhS{q@HS%RNY=sBIYr)}>g>ABC40oB8+GG&=E59KtzYmRNojEd91gq*ZjRSwi{y zy}9STag6S=iR9God3t>AR4!(3cflWOdg&s)q85B-TQhqFL{%Kh&nI81y(N*c>@GO| zqr&x9EbJlv_+x|BoagsI$ua*^0Gf_EBp@Tddh2bk(g5Ec!$3Qtf&i{cq&B8j8zPTB zdn8(Po+0~JyQB3olrn*AIMsxTE%~7$$yZeu36xs8=$cC{rrq!ZipDR3?lH&EQk%TB zmQhmH>3wPmdJi45tOu2Ru5A?;%SVy*@UIOJ(!Z=%F{D zKx%bDaka|uB-cWK@HNlX$yixE!}jQdN4Y`mXQbXdrk~A^Z+2fZpNchS1@-r+Y(EAi zQyTp;&B6sR&H&~BSP=h@G9^cGKLGD$@x`rSJ`l@w;yY zQR4v?aYZHnir>tKfi8~v`uo4V{DIT>ekiMSUg|?In=(@#o@R?pTM1KtMG?#3*G_u+mAA#bQp8hUJ2N{+q)Gbu$baA=WJ{R2q?6ziGQEMD6Zsh! zW|>WxVVMj5!@!DTEYdLF@5A%tdG4Ch&B2281Uo)qbTWO!_UB{s_vW9)Mjy{4#;c=U z&zsAZBU$)ZZ&uQ~2Dv-sB-6VTYV_$+UdC}a77vGG}%uIKuN=e_g;ywl`8Zi$}wsH*GU-OIW7 z{^I`fe{H-1&B^js_*jxnh$YxNMeHoh#Tzg$Bu-f^?zHS}-{w1!C9duG0s{|XiU!rN zTDeM|zvV8W);&?x(-W~4-*9Cx)Qb=RTjB*wCC0lYZ!M@=O#AJ6{_70@S*BU+-Gsf& z{XQb5h+zxAn%(YtQN-D9kGaP;CUC;N^m;Fg&-xDcF1-vuF`(yyw@5>8vWeLf^g7t+ zMqx2}3E?W@{?=?7JME=!#$AV>0q`yt2hUP`)w#U|A>t&l6`Y7iD}%D@z3%P(Lmq~o zR{k|j24FE<9wpDtZ_m=3lNdHa1jg4~>b-|}R6Fw6zxGM;_=VM8;S)Z|D@RO@FavS} z+fVmsYu^Oy^+PLV{nud5eskW3{fBM!Pkg6YxbLULp9$7;#r>?hD09wO1UEI> zW`K=&-sb^uOP_XH69~&au~pa$??=fdQNth>qs&f$Qt$o=p2UE%@Z=nc=h7 zdB5wKiXcV!=b!P0H1jga?lNc+pG~F<_4O``EHmYqkeDBQXm|{Z zfWfET_1$_7*bq&H1*fCn+jIXBJ$(;-E-9DCRE%{ytE>b2Lf9ibE3v7(w`XFttO$pj zl=*Bh^?{W6iBFdMt|K@494zB}^Nr4Alu>1MWhMUelmi&Xh(N}@`{i|Hv0dG< z96o`Bd7=AO_HMG9&^+gkH^;vTs$*)GkZ4G~+xF$kLj+=_aXr@2SgPQ% zy?*wImGj$(tK(Kn$6aAJRktr3hj}}HZCzq?G%R^i0jLZAX%u%F%y&@No?!nAm&tlb z3=dQvoP#dbttlv%Gsv|@6e(3rD3sQCkAJp->85OUj?Elp&fQ)%8$h`lOLm3>_N6FK zKfc^4Ar%Cb^F z&PsCpW|Y;pv-69WVABuRbk5d^Dg@eRh47(#e(eP_K!X7>0l>R4WNhAR6;<@60rUtv zV6<@Pv*YOy?eIaqih&@yHtVA5C>6Tr)8lh6FX5g)b4?o4Sj%6uH+;H=Je(mVY3GP-<)QaEWju2I0Y_4AK)H(7O)k!9VWhv3%_Qgef|RCUqws|Q zF)EVl>iWL}#5WuE*v^!E(3+m~ro#ubU2J>yTd=MEVV0DoE@Hz!jj<}@_AqIJB_&mj8-4Nn z_}1Xo<(0(<0pdGt_vWO8XW51ikRMM{{B~uMdykMjVXx1RSL!-t|Acye2)-tyB865t zwrurFEK@nRuePK?xS)T-D}^l07kt10Bg7(=ARy35M_&ob*QFaCHy+k|P(3GL6!2+l zO!1+O%>6)~3}LT=S21|hFPD*NXMFDaJJ*K(XU6abICZ)X|N92iWi2Hx)?@ZP9*V)_ zCp`mJj-sb;a3125>Oj8}6fe*JF&P&;%|?d0cZSW{H1IicQ89yK*m_R)aGu~p|EZ+a zHavZb$!QV?Ca^GLaRTuj-BFw{?2&1Z@KbCzjoE8bv2#DkT$9Zju`i)40W=vLsT_Pz z3fIZG&CC2jzd!Z!1+<8_A6lKd`~vZI!{b|2}G+ z^+}|2EyS@qHw^cQ6qVuTXPSKvrKB^m>L#H?vE3@%WgO zg$Pghcq{WsI$q->UA1r29f$vk_W3T}G#eO$pgP?ayzdm1NIvp8Ppq4(2g$g1y*Scqnm;3ULG{ z+%CwJ9ZLRBnJT{nB^JnGQnEIXRVE3qTLySJw$B%E}EauAl)*P`V=HPNG zPg}V|IIm3d8F*ySvu3hIqNrJyna8n0X{VNW6y6$Tid7?Pt4|u8`vva*9Dm4)FJSl- z6%~1HX((zi*MPiD7mJPaAhL7Eh`N3c0);)q&J zmu1zzI(h8QFX@YP^Xb%L?1fT}DTYf1E6kmYZ%dGd)*zD%9LbneX$xn(bJbUkeXi~E zf(lU&1Kh`INfPD(@h?pumt}j5K|8 zAY9BpUa;yoVA{P|!dcV#@Nsp4aK&kBvb)L^>jtvvpTYFst@XM zrO_8rZG;|UQ7RSbGVGLR*43!-9V}1mbg~>R9!Y;w0+*0A zi`e_CU+)*N;|Z<9v-%U$dpdP~CY`v5&cxPnP|5m@|C-s{K|p^LWL7)M2lB$08A#kjYQ`4DH+r(d*@9m-OdSvq$UXe%7p@yVc4f5(Dqoc+ z)%d5^!fsj26 zRu#UWw}Yj3QSzwc0Ideb1&r7ygjQnvwIr>(!joOP)*py?Ch+!0oWwvLyFxFR}2j?mqTi*XuNnGQ0=nH)OL!5v~ z$u3#~E3=>6Ia`4f>O8;QaO}m4JmW_50vJya75jhU57yPP{y`vUurcSz$+ym~4a-$Z zN`C)FMQ#Fi9^8o7N5Mj&4lV5T>-Xh*PduXYhZGBh1COCMrL~4SgM?7&UyY6H8|XS6 zGpswysd#nO3|!+Bgt!+Q1duH?F`H%P2d-z$#r_nha(jx74F%%x%owDmnzyzma?oW; zSS>Zq?viY|8V~#1zEyyu_qOw?5Byedm$~BOcacJ^YlBZ4^_mgLQpA}50lD+~{kw7V z!EAQ^Z}>C%w${5(32bw1U}0flZf0%-eG8F6R8?`9HWe||80zUwr|9V=*9z$3`Wa|k z`E-cr)Q0&@N}L!#qp3z_F=x0hqyk3wQEDT;?*YMd8UGEX1n^g-K~zdZn9ZWoL}>sQ z*QMTwBL1gMQ7dOkQJ~Fd?SZUa`=EsvY&yv9yv68#&c;_$&o7C-w0n;fwI-Ww&gN*i zt}U!KT~R{k)S<U4&31(V}(OOAR3i~t%xP)1W-f6*i?A#jO2Y-X{ zB4=jE=B7q9nYgPa`NwY(>Jh^rHusW0`~5* zmA$=t{T9ZZ47!q@Kk`^S7%$Cn+H+~LF){9pBG6wwI%!-d!%EOyV*t?0wY=hPaRcT4 zPD|q?ufnvdiwXlM9De8xrR1OU4y26#syFA-lS68ZxXG)+KVaV%!*qXSZ6i8c>PUO{ zIziS7U)U{&1aX)GLBdZ^kKWSJsb)GH;B6|3-Er~*`RUsT$#ePn)4l~o{kjyo7vg%D zV!F6fTr4Rv{jW>h9OOUDEX|*xSO5|@Ap?hoGyx#)EZ)UsRczNkp#CNRo>{hj*V22H zPCAEbgQ_cwnt5IA7uT!8Y{r!=lb1h3X)(HonXFF!2vkhpI3qg zUhxnEb;d!`YK$klm>7$LzaV!HEM3~R2-FD#9__zMcXoH;KLOsRl_wA&5Jv8uFW~=6 z_O$;=-2X2^@qa5B-TyzF|06Nq|9dQ2Z-K4z_V)AZ=6&D<*4=!)4^lM%GtXK+-Mzc? Njl8NHROZ9y{|5`tC$Inj literal 0 HcmV?d00001 diff --git a/docs/public/images/console/console-create.png b/docs/public/images/console/console-create.png new file mode 100644 index 0000000000000000000000000000000000000000..582595c166b63795d2fa6f6f4cfdc2dbc3c94e95 GIT binary patch literal 52115 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 literal 0 HcmV?d00001 diff --git a/docs/public/images/console/console-detail.png b/docs/public/images/console/console-detail.png new file mode 100644 index 0000000000000000000000000000000000000000..bc15b256cf5af8d214a715994e7e9f794ff075c8 GIT binary patch literal 55164 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 Mt<8 literal 0 HcmV?d00001 diff --git a/docs/public/images/console/console-list.png b/docs/public/images/console/console-list.png new file mode 100644 index 0000000000000000000000000000000000000000..d9dd7a26ef29299cbdddc7000e7a6e8862833ad9 GIT binary patch literal 63812 zcmd43Ra{$L)Q3r_(4wUjx3&}wQi^+l0>z6vDNx*(pllOhUZ{}_;=3-{fP4+LzK4>cz7hr@7`+T;oZkI z@y8$H<6hn{tK7lEdyJ?2R#w+9V|U@v8{NqjnL`2k?iY7!ieLQ-4|&GfBukaTX3X3C z-eygwuIR#*U#CQk2m#+!I1bduzBL*D@kblb_;lpHko#R_S-H0}x>Od1VMZ6(67+ZH zKU-)`V8Bd#VG_P#zNyRL1F>_{4#W9)+@=3HBvDQG{%dvKdxm%S-{vRfH+T>Jdo%tM z9`S!IwmWxd{%fh>*ul3={<4?E=;f~Ii{|`*LJYMftbwo@H;0b<~-j7vK4BQhwnx55+T8dzZaXxTHQ3HWE3=nw18$-|e|=zNVuAqhdo*8fM5W&v zoopXjDg9@q6g*E)A|h2lY8%gXGFy202mohJ#cuniUw4$w9liB>&m0Zfn3*jtCKU{y z$0Gmy3}e7|D}!9D#Vf}brKF}#u;i7Ne(ztMnVpRewD~BL4R*|vzP*_O4oXqc2nJ$? zC@rfaTkuq@Gebk0tr}hGX#VxAUq({?r{R9*3%`n#ytd(?SpUtjOg(@>53{t|>~dq! zRqMH1?nzE%vH~wOsO-bah5Oob!IS#)p7zUP;O0V8gLSuUK85G*a(??kqqki&$G|XB z6RNMDEU7SZ(oQV@T&XLMsGQ_xZp3k`O!8uj3Mn!;~4$Pl0SIm<>xnU z>qCi$!M6e7*$182nBXH!u*&zyR^J0@TbHJOE2cH3bU{!}ZLQ&p*g*6)D(CJE{sXd% zBNaP%D)gz!{eQjE4oNU?!UbR48q|rBm=>Sql5XO zocQtHiO{$(28T~3T@c@c>easV^z@>l0zQzf`-V&^YI(_N_GO|9HMi+oB|k7cVXGK6 z*#2j7EV_I7=_2R+*@?#4&xEx#OeIwDU&k_r*A_V{yAP)6l|^GQ>~|;~WB2mM#f)f! zZ#!;G7COyLM1E?F%>T}fcAFjF6o6oP+Tts(CcC<@f4iilrR9Hj1EZp`HFb4Ayh~Sp zn>3iU5{7P&U3Gufx;syJmw?o3cf(Le$MeIjx!I-OmBnz<&Qv8LZTY$U1B&*Jyx%sk z#oPXzVFLke1pJ&)^7J}ixkcH|>}ZPKDvtLhaPVRio>XCAEM$}jZ&fP3E-_{EDeod#y>{v4ANpr^r_!b+3`NrTrO^; z=8;bGi_5wPXZHVAD6KGmy>NcI>%Q0G&3-GMCzFvi)E9Wd6BJ2$9ds+3)BHYIpMf+v zs~_&WZL}T@^uZ8F$`ZhPA=q7+qPQ_sHa+6AS4hIJ0hMB#d_U&d>$t2p6*mXL+iw1G zCDM9@w`Lv3R=Eys5NWBxyW7D_W;G#RkEa&1@ISxWejA8r%wiA|4m=u~aSgh_rqNs) zONDe4%0wDe9Bl4XGDYd5O9@bldEU?%vOu}{vKW`}mN+9RQpW5$ve8^;@N{Kei^}=s zf-iN)QQwQpm7WM4zV@e3B!+~UXHxw9U34D{I_7#njr ztyt=mU0hsDq)c>|amiyomi?N;(50zVra_eQo@ss-84_9(}5aRE*|PPn*+_ICFquh8`k4btG< zl*qE3;)y&+dHJQE-}LM({FyosK^=2;rq=GYjfR+XLvxU$^AxDf=X=Ez-o1W3vPX4881LM98N9zAr`xG%IgH# z*_G^E7Q0K7Iw>?cD32i~mRDyxn@1@{xOL9}foN}QxBWKyB;`>D@{;b(W|F-tER}t| z-S4cgZ+1yWGu|F>z64%4ndmQLle`TyciE4bYtYlx9jCAY$}B;3bX-3!(V!0ae0(~B zu8<=$l}S&jD^07fA)%4|nkjDU$4g7#+Gbx@Uw?F&F4s4io70QUZ0NNw>ehMs4a^Dv z;EHCHQW5`cb@M>f%eUoXYN9Yxt{Dv;3FsU@DT?lKjvgBv%h&R;`@03XJn^Oh|PrMDV8L`k)cU##DHM z*;E;-BDVqzvtI7~G#MW2(&BZs!W)CNSL|$~;x&_kZ@ks4t@X6DCTd~P4ULVhZnB74 z*EgNxZIbF{{0rCx|0crFFE#MiHH&X_fA!-5YeG)#*_=<073J+5q)w)QuqY-WY%_nE z3?{$4ygcBX$to)#xykZMK}NcXT`CXur=%1-(J{ z9IfhupBfn(bJbMSO8Px!)F|?|oyd)kV5cNhfSJ1-w4E)qg=3!5=ZNCvr z%tQzHj5OEUf+QF5pr2c2jxpE2C46nLJCx#^B)}l$sot(G_ko|!Pa%WPb?lhN2AeUL zZJ^Ty>LRs{gt*`9!qV@!M|hIku6WHjTO5isvT!ciVtpwO@URb|A`JC<7UXz*LV=|e z=aJ&c1WQSYfbrXZoSzv07YD`)mp+}&`FIROqDj%4u*^Azli+`M>iuLh=yluQQ4!Lc zqp;@C-Vvi(kznew{a)O#Dkqjt7o1J3h=_eYLCLmZhZHstCxa+rlx8D zZ<~-&)aI(0scxGv>O`GhL=k)_>oTT(qwtiHvckCeOGmwX_mr35x%7UeTJ}*-75jeD z{LOVOY>RWt#I+5KT2of)caiPRE>!&?6F6S8{qAKhbavXSfCV9`rnK1Ly_01o zg8-p4d`qs|+U~n{Fr5USZwuBm9qOYn2-Fml`Q4h=*p5tCIUgfO{Gi>QdvpG2G6aaY zg@2xc-xekgCYimBvVj*>YI^I1`b&Eqr%zR)kNh(vpVfUgKM=sxAD==f zdS_F9Q($+HJ)T$4&m%6%*2MaCI_p8?Z-0r#>2z*1*a+IOqw?6mo~J z(bn34Kp-jX#j2z9t>#({+1AA=Y8GlUHh;MqV@XYI-0o^&A=@uPjTBLd9bS)M*CE}?f4(`rZ&oi8 z^YVh^G;a#s;=MD*J)!t^>T)x4ZqiXb+2a((ateqshX-)kT`&`nj=KsPD_oxr*!oHC zXG-{Pu_~UW{IesvEse(yHf}<+y+zTPffD{6U=-?*kWHfLf-u zRr1UKqd(eETU##WrJ5#X;%R7RW@bUbWaeDsBzxHmE?7VTZ!iW1dS%&UDm%t&YCPm& z`a(OJa=On`d3%jNssnH9oJ~3@?gU9*W=N#hiLrglS~oVrg}1&DbJaQ`n*PVvI`c7g zm;Ex&KCEtxPSsXaMqS2K{M^$u)-Kn8pdLGh*AF1);ix0?ASPzR>^|qc-H1}NQv^&y zi_D^371@y<-)5pj_PC+P9a- z!xoy7%LfNCYl#Ln>RH!q*q4G2CXp(G@}zf~D7V0mxd>7{Z$@OH8 zTb>JPbE_pjLvy2$PdUtn3PHp2bFujGHAue@9ARYPklzRNi<23JWSjORD z^ND$$_i#L$W+9NHOy&x9LctC%6UAUa)O@D96L|zTToc*f!OnUH24%pVj$V$>pO}b8 zSVBn_E-F0FU8?uJ6+q4VI!a|}VhkF2^@YH2ZF`;=zQ<|&&ojVzQG`tDQ0LVApzEf6 z`p5s=(S3n5E!eep-{uikP_N^9=oD1hu4U>KrVrEC*KL=+9kpTO=1U&`IM+LSiOXQH z;z4Oyvqpx-TQP4mxflG;3pYQbNl7<0HYh3AaXzf4mnbaU@L9Hd()JT6X>@&ky|0S^ zJB&{=Tf_0|c0`>mu+l!K@+0;{)7lzjG_#OGLhl{1GnP45=fQftR0+RCH7=S99;e#c zv|plPL+-)JJt}MmK582)msQLPHLb+KaoL|$@UHaP=AD~m!5e95yXoYzr2>}IlQU*9 zbZ8-hYKe1edXsCy?1Q~^;Y(*(N2u4Gi?T{#mzf&S#UX!-FD~+qz-CCMzH**byHRdp z@VCDUyCck6q`}tl`UY4h?RlDFyV`K`3+Sy3Fx&1=>{}g9=G%FInxgdGwp`!ZdZ&xg z5&6B)T5te8C$(ojQ+U18&$Q!UD`L4e(AoJc4ZPameC0LC{&vz;HVQG-DS4Ij?Q>;KUl8)UT^y>~**?Pq;? z+Dk+|UaqQ?A_`hrTsohfn#gQWlCI42Adce6%-WgVb^UJE>3GN zak&brWd--y=4>sy`hXY1d#6J+iT6rEs6M|bAA=beSPu7H+S^=3Obr9JM^bUI zI!ZvhK3&Ac*w`BsFk$JB4nVC9M%|tZF1IQ@x z#+NKjk{B4!F=O|Ns`*CVHzpJc%@KfS{8?Ee>h$!iL))!H;DtMQR8;y4nWnDx0@wA9 zQ??@)=R1{QF*M|FgK=O&6mm6L04xpj)Wo3?V?K11%-x8U@ZLTDrrJlw^&7RD>>Bym z+Lwu1dU^`LhkCx#8|&*8;ieT8u)q_@eb3+BlT!tRL6E0M7qQ>P`T7rh_I?6ovjZaHoDy%i6-#6b^xNi1kHdcb=4578vE9Kbs}5XE^} zT@;aQ{-e6;RZ)G3uc}JN#r4ULA7xsOQ|R4weRFfIpV>-$3#lfrUf*JDah&9oYZe8= zTG`qvb)O{PTJ77p3a&+ATqj;>-sR#M#O<@K-Uk}BUbrmh`}gmsM2brVj%GiBf2T)d z&Ea{IP#jNq3l-TWHmEQb0;za6=mhL%>a6>bcqbF}Zga)xEm=Hx^fpClzF-qd&!Koa-%7Sq`VabdFo zZ%qDna)(0y^?PscD5k*Eg~k2-Q-^tG0yM&5nu^E`JKtRHXmsk4R_{#>0I1Uawl6qp zB`0qMV^YUnZQKgO)0TP7%IE#hI|4BtsDdQhV)?II=qAw?aU>Fni}-K|rzM~fobFuT zv)W%%VdlBBr>N8#bS0drlKjD58c5xqWKL97U4OPam+X>}uoij1ZLWskQ=ih5K6}Z7 z#s*#KN^Q6*D{B;5NUZfz+mW*WYmsp}cp3^-s$!o0f)Nt#;eGPTMs4HyV4%64^*YN> z@FRy$2jt!)Zpz9LEh`EC_Q0FJyO3#ifuCc#*n+M4BZ3E@{Ql*d-q&TlCK>^0jf`(f zPW{PO2FaO(uEl-|_#d#!q~CbrxyRUQVr(=yWIREY*1bGVJhFb2Vdn2!@5{~Cj$6*m z^^7!@YU*linB=YJCgHh}skDtXxhpR2&q2EXi)AHyz7x47tB$lvh#@|dm9yZ;9Y2gf z-}b{LP*v5DD3Z5j*_1a7TWq4gGzNq{(m*l|GioNUqbo+dsF^jBm7H&Gvd~~CX;$LM!oHGrKLqvA_kk}#I9ZZqD^p(xY#p-bgQV@n(TU!rw_Q}(8 z{d4{Z+|mdH#H%{Sg74sc3f1Mx3ufEh-J5tl;PT_K4XjHXDc3h2 z(F{k3wz+Pcy@WM12(2_Q3`q2qvs3t~@M>@-8THh+8b} zW??esCC)YQ`~-6zG$lH92%LMa{zTUb2cVQZEMM;HJAYrL zbwP&W%?E#W6s}}iB3P#Wo725{;%CZx5=;pnYv{v!YKiO|EL z&CoK!lA4+~Mv$Ayp@=C3J5e5!bdvevcNqpsZK`Y7C80j`%VBo@y=0gLAFUef2|{FM==T9O0**9i+PqL(Q_p; zV5vh857b>Z>zvh|(%5k};NH?igkxbQ<;@87I3SIUsNN*LE zR;RIa@bp4_HV9MU=39kJ@Q3uk~|-ql3NYA9maJt#uVbx6SJN|g?QN} z!jJ0`r`CE;itb;X`Ah&MYdLk({jE;-1EQ|2aL@REbpcIIO+|JKmD)MwiGzdP5lQoE zokfnH{XgI|1>!r?qEr@v5@_GGWEnlWlk;;Vbv^4&sFT3*SzwlDQ02ttjz+#oLbEH- zCC}%#Tp z-~xu*H8-T1J#;;^w1{U45u+jvJm19ZTdgHSi$`ZBe@l)mQeQyFy`Xj?3o%1qfDYO< z%3;5T>Cj((DQDS|e#&CmiaACz3sEQ6OESAWgt5<$ywM4celtKW>e(tvG@lKL-x$`0 zCQu8BNe|1!End&$va1rVpOYjnIMCSW#oy~ESs{9vX$-AkXg;vb5R?G*VCak{dOYyTHJ5776k}LdJJ~rjpEY}G}2VHMz zzPAfmo9(D4qTb*7%Yi!6_BzcyDlhADB0MY161yEiuL~eEo*lX^A2mtxHlm)#L<`54N#XMNXSXdcKvau`r+w$x;q3sNuEP`fiEALayji|#N;|)mbFaXX{iL& zS7X2g4|Ax%J;Zn`DK`0Ve_G`agf8_DT5DeFq#O zyR_67Ig5uyUnWQv zo1MnQ)wZaBj=1sOU}W$)BL>CFZ_l%t3>CljMx2(xCVcfz&nyXi*X;FZ<5 z9h6DzqPKimaq%e+N&uU;q|wJi@`5pxvvVw_ud$6zF8NdHQifoi5R;90*FkxtVF?x^ zI{mdS9`PrJWg}X-m* zL(yE*2V`D<#puq|K4`?8ZR{)d5@%hdfBQSXeck0DaNDIQHP$lx!Lvb?l1n4QoX0HILY^=J&KRSoJ#y+1(OsN@W9#ef&Y^YkXv znl2)(^Llx*H6pJ!D^n-D+@_HSEcgt{SN9+n>tW%~l95>mgdz59QHU}2v;BR)9m2m` z(A_B|z+J?T%e0&A`&zN3**vNF1fYaNjAW>s{jVIQpyOhn z8Z$foZV^-`5wJF_z6dAhvAR3uWG*;g9i=Ck`$-{v>K>g`i>O~xWvuFx`)Vc#j$`#1 z8m93@aCxuAl2W7ej^o1x`P@0ixAu2zFbyqur2zaGwR>nc^4Q(ew|kYT)khI)T5!{J zix|9IWh{L~YrF911sUwC{8Z=^hRKu|@9<%_RHx!!iCU3Ny)}Tz%yLytUASnqE}JJr zO1Z3@*^-h_m^kIX}I%4*%1<`>=S0{`a9~BfwGg!u5c+V=H zqa^`fc`BMUSVU=~_HBJuE`Qjw#lAMt)*h~g98i?qr4{h(2ryhlVz2dVZb*Q!TCN24 z)D;71Rv#MFDtx4L)ANGEFy;zFC*=Xz`uM}gHf6%I_d3{BW4wlTg$2PjKRiBYs>ZeD z*XA#L$xRkHcd|Is2~{BFeIZXqL4|%~gq`e|aHUD?-3chOYyl1NP&bDwJfPm+msm>W zE~#80C9-f9<>66VWWKujyn01}h^P&&ssxU&{e1?Mp>BY%WmVgBK$j1diS!ao-Z7Xl z5KD_2tud3gzrq%5H3&;)w~1xc|wBAmY2WWrJ6ZDY`}j(TqU3Q z-nHFO8F*ItCJruo4xgQvx)uz+Q$wbtSTr7dms*h+AeOPm-vV{$UcaWQ=adx?4|4pG z?!UJ7N3k|HafHGwl{#*Xhq=#II1}pAq%=LB$}}ak`bsB4Tf}KLwL;|NQcP6+#;>J? zre&oXw^zW8?*_S!*Hv<=`CD*Svyd%ZSOLwMWV@m1qeJaQJ1uK4KLDZchi$#rF(_tz;R z`b?5Jk_{2kvImoT`6}^ATtXANlkQW$I-Jrc(i}xnbcNOQ6Pc9}9gfWovFvLRN`~P| z97LLvh2T;r&IeO^wo`2V97A+^v)M(e2By~qeLG4-jeW@vE>_+fO&D7W_Kq?a7H%k*O_YXsZ&G?>ZL9AZQ2LWKI~KgRp&2kjF8z>>L*aK^+FuRM-!)pwI;|{r}vF1 z$+DsE1AlL+ag0i|Ibi8!>{}9!370NLub(05?5NM}L*DLy&ur!%!|V3=+Upcsx32U3 zE`|Babjd;TkMDlVMCo6RAwH)(BE8Cmr*Zks_kLw>PSUI2n#FnA z-Klj)oyc(NY=Uxq+n;U9)j$YlAj7v`b)5%@75$CxIz{74&gwnK zK<~2SjdTC)Wbu5ph!Thm<5U`Ai*%7`x4nD%~Q0Z30&;mB3>#!5{~ zpTKQR!xwL_=asG4#o%n1e^JQ`p#N@jU#29$my?$l!uhfFuDRamqT72}v&)0m4U1KF z#v4E1gO>^?$Fyfc3o?&nf2~kj&16?3wBDW-*$AQ5#BPehtBHJpRtqZN&&z$nuu+NY zOFOemYa4n2*X0gwq*f@YWqb43hZgzZz!F+Dn9tMqjX)?!@M$cYE06}!6t+!4bj~g^^XJ8Z7GJ#5y{ODsqFmD0 z3BZ)SM5W~e7Xu}sIIx(BrB&I2**Z14G|aY-ZT)d9h38IrWuu7^>hsgRkN};tb4vAy zS?*-NXU+3bNMVH6!kqY3RR!8m#PlYOm`++R(N&K{Fh%U7A}prRfJZ{zRF;^p1xYI!F`RD_^z^lZID27Qt^I6}#2Ghq;M#U-{kLD)N z4V18K+cHVCa$ompTGx0=Kim7HIk@ThEtWCG??*Q0;YgMj{=-hP_?9p2^^Mb6?_L7g zhOPF1!)cCX$-EiU+DU6|b*CCR+$r?_&?gv2#;Y}f9IC!wQBcw0t%*I(Rv%yxGd#C@I zj8C5p@{5>%45N~<>-@(C{HIx%toma~aX{nXt60&$6DU$~esL}j`I^1AX1?zQ@03b} z!y5#JL-ezsD1YYHEVMMH6Y!s&Nl2cz7d6c+*xtEVpniKppiEj~B_QCLj;*wpC;^Yc zj6W{PQ_ePiL}v{WJ7E{ijBkdWD~1xb2^{gG%3B1@q1tEU?H@KL7^d!|`P=NVe)bU#KGAFl~E ze(%!a1?=k2xQRE2b|{PcW~H=c3NA>pK3kp7Z5^9qdu{XTbCuTg)^9;omy%K*pnLk> z#Cx^h46??PzV0(7@DP56DYu;Dbp9J+&)pOyfL=Cm;*K)0@uv~l1_DI2Obn)U>OE-4jNfy=-MKDNg#^!fu<5yc)GGEKz zN}>_}G%-%Xd>8()X*%G+bDoB=#KO#(5#hi9^JP<+6Yw7zZJE)!1WY zKLx_iPOSKXKfu-!UKiv@EV8)R#s@knRQ>S3EFc<)ut7wxEQjo}Lj*G*q$VCWA)jdr zCVG~ddYgqjm4Dq$>+oe$v#w;;nMv0`R0eNMN(#n~4;5X-b)epc%#9#l`Q5bB}QHH@p!#$v~hQdPw*wr>aXI^z9UqH<0T- zC1j21*W)ncy9+;+$lNur;*6h{D24b6a=n(V6&3FptSC>|dou+sn#JRdDf?GxnwKfQ z<3do&3)zo;*NN?15(kOd8+FtS9HuAsmIup-QMCZZa!ea2XezEOhd0NnZ@7gW>GY#Y z$P=jmGdp;ZJ3k;}lfK2go4sk>C#CVQVSq7H`szhn)zm%o`N-nh*_FB|Dq}t6ED6S4 zPjgyc~GR}txQH>q1_)R1TV{dDI0BD_Lh7I=(sKor@l|Cs=*IlyXfiO)Gx-)7}?PS{NYsvit z6{kN@0eS+bzLUW^`Ht)(b%Jcv(TMys(1w4rNf2PEGc?akNJ@!)%ilk_IZ*r7wFu!|e&a`pxvsHktYox6vRM2lKc|4-|IVMhv1Oln&sY~7K zEPvY0@GGKKt=n}2XzZ=#l@4$#FMK4}&hb|~x7U^}?*R?oUuk)gDsryKgz{uUKQu#{ ztz*QIC~`MQ8>wd-pEM~8H9FK{>)c#%I_?+XkKMvnT7`8%boG3Rs^jy_L9CA? zf#{E$!r9+#jH4P!?_Ywl>#99~#xBoHHkIv4UF-|_fqUsS9nI2Zj}Y&sU9YcXegx;a z7wJE^9Dfm)D+uQat!BqUF38c|!Q|1tZ>4>;SrAACl|3mel4))Ht` zcAVnal=EoL^nZr9Gd>cg{EMb~$bAi%)&tHx8cP z1NxE0gNkzPM%Q1YxeCM#1xUd}0_V9@1>Ut52+aKCDMcS;0oVYumy%h+!^sVm9#F<7 z$s>1rl`v&MrhN}FvXIJBxA!A1HDN>y0O=DRI2Ziuf*nd{8{^D zVtDOJiv1&*gq#b^S~kER8k{Tof6Kj_3jGJKhp>(Jh3h2MK)ZaM)g`3T`2*1sA8IKT z^4l+VDXoWwNZ&ooBt;kgp<(zZ&RHZ;JN07p+FY=;Kq@r*g$9vH*97&;d z_!O1fmZ_dGZ@BoFgi2JDcIu%g4J=RQ>g-!w8ROv;Y$z*eLuH{k$eqzd71r`SP@&-H z%wr~V!$N@8dt0^OMG`pfAp%(4U~7*}|5B)#z^aW|=X3O=lM)s_{R~OVXx3~}X04(v zYS!HR8(@?zQ*OaNX?*5Q7`ll) zqmzzzV`=@1X0@w_7%kaWmRZ9=+H`}aA5{xB;z@-r-SJ`%XBxGyD&uH5Z;C=Gz~AZU zgm0sgzXn}}rdJxYY7~az72C$tnKL`{a?)?m$}bFe^`8hK<(1Vv<>00A%2T zu%VnApX^ad9n#cJQQ#v)O@pcJlKn3IOjBA?#_;-|cP3N3xSx8>lHu5RhVQWQNyxS{}SB(Vc1%&))u8y1i|InV5Zw z>|gc*7g2&RJ!8$yS}o_U{^5Yyv_hcA?S8CFY0DQLJfh|XtMD!Y>X_x(pXk32`yX3= z*r3WXMULwj1xLD?37H@t^S6U&JJJMF0G32`yLUtrWZbIwNpwY{t?rTX@3m6+oz2u4 zE$(JAH~)xSOnYtLhZGhHv4OTuYcX)J5fG1!F9ApV`5-+h>~OD)w%Te{n_l=Q6izlB zAMBFF22BGTdOIxSmElI0Tu&*>; zCcS!$Tuy310m$21eL<=vWN?wAh4b#{*o>Pf0};`4$dv+4TJ(E1esA_&_K+)$og2VS zAb3T>#na$PM)-v-k&FoXO0o<#93|!3xZy1@4&{GD(NvwKxB*b{s`Rb)DebA=S!#B9 zv)7>YjFjK!qX2rOv>fTB+*|FE)-<$!-<=y_c(yK89FPw7^Z zVDpq|MBG(D`G%qvrD8~CEYdN37@y1Y`@(7RY?o{WMQMYqYp%;R9n?-_e|idB7_;&l ztq^!Hcl{lI@%C(Lef5XkHUOPX9`xa0<8tmrL#?EJ5^9xs+0~6r=Qlx>RK>eE@7z?C z)c_7Ydl)s5Z4~2^YxFs1`P{pga4(UJgr3Eq`lSo8p2tVW%U~yJlZt}IWv#a|1=Z$y zZs$bohg-R4o$xecfGQD@xA&_UrB&ALK)VvV8M{aJVi4|8`_zJWP&_=chxk?1sMp|R zTL15w$=D5++o$33MvJcl;zbUXBC!wPe6d5k!5^=!9kU2_7-KXTU`F^zE(zrZY`1;t zw|VtPS45^^v14By`L>{cR<^T2J`~0@2&cqU3CCS_?^%iLnJlDLpM@AIQ?#8a*p2xc zf)H(Yl%Ja7Jaw3y4iYvwa3-GkP|M^b(74F4JLvz4l5>A+l7`yM+WEBe+<&RF_ZLnW zC~r0{ZCY6K59z%PS#*Mxk6m@JMM9oeRYAw&_Yk&i(`S#Sh(+QlL7GqxV6gkos0;dbl|aJkHC&~5H71hO~{2ZMqhe^yG7D=9B7Pa0fj z*wFeKNXMsu)X&@tOZuJ^mGmo-*x(HpCAidqd=y}A8 zwa8fpc`2=cC%9jWxC&q}jr((EH4H=|Ik!r0s|~dhp2NfneqO6Pz^0P>9f&KZXg*J; z4x1AhS8%`;g_1mppD$S@?q0uce_r<;+zx^fOsk(hSLk~2!ZIT88mkpDSJl;^moP)Y zak4UOwxv;@+-{ERtCd^& zxy7!++M#Tg2}E-vgfJ&pv_%V}Wgn77AXM2T7wM{QshK^t?*yZbQ}Ld9Dcql#x-8OW z|B3qk+a71{81wo-%^(4n%0YlWFY%d`qXCb4WQ@4#?6ZTi{5sB{G#8g-Eg-WI6m^D? zp|w-k7qVWyKGgc1%{Bt+G@$(@wdUd9PEAIEkM}9*IbLI5CPv6eM)>@LcGgdT$12@S ziG437EfR9jru3Ja__A6V)i9I(e2*<<*BvL}|pkK~OVV!yZWh^I*uX z_k*h^xxeG(+?zHZYc z+r4Qu&?@z0Cfd=OmgSzbI^Zc|$W2~p+>3YVnT9ns*{-UZT?LNpPn%EqTb5SZD3_Rn zHw2Q@-n|X#;oI+RLmt zs3IoUT{V463xsOr=qTPlwHJ>PA;25eY7K1I>7#_U=tKti-B0Mr_( z#~eIJ`%k4zA>N%PJwnv$B&1=vu&+b4=WLR0KJr?qa+I=^;j<8|8uR+ydWmDYelXIy z=Z}wxQy=;jUG(_p1L>7znP)?ed{afgTIOa{e3Ql+#d14rXp~p`p*T=-2QA+srff# zesV*-?K6@*Viffkkyn?mEa{Gd!Vt_>F1>te-w~DRVo?i? zTJL?=hi3`%EY$h+|Ca?QRSqOI9+dRfeM3%bS5MtblVy!Rc&8IBhj1 zKb}jHNyyc=TNYHd32o#XzUU_v$^dHr$*^_~+)7Xc@BRoko0@G3#7*zj9bBc;Pc7e@ zW>o&kcj2CQ3eDW!-e*AP*3^by!rN{9H#QtZVEZRLrvnCmvMv=H0(kAX;L6?`w- zDNAp-rG~<=#jorg_M+NJzRh1|-L!oBRcQnjQNDV~oBi|{eXiWhc=1SiNzw{l)Ndl8 zWWRV+JYKCjtHtQAsZ118NtjvNx=oL{qSo+hKQkbIX+O6Fw%2ze_~R`ggkcP#{O6w# zm~di&v)*gXv{#klXQjUfYK=p6!6)aakl30wvwR3@Xmc_{a?8-D z2{rIs^MaNqE%Le+zVfCl5sc-Uy}ck-$dEiDvbXmPtHc#UOa?bx{Mh-@LQhc^b8Tqy zLdb@mOI6!ZV=LO885fzTVQBqDq!{dnh`52OXS}bH2BQ@F23Y#afJ?~Vnso}Tq!HvFy;eeQ1t{S zM-t65eZ^?FnC7H{gJsgdpHynr#oULE5&7~7P*!!CZg&#O)8jv9pL)5ATi#@yMPBP? zObCeQkBI8V^QWvn?(_9tot^c>_}|!b<_n_#=uvSH>8v-Rk$R0c9Ygx@{ao7HG3?dg z&zpY-Ydu%Kmp?L?{uRtt=vVLkq{TT3z;vYuFi841@Y*V2{oH-exIxw3-JrfVy={@@ zB3i?gXmmNBmuI2(dyRa4cMm64+}N6N?3|&i5pr;+tfJ+emYaDJg^59?#cqbf)KENf zs5!^<=wVhoJFIk>K$gq;a)wKi4|)`o8CO4d()E8Z_nuKrMQyhz7DPorL_xZOfJl=r zU8&NHO0SVlsM1Ryi1glj@4a_I2p#FYm(WY-CA1JY(f54kJAdwvGw!)#-1ReA+1Y!q zwfB0~eCC|b{Efjhw6-*tK@YwA++9PfGPMNCYR-+g?E%1~W?2qe$gHU@et$p%5=glg z5mT_bpser)6EzR2MDmV z_EE*z7*0;#E4}7C$9|W0BZs}nz__QBb(h|pt-|izZ52YKu8O+C<`%!o;=ox}&0D@v zYwJciw1}F^poMd~%_-}>!y}1>g-(LH+IRc%LAF(u-v_CtQDdoi6|Gox@{;G0e<$pr zJ)a1q_SDXtiwC-~6y$9G3a5p>80&b^+WP6ct}J$xH35c}Sp9Iv^3R7ae=+_Onc(33 zA6kAaeOq~PJ=h5HBzSN+sKf76NOsaoZ?WI&)0`cWoC~4ibb9X65t3M2dGug+H-4d! zU4Om1m1gaAt8Fwz+2v|D%^u^==(`{1UjMZ#Y02_5o;vgWh-e^@Dxj=oiuChSJBc1a6sHs)rjk1QD5+Kvgz zm=>ItPyqHiTdD>ACSEO#EB9WBnaduNYDR2fPud(ZiR7PW6$@6&W>kL;WT+I-_YX&RwoCMmQ-ta*ioGSF?b$ zv%Adm_!AwACe0fkTi6XF@D-<&!;s;BnqMKs{^ZU$ybkFdHVnWe<#B z^B?0oow(P_LgQ)Y8lDRBErtzwa^Ls^!Aov3Qlxmw0}{fX4BT%4$f+?e4#>+PQJ}HH zH>B@dm0yOH!$Nva)dIZ^{ipgU@q}0BHivACE$>(BjjV#k8K)bvzVj8xVUw6Qr@!+R z3z1wFE*arJ(c^ybS&9N)<%nPm+$fa;bF^+dw z=5Gru!cJKUwn04nH(|zTLmc@xg*H$$LLK9IPrHwUYD#&XfeOF?!*h*Lpy)~*A1UaX zFS1nDE&x<6B6Um_6b)h_aRJS1{GJC0S3NG<;+1hRv<$LXmNQg5f&cfLvHn zNJ%_TZsO2u9oT}h2rdWa}lCbLEd`76j#VVDN#en9BPFe zc}AlCyJ+#<%XkUbYsS6C;XsYO`6nYK^_<-q%Oa6#+U%7FpKx#z#s6C5v}6IqrfSmt zQWRZ!QmznV`0}#WOcaVcn@ekkGQ|tIWmL*ys-fy0-VY37bw}~P%o>hno9%xokVKVD zFD6btDa+yoH&&8Wq0Dope#f!Hw$UL6!EA5ezP3`(x5&Nv2JFqnHQaA}s5w$%8M)V4#c22=S)?jtn zq9B4AZ)*@?!c|FhfrWl{>{MsI$Xy()8PmV>_ZDHQZYz1_z?%a3Y7w25@E!!_ky8^g zd8xH)cu-N0RN^1~Ohr8Q`GHkr4aMZf$mtHS@wHi0tp;!3<4T6T(RnQY`Lp%d$dlwu z%UVV5_Y36$d1Te&pX|k52PJbeN9Wj>*fg~wW2f@?a^ANcR~3MWF7FBfQ~scX@!@k4_|9~FtC6P30&gAVgA7dREMHfQX_{JAnUpOx zByznc*Y&Ydhmlis1aH8Giy&F#Rnj4Hi-kw1msJOe!5cih{ZlQo0*X5MGa3ClHEci9 z?v365Xt&_GZ4mr&z~6zhUM7BHjKL@W@dF&be~wJf)%i&!gW8*UH#MM57~AKtTeo{I zo4mOw4v8uJie~RUE$xlLao#bNnlPg@t+unzT$VYtDQ4|$YlcyeSf*x7H`8_HS_g@U zhIG^r&4QbCj4A{D)aAs9(ruB+R60EJA5sPN8%xF3{)5w-BpwtR+vRumYGD?=#_)U0 zXSs&i(%;$XpYr8hkyFf<^PGg~2@a21^3YG_>v3udG}9QI=|qie1-{gT-sRE(zZxN! z7SG|?p^E2QCYiv0*Vn^N-^H!3^JFiA#*t4TZWxybryMIGg#C?*?C&}_IAHyLB9G;c zx0J*l6s&)BO)H>Ovv&PMgZt!4a61F{r-y%W309zo)qvc&H+-K0`>T)iMq}2IH)Xt5 z3j{<&h=%_5sBp-gmZvY$1{d zyCRYPdoUcDx1ye*<(rbn`JxcI@9{LWKr#FIK!H1jyDV%9vo#QOLS|;P^WBu#X%A`V zi~3h$9H8r#3VD0487Hf}iZ^r}Ix+6p$k$TW<3pM74o(Ar^Z|J1d!5tQj<1!6tbaXR zaJHJB{u1h%ncxtqXzUqY)|Z)pN3O+d^m)(?_uyjbVag7jE1F)dyFztU4^KwNn{?&l ztA5(iGK<=Oq^s3o$;0<7H3{5GV>Ij0C{9Xcz&V^d&|nI zjM-VC-1r-cU{ySv<2d&|QeJR8{=SL8W|lQ1;sgXo-|4{*d)|PE?)>`F`j?$kIt#Rg z1kFZ&R2a_(vEXH6NoZK15zhT3LN0@yhI5q=lPX7-FY4cKixR<~yJDVRmAwH?gli=B z{wRyf6a&-$S@^c#5P{WhIII+%lkR`E{s26x_mSUJBpeRy3U+4G!%Xi%Wc0hC@8OKt zcy^TRO6ds(`FYoLqole%f`e5Jt48Q%*A-;SZq=?_nF6R!TfnNZysESTB8#srs6Nu0 z=r5XUsL@0tZm*`!+0GHrN#wbEjT=_k`>wbAV2JHRt%h%LPrz}fZ{!;5c*QKyU) zkIq;5^$`TF`5Ub`zX$%?@lz{TAv&drt^<+fuQTgi~Y05^A=dPx>VAPXL zYjVD3ekmHLj^Jr&p*m`FK_;8+K>n@JuOWlGz4<=G1ggrs?LWkvVBd4Q%O2}grj@H# z@zZzcoF~0q!UPAOC#>%x^I)DcF7qqrCLy%Qz3*c1`Rb1NKM(MA?3Uy{)1rEPcTa)> zULJgr*K)qj%G=m|<$LLO)tIlFouW<&wf;7JXmhG2b5No5Cv_n^go1YOd*f9A=TLvx zH~50Hxw$BB8OMA?cO^TZIFbu5I2TV)b}>Q6JZ|fNY=R0Hs>=&Jf2|xgCQc3u7E)DX z!^b~$91&u-RXQsHR=9dJBhNF;2|K*=T`S@>5B>`aP>o8ywEq)aGP})aE!yB(pVfH% znp=N@UMNhf(GBA7LA%gY%CJ|Mrh>fjpm3dmw-#BBXIkF!~w(vet)gG;VQ8^f%)q3kghs7Cb_k_#eeYUPn z=LHkpIPIIlM&gOl&Mr|^Hq1zFmkg(twg**bY1<_|3~~D7S@%l>e;x+8glw+tuP-Rv zNonV!oolewUcFlyEL6@I{<6aCN>7Wo1bwIhq_U`Y)Xe0?TTu;ln$n5{N8#2K3X7jp z@0b*fmLd5!#5k-m5QdPa1pJ;^=k+o0T1=0$rN-DmkqN<63G+hCs_M<5yZh81y`2ee z3-#W`M9Lm5wx69?DwmLTbeg=^Wy3{?*5zD9iQXXVIwdXidkiFj*k|CJS%hyM2wNVF zG&q(Woh1PkBB(GD?Xq(h9GLYftVB4Rw62bC1zwHvlh)EYDY$glc@pcsb&eq_yeEB9 z^qK0tIckGf0@d|UncCU4PbqBGT=5Clje5$n;k;#=5hhbCqaD;O3^lyGG5?S-VaTYjjQ=v=WM1EJZ9~_Z^bokPk5L5YdXc>bE`F zz`A}*E%xZY!Z?C)90bXuow(HP_*V4w5<9)8&>yn!}EyPCG0WzQ1Tv= z6hm%E+Qz_}AGe*}TgYGCK15&FfuQ5&7Ya8oIOdN~(W0{rREQdPpr!z^O9&zPl*0ZA zkc5O&Tc;&k=bL%#LF~mpc=@?fOE`nMqoTTgeqPqAy+Tf%XG|TPTt<%Mq9^M|&Ftdh zQ&s~TRe%A|v}Em2{!_vIWwr|RC<9&Dk=~f}c}9lRw8?c_&JKy%W=)2$zL*$je;E*0 zyqK6>a-cL(W0VqrAc2~DxP8a2JL`RPjCjKuf!Dy)11jhnF$iX~?L!8lc)jX6{!cX6 z+2(mwd$trabz|;m+nSCRTr5?_VCecQkTJvmT0-?hH+vq)#MFNJ?Zgw%GF-D6auQ(w zSKHIpCPS`}i70bH*Yh4FHQ2_M`EP?P<=4&?Z2GDmlC;-K`?wZCU**M&&TWg!Vm2I} z_^ZQtZhR&PkP=dh5IWi8GnEJy$2xT-E~Co2cR<-FqI^yAH4d>TQe=rBjn`&){6O}^ zhNNjAi)^SC$%`r`<{_51Ze7=L4l477?#^t?kn>m_bm*{CUa|T!;+3J@_FtfrPOUf| z?YLh@H}-G~!?Pe~w+_nF_D(A0Pjp}-4tkM}yY;XgHG8jH<)(S4(Qf^eS($G{$y6G#XI=?Of ztHm%eSoNr)X15$O*Jf`}eN$bG0lCHM&8*S;R^7iYi`w-~I%}7)ua>31n;#(%(;VIm zc3-amePe0xgOPTY#>m!osm))Hp(n@dT3dE719wig9Z2GU%2z(q8(1q$T$D(Fbw8;b zAu!n(x<(4xr2sE|EYrG4x7f5{^_DsGJXM9(F^VK;b4X$kpz)FFWYp;bi?P3JkrpGg9zg#_kYoEHHG z?Z?#i3TtU@p}vJAW*m___QT_<8w{`t4VCfh?1hoj?jNs5NzTVew7Wqpygq|-a3C7z z&8?JPBHmA!dgtJjf=nhN8= znDN2{UZjLdcNStg$Ixy^Xkg#6{9GL5y^mNn1JGX-(+AmqfMTh3T;}-nSFKH~y*a&^h~E1Q+3M zn84vgMEnT5?_*{~w5Z`IawBx&a+NnMd(^yL;p_m>bWqfe2qxw3?kr|7Jf{#m>}Y%8 z=bm8n2-aO1UuleRM#;oL*jQC**E+=Hsz|mWBgOJJ`|BB!ZoAx?O037!@c0hM$W{9T zg^xr+Nym{w*)hd2e`^ML*+*9e~Uo;1s*biVb>t;7R!Z(JY{>}^Ag|^;->?Ty1tZ_mOynL9E0Zqdi>+m+ zIVK-IegY(}ap_*XCvzjG2#e2X=7_TrHOCt@v(y@kuAjc*6^XFd8BND|q>)J85GMAl zj?uzYE6KRX+qpnOdpTi1o?`dnnuEuWfWRw@f{l)UU1^mtQeagDwPqjUIDwFE7T6DV z?OMFUk}O-4GN1%tJ5KZXfmvbJRg}5d&XX)l7huWc-~Uj4{-+_Rpg^s<&Jw>QX?!-f zQ)awOeld{;RUuw1g^azO^$N_U)>vGMUGJD+_C=4fD_lWpzIDHo6)JzD>Y-q(?~r*` ze>h4*7GPa0mZ1siUOw?iC^N$-lr6B1yHiyQRg6Gp+{NA39xh;XkxSSapmpi98c+;( z#nSd{znCL!@f~RfY^axt$Ymp&v_ynOZtVFg9Bj;Nm<8aLqsofhQPcIf@1qR6tfmN? zlG!RMj6w~7)^(NR_Yw=2`ln$9&jzUO;?+;au`N5l+ZGuP3N2@@teSUtf)%4^n0VvKydj>m!-dgM7TeoQPRMQ!30n1xIGL=99ITFCjwy?eOE(76jLXO-2h zJPI?JVAq@~h#avGVi_`{qC2j7bGqi!%w}t9e^8N~Z<8kLvOfC_Hmgh%>-Pn>b+(8&NX_C#DJA%y=2io^i!IV_GNJodJ$sRX(+`a4>NJ;U!(532DM7M zOAnwe#+zbXdeD4Q>?02NWc4dkZ?n!bBuB4T9MU*sSE=KfVknY(w+w2wEo5otY%0LI zCI;eH;sPaqJ89Z6uHPb{Ep`xe4x6O1q$Q7)TCc;jUPwYbdUbJr%jc{+ohr^ zRWJ)V@ZsYGaopEkvg47#^l#rd27(60;FM<_%?sc!Z}K{r*7=c8^c}O43Qc8Yc81W| z!K56^guZs0=@$1GEE)9DT-WmrLxjC|(~7O^(LVSqe71eiH~Oi{6^diwX-&YRibAwE z??F&)b5lvU|d7i^yZzz`Kd>+p{=z^f>d5#SgxXARz(S%;?x1 zib^ZM*r3sMB6-&$)k_Z@rH^o%AT1|}3aX1LQ~U>t=4MYX7UN@DgD;ClQg9T_6$ zD(uO#p?KQ#ROLvAm5=D&tmP6}w=0zS;H>gIpQYxQ>L0)?OutM1QKGYk$5ejBF`)^_ z>Xab9BC~a`dN9c3(tMvCfRNA}5~KSHgG3g4$78Tz#dW|;OsQ^Z&~36!_P{8hLo!$= zExUPj=o^(fLL&Y~^!d=kGpilZ;HmkK?5C7u3x|jJDDHXu>b_rxaVaPnE4SjVT=TFX z6V;~qB?HRSiMInb5i$89?04as*eET|dhv*%Z$$<78E#Vsx@nVoYPUo5X2;8)a~3?^ zlJy3wbU5ahy`7>{EmgX`h$}qEQ=EppO#%%m;C*>5+em4iq*8v;-k#%?Yi{y_(@D81 zi~D^2YCxOp=*!DKwpM`z!$4Qjey>F3*RcURiF75&R={@!K-18w`- zda4krT`b&<6YNK^H2GBUzO)RQk1E#5P4Co7zv+4%8rj7iV(kMtaisofQYTftY%-ZbW>N!KN4!!5c8&z5{f`;B_x_%xP-zQvPtZnKqF{_l(_&6iJKKniV zl}v_Oc_ER8?D5S@jFGzs-9^=_32iLV?N9ziS}m=~#tN^feWk(d7)OOF%X-OlX#b0V z|H1+sj9JGjmA8ZrreOOvT4xM8vuQ@uyE~N6x%j4GZ#NQLe9Z{eJUsN9%xps25r`x6 z@T(EUM8A8S2l8cmT9jkXbVpldMAi#6%=-!r8v;DYuXb#D?iS#_tk~R=QWU-xZ7OO) zv|y@l*}7lfTu>_?b{i!fdT4`YXlKI99?*I@_sx8^i9Cr&fh5qy&!NHbV!Uu8EpR9bf zu?!8>eXaE8M%C@T>fVQKm3nhD0PUFknMwb)XL*%Z?d{`}E%Hh)N7^f2cU~rQ45Z+^ z`tXu_Zt+?GWa&2h))@Wt=wb_Ie3~xDl1E!wKK#u+F)45L(8xyU`ZP6Rd`q3dyje_C z6;)x>b~;nJ^o z!_?*_el@%#g506EK|^Mrr6oV(u%X@Lw?Do3_9-*^byRGP+qA!l#ln$jNsRS0k#M}P z_g;j>o;QeZvPei#k8=7#ZG*E7|9fH@eDO_xyPxB;d$y}$VIkJFpNsKPDbUhl<+t@_Yvkk1RgPdA@5qV`ui~+rmwhGMW<1rinVy^O5N)EV( z;4cDHswTink)(A$Rkdct!je|Y_T$xEMH1YvN{&F*_usdUk8l`1m{|y9+?fnrg!B($ z@>{>ke($%*h!II-jW+g|9xj}??EQ{|C0;y}nK4bhXP@i&W6zb0O zo^j2lxYJEkAjvAX;8&DY%N25BwTnWk1IJ3#Wc_(s9{p{r-# z3j+}_eYJe#Bb7qm;R*%w+bVfg>+{f5X|;&$rFSd%n9fM^#2>?LdfXm~S1y1yQBOUk zKLL~ODtGKjJ@I9ra*?-^05vSMe+Mgqy7MP2<>Yky52mLf@`}(T=ew-lBo-Z1o7fEO zSdS84T#OG_RTrb6-kv?`a3|Pse8ngWRuMk8P>3R#$FeozPtS7OLpE(!Iz;uqVksT~ zAL)I0kaml?>}hf1<&wR=!DG~td3300oM50lV2WK z$l0dq$~s7?O!$nJV5HU8L}BjnNv9a_@{x4SBF|DsWrdR1XcEK1JkyqNTr_2BN4!DI zn^FjK^-;HK6-!!bmJ8o#a7o{9Wv%atqN?p3GnTwS`F>Y3iJy(UpTR ze$n@~Q6zk&w{j>gdB1_HJ!GDeY#Bor9i~l!;b%rE#+j)<5)S{xIdb1vbMf1~9l9Q@fvrQ^O zzh8Gu?_!3>7Ge`(t8ZS*VkyCY8xhXk!*=%FkXH_M@;!L5bd+WSBR?O_Lm^j>d7uj+URP&WKwyytD*w$M8q4#|p8kJbAl^3@6FLW>K(m2PLno^sX;M#ahPjL5y130od;R_~OZ3oUVxMuBHSz!H1$=e}|~+ z6TzYTSbIa#y@QCRrKPP|SWzde@DbiG(q-nR=Yc{d9vHzh;}FNkQ_LJqT>V(4f!T;kXIyjGOhVfek1N=D z_bl+jaa-ejO83c@xpk|8mg|XyoA$1l6N+@&LH*~Jx2Fj8I0M`UbWkP)P(#9o?9#Yg z3LS6k2{oXQVB%mP$*4SPKV6NVF!MG#EkvhA0Dj3k7H!g=LR^LmojY`hIs_9Xb21Gt zWCVRwxhY;1vtH1lv#w8Zl1oiF8vSJzTm9r1>Op($C}Eb-w65|{{d6$8xijFv4XTz& zK4v#)UPibVS+jQ%zuNfv1W2t}AQt+1BUM7Y-M65xXV+)V3t>5FDZ4p*>wQT|d%c7X zEGQG3D)Teb3FIQYIf#p@o7sHD0=0o?>Ej2D?_$`<+jZlYm~z2iRby=q@dW`zt#&*T zpH*~wLxC%&jvC;;o;q@2_okaxH`|j?uPKzA(a2tdK-5lHz7jfAGc-*gdYra;;9!jL zR3;a<8GJT1q;Fk_u%#jub`bYq!n2#u%F%c<2e(|W$Vl|sn9gT!e0B>@DmEGx@?M*e z(lFt>9sLnQX-T31@?6~f><~Ypy-ym=B#o%hc{|l z$~E6x7gi{=>a6Ckl{`}6$uaJiv+FzSH3J+ISsvweE*S9yAoMLYp6I$p3gp^N+W65_ zOC*FMrx4AiOV430Xy)WXb@gntmTrb=EEzUmMQpA|oo;Ak( z_-=}-8KMY)=ynGirZj&oRUT;GsVZZAyl0{lli-$2?m1SJHbmmJz>qhH_!`%R+&*7) zb;+lPi;IY>RuubXxtwit*Ii8DexDO1T2u6!=ezjjWG^?=^BGDTn_H-0r8q)5m*Ogv+u*2?pYk) zUdX_hwiBDH-VbYhO?BB=L&R0d$(DJp;t3ts3v0AWNx(j0v*aE?Kdnswuu*n%d$JE4 zP5_@eJQ7`x3r!uW{|4H=QBP~7(P2*maO}m~x;9@S;i=^%vho6U%W`MbEAw?j26qN0 z0^;nlkrO`&F@|R(A`LGmeq7YTsmr^a$B}Eiu~=gdDLyy2BVuIo-CEgP)dd;Y(CTiv&L26H zdMU(GC27U6Kj!rD7r^MaM~jDlLx=sG>e4xF@w!N+GNI`08Nhqsm=Ah2=i%}0du4IC z8JRNw+mCkUt>=WdtR3CCteA^!7IKYFUdJyB(d06vFXUNf#JmQqt7Dr1JW%VNw{@k* zP54aP4|6z{ zS9jE(K*kW~7^m$zY1gsZ)MmFyNHO(@G>LssL293t9O)Xn`XWd*%$|m;A9Ox)cXaQO z82Gy*Xj9XYD0* z%>vlX%pQ8cpEU~hpU)6~>E7-KHKvMArW6!nEUFv{C6#&{-t+H(hDO|RJDH(FHJSHl zD3hRb`e&1XC1GBRaEF;3>$v@~;)VV3O;Km}jwt$jkn8ne$84+8wU%S+umbLt11Dn- z(UP*EVI12i@{CKB4=6zEspuEc#kDY=4YoK>cWSu6=TG`Iv-)qdV-3$iZm0A?aije(VfySw@#qO_dhx zc6qLgnW=gwqunp$`*P1gpaz{}w%coqOJL!8YTpmDa=hTftdp6HRKPV`(fSzrl-gLL zG|oX%02FgP(7}s7Gee3Er4&F=uy5`U!swWsxZ&Wz#5HqzcmvCZ`Q94dIiZ`oPi~G^ z;5W2#w|l*l+V_ZOP>DJDdEf?$zediQ(IR**qvVG&^`%JKZnv22H#LFTm5SGI$I!ds z@Ih}MKQRy@|H&X^A&ed9@f8XrWCUpP)d3G#!l<=w!(3zVgoagaG@-rrAuB41kACM7 z!vBmP$$eji9KhciZk-IJ-dqtKpC+eURR%BI%5y8PE-2fXb2h=Dzt*tp*p8Lr(FJlZ zD#>VxdwNCMd;{&>GgsC;x%Dvz@h)+1d^eT%l=p7{FigHgvlObVsw*YommwBzm_(c(n|6@IaWo|zol0**Fkvezpwz9sph)jm|b}pJNH{N3gxPWFEbHkYO%er zN90`k2)nl@@^PCF&Yq7OQa7n1=rmJ3M6-T>_yFdDf502H*Rgl(+qe$ZN}A&bY-9Kg z^YaKA?AV(jd!LQ2*XJaM2Xqd$mDYP?c7{6FHPmd^>`~C;5Ov(km|`lrpD3zy*Sx-{ zeVwA?)v!(CBwdVl{OfDI{e{mqliOUQmE$QpsKn5|+n&bGH5Vq#x8Bl@+i!^Hm*3@@ zgAWHLKz%0ZP=W^V*%BwLa4~T;KrL9^7IF-1TsMBRIkuqXj)gYe*7D*L=h#J(g>sYa ziN%nk9IN~5!kSJNmSw5%lOF)B@AgMdupZf?&I>~r{==?4r4*rOf<^56?XJkS?}|0G z%&qHLT25QM$?VMEO>*zaDCTf^qR}yb7i|- zG2U5lrZu&#H2c_vK~sBka~r7;#-TN#AaC#Z^Az+=VnaymTc=!;>Nu!V&Fcw|v4=A@ zy@6GyK_=E%=O{L5HcjrrBYz#x$BD@V!~iH3HcDak9iLfp=bBx36cw{=Uxi>ciB^ z^#dU}Yb6pgO7rh_DBDM%8ecG6Glsbk70Mxip(TFUj~&J*sbk@u^u7&3XIqOCkwbNc zRt#)Y!Dd_G!*d0H$1f1EdV8F7)-k+Idl|E+@Ff@#QRvZ;2(1ruwn_&Rp-azH?RDa{ zzEkRQnQlz~tgF*kwPKep*ZzH6(fpPR%t{Qj~3w#k@7%*g+hopHK$!!i?QSJwKS24k?clh z_zSkKBrTsnB@HJTf}L%1iPFaN-PatS|IkIQ6lX>NPWQW+>K4F$2eMFO7%xs=mAo!1U_95vhOP4l6nxX3e*&$@K3nMu#Hm>=i(YI$ z*sBit@J6!=uzm@SmFY%5NjM(;I+Q`8OHEO14lF^+^~U~@J6L;;P-y=8HseKSyKAIs zL|;+x%K!(eglY%B=Hg0eX%4AyiU3Z6$H8hY#+a+;WCxphcqp*j8cSFh75Lp*kh)h0 z*Zz^Txz&(pOW9vM^;hgG9H)9@BzJ|-V8`)1@KF-xG`*neyVmZDhW0->6XI!|^JE+YLS&=<&W#<#I zG_%_O!=*1UI23}UABLkN!Z+O*pRRJ6&vy36d}hf(`6hDL6j9G?Dq?RVEw6sLm+%~nt6x@+28&QGpyUM$ciRa|s7zm7`% za&h#M{N;6aiNfu^)e6;YB5R%?-)_pa{dq}|zqg{b!XC1)2%~D}bkJI@`qkm3j;f9n zt7|w1VhUJNe5r+JXt9-8g*~?YyA9kwil^5Gg(XdrZSApaE@n4_Y}Sv{zR2 zqC;;h+N8w)HQ!aA8B9sd)-Zc6{KrtOZUhMQ;-Ovp4(j`8)}|qC1?x_)3rd-pzZZ89 zlUwf;Ga~mz|2{a;(fcP!D!2}B==^L5mS^Dtjg10X+{!GEZUy!e#$Fj?$3uK z99p>dCs79`@0rWHTQ*Vq8|zM3klY6Lb*9(7w{B@$IvR1aY4|{KQm1kBdV4Wse=Jd} z&f%QR_e8I$C4c!$EBq4VWruH2fK>RigjT%5PsdOwAM50 zYBBbO3@jBY)ri>xA6(n{pE-{Akhi6a%ZZ9>O#lG9V{{oHb4=+-A-ptk8(MxN#LG|@ zZKWXt*Bi`ULD~!?aQ3jVGr?R%&)e=NQBI09CL?}UU&kEc0{TK+4vUGC^NQp5b={a5 zvf`y_b{5;+EdQk3hn=;k2_8T!__iL;KX0CET`aJI0 zxhOrUh@SgG*9e`%PS|Dty>B|)5bcW{2Kpclz}zLeMe4Qvy}+_m*w+?|r&=i`=%-o83X8BaZ#x%e3plPy6lzvV)x`jT#NX&Ij#!mEJYxPCO+` z`oDpIsin@7y#yGmEv@jls;;E}JAJOH$XVy5QAdJ( z`dKVD%cU_Y>Ym$X|C>VbQLmXOpvIR*Q$!riyu03^mX{kM@xb|vRfO*1EVUWJ=VbTJ zpWV35Hmwmrl(wwlCyTYAzC)3`92 z0gA&Wv|W&D15i+=y}V>B{ANFL&gpZUG`Vw!5Dq$iZ}Ahsp1s?)Pn}3v zTV`>^kkgv?D**t+J2|eZNCdm@;SGLukfL$&rV+Rla?(CZ7W#oK4oY+Qd|=a_=YFak zSJYF;*5l^gnT_lP&Ze#bKjRf9aS;e6xTH?$-}91 zq(=9K+Bu|_Z^D0n^6!JakBjxMO%M_jX#6C}3=Mb4)}s|)b5vCwP;&PRbxv-M?F+Pe z1A$pv2c8Y|dF%>LikyOmS~i7^>TZPPc0#rHWJk77sKX%zE5$oT`=VZVRot6;dymWIir&y7G=Mo(*r6VJkgr>KWWK+lEh}7V zS@U)%OzUP~K_Ck6T!rIzsm|i%{Q9pEsK2FlbNEv3`e{N+jS%8i zySY+c+TlDJmFBKP!pH2Kzxl(JSYFlU961tCfUVu@%1d!zCEjm^1}XW5o#|<2tIcE3 zOJS7k8(X1Iu9(#uk2dBX(c9BjXHqiH5$y|S?pw^5wP_6r;9D_S4Oa`srU|_i>pi*5 zpdNoJ$iHf=5)O5gUmUoylivlZRU4Tfle3ziOONd}-^}`q?#0GWEU`_B%nt7qwfx&{g-Tg-#wp0#G;P>?x12e5POO^TiIA@#?zbORGq%cK_^q9 ziWKj>ysB3$dutL`<0Q>sfZu5G*j7dDM58Er9tSUT=f^+vJN=|pq=m@e?L3wN+C$H5 zaB{`4mcanOfV*@#K#f*XUGy&BE)VQz;40La*M>qCMMWCJ3xgMu#tvJsSsCo#7Gvvd zq!U;n-o9y{$GDkOG(O?TXNY&$E zt9JZt|DV=O|8G$EKl;Z1V@2lw%`@}_q-Q;BwB37fVnMs99@s$or9-^`g?iK%31$|i z376i&>qexT)Hb3w@(o>7IG$(w@?-A}V`Fiew4qf_F>RW#U&PUm?eb`(RL{!FgH_WBAGWX?8Hx~DZCc_qKama+;mLmRw42lc?Z z*KOH~CB^OVJrlw|CJnRTp31=PlqgFW)_eykIymG=7ARxOJ7AkBsSNABRJx_67Kd{vL^JXe~I6&1VsiaU@;^YixKin?A81Kw2QX!*s zyL&*?m@?4cFQEO!Qjz!0FTYr9#ky+MzseU)z+0Z8!os#~C+@EtQ*w7HGj63n4UzrH z@-HRRS#kaGzcd0ujiK3?56Pv3BCI8n_xCv(2exCGE++5&yAyZs)boB2UQRdFRRbPx zWtuZlVJn6zjf(2*L6aWQMw~4ZQ8%t{pE3RIj5GZ7?oXD7O^9^|>W?%IO{EZfMOh*H ziVBG<_wuc?Jh11#UcYFt>(7x6&{*4RbH6he-K|>$@_WSkVOK1q);ls^C?~dr7 z&KuW@Ri9ZjH0FLbY(WaDmVU-}fBjc22IuQ194qo}@qVWi1%knJ=VW}3&^3Y3!>yy>2K6(BS94Ayp|hr1!un#+X#$lP|N8PD-#~+e=e%3WfG6~;pgWViwP~G zifpz?x80{JZxFnB_K&u4?D7AUc_3Cy#qse+bu;re`q5KSr3=N%ppshTK^hsyZH#b} zu}7`mZ>_;aP!XJ-L)wydLP%)Y=N!UFbaz2W@)L6d>1`naNLW zfZXtAQuD=d5tK?vhD}`a58|b-ao`Frr0E`Hl=-JrYF99$sWr;_U?z<%d5Q5=Ox=xW zQiR&OXXiVLv+f7Tcz?jnx)y5l3B@PHM4XtDf7kl3g>5Ci23=5F*+{~{)7T*q#$WqN1YnIPuBys`rdUOA$m7Dl0b9vClY)@_kktq z0MH|jHO zV*kxa+&6IoPjVMFmePI}HNbYg*(89LJ+~^%QQgp4yg|jGkGtI(Z36RH4OWXr{QNtk z0ix(BgGjMB_d_E_5G4)K|E&0y@}njn!`pKA%h!4b`+8L9Pj0eIVJe_3KX_Vbvg*o~ zfs6&QsF|YR+zx!oR#T>PTe9|(IOU^_bCzL`HBwW*v<|G(A4r!72>`?maNwh#y3DJ} zj!Wf~=XEC5`4}cTbgT;7Rtee~NX@Iz6~htnVzX&>D{;L#{4pKUPerq+*?xrJx6C_E zaGurX9F@J_49__4OkIgaap$t#ytxb@T1*fe{&u`MWj?jvlmCJ!q0atE)hML2)+cYO z?8k$}8YJ<{vxB@pF$bBGHwiKbC~7mXCIU!HN5fI7{l1C6q1k+479|r@LIZXj(ZW!$ z8jUv1NPxpVZq%XrC#7p<+_`sZgTj(nFS`dKSoClEqa56KXI%6ewNlvdLLGbj7q0ha zv}$ViNsL4Eii;yY6+R*A|w-uk{KG#=gXMuD$2br7@Y5Nf&OevAARw4<)|kkZfNV0J0CWO1>-5 zxp66}7cF`>MDi?*v(Up4Z=2Dy( zzGWqPi=9Ifxe*eGPHA{9&52xf9>VvL6&>Ka`*E*Wj6ye{vX|vno3TSo|Fd;fCHJ?H zx$h4JP^&GFpu6tBuX|#$8Res5bZ5@CU&u46r`--^o*+HrJZM(nEqO`Y1O3R?mBH;J zoYgP8@85$HP;TvBK5xK86DfsZ+QC}!Z#DO|)$ONb)pj%5wQY4*19hM@$g_ek%v6Rkvh_M=Lx;GW#OS5Mc1!x7IG(Sqno8VKmfN>; zG=7Tt{Xe*S%cwY_Zd(*dkYEV}mq4)K4#5c;Jb3Vs-~WW zp%o3pLG8=*pR*S4{B(P~^^}F;rkaq-KD@cM^_x44j(rICTkf~GOzU6OnTL^m;|94$ zXHtK8fWI2s51!P6WnH~45K*bQw7cgj{&1$bAUEDpt>o2Q_4Rw|-AK!V&&3s~BH3K2 z5#(ww%qL+*E(ihgpuM}Vr`~nE=lLZIh6uLq4C4rTjEz^M9Z03b>MNXAe~Buz6ihb0 z73AOVU7Au?!kB&oo{IOF(y(%2y4g|LD_V`L_=2Hk@kw0nMZ|6XNcrH3MaYe7DJy`s ziT3{xBYJg(XC>U?_P9}lV0xvDUhPab z(K-^2rM__ZpiIInv>ddtGp8usyK+xhkfa>t#aRgKN+D!(WP+WDyR zhs}?;FYvsaRP3j4UH;Jl0O?CO-+sl#VE^R}cK%sPSo%QZ&GUitL2LE*gZ48>12&aq zvm{zEbwrF&9Qu~_wSSr34PbqLE}5rwcKs^|G?ZSDE!%_G7+FP~*zm;>1>0 z7lWGqUusimvlJEudZPuC%G)2mzMB|R)qhRn0IbE^!zn?l47!FE_i40 z`AhG* z52}qWFij%9!+?Mncws;hKMjYFvQqrrrP_vE!kn`=>Q_rp%0Bad+nD$vo){c-)WbmRbK%y=*dbnZ2nw zeEZomslKA!=I&bIM_WlwxP*n|)q6wA4s>KH2FK>K;a+2l$5ICPPT>6unhDmsP`T02i_(*u2{*o_Qv4s6@J26{!d z)m#o0_QcKHw}-vaTfwmcrX{Z|NMeV&SzpNw69~J)lo&P~Cm(4V^dkOil)o797x6hI z4SVZ_B1y-`qLTcVY#=Ol_tHz!G!|km=6_s82}zbJCDru|wN#MSOr$F5Qj(Jg37b5C zFhnn8d}D5A=K1Vs=M!vT>9Y(x+$g9y28isx`~hTp#JCpyUY3cPz|&TCAzZ-%lFcL) z@@pEjnRy8wtNU{?f1tp3a&gNa^u8l4GrLS}Ud7Ee4&?r-NarU?3Sc-n z$+39L7QGvm!xr9}N#6bD+|2uC1Ki^$Kmm#(d%*Xj_LN0W*?uQWwPd|j4=jF&qq3^Eu~oJtJ^Wu zb?%&qee$GcVfzzXghb`q!ZDJPVy#aOyrh83uK=KdQGT1h*VxpAu{RG0v)bmx)&K_ojDmMTcmot22y)S(InP ze}#17zi07DQISO~im{(y8-Q#uow7+Y`(;+_OTRsSE=vJlRwE!@M5-d8!+rxx@VUX# z&F0pvMex4$@o%rF;SF7vYOcn*z70{K-|B=urOb5hcif%1ayq9rc6N5=40(lo&*%|l z)8p;BF}N|w|$qW zeL1J8Gxf|B1u2T%a6;=Mw3j$(k_F){*1j|@^8i3m1wS*M3~fDwi|eWum+Ad?UVv@6 zBuyb>@c#OHP@fbMunKnD7MQ)%5Lif`hR7@o^B~&m1i%8@Et@Ii!}-Fg;tc%L0(QY@ z{~)GxN)-p-q|g()*Z=v%kz)Ok{(qBa>Wxy~zy9qSJ3yjy=M+#)t)K;-6>z!R^Z$1l zCb#`;3Ec1V^jdb;5l=d0nMJ2$_{NW8N3yTyQKh@GPS=9fOFb2l3h8(M4 z#+$LQ2+ox$dE{=FtgLrFvesAA&HwP`Kkljoqr`^1j7)rcJLNGtKcHbdtjU&XTWXW{ z?~j0#6kXW-~Y>E_E(AQBPzII29%APo{CVZ7HdA*GhmUaC>7@+C&^{$Hc3vP z0P@A!fDj*WObpJ>&Sn#o5t*R=2M+u{R}lLIbJ->Ui?*++*vRZvWVi~=zXw1%K{}Cs zt&b9{tEZ=6W0p56@@G5vW7~{_8MTs`*XIom5RoY9dM&tszF>q zfaDGX=t7%6Kw449_KADQSva(g1s8j#b+QLcpj%;SGwW*O@Ou+xRZfVRlRdXJb= zvEDU4PsyiOxLZ`LnAdM-3bTCq4nT;cQEu1G1;*seLRP1l(h088>4GV;dV|7Z9G#G& z`zLqc$~8@er_IJoHPYs8K_Z2qVtvW0nYxa7=#D2)8R-xtSzA)Vj2j}=iNqVw(GRG{ zY#L0>z|H&m81VsoZiv2ztyA9b{o2B+^k;VzG95A8OX?$Oslz%6+P-s)^TzfnHpi}l z^gX83q=yDGhKMJT7c2-{FDyvNVhAm<^kk+Ky|2cfG>l~y6^+8HvDK`_8Cp3~KIPAk z+7{j5ssrHVQ}I4o^I|I9VQyWy-xN>oLYdGv7J_&GH@N_Ly?==GCpX{S zw~fi13(>vkdKhKpqJO+tP0Zn^FG7!5!OGzdEqS*l36Wlw#6H1LP#vaHu9ePVCgA3) zNS4p1YOjZuRHmpsvKAcC!ESrTQ+_8O+&vQcNEei+6?tkPDoQ-H=b;i~KTJnO+ITg% zQ1>)3Q#@KKev!DEfOjms$vX6nxudF@dC(W1bNl4Woh9I5$<6t>%@QjC^~Nae`9+{( z%g7W7Zh8cIGk?<28;kSmntxU<78d2FR;uo*8OP^;J>o~5+ zPFI*Cf(}!ocO!}qrcL5piA@;`$=Fk@C3B3@?8dfIBJBP2YCp8}7J#X2tFXu_((jW9 z1{#!wds)$uCKHU$3-h`T`VLecu#c8q4KL(MPFL-z+Bvz*{W;`2jp+gT>LzL=2>RZs z(ojC5{6RtV(+GXUtXs5;X{J5(NIsWx0+^~;g%w6_UIrb5y(FvaTcEes?Q9$N zZhx-juHlCj4Y#o!H4dHj83*dHmn1ENmxnj?{|eW z^2x@U{;NKMhS8$1X)jvh3FR*AG+|~lg7ywUwYV0VF!>lIe1=0(_Zi-m*E71j+$lv; zhci5>Di(5s~ci9sOQ` zaS;?gW%#OUYHs8mT}M~huezPT3Gzps%$Lyp2x%^lz`?^n#XUnjoe*5R3img_OoP>Q$b0Vp-TUSoDbUT+~l%4u`)g3`Fvdddi+Ao5RO8x9g!QHS&8(QOOKtn&J2FI$7xS!2toH&@-R+ z&;<>NO7y94l!7IW5%P;-{9L0<)e9WA)Tz3ph?!Ncu1nEw%X>ZjVV%F>GuPkKr#p!l z-n@BJeM&UlX7Mfju}Z%#B@SWpTl?Jso63k|?{E6&Q)$bNs9!Hf-+xT``Rl!+rHozk zfDyDta-(4=Xkt@DT2o7#{(EdVW0U{*ql=LM?&|6rDrjT7Cm@Fil1dcaj6AcQb-Y|C(%hm2)BjV?Hax=bRp?0Ifkb#_~R~?|960WbJ zw=p+1k{@tC9kF|{G=NgnaBk49>{%K#GwwHYavyYzGB>(VRk!aV=#;)I#F;x?)4P5) z(w_Ms+h-}nZORR@8*6cCOJX|1J$GP{pI@fUofpsX z7*}%7Bkw=G@oX)=Hxo=FOXz2DC!y4s@yasinZkO#uR?KQYA~%Mn-BT@>6ZQPb2anC5dEH+s)*Va;t7Jfd4{=bDt)NBC!PT*(N8$%3f`M_` zGV-E;a9|RgR_8Q#(JT`Pi<|efu>V3txn+AC0Sp@5kbc7@ybaZys)~J~(=>T_14JP~sEw}}p zBhYq5m1XA>GwP zx>zG5B2-bp>w1^{VMblusyQ$1vAG0eFp_uX$B%l>^|dUm`pog#1J*Y23iQbSawv(z zghdtBp@xPEVPU1ErShq3WWev*U%rbEhK_=K?ORnwup%F#bk z!mmS-KCmz-%*81uS(h*EM$p;qWQ5ONW33(1ad?5BK#kj6rjsumsN5~I^LYE$HSdw`M4MneOvlhWg+SB7Ni{(D;lBqEoAp8{3%JS;F zo**q9j9&*WR*vm2md?9XL&h=$Q*pC7b}I$!m*2X=NSZ?|>Gv0mN_6szmGv(@BYk#e zyXHnp$wP2|a|Or*gVA_wlWYk4m8BArGbxh?ZJ(->Tu(VT&)TDRPB2`dyshvs)>BzwMl#g94N5`Xu$gmHdWB532m%f4( z-nb5G&!Txamh3z=59DOAj4fHL8o7gDwy$KQWJ(OZa&%#Y1gOxT`QhNE;WY80hBy${;{%Jn{1Q`E(%#?sO}Rx0kYr9-e_ z3%MN+Zd!-c0slv*SX-4GejP8|Z?zo`?{(JYiS<^8Z>?w*wgg>2j>9o|HgI_(DnpHJ z?EbE3a=M1y)~`62?$_XHS7eJ{2*9lSn-AN!1KnW9$o%iM)~1)7Q?4)WDMrZy+q@DA z`fP?`N&GUJ@Ftcc419eXt-TA#k}dlOo8k=ZY!34DyH=KPQX6?rgX`@8>gC!`8OVHi9^%7ao4bLu1!r-w1}<-!Nl46%0|U< zAH8@^)IjvuI4FK9QPN<0o~Ed(;|46IJT$L9#oSlV>g(4A^hXE>Q#nhR20=6oze4pB z>1^+{Y8(}GY`@|OrKKD@zLO9>G#-Z$?$sBg`QyeUCd!q5AN2X{eiMAKKpKF9?itik z?A!3%ClThY4t95+cweByNT`=>y~gyeBES=meqR^j*hDpr-!z{zUr)(eDyt|w=U7ap zy!%3k|92G`uN$jcV_bpOM1@$%Y38Ma7X3))yh|;iYP!$@eA}$ynmME8Fp$fTZ!}5k zUE@5b?H4PuaK3i8Xe=v%3++6<-Di5YDwB2+1$5HWevjamNZtq3WlY=K^L-mAHy%6u zgNwfX-&RHTeAevlO5<_v&(;MjW%8F6QD3KVvT1}4@LT-R+>3ebUHB~#OCZ4%= zGRT=)ZH0#7t^PC?Z>)}$E2SK+cTR?h`SP*m=(RIb$>q44?Y;%L3TkEpfy>SdqKk!= zjc*e3v(Y%sTds}@R*4tx#O@2=gYL(QePw<&V9UO3;ENggSZph{5{3zwhpgCtG}ZsZ zFSOm7n&!JQS1D!uBIHe_TgOnP9ED=XTVC+uViOMoX<0zl_Yw>zoMbIf-KZ$U7O#>+ zkmPNXX$q94#{xcy1kaL(9d%6uGc)twZ*PI^48Z#Abq{)lg|Oea&|B5VgOvBvFACWO zEANilvt>Z95hzL6;IHS#dvdHyXYp{p z8PZ}BO@N+t%F~$}>ET>{oS;)+aw1xsG0RRb2X+RWKVWYqkYQrfYxLV+7yChL zo2TZ0HB<^sPS&-xE$903m*w$~?i?=u!=GOYsiQGZohfsi7U`3MyEhf$ker91<27(z zqW@Y0!;%Mps!9#i?ZEXc$VeKJ;unNlTYcf7mH(>wxb9z~EMX&GXRt6aS36ZPB7Gv; z-Dz(sTDU$vC4BUb@-7OWPw)e-sey}&mWvsm$~R;sWJI2hj^?bU4!JnyzZ>~yU}|b< zHtYk}3RYcP&1GzI$x}JpSNFWCkgO;wFV|uQ7%HeR4cO;LM#B4dz^?WA#CA5;dWb?S zp1CSK_36A}{s7e&H#hdSkQQza$R!ur2iTFdw&+6goTj{oWT|9`{-@t8>Y zcy!YJpLIcz=z#=(lZ1qXDLqmv;>tBQF>DXJ!&utAag7AQN4OTBTE)_=aII;$&)_;FkBMY8lK|{?OLA&go_8 za@c5~Uj?;6+NGAv*45G?4C$u%gp}FYF7q&@L>EE?U@*St7+A;e-ZOYi;5W!I{0>Rd zIpJ(fo1T=uaGL=+cLQkerrOpE{%pU&^??PH&-g;#AycfpE0>lt1?Y~Qh5df_;JWyK zG9rGJ;up!)sjmlpBsE5rTJr{WUphujRF1yr)#oK=Gn@YI)j;iI)TMzYkIS|Y_dYB4 zI+B8JaM1OFN(P_Y8);abFVkUrquG(BIix?PddWLU6z&%YAOwPzC_YghO+Vkewj75u z^YBwy=p605m`F8vYe%LZlkqNQ1{Lq_@c9xmji?-eGY|)isr*M6CTk=7%yCo&;>^_V zB3*@kOY&5>kvjCVxTy`ZoF{a9fAtkG{^*xm! z@*GhQDzBF$tm=W1VMn0`HV8DYnF!(8KuDW(O!42;^G%Q+PC(#cgI4SM-Vs z6xb;@cN83Pt)3n7P6V3TvrI%l;d;`3NBpLXCGgV9)KE{XJK4UXp>bAr)m9iB^J4Ns zoj57Zc%m%YH=rK%B2f!uz3(D!D`05Y02c%(-SKs}zr1yyiqDOBtedu~p-8?W3v3AKFo7 zULE1^>#6MjKpUPN`B2MM?`8YBXDD>Pyvvo7Br7G=HLb6;SgjtfmikYb-t-0?J)0M- z3W+ghwFnn-V$z)Lo@8E5e)tZY3s#V`WH7l&5-8&1J7Jq#j@EwAofA(wU>UDBCX=p> zma8-n&;xksS7uI}bROg_!W`uEncGo#bt^8BT+^G!RQZB-iKU^P8L^$nDbT>Im`oOVk{Yz~W`}>a!Z(b^@tPjZ2qGag_yJ`~+P- zBeSVp^5En=H-2ye9>rRUla;VMwIBgvk1tDU&QPjS`1j|}FK4%BZ_#YdjwW|G7!-?P z{N}k)eu25^ThRC2;-qMx8a)J)4O}477DV!<@IKQq5IsJi{x};Pb)jf-tFBJT8N%%q zWhGLxy8k{V`Gs@2uGC4+YPhn}sJJMdL=6 z!9gZOLiT0*lN=uAQG78&Yw^1>vkDD5d-+eoDj3aL>gKld5!$YNA0>D~wqT!Uvx}At zUZF##sPHQY7~{B8HQVQ$3b#q0d6juByHy-d@8^qgl2t&F+6mL%ig3=Y6^S55>h&OhF>lVoTin5WkxKqlCy7p}wdzZilmk&F3fFy_xjHe{EnJHHwoh5Whz zZ#XvO-E$NGu>HXyT>?!1VWoFDvS(<`*kUCUTgzmjqh{%vY5JDj;##%s#R4ufstwy2 zdBMHLpTphBlzbAn`e(}K)2ph8ZLcK6nw zQX}hqcv*&#J@uLxp|Knh-=dKDx5Hq_@5Ks&7|wjXzrlQ4CF0Z2(J<*!ou|K*Z{V!> z5cE?r?imqA06s7MByu-B=3tOSOtUBmBFGaf&)vApII?57q%xM~w4KHS5_Ia=aU=DK zB^&5OLc>+gy3J^*(`)z0EaB8`o$^s;t;8m4JJM6U%y`aBZ(YWS+XOl)iPGWC+u8Ko zsf24r#U*qfb8PHWzS0xnxn|#uDvZ#TcL*u%cGXv}K0wyQF2w-m8(D;jJ17JMT5OWgjFN4=ee?O@o@P@`gWa zY?3AuL-NsAWQIODJT>i!_Zg}!@Q;|P4re}YEoUuyZvK+v79e*}Lj&ik^LX%$52&S9 z<9huwFilX(=Ar$$sn(HOGlI0h zAXq~D>T@0H-{bq-j+aY5ow1CSdMu#l0(Q1GuV2m-nWbvNFGlvR4zmee-1HA-iggDQ zVvj(Fb^2bbjkMp~@~8_Lpnvx%sMZcA(^YlBd-MZ3-XwP~OC-E(MXNRZ2rp#4IskT~ zxVurn&prgbtOs*=P=v9fAff%%&GH+tj>U%}yc`a`MVFB&*NfWkiM&mK(B z>KoAfFnIKX-_*{ZEy?l>TjtF`8S7dZ74_NG#uB-*c!(cyzj;{LRiJ@GBvmvkw`Q&a zG_J2@CNkbUtWi5V_RwE$wh<@0uwi0;%hM*XiTe9*`^=BE+O`?3G6sb!#Ob%swRPr0 z!>fHSB^9M#hX+!GKhSEoJ;dvnap|{Cf-hM|{gSOzjmAemmXuib%&;bNDWFz}e+s~S z4_;)UguoB0#88fw;-0maEJC8MPOM&)x=uqyvY!Q(94Cn@`>MC;+##WsWTw zTUHgsWPl|U#q9a%>Xx#ioIXTCIX<#!4-Hfo`Cxf-K!`ww#c9!7c`KqRXDk=5H4Hrx z-Imv69TvyW)s|brgspd zl$ErfsaTLaXg(p7@32w4veG5Ut8RG}I(^2VbA$baWcQFpBY$;HZ{W6^(baXKf%Dcq z#LN1uw5~9-rtVwE-`1R8qTuegOnWF9A*gY$g%(hMzIn2CpI6aYPz3JoJ#!p#baxpD zwl3L9YVj($%EQ+isGB>+b}%t)|AN@2JYp=?+qKn8%`YGR0X=YSt0L!HPEVVy*PEzz zLKu$hLB1VhjT&%Cx)_zeuN@g5q-2Xh{Xvx8B39ugZmQ+4UHUDr6Y8{eA62rN>k{Y+ zkh&SMw8*XeIjK{KU+fz{_OeI5X4Udr9j7V|9#t3!EW5T6EyZ;2(fl#u%O+G|$iz?l zFhyB@4od z>7Fpr-#qKZQU?-pyP}&xFVzV#3>q~?wYPCeS}ahkJmB~dd|u#n?FYKs?^C(ar8^|} z9)$~y&NMuJUfV6=1fN~^qUkKTGy4SL35QH48r>e9BkMrpM6P>RNXIKrllGUE_&+*# z)`ql}yCj^rfo37#bvchD$XEdEP=jbr$6F_;c|N7WR|ogXjq1`ktOjf!+%imUiGuTK zZQ@Bwlv(-5juCK?sv!aIOlgYjjc7VR;;~zxOwY)+CpfH?Ug#T5Q=6Yu<1ACM8>VZM z&`J&+g#L+C9bzun9K>=I)fc8+xymPI%OrFL;}^2Q4h{Ef?wJ0plzcF>#P$2s_YI-y zq2t>Vp1f*^A@Q`Tq5f%3FzA^GT@56+WTnW}*x5QBe=%298u?*Nq<2s{K`l-7P)pKZ`)Y56s4gv@0c6vhOPccmq-XB_K46=3Oyy8h57VILZ*{vywfdQgeKYpZU{ z%WW=lB@t7o1ZQG|_c@o5V>i(6@rc^bmGOD(i6Kb9!BewgbL$ex{ZQcyQTu$au%UCx z8*PHo(Mt+pzQV$%1#o{xxEJ4SnJDeJ#y-rJ9Zx{y+HXa|G+90)4JwLpZn}=Wp?tMC z6A9H>6Kw6!I3kueYl2qggHnKNGWPc)B_|XO|lUv3!N( ziO$#QQ}(ybJ*S*YOTV=;hV4gj>RJ0}(49aczMiK+9$d_$g3k2FZgQv`0F46qC7oJ7 zCc=VR<>Qz9FB2UcfBg7UOMfQomCR?~bek{Q?Y8IVYB!~OH-}d3oofO0c6)dGYy@Ud zWLQBwcqSy)!&iO(tJt=G2!heBC-*n~Xx~H=yuv6#`wcEyA}m%{#8p95ywa%2_w%@@ zg28$1-CDAGX`!=iRW+PGB`&T}$WUt_af>&4HtIVWB zTZ5+3$D+a!T97{k?j6xbnz;-yCfO`KB+oYT`7UG-u*#sd!Ryz9sk-Cpx0PpmEnxgE zEI(g?b-9Ue@m$hYZB(5m!OpPM@U81Tb)#`}rrTZE2?d4P?4CvbFT4cNwZ${$Usnt9 zUzt*>wCqP0J~Zq|Z{)lj?*d$+eafev4=j~oJ0kgGkf!>8r|yPOXP=jrlMSLJKbps1 z5YhjErc%8Rxnik0tAJ|W%4{aMy=@Jqa4`rD8hczsm-CKqVaXbZnF*(3KK;w1>OAqR z0{D#)#tz-`4K|Meef7plz_Q?U44c=|E3R_0-~1IZyF^+gA*Jexvn_}CS#@J=Qp~Ni}riz<+#z&3m(i5+wC5%60mIdQR zoFh|z2h!UEgT1uks zRJDr4Ibn%Hi4EYc02+r)#2bJc`kxJd|DyjQdX;|qpL76f=Klii)B^sKG5qt{lTVKr zo`mLqVR$(x|EW2APWUg^7|Flr6VXo++1x=(Ub0Pbyh*TuP$9gsk0vMe9Z9%(?Vu@l ziV~W~ZhasJJlJ=l9u+#V5TpppOj-|K*T9kYGb4#H|M`jyzmH_3Tbj;cu`-n0xYbUx zb@M^*;jj(P7`Va=+}(dw3W>_y+$mhinAjB7T+w z8X%6B*DulupDY}ZvTp=`Eob8<|JAV`rpy)LGxOUqlk1fHbxpBEhI=B-DLeFK;YC^L zaa80vluXa&DR7sGFHhD2=unfG`dC?LdONwSN4U&GM(v`$ldx)7el_q``dUUZ(OnUH zDy)6~>z9qKZGAoOl^wSB(EyA)D+Jrh z$_?WQHR|9%G;Mi=a!#X+Bk07ArsY*Jxe&<`qjiz`<^nXvu26#3^1jP3IaxlWSWs&CSzviEx zV@d^E?Fmc`M#QBqA%5Gba4X;DGuehV$qyRF3cqpF00x3&D2H^c!~HQOJa8>(IHJvp zyGrqAP>@N%K*!#iqFu>I=ZY1YEoZW$F+YPUkpN$FU_n~q=GZf!XVx!_PDDQi0neMM z%CS7zzOWa8#@^n4s1-m$v4gAv`p(gN6?}c0@~@wfgzOBY69Qc%h+p{MhKBQ+pHmOV zTIaD(uN~W4MTSzoWXlxR08P!rk1h0ZDTWK!5ATd^jtv!2quwlRFGX~W&bSaQObwiaGKeh5JkNXxJ4xH8e{1GVv3(F&WIuHT6v zwbAhNmt`yzjDYx((9C4qR0j9$0r;Te;A* zFcIH*`UU$76_MKDTk{w3oj_xZSIAA{IIH`{>F>Z#xfXg>>2g1k|CK!Y$#fnPY)Of) ztBsP%o%s@IDdCRM=`&_Y0bB2}r0|w9!D)%kM8#b6?)?3dkX9W1Gl|%vLKf0G4bm0c z{qLsY|Hc9WIY}Y{+h|Lkb)p55D0!L0$@!ez{nh+tt%@CRMZfOaJpq<^snMGGd{*vX z|5kbH!Pg0s%dz_q@Uh1mi@3)7ao3IgALDq5h7G@vsoi=^@L)rW=NbBZ^pod08`Xbw z8nk|i#nyk`Qa|;bNZd66ZD9^)=*5sSk}M0(>xHP8xiL;g%+tgu8=^Lv7L#5l+I!IH z-)YD!;uC0>@QnH`Xo(#duKx#MQdmB&7$#9D&nA?zy}nGSQ)@I;%JA68l_Era9^F z3_Ct4$t!*{5^KJi-gaS#B)Rt&C<8`&AETZ(O@X%~#?Oa)UX7=c3 z(bH`>f{;?jK(DGrb2Ncg($1EH!nJ>%KAy>`)~Uk@V)1a}&|j9AQhV1r-Jf5SscT(P zz5wzuPELaNKt`3E46eK9R{CX@NBVU{IJadNvrFB^It+3CiZEZjn%^>lPgmaOesq?q zS_wTEJrtN?wRdXLBb(W%vuEtMv^*3DE@A?*BIdt-7234r5cOTysq6*a)`?!#U)#?t z;66J?&>HH+ZYTM9+AYv-DDSVdAHQuXT;D^C)$K|7T?AfZT8rl77i`@SldW*@3pqeb z9U#AD=FI)B9%ew_^qf-zj3t46LkagH(CiQ-I zQj@EyHq+3tbYAA{MMwCCxYq33-~#206he)QS*=u(n6zxUz%u<%O3z*$PaoLJtNZgu z?~ZLhORkE38Nn&MZ1*eMOoznJ56D;#WBD~6AjHFtF}1@yX>aKxRF=6dQwYic?6qie193amxAK(!#=MEOjN0=N4I+a6vxP0^|GgS3%t55a5eNX)2hg{0^ z7~hzOUV-sO*bctm+f_@^Rah)!`7Mpom3RtFm8n|XkJaSQbBjsgxy-g{MTM)penqII69R-ZvL~sqdTU4P}0nP0BNb)Ngb6 zEO@;2xu2^ipB0}~XqG75WnIjf^J$c0AtEklMd2 zs2{xi*QcKY99s)QG+sfdARkUc07JueVeB)TvwuX%X6$ouvBfZXn^vCFZ-W>)a=Qn)*w;I~#OWWsh;)Vl!k20?a^kkBUCOI{4mq+6->W{* zoy{<5s=_48V^Pt>4nRiVXnKESMOh_=F&7bxMa(g8bgo==NhBH(PAVjgG0jE_@p+rO z^4(Z_ZrMfATA`0~GA_lm&ahnU3&9Ez_kb6A*V&|<^KP)`jkpz4=-kq;^u)0am!|hB z#`Uf>nn^(ydJEtx||xtMeOEsPW>*GQaB}#W0ZPT5gVeE<`s~Kce933 zW-AHHXImW&>$zTIgqypKGnnU`lRVZUI|xjWhu!W955BK^-qw%l&k8Xue6A0W$h$jx zQ658L2VdfjR0)ih6gR8ry{mSP{bxEcP?4MLn+?vGK^C<5Hi4gfC{2SaQ!D(OHmgdF?@vbvp}eWq2L}3D zkQgB$kL4a3`GSOD!W|hrOW`ScW<&vnQTrkfFAVz%mKWDxTIyK&p{wamp}mUYm?FEi zwO*xgw|sTp0@wR3ldZ|NsGEmx$gYy%MvT(*5ApVgb>h`zy?U<~-8-U5(aTc{)cDgK zH%DBIVsJeeZoP0ohIRQ-It004`ML{~D*ln+Y5))a;r2%#f%Xn<@!vIQMLT#cxoevl zrz02<$r9Du<1>4Ax7whv*Z1r1_k4#gxroIp+?DH(f8x-CD@2z(C1Kn;Hk;x7fV=1a zQJ5nAdQMI|rUiyfe($FsHE%o3ZG@PVn2`6a{ zS6@JeR+|@(J330(oZ(8U539OZZ5Q4779~?Vm4k%sxd#rSKND(1QP(>PYZblJ zJ<|CeJGF7&pVQ=!a6QkD<7f5m@NMe(kjtcwUNMmCj3n{#(T1xe)(ZGW^EVY}gbauC z)J{+L`zdZIfiH%&v=PZRT8ZTSrgVM0Mn$_O_b#r2>O`+ty{I2T%Qa>;n12;k+FNy% zHi|Ve`vJO)A!e%4^xcL$fXU!=(dgKhEITFTYas{3L$>2!e$Q_4?2FXu#u(^ zhVTqAH`otxU%D2VW0kBzg9ziF;O#J@nj`TP(AD4HOd25_Ff~L<&SoZr_7 z%?{!xvN_d2d=JP#F-mPKQ`R_?+)aA`-i$?%Qy8b%nPrSYXw&g^-R@FKZO(k_ZOLOW zffUsBJHa(X0*O7*W^*8VuJ0Jn$n<$|Q_vHx>-y!Ub^Lat)FV5F6@9kM_HeX~-#v5= zwN0y*IPR#g*DsTjL4=c|uGjWG5tV>tk(g0bpwQFMn6?2)JMrXTIzt?b?^6*I&syvU z4R-@!Eb56{s`6#b&?6X29o)LGmRSEC9$h`-yAp`K_m*|o{$ZhO=3|Yz{|SKGjSEt` z=2jDfH*k@fv2qfNAjlbq<*DV&OI7)rFwE9bB64CzH1Srp9z06H12Mjfp|>9cW6y`) z{d3Af7$&-4ZJ2ek_1~%5%AXsQAe0U)J~>C-!YjVjIhVy=oSXuMGX5YOOqzH58iC0 zJ|-k#kRvx`E=XQ%hARa+*S4d5*|4qLJ`q@*+|J>X))(MgjelxItFxR#CT+gl{m&FBIp?FKF1(uolEqu^&7x4IATYv&{KT{H? zw`imG=Wk6vzp9JK^Z^9u8?Tn$%ps)gz6m(Co-c!h4^)4?St~>2F6%x{obwChCgVlD z)bzk?os#AEx=ze?L~Qe^27x;k0b2$9#!#aWqr}|9!isIhzXc3#6At?plGyOf`Nyy9 zffnhiac-OZg%M40$*WZRpQ%3;n^G!t(Pi=)>l=oY30KTZ8($e6H&rPv=cO5g7~MX+ z^}HT71#bIOy&x}d9VGyVlshlKK2aYX*g65`=W!44L{d7l~blumO8wx7vM8?LJO(;NvXc_w@D}fqVgD ziu)ajLelujr)-2m`N$W^1m!?3{Gmd{UaXMvd>Ol>CXYdVJbwLB0Hl!Bc#=+Jz}~FI zjTqvl@eD0JEjyn=|MSv@gnR50Z)n%ZR-It!!`fSflfy+3a1V)3Pu70_S4971{a-}y z174l_8p+k$gRM!UH92nw66|Yzme~eZ(oqJuFm+sBPc|PPAo)m*(b)x4MfzXOeRoh4-?!(efB{J&83aT? zvVe%hK}jN6B$l;* zuAX~OPv6^p`+Uy%oX;Iays_X*G#mNxis>w8Jl{WX$)NT84{xHXviq9ilI26R<8WZ`4_irv=x0QvEA4N>+qq@K zO9bzE#UMxLt)Ds@oT_N`Vfk*rLroj4&A|8jy;=28ZD}*&$>c&!p3KF)-;nMPV$47b zzo(r~fr*p8BVj6y*=jJOC z^y`l=X#uJXwlZ2mDyB3DMEhWX8wZB6cs_`gaQJ&tI+(&gz31kuJlIq21mGOV@6C#n z>Mhy_0xgU$Q+h{7<7>t#vClGygpHe9rjwjP-93ZYFL0m>Q2nvzr6xGQiYS1H{mtI< zQs3vRx}*H*6~m{x5-m7^AQBtW>Q*Yq@k^m9DY7Sg*3ahJ|Y&VwtdHZiq zMl;bU#_Xyp89a(thYpmX&Kai`^W)#YpVVmmz3#2IcroLY{Oi^7l(@p2WzT9K4)m+g z^K^6|*a&ln7_T>72FHLJIFN*43{|_;^KKo(n1QGKZ)|Mjot?40?zH?F2bw0(U@uK5oW$lYJ?mMz3F{NG{$$jlX& zNl`Gr;vsf9^Sc`0-Hi6B2PpaisaGvc83{K5;x8z@nFLx7NNLdhGdq+nYYo`o4CucQ z%q1OJr zu()VyYHEJRO;{iPBBVS!F5vGN6eA!nwU1eB<`+zRelhFl>L$Tp%ToR&B!2Z0Ide-( zW{F-fU0bYO&{Mun*>QgbR*d{AE5^-4 zsiLA^hY#8F3GfHU7;5#QJ5@VpxP z+JQo!jX69=w!Fq8ynW*(j@@tpPVSK0%-?Ce^Cau@&~j{P(0$qmz;9Utlr%K*&3~m< zNeZ$%$Z2TWw#$RtZ`_laeZ>ny1@Mkw4NAQL#)vO54~H-USOOIkz6{AxVef}{fL1Bt z?~CUr`AwkZ;BTcr@gFl=Rg@;lL%@CvV#!4q3mHIC&vMo9jAA5IrWx#0P+FnHA25uz~SU?sH;9a9`4kKzwNyuWS+RnJ;%VRL1s7p zZbSQteJfZ@WWP$uob%6U0~vmETwF9U({+iU2HgcJ-jqUmgKsVX9yfCD#sNQgd1qmI z*@iqj)prxv-AhUlcCe^E*LMc#!>HK=%^)ncB0EnFVo}cX?0m(r}Cd=vOd4Fc#w8Qmc z!AHV8Afo*fdQq6*(O~Oblb%7^H({^YpT?PxZ606f8`#Y@Lm-%`=KV+9LJrHa7^*;Nv=EgR8QEb|%djj&4bY zcv1-Y?r1iTl$B{{`1=nu(N2bkD@`9)53Qo8&Jq6mOcp(f&knjnq*HaliiQu#$@Sl) zo(V?wdP5v56*}m)O*Z-4zd}6_nV$F*v|js-_-6-4tbar-z=_}v@0G@DkK#H{snIt6 z1sqU%8W2W+W$gwWW)f4lH>nv^?$pxz@NdsbI(R<@RTmya@uJk}>DV;K6h`UDEJxar zqc$5nAs6(G7i&!dTtipOMUP8&&WCacIKAjrCOG01luEYFJ9L@P z^kMk2`4Q)wrVbwrRs8+?OKC^Z&|>dFBIkXKnAQvQtgd8(eBD^DcBsZDluxe`^&C7d~|C_*y!_Pm0^0u(gWhuuF}8ar7sw zI+cgZ;ULAhy=NCq4_aL9sZg6@n?3vr3hJOf)IM9MGBYktW!Yu>O#9>zQ!=Y^{01 zX%~@V@Abe^Nb`8P`LX4V=s?fTAEj0KDfxV8myyF|ZK8Iv#<2$;mFxS}MJYUMAHp8J z^ei;l*xT}Slokn5>U*0f?o-FR*;-|}{Tl@cBQ-TkKTj=sc|Bik@gtlsoEWhyDRt@D zC{(e*o_82|u>K@Ds@l;e_2OEl5uf=P!$oXcd~_to>e5`I%6aasE)i7Pv&Rmv+!hY^ z_`r*7_Z#`IW!>)?*B`9IXo%D+pvY?QUBOhJ#e@}l(GRTrr*Ap-?bg`yr&01|p(d8oj1);WU-%KK}WczWog|FeY7a&}A z9HEC_^G^AgAEJ|a&e_G0bY-#EXxjI^>>X#waaE}0R4Nj}ut^>zy5CXBf7mR$r1T(O zEpx)2l*yr9J(S~h^5m|QdBaAxc!eFW@|=8qTyQ_-OP}sPp&|5jYI=3vhgH)JUPLDc z@8702Si`O7jOx7)1e#ebU42(ufZat@^4zS?JUOnKeXHciAr`z69=DjK3jH|a! zBaaVq%T`2B-yChVGkMY6f=7zCtl=)7Ba5$^sqo3$yL@+B>Cp?RRhI8^Etn0(jz)da z4l2IP-p&){q`69Dxe;lX3%}YDNt+VQuOx$JRDE0X-OU{&>5ZfUl$Jl9v=J%F<%#V! z&ldWeu!fV1#=0&Mku?PxsaFo`+23_Ku3Kr;zg?z*@-;IUb#w@)X=XL<>QFvvcAWa| zN;EAbP0K+7JcknW^H&yc71SC7S-7AsBMrQ$uB)Bdgqv0|VqVLUmI~d`K~KcC{lDPo z>Ss3}b+?f#C{T=8ioY{0{pm{|XISyI+u++nk48wV+I2ANope|eCp=$Kt}j%?cgwcE z;2Ke@El02luB^c~>qyJDUgXEn@T9R#oaYwE#auyVB+FTBa+|&|qBX1{Xwck=k_))c zB!pwz+QX1dVlv*lh*_Qv%k8|I7duQ$qUF}6dboKeL%@H1^RyWIIYpp zs+nK==xDa@;$c)x;)JObI#(Rp*_+yY@X^8zW99f%LZG;QKCEhv7_Tm)=;zknFqNz9 zRHS~I7!!Z%k*m3`8t!Hy4=hKy_bUG7x%R2Q;Z<>6vw`DwaxQBg;kT-s0?R9xivY{c z(qnKr#s?i`KDSZ*^F~{Xl_BBg{|O>!f>`QPW>v0e#788b5>Ak)N4 zdH*J`6)zScfd0>VA^#y6ZsCu4`<51)k_M(hn+Aqc7X+GyJ`};8pp*hQNZ5n_BmQ)N zHYn7nS1Trzx3)({^9#a=x|33ueaqZ9mpw9lU2hf#D04>cuH%GXE)fDHE>+)aZKHW2;h)zA=Js zk;vv4YQ76-eOVIM)cV`bnVI!B8k?$YnSTYsdmL_kD#~eaAJ0idpCow@=;KHDvt`}) zv>3W~@tepg{^Grh@)Zs9W)5qf>}VFx-1++que}`oTyjh6NOpHuz0*!HR%a!*YqEi~ zWW;Wi-ti_f{qWrj{YFMVx3X2SYWC%wjq0~j#R#3!y|536GtnT8k#m8Nqsu{-#U>Pi9sbE!wOSmRjwV&^^H<2K zOVd=vR(aWD_Pz=&)3Kh-4F{KL;=9G3i?a$EWju9Jw)Kj85f7b|ZMnw7(Ji-}BF{cA zVl8YWbkD0|OA(|5L1=Dxlz2KG*R5C9v!W)`J0@MdUXXF?6bEf|u*qHv9V1@vo@|B~ zJoSWoj}A>$o{prkb8DtP4E5$N=QV1cv-GVvcy(-^U?r@5a1vpY8DcS4?`+Yw$&=<| z=weu?ZC~Dsc^K-%o*wb?!PaD>?7Q5EpWe=u4H;|Td0w?gR2+HSf4{47^!>CbD{KWt>0dzqKY5dqy?xW3mQ-RQ}}7RMa$!D2aeys z-A+R6Xh@B2-7MB-UEIz5QKl(~G%6HVfvxBdNBf30Vt&0F5gB!u9jN-zGyw$2pn#l2 zU{7bV!sbA=pjD(`19VuJ+cL}h;3RM{hMh<>PaV&}ccZ2D$FB4Kh-sWpR~mFZEm8*; z4_EUG+xXDxF5Hx2(DJg6 z_t0f4q25+`|4dlIvcVsevskk_#U1mp-rF*b4PX+uf2ygKnT8h@*WZ+{(6kVK^Ft?AWC z9nSFXnHX}19R(kB%<3TmLyp9hUxF5v?p*5@6ERzqnrc%SEz%S2x_Jw#p zUsUYMtrB{Sa=u6||DdaG*HT;@$oRgOVqj8yLMD;S(i^DP#=_f%wj>^HeW^TJ;Y z-*MyLVgaxSgS%>4*M9XMGG}*cBo&KOpGA%+CdM;fcTd#_D5Kn7dG$`64OD0URCZQN zTR*cQ3j>Hx@7$Rb+`a8=%=4QUz8vH;;nHm)-r{Q%C?>@X*;{v11rSVz-($F+CHq8h zR9_?VJxihJJevhsn#PNw?o{Tt(2Xa~s|uv%#qq&{p>T`e3RPHRPLxtxW$EvyyK*Tun;>RbO;G+sHavuB1 zwZ!CFGj)`_Q0-KZa`_;n>@ z2dU`p-EhF zS!_eOV;HCE*k0c(Dej-w_$4)!o!bensIm9-L^62wOyfg3^z)f&U#n*Xba7nQZ#7mO z^(U;7#_$I!y4VPxA|P@6MNpjy7v!38*_r$BPL3M=ntIdND1FnDx_VU22fadPcNI>d zb&T-STkw}|&o1hcs*bYj02;dwWIvceZCBjk=52`N=;t_^k(%XK`>B;jCiTto_y%H( zIWu66ogV8--LZ{pjow4Bp=(P6K)x8#>07wkKB+i=?izDVaHn?VZ2xKlA@V4bn>(8t z9A5V8l4PLvb_?nYKtbu7A#oq#rZNdin|n;17X8F}CQ{JH7lN+s?zwH)N3n7I^ ze4<0|i+qOY5LSb8(Ly9~E$ZfgM5m@|u9%ZModTJ5W_}4iJj(pjXnp{c;x2&puI4eL zvl&C{K|1dIIMCF3e$ved98o5`Bv0MeV7sP$got?JMnt5+ihAFl$y@%TY{K`yaF$I% zz29#Ko)OiLQ>2Tw;C<3^c6CkpO$pmbeD&2ty;)QIj!$S^U99y5X9O8${u3y@4_HpS z45ra}wQ&=bc4g#_GyI&7Rv)!!j~;WA*C%l5G}K$1Mb7;`uCDu)3;x-oSEjoy&pG5Y zSR0dHq-7(5HdxvnTm4d=%9V4|zvW)6$HEwyqHIz{f>O=IR&1RkqHhG9Rx2@q1q* z2&EH~V4(i!$4zbZbi2V93|_Nu9pZ{7ql5F659d;~>m3z~Ky}JANNa~&mUr>1i z^Qv4`^017y?jrn)x<8jHPsY!d7&j!)oj84An^%b$RC!VV>>W*)%0UMPbm+mFS zQYgi9s}jLwuRW`B&#LzdPj>TSHV+Uy>yl<@13`USsGRHNOo`QTky;-;M6w4XN;s-a z(VVPNh8ae5?CsXh)MAQ$1u4(LxG31f4jYj zvFjKP_WrPXku9={Z(QqQaFAnob@p2yw&#}5>k3m!X?SA43x>@wR%_eWv?wC}As7=( z7Rp#01*=n;cT}*6OEy@K_3KVW-ki}sz0x!w>x1jmJ4PK@s;kyOagoKL>Zdfh>%KKz zp?DCvy(M)U*6Pa-6S=E(5h80iC-r)BUuf47`c>^)ooIE}IB91%0-IJ1scVROs zUh4X}KiJtT+s3oFVtzzdjGq2j#!cOi;7+B*DPpGFJIg258_$)fbnFig@W!#?^GeS> zi?qiufj*hMk*}9z00EM#PD^iWOk+T2;YeGTFO(*sKcliMj5EQW-d&YPG0V5x-+Rr> zr{kTh{J4Iu>L>3hV4~7(%&mL_6w3JzkMr^7TfSdd_nQ;1erSCB@ngA3cPFXkU7M5~ zB>Ksv3~OWc?5jZ_{!3{K;0Y<(MFN;wO>)};L8pJc7po04t(z(zEZ*#}dgdlJqNfx$ z_!Z^mZNcuFf^+kaCHDSbvrJh?z=?L4Fn`A-TJi{4)D~wbg1E4@L zN3o5dp4jbd6CkyM#M-6*CVBpMD8v6D-249<(Ave5Ol~E%Auh!WoG>D8?8Z1%)=3p0 zM;4p}>h-Wdt0s@c&;O431Ct*JN{6jB3u+6p{0`y_C)e^J?ZuCOqv#)pGVeYo4hyDg zr!3D?9&)(7SGmMNh+Y=90Qxfo5c~Ou^7mN1;e~w-z9P)pBa=-+?Tug)QXlz&(T?ii zZ0G9qzcA|7%(cNzn*Ce7wWH>XF>z0yrQ1UCKJ0a@*I%Puhhm1AHU|>6q51t{oZXI`fs_P<0gIIW(A#rAB^28%6tG#f zTqEK-*JxHB^n|Dn1aoEIT*Lo|2de5d?oqfCXE&0{`W~Py4jV%--EhzKv;9$0hoU({ z5?c#!K-@+xQg9{qHphk?Y*OXOx;4USZgmNd^I;GsOgxQ^=QsrUU&B)j?Eo+wLr=W_ zX#&4L60R0;<&235TG$;UYE;?NIW5esM*d7z+ke*^jee~>6EgcW?b0km0w;XvY~joo z(?r&>KR6v(e0e%|WjPVfgW!u=uN4_7u-!0z|o3{TqV71)#i_l=P>Ez#YRUm^Lvz?fFcQ%96g2=j{gW z4s4b4pM&@qLxwzXKy^XX^WtNpCfe^YsTCf9mSu&}BdqnhoCQf!yOxGY+0q&#F507O zZuuoMvHZP|BfGwKG<`C3+Gq0Qhx2Vw8*&;Yjd<~=<@OodzfM1}Wv)CqY$oA$ zGRd8M;6wGYZawaqv`N(L{7|*=4+pOfHv$%@j%Mo9eSK|R`8UTG`7%!n26J)qSk6cP z{DR07as2~rj_=jfOqW>&H$aN+yc5Ga7+)hne05D2VT!xULFCb`3l!ddEhw`LdzU7R z0IF1^CJM6upbGD5<*Q%jslky3ZCv+8Ui5H_sl^HIW8O!t_!b9=-VMzp(B*djkXL0I2b7#X-)tIj@dWj| z1McwF?wIe^hf+?1>52hQi79HHGUd7QW@1dtAEzBc{-K$z@Y~rS}js72;*?j zc_EhPL;>|<($}Nd+wKwcd9mt-9;j4n*1hNzvg_8j^l3ty&($^%X&pu3vtmp=1yQO4k;S$EeY8*u!PIp4G{% zPhPzAk|IC5;_a^mspBdf)n8>@Gr|>S+hK_X?_`kFH1G!><{-+K3cIakxA$0k$e25H zke!+{LmX9&hVgM=J+DIRq6o713RJU-hA?_uqa@?fCQpxdp&hoFciJk^FXJCQU?cle z11}X*2}G}oQm9-S3SJder0b59%woDSH{UN>IxZ+ugKw?r0*IDn(>17YQB34MYNaQ{kf+u=^b!^J)|&vXep_9W~7S;!>u3fG%Uu=UkUI6 z?nui?y?XzU{Ba6uneVn-st)GxikTlhr%0V6)}V1rz$kop1vPVV(zVbCpJQ2yRokvV zzw7J0)-HxGJDwHr;vLaBZ!1v`BcWYf6-lU9kE(^8!5nY4~Myc;hD!{mK zyr)h+P-daT(aGu&rbHk5LQl^@le#NHayYl#;e>Hx<6Zu4`AdzOz;iv&8EuxtaVdrSHbm9; zNx99G8P`IcrIu)~m|uC}wmzpDk9LKJJL-VG`Hy>xg}`^`mFO9Wp-EzVOmE72*A6iB z{O1nm-pQtOr*yH6IM(#d9rxYSCxkjtE1X?oXR8iGQ2ba!i6S9^A0wEND!bJd583 z>n(GwI;EN+VY20G7T!OnqUU*+GJ-KuqW4;n(Xh~A1 zae$g2W=TyZf0<9txF7mgYCPg0ata-_A7*j;z}C3CujI3FU&0qqzm1F=`_b^R%q0wB zb+{g+>#1>ZGK_FNTS_=!EORS{aYr=$sJpV)s_+`iTP12_ytuI4{&&D9{NR5?K7GaX zVLi+kO^VbpNK#cl$yVLa#0Fn5KluTY{>t-sF_KIR1~c|^0NHL&_SoE zwE3uLBT>mypG{OLN7{IuThmqUVLOZtueJO4exafjGT$eTZ0g<-T>HFzlx>#n<{Bv? zMiZVgeUw}OCc&0X;V{;6#7|ouMzI+^`PK>R*W`^55&{zi|&@( z$QadYx{eb=3U0{ConLI!inSYrG-JVc^Vgg;nw0?*6vt2HAIjUjQ>{z{?FT#C2NJ$v zQGzR|oEx-(Z^_TC>)U{oH0asyCx3G^#?ObaCD~HsiK4~A$tLr^HCeIRaq~G+q*e?I z&%57hg)EY14U9LtZ_{#*sfT2R&0_blsF4HQ>NhT^^}0VUU!GGfe!U!4$eLJhSolb2 zHo^}y=wU9l!8NuF^t+-{&{^eirbh1XxE|`0lF{sJW;;5 zb_oo(9({$&h3yT!AMls*^{+|M%o8Uo<+d6DYEwY$wbjfv>W?(T)0-dfxYf^CEj`5& zxI(S?s($Z0CXUr+cpB1X%$K-y?dHSI=7{AH0-+OgJKGJ>0W*LfCj%mEwJ7gkJpsS< z38%yUdH~KE=pVEtz4IAF8&DUv;PiI`kx(;k3V``GP%i+`geE2@A5hRRVOuji?&#?F zJ-(2+!Ts>&$7g3JeJB){OjfJe!e5eF=u5tQN&-A8-A}B)7>Hl%YZZM{j}r=JPlZ?En5=KpA` zpx~`ni0$ongE>&n_DkZy6!VvxbmZFK^*&o!{ftalSXg=(GPM5f+EsYnh7f3&UW~YJ z@|@ZnUW~AcrTS4>nGNjB0`$U}|F}-2^^> CreateSandboxResponse: """ @@ -85,7 +95,7 @@ async def create_sandbox( the specified image without requiring a pre-created template. Args: - request: Sandbox creation request + body: Sandbox creation request x_request_id: Unique request identifier for tracing (optional; server generates if omitted). Returns: @@ -94,8 +104,46 @@ async def create_sandbox( Raises: HTTPException: If sandbox creation scheduling fails """ - validate_extensions(request.extensions) - return await sandbox_service.create_sandbox(request) + cfg = get_config() + principal = get_principal(http_request) + authorize_mutating_action( + http_request, + principal, + LifecycleAction.CREATE, + owner_key=cfg.authz.owner_metadata_key, + team_key=cfg.authz.team_metadata_key, + ) + body = apply_reserved_metadata_for_create(body, principal, cfg) + validate_extensions(body.extensions) + try: + res = await sandbox_service.create_sandbox(body) + log_mutation_audit( + http_request, action=LifecycleAction.CREATE, sandbox_id=res.id, outcome="success" + ) + return res + except HTTPException as exc: + err = exc.detail + if isinstance(err, dict): + code = err.get("code") + else: + code = None + log_mutation_audit( + http_request, + action=LifecycleAction.CREATE, + sandbox_id=None, + outcome="error", + error_code=code, + ) + raise + except Exception: + log_mutation_audit( + http_request, + action=LifecycleAction.CREATE, + sandbox_id=None, + outcome="error", + error_code="UNEXPECTED", + ) + raise # Search endpoint @@ -111,6 +159,7 @@ async def create_sandbox( }, ) async def list_sandboxes( + http_request: Request, state: Optional[List[str]] = Query(None, description="Filter by lifecycle state. Pass multiple times for OR logic."), metadata: Optional[str] = Query(None, description="Arbitrary metadata key-value pairs for filtering (URL encoded)."), page: int = Query(1, ge=1, description="Page number for pagination"), @@ -150,17 +199,28 @@ async def list_sandboxes( ) # Construct request object - request = ListSandboxesRequest( + list_req = ListSandboxesRequest( filter=SandboxFilter(state=state, metadata=metadata_dict if metadata_dict else None), - pagination=PaginationRequest(page=page, pageSize=page_size) + pagination=PaginationRequest(page=page, pageSize=page_size), ) import logging + logger = logging.getLogger(__name__) - logger.info("ListSandboxes: %s", request.filter) + logger.info("ListSandboxes: %s", list_req.filter) + + cfg = get_config() + principal = get_principal(http_request) + authorize_action( + principal, + LifecycleAction.LIST_SANDBOXES, + owner_key=cfg.authz.owner_metadata_key, + team_key=cfg.authz.team_metadata_key, + ) + list_req = merge_list_scope_from_request(http_request, list_req, cfg) # Delegate to the service layer for filtering and pagination - return sandbox_service.list_sandboxes(request) + return sandbox_service.list_sandboxes(list_req) @router.get( @@ -176,6 +236,7 @@ async def list_sandboxes( }, ) async def get_sandbox( + http_request: Request, sandbox_id: str, x_request_id: Optional[str] = Header(None, alias="X-Request-ID", description="Unique request identifier for tracing"), ) -> Sandbox: @@ -195,8 +256,23 @@ async def get_sandbox( Raises: HTTPException: If sandbox not found or access denied """ - # Delegate to the service layer for sandbox lookup - return sandbox_service.get_sandbox(sandbox_id) + cfg = get_config() + principal = get_principal(http_request) + authorize_action( + principal, + LifecycleAction.GET_SANDBOX, + owner_key=cfg.authz.owner_metadata_key, + team_key=cfg.authz.team_metadata_key, + ) + box = sandbox_service.get_sandbox(sandbox_id) + authorize_action( + principal, + LifecycleAction.GET_SANDBOX, + owner_key=cfg.authz.owner_metadata_key, + team_key=cfg.authz.team_metadata_key, + sandbox=box, + ) + return box @router.delete( @@ -212,6 +288,7 @@ async def get_sandbox( }, ) async def delete_sandbox( + http_request: Request, sandbox_id: str, x_request_id: Optional[str] = Header(None, alias="X-Request-ID", description="Unique request identifier for tracing"), ) -> Response: @@ -230,8 +307,64 @@ async def delete_sandbox( Raises: HTTPException: If sandbox not found or deletion fails """ - # Delegate to the service layer for deletion - sandbox_service.delete_sandbox(sandbox_id) + cfg = get_config() + principal = get_principal(http_request) + authorize_mutating_action( + http_request, + principal, + LifecycleAction.DELETE, + owner_key=cfg.authz.owner_metadata_key, + team_key=cfg.authz.team_metadata_key, + sandbox_id=sandbox_id, + ) + try: + box = sandbox_service.get_sandbox(sandbox_id) + except HTTPException as exc: + if exc.status_code == status.HTTP_404_NOT_FOUND: + log_mutation_audit( + http_request, + action=LifecycleAction.DELETE, + sandbox_id=sandbox_id, + outcome="not_found", + ) + raise + authorize_mutating_action( + http_request, + principal, + LifecycleAction.DELETE, + owner_key=cfg.authz.owner_metadata_key, + team_key=cfg.authz.team_metadata_key, + sandbox_id=sandbox_id, + sandbox=box, + ) + try: + sandbox_service.delete_sandbox(sandbox_id) + log_mutation_audit( + http_request, action=LifecycleAction.DELETE, sandbox_id=sandbox_id, outcome="success" + ) + except HTTPException as exc: + err = exc.detail + if isinstance(err, dict): + code = err.get("code") + else: + code = None + log_mutation_audit( + http_request, + action=LifecycleAction.DELETE, + sandbox_id=sandbox_id, + outcome="error", + error_code=code, + ) + raise + except Exception: + log_mutation_audit( + http_request, + action=LifecycleAction.DELETE, + sandbox_id=sandbox_id, + outcome="error", + error_code="UNEXPECTED", + ) + raise return Response(status_code=status.HTTP_204_NO_CONTENT) @@ -252,6 +385,7 @@ async def delete_sandbox( }, ) async def pause_sandbox( + http_request: Request, sandbox_id: str, x_request_id: Optional[str] = Header(None, alias="X-Request-ID", description="Unique request identifier for tracing"), ) -> Response: @@ -271,8 +405,64 @@ async def pause_sandbox( Raises: HTTPException: If sandbox not found or cannot be paused """ - # Delegate to the service layer for pause orchestration - sandbox_service.pause_sandbox(sandbox_id) + cfg = get_config() + principal = get_principal(http_request) + authorize_mutating_action( + http_request, + principal, + LifecycleAction.PAUSE, + owner_key=cfg.authz.owner_metadata_key, + team_key=cfg.authz.team_metadata_key, + sandbox_id=sandbox_id, + ) + try: + box = sandbox_service.get_sandbox(sandbox_id) + except HTTPException as exc: + if exc.status_code == status.HTTP_404_NOT_FOUND: + log_mutation_audit( + http_request, + action=LifecycleAction.PAUSE, + sandbox_id=sandbox_id, + outcome="not_found", + ) + raise + authorize_mutating_action( + http_request, + principal, + LifecycleAction.PAUSE, + owner_key=cfg.authz.owner_metadata_key, + team_key=cfg.authz.team_metadata_key, + sandbox_id=sandbox_id, + sandbox=box, + ) + try: + sandbox_service.pause_sandbox(sandbox_id) + log_mutation_audit( + http_request, action=LifecycleAction.PAUSE, sandbox_id=sandbox_id, outcome="success" + ) + except HTTPException as exc: + err = exc.detail + if isinstance(err, dict): + code = err.get("code") + else: + code = None + log_mutation_audit( + http_request, + action=LifecycleAction.PAUSE, + sandbox_id=sandbox_id, + outcome="error", + error_code=code, + ) + raise + except Exception: + log_mutation_audit( + http_request, + action=LifecycleAction.PAUSE, + sandbox_id=sandbox_id, + outcome="error", + error_code="UNEXPECTED", + ) + raise return Response(status_code=status.HTTP_202_ACCEPTED) @@ -289,6 +479,7 @@ async def pause_sandbox( }, ) async def resume_sandbox( + http_request: Request, sandbox_id: str, x_request_id: Optional[str] = Header(None, alias="X-Request-ID", description="Unique request identifier for tracing"), ) -> Response: @@ -308,8 +499,64 @@ async def resume_sandbox( Raises: HTTPException: If sandbox not found or cannot be resumed """ - # Delegate to the service layer for resume orchestration - sandbox_service.resume_sandbox(sandbox_id) + cfg = get_config() + principal = get_principal(http_request) + authorize_mutating_action( + http_request, + principal, + LifecycleAction.RESUME, + owner_key=cfg.authz.owner_metadata_key, + team_key=cfg.authz.team_metadata_key, + sandbox_id=sandbox_id, + ) + try: + box = sandbox_service.get_sandbox(sandbox_id) + except HTTPException as exc: + if exc.status_code == status.HTTP_404_NOT_FOUND: + log_mutation_audit( + http_request, + action=LifecycleAction.RESUME, + sandbox_id=sandbox_id, + outcome="not_found", + ) + raise + authorize_mutating_action( + http_request, + principal, + LifecycleAction.RESUME, + owner_key=cfg.authz.owner_metadata_key, + team_key=cfg.authz.team_metadata_key, + sandbox_id=sandbox_id, + sandbox=box, + ) + try: + sandbox_service.resume_sandbox(sandbox_id) + log_mutation_audit( + http_request, action=LifecycleAction.RESUME, sandbox_id=sandbox_id, outcome="success" + ) + except HTTPException as exc: + err = exc.detail + if isinstance(err, dict): + code = err.get("code") + else: + code = None + log_mutation_audit( + http_request, + action=LifecycleAction.RESUME, + sandbox_id=sandbox_id, + outcome="error", + error_code=code, + ) + raise + except Exception: + log_mutation_audit( + http_request, + action=LifecycleAction.RESUME, + sandbox_id=sandbox_id, + outcome="error", + error_code="UNEXPECTED", + ) + raise return Response(status_code=status.HTTP_202_ACCEPTED) @@ -328,8 +575,9 @@ async def resume_sandbox( }, ) async def renew_sandbox_expiration( + http_request: Request, sandbox_id: str, - request: RenewSandboxExpirationRequest, + renew_body: RenewSandboxExpirationRequest, x_request_id: Optional[str] = Header(None, alias="X-Request-ID", description="Unique request identifier for tracing"), ) -> RenewSandboxExpirationResponse: """ @@ -340,7 +588,7 @@ async def renew_sandbox_expiration( Args: sandbox_id: Unique sandbox identifier - request: Renewal request with new expiration time + renew_body: Renewal request with new expiration time x_request_id: Unique request identifier for tracing (optional; server generates if omitted). Returns: @@ -349,8 +597,65 @@ async def renew_sandbox_expiration( Raises: HTTPException: If sandbox not found or renewal fails """ - # Delegate to the service layer for expiration updates - return sandbox_service.renew_expiration(sandbox_id, request) + cfg = get_config() + principal = get_principal(http_request) + authorize_mutating_action( + http_request, + principal, + LifecycleAction.RENEW, + owner_key=cfg.authz.owner_metadata_key, + team_key=cfg.authz.team_metadata_key, + sandbox_id=sandbox_id, + ) + try: + box = sandbox_service.get_sandbox(sandbox_id) + except HTTPException as exc: + if exc.status_code == status.HTTP_404_NOT_FOUND: + log_mutation_audit( + http_request, + action=LifecycleAction.RENEW, + sandbox_id=sandbox_id, + outcome="not_found", + ) + raise + authorize_mutating_action( + http_request, + principal, + LifecycleAction.RENEW, + owner_key=cfg.authz.owner_metadata_key, + team_key=cfg.authz.team_metadata_key, + sandbox_id=sandbox_id, + sandbox=box, + ) + try: + res = sandbox_service.renew_expiration(sandbox_id, renew_body) + log_mutation_audit( + http_request, action=LifecycleAction.RENEW, sandbox_id=sandbox_id, outcome="success" + ) + return res + except HTTPException as exc: + err = exc.detail + if isinstance(err, dict): + code = err.get("code") + else: + code = None + log_mutation_audit( + http_request, + action=LifecycleAction.RENEW, + sandbox_id=sandbox_id, + outcome="error", + error_code=code, + ) + raise + except Exception: + log_mutation_audit( + http_request, + action=LifecycleAction.RENEW, + sandbox_id=sandbox_id, + outcome="error", + error_code="UNEXPECTED", + ) + raise # ============================================================================ @@ -488,7 +793,7 @@ async def delete_snapshot( }, ) async def get_sandbox_endpoint( - request: Request, + http_request: Request, sandbox_id: str, port: int, use_server_proxy: bool = Query(False, description="Whether to return a server-proxied URL"), @@ -507,7 +812,7 @@ async def get_sandbox_endpoint( This requires the ingress gateway to be configured with secure_access signing keys. Args: - request: FastAPI request object + http_request: FastAPI request object sandbox_id: Unique sandbox identifier port: Port number where the service is listening inside the sandbox (1-65535) use_server_proxy: Whether to return a server-proxied URL @@ -523,12 +828,28 @@ async def get_sandbox_endpoint( HTTPException: If sandbox not found, endpoint not available, or signed routes are not supported by the runtime/configuration (400). """ + cfg = get_config() + principal = get_principal(http_request) + authorize_action( + principal, + LifecycleAction.GET_ENDPOINT, + owner_key=cfg.authz.owner_metadata_key, + team_key=cfg.authz.team_metadata_key, + ) + box = sandbox_service.get_sandbox(sandbox_id) + authorize_action( + principal, + LifecycleAction.GET_ENDPOINT, + owner_key=cfg.authz.owner_metadata_key, + team_key=cfg.authz.team_metadata_key, + sandbox=box, + ) # Delegate to the service layer for endpoint resolution endpoint = sandbox_service.get_endpoint(sandbox_id, port, expires=expires) if use_server_proxy: # Prefer configured external address when available. - base_url = str(request.base_url).rstrip("/") + base_url = str(http_request.base_url).rstrip("/") eip = (get_config().server.eip or "").strip().rstrip("/") if eip: base_url = eip diff --git a/server/opensandbox_server/api/lifecycle_helpers.py b/server/opensandbox_server/api/lifecycle_helpers.py new file mode 100644 index 000000000..c69206ca6 --- /dev/null +++ b/server/opensandbox_server/api/lifecycle_helpers.py @@ -0,0 +1,115 @@ +# Copyright 2025 Alibaba Group Holding Ltd. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. + +"""Shared helpers for lifecycle routes: scoping, reserved metadata, audit logging.""" + +from __future__ import annotations + +import logging +from typing import Optional + +from fastapi import Request + +from fastapi.exceptions import HTTPException + +from opensandbox_server.api.schema import CreateSandboxRequest, ListSandboxesRequest, SandboxFilter +from opensandbox_server.config import AppConfig +from opensandbox_server.middleware.request_id import get_request_id +from opensandbox_server.middleware.authorization import authorize_action, is_user_scoped +from opensandbox_server.middleware.principal import Principal + +logger = logging.getLogger(__name__) + + +def get_principal(request: Request) -> Optional[Principal]: + return getattr(request.state, "principal", None) + + +def merge_list_scope_from_request(http_request: Request, body: ListSandboxesRequest, config: AppConfig) -> ListSandboxesRequest: + """AND server-side owner/team scope into list metadata filters for user principals.""" + return _merge_list_scope_inner(body, get_principal(http_request), config) + + +def _merge_list_scope_inner( + request: ListSandboxesRequest, + principal: Optional[Principal], + config: AppConfig, +) -> ListSandboxesRequest: + if not is_user_scoped(principal): + return request + assert principal is not None + owner_k = config.authz.owner_metadata_key + team_k = config.authz.team_metadata_key + meta = dict(request.filter.metadata or {}) + meta[owner_k] = principal.canonical_owner + if principal.canonical_team is not None: + meta[team_k] = principal.canonical_team + new_filter = SandboxFilter( + state=request.filter.state, + metadata=meta, + ) + return ListSandboxesRequest(filter=new_filter, pagination=request.pagination) + + +def apply_reserved_metadata_for_create( + req: CreateSandboxRequest, + principal: Optional[Principal], + config: AppConfig, +) -> CreateSandboxRequest: + if not is_user_scoped(principal): + return req + assert principal is not None + meta = dict(req.metadata or {}) + meta[config.authz.owner_metadata_key] = principal.canonical_owner + if principal.canonical_team is not None: + meta[config.authz.team_metadata_key] = principal.canonical_team + return req.model_copy(update={"metadata": meta}) + + +def authorize_mutating_action( + request: Request, + principal: Optional[Principal], + action: str, + *, + owner_key: str, + team_key: str, + sandbox_id: Optional[str] = None, + sandbox=None, +) -> None: + """Calls authorize_action and emits a mutation_audit entry when 403 is raised.""" + try: + authorize_action(principal, action, owner_key=owner_key, team_key=team_key, sandbox=sandbox) + except HTTPException: + log_mutation_audit(request, action=action, sandbox_id=sandbox_id, outcome="forbidden") + raise + + +def log_mutation_audit( + request: Request, + *, + action: str, + sandbox_id: Optional[str], + outcome: str, + error_code: Optional[str] = None, +) -> None: + principal = get_principal(request) + rid = get_request_id() or request.headers.get("X-Request-ID") or "-" + subj = getattr(principal, "subject", None) if principal else None + team = getattr(principal, "canonical_team", None) if principal else None + role = getattr(principal, "role", None) if principal else None + src = getattr(principal, "source", None) if principal else None + logger.info( + "mutation_audit request_id=%s action=%s sandbox_id=%s outcome=%s error_code=%s " + "principal_source=%s principal_subject=%s principal_team=%s principal_role=%s", + rid, + action, + sandbox_id, + outcome, + error_code, + src, + subj, + team, + role, + ) diff --git a/server/opensandbox_server/config.py b/server/opensandbox_server/config.py index 514d6e969..2bff720fe 100644 --- a/server/opensandbox_server/config.py +++ b/server/opensandbox_server/config.py @@ -55,6 +55,9 @@ EGRESS_MODE_DNS = "dns" EGRESS_MODE_DNS_NFT = "dns+nft" +AUTH_MODE_API_KEY_ONLY = "api_key_only" +AUTH_MODE_API_KEY_AND_USER = "api_key_and_user" +USER_MODE_TRUSTED_HEADER = "trusted_header" def _is_valid_ip(host: str) -> bool: @@ -372,6 +375,96 @@ def validate_ingress_mode(self) -> "IngressConfig": return self +class TrustedHeaderConfig(BaseModel): + """Identity headers set by a reverse proxy in trusted-header user mode (OSEP-0006).""" + + user_header: str = Field( + default="X-OpenSandbox-User", + min_length=1, + description="Header carrying the end-user subject id (required for user auth).", + ) + team_header: str = Field( + default="X-OpenSandbox-Team", + min_length=1, + description="Optional team id for owner/team scope.", + ) + roles_header: str = Field( + default="X-OpenSandbox-Roles", + min_length=1, + description="Comma-separated roles (read_only, operator) for the end user.", + ) + + +class AuthConfig(BaseModel): + """High-level authentication behavior (API key, optional user path for the console).""" + + mode: Literal[AUTH_MODE_API_KEY_ONLY, AUTH_MODE_API_KEY_AND_USER] = Field( + default=AUTH_MODE_API_KEY_ONLY, + description='Use "api_key_only" (default) or "api_key_and_user" for console + proxy identity headers.', + ) + user_mode: Literal[USER_MODE_TRUSTED_HEADER] = Field( + default=USER_MODE_TRUSTED_HEADER, + description="How user identity is obtained when mode is api_key_and_user. Phase 1: trusted_header only.", + ) + trusted_header: TrustedHeaderConfig = Field( + default_factory=TrustedHeaderConfig, + description="Header names for trusted user/team/roles (when user_mode = trusted_header).", + ) + + +class AuthzConfig(BaseModel): + """Role defaults and owner/team metadata keys for resource scoping (OSEP-0006).""" + + default_role: Literal["read_only", "operator"] = Field( + default="read_only", + description="When not overridden by subject lists or roles header.", + ) + owner_metadata_key: str = Field( + default="access.owner", + min_length=1, + description="Reserved label key for scope owner (injected on create, enforced on read/mutate).", + ) + team_metadata_key: str = Field( + default="access.team", + min_length=1, + description="Reserved label key for team; optional when the team header is absent.", + ) + operator_subjects: list[str] = Field( + default_factory=list, + description="Raw user subject values that always receive the operator role.", + ) + read_only_subjects: list[str] = Field( + default_factory=list, + description="Raw user subject values that always receive the read_only role.", + ) + + +class ConsoleConfig(BaseModel): + """Static hosting of the developer console SPA (optional).""" + + enabled: bool = Field( + default=False, + description="If true and the dist directory exists, mount the console under mount_path.", + ) + mount_path: str = Field( + default="/console", + min_length=1, + description="URL prefix for the single-page app (Vite base should match).", + ) + + @model_validator(mode="after") + def validate_mount_path(self) -> "ConsoleConfig": + mount = self.mount_path.rstrip("/") or "/" + reserved_exact = {"/", "/v1", "/health", "/docs", "/redoc", "/openapi.json"} + reserved_prefixes = ("/v1/", "/sandboxes") + if mount in reserved_exact or any(mount.startswith(p) for p in reserved_prefixes): + raise ValueError( + "console.mount_path must not overlap API or system routes " + "(/, /v1, /v1/*, /sandboxes*, /health, /docs, /redoc, /openapi.json)." + ) + return self + + class LogConfig(BaseModel): """Logging configuration.""" @@ -805,6 +898,9 @@ class AppConfig(BaseModel): """Root application configuration model.""" server: ServerConfig = Field(default_factory=ServerConfig) + auth: AuthConfig = Field(default_factory=AuthConfig) + authz: AuthzConfig = Field(default_factory=AuthzConfig) + console: ConsoleConfig = Field(default_factory=ConsoleConfig) log: LogConfig = Field( default_factory=LogConfig, description="Logging configuration (level, file output, rotation).", @@ -945,6 +1041,13 @@ def get_config_path() -> Path: __all__ = [ "AppConfig", + "AuthConfig", + "AuthzConfig", + "AUTH_MODE_API_KEY_ONLY", + "AUTH_MODE_API_KEY_AND_USER", + "ConsoleConfig", + "TrustedHeaderConfig", + "USER_MODE_TRUSTED_HEADER", "RenewIntentConfig", "RenewIntentRedisConfig", "ServerConfig", diff --git a/server/opensandbox_server/examples/example.config.toml b/server/opensandbox_server/examples/example.config.toml index f49f386c1..5ef3bf94b 100644 --- a/server/opensandbox_server/examples/example.config.toml +++ b/server/opensandbox_server/examples/example.config.toml @@ -70,3 +70,25 @@ mode = "dns" [renew_intent] enabled = false min_interval_seconds = 60 + +# --- Developer console & dual auth (OSEP-0006) --- +# [auth] +# mode = "api_key_only" # default: API key only (backward compatible) +# mode = "api_key_and_user" # optional: trusted identity headers for the console +# user_mode = "trusted_header" +# +# [auth.trusted_header] +# user_header = "X-OpenSandbox-User" +# team_header = "X-OpenSandbox-Team" +# roles_header = "X-OpenSandbox-Roles" +# +# [authz] +# default_role = "read_only" +# owner_metadata_key = "access.owner" +# team_metadata_key = "access.team" +# operator_subjects = [] +# read_only_subjects = [] +# +# [console] +# enabled = false +# mount_path = "/console" diff --git a/server/opensandbox_server/main.py b/server/opensandbox_server/main.py index ba0004f67..f4b1940f8 100644 --- a/server/opensandbox_server/main.py +++ b/server/opensandbox_server/main.py @@ -153,6 +153,21 @@ async def lifespan(app: FastAPI): app.include_router(pool_router, prefix="/v1") app.include_router(proxy_router, prefix="/v1") +# Optional static hosting of the developer console (OSEP-0006) +if app_config.console.enabled: + from pathlib import Path + + from starlette.staticfiles import StaticFiles + + _console_dist = Path(__file__).resolve().parent.parent.parent / "console" / "dist" + if _console_dist.is_dir(): + _mount = app_config.console.mount_path.rstrip("/") or "/console" + app.mount( + _mount, + StaticFiles(directory=str(_console_dist), html=True), + name="console", + ) + DEFAULT_ERROR_CODE = "GENERAL::UNKNOWN_ERROR" DEFAULT_ERROR_MESSAGE = "An unexpected error occurred." diff --git a/server/opensandbox_server/middleware/auth.py b/server/opensandbox_server/middleware/auth.py index 323bdc9d4..acdb72653 100644 --- a/server/opensandbox_server/middleware/auth.py +++ b/server/opensandbox_server/middleware/auth.py @@ -2,21 +2,9 @@ # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. """ -Authentication middleware for OpenSandbox Lifecycle API. - -This module implements API Key authentication as specified in the OpenAPI spec. -API keys are configured via config.toml and validated against the OPEN-SANDBOX-API-KEY header. +Authentication middleware: API key path (legacy) and optional user identity (OSEP-0006). """ import re @@ -26,109 +14,134 @@ from fastapi.responses import JSONResponse from starlette.middleware.base import BaseHTTPMiddleware -from opensandbox_server.config import AppConfig, get_config - -SANDBOX_API_KEY_HEADER = "OPEN-SANDBOX-API-KEY" - +from opensandbox_server.config import ( + AUTH_MODE_API_KEY_AND_USER, + AUTH_MODE_API_KEY_ONLY, + USER_MODE_TRUSTED_HEADER, + AppConfig, + get_config, +) +from opensandbox_server.middleware.principal import build_user_principal, principal_for_api_key class AuthMiddleware(BaseHTTPMiddleware): """ - Middleware for API Key authentication. - - Validates the OPEN-SANDBOX-API-KEY header for all requests except health check. - Returns 401 Unauthorized if authentication fails. + Validates ``OPEN-SANDBOX-API-KEY`` when configured, with optional dual auth for the console. """ - # Paths that don't require authentication - EXEMPT_PATHS = ["/health", "/docs", "/redoc", "/openapi.json"] + API_KEY_HEADER = "OPEN-SANDBOX-API-KEY" - # Strict pattern for proxy-to-sandbox: /sandboxes/{id}/proxy/{port}/... with numeric port only. - # Matches the actual route in proxy.py; rejects path traversal (..) and malformed port. + EXEMPT_PATHS = ["/health", "/docs", "/redoc", "/openapi.json"] _PROXY_PATH_RE = re.compile(r"^(/v1)?/sandboxes/[^/]+/proxy/\d+(/|$)") @staticmethod def _is_proxy_path(path: str) -> bool: - """True only for the exact proxy-route shape; rejects path traversal (..).""" if ".." in path: return False return bool(AuthMiddleware._PROXY_PATH_RE.match(path)) - def __init__(self, app, config: Optional[AppConfig] = None): - """ - Initialize authentication middleware. + @staticmethod + def _is_console_path(path: str, mount: str) -> bool: + if ".." in path: + return False + base = mount.rstrip("/") or "/console" + return path == base or path.startswith(base + "/") - Args: - app: FastAPI application instance - config: Optional application configuration (for dependency injection) - """ + def __init__(self, app, config: Optional[AppConfig] = None): super().__init__(app) self.config = config or get_config() - # Read the API key directly from config; suitable for dev/test usage self.valid_api_keys = self._load_api_keys() def _load_api_keys(self) -> set: - """ - Load valid API keys from configuration. - - Returns: - set: Set of valid API keys - """ - # Supports a single API key from config; extend later for secret managers api_key = self.config.server.api_key - # Treat empty string as no key configured if api_key and api_key.strip(): return {api_key} return set() - async def dispatch(self, request: Request, call_next: Callable) -> Response: - """ - Process each request and validate authentication. - - Args: - request: Incoming HTTP request - call_next: Next middleware or route handler + def _try_trusted_user_principal(self, request: Request): + if self.config.auth.user_mode != USER_MODE_TRUSTED_HEADER: + return None + th = self.config.auth.trusted_header + raw_user = request.headers.get(th.user_header) + if raw_user is None or not str(raw_user).strip(): + return None + raw_team = request.headers.get(th.team_header) + roles = request.headers.get(th.roles_header) + try: + return build_user_principal( + str(raw_user).strip(), + str(raw_team).strip() if raw_team is not None else None, + roles, + self.config.authz, + ) + except ValueError: + return None - Returns: - Response: HTTP response - """ - # Skip authentication for exempt paths + async def dispatch(self, request: Request, call_next: Callable) -> Response: if any(request.url.path.startswith(path) for path in self.EXEMPT_PATHS): return await call_next(request) - # Skip authentication only for the exact proxy-to-sandbox route shape - # (no path traversal, no loose substring match) if self._is_proxy_path(request.url.path): return await call_next(request) - # If no API keys are configured, skip authentication - if not self.valid_api_keys: + if self._is_console_path(request.url.path, self.config.console.mount_path): return await call_next(request) - # Extract API key from header - api_key = request.headers.get(SANDBOX_API_KEY_HEADER) + mode = self.config.auth.mode + has_keys = bool(self.valid_api_keys) + + # No API keys: open access (legacy) OR require user headers for api_key_and_user + if not has_keys: + if mode == AUTH_MODE_API_KEY_AND_USER: + principal = self._try_trusted_user_principal(request) + if principal is None: + return JSONResponse( + status_code=status.HTTP_401_UNAUTHORIZED, + content={ + "code": "MISSING_TRUSTED_IDENTITY", + "message": "User authentication requires trusted identity headers (e.g. " + f"{self.config.auth.trusted_header.user_header}).", + }, + ) + request.state.principal = principal + return await call_next(request) + return await call_next(request) - # Validate API key - if not api_key: + api_key = request.headers.get(self.API_KEY_HEADER) + if api_key: + if api_key in self.valid_api_keys: + request.state.principal = principal_for_api_key() + return await call_next(request) + return JSONResponse( + status_code=status.HTTP_401_UNAUTHORIZED, + content={ + "code": "INVALID_API_KEY", + "message": "Authentication credentials are invalid. " + "Check your API key and try again.", + }, + ) + + if mode == AUTH_MODE_API_KEY_ONLY: return JSONResponse( status_code=status.HTTP_401_UNAUTHORIZED, content={ "code": "MISSING_API_KEY", "message": "Authentication credentials are missing. " - f"Provide API key via {SANDBOX_API_KEY_HEADER} header.", + "Provide API key via OPEN-SANDBOX-API-KEY header.", }, ) - # Enforce strict comparison whenever API keys are configured - if self.valid_api_keys and api_key not in self.valid_api_keys: + principal = self._try_trusted_user_principal(request) + if principal is None: return JSONResponse( status_code=status.HTTP_401_UNAUTHORIZED, content={ - "code": "INVALID_API_KEY", - "message": "Authentication credentials are invalid. " - "Check your API key and try again.", + "code": "MISSING_TRUSTED_IDENTITY", + "message": "User authentication requires trusted identity headers (e.g. " + f"{self.config.auth.trusted_header.user_header}).", }, ) + request.state.principal = principal + return await call_next(request) + - # Authentication successful, proceed to next middleware/handler - response = await call_next(request) - return response +SANDBOX_API_KEY_HEADER = AuthMiddleware.API_KEY_HEADER diff --git a/server/opensandbox_server/middleware/authorization.py b/server/opensandbox_server/middleware/authorization.py new file mode 100644 index 000000000..c6cf55711 --- /dev/null +++ b/server/opensandbox_server/middleware/authorization.py @@ -0,0 +1,137 @@ +# Copyright 2025 Alibaba Group Holding Ltd. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. + +""" +Lifecycle action authorization: role matrix + owner/team scope (OSEP-0006). +""" + +from __future__ import annotations + +import logging +from typing import TYPE_CHECKING, Optional + +from fastapi import status +from fastapi.exceptions import HTTPException + +from opensandbox_server.middleware.principal import Principal + +logger = logging.getLogger(__name__) + +if TYPE_CHECKING: + from opensandbox_server.api.schema import Sandbox + +ActionName = str + + +# Actions match OSEP-0006 lifecycle surface. +class LifecycleAction: + LIST_SANDBOXES = "list_sandboxes" + GET_SANDBOX = "get_sandbox" + GET_ENDPOINT = "get_endpoint" + CREATE = "create_sandbox" + RENEW = "renew_expiration" + DELETE = "delete_sandbox" + PAUSE = "pause_sandbox" + RESUME = "resume_sandbox" + + +_READ_ONLY = { + LifecycleAction.LIST_SANDBOXES, + LifecycleAction.GET_SANDBOX, + LifecycleAction.GET_ENDPOINT, +} +_OPERATOR = _READ_ONLY | { + LifecycleAction.CREATE, + LifecycleAction.RENEW, + LifecycleAction.DELETE, + LifecycleAction.PAUSE, + LifecycleAction.RESUME, +} + + +def _actions_for_role(role: str) -> set[str] | None: + if role == "service_admin": + return None # all allowed; caller checks + if role == "operator": + return set(_OPERATOR) + if role == "read_only": + return set(_READ_ONLY) + return set() + + +def _scope_match( + principal: Principal, + owner_key: str, + team_key: str, + metadata: Optional[dict[str, str]], +) -> bool: + if principal.source not in ("user",): + return True + meta = metadata or {} + got_owner = (meta.get(owner_key) or "").strip() + if got_owner != principal.canonical_owner: + return False + if principal.canonical_team is not None: + if (meta.get(team_key) or "").strip() != principal.canonical_team: + return False + return True + + +def sandbox_in_scope( + principal: Optional[Principal], + sandbox: "Sandbox | dict", + owner_key: str, + team_key: str, +) -> bool: + if principal is None or principal.is_service_admin: + return True + if isinstance(sandbox, dict): + metadata = sandbox.get("metadata") + else: + metadata = sandbox.metadata + return _scope_match(principal, owner_key, team_key, metadata) + + +def authorize_action( + principal: Optional[Principal], + action: str, + *, + owner_key: str, + team_key: str, + sandbox: Optional["Sandbox | dict"] = None, +) -> None: + """ + Raise ``HTTPException``(403) if the action is not allowed for this principal, or + the sandbox (when provided) is out of owner/team scope. + Unauthenticated (``None``) principals are only allowed in dev mode (no API key configured); + the lifecycle layer should treat that as allow-all to preserve legacy tests. + """ + if principal is None: + # Dev mode: no API key configured. Log so misconfigured production deployments are visible. + logger.debug("authorize_action called with no principal (dev/open mode) — action=%s", action) + return + if principal.is_service_admin: + return + allowed = _actions_for_role(principal.role) + if allowed is not None and action not in allowed: + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, + detail={ + "code": "INSUFFICIENT_ROLE", + "message": f"Role '{principal.role}' is not allowed to perform this operation.", + }, + ) + if sandbox is not None and not sandbox_in_scope(principal, sandbox, owner_key, team_key): + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, + detail={ + "code": "OUT_OF_SCOPE", + "message": "The sandbox is outside the authenticated user owner/team scope.", + }, + ) + + +def is_user_scoped(principal: Optional[Principal]) -> bool: + return bool(principal and principal.source == "user" and not principal.is_service_admin) diff --git a/server/opensandbox_server/middleware/principal.py b/server/opensandbox_server/middleware/principal.py new file mode 100644 index 000000000..0be886418 --- /dev/null +++ b/server/opensandbox_server/middleware/principal.py @@ -0,0 +1,120 @@ +# Copyright 2025 Alibaba Group Holding Ltd. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. + +""" +Authenticated principal (API key or trusted identity) for lifecycle authz and audit. +""" + +from __future__ import annotations + +import hashlib +from dataclasses import dataclass +from typing import TYPE_CHECKING, Literal, Optional + +from opensandbox_server.services.validators import LABEL_VALUE_RE + +if TYPE_CHECKING: + from opensandbox_server.config import AuthzConfig + +AuthRole = Literal["read_only", "operator", "service_admin"] +PrincipalSource = Literal["api_key", "user"] + + +@dataclass(frozen=True, slots=True) +class Principal: + """ + Runtime identity for authorization. ``service_admin`` (API key) bypasses owner/team scope. + """ + + source: PrincipalSource + subject: str + role: AuthRole + canonical_owner: str + canonical_team: Optional[str] = None + """When ``None`` (no team in trusted headers), only ``access.owner`` is enforced for scope.""" + + @property + def is_service_admin(self) -> bool: + return self.role == "service_admin" + + +def canonicalize_scoped_value(raw: str) -> str: + """ + Map an arbitrary string to a stable Kubernetes label value (≤63 chars) for metadata scope keys. + + Deterministic: the same input always maps to the same output. If the value is already + a valid label value, it is returned unchanged. + """ + s = (raw or "").strip() + if s == "": + return "" + if len(s) <= 63 and _is_valid_label_value(s): + return s + digest = hashlib.sha256(s.encode("utf-8")).hexdigest()[:32] + return digest + + +def _is_valid_label_value(value: str) -> bool: + if len(value) > 63: + return False + return bool(LABEL_VALUE_RE.match(value)) + + +def resolve_effective_role( + raw_subject: str, + roles_header_value: Optional[str], + authz: "AuthzConfig", +) -> AuthRole: + """ + Derive the effective role from static subject lists, then the roles header, then default. + """ + if raw_subject in authz.operator_subjects: + return "operator" + if raw_subject in authz.read_only_subjects: + return "read_only" + if roles_header_value: + parts = {p.strip().lower() for p in roles_header_value.split(",") if p.strip()} + if "operator" in parts or "op" in parts: + return "operator" + if "read_only" in parts or "readonly" in parts or "read-only" in parts: + return "read_only" + d = (authz.default_role or "read_only").lower() + if d == "operator": + return "operator" + return "read_only" + + +def principal_for_api_key() -> Principal: + return Principal( + source="api_key", + subject="api-key", + role="service_admin", + canonical_owner="", + canonical_team=None, + ) + + +def build_user_principal( + raw_subject: str, + raw_team: Optional[str], + roles_header: Optional[str], + authz: "AuthzConfig", +) -> Principal: + if not (raw_subject or "").strip(): + raise ValueError("raw_subject is required for user principal") + subj = (raw_subject or "").strip() + team_raw = (raw_team or "").strip() or None + role = resolve_effective_role(subj, roles_header, authz) + owner = canonicalize_scoped_value(subj) + if not owner: + raise ValueError("invalid subject after canonicalization") + team = canonicalize_scoped_value(team_raw) if team_raw else None + return Principal( + source="user", + subject=subj, + role=role, + canonical_owner=owner, + canonical_team=team, + ) diff --git a/server/tests/test_auth_trusted_header.py b/server/tests/test_auth_trusted_header.py new file mode 100644 index 000000000..7670a3436 --- /dev/null +++ b/server/tests/test_auth_trusted_header.py @@ -0,0 +1,129 @@ +# Copyright 2025 Alibaba Group Holding Ltd. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. + +from fastapi import FastAPI, Request +from fastapi.testclient import TestClient + +from opensandbox_server.config import ( + AUTH_MODE_API_KEY_AND_USER, + AppConfig, + AuthConfig, + AuthzConfig, + IngressConfig, + RuntimeConfig, + ServerConfig, + TrustedHeaderConfig, +) +from opensandbox_server.middleware.auth import AuthMiddleware + + +def _app_dual_auth() -> AppConfig: + return AppConfig( + server=ServerConfig(api_key="api-secret"), + auth=AuthConfig( + mode=AUTH_MODE_API_KEY_AND_USER, + trusted_header=TrustedHeaderConfig( + user_header="X-OpenSandbox-User", + team_header="X-OpenSandbox-Team", + roles_header="X-OpenSandbox-Roles", + ), + ), + authz=AuthzConfig(), + runtime=RuntimeConfig(type="docker", execd_image="opensandbox/execd:latest"), + ingress=IngressConfig(mode="direct"), + ) + + +def test_trusted_user_missing_identity_returns_401(): + app = FastAPI() + app.add_middleware(AuthMiddleware, config=_app_dual_auth()) + + @app.get("/secured") + def secured(): + return {"ok": True} + + c = TestClient(app) + r = c.get("/secured") + assert r.status_code == 401 + assert r.json()["code"] == "MISSING_TRUSTED_IDENTITY" + + +def test_trusted_user_with_user_and_roles_succeeds(): + app = FastAPI() + app.add_middleware(AuthMiddleware, config=_app_dual_auth()) + + @app.get("/who") + def who(request: Request): + p = getattr(request.state, "principal", None) + return {"subject": p.subject, "role": p.role if p else None} + + c = TestClient(app) + r = c.get( + "/who", + headers={ + "X-OpenSandbox-User": "dev-user", + "X-OpenSandbox-Roles": "read_only", + }, + ) + assert r.status_code == 200 + assert r.json()["subject"] == "dev-user" + assert r.json()["role"] == "read_only" + + +def test_trusted_user_with_only_user_header_uses_default_role(): + app = FastAPI() + app.add_middleware(AuthMiddleware, config=_app_dual_auth()) + + @app.get("/who") + def who(request: Request): + p = getattr(request.state, "principal", None) + return {"subject": p.subject, "role": p.role if p else None} + + c = TestClient(app) + r = c.get( + "/who", + headers={ + "X-OpenSandbox-User": "dev-user", + }, + ) + assert r.status_code == 200 + assert r.json()["subject"] == "dev-user" + assert r.json()["role"] == "read_only" + + +def test_valid_api_key_grants_service_admin(): + app = FastAPI() + app.add_middleware(AuthMiddleware, config=_app_dual_auth()) + + @app.get("/role") + def role(request: Request): + p = getattr(request.state, "principal", None) + return {"admin": bool(p and p.is_service_admin)} + + c = TestClient(app) + r = c.get("/role", headers={"OPEN-SANDBOX-API-KEY": "api-secret"}) + assert r.status_code == 200 + assert r.json() == {"admin": True} + + +def test_api_key_mismatch_still_401_with_valid_user_headers(): + app = FastAPI() + app.add_middleware(AuthMiddleware, config=_app_dual_auth()) + + @app.get("/x") + def x(): + return {"n": 1} + + c = TestClient(app) + r = c.get( + "/x", + headers={ + "OPEN-SANDBOX-API-KEY": "wrong", + "X-OpenSandbox-User": "u", + "X-OpenSandbox-Roles": "operator", + }, + ) + assert r.status_code == 401 + assert r.json()["code"] == "INVALID_API_KEY" diff --git a/server/tests/test_authorization.py b/server/tests/test_authorization.py new file mode 100644 index 000000000..3d79f7d2d --- /dev/null +++ b/server/tests/test_authorization.py @@ -0,0 +1,83 @@ +# Copyright 2025 Alibaba Group Holding Ltd. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. + +import pytest +from fastapi import status +from fastapi.exceptions import HTTPException + +from opensandbox_server.api.schema import ImageSpec, Sandbox, SandboxStatus +from opensandbox_server.config import AuthzConfig +from opensandbox_server.middleware.authorization import ( + LifecycleAction, + authorize_action, + sandbox_in_scope, +) +from opensandbox_server.middleware.principal import build_user_principal, principal_for_api_key + + +def _box(owner: str, team: str | None = "t1") -> Sandbox: + from datetime import datetime, timedelta, timezone + + now = datetime.now(timezone.utc) + meta = {"access.owner": owner} + if team is not None: + meta["access.team"] = team + return Sandbox( + id="s1", + image=ImageSpec(uri="x"), + status=SandboxStatus(state="Running"), + metadata=meta, + entrypoint=["sh"], + expiresAt=now + timedelta(hours=1), + createdAt=now, + ) + + +def test_service_admin_bypasses_scope(): + p = principal_for_api_key() + z = AuthzConfig() + assert sandbox_in_scope(p, _box("other"), z.owner_metadata_key, z.team_metadata_key) + + +def test_user_in_scope_owner_and_team(): + z = AuthzConfig() + p = build_user_principal("u1", "t1", "read_only", z) + assert sandbox_in_scope(p, _box(p.canonical_owner, "t1"), z.owner_metadata_key, z.team_metadata_key) + assert not sandbox_in_scope( + p, _box("someone-else", "t1"), z.owner_metadata_key, z.team_metadata_key + ) + + +def test_read_only_cannot_create(): + z = AuthzConfig() + p = build_user_principal("u1", "t1", "read_only", z) + with pytest.raises(HTTPException) as ei: + authorize_action( + p, + LifecycleAction.CREATE, + owner_key=z.owner_metadata_key, + team_key=z.team_metadata_key, + ) + assert ei.value.status_code == status.HTTP_403_FORBIDDEN + + +def test_operator_can_create(): + z = AuthzConfig() + p = build_user_principal("u1", "t1", "operator", z) + authorize_action( + p, + LifecycleAction.CREATE, + owner_key=z.owner_metadata_key, + team_key=z.team_metadata_key, + ) + + +def test_none_principal_allows_mutation_for_legacy_dev(): + authorize_action( + None, + LifecycleAction.CREATE, + owner_key="access.owner", + team_key="access.team", + ) diff --git a/server/tests/test_config.py b/server/tests/test_config.py index e45821670..bd8d085d2 100644 --- a/server/tests/test_config.py +++ b/server/tests/test_config.py @@ -1357,3 +1357,19 @@ def test_secure_access_active_key_mismatch(self, tmp_path, monkeypatch) -> None: with pytest.raises(ValidationError, match="not found in secure_access.keys"): config_module.load_config(config_path) +def test_console_mount_path_rejects_reserved_prefixes(): + server_cfg = ServerConfig() + runtime_cfg = RuntimeConfig(type="docker", execd_image="busybox:latest") + for bad in ("/v1", "/v1/", "/v1/admin", "/sandboxes", "/"): + with pytest.raises(ValueError): + AppConfig(server=server_cfg, runtime=runtime_cfg, console={"mount_path": bad}) + # /v1ui has no path conflict — should be accepted + cfg = AppConfig(server=server_cfg, runtime=runtime_cfg, console={"mount_path": "/v1ui"}) + assert cfg.console.mount_path == "/v1ui" + + +def test_console_mount_path_allows_custom_non_api_path(): + server_cfg = ServerConfig() + runtime_cfg = RuntimeConfig(type="docker", execd_image="busybox:latest") + app_cfg = AppConfig(server=server_cfg, runtime=runtime_cfg, console={"mount_path": "/console-ui"}) + assert app_cfg.console.mount_path == "/console-ui" diff --git a/server/tests/test_helpers.py b/server/tests/test_helpers.py index a312b4cc9..ec8dcc9d8 100644 --- a/server/tests/test_helpers.py +++ b/server/tests/test_helpers.py @@ -1,48 +1,63 @@ -# Copyright 2025 Alibaba Group Holding Ltd. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from datetime import datetime, timezone - -from opensandbox_server.services.helpers import parse_timestamp - - -def test_parse_timestamp_truncates_nanoseconds(): - ts = "2025-12-10T05:29:56.359015208Z" - - result = parse_timestamp(ts) - - assert result.tzinfo is not None - assert result.astimezone(timezone.utc) == result - assert result.year == 2025 - assert result.month == 12 - assert result.day == 10 - assert result.microsecond == 359015 - - -def test_parse_timestamp_parses_valid_rfc3339(): - ts = "2024-01-01T12:34:56.123456Z" - - result = parse_timestamp(ts) - - assert result.tzinfo is not None - assert result == datetime(2024, 1, 1, 12, 34, 56, 123456, tzinfo=timezone.utc) - - -def test_parse_timestamp_invalid_falls_back_to_now(): - before = datetime.now(timezone.utc) - result = parse_timestamp("not-a-time") - after = datetime.now(timezone.utc) - - assert result.tzinfo is not None - assert before <= result <= after +# Copyright 2025 Alibaba Group Holding Ltd. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from datetime import datetime, timedelta, timezone + +from opensandbox_server.api.schema import ImageSpec, Sandbox, SandboxStatus +from opensandbox_server.services.helpers import parse_timestamp + + +def minimal_sandbox(sandbox_id: str = "sbx-001") -> Sandbox: + """Minimal valid Sandbox for stubbing ``get_sandbox`` in route tests.""" + now = datetime.now(timezone.utc) + return Sandbox( + id=sandbox_id, + image=ImageSpec(uri="test:latest"), + status=SandboxStatus(state="Running"), + metadata={}, + entrypoint=["sh"], + expiresAt=now + timedelta(hours=1), + createdAt=now, + ) + + +def test_parse_timestamp_truncates_nanoseconds(): + ts = "2025-12-10T05:29:56.359015208Z" + + result = parse_timestamp(ts) + + assert result.tzinfo is not None + assert result.astimezone(timezone.utc) == result + assert result.year == 2025 + assert result.month == 12 + assert result.day == 10 + assert result.microsecond == 359015 + + +def test_parse_timestamp_parses_valid_rfc3339(): + ts = "2024-01-01T12:34:56.123456Z" + + result = parse_timestamp(ts) + + assert result.tzinfo is not None + assert result == datetime(2024, 1, 1, 12, 34, 56, 123456, tzinfo=timezone.utc) + + +def test_parse_timestamp_invalid_falls_back_to_now(): + before = datetime.now(timezone.utc) + result = parse_timestamp("not-a-time") + after = datetime.now(timezone.utc) + + assert result.tzinfo is not None + assert before <= result <= after diff --git a/server/tests/test_lifecycle_helpers.py b/server/tests/test_lifecycle_helpers.py new file mode 100644 index 000000000..c915ddf51 --- /dev/null +++ b/server/tests/test_lifecycle_helpers.py @@ -0,0 +1,84 @@ +# Copyright 2025 Alibaba Group Holding Ltd. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. + +from types import SimpleNamespace +from unittest.mock import MagicMock + +import pytest +from fastapi import status +from fastapi.exceptions import HTTPException + +from opensandbox_server.api.lifecycle_helpers import authorize_mutating_action, merge_list_scope_from_request +from opensandbox_server.api.schema import ListSandboxesRequest, PaginationRequest, SandboxFilter +from opensandbox_server.config import AppConfig, AuthzConfig, IngressConfig, RuntimeConfig, ServerConfig +from opensandbox_server.middleware.authorization import LifecycleAction +from opensandbox_server.middleware.principal import build_user_principal + + +def _min_config() -> AppConfig: + return AppConfig( + server=ServerConfig(), + authz=AuthzConfig( + owner_metadata_key="access.owner", + team_metadata_key="access.team", + ), + runtime=RuntimeConfig(type="docker", execd_image="x"), + ingress=IngressConfig(mode="direct"), + ) + + +def _make_request(principal=None) -> MagicMock: + req = MagicMock() + req.state = SimpleNamespace(principal=principal) + req.headers = {} + return req + + +def test_authorize_mutating_action_logs_and_reraises_on_403(): + from unittest.mock import patch + + z = _min_config() + p = build_user_principal("u1", None, "read_only", z.authz) + req = _make_request(p) + with patch("opensandbox_server.api.lifecycle_helpers.logger") as mock_log: + with pytest.raises(HTTPException) as ei: + authorize_mutating_action( + req, p, LifecycleAction.CREATE, + owner_key=z.authz.owner_metadata_key, + team_key=z.authz.team_metadata_key, + sandbox_id=None, + ) + assert ei.value.status_code == status.HTTP_403_FORBIDDEN + mock_log.info.assert_called_once() + logged_msg = str(mock_log.info.call_args) + assert "forbidden" in logged_msg + + +def test_authorize_mutating_action_passes_for_operator(): + z = _min_config() + p = build_user_principal("u1", None, "operator", z.authz) + req = _make_request(p) + # Should not raise + authorize_mutating_action( + req, p, LifecycleAction.CREATE, + owner_key=z.authz.owner_metadata_key, + team_key=z.authz.team_metadata_key, + ) + + +def test_merge_list_scope_injects_owner_for_user(): + z = _min_config() + p = build_user_principal("alice", "t1", "read_only", z.authz) + list_req = ListSandboxesRequest( + filter=SandboxFilter(state=None, metadata={"k": "v"}), + pagination=PaginationRequest(page=1, pageSize=20), + ) + http_request = MagicMock() + http_request.state = SimpleNamespace(principal=p) + out = merge_list_scope_from_request(http_request, list_req, z) + assert out.filter.metadata + assert out.filter.metadata.get("k") == "v" + assert out.filter.metadata.get("access.owner") == p.canonical_owner + assert out.filter.metadata.get("access.team") == p.canonical_team diff --git a/server/tests/test_principal.py b/server/tests/test_principal.py new file mode 100644 index 000000000..b5eecbf24 --- /dev/null +++ b/server/tests/test_principal.py @@ -0,0 +1,63 @@ +# Copyright 2025 Alibaba Group Holding Ltd. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. + +from opensandbox_server.config import AuthzConfig +from opensandbox_server.middleware.principal import ( + build_user_principal, + canonicalize_scoped_value, + principal_for_api_key, + resolve_effective_role, +) + + +def test_canonicalize_scoped_value_passes_valid_label_unchanged(): + assert canonicalize_scoped_value("ab") == "ab" + assert canonicalize_scoped_value("My-Team_1") == "My-Team_1" + + +def test_canonicalize_scoped_value_deterministic_for_long_or_invalid(): + a = canonicalize_scoped_value("not a valid label value because it has spaces") + b = canonicalize_scoped_value("not a valid label value because it has spaces") + assert a == b + assert len(a) <= 63 + a2 = canonicalize_scoped_value("a" * 200) + b2 = canonicalize_scoped_value("a" * 200) + assert a2 == b2 + + +def test_principal_for_api_key_is_service_admin(): + p = principal_for_api_key() + assert p.is_service_admin + assert p.role == "service_admin" + + +def test_resolve_effective_role_default_read_only(): + z = AuthzConfig() + assert resolve_effective_role("anyone", None, z) == "read_only" + + +def test_resolve_effective_role_operator_subjects(): + z = AuthzConfig(operator_subjects=["alice"], default_role="read_only") + assert resolve_effective_role("alice", None, z) == "operator" + + +def test_resolve_effective_role_roles_header_operator(): + z = AuthzConfig() + assert resolve_effective_role("u", "read_only, operator", z) == "operator" + + +def test_build_user_principal_injects_scope_and_respects_role(): + z = AuthzConfig() + p = build_user_principal("Alice", "t1", "operator", z) + assert p.role == "operator" + assert p.canonical_owner + assert p.canonical_team == "t1" + + +def test_build_user_principal_team_optional(): + z = AuthzConfig() + p = build_user_principal("Bob", None, "read_only", z) + assert p.canonical_team is None + assert p.canonical_owner == "Bob" diff --git a/server/tests/test_routes_authorization.py b/server/tests/test_routes_authorization.py new file mode 100644 index 000000000..aa2863891 --- /dev/null +++ b/server/tests/test_routes_authorization.py @@ -0,0 +1,180 @@ +# Copyright 2025 Alibaba Group Holding Ltd. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. + +from datetime import datetime, timedelta, timezone + +from fastapi import FastAPI +from fastapi.testclient import TestClient + +from opensandbox_server.api import lifecycle +from opensandbox_server.api.schema import ( + CreateSandboxResponse, + Endpoint, + ImageSpec, + ListSandboxesResponse, + PaginationInfo, + RenewSandboxExpirationResponse, + Sandbox, + SandboxStatus, +) +from opensandbox_server.config import ( + AUTH_MODE_API_KEY_AND_USER, + AppConfig, + AuthConfig, + AuthzConfig, + IngressConfig, + RuntimeConfig, + ServerConfig, +) +from opensandbox_server.middleware.auth import AuthMiddleware + + +def _cfg() -> AppConfig: + return AppConfig( + server=ServerConfig(api_key="api-secret"), + auth=AuthConfig(mode=AUTH_MODE_API_KEY_AND_USER), + authz=AuthzConfig(default_role="read_only"), + runtime=RuntimeConfig(type="docker", execd_image="opensandbox/execd:latest"), + ingress=IngressConfig(mode="direct"), + ) + + +def _sandbox(owner: str) -> Sandbox: + now = datetime.now(timezone.utc) + return Sandbox( + id="sbx-1", + image=ImageSpec(uri="python:3.11"), + status=SandboxStatus(state="Running"), + metadata={"access.owner": owner}, + entrypoint=["python", "-V"], + expiresAt=now + timedelta(hours=1), + createdAt=now, + ) + + +def _build_app(monkeypatch, service_obj) -> TestClient: + cfg = _cfg() + app = FastAPI() + app.add_middleware(AuthMiddleware, config=cfg) + app.include_router(lifecycle.router, prefix="/v1") + monkeypatch.setattr(lifecycle, "sandbox_service", service_obj) + monkeypatch.setattr(lifecycle, "get_config", lambda: cfg) + return TestClient(app) + + +def _user_headers(role: str) -> dict[str, str]: + return { + "X-OpenSandbox-User": "alice", + "X-OpenSandbox-Roles": role, + } + + +def test_read_only_can_list_get_and_endpoint_but_cannot_mutate(monkeypatch) -> None: + owner = "alice" + + class StubService: + @staticmethod + def list_sandboxes(_request) -> ListSandboxesResponse: + return ListSandboxesResponse( + items=[_sandbox(owner)], + pagination=PaginationInfo( + page=1, + pageSize=20, + totalItems=1, + totalPages=1, + hasNextPage=False, + ), + ) + + @staticmethod + def get_sandbox(_sandbox_id: str) -> Sandbox: + return _sandbox(owner) + + @staticmethod + def get_endpoint(_sandbox_id: str, _port: int, resolve_internal: bool = False, expires=None) -> Endpoint: + return Endpoint(endpoint="127.0.0.1:18080") + + c = _build_app(monkeypatch, StubService()) + + r_list = c.get("/v1/sandboxes", headers=_user_headers("read_only")) + assert r_list.status_code == 200 + r_get = c.get("/v1/sandboxes/sbx-1", headers=_user_headers("read_only")) + assert r_get.status_code == 200 + r_ep = c.get("/v1/sandboxes/sbx-1/endpoints/8080", headers=_user_headers("read_only")) + assert r_ep.status_code == 200 + + r_create = c.post( + "/v1/sandboxes", + headers=_user_headers("read_only"), + json={ + "image": {"uri": "python:3.11"}, + "timeout": 3600, + "resourceLimits": {"cpu": "500m", "memory": "512Mi"}, + "entrypoint": ["python", "-V"], + }, + ) + assert r_create.status_code == 403 + payload = r_create.json() + code = payload.get("code") + if code is None and isinstance(payload.get("detail"), dict): + code = payload["detail"].get("code") + assert code == "INSUFFICIENT_ROLE" + + +def test_operator_can_mutate(monkeypatch) -> None: + owner = "alice" + + class StubService: + @staticmethod + def get_sandbox(_sandbox_id: str) -> Sandbox: + return _sandbox(owner) + + @staticmethod + async def create_sandbox(_request) -> CreateSandboxResponse: + now = datetime.now(timezone.utc) + return CreateSandboxResponse( + id="sbx-2", + status=SandboxStatus(state="Pending"), + metadata={"access.owner": owner}, + expiresAt=now + timedelta(hours=1), + createdAt=now, + entrypoint=["python", "-V"], + ) + + @staticmethod + def delete_sandbox(_sandbox_id: str) -> None: + return None + + @staticmethod + def renew_expiration(_sandbox_id: str, _request) -> RenewSandboxExpirationResponse: + return RenewSandboxExpirationResponse(expiresAt=datetime.now(timezone.utc) + timedelta(hours=2)) + + @staticmethod + def pause_sandbox(_sandbox_id: str) -> None: + return None + + @staticmethod + def resume_sandbox(_sandbox_id: str) -> None: + return None + + c = _build_app(monkeypatch, StubService()) + h = _user_headers("operator") + + r_create = c.post( + "/v1/sandboxes", + headers=h, + json={ + "image": {"uri": "python:3.11"}, + "timeout": 3600, + "resourceLimits": {"cpu": "500m", "memory": "512Mi"}, + "entrypoint": ["python", "-V"], + }, + ) + assert r_create.status_code == 202 + + assert c.post("/v1/sandboxes/sbx-1/renew-expiration", headers=h, json={"expiresAt": "2030-01-01T00:00:00Z"}).status_code == 200 + assert c.post("/v1/sandboxes/sbx-1/pause", headers=h).status_code == 202 + assert c.post("/v1/sandboxes/sbx-1/resume", headers=h).status_code == 202 + assert c.delete("/v1/sandboxes/sbx-1", headers=h).status_code == 204 diff --git a/server/tests/test_routes_create_delete.py b/server/tests/test_routes_create_delete.py index 57c946f10..91d1a9628 100644 --- a/server/tests/test_routes_create_delete.py +++ b/server/tests/test_routes_create_delete.py @@ -1,214 +1,126 @@ -# Copyright 2025 Alibaba Group Holding Ltd. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from datetime import datetime, timedelta, timezone - -from fastapi.testclient import TestClient - -from opensandbox_server.api import lifecycle -from opensandbox_server.api.schema import CreateSandboxResponse, SandboxStatus - - -def test_create_sandbox_returns_202_and_service_payload( - client: TestClient, - auth_headers: dict, - sample_sandbox_request: dict, - monkeypatch, -) -> None: - now = datetime.now(timezone.utc) - calls: list[object] = [] - - class StubService: - @staticmethod - async def create_sandbox(request) -> CreateSandboxResponse: - calls.append(request) - return CreateSandboxResponse( - id="sbx-001", - status=SandboxStatus(state="Pending"), - metadata={"project": "test-project"}, - expiresAt=now + timedelta(hours=1), - createdAt=now, - entrypoint=["python", "-c", "print('Hello from sandbox')"], - ) - - monkeypatch.setattr(lifecycle, "sandbox_service", StubService()) - - response = client.post( - "/v1/sandboxes", - headers=auth_headers, - json=sample_sandbox_request, - ) - - assert response.status_code == 202 - payload = response.json() - assert payload["id"] == "sbx-001" - assert payload["status"]["state"] == "Pending" - assert payload["metadata"]["project"] == "test-project" - assert payload["entrypoint"] == ["python", "-c", "print('Hello from sandbox')"] - assert len(calls) == 1 - assert calls[0].image.uri == "python:3.11" - - -def test_create_sandbox_manual_cleanup_omits_none_fields( - client: TestClient, - auth_headers: dict, - sample_sandbox_request: dict, - monkeypatch, -) -> None: - now = datetime.now(timezone.utc) - - class StubService: - @staticmethod - async def create_sandbox(request) -> CreateSandboxResponse: - return CreateSandboxResponse( - id="sbx-manual", - status=SandboxStatus(state="Pending"), - metadata=None, - expiresAt=None, - createdAt=now, - entrypoint=["python", "-c", "print('Hello from sandbox')"], - ) - - monkeypatch.setattr(lifecycle, "sandbox_service", StubService()) - sample_sandbox_request.pop("timeout", None) - - response = client.post( - "/v1/sandboxes", - headers=auth_headers, - json=sample_sandbox_request, - ) - - assert response.status_code == 202 - payload = response.json() - assert "expiresAt" not in payload - assert "metadata" not in payload - assert "reason" not in payload["status"] - assert "message" not in payload["status"] - assert "lastTransitionAt" not in payload["status"] - - -def test_create_sandbox_rejects_invalid_request( - client: TestClient, - auth_headers: dict, -) -> None: - response = client.post( - "/v1/sandboxes", - headers=auth_headers, - json={"timeout": 10}, - ) - - assert response.status_code == 422 - - -def test_create_sandbox_accepts_snapshot_id_without_entrypoint( - client: TestClient, - auth_headers: dict, - monkeypatch, -) -> None: - now = datetime.now(timezone.utc) - calls: list[object] = [] - - class StubService: - @staticmethod - async def create_sandbox(request) -> CreateSandboxResponse: - calls.append(request) - return CreateSandboxResponse( - id="sbx-from-snapshot", - status=SandboxStatus(state="Pending"), - metadata=None, - expiresAt=now + timedelta(hours=1), - createdAt=now, - entrypoint=None, - ) - - monkeypatch.setattr(lifecycle, "sandbox_service", StubService()) - - response = client.post( - "/v1/sandboxes", - headers=auth_headers, - json={ - "snapshotId": "snap-001", - "resourceLimits": {"cpu": "500m", "memory": "512Mi"}, - }, - ) - - assert response.status_code == 202 - assert calls[0].snapshot_id == "snap-001" - assert calls[0].entrypoint is None - - -def test_create_sandbox_accepts_snapshot_id_with_entrypoint( - client: TestClient, - auth_headers: dict, - monkeypatch, -) -> None: - now = datetime.now(timezone.utc) - calls: list[object] = [] - - class StubService: - @staticmethod - async def create_sandbox(request) -> CreateSandboxResponse: - calls.append(request) - return CreateSandboxResponse( - id="sbx-from-snapshot", - status=SandboxStatus(state="Pending"), - metadata=None, - expiresAt=now + timedelta(hours=1), - createdAt=now, - entrypoint=["python", "app.py"], - ) - - monkeypatch.setattr(lifecycle, "sandbox_service", StubService()) - - response = client.post( - "/v1/sandboxes", - headers=auth_headers, - json={ - "snapshotId": "snap-001", - "resourceLimits": {"cpu": "500m", "memory": "512Mi"}, - "entrypoint": ["python", "app.py"], - }, - ) - - assert response.status_code == 202 - assert calls[0].snapshot_id == "snap-001" - assert calls[0].entrypoint == ["python", "app.py"] - - -def test_delete_sandbox_returns_204_and_calls_service( - client: TestClient, - auth_headers: dict, - monkeypatch, -) -> None: - calls: list[str] = [] - - class StubService: - @staticmethod - def delete_sandbox(sandbox_id: str) -> None: - calls.append(sandbox_id) - - monkeypatch.setattr(lifecycle, "sandbox_service", StubService()) - - response = client.delete("/v1/sandboxes/sbx-001", headers=auth_headers) - - assert response.status_code == 204 - assert response.text == "" - assert calls == ["sbx-001"] - - -def test_delete_sandbox_requires_api_key(client: TestClient) -> None: - response = client.delete("/v1/sandboxes/sbx-001") - - assert response.status_code == 401 - assert response.json()["code"] == "MISSING_API_KEY" +# Copyright 2025 Alibaba Group Holding Ltd. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from datetime import datetime, timedelta, timezone + +from fastapi.testclient import TestClient + +from opensandbox_server.api import lifecycle +from opensandbox_server.api.schema import CreateSandboxResponse, SandboxStatus +from opensandbox_server.services.constants import SandboxErrorCodes +from tests.test_helpers import minimal_sandbox + + +def test_create_sandbox_returns_202_and_service_payload( + client: TestClient, + auth_headers: dict, + sample_sandbox_request: dict, + monkeypatch, +) -> None: + now = datetime.now(timezone.utc) + calls: list[object] = [] + + class StubService: + @staticmethod + async def create_sandbox(request) -> CreateSandboxResponse: + calls.append(request) + return CreateSandboxResponse( + id="sbx-001", + status=SandboxStatus(state="Pending"), + metadata={"project": "test-project"}, + expiresAt=now + timedelta(hours=1), + createdAt=now, + entrypoint=["python", "-c", "print('Hello from sandbox')"], + ) + + monkeypatch.setattr(lifecycle, "sandbox_service", StubService()) + + response = client.post( + "/v1/sandboxes", + headers=auth_headers, + json=sample_sandbox_request, + ) + + assert response.status_code == 202 + payload = response.json() + assert payload["id"] == "sbx-001" + assert payload["status"]["state"] == "Pending" + assert payload["metadata"]["project"] == "test-project" + assert payload["entrypoint"] == ["python", "-c", "print('Hello from sandbox')"] + assert len(calls) == 1 + assert calls[0].image.uri == "python:3.11" + + +def test_create_sandbox_rejects_invalid_extensions( + client: TestClient, + auth_headers: dict, + sample_sandbox_request: dict, +) -> None: + payload = { + **sample_sandbox_request, + "extensions": {"access.renew.extend.seconds": "not-an-int"}, + } + response = client.post("/v1/sandboxes", headers=auth_headers, json=payload) + + assert response.status_code == 400 + payload = response.json() + code = payload.get("code") + if code is None and isinstance(payload.get("detail"), dict): + code = payload["detail"].get("code") + assert code == SandboxErrorCodes.INVALID_PARAMETER + + +def test_create_sandbox_rejects_invalid_request( + client: TestClient, + auth_headers: dict, +) -> None: + response = client.post( + "/v1/sandboxes", + headers=auth_headers, + json={"timeout": 10}, + ) + + assert response.status_code == 422 + + +def test_delete_sandbox_returns_204_and_calls_service( + client: TestClient, + auth_headers: dict, + monkeypatch, +) -> None: + calls: list[str] = [] + + class StubService: + @staticmethod + def get_sandbox(sandbox_id: str): + return minimal_sandbox(sandbox_id) + + @staticmethod + def delete_sandbox(sandbox_id: str) -> None: + calls.append(sandbox_id) + + monkeypatch.setattr(lifecycle, "sandbox_service", StubService()) + + response = client.delete("/v1/sandboxes/sbx-001", headers=auth_headers) + + assert response.status_code == 204 + assert response.text == "" + assert calls == ["sbx-001"] + + +def test_delete_sandbox_requires_api_key(client: TestClient) -> None: + response = client.delete("/v1/sandboxes/sbx-001") + + assert response.status_code == 401 + assert response.json()["code"] == "MISSING_API_KEY" diff --git a/server/tests/test_routes_endpoint_behavior.py b/server/tests/test_routes_endpoint_behavior.py index 6d71c6853..983384b71 100644 --- a/server/tests/test_routes_endpoint_behavior.py +++ b/server/tests/test_routes_endpoint_behavior.py @@ -1,155 +1,86 @@ -# Copyright 2025 Alibaba Group Holding Ltd. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from types import SimpleNamespace - -from fastapi.testclient import TestClient - -from opensandbox_server.api import lifecycle -from opensandbox_server.api.schema import Endpoint - - -def test_get_endpoint_returns_service_result( - client: TestClient, - auth_headers: dict, - monkeypatch, -) -> None: - calls: list[tuple[str, int]] = [] - - class StubService: - @staticmethod - def get_endpoint(sandbox_id: str, port: int, **kwargs) -> Endpoint: - calls.append((sandbox_id, port)) - return Endpoint(endpoint="10.57.1.91:40109/proxy/44772") - - monkeypatch.setattr(lifecycle, "sandbox_service", StubService()) - - response = client.get( - "/v1/sandboxes/sbx-001/endpoints/44772", - headers=auth_headers, - ) - - assert response.status_code == 200 - assert response.json()["endpoint"] == "10.57.1.91:40109/proxy/44772" - assert calls == [("sbx-001", 44772)] - - -def test_get_endpoint_use_server_proxy_rewrites_url( - client: TestClient, - auth_headers: dict, - monkeypatch, -) -> None: - class StubService: - @staticmethod - def get_endpoint(sandbox_id: str, port: int, **kwargs) -> Endpoint: - return Endpoint(endpoint="10.57.1.91:40109/proxy/44772") - - monkeypatch.setattr(lifecycle, "sandbox_service", StubService()) - - response = client.get( - "/v1/sandboxes/sbx-001/endpoints/44772", - params={"use_server_proxy": "true"}, - headers=auth_headers, - ) - - assert response.status_code == 200 - assert response.json()["endpoint"] == "testserver/sandboxes/sbx-001/proxy/44772" - - -def test_get_endpoint_use_server_proxy_prefers_server_eip( - client: TestClient, - auth_headers: dict, - monkeypatch, -) -> None: - class StubService: - @staticmethod - def get_endpoint(sandbox_id: str, port: int, **kwargs) -> Endpoint: - return Endpoint(endpoint="10.57.1.91:40109/proxy/44772") - - monkeypatch.setattr(lifecycle, "sandbox_service", StubService()) - monkeypatch.setattr( - lifecycle, - "get_config", - lambda: SimpleNamespace(server=SimpleNamespace(eip="sandbox.example.com/opensandbox/")), - ) - - response = client.get( - "/v1/sandboxes/sbx-001/endpoints/44772", - params={"use_server_proxy": "true"}, - headers=auth_headers, - ) - - assert response.status_code == 200 - assert response.json()["endpoint"] == "sandbox.example.com/opensandbox/sandboxes/sbx-001/proxy/44772" - - -def test_get_endpoint_rejects_non_numeric_port( - client: TestClient, - auth_headers: dict, -) -> None: - response = client.get( - "/v1/sandboxes/sbx-001/endpoints/not-a-port", - headers=auth_headers, - ) - - assert response.status_code == 422 - - -def test_get_endpoint_passes_expires_to_service( - client: TestClient, - auth_headers: dict, - monkeypatch, -) -> None: - captured: dict = {} - - class StubService: - @staticmethod - def get_endpoint(sandbox_id: str, port: int, **kwargs) -> Endpoint: - captured.update({"sandbox_id": sandbox_id, "port": port, **kwargs}) - return Endpoint(endpoint="sandbox.example.com") - - monkeypatch.setattr(lifecycle, "sandbox_service", StubService()) - - response = client.get( - "/v1/sandboxes/sbx-001/endpoints/44772", - params={"expires": "2000000000"}, - headers=auth_headers, - ) - - assert response.status_code == 200 - assert captured.get("expires") == 2000000000 - - -def test_get_endpoint_unsigned_when_expires_omitted( - client: TestClient, - auth_headers: dict, - monkeypatch, -) -> None: - captured: dict = {} - - class StubService: - @staticmethod - def get_endpoint(sandbox_id: str, port: int, **kwargs) -> Endpoint: - captured.update(kwargs) - return Endpoint(endpoint="sandbox.example.com") - - monkeypatch.setattr(lifecycle, "sandbox_service", StubService()) - - response = client.get( - "/v1/sandboxes/sbx-001/endpoints/44772", - headers=auth_headers, - ) - - assert response.status_code == 200 - assert captured.get("expires") is None +# Copyright 2025 Alibaba Group Holding Ltd. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from fastapi.testclient import TestClient + +from opensandbox_server.api import lifecycle +from opensandbox_server.api.schema import Endpoint +from tests.test_helpers import minimal_sandbox + + +def test_get_endpoint_returns_service_result( + client: TestClient, + auth_headers: dict, + monkeypatch, +) -> None: + calls: list[tuple[str, int]] = [] + + class StubService: + @staticmethod + def get_sandbox(sandbox_id: str): + return minimal_sandbox(sandbox_id) + + @staticmethod + def get_endpoint(sandbox_id: str, port: int, **kwargs) -> Endpoint: + calls.append((sandbox_id, port)) + return Endpoint(endpoint="10.57.1.91:40109/proxy/44772") + + monkeypatch.setattr(lifecycle, "sandbox_service", StubService()) + + response = client.get( + "/v1/sandboxes/sbx-001/endpoints/44772", + headers=auth_headers, + ) + + assert response.status_code == 200 + assert response.json()["endpoint"] == "10.57.1.91:40109/proxy/44772" + assert calls == [("sbx-001", 44772)] + + +def test_get_endpoint_use_server_proxy_rewrites_url( + client: TestClient, + auth_headers: dict, + monkeypatch, +) -> None: + class StubService: + @staticmethod + def get_sandbox(sandbox_id: str): + return minimal_sandbox(sandbox_id) + + @staticmethod + def get_endpoint(sandbox_id: str, port: int, **kwargs) -> Endpoint: + return Endpoint(endpoint="10.57.1.91:40109/proxy/44772") + + monkeypatch.setattr(lifecycle, "sandbox_service", StubService()) + + response = client.get( + "/v1/sandboxes/sbx-001/endpoints/44772", + params={"use_server_proxy": "true"}, + headers=auth_headers, + ) + + assert response.status_code == 200 + assert response.json()["endpoint"] == "testserver/sandboxes/sbx-001/proxy/44772" + + +def test_get_endpoint_rejects_non_numeric_port( + client: TestClient, + auth_headers: dict, +) -> None: + response = client.get( + "/v1/sandboxes/sbx-001/endpoints/not-a-port", + headers=auth_headers, + ) + + assert response.status_code == 422 diff --git a/server/tests/test_routes_pause_resume.py b/server/tests/test_routes_pause_resume.py index db3359359..b5071bdb8 100644 --- a/server/tests/test_routes_pause_resume.py +++ b/server/tests/test_routes_pause_resume.py @@ -1,89 +1,120 @@ -# Copyright 2025 Alibaba Group Holding Ltd. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from fastapi.exceptions import HTTPException -from fastapi.testclient import TestClient - -from opensandbox_server.api import lifecycle - - -def test_pause_route_calls_service_and_returns_202( - client: TestClient, - auth_headers: dict, - monkeypatch, -) -> None: - calls: list[str] = [] - - class StubService: - @staticmethod - def pause_sandbox(sandbox_id: str) -> None: - calls.append(sandbox_id) - - monkeypatch.setattr(lifecycle, "sandbox_service", StubService()) - - response = client.post("/v1/sandboxes/sbx-001/pause", headers=auth_headers) - - assert response.status_code == 202 - assert calls == ["sbx-001"] - - -def test_resume_route_calls_service_and_returns_202( - client: TestClient, - auth_headers: dict, - monkeypatch, -) -> None: - calls: list[str] = [] - - class StubService: - @staticmethod - def resume_sandbox(sandbox_id: str) -> None: - calls.append(sandbox_id) - - monkeypatch.setattr(lifecycle, "sandbox_service", StubService()) - - response = client.post("/v1/sandboxes/sbx-001/resume", headers=auth_headers) - - assert response.status_code == 202 - assert calls == ["sbx-001"] - - -def test_pause_route_propagates_service_http_error( - client: TestClient, - auth_headers: dict, - monkeypatch, -) -> None: - class StubService: - @staticmethod - def pause_sandbox(sandbox_id: str) -> None: - raise HTTPException( - status_code=404, - detail={"code": "SANDBOX_NOT_FOUND", "message": f"Sandbox {sandbox_id} not found"}, - ) - - monkeypatch.setattr(lifecycle, "sandbox_service", StubService()) - - response = client.post("/v1/sandboxes/missing/pause", headers=auth_headers) - - assert response.status_code == 404 - assert response.json() == { - "code": "SANDBOX_NOT_FOUND", - "message": "Sandbox missing not found", - } - - -def test_pause_route_requires_api_key(client: TestClient) -> None: - response = client.post("/v1/sandboxes/sbx-001/pause") - - assert response.status_code == 401 - assert response.json()["code"] == "MISSING_API_KEY" +# Copyright 2025 Alibaba Group Holding Ltd. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from datetime import datetime, timedelta, timezone + +from fastapi import status +from fastapi.exceptions import HTTPException +from fastapi.testclient import TestClient + +from opensandbox_server.api import lifecycle +from opensandbox_server.api.schema import ImageSpec, Sandbox, SandboxStatus +from tests.test_helpers import minimal_sandbox + + +def test_pause_route_calls_service_and_returns_202( + client: TestClient, + auth_headers: dict, + monkeypatch, +) -> None: + calls: list[str] = [] + + class StubService: + @staticmethod + def get_sandbox(sandbox_id: str) -> Sandbox: + return minimal_sandbox(sandbox_id) + + @staticmethod + def pause_sandbox(sandbox_id: str) -> None: + calls.append(sandbox_id) + + monkeypatch.setattr(lifecycle, "sandbox_service", StubService()) + + response = client.post("/v1/sandboxes/sbx-001/pause", headers=auth_headers) + + assert response.status_code == 202 + assert calls == ["sbx-001"] + + +def test_resume_route_calls_service_and_returns_202( + client: TestClient, + auth_headers: dict, + monkeypatch, +) -> None: + calls: list[str] = [] + + class StubService: + @staticmethod + def get_sandbox(sandbox_id: str) -> Sandbox: + return minimal_sandbox(sandbox_id) + + @staticmethod + def resume_sandbox(sandbox_id: str) -> None: + calls.append(sandbox_id) + + monkeypatch.setattr(lifecycle, "sandbox_service", StubService()) + + response = client.post("/v1/sandboxes/sbx-001/resume", headers=auth_headers) + + assert response.status_code == 202 + assert calls == ["sbx-001"] + + +def test_pause_route_propagates_service_http_error( + client: TestClient, + auth_headers: dict, + monkeypatch, +) -> None: + class StubService: + @staticmethod + def get_sandbox(sandbox_id: str) -> Sandbox: + if sandbox_id == "missing": + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail={ + "code": "SANDBOX_NOT_FOUND", + "message": f"Sandbox {sandbox_id} not found", + }, + ) + now = datetime.now(timezone.utc) + return Sandbox( + id=sandbox_id, + image=ImageSpec(uri="t"), + status=SandboxStatus(state="Running"), + metadata={}, + entrypoint=["sh"], + expiresAt=now + timedelta(hours=1), + createdAt=now, + ) + + @staticmethod + def pause_sandbox(sandbox_id: str) -> None: + raise AssertionError("pause_sandbox should not be called when get_sandbox fails") + + monkeypatch.setattr(lifecycle, "sandbox_service", StubService()) + + response = client.post("/v1/sandboxes/missing/pause", headers=auth_headers) + + assert response.status_code == 404 + assert response.json() == { + "code": "SANDBOX_NOT_FOUND", + "message": "Sandbox missing not found", + } + + +def test_pause_route_requires_api_key(client: TestClient) -> None: + response = client.post("/v1/sandboxes/sbx-001/pause") + + assert response.status_code == 401 + assert response.json()["code"] == "MISSING_API_KEY" diff --git a/server/tests/test_routes_renew_expiration.py b/server/tests/test_routes_renew_expiration.py index c8cdbc300..b971ef028 100644 --- a/server/tests/test_routes_renew_expiration.py +++ b/server/tests/test_routes_renew_expiration.py @@ -1,134 +1,112 @@ -# Copyright 2025 Alibaba Group Holding Ltd. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from datetime import datetime, timedelta, timezone - -from fastapi.exceptions import HTTPException -from fastapi.testclient import TestClient - -from opensandbox_server.api import lifecycle -from opensandbox_server.api.schema import RenewSandboxExpirationResponse - - -def test_renew_expiration_returns_updated_timestamp( - client: TestClient, - auth_headers: dict, - monkeypatch, -) -> None: - target = datetime.now(timezone.utc) + timedelta(hours=2) - calls: list[tuple[str, datetime]] = [] - - class StubService: - @staticmethod - def renew_expiration(sandbox_id: str, request) -> RenewSandboxExpirationResponse: - calls.append((sandbox_id, request.expires_at)) - return RenewSandboxExpirationResponse(expiresAt=target) - - monkeypatch.setattr(lifecycle, "sandbox_service", StubService()) - - response = client.post( - "/v1/sandboxes/sbx-001/renew-expiration", - headers=auth_headers, - json={"expiresAt": target.isoformat()}, - ) - - assert response.status_code == 200 - expires_at = datetime.fromisoformat(response.json()["expiresAt"].replace("Z", "+00:00")) - assert expires_at == target - assert calls == [("sbx-001", target)] - - -def test_renew_expiration_rejects_invalid_payload( - client: TestClient, - auth_headers: dict, -) -> None: - response = client.post( - "/v1/sandboxes/sbx-001/renew-expiration", - headers=auth_headers, - json={"expiresAt": "not-a-datetime"}, - ) - - assert response.status_code == 422 - - -def test_renew_expiration_propagates_service_http_error( - client: TestClient, - auth_headers: dict, - monkeypatch, -) -> None: - class StubService: - @staticmethod - def renew_expiration(sandbox_id: str, request) -> RenewSandboxExpirationResponse: - raise HTTPException( - status_code=409, - detail={ - "code": "INVALID_EXPIRES_AT", - "message": f"Requested expiresAt is not valid for sandbox {sandbox_id}", - }, - ) - - monkeypatch.setattr(lifecycle, "sandbox_service", StubService()) - - response = client.post( - "/v1/sandboxes/sbx-001/renew-expiration", - headers=auth_headers, - json={"expiresAt": "2030-01-01T00:00:00Z"}, - ) - - assert response.status_code == 409 - assert response.json() == { - "code": "INVALID_EXPIRES_AT", - "message": "Requested expiresAt is not valid for sandbox sbx-001", - } - - -def test_renew_expiration_returns_409_for_manual_cleanup_sandbox( - client: TestClient, - auth_headers: dict, - monkeypatch, -) -> None: - class StubService: - @staticmethod - def renew_expiration(sandbox_id: str, request) -> RenewSandboxExpirationResponse: - raise HTTPException( - status_code=409, - detail={ - "code": "DOCKER::INVALID_EXPIRATION", - "message": f"Sandbox {sandbox_id} does not have automatic expiration enabled.", - }, - ) - - monkeypatch.setattr(lifecycle, "sandbox_service", StubService()) - - response = client.post( - "/v1/sandboxes/sbx-manual/renew-expiration", - headers=auth_headers, - json={"expiresAt": "2030-01-01T00:00:00Z"}, - ) - - assert response.status_code == 409 - assert response.json() == { - "code": "DOCKER::INVALID_EXPIRATION", - "message": "Sandbox sbx-manual does not have automatic expiration enabled.", - } - - -def test_renew_expiration_requires_api_key(client: TestClient) -> None: - response = client.post( - "/v1/sandboxes/sbx-001/renew-expiration", - json={"expiresAt": "2030-01-01T00:00:00Z"}, - ) - - assert response.status_code == 401 - assert response.json()["code"] == "MISSING_API_KEY" +# Copyright 2025 Alibaba Group Holding Ltd. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from datetime import datetime, timedelta, timezone + +from fastapi.exceptions import HTTPException +from fastapi.testclient import TestClient + +from opensandbox_server.api import lifecycle +from opensandbox_server.api.schema import RenewSandboxExpirationResponse +from tests.test_helpers import minimal_sandbox + + +def test_renew_expiration_returns_updated_timestamp( + client: TestClient, + auth_headers: dict, + monkeypatch, +) -> None: + target = datetime.now(timezone.utc) + timedelta(hours=2) + calls: list[tuple[str, datetime]] = [] + + class StubService: + @staticmethod + def get_sandbox(sandbox_id: str): + return minimal_sandbox(sandbox_id) + + @staticmethod + def renew_expiration(sandbox_id: str, request) -> RenewSandboxExpirationResponse: + calls.append((sandbox_id, request.expires_at)) + return RenewSandboxExpirationResponse(expiresAt=target) + + monkeypatch.setattr(lifecycle, "sandbox_service", StubService()) + + response = client.post( + "/v1/sandboxes/sbx-001/renew-expiration", + headers=auth_headers, + json={"expiresAt": target.isoformat()}, + ) + + assert response.status_code == 200 + expires_at = datetime.fromisoformat(response.json()["expiresAt"].replace("Z", "+00:00")) + assert expires_at == target + assert calls == [("sbx-001", target)] + + +def test_renew_expiration_rejects_invalid_payload( + client: TestClient, + auth_headers: dict, +) -> None: + response = client.post( + "/v1/sandboxes/sbx-001/renew-expiration", + headers=auth_headers, + json={"expiresAt": "not-a-datetime"}, + ) + + assert response.status_code == 422 + + +def test_renew_expiration_propagates_service_http_error( + client: TestClient, + auth_headers: dict, + monkeypatch, +) -> None: + class StubService: + @staticmethod + def get_sandbox(sandbox_id: str): + return minimal_sandbox(sandbox_id) + + @staticmethod + def renew_expiration(sandbox_id: str, request) -> RenewSandboxExpirationResponse: + raise HTTPException( + status_code=409, + detail={ + "code": "INVALID_EXPIRES_AT", + "message": f"Requested expiresAt is not valid for sandbox {sandbox_id}", + }, + ) + + monkeypatch.setattr(lifecycle, "sandbox_service", StubService()) + + response = client.post( + "/v1/sandboxes/sbx-001/renew-expiration", + headers=auth_headers, + json={"expiresAt": "2030-01-01T00:00:00Z"}, + ) + + assert response.status_code == 409 + assert response.json() == { + "code": "INVALID_EXPIRES_AT", + "message": "Requested expiresAt is not valid for sandbox sbx-001", + } + + +def test_renew_expiration_requires_api_key(client: TestClient) -> None: + response = client.post( + "/v1/sandboxes/sbx-001/renew-expiration", + json={"expiresAt": "2030-01-01T00:00:00Z"}, + ) + + assert response.status_code == 401 + assert response.json()["code"] == "MISSING_API_KEY" diff --git a/specs/sandbox-lifecycle.yml b/specs/sandbox-lifecycle.yml index 841805b43..ea64455b1 100644 --- a/specs/sandbox-lifecycle.yml +++ b/specs/sandbox-lifecycle.yml @@ -23,19 +23,37 @@ info: ## Authentication - API Key authentication is required for all operations: - - 1. **HTTP Header** - ``` - OPEN-SANDBOX-API-KEY: your-api-key - ``` - - 2. **Environment Variable** (for SDK clients) - ``` - OPEN_SANDBOX_API_KEY=your-api-key - ``` - - SDK clients will automatically pick up this environment variable. + The server supports two complementary authentication modes (config-gated; see OSEP-0006): + + 1. **API key (SDK / automation)** — unchanged for backward compatibility: + - **HTTP header** + ``` + OPEN-SANDBOX-API-KEY: your-api-key + ``` + - **Environment variable** (for SDK clients) + ``` + OPEN_SANDBOX_API_KEY=your-api-key + ``` + SDK clients will automatically pick up this environment variable. + - API key clients are treated as a **service admin** (full access; no owner/team scope filter). + + 2. **User / console path** (only when `auth.mode = "api_key_and_user"` in server config) — for browser + clients that must not hold the global API key. Identity is supplied by a reverse proxy using + **trusted headers** (Phase 1), for example: + - `X-OpenSandbox-User` (required for authenticated user requests in this mode) + - `X-OpenSandbox-Team` (optional; used with owner for metadata scope) + - `X-OpenSandbox-Roles` (comma-separated: `read_only` or `operator`) + Requests in this mode without the required user header return **401** with + `code: MISSING_TRUSTED_IDENTITY` (not anonymous access). + + **Authorization:** lifecycle operations enforce **read_only** vs **operator** (and service-wide admin for + API key) on the server. **403** responses use `code: INSUFFICIENT_ROLE` or `code: OUT_OF_SCOPE` when the + caller is not allowed to perform the action or the sandbox is outside the caller’s `access.owner` / + `access.team` metadata scope. Those reserved keys are **server-controlled** on create for user principals. + + When `auth.mode` is the default `api_key_only` and an API key is configured, only the API key path applies. + When no API key is configured, the server may allow unauthenticated access (development mode); production + deployments should always configure authentication. servers: - url: http://localhost:8080/v1 description: Local development @@ -122,8 +140,10 @@ paths: ## Authentication - API Key authentication is required via: - - `OPEN-SANDBOX-API-KEY: ` header + Authentication follows the server mode: + - API key path: `OPEN-SANDBOX-API-KEY: ` + - User path (when `auth.mode = "api_key_and_user"`): trusted identity headers, including + `X-OpenSandbox-User` (roles optional; default role from config applies). requestBody: required: true content: @@ -230,6 +250,8 @@ paths: $ref: '#/components/responses/BadRequest' '401': $ref: '#/components/responses/Unauthorized' + '403': + $ref: '#/components/responses/Forbidden' '409': $ref: '#/components/responses/Conflict' '500': @@ -1058,7 +1080,8 @@ components: provided, `entrypoint` is optional. If omitted, the server defaults the sandbox entrypoint to `["tail", "-f", "/dev/null"]`. - **Note**: API Key authentication is required via the `OPEN-SANDBOX-API-KEY` header. + **Note**: Authentication can be API key (`OPEN-SANDBOX-API-KEY`) or trusted user headers + when `auth.mode = "api_key_and_user"` (Phase 1). properties: image: $ref: '#/components/schemas/ImageSpec' From b513bf537dfa68650d05f9167635370f304772a1 Mon Sep 17 00:00:00 2001 From: divyamagrawal06 Date: Wed, 6 May 2026 18:11:20 +0530 Subject: [PATCH 02/10] fix(server): enforce role checks on snapshot routes and fix SPA client-side routing Snapshot authz gap: snapshot routes were not covered by the OSEP-0006 role matrix, allowing read_only principals to call create/delete snapshot when auth.mode=api_key_and_user. Add CREATE_SNAPSHOT and DELETE_SNAPSHOT as operator-only actions; LIST_SNAPSHOTS and GET_SNAPSHOT as read_only-allowed. Wire authorize_action / authorize_mutating_action + full mutation audit logging (success/error/UNEXPECTED) into all four snapshot handlers. create_snapshot also enforces sandbox owner/team scope, matching the pattern used for delete/pause/resume/renew. SPA fallback: StaticFiles(html=True) serves index.html for directory paths but returns 404 for unknown sub-paths, breaking BrowserRouter routes like /console/sandboxes/:id on direct load or refresh. Replace with _SPAStaticFiles which catches 404 from the parent and falls back to index.html, the standard pattern for single-page apps on Starlette. Update test_routes_snapshots to patch lifecycle.sandbox_service in create_snapshot tests now that the handler calls get_sandbox for scope resolution. Signed-off-by: divyamagrawal06 --- server/opensandbox_server/api/lifecycle.py | 108 +++++++++++++++++- server/opensandbox_server/main.py | 14 ++- .../middleware/authorization.py | 8 ++ server/tests/test_routes_snapshots.py | 15 +++ 4 files changed, 138 insertions(+), 7 deletions(-) diff --git a/server/opensandbox_server/api/lifecycle.py b/server/opensandbox_server/api/lifecycle.py index e467d71c8..e85ed5c03 100644 --- a/server/opensandbox_server/api/lifecycle.py +++ b/server/opensandbox_server/api/lifecycle.py @@ -680,6 +680,7 @@ async def renew_sandbox_expiration( }, ) async def create_snapshot( + http_request: Request, sandbox_id: str, response: Response, request: Optional[CreateSnapshotRequest] = None, @@ -688,12 +689,55 @@ async def create_snapshot( """ Create a persistent point-in-time snapshot from a sandbox. """ - create_request = request or CreateSnapshotRequest() - snapshot = await asyncio.to_thread( - snapshot_service.create_snapshot, - sandbox_id, - create_request, + cfg = get_config() + principal = get_principal(http_request) + authorize_mutating_action( + http_request, + principal, + LifecycleAction.CREATE_SNAPSHOT, + owner_key=cfg.authz.owner_metadata_key, + team_key=cfg.authz.team_metadata_key, + sandbox_id=sandbox_id, ) + box = sandbox_service.get_sandbox(sandbox_id) + authorize_mutating_action( + http_request, + principal, + LifecycleAction.CREATE_SNAPSHOT, + owner_key=cfg.authz.owner_metadata_key, + team_key=cfg.authz.team_metadata_key, + sandbox_id=sandbox_id, + sandbox=box, + ) + create_request = request or CreateSnapshotRequest() + try: + snapshot = await asyncio.to_thread( + snapshot_service.create_snapshot, + sandbox_id, + create_request, + ) + log_mutation_audit( + http_request, action=LifecycleAction.CREATE_SNAPSHOT, sandbox_id=sandbox_id, outcome="success" + ) + except HTTPException as exc: + err = exc.detail + log_mutation_audit( + http_request, + action=LifecycleAction.CREATE_SNAPSHOT, + sandbox_id=sandbox_id, + outcome="error", + error_code=err.get("code") if isinstance(err, dict) else None, + ) + raise + except Exception: + log_mutation_audit( + http_request, + action=LifecycleAction.CREATE_SNAPSHOT, + sandbox_id=sandbox_id, + outcome="error", + error_code="UNEXPECTED", + ) + raise response.headers["Location"] = f"/v1/snapshots/{snapshot.id}" return snapshot @@ -711,6 +755,7 @@ async def create_snapshot( }, ) async def list_snapshots( + http_request: Request, sandbox_id: Optional[str] = Query(None, alias="sandboxId", description="Filter snapshots by source sandbox identifier"), state: Optional[List[str]] = Query(None, description="Filter by snapshot lifecycle state. Pass multiple times for OR logic."), page: int = Query(1, ge=1, description="Page number for pagination"), @@ -720,6 +765,14 @@ async def list_snapshots( """ List snapshots with optional filtering and pagination. """ + cfg = get_config() + principal = get_principal(http_request) + authorize_action( + principal, + LifecycleAction.LIST_SNAPSHOTS, + owner_key=cfg.authz.owner_metadata_key, + team_key=cfg.authz.team_metadata_key, + ) request = ListSnapshotsRequest( filter=SnapshotFilter(sandboxId=sandbox_id, state=state), pagination=PaginationRequest(page=page, pageSize=page_size), @@ -742,12 +795,21 @@ async def list_snapshots( }, ) async def get_snapshot( + http_request: Request, snapshot_id: str, x_request_id: Optional[str] = Header(None, alias="X-Request-ID", description="Unique request identifier for tracing"), ) -> Snapshot: """ Fetch a snapshot by id. """ + cfg = get_config() + principal = get_principal(http_request) + authorize_action( + principal, + LifecycleAction.GET_SNAPSHOT, + owner_key=cfg.authz.owner_metadata_key, + team_key=cfg.authz.team_metadata_key, + ) return snapshot_service.get_snapshot(snapshot_id) @@ -766,13 +828,47 @@ async def get_snapshot( }, ) async def delete_snapshot( + http_request: Request, snapshot_id: str, x_request_id: Optional[str] = Header(None, alias="X-Request-ID", description="Unique request identifier for tracing"), ) -> Response: """ Delete a snapshot by id. """ - snapshot_service.delete_snapshot(snapshot_id) + cfg = get_config() + principal = get_principal(http_request) + authorize_mutating_action( + http_request, + principal, + LifecycleAction.DELETE_SNAPSHOT, + owner_key=cfg.authz.owner_metadata_key, + team_key=cfg.authz.team_metadata_key, + sandbox_id=None, + ) + try: + snapshot_service.delete_snapshot(snapshot_id) + log_mutation_audit( + http_request, action=LifecycleAction.DELETE_SNAPSHOT, sandbox_id=None, outcome="success" + ) + except HTTPException as exc: + err = exc.detail + log_mutation_audit( + http_request, + action=LifecycleAction.DELETE_SNAPSHOT, + sandbox_id=None, + outcome="error", + error_code=err.get("code") if isinstance(err, dict) else None, + ) + raise + except Exception: + log_mutation_audit( + http_request, + action=LifecycleAction.DELETE_SNAPSHOT, + sandbox_id=None, + outcome="error", + error_code="UNEXPECTED", + ) + raise return Response(status_code=status.HTTP_204_NO_CONTENT) diff --git a/server/opensandbox_server/main.py b/server/opensandbox_server/main.py index f4b1940f8..290ae9a3e 100644 --- a/server/opensandbox_server/main.py +++ b/server/opensandbox_server/main.py @@ -157,14 +157,26 @@ async def lifespan(app: FastAPI): if app_config.console.enabled: from pathlib import Path + from starlette.exceptions import HTTPException as _StarletteHTTPException from starlette.staticfiles import StaticFiles + class _SPAStaticFiles(StaticFiles): + """Serve index.html for unknown paths so BrowserRouter client-side routes work.""" + + async def get_response(self, path: str, scope): + try: + return await super().get_response(path, scope) + except _StarletteHTTPException as exc: + if exc.status_code == 404: + return await super().get_response("index.html", scope) + raise + _console_dist = Path(__file__).resolve().parent.parent.parent / "console" / "dist" if _console_dist.is_dir(): _mount = app_config.console.mount_path.rstrip("/") or "/console" app.mount( _mount, - StaticFiles(directory=str(_console_dist), html=True), + _SPAStaticFiles(directory=str(_console_dist), html=True), name="console", ) diff --git a/server/opensandbox_server/middleware/authorization.py b/server/opensandbox_server/middleware/authorization.py index c6cf55711..c7aea437c 100644 --- a/server/opensandbox_server/middleware/authorization.py +++ b/server/opensandbox_server/middleware/authorization.py @@ -35,12 +35,18 @@ class LifecycleAction: DELETE = "delete_sandbox" PAUSE = "pause_sandbox" RESUME = "resume_sandbox" + LIST_SNAPSHOTS = "list_snapshots" + GET_SNAPSHOT = "get_snapshot" + CREATE_SNAPSHOT = "create_snapshot" + DELETE_SNAPSHOT = "delete_snapshot" _READ_ONLY = { LifecycleAction.LIST_SANDBOXES, LifecycleAction.GET_SANDBOX, LifecycleAction.GET_ENDPOINT, + LifecycleAction.LIST_SNAPSHOTS, + LifecycleAction.GET_SNAPSHOT, } _OPERATOR = _READ_ONLY | { LifecycleAction.CREATE, @@ -48,6 +54,8 @@ class LifecycleAction: LifecycleAction.DELETE, LifecycleAction.PAUSE, LifecycleAction.RESUME, + LifecycleAction.CREATE_SNAPSHOT, + LifecycleAction.DELETE_SNAPSHOT, } diff --git a/server/tests/test_routes_snapshots.py b/server/tests/test_routes_snapshots.py index d43b3f251..b5b1a1d97 100644 --- a/server/tests/test_routes_snapshots.py +++ b/server/tests/test_routes_snapshots.py @@ -29,6 +29,17 @@ from opensandbox_server.services.snapshot_service import PersistedSnapshotService +def _stub_sandbox_service(): + """Returns a minimal sandbox_service stub that satisfies scope-checking in create_snapshot.""" + + class _Stub: + @staticmethod + def get_sandbox(_sandbox_id: str): + return {"id": _sandbox_id, "metadata": {}} + + return _Stub() + + def _sample_snapshot(now: datetime, snapshot_id: str = "snap-001") -> Snapshot: return Snapshot( id=snapshot_id, @@ -54,6 +65,7 @@ def create_snapshot(sandbox_id: str, request) -> Snapshot: return _sample_snapshot(now) monkeypatch.setattr(lifecycle, "snapshot_service", StubService()) + monkeypatch.setattr(lifecycle, "sandbox_service", _stub_sandbox_service()) response = client.post( "/v1/sandboxes/sbx-001/snapshots", @@ -83,6 +95,7 @@ def create_snapshot(sandbox_id: str, request) -> Snapshot: return _sample_snapshot(now) monkeypatch.setattr(lifecycle, "snapshot_service", StubService()) + monkeypatch.setattr(lifecycle, "sandbox_service", _stub_sandbox_service()) response = client.post("/v1/sandboxes/sbx-001/snapshots", headers=auth_headers) @@ -236,6 +249,7 @@ def delete_snapshot(snapshot_id: str, image: str | None = None) -> None: snapshot_runtime=StubSnapshotRuntime(), ) monkeypatch.setattr(lifecycle, "snapshot_service", service) + monkeypatch.setattr(lifecycle, "sandbox_service", _stub_sandbox_service()) created = client.post("/v1/sandboxes/sbx-001/snapshots", headers=auth_headers) assert created.status_code == 202 @@ -262,6 +276,7 @@ def get_sandbox(sandbox_id: str): snapshot_runtime=NoopSnapshotRuntime(), ) monkeypatch.setattr(lifecycle, "snapshot_service", service) + monkeypatch.setattr(lifecycle, "sandbox_service", _stub_sandbox_service()) response = client.post("/v1/sandboxes/sbx-001/snapshots", headers=auth_headers) From f53528e8975cc42073493d687fc5b42ff709ab14 Mon Sep 17 00:00:00 2001 From: divyamagrawal06 Date: Wed, 6 May 2026 18:49:53 +0530 Subject: [PATCH 03/10] fix(console,server): address Copilot review comments - Remove undocumented CONSOLE=1 env injection from CreatePage; it made console-created sandboxes behave differently from API requests and risked colliding with existing env vars in user workloads - Fix read-only banner to reference VITE_UI_ROLE (the actual UI hint var) instead of VITE_DEV_IDENTITY_ROLES (the dev proxy identity header var) - Fix vite.config.ts proxy key: try VITE_API_PREFIX first so the proxy and the browser client both key off the same env var when the API prefix is customized; fall back to VITE_API_BASE_PATH then /v1 - Replace external raw.githubusercontent.com logo img with an inline SVG so the console works in offline and CSP-restricted deployments - Disable 'Next' pagination button when data.pagination.hasNextPage is false instead of only while a request is in-flight - Add accessible sr-only label to the port input in DetailPage - Replace hardcoded 127.0.0.1:8080 in the 500 fallback message with a generic prompt to check server and proxy logs - Log a warning when console.enabled = true but console/dist is not found so operators get an actionable message instead of a silent 404 Signed-off-by: divyamagrawal06 --- console/src/App.tsx | 9 ++++----- console/src/api/client.ts | 2 +- console/src/pages/CreatePage.tsx | 4 ++-- console/src/pages/DetailPage.tsx | 2 ++ console/src/pages/ListPage.tsx | 2 +- console/vite.config.ts | 2 +- server/opensandbox_server/main.py | 7 +++++++ 7 files changed, 18 insertions(+), 10 deletions(-) diff --git a/console/src/App.tsx b/console/src/App.tsx index 790dbe0ff..c9ad67377 100644 --- a/console/src/App.tsx +++ b/console/src/App.tsx @@ -46,11 +46,10 @@ export function App() {

- OpenSandbox logo + + + + OpenSandbox
diff --git a/console/src/api/client.ts b/console/src/api/client.ts index d5a50f62b..7b0d6db58 100644 --- a/console/src/api/client.ts +++ b/console/src/api/client.ts @@ -63,7 +63,7 @@ export async function apiFetch(path: string, init?: RequestInit): Promise message = parsed.text; } if (!message && res.status === 500) { - message = "API proxy returned 500. Ensure the server is running on http://127.0.0.1:8080."; + message = "The API returned an internal server error (500). Check the server and proxy logs for details."; } if (!message) { message = res.statusText || `HTTP ${res.status}`; diff --git a/console/src/pages/CreatePage.tsx b/console/src/pages/CreatePage.tsx index 12fe758ae..26afe6e06 100644 --- a/console/src/pages/CreatePage.tsx +++ b/console/src/pages/CreatePage.tsx @@ -35,7 +35,7 @@ export function CreatePage() { timeout, resourceLimits: { cpu, memory: mem }, entrypoint: ep, - env: { CONSOLE: "1" }, + env: undefined, }); nav(`/sandboxes/${encodeURIComponent(res.id)}`); } catch (ex) { @@ -63,7 +63,7 @@ export function CreatePage() {
Read-only role. You cannot create sandboxes. Ask an operator to change your - X-OpenSandbox-Roles to operator (or set VITE_DEV_IDENTITY_ROLES=operator{" "} + X-OpenSandbox-Roles to operator (or set VITE_UI_ROLE=operator{" "} for the dev UI hint).
diff --git a/console/src/pages/DetailPage.tsx b/console/src/pages/DetailPage.tsx index 89756ae42..bef211ce7 100644 --- a/console/src/pages/DetailPage.tsx +++ b/console/src/pages/DetailPage.tsx @@ -181,7 +181,9 @@ export function DetailPage() {

Get endpoint

Resolves a published port to a reachable host (per server ingress settings).

+ setPort(e.target.value)} diff --git a/console/src/pages/ListPage.tsx b/console/src/pages/ListPage.tsx index 6b3e82f7e..7f14bceca 100644 --- a/console/src/pages/ListPage.tsx +++ b/console/src/pages/ListPage.tsx @@ -137,7 +137,7 @@ export function ListPage() {
- + + placeholder="e.g. project=demo" + />

diff --git a/server/opensandbox_server/repositories/snapshots/sqlite.py b/server/opensandbox_server/repositories/snapshots/sqlite.py index 72e846f26..1fa899a3f 100644 --- a/server/opensandbox_server/repositories/snapshots/sqlite.py +++ b/server/opensandbox_server/repositories/snapshots/sqlite.py @@ -89,7 +89,9 @@ def get(self, snapshot_id: str) -> SnapshotRecord | None: message, last_transition_at, created_at, - updated_at + updated_at, + access_owner, + access_team FROM snapshots WHERE id = ? """, @@ -147,7 +149,9 @@ def list(self, query: SnapshotListQuery) -> SnapshotListResult: message, last_transition_at, created_at, - updated_at + updated_at, + access_owner, + access_team FROM snapshots {where_clause} ORDER BY created_at DESC, id DESC diff --git a/server/opensandbox_server/services/snapshot_service.py b/server/opensandbox_server/services/snapshot_service.py index 6817405b9..94aa1918f 100644 --- a/server/opensandbox_server/services/snapshot_service.py +++ b/server/opensandbox_server/services/snapshot_service.py @@ -252,6 +252,8 @@ def _mark_snapshot_deleting(self, record: SnapshotRecord) -> SnapshotRecord | No ), created_at=record.created_at, updated_at=now, + access_owner=record.access_owner, + access_team=record.access_team, ) if self._snapshot_repository.update_if_state( deleting_record, diff --git a/server/tests/test_snapshot_repository_sqlite.py b/server/tests/test_snapshot_repository_sqlite.py index aaf9ed19e..ddb3bf013 100644 --- a/server/tests/test_snapshot_repository_sqlite.py +++ b/server/tests/test_snapshot_repository_sqlite.py @@ -34,6 +34,9 @@ def _record( sandbox_id: str, created_at: datetime, state: SnapshotState = SnapshotState.CREATING, + *, + access_owner: str | None = None, + access_team: str | None = None, ) -> SnapshotRecord: return SnapshotRecord( id=snapshot_id, @@ -51,12 +54,20 @@ def _record( ), created_at=created_at, updated_at=created_at, + access_owner=access_owner, + access_team=access_team, ) def test_sqlite_snapshot_repository_persists_and_fetches_records(tmp_path) -> None: repo = SQLiteSnapshotRepository(tmp_path / "snapshots.db") - record = _record("snap-001", "sbx-001", datetime.utcnow()) + record = _record( + "snap-001", + "sbx-001", + datetime.utcnow(), + access_owner="user-001", + access_team="team-001", + ) repo.create(record) loaded = repo.get("snap-001") @@ -66,6 +77,8 @@ def test_sqlite_snapshot_repository_persists_and_fetches_records(tmp_path) -> No assert loaded.source_sandbox_id == "sbx-001" assert loaded.restore_config.image == record.restore_config.image assert loaded.status.state == SnapshotState.CREATING + assert loaded.access_owner == "user-001" + assert loaded.access_team == "team-001" def test_sqlite_snapshot_repository_enables_wal_and_busy_timeout(tmp_path) -> None: @@ -83,7 +96,14 @@ def test_sqlite_snapshot_repository_lists_and_updates_records(tmp_path) -> None: repo = SQLiteSnapshotRepository(tmp_path / "snapshots.db") now = datetime.utcnow() first = _record("snap-001", "sbx-001", now) - second = _record("snap-002", "sbx-001", now + timedelta(seconds=1), state=SnapshotState.READY) + second = _record( + "snap-002", + "sbx-001", + now + timedelta(seconds=1), + state=SnapshotState.READY, + access_owner="user-001", + access_team="team-001", + ) third = _record("snap-003", "sbx-002", now + timedelta(seconds=2), state=SnapshotState.FAILED) repo.create(first) @@ -96,6 +116,8 @@ def test_sqlite_snapshot_repository_lists_and_updates_records(tmp_path) -> None: assert page.total_items == 1 assert [item.id for item in page.items] == ["snap-002"] + assert page.items[0].access_owner == "user-001" + assert page.items[0].access_team == "team-001" updated = SnapshotRecord( id=first.id, diff --git a/server/tests/test_snapshot_service.py b/server/tests/test_snapshot_service.py index dcb4f2e33..e19dfdbde 100644 --- a/server/tests/test_snapshot_service.py +++ b/server/tests/test_snapshot_service.py @@ -115,12 +115,16 @@ def _snapshot_record( state: SnapshotState, *, image: str | None = None, + access_owner: str | None = None, + access_team: str | None = None, ) -> SnapshotRecord: return SnapshotRecord( id=snapshot_id, source_sandbox_id="sbx-001", restore_config=SnapshotRestoreConfig(image=image), status=SnapshotStatusRecord(state=state), + access_owner=access_owner, + access_team=access_team, ) @@ -410,6 +414,8 @@ def test_snapshot_service_propagates_snapshot_delete_conflict(tmp_path) -> None: "snap-in-use", SnapshotState.READY, image="opensandbox-snapshots:snap-in-use", + access_owner="user-001", + access_team="team-001", ) repo.create(record) @@ -431,6 +437,8 @@ def delete_snapshot(snapshot_id: str, image: str | None = None) -> None: assert exc_info.value.status_code == 409 assert stored is not None assert stored.status.state == SnapshotState.DELETING + assert stored.access_owner == "user-001" + assert stored.access_team == "team-001" def test_snapshot_service_recovers_delete_after_runtime_cleanup_succeeds(tmp_path) -> None: