From cf8586b494d67d668e0a4cfa5d6fb4e7b16ce569 Mon Sep 17 00:00:00 2001 From: Talha Date: Sat, 1 Aug 2026 15:02:47 +0500 Subject: [PATCH 01/21] feat: add user-completable MCP upload handoff --- MANIFEST.in | 8 + README.md | 6 + compose.coolify.yaml | 3 + docs/architecture/platform.md | 23 +- docs/deployment/coolify.md | 25 +- pyproject.toml | 1 + src/vidxp/api.py | 10 +- src/vidxp/api_middleware.py | 110 ++- src/vidxp/api_models.py | 19 + src/vidxp/application_models.py | 82 +- .../upload_page/THIRD_PARTY_NOTICES.txt | 527 +++++++++++ src/vidxp/assets/upload_page/index.html | 69 ++ src/vidxp/assets/upload_page/upload-page.css | 1 + src/vidxp/assets/upload_page/upload-page.js | 8 + src/vidxp/core/uploads.py | 76 +- src/vidxp/infrastructure/sql_catalog.py | 240 +++-- src/vidxp/infrastructure/sql_tables.py | 20 + src/vidxp/mcp.py | 154 +++- .../versions/20260801_01_upload_handoffs.py | 49 + src/vidxp/settings.py | 202 ++-- src/vidxp/tusd_hooks.py | 37 +- src/vidxp/upload_page.py | 184 ++++ src/vidxp/upload_service.py | 827 +++++++++++++++-- tests/test_database_cli.py | 4 +- tests/test_mcp.py | 256 ++++-- tests/test_packaging.py | 187 ++-- tests/test_settings.py | 29 + tests/test_tusd_hooks.py | 84 +- tests/test_tusd_live.py | 243 +++++ tests/test_upload_page.py | 194 ++++ tests/test_upload_service.py | 187 +++- web/upload-page/package-lock.json | 866 ++++++++++++++++++ web/upload-page/package.json | 20 + web/upload-page/scripts/build.mjs | 104 +++ web/upload-page/src/app.css | 211 +++++ web/upload-page/src/app.js | 542 +++++++++++ web/upload-page/src/index.html | 69 ++ 37 files changed, 5171 insertions(+), 506 deletions(-) create mode 100644 src/vidxp/assets/upload_page/THIRD_PARTY_NOTICES.txt create mode 100644 src/vidxp/assets/upload_page/index.html create mode 100644 src/vidxp/assets/upload_page/upload-page.css create mode 100644 src/vidxp/assets/upload_page/upload-page.js create mode 100644 src/vidxp/migrations/versions/20260801_01_upload_handoffs.py create mode 100644 src/vidxp/upload_page.py create mode 100644 tests/test_tusd_live.py create mode 100644 tests/test_upload_page.py create mode 100644 web/upload-page/package-lock.json create mode 100644 web/upload-page/package.json create mode 100644 web/upload-page/scripts/build.mjs create mode 100644 web/upload-page/src/app.css create mode 100644 web/upload-page/src/app.js create mode 100644 web/upload-page/src/index.html diff --git a/MANIFEST.in b/MANIFEST.in index 383bfce..e74fc57 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,6 +1,14 @@ include README.md include LICENSE include docs/images/logo.png +recursive-include src/vidxp/assets/upload_page * +include web/upload-page/package.json +include web/upload-page/package-lock.json +include web/upload-page/scripts/build.mjs +include web/upload-page/src/app.js +include web/upload-page/src/app.css +include web/upload-page/src/index.html +prune web/upload-page/node_modules global-exclude *.whl global-exclude *.pyc global-exclude __pycache__ diff --git a/README.md b/README.md index f314c36..c8cfe0a 100644 --- a/README.md +++ b/README.md @@ -171,6 +171,12 @@ scenes, ask questions about a library, and create clips or actor overlays. Local agents can connect over stdio; remote agents can connect to a self-hosted VidXP server. +On a configured remote server, an agent calls `create_media_upload` with the +expected filename, byte size, and MIME type. VidXP returns a short-lived HTTPS +handoff page for the user; video bytes go from that page directly to tusd and +never pass through MCP. The agent polls `get_media_upload` until it receives a +`media_id`, then passes that ID to `start_indexing`. + Agents can call `get_workspace` before acting to inspect registered media, active-index coverage, model readiness, and the searchable, queryable, inspectable, or renderable roles available for each video. Invalid capability diff --git a/compose.coolify.yaml b/compose.coolify.yaml index cd6d601..7a2d5a9 100644 --- a/compose.coolify.yaml +++ b/compose.coolify.yaml @@ -12,6 +12,8 @@ x-vidxp-environment: &vidxp-environment VIDXP_UPLOAD_PUBLIC_ENDPOINT: ${VIDXP_UPLOAD_PUBLIC_ENDPOINT:?Set the public tusd endpoint ending in /uploads/} VIDXP_UPLOAD_INTERNAL_ENDPOINT: http://tusd:8080/uploads/ VIDXP_UPLOAD_CLEANUP_TOKEN: ${VIDXP_UPLOAD_CLEANUP_TOKEN:?Set a cleanup token of at least 32 characters} + VIDXP_UPLOAD_HANDOFF_PUBLIC_URL: ${VIDXP_UPLOAD_HANDOFF_PUBLIC_URL:?Set the HTTPS VidXP URL ending in /upload-handoff} + VIDXP_UPLOAD_HANDOFF_SECRET: ${VIDXP_UPLOAD_HANDOFF_SECRET:?Set a dedicated handoff secret of at least 32 characters} VIDXP_UPLOAD_QUARANTINE_ROOT: /var/lib/vidxp/uploads VIDXP_UPLOAD_MAX_BYTES: ${VIDXP_UPLOAD_MAX_BYTES:-53687091200} VIDXP_UPLOAD_QUOTA_BYTES: ${VIDXP_UPLOAD_QUOTA_BYTES:-107374182400} @@ -180,6 +182,7 @@ services: - -hooks-http-backoff=1s - -cors-allow-origin=${VIDXP_UPLOAD_CORS_ORIGIN_REGEX:?Set an anchored browser-origin regex} - -cors-allow-headers=Authorization + - -cors-expose-headers=X-VidXP-Error - -cors-max-age=600 - -network-timeout=60s - -shutdown-timeout=30s diff --git a/docs/architecture/platform.md b/docs/architecture/platform.md index 41e5c09..cf28a00 100644 --- a/docs/architecture/platform.md +++ b/docs/architecture/platform.md @@ -325,6 +325,24 @@ Remote upload is a four-stage protocol: 4. The non-blocking `post-finish` hook idempotently upserts completion by upload ID and enqueues the durable ffprobe/import workflow. +Remote MCP adds a user-completable handoff in front of the same protocol. The +`create_media_upload` write tool idempotently creates the authoritative upload +intent and returns an HTTPS page whose fragment contains a short-lived HMAC +capability bound to that intent, repository, declared size, purpose, version, +and expiry. The page exchanges the fragment for a digest-backed `HttpOnly`, +`Secure`, `SameSite=Strict` cookie, then requests a separate one-time creation +grant for the exact tus `POST`. Neither the raw handoff capability nor session +secret is stored, and the user's MCP bearer is never exposed to the browser. +The existing bearer-authenticated HTTP/tus route remains available. + +The packaged page uses Uppy Core, Tus, and Golden Retriever with native controls. +It validates the expected filename and byte size, limits tus metadata to the +intent identifier, supports progress/pause/resume/retry, and recovers interrupted +uploads from browser storage. It does not choose a tus chunk size, so a few-MiB +file follows the same resumable path rather than a separate MCP or small-upload +path. Scripts and styles are self-hosted under a strict CSP; the handoff API is +same-origin and tus CORS remains restricted to configured origins. + Application responses carrying upload URLs use `private, no-store` and `no-referrer`; hook payloads and MCP results do not persist them. The opaque path is still a bearer credential and may appear in an upstream proxy's access log unless @@ -372,7 +390,8 @@ ffprobe validation. Remote MCP workflow: ```text -HTTP upload/import service -> media_id -> MCP start_indexing(media_id) +MCP create_media_upload -> user HTTPS page -> direct tus upload +MCP get_media_upload -> media_id -> MCP start_indexing(media_id) ``` MCP does not carry video bytes, base64 video, or arbitrary server paths. Stdio may @@ -917,6 +936,8 @@ Initial curated tools: - `get_capability` - `list_media` - `get_media` +- `create_media_upload` +- `get_media_upload` - `get_index_status` - `start_indexing` - `search_moments` diff --git a/docs/deployment/coolify.md b/docs/deployment/coolify.md index 1113e8b..a6c9afe 100644 --- a/docs/deployment/coolify.md +++ b/docs/deployment/coolify.md @@ -25,9 +25,17 @@ VIDXP_HTTP_STATIC_BEARER_TOKEN= VIDXP_UPLOAD_CLEANUP_TOKEN= VIDXP_PUBLIC_API_HOST=api.example.com VIDXP_UPLOAD_PUBLIC_ENDPOINT=https://uploads.example.com/uploads/ +VIDXP_UPLOAD_HANDOFF_PUBLIC_URL=https://api.example.com/upload-handoff +VIDXP_UPLOAD_HANDOFF_SECRET= VIDXP_UPLOAD_CORS_ORIGIN_REGEX=^https://app\.example\.com$ ``` +`VIDXP_UPLOAD_HANDOFF_PUBLIC_URL` must be the externally reachable HTTPS API +URL ending exactly in `/upload-handoff`. Keep its secret distinct from the MCP +bearer and upload-cleanup credentials. The generated handoff URL contains a +short-lived capability in its fragment; the fragment is not sent in HTTP +requests, and the page removes it from browser history after bootstrap. + Optional deployment-wide upload limits use `VIDXP_UPLOAD_MAX_BYTES` for one object and `VIDXP_UPLOAD_QUOTA_BYTES` for all reserved upload bytes in the singleton repository. There is no per-principal or per-repository quota setting. @@ -46,9 +54,20 @@ The upload path is a capability URL used to resume an upload. Disable or redact reverse-proxy access logging for `/uploads/`; VidXP cannot control logs written by an upstream proxy. -Video bytes do not travel through MCP. Create and resume the upload through the -HTTP/tus endpoints, wait for its `media_id`, and then use that ID with -`start_indexing`. +Video bytes do not travel through MCP. A remote MCP client calls +`create_media_upload` with the expected filename, size, and MIME type and gives +the returned HTTPS page to the user. The page validates that exact file and +uploads it directly to tusd with pause, resume, retry, and browser recovery. +The client polls `get_media_upload`; once it returns a `media_id`, pass that ID +to `start_indexing`. + +The page exchanges its fragment capability for an `HttpOnly`, `Secure`, +`SameSite=Strict` session cookie. Each tus creation uses a separate one-time, +five-minute creation grant; the MCP bearer never enters the page or tusd. Keep +access logging disabled or redacted for `/uploads/`, because the tus resume URL +is itself a bearer capability. The page's Content Security Policy permits only +same-origin scripts/styles and connections to the configured tus origin, so +both public URLs must be correct before startup. ## Prepare worker models diff --git a/pyproject.toml b/pyproject.toml index c99385a..d57e4bb 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -69,6 +69,7 @@ include = ["vidxp*"] [tool.setuptools.package-data] vidxp = [ + "assets/upload_page/*", "benchmarks/requirements.txt", "capabilities/*/requirements.txt", "requirements/*.txt", diff --git a/src/vidxp/api.py b/src/vidxp/api.py index 7f4d86d..533767f 100644 --- a/src/vidxp/api.py +++ b/src/vidxp/api.py @@ -14,6 +14,7 @@ RequestBodyLimitMiddleware, RequestBodyTooLarge, TypedTrustedHostMiddleware, + UploadHandoffSecurityHeadersMiddleware, request_too_large_response, ) from vidxp.api_models import HealthResponse, ReadinessResponse @@ -22,14 +23,14 @@ from vidxp.composition import HttpApplicationContext, create_http_application from vidxp.mcp import MCPTransportSecurityBoundary, create_remote_mcp from vidxp.settings import VidXPSettings +from vidxp.upload_page import router as upload_page_router _BEARER_SECURITY = HTTPBearer( auto_error=False, scheme_name="BearerAuth", description=( - "Bearer access token. Authentication is enforced once by the " - "server middleware." + "Bearer access token. Authentication is enforced once by the server middleware." ), ) @@ -127,6 +128,7 @@ def ready( create_api_router(), dependencies=api_dependencies, ) + app.include_router(upload_page_router) app.mount("/", remote_mcp.app) app.add_exception_handler( RequestBodyTooLarge, @@ -143,6 +145,10 @@ def ready( authenticator=active_context.authenticator, delegated_paths=delegated_auth_paths, ) + app.add_middleware( + UploadHandoffSecurityHeadersMiddleware, + upload_endpoint=active_settings.upload_public_endpoint, + ) if active_settings.http_allowed_origins: app.add_middleware( ApiCORSMiddleware, diff --git a/src/vidxp/api_middleware.py b/src/vidxp/api_middleware.py index 9ae5175..cc5a693 100644 --- a/src/vidxp/api_middleware.py +++ b/src/vidxp/api_middleware.py @@ -1,8 +1,11 @@ from __future__ import annotations +import re +from urllib.parse import urlsplit + from fastapi.security.utils import get_authorization_scheme_param from starlette.concurrency import run_in_threadpool -from starlette.datastructures import Headers +from starlette.datastructures import Headers, MutableHeaders from starlette.exceptions import HTTPException from starlette.middleware.cors import CORSMiddleware, SAFELISTED_HEADERS from starlette.types import ASGIApp, Message, Receive, Scope, Send @@ -18,6 +21,41 @@ PUBLIC_HTTP_PATHS = frozenset({"/favicon.ico", "/health", "/ready"}) UPLOAD_PATH = "/api/v1/media" +_UPLOAD_HANDOFF_PAGE = re.compile(r"^/upload-handoff/[0-9a-f]{32}$") +_UPLOAD_HANDOFF_STATUS = re.compile(r"^/upload-handoff/[0-9a-f]{32}/status$") +_UPLOAD_HANDOFF_BOOTSTRAP = re.compile(r"^/upload-handoff/[0-9a-f]{32}/bootstrap$") +_UPLOAD_HANDOFF_GRANT = re.compile(r"^/upload-handoff/[0-9a-f]{32}/creation-grant$") +_UPLOAD_HANDOFF_ASSETS = frozenset( + { + "/upload-handoff/assets/upload-page.js", + "/upload-handoff/assets/upload-page.css", + "/upload-handoff/assets/THIRD_PARTY_NOTICES.txt", + } +) + + +def _public_upload_handoff_request(scope: Scope) -> bool: + path = str(scope.get("path", "")) + method = str(scope.get("method", "")).upper() + return ( + method == "GET" + and ( + path in _UPLOAD_HANDOFF_ASSETS + or _UPLOAD_HANDOFF_PAGE.fullmatch(path) is not None + or _UPLOAD_HANDOFF_STATUS.fullmatch(path) is not None + ) + ) or ( + method == "POST" + and ( + _UPLOAD_HANDOFF_BOOTSTRAP.fullmatch(path) is not None + or _UPLOAD_HANDOFF_GRANT.fullmatch(path) is not None + ) + ) + + +def _upload_handoff_path(scope: Scope) -> bool: + path = str(scope.get("path", "")) + return path == "/upload-handoff" or path.startswith("/upload-handoff/") class RequestBodyTooLarge(HTTPException, OSError): @@ -79,10 +117,7 @@ async def __call__( receive: Receive, send: Send, ) -> None: - if ( - scope["type"] == "http" - and str(scope.get("path", "")).startswith("/api/") - ): + if scope["type"] == "http" and str(scope.get("path", "")).startswith("/api/"): headers = Headers(scope=scope) origin = headers.get("origin") requested_method = headers.get("access-control-request-method") @@ -95,10 +130,7 @@ async def __call__( ).split(",") if value.strip() } - if ( - "*" not in self.allow_origins - and origin not in self.allow_origins - ): + if "*" not in self.allow_origins and origin not in self.allow_origins: await public_error_response( ErrorDetail( code="cors_origin_forbidden", @@ -148,11 +180,7 @@ async def __call__( return host = _host_name(Headers(scope=scope).get("host", "")) if any( - host == pattern - or ( - pattern.startswith("*.") - and host.endswith(pattern[1:]) - ) + host == pattern or (pattern.startswith("*.") and host.endswith(pattern[1:])) for pattern in self.allowed_hosts ): await self.app(scope, receive, send) @@ -174,10 +202,7 @@ def _host_name(header: str) -> str: if closing < 0: return "" remainder = value[closing + 1 :] - if remainder and ( - not remainder.startswith(":") - or not remainder[1:].isdigit() - ): + if remainder and (not remainder.startswith(":") or not remainder[1:].isdigit()): return "" return value[1:closing] if value.count(":") == 1: @@ -208,6 +233,7 @@ async def __call__( if ( scope["type"] != "http" or str(scope.get("path", "")) in PUBLIC_HTTP_PATHS + or _public_upload_handoff_request(scope) or str(scope.get("path", "")) in self.delegated_paths ): await self.app(scope, receive, send) @@ -231,6 +257,54 @@ async def __call__( await self.app(scope, receive, send) +class UploadHandoffSecurityHeadersMiddleware: + """Apply a no-store browser boundary to the exact handoff subtree.""" + + def __init__(self, app: ASGIApp, *, upload_endpoint: str | None) -> None: + self.app = app + origin = None + if upload_endpoint is not None: + parsed = urlsplit(upload_endpoint) + origin = f"{parsed.scheme}://{parsed.netloc}" + connect_sources = "'self'" if origin is None else f"'self' {origin}" + self.headers = { + "Cache-Control": "private, no-store", + "Referrer-Policy": "no-referrer", + "X-Content-Type-Options": "nosniff", + "X-Frame-Options": "DENY", + "Cross-Origin-Opener-Policy": "same-origin", + "Cross-Origin-Resource-Policy": "same-origin", + "Permissions-Policy": ( + "camera=(), microphone=(), geolocation=(), payment=(), usb=()" + ), + "Content-Security-Policy": ( + "default-src 'none'; script-src 'self'; style-src 'self'; " + f"connect-src {connect_sources}; img-src 'self' data:; " + "font-src 'self'; object-src 'none'; base-uri 'none'; " + "form-action 'none'; frame-ancestors 'none'; worker-src 'none'" + ), + } + + async def __call__( + self, + scope: Scope, + receive: Receive, + send: Send, + ) -> None: + if scope["type"] != "http" or not _upload_handoff_path(scope): + await self.app(scope, receive, send) + return + + async def add_headers(message: Message) -> None: + if message["type"] == "http.response.start": + headers = MutableHeaders(scope=message) + for name, value in self.headers.items(): + headers[name] = value + await send(message) + + await self.app(scope, receive, add_headers) + + class RequestBodyLimitMiddleware: def __init__( self, diff --git a/src/vidxp/api_models.py b/src/vidxp/api_models.py index 6f7f269..23e6c61 100644 --- a/src/vidxp/api_models.py +++ b/src/vidxp/api_models.py @@ -2,9 +2,12 @@ from typing import Literal +from pydantic import AwareDatetime, Field + from vidxp.application_models import ( ApplicationModel, ErrorDetail, + MediaUploadStatus, UploadIntent, ) @@ -27,3 +30,19 @@ class UploadIntentResponse(ApplicationModel): creation_url: str upload_metadata: str resume_url: str | None = None + + +class UploadHandoffBootstrapRequest(ApplicationModel): + capability: str = Field(min_length=32, max_length=512) + + +class UploadPageSessionResponse(ApplicationModel): + status: MediaUploadStatus + creation_url: str = Field(min_length=1, max_length=2048) + resume_url: str | None = Field(default=None, max_length=4096) + + +class UploadCreationGrantResponse(ApplicationModel): + scheme: Literal["VidXP-Handoff"] = "VidXP-Handoff" + grant: str = Field(min_length=32, max_length=512) + expires_at: AwareDatetime diff --git a/src/vidxp/application_models.py b/src/vidxp/application_models.py index be6ab3e..95ba663 100644 --- a/src/vidxp/application_models.py +++ b/src/vidxp/application_models.py @@ -172,8 +172,7 @@ def __init__( super().__init__( "dependency_unavailable", ErrorCategory.unavailable, - f"Dependencies for the {label} capability are unavailable. " - f"{install_hint}", + f"Dependencies for the {label} capability are unavailable. {install_hint}", details={ "capabilities": list(capabilities), "install_hint": install_hint, @@ -192,9 +191,7 @@ def __init__(self, capability: str) -> None: f"`vidxp prepare --modalities {modality}` before retrying.", details={ "capability": capability, - "remediation": ( - f"vidxp prepare --modalities {modality}" - ), + "remediation": (f"vidxp prepare --modalities {modality}"), }, ) @@ -213,8 +210,7 @@ def __init__( modality = capability.split(".", 1)[0] remediation = f"vidxp prepare --modalities {modality}" retry_message = ( - "Partial files were kept and the next preparation attempt will " - "resume them." + "Partial files were kept and the next preparation attempt will resume them." if resumable else "The next preparation attempt will restart this file." ) @@ -311,9 +307,7 @@ class CapabilitySummary(ApplicationModel): supports_indexing: bool prepares_models: bool roles: tuple[CapabilityRole, ...] = () - identity_mode: CapabilityIdentityMode = ( - CapabilityIdentityMode.not_applicable - ) + identity_mode: CapabilityIdentityMode = CapabilityIdentityMode.not_applicable provenance: CapabilityProvenance | None = None @@ -414,6 +408,23 @@ class UploadIntent(ApplicationModel): media_id: MediaId | None = None +class MediaUploadStatus(ApplicationModel): + """Actionable public projection of the shared upload intent state.""" + + intent_id: UploadIntentId + state: UploadState + original_filename: str = Field(min_length=1, max_length=255) + byte_size: int = Field(gt=0) + declared_mime_type: MimeType | None = None + maximum_bytes: int = Field(gt=0) + expires_at: AwareDatetime + transport: Literal["tus"] = "tus" + job_id: JobId | None = None + media_id: MediaId | None = None + status: str = Field(min_length=1, max_length=512) + next_action: str = Field(min_length=1, max_length=1024) + + class CreateIndexCommand(ApplicationModel): media_id: MediaId = Field( description=( @@ -481,13 +492,8 @@ def _canonicalize_scene_sampling(cls, value: Any) -> Any: @model_validator(mode="after") def _scene_sampling_requires_scene(self) -> "CreateIndexCommand": - if ( - self.scene_sample_fps is not None - and "scene" not in self.modalities - ): - raise ValueError( - "scene_sample_fps requires the scene modality." - ) + if self.scene_sample_fps is not None and "scene" not in self.modalities: + raise ValueError("scene_sample_fps requires the scene modality.") return self @@ -504,9 +510,7 @@ class RemoveIndexCommand(ApplicationModel): class Artifact(ApplicationModel): - schema_version: Literal[ARTIFACT_SCHEMA_VERSION] = ( - ARTIFACT_SCHEMA_VERSION - ) + schema_version: Literal[ARTIFACT_SCHEMA_VERSION] = ARTIFACT_SCHEMA_VERSION artifact_id: ArtifactId media_id: MediaId generation_id: IndexGenerationId | None = None @@ -537,9 +541,7 @@ class SnippetProfile(StrEnum): class CreateSnippetCommand(ApplicationModel): media_id: MediaId = Field( - description=( - "Source video ID, normally copied from a search or query result." - ) + description=("Source video ID, normally copied from a search or query result.") ) start_seconds: float = Field( ge=0, @@ -596,9 +598,7 @@ class IndexStatusSummary(ApplicationModel): def _validate_media_id_window(self) -> "IndexStatusSummary": if self.media_count < len(self.media_ids): raise ValueError("media_count must cover every returned media ID") - if self.media_ids_truncated != ( - self.media_count > len(self.media_ids) - ): + if self.media_ids_truncated != (self.media_count > len(self.media_ids)): raise ValueError( "media_ids_truncated must reflect the returned media-ID window" ) @@ -623,9 +623,7 @@ class WorkspaceMediaCapability(ApplicationModel): default=(), description="Capability roles currently usable for this media item.", ) - identity_mode: CapabilityIdentityMode = ( - CapabilityIdentityMode.not_applicable - ) + identity_mode: CapabilityIdentityMode = CapabilityIdentityMode.not_applicable class WorkspaceMedia(ApplicationModel): @@ -723,9 +721,7 @@ def inspect(item: JsonValue) -> None: inspect(value) if forbidden: - raise ValueError( - "Search metadata contains internal location fields." - ) + raise ValueError("Search metadata contains internal location fields.") return value @model_validator(mode="after") @@ -749,17 +745,11 @@ def to_dict(self) -> dict[str, Any]: return self.model_dump(mode="json") def to_prediction(self) -> dict[str, list[dict[str, Any]]]: - return { - self.query_id: [ - hit.model_dump(mode="json") for hit in self.hits - ] - } + return {self.query_id: [hit.model_dump(mode="json") for hit in self.hits]} class FusionProvenance(ApplicationModel): - profile: Literal[FusionProfile.reciprocal_rank] = ( - FusionProfile.reciprocal_rank - ) + profile: Literal[FusionProfile.reciprocal_rank] = FusionProfile.reciprocal_rank rank_constant: int = Field(default=60, gt=0) overlap_rule: Literal["connected_intervals"] = "connected_intervals" requested_modalities: tuple[Identifier, ...] = () @@ -972,9 +962,7 @@ class QueryAnswer(ApplicationModel): def _validate_answer_grounding(self) -> "QueryAnswer": evidence_ids = {item.evidence_id for item in self.evidence} cited_ids = { - evidence_id - for claim in self.claims - for evidence_id in claim.evidence_ids + evidence_id for claim in self.claims for evidence_id in claim.evidence_ids } if not cited_ids.issubset(evidence_ids): raise ValueError("Every claim citation must resolve to evidence.") @@ -1113,9 +1101,7 @@ class PrepareModelsJobResult(ApplicationModel): class JobProgress(ApplicationModel): - schema_version: Literal[JOB_PROGRESS_SCHEMA_VERSION] = ( - JOB_PROGRESS_SCHEMA_VERSION - ) + schema_version: Literal[JOB_PROGRESS_SCHEMA_VERSION] = JOB_PROGRESS_SCHEMA_VERSION stage: str = Field(min_length=1, max_length=128) message: str = Field(min_length=1, max_length=512) current: int | None = Field(default=None, ge=0) @@ -1150,9 +1136,7 @@ class Job(ApplicationModel): def _validate_terminal_payload(self) -> "Job": if self.state == JobState.succeeded: if self.result is None or self.error is not None: - raise ValueError( - "succeeded jobs require a result and no error" - ) + raise ValueError("succeeded jobs require a result and no error") if self.result.kind != self.kind: raise ValueError("job result kind must match job kind") elif self.result is not None: diff --git a/src/vidxp/assets/upload_page/THIRD_PARTY_NOTICES.txt b/src/vidxp/assets/upload_page/THIRD_PARTY_NOTICES.txt new file mode 100644 index 0000000..c5254b1 --- /dev/null +++ b/src/vidxp/assets/upload_page/THIRD_PARTY_NOTICES.txt @@ -0,0 +1,527 @@ +VidXP upload page third-party notices + +This file covers JavaScript packages bundled into upload-page.js. +Build-only development packages are not included. + +======================================================================== + +@transloadit/prettier-bytes@0.3.5 +Declared license: MIT + +--- LICENSE.md --- + +The MIT License (MIT) + +Copyright (c) 2020 Transloadit (https://transloadit.com) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +======================================================================== + +@uppy/core@5.2.0 +Declared license: MIT + +--- LICENSE --- + +The MIT License (MIT) + +Copyright (c) 2018 Transloadit + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +======================================================================== + +@uppy/golden-retriever@5.2.1 +Declared license: MIT + +--- LICENSE --- + +The MIT License (MIT) + +Copyright (c) 2018 Transloadit + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +======================================================================== + +@uppy/store-default@5.0.0 +Declared license: MIT + +--- LICENSE --- + +The MIT License (MIT) + +Copyright (c) 2018 Transloadit + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +======================================================================== + +@uppy/tus@5.1.1 +Declared license: MIT + +--- LICENSE --- + +The MIT License (MIT) + +Copyright (c) 2018 Transloadit + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +======================================================================== + +@uppy/utils@7.2.0 +Declared license: MIT + +--- LICENSE --- + +The MIT License (MIT) + +Copyright (c) 2018 Transloadit + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +======================================================================== + +js-base64@3.9.2 +Declared license: BSD-3-Clause + +--- LICENSE.md --- + +Copyright (c) 2014, Dan Kogai +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +* Neither the name of {{{project}}} nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +======================================================================== + +lodash@4.18.1 +Declared license: MIT + +--- LICENSE --- + +Copyright OpenJS Foundation and other contributors + +Based on Underscore.js, copyright Jeremy Ashkenas, +DocumentCloud and Investigative Reporters & Editors + +This software consists of voluntary contributions made by many +individuals. For exact contribution history, see the revision history +available at https://github.com/lodash/lodash + +The following license applies to all parts of this software except as +documented below: + +==== + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +==== + +Copyright and related rights for sample code are waived via CC0. Sample +code is defined as all source code displayed within the prose of the +documentation. + +CC0: http://creativecommons.org/publicdomain/zero/1.0/ + +==== + +Files located in the node_modules and vendor directories are externally +maintained libraries used by this software which have their own +licenses; we recommend you read them, as their terms may differ from the +terms above. + +======================================================================== + +mime-match@1.0.2 +Declared license: ISC + +--- README.md license section --- + +## License(s) + +### ISC + +Copyright (c) 2016, Damon Oehlman + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +PERFORMANCE OF THIS SOFTWARE. + +======================================================================== + +namespace-emitter@2.0.1 +Declared license: MIT + +--- LICENSE.md --- + +The MIT License (MIT) + +Copyright (c) 2015 Seth Vincent + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +contributorS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +======================================================================== + +nanoid@5.1.16 +Declared license: MIT + +--- LICENSE --- + +The MIT License (MIT) + +Copyright 2017 Andrey Sitnik + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +======================================================================== + +preact@10.29.7 +Declared license: MIT + +--- LICENSE --- + +The MIT License (MIT) + +Copyright (c) 2015-present Jason Miller + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +======================================================================== + +querystringify@2.2.0 +Declared license: MIT + +--- LICENSE --- + +The MIT License (MIT) + +Copyright (c) 2015 Unshift.io, Arnout Kazemier, the Contributors. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +======================================================================== + +requires-port@1.0.0 +Declared license: MIT + +--- LICENSE --- + +The MIT License (MIT) + +Copyright (c) 2015 Unshift.io, Arnout Kazemier, the Contributors. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +======================================================================== + +tus-js-client@4.3.1 +Declared license: MIT + +--- LICENSE --- + +The MIT License (MIT) + +Copyright (c) 2015 tus - Resumable File Uploads + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +======================================================================== + +url-parse@1.5.10 +Declared license: MIT + +--- LICENSE --- + +The MIT License (MIT) + +Copyright (c) 2015 Unshift.io, Arnout Kazemier, the Contributors. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +======================================================================== + +wildcard@1.1.2 +Declared license: MIT + +--- README.md license section --- + +## License(s) + +### MIT + +Copyright (c) 2014 Damon Oehlman + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +'Software'), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/src/vidxp/assets/upload_page/index.html b/src/vidxp/assets/upload_page/index.html new file mode 100644 index 0000000..cf4657a --- /dev/null +++ b/src/vidxp/assets/upload_page/index.html @@ -0,0 +1,69 @@ + + + + + + + Upload video · VidXP + + + + +
+
+

VidXP media intake

+

Upload your video

+

Opening the secure upload handoff…

+
+ +
+

Expected file

+
+
Filename
+
Size
+
Maximum
+
Expires
+
+

+ VidXP checks the selected name and size before transfer. After upload, + the server probes and validates the media before it becomes available. +

+
+ +
+

Choose and upload

+ + +

No file selected.

+ + 0% +

+ Waiting for a valid handoff. +

+ +
+ + + +
+
+ +
+

VidXP status

+

State

+

The page will refresh the server-owned status automatically.

+
+
Upload intent
+
Import job
+
Media ID
+
+
+ + +
+ + diff --git a/src/vidxp/assets/upload_page/upload-page.css b/src/vidxp/assets/upload_page/upload-page.css new file mode 100644 index 0000000..f0b5fcb --- /dev/null +++ b/src/vidxp/assets/upload_page/upload-page.css @@ -0,0 +1 @@ +:root{color-scheme:light dark;font-family:Inter,ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,Segoe UI,sans-serif;background:#08101f;color:#e8eefc}*{box-sizing:border-box}body{min-height:100vh;margin:0;background:radial-gradient(circle at top left,#1b3261 0,transparent 38rem),#08101f}button,input{font:inherit}.shell{width:min(48rem,calc(100% - 2rem));margin:0 auto;padding:3rem 0 5rem}header{margin-bottom:2rem}.eyebrow,.label{color:#96b6ff;font-size:.78rem;font-weight:700;letter-spacing:.08em;text-transform:uppercase}h1,h2,p{margin-top:0}h1{margin-bottom:.65rem;font-size:clamp(2rem,8vw,3.6rem);line-height:1}h2{font-size:1.15rem}.lede,.hint,#next-action{color:#b6c4df;line-height:1.55}.card{margin-top:1rem;padding:1.4rem;border:1px solid #2c3d60;border-radius:1rem;background:#101a2dbf;box-shadow:0 1rem 3rem #0004}.facts{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:.85rem 1.25rem;margin:1rem 0}.facts div{min-width:0}.facts dt{margin-bottom:.2rem;color:#8fa0bd;font-size:.8rem}.facts dd{margin:0;overflow-wrap:anywhere;font-weight:650}.compact{grid-template-columns:1fr}.field-label{display:block;margin-bottom:.45rem;color:#b6c4df;font-weight:700}button,input::file-selector-button{display:inline-flex;min-height:2.75rem;align-items:center;justify-content:center;padding:.7rem 1rem;border:1px solid #6d96ee;border-radius:.7rem;background:#386ee0;color:#fff;cursor:pointer;font-weight:700}button:focus-visible,input:focus-visible{outline:3px solid #a9c3ff;outline-offset:3px}input[type=file]{display:block;width:100%;margin:.8rem 0;color:#b6c4df}input::file-selector-button{margin-right:.7rem}.selected-file{min-height:1.5rem;overflow-wrap:anywhere}progress{width:100%;height:1rem;accent-color:#6392f5}.state{min-height:1.5rem;margin:.8rem 0}.state.error{color:#ffaaa2}.state.success{color:#91e6bc}.actions{display:flex;flex-wrap:wrap;gap:.65rem}button.secondary{border-color:#52678c;background:#1b2943}button:disabled,input:disabled{cursor:not-allowed;opacity:.5}noscript{display:block;margin-top:1rem;color:#ffaaa2}@media(max-width:36rem){.shell{padding-top:1.5rem}.facts{grid-template-columns:1fr}.actions button{width:100%}}@media(prefers-reduced-motion:reduce){*,*:before,*:after{scroll-behavior:auto!important}} diff --git a/src/vidxp/assets/upload_page/upload-page.js b/src/vidxp/assets/upload_page/upload-page.js new file mode 100644 index 0000000..74e5d10 --- /dev/null +++ b/src/vidxp/assets/upload_page/upload-page.js @@ -0,0 +1,8 @@ +var ys=Object.create;var pr=Object.defineProperty;var bs=Object.getOwnPropertyDescriptor;var vs=Object.getOwnPropertyNames;var ws=Object.getPrototypeOf,Ss=Object.prototype.hasOwnProperty;var mo=r=>{throw TypeError(r)};var xs=(r,e,t)=>e in r?pr(r,e,{enumerable:!0,configurable:!0,writable:!0,value:t}):r[e]=t;var z=(r,e)=>()=>{try{return e||r((e={exports:{}}).exports,e),e.exports}catch(t){throw e=0,t}};var _s=(r,e,t,o)=>{if(e&&typeof e=="object"||typeof e=="function")for(let n of vs(e))!Ss.call(r,n)&&n!==t&&pr(r,n,{get:()=>e[n],enumerable:!(o=bs(e,n))||o.enumerable});return r};var Ne=(r,e,t)=>(t=r!=null?ys(ws(r)):{},_s(e||!r||!r.__esModule?pr(t,"default",{value:r,enumerable:!0}):t,r));var w=(r,e,t)=>xs(r,typeof e!="symbol"?e+"":e,t),hr=(r,e,t)=>e.has(r)||mo("Cannot "+t);var f=(r,e,t)=>(hr(r,e,"read from private field"),t?t.call(r):e.get(r)),S=(r,e,t)=>e.has(r)?mo("Cannot add the same private member more than once"):e instanceof WeakSet?e.add(r):e.set(r,t),P=(r,e,t,o)=>(hr(r,e,"write to private field"),o?o.call(r,t):e.set(r,t),t),v=(r,e,t)=>(hr(r,e,"access private method"),t);var Fo=z((yc,Uo)=>{"use strict";Uo.exports=function(e){if(typeof e!="number"||Number.isNaN(e))throw new TypeError(`Expected a number, got ${typeof e}`);let t=e<0,o=Math.abs(e);if(t&&(o=-o),o===0)return"0 B";let n=["B","KB","MB","GB","TB","PB","EB","ZB","YB"],i=Math.min(Math.floor(Math.log(o)/Math.log(1024)),n.length-1),s=Number(o/1024**i),a=n[i];return`${s>=10||s%1===0?Math.round(s):s.toFixed(1)} ${a}`}});var Oo=z((bc,Po)=>{"use strict";function Eo(r,e){this.text=r=r||"",this.hasWild=~r.indexOf("*"),this.separator=e,this.parts=r.split(e)}Eo.prototype.match=function(r){var e=!0,t=this.parts,o,n=t.length,i;if(typeof r=="string"||r instanceof String)if(!this.hasWild&&this.text!=r)e=!1;else{for(i=(r||"").split(this.separator),o=0;e&&o{var ks=Oo(),As=/[\/\+\.]/;Ro.exports=function(r,e){function t(o){var n=ks(o,r,As);return n&&n.length>=2}return e?t(e.split(";")[0]):t}});var $t=z((Ec,Io)=>{function Ls(r){var e=typeof r;return r!=null&&(e=="object"||e=="function")}Io.exports=Ls});var qo=z((Pc,No)=>{var Is=typeof global=="object"&&global&&global.Object===Object&&global;No.exports=Is});var Er=z((Oc,zo)=>{var Ns=qo(),qs=typeof self=="object"&&self&&self.Object===Object&&self,zs=Ns||qs||Function("return this")();zo.exports=zs});var Mo=z((Rc,Bo)=>{var Bs=Er(),Ms=function(){return Bs.Date.now()};Bo.exports=Ms});var $o=z((Tc,Do)=>{var Ds=/\s/;function $s(r){for(var e=r.length;e--&&Ds.test(r.charAt(e)););return e}Do.exports=$s});var Ho=z((jc,Go)=>{var Gs=$o(),Hs=/^\s+/;function Ws(r){return r&&r.slice(0,Gs(r)+1).replace(Hs,"")}Go.exports=Ws});var Pr=z((kc,Wo)=>{var Vs=Er(),Xs=Vs.Symbol;Wo.exports=Xs});var Yo=z((Ac,Ko)=>{var Vo=Pr(),Xo=Object.prototype,Ks=Xo.hasOwnProperty,Ys=Xo.toString,dt=Vo?Vo.toStringTag:void 0;function Js(r){var e=Ks.call(r,dt),t=r[dt];try{r[dt]=void 0;var o=!0}catch{}var n=Ys.call(r);return o&&(e?r[dt]=t:delete r[dt]),n}Ko.exports=Js});var Zo=z((Cc,Jo)=>{var Zs=Object.prototype,Qs=Zs.toString;function ea(r){return Qs.call(r)}Jo.exports=ea});var rn=z((Lc,tn)=>{var Qo=Pr(),ta=Yo(),ra=Zo(),oa="[object Null]",na="[object Undefined]",en=Qo?Qo.toStringTag:void 0;function ia(r){return r==null?r===void 0?na:oa:en&&en in Object(r)?ta(r):ra(r)}tn.exports=ia});var nn=z((Ic,on)=>{function sa(r){return r!=null&&typeof r=="object"}on.exports=sa});var an=z((Nc,sn)=>{var aa=rn(),la=nn(),ua="[object Symbol]";function ca(r){return typeof r=="symbol"||la(r)&&aa(r)==ua}sn.exports=ca});var dn=z((qc,cn)=>{var da=Ho(),ln=$t(),fa=an(),un=NaN,pa=/^[-+]0x[0-9a-f]+$/i,ha=/^0b[01]+$/i,ma=/^0o[0-7]+$/i,ga=parseInt;function ya(r){if(typeof r=="number")return r;if(fa(r))return un;if(ln(r)){var e=typeof r.valueOf=="function"?r.valueOf():r;r=ln(e)?e+"":e}if(typeof r!="string")return r===0?r:+r;r=da(r);var t=ha.test(r);return t||ma.test(r)?ga(r.slice(2),t?2:8):pa.test(r)?un:+r}cn.exports=ya});var hn=z((zc,pn)=>{var ba=$t(),Or=Mo(),fn=dn(),va="Expected a function",wa=Math.max,Sa=Math.min;function xa(r,e,t){var o,n,i,s,a,l,p=0,m=!1,h=!1,U=!0;if(typeof r!="function")throw new TypeError(va);e=fn(e)||0,ba(t)&&(m=!!t.leading,h="maxWait"in t,i=h?wa(fn(t.maxWait)||0,e):i,U="trailing"in t?!!t.trailing:U);function j(k){var N=o,te=n;return o=n=void 0,p=k,s=r.apply(te,N),s}function O(k){return p=k,a=setTimeout(B,e),m?j(k):s}function T(k){var N=k-l,te=k-p,Z=e-N;return h?Sa(Z,i-te):Z}function I(k){var N=k-l,te=k-p;return l===void 0||N>=e||N<0||h&&te>=i}function B(){var k=Or();if(I(k))return _(k);a=setTimeout(B,T(k))}function _(k){return a=void 0,U&&o?j(k):(o=n=void 0,s)}function F(){a!==void 0&&clearTimeout(a),p=0,o=l=n=a=void 0}function L(){return a===void 0?s:_(Or())}function K(){var k=Or(),N=I(k);if(o=arguments,n=this,l=k,N){if(a===void 0)return O(l);if(h)return clearTimeout(a),a=setTimeout(B,e),j(l)}return a===void 0&&(a=setTimeout(B,e)),s}return K.cancel=F,K.flush=L,K}pn.exports=xa});var Rr=z((Bc,mn)=>{var _a=hn(),Ua=$t(),Fa="Expected a function";function Ea(r,e,t){var o=!0,n=!0;if(typeof r!="function")throw new TypeError(Fa);return Ua(t)&&(o="leading"in t?!!t.leading:o,n="trailing"in t?!!t.trailing:n),_a(r,e,{leading:o,maxWait:e,trailing:n})}mn.exports=Ea});var yn=z((Mc,gn)=>{gn.exports=function(){var e={},t=e._fns={};e.emit=function(s,a,l,p,m,h,U){var j=o(s);j.length&&n(s,j,[a,l,p,m,h,U])},e.on=function(s,a){t[s]||(t[s]=[]),t[s].push(a)},e.once=function(s,a){function l(){a.apply(this,arguments),e.off(s,l)}this.on(s,l)},e.off=function(s,a){var l=[];if(s&&a){var p=this._fns[s],m=0,h=p?p.length:0;for(m;m{"use strict";pi.exports=function(e,t){if(t=t.split(":")[0],e=+e,!e)return!1;switch(t){case"http":case"ws":return e!==80;case"https":case"wss":return e!==443;case"ftp":return e!==21;case"gopher":return e!==70;case"file":return!1}return e!==0}});var yi=z(Wr=>{"use strict";var cl=Object.prototype.hasOwnProperty,dl;function mi(r){try{return decodeURIComponent(r.replace(/\+/g," "))}catch{return null}}function gi(r){try{return encodeURIComponent(r)}catch{return null}}function fl(r){for(var e=/([^=?#&]+)=?([^&]*)/g,t={},o;o=e.exec(r);){var n=mi(o[1]),i=mi(o[2]);n===null||i===null||n in t||(t[n]=i)}return t}function pl(r,e){e=e||"";var t=[],o,n;typeof e!="string"&&(e="?");for(n in r)if(cl.call(r,n)){if(o=r[n],!o&&(o===null||o===dl||isNaN(o))&&(o=""),n=gi(n),o=gi(o),n===null||o===null)continue;t.push(n+"="+o)}return t.length?e+t.join("&"):""}Wr.stringify=pl;Wr.parse=fl});var Fi=z((Od,Ui)=>{"use strict";var vi=hi(),tr=yi(),hl=/^[\x00-\x20\u00a0\u1680\u2000-\u200a\u2028\u2029\u202f\u205f\u3000\ufeff]+/,wi=/[\n\r\t]/g,ml=/^[A-Za-z][A-Za-z0-9+-.]*:\/\//,Si=/:\d+$/,gl=/^([a-z][a-z0-9.+-]*:)?(\/\/)?([\\/]+)?([\S\s]*)/i,yl=/^[a-zA-Z]:/;function Xr(r){return(r||"").toString().replace(hl,"")}var Vr=[["#","hash"],["?","query"],function(e,t){return ae(t.protocol)?e.replace(/\\/g,"/"):e},["/","pathname"],["@","auth",1],[NaN,"host",void 0,1,1],[/:(\d*)$/,"port",void 0,1],[NaN,"hostname",void 0,1,1]],bi={hash:1,query:1};function xi(r){var e;typeof window<"u"?e=window:typeof global<"u"?e=global:typeof self<"u"?e=self:e={};var t=e.location||{};r=r||t;var o={},n=typeof r,i;if(r.protocol==="blob:")o=new le(unescape(r.pathname),{});else if(n==="string"){o=new le(r,{});for(i in bi)delete o[i]}else if(n==="object"){for(i in r)i in bi||(o[i]=r[i]);o.slashes===void 0&&(o.slashes=ml.test(r.href))}return o}function ae(r){return r==="file:"||r==="ftp:"||r==="http:"||r==="https:"||r==="ws:"||r==="wss:"}function _i(r,e){r=Xr(r),r=r.replace(wi,""),e=e||{};var t=gl.exec(r),o=t[1]?t[1].toLowerCase():"",n=!!t[2],i=!!t[3],s=0,a;return n?i?(a=t[2]+t[3]+t[4],s=t[2].length+t[3].length):(a=t[2]+t[4],s=t[2].length):i?(a=t[3]+t[4],s=t[3].length):a=t[4],o==="file:"?s>=2&&(a=a.slice(2)):ae(o)?a=t[4]:o?n&&(a=a.slice(2)):s>=2&&ae(e.protocol)&&(a=t[4]),{protocol:o,slashes:n||ae(o),slashesCount:s,rest:a}}function bl(r,e){if(r==="")return e;for(var t=(e||"/").split("/").slice(0,-1).concat(r.split("/")),o=t.length,n=t[o-1],i=!1,s=0;o--;)t[o]==="."?t.splice(o,1):t[o]===".."?(t.splice(o,1),s++):s&&(o===0&&(i=!0),t.splice(o,1),s--);return i&&t.unshift(""),(n==="."||n==="..")&&t.push(""),t.join("/")}function le(r,e,t){if(r=Xr(r),r=r.replace(wi,""),!(this instanceof le))return new le(r,e,t);var o,n,i,s,a,l,p=Vr.slice(),m=typeof e,h=this,U=0;for(m!=="object"&&m!=="string"&&(t=e,e=null),t&&typeof t!="function"&&(t=tr.parse),e=xi(e),n=_i(r||"",e),o=!n.protocol&&!n.slashes,h.slashes=n.slashes||o&&e.slashes,h.protocol=n.protocol||e.protocol||"",r=n.rest,(n.protocol==="file:"&&(n.slashesCount!==2||yl.test(r))||!n.slashes&&(n.protocol||n.slashesCount<2||!ae(h.protocol)))&&(p[3]=[/(.*)/,"pathname"]);U"error"in r&&!!r.error,Fs=r=>r.progress.uploadComplete;function yr(r){return r.filter(e=>!Us(e)&&!Fs(e))}function br(r){return r.filter(e=>!e.progress?.uploadStarted||!e.isRestored)}function ze(r){let e=r.lastIndexOf(".");return e===-1||e===r.length-1?{name:r,extension:void 0}:{name:r.slice(0,e),extension:r.slice(e+1)}}var vr={__proto__:null,md:"text/markdown",markdown:"text/markdown",mp4:"video/mp4",mp3:"audio/mp3",svg:"image/svg+xml",jpg:"image/jpeg",png:"image/png",webp:"image/webp",gif:"image/gif",heic:"image/heic",heif:"image/heif",yaml:"text/yaml",yml:"text/yaml",csv:"text/csv",tsv:"text/tab-separated-values",tab:"text/tab-separated-values",avi:"video/x-msvideo",mks:"video/x-matroska",mkv:"video/x-matroska",mov:"video/quicktime",dicom:"application/dicom",doc:"application/msword",msg:"application/vnd.ms-outlook",docm:"application/vnd.ms-word.document.macroenabled.12",docx:"application/vnd.openxmlformats-officedocument.wordprocessingml.document",dot:"application/msword",dotm:"application/vnd.ms-word.template.macroenabled.12",dotx:"application/vnd.openxmlformats-officedocument.wordprocessingml.template",xla:"application/vnd.ms-excel",xlam:"application/vnd.ms-excel.addin.macroenabled.12",xlc:"application/vnd.ms-excel",xlf:"application/x-xliff+xml",xlm:"application/vnd.ms-excel",xls:"application/vnd.ms-excel",xlsb:"application/vnd.ms-excel.sheet.binary.macroenabled.12",xlsm:"application/vnd.ms-excel.sheet.macroenabled.12",xlsx:"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",xlt:"application/vnd.ms-excel",xltm:"application/vnd.ms-excel.template.macroenabled.12",xltx:"application/vnd.openxmlformats-officedocument.spreadsheetml.template",xlw:"application/vnd.ms-excel",txt:"text/plain",text:"text/plain",conf:"text/plain",log:"text/plain",pdf:"application/pdf",zip:"application/zip","7z":"application/x-7z-compressed",rar:"application/x-rar-compressed",tar:"application/x-tar",gz:"application/gzip",dmg:"application/x-apple-diskimage"};function Be(r){if(r.type)return r.type;let e=r.name?ze(r.name).extension?.toLowerCase():null;return e&&e in vr?vr[e]:"application/octet-stream"}function Es(r){return r.charCodeAt(0).toString(32)}function go(r){let e="";return r.replace(/[^A-Z0-9]/gi,t=>(e+=`-${Es(t)}`,"/"))+e}function wr(r,e){let t=e||"uppy";return typeof r.name=="string"&&(t+=`-${go(r.name.toLowerCase())}`),r.type!==void 0&&(t+=`-${r.type}`),r.meta&&typeof r.meta.relativePath=="string"&&(t+=`-${go(r.meta.relativePath.toLowerCase())}`),r.data?.size!==void 0&&(t+=`-${r.data.size}`),r.data.lastModified!==void 0&&(t+=`-${r.data.lastModified}`),t}function Ps(r){return!r.isRemote||!r.remote?!1:new Set(["box","dropbox","drive","facebook","unsplash"]).has(r.remote.provider)}function Sr(r,e){if(Ps(r))return r.id;let t=Be(r);return wr({...r,type:t},e)}function qt(r,e){return r===!0?Object.keys(e):Array.isArray(r)?r:[]}function xr(r){return r<10?`0${r}`:r.toString()}function Ue(){let r=new Date,e=xr(r.getHours()),t=xr(r.getMinutes()),o=xr(r.getSeconds());return`${e}:${t}:${o}`}function Os(r){return r?r.readyState===4&&r.status===0:!1}var _r=Os;function Rs(r){return new Error("Cancelled",{cause:r})}function yo(r){if(r!=null){let e=()=>this.abort(r.reason);r.addEventListener("abort",e,{once:!0});let t=()=>{r.removeEventListener("abort",e)};this.then?.(t,t)}return this}var ue,ne,ce,Me,ie,De,Fe,W,Ur,it,bo,vo,wo,zt,$e,st=class{constructor(e){S(this,W);S(this,ue,0);S(this,ne,[]);S(this,ce,!1);S(this,Me);S(this,ie,1);S(this,De);S(this,Fe);w(this,"limit");S(this,zt,()=>this.resume());S(this,$e,()=>{if(f(this,ce)){P(this,Fe,setTimeout(f(this,$e),0));return}P(this,ie,this.limit),this.limit=Math.ceil((f(this,De)+f(this,ie))/2);for(let e=f(this,ie);e<=this.limit;e++)v(this,W,it).call(this);f(this,De)-f(this,ie)>3?P(this,Fe,setTimeout(f(this,$e),2e3)):P(this,ie,Math.floor(f(this,ie)/2))});typeof e!="number"||e===0?this.limit=1/0:this.limit=e}run(e,t){return!f(this,ce)&&f(this,ue){let n=this.run(()=>(e(...o),queueMicrotask(()=>n.done()),()=>{}),t);return{abortOn:yo,abort(){n.abort()}}}}wrapPromiseFunction(e,t){return(...o)=>{let n,i=new Promise((s,a)=>{n=this.run(()=>{let l,p;try{p=Promise.resolve(e(...o))}catch(m){p=Promise.reject(m)}return p.then(m=>{l?a(l):(n.done(),s(m))},m=>{l?a(l):(n.done(),a(m))}),m=>{l=Rs(m)}},t)});return i.abort=s=>{n.abort(s)},i.abortOn=yo,i}}resume(){P(this,ce,!1),clearTimeout(f(this,Me));for(let e=0;e1&&Number.isFinite(this.limit)&&(P(this,De,this.limit-1),this.limit=f(this,ie),P(this,Fe,setTimeout(f(this,$e),e)))}get isPaused(){return f(this,ce)}};ue=new WeakMap,ne=new WeakMap,ce=new WeakMap,Me=new WeakMap,ie=new WeakMap,De=new WeakMap,Fe=new WeakMap,W=new WeakSet,Ur=function(e){P(this,ue,f(this,ue)+1);let t=!1,o;try{o=e()}catch(n){throw P(this,ue,f(this,ue)-1),n}return{abort:n=>{t||(t=!0,P(this,ue,f(this,ue)-1),o?.(n),v(this,W,it).call(this))},done:()=>{t||(t=!0,P(this,ue,f(this,ue)-1),v(this,W,it).call(this))}}},it=function(){queueMicrotask(()=>v(this,W,bo).call(this))},bo=function(){if(f(this,ce)||f(this,ue)>=this.limit||f(this,ne).length===0)return;let e=f(this,ne).shift();if(e==null)throw new Error("Invariant violation: next is null");let t=v(this,W,Ur).call(this,e.fn);e.abort=t.abort,e.done=t.done},vo=function(e,t){let o={fn:e,priority:t?.priority||0,abort:()=>{v(this,W,wo).call(this,o)},done:()=>{throw new Error("Cannot mark a queued request as done: this indicates a bug")}},n=f(this,ne).findIndex(i=>o.priority>i.priority);return n===-1?f(this,ne).push(o):f(this,ne).splice(n,0,o),o},wo=function(e){let t=f(this,ne).indexOf(e);t!==-1&&f(this,ne).splice(t,1)},zt=new WeakMap,$e=new WeakMap;function Ts(r,e,t){let o=[];return r.forEach(n=>typeof n!="string"?o.push(n):e[Symbol.split](n).forEach((i,s,a)=>{i!==""&&o.push(i),s{throw new Error(`missing string: ${r}`)},at,lt,Fr,fe=class{constructor(e,{onMissingKey:t=js}={}){S(this,lt);w(this,"locale");S(this,at);this.locale={strings:{},pluralize(o){return o===1?0:1}},Array.isArray(e)?e.forEach(v(this,lt,Fr),this):v(this,lt,Fr).call(this,e),P(this,at,t)}translate(e,t){return this.translateArray(e,t).join("")}translateArray(e,t){let o=this.locale.strings[e];if(o==null&&(f(this,at).call(this,e),o=e),typeof o=="object"){if(t&&typeof t.smart_count<"u"){let i=this.locale.pluralize(t.smart_count);return So(o[i],t)}throw new Error("Attempted to use a string with plural forms, but no value was given for %{smart_count}")}if(typeof o!="string")throw new Error("string was not a string");return So(o,t)}};at=new WeakMap,lt=new WeakSet,Fr=function(e){if(!e?.strings)return;let t=this.locale;Object.assign(this.locale,{strings:{...t.strings,...e.strings},pluralize:e.pluralize||t.pluralize})};var pe=class{constructor(e,t){w(this,"uppy");w(this,"opts");w(this,"id");w(this,"defaultLocale");w(this,"i18n");w(this,"i18nArray");w(this,"type");w(this,"VERSION");this.uppy=e,this.opts=t??{}}getPluginState(){let{plugins:e}=this.uppy.getState();return e?.[this.id]||{}}setPluginState(e){let{plugins:t}=this.uppy.getState();this.uppy.setState({plugins:{...t,[this.id]:{...t[this.id],...e}}})}setOptions(e){this.opts={...this.opts,...e},this.setPluginState(void 0),this.i18nInit()}i18nInit(){let e=new fe([this.defaultLocale,this.uppy.locale,this.opts.locale]);this.i18n=e.translate.bind(e),this.i18nArray=e.translateArray.bind(e),this.setPluginState(void 0)}addTarget(e){throw new Error("Extend the addTarget method to add your plugin to another plugin's target")}install(){}uninstall(){}update(e){}afterUpdate(){}};var se,ut,Ge=class{constructor(e){S(this,se);S(this,ut,[]);P(this,se,e)}on(e,t){return f(this,ut).push([e,t]),f(this,se).on(e,t)}remove(){for(let[e,t]of f(this,ut).splice(0))f(this,se).off(e,t)}onFilePause(e,t){this.on("upload-pause",(o,n)=>{e===o?.id&&t(n)})}onFileRemove(e,t){this.on("file-removed",o=>{e===o.id&&t(o.id)})}onPause(e,t){this.on("upload-pause",(o,n)=>{e===o?.id&&t(n)})}onRetry(e,t){this.on("upload-retry",o=>{e===o?.id&&t()})}onRetryAll(e,t){this.on("retry-all",()=>{f(this,se).getFile(e)&&t()})}onPauseAll(e,t){this.on("pause-all",()=>{f(this,se).getFile(e)&&t()})}onCancelAll(e,t){this.on("cancel-all",(...o)=>{f(this,se).getFile(e)&&t(...o)})}onResumeAll(e,t){this.on("resume-all",()=>{f(this,se).getFile(e)&&t()})}};se=new WeakMap,ut=new WeakMap;var xo={debug:()=>{},warn:()=>{},error:(...r)=>console.error(`[Uppy] [${Ue()}]`,...r)},_o={debug:(...r)=>console.debug(`[Uppy] [${Ue()}]`,...r),warn:(...r)=>console.warn(`[Uppy] [${Ue()}]`,...r),error:(...r)=>console.error(`[Uppy] [${Ue()}]`,...r)};var ct=Ne(Fo(),1),jo=Ne(To(),1);var ko={maxFileSize:null,minFileSize:null,maxTotalFileSize:null,maxNumberOfFiles:null,minNumberOfFiles:null,allowedFileTypes:null,requiredMetaFields:[]},Y=class extends Error{constructor(t,o){super(t);w(this,"isUserFacing");w(this,"file");w(this,"isRestriction",!0);this.isUserFacing=o?.isUserFacing??!0,o?.file&&(this.file=o.file)}},Bt=class{constructor(e,t){w(this,"getI18n");w(this,"getOpts");this.getI18n=t,this.getOpts=()=>{let o=e();if(o.restrictions?.allowedFileTypes!=null&&!Array.isArray(o.restrictions.allowedFileTypes))throw new TypeError("`restrictions.allowedFileTypes` must be an array");return o}}validateAggregateRestrictions(e,t){let{maxTotalFileSize:o,maxNumberOfFiles:n}=this.getOpts().restrictions;if(n&&e.filter(s=>!s.isGhost).length+t.length>n)throw new Y(`${this.getI18n()("youCanOnlyUploadX",{smart_count:n})}`);if(o){let i=[...e,...t].reduce((s,a)=>s+(a.size??0),0);if(i>o)throw new Y(this.getI18n()("aggregateExceedsSize",{sizeAllowed:(0,ct.default)(o),size:(0,ct.default)(i)}))}}validateSingleFile(e){let{maxFileSize:t,minFileSize:o,allowedFileTypes:n}=this.getOpts().restrictions;if(n&&!n.some(s=>s.includes("/")?e.type?(0,jo.default)(e.type.replace(/;.*?$/,""),s):!1:s[0]==="."&&e.extension?e.extension.toLowerCase()===s.slice(1).toLowerCase():!1)){let s=n.join(", ");throw new Y(this.getI18n()("youCanOnlyUploadFileTypes",{types:s}),{file:e})}if(t&&e.size!=null&&e.size>t)throw new Y(this.getI18n()("exceedsSize",{size:(0,ct.default)(t),file:e.name??this.getI18n()("unnamed")}),{file:e});if(o&&e.size!=null&&e.size{this.validateSingleFile(o)}),this.validateAggregateRestrictions(e,t)}validateMinNumberOfFiles(e){let{minNumberOfFiles:t}=this.getOpts().restrictions;if(t&&Object.keys(e).length{f(this,He).delete(e)}}};He=new WeakMap,Dt=new WeakSet,Co=function(...e){f(this,He).forEach(t=>{t(...e)})},w(Mt,"VERSION",Ao.version);var Lo=Mt;var Sn=Ne(Rr(),1),xn=Ne(yn(),1);var Pa="useandom-26T198340PX75pxJACKVERYMINDBUSHWOLF_GQZbfghjklqvwyzrict";var bn=(r=21)=>{let e="",t=r|0;for(;t-- >0;)e+=Pa[Math.random()*64|0];return e};var vn={name:"@uppy/core",description:"Core module for the extensible JavaScript file upload widget with support for drag&drop, resumable uploads, previews, restrictions, file processing/encoding, remote providers like Instagram, Dropbox, Google Drive, S3 and more :dog:",version:"5.2.0",license:"MIT",style:"dist/style.min.css",type:"module",sideEffects:["*.css"],scripts:{build:"tsc --build tsconfig.build.json","build:css":"sass --load-path=../../ src/style.scss dist/style.css && postcss dist/style.css -u cssnano -o dist/style.min.css",typecheck:"tsc --build",test:"vitest run --environment=jsdom --silent='passed-only'"},keywords:["file uploader","uppy","uppy-plugin"],homepage:"https://uppy.io",bugs:{url:"https://github.com/transloadit/uppy/issues"},repository:{type:"git",url:"git+https://github.com/transloadit/uppy.git"},files:["src","lib","dist","CHANGELOG.md"],exports:{".":"./lib/index.js","./css/style.min.css":"./dist/style.min.css","./css/style.css":"./dist/style.css","./css/style.scss":"./src/style.scss","./package.json":"./package.json"},dependencies:{"@transloadit/prettier-bytes":"^0.3.4","@uppy/store-default":"^5.0.0","@uppy/utils":"^7.1.4",lodash:"^4.17.21","mime-match":"^1.0.2","namespace-emitter":"^2.0.1",nanoid:"^5.0.9",preact:"^10.5.13"},devDependencies:{"@types/deep-freeze":"^0",cssnano:"^7.0.7","deep-freeze":"^0.0.1",jsdom:"^26.1.0",postcss:"^8.5.6","postcss-cli":"^11.0.1",sass:"^1.89.2",typescript:"^5.8.3",vitest:"^3.2.4"}};function Tr(r,e){return e.name?e.name:r.split("/")[0]==="image"?`${r.split("/")[0]}.${r.split("/")[1]}`:"noname"}var wn={strings:{addBulkFilesFailed:{0:"Failed to add %{smart_count} file due to an internal error",1:"Failed to add %{smart_count} files due to internal errors"},youCanOnlyUploadX:{0:"You can only upload %{smart_count} file",1:"You can only upload %{smart_count} files"},youHaveToAtLeastSelectX:{0:"You have to select at least %{smart_count} file",1:"You have to select at least %{smart_count} files"},aggregateExceedsSize:"You selected %{size} of files, but maximum allowed size is %{sizeAllowed}",exceedsSize:"%{file} exceeds maximum allowed size of %{size}",missingRequiredMetaField:"Missing required meta fields",missingRequiredMetaFieldOnFile:"Missing required meta fields in %{fileName}",inferiorSize:"This file is smaller than the allowed size of %{size}",youCanOnlyUploadFileTypes:"You can only upload: %{types}",noMoreFilesAllowed:"Cannot add more files",noDuplicates:"Cannot add the duplicate file '%{fileName}', it already exists",companionError:"Connection with Companion failed",authAborted:"Authentication aborted",companionUnauthorizeHint:"To unauthorize to your %{provider} account, please go to %{url}",failedToUpload:"Failed to upload %{file}",noInternetConnection:"No Internet connection",connectedToInternet:"Connected to the Internet",noFilesFound:"You have no files or folders here",noSearchResults:"Unfortunately, there are no results for this search",selectX:{0:"Select %{smart_count}",1:"Select %{smart_count}"},allFilesFromFolderNamed:"All files from folder %{name}",openFolderNamed:"Open folder %{name}",cancel:"Cancel",logOut:"Log out",logIn:"Log in",pickFiles:"Pick files",pickPhotos:"Pick photos",filter:"Filter",resetFilter:"Reset filter",loading:"Loading...",loadedXFiles:"Loaded %{numFiles} files",authenticateWithTitle:"Please authenticate with %{pluginName} to select files",authenticateWith:"Connect to %{pluginName}",signInWithGoogle:"Sign in with Google",searchImages:"Search for images",enterTextToSearch:"Enter text to search for images",search:"Search",resetSearch:"Reset search",emptyFolderAdded:"No files were added from empty folder",addedNumFiles:"Added %{numFiles} file(s)",folderAlreadyAdded:'The folder "%{folder}" was already added',folderAdded:{0:"Added %{smart_count} file from %{folder}",1:"Added %{smart_count} files from %{folder}"},additionalRestrictionsFailed:"%{count} additional restrictions were not fulfilled",unnamed:"Unnamed",pleaseWait:"Please wait"}};function jr(r){if(r==null&&typeof navigator<"u"&&(r=navigator.userAgent),!r)return!0;let e=/Edge\/(\d+\.\d+)/.exec(r);if(!e)return!0;let o=e[1].split(".",2),n=parseInt(o[0],10),i=parseInt(o[1],10);return n<15||n===15&&i<15063||n>18||n===18&&i>=18218}var Gt={totalProgress:0,allowNewUpload:!0,error:null,recoveredState:null},re,oe,ht,Pe,We,Ve,Oe,x,Ee,Ar,_n,Un,Fn,Cr,Lr,Ir,Nr,Ht,qr,Re,En,Pn,he,mt,ft,On,zr,pt,Wt=class Wt{constructor(e){S(this,x);S(this,re,Object.create(null));S(this,oe);S(this,ht);S(this,Pe,(0,xn.default)());S(this,We,new Set);S(this,Ve,new Set);S(this,Oe,new Set);w(this,"defaultLocale");w(this,"locale");w(this,"opts");w(this,"store");w(this,"i18n");w(this,"i18nArray");w(this,"scheduledAutoProceed",null);w(this,"wasOffline",!1);S(this,Ht,(e,t)=>{let o=e?this.getFile(e.id):void 0;if(e==null||!o){this.log(`Not setting progress for a file that has been removed: ${e?.id}`);return}if(o.progress.percentage===100){this.log(`Not setting progress for a file that has been already uploaded: ${e.id}`);return}let n={bytesTotal:t.bytesTotal,percentage:t.bytesTotal!=null&&Number.isFinite(t.bytesTotal)&&t.bytesTotal>0?Math.round(t.bytesUploaded/t.bytesTotal*100):void 0};o.progress.uploadStarted!=null?this.setFileState(e.id,{progress:{...o.progress,...n,bytesUploaded:t.bytesUploaded}}):this.setFileState(e.id,{progress:{...o.progress,...n}}),f(this,Re).call(this)});S(this,Re,(0,Sn.default)(()=>v(this,x,qr).call(this),500,{leading:!0,trailing:!0}));S(this,he,this.updateOnlineStatus.bind(this));S(this,mt,new Map);this.defaultLocale=wn;let t={id:"uppy",autoProceed:!1,allowMultipleUploadBatches:!0,debug:!1,restrictions:ko,meta:{},onBeforeFileAdded:(n,i)=>!Object.hasOwn(i,n.id),onBeforeUpload:n=>n,store:new Lo,logger:xo,infoTimeout:5e3},o={...t,...e};this.opts={...o,restrictions:{...t.restrictions,...e?.restrictions}},e?.logger&&e.debug?this.log("You are using a custom `logger`, but also set `debug: true`, which uses built-in logger to output logs to console. Ignoring `debug: true` and using your custom `logger`.","warning"):e?.debug&&(this.opts.logger=_o),this.log(`Using Core v${Wt.VERSION}`),this.i18nInit(),this.store=this.opts.store,this.setState({...Gt,plugins:{},files:{},currentUploads:{},capabilities:{uploadProgress:jr(),individualCancellation:!0,resumableUploads:!1},meta:{...this.opts.meta},info:[]}),P(this,oe,new Bt(()=>this.opts,()=>this.i18n)),P(this,ht,this.store.subscribe((n,i,s)=>{this.emit("state-update",n,i,s),this.updateAll(i)})),this.opts.debug&&typeof window<"u"&&(window[this.opts.id]=this),v(this,x,Pn).call(this)}emit(e,...t){f(this,Pe).emit(e,...t)}on(e,t){return f(this,Pe).on(e,t),this}once(e,t){return f(this,Pe).once(e,t),this}off(e,t){return f(this,Pe).off(e,t),this}updateAll(e){this.iteratePlugins(t=>{t.update(e)})}setState(e){this.store.setState(e)}getState(){return this.store.getState()}patchFilesState(e){let t=this.getState().files;this.setState({files:{...t,...Object.fromEntries(Object.entries(e).map(([o,n])=>[o,{...t[o],...n}]))}})}setFileState(e,t){if(!this.getState().files[e])throw new Error(`Can’t set state for ${e} (the file could have been removed)`);this.patchFilesState({[e]:t})}i18nInit(){let e=o=>this.log(`Missing i18n string: ${o}`,"error"),t=new fe([this.defaultLocale,this.opts.locale],{onMissingKey:e});this.i18n=t.translate.bind(t),this.i18nArray=t.translateArray.bind(t),this.locale=t.locale}setOptions(e){this.opts={...this.opts,...e,restrictions:{...this.opts.restrictions,...e?.restrictions}},e.meta&&this.setMeta(e.meta),this.i18nInit(),e.locale&&this.iteratePlugins(t=>{t.setOptions(e)}),this.setState(void 0)}resetProgress(){let e={percentage:0,bytesUploaded:!1,uploadComplete:!1,uploadStarted:null},t={...this.getState().files},o=Object.create(null);Object.keys(t).forEach(n=>{o[n]={...t[n],progress:{...t[n].progress,...e},tus:void 0,transloadit:void 0}}),this.setState({files:o,...Gt})}clear(){let{capabilities:e,currentUploads:t}=this.getState();if(Object.keys(t).length>0&&!e.individualCancellation)throw new Error("The installed uploader plugin does not allow removing files during an upload.");this.setState({...Gt,files:{}})}addPreProcessor(e){f(this,We).add(e)}removePreProcessor(e){return f(this,We).delete(e)}addPostProcessor(e){f(this,Oe).add(e)}removePostProcessor(e){return f(this,Oe).delete(e)}addUploader(e){f(this,Ve).add(e)}removeUploader(e){return f(this,Ve).delete(e)}setMeta(e){let t={...this.getState().meta,...e},o={...this.getState().files};Object.keys(o).forEach(n=>{o[n]={...o[n],meta:{...o[n].meta,...e}}}),this.log("Adding metadata:"),this.log(e),this.setState({meta:t,files:o})}setFileMeta(e,t){let o={...this.getState().files};if(!o[e]){this.log(`Was trying to set metadata for a file that has been removed: ${e}`);return}let n={...o[e].meta,...t};o[e]={...o[e],meta:n},this.setState({files:o})}getFile(e){return this.getState().files[e]}getFiles(){let{files:e}=this.getState();return Object.values(e)}getFilesByIds(e){return e.map(t=>this.getFile(t))}getObjectOfFilesPerState(){let{files:e,totalProgress:t,error:o}=this.getState(),n=Object.values(e),i=[],s=[],a=[],l=[],p=[],m=[],h=[],U=[],j=[];for(let O of n){let{progress:T}=O;!T.uploadComplete&&T.uploadStarted&&(i.push(O),O.isPaused||U.push(O)),T.uploadStarted||s.push(O),(T.uploadStarted||T.preprocess||T.postprocess)&&a.push(O),T.uploadStarted&&l.push(O),O.isPaused&&p.push(O),T.uploadComplete&&m.push(O),O.error&&h.push(O),(T.preprocess||T.postprocess)&&j.push(O)}return{newFiles:s,startedFiles:a,uploadStartedFiles:l,pausedFiles:p,completeFiles:m,erroredFiles:h,inProgressFiles:i,inProgressNotPausedFiles:U,processingFiles:j,isUploadStarted:l.length>0,isAllComplete:t===100&&m.length===n.length&&j.length===0,isAllErrored:!!o&&h.length===n.length,isAllPaused:i.length!==0&&p.length===i.length,isUploadInProgress:i.length>0,isSomeGhost:n.some(O=>O.isGhost)}}validateRestrictions(e,t=this.getFiles()){try{f(this,oe).validate(t,[e])}catch(o){return o}return null}validateSingleFile(e){try{f(this,oe).validateSingleFile(e)}catch(t){return t.message}return null}validateAggregateRestrictions(e){let t=this.getFiles();try{f(this,oe).validateAggregateRestrictions(t,e)}catch(o){return o.message}return null}checkIfFileAlreadyExists(e){let{files:t}=this.getState();return!!(t[e]&&!t[e].isGhost)}addFile(e){let{nextFilesState:t,validFilesToAdd:o,errors:n}=v(this,x,Lr).call(this,[e]),i=n.filter(a=>a.isRestriction);if(v(this,x,Ee).call(this,i),n.length>0)throw n[0];this.setState({files:t});let[s]=o;return this.emit("file-added",s),this.emit("files-added",o),this.log(`Added file: ${s.name}, ${s.id}, mime type: ${s.type}`),v(this,x,Cr).call(this),s.id}addFiles(e){let{nextFilesState:t,validFilesToAdd:o,errors:n}=v(this,x,Lr).call(this,e),i=n.filter(a=>a.isRestriction);v(this,x,Ee).call(this,i);let s=n.filter(a=>!a.isRestriction);if(s.length>0){let a=`Multiple errors occurred while adding files: +`;if(s.forEach(l=>{a+=` + * ${l.message}`}),this.info({message:this.i18n("addBulkFilesFailed",{smart_count:s.length}),details:a},"error",this.opts.infoTimeout),typeof AggregateError=="function")throw new AggregateError(s,a);{let l=new Error(a);throw l.errors=s,l}}this.setState({files:t}),o.forEach(a=>{this.emit("file-added",a)}),this.emit("files-added",o),o.length>5?this.log(`Added batch of ${o.length} files`):Object.values(o).forEach(a=>{this.log(`Added file: ${a.name} + id: ${a.id} + type: ${a.type}`)}),o.length>0&&v(this,x,Cr).call(this)}removeFiles(e){let{files:t,currentUploads:o}=this.getState(),n={...t},i={...o},s=Object.create(null);e.forEach(m=>{t[m]&&(s[m]=t[m],delete n[m])});function a(m){return s[m]===void 0}Object.keys(i).forEach(m=>{let h=o[m].fileIDs.filter(a);if(h.length===0){delete i[m];return}let{capabilities:U}=this.getState();if(h.length!==o[m].fileIDs.length&&!U.individualCancellation)throw new Error("The installed uploader plugin does not allow removing files during an upload.");i[m]={...o[m],fileIDs:h}});let l={currentUploads:i,files:n};Object.keys(n).length===0&&(l.allowNewUpload=!0,l.error=null,l.recoveredState=null),this.setState(l),f(this,Re).call(this);let p=Object.keys(s);p.forEach(m=>{this.emit("file-removed",s[m])}),p.length>5?this.log(`Removed ${p.length} files`):this.log(`Removed files: ${p.join(", ")}`)}removeFile(e){this.removeFiles([e])}pauseResume(e){if(!this.getState().capabilities.resumableUploads||this.getFile(e).progress.uploadComplete)return;let t=this.getFile(e),n=!(t.isPaused||!1);return this.setFileState(e,{isPaused:n}),this.emit("upload-pause",t,n),n}pauseAll(){let e={...this.getState().files};Object.keys(e).filter(o=>!e[o].progress.uploadComplete&&e[o].progress.uploadStarted).forEach(o=>{let n={...e[o],isPaused:!0};e[o]=n}),this.setState({files:e}),this.emit("pause-all")}resumeAll(){let e={...this.getState().files};Object.keys(e).filter(o=>!e[o].progress.uploadComplete&&e[o].progress.uploadStarted).forEach(o=>{let n={...e[o],isPaused:!1,error:null};e[o]=n}),this.setState({files:e}),this.emit("resume-all")}async retryAll(){let e=await v(this,x,Nr).call(this);return this.emit("complete",e),e}cancelAll(){this.emit("cancel-all");let{files:e}=this.getState(),t=Object.keys(e);t.length&&this.removeFiles(t),this.setState(Gt)}retryUpload(e){this.setFileState(e,{error:null,isPaused:!1}),this.emit("upload-retry",this.getFile(e));let t=v(this,x,ft).call(this,[e],{forceAllowNewUpload:!0});return v(this,x,pt).call(this,t)}logout(){this.iteratePlugins(e=>{e.provider?.logout?.()})}[Symbol.for("uppy test: updateTotalProgress")](){return v(this,x,qr).call(this)}updateOnlineStatus(){window.navigator.onLine??!0?(this.emit("is-online"),this.wasOffline&&(this.emit("back-online"),this.info(this.i18n("connectedToInternet"),"success",3e3),this.wasOffline=!1)):(this.emit("is-offline"),this.info(this.i18n("noInternetConnection"),"error",0),this.wasOffline=!0)}getID(){return this.opts.id}use(e,...t){if(typeof e!="function"){let s=`Expected a plugin class, but got ${e===null?"null":typeof e}. Please verify that the plugin was imported and spelled correctly.`;throw new TypeError(s)}let o=new e(this,...t),n=o.id;if(!n)throw new Error("Your plugin must have an id");if(!o.type)throw new Error("Your plugin must have a type");let i=this.getPlugin(n);if(i){let s=`Already found a plugin named '${i.id}'. Tried to use: '${n}'. +Uppy plugins must have unique \`id\` options.`;throw new Error(s)}return e.VERSION&&this.log(`Using ${n} v${e.VERSION}`),o.type in f(this,re)?f(this,re)[o.type].push(o):f(this,re)[o.type]=[o],o.install(),this.emit("plugin-added",o),this}getPlugin(e){for(let t of Object.values(f(this,re))){let o=t.find(n=>n.id===e);if(o!=null)return o}}[Symbol.for("uppy test: getPlugins")](e){return f(this,re)[e]}iteratePlugins(e){Object.values(f(this,re)).flat(1).forEach(e)}removePlugin(e){this.log(`Removing plugin ${e.id}`),this.emit("plugin-remove",e),e.uninstall&&e.uninstall();let t=f(this,re)[e.type],o=t.findIndex(s=>s.id===e.id);o!==-1&&t.splice(o,1);let i={plugins:{...this.getState().plugins,[e.id]:void 0}};this.setState(i)}destroy(){this.log(`Closing Uppy instance ${this.opts.id}: removing all files and uninstalling plugins`),this.cancelAll(),f(this,ht).call(this),this.iteratePlugins(e=>{this.removePlugin(e)}),typeof window<"u"&&window.removeEventListener&&(window.removeEventListener("online",f(this,he)),window.removeEventListener("offline",f(this,he)))}hideInfo(){let{info:e}=this.getState();this.setState({info:e.slice(1)}),this.emit("info-hidden")}info(e,t="info",o=3e3){let n=typeof e=="object";this.setState({info:[...this.getState().info,{type:t,message:n?e.message:e,details:n?e.details:null}]}),setTimeout(()=>this.hideInfo(),o),this.emit("info-visible")}log(e,t){let{logger:o}=this.opts;switch(t){case"error":o.error(e);break;case"warning":o.warn(e);break;default:o.debug(e);break}}registerRequestClient(e,t){f(this,mt).set(e,t)}getRequestClientForFile(e){if(!("remote"in e&&e.remote))throw new Error(`Tried to get RequestClient for a non-remote file ${e.id}`);let t=f(this,mt).get(e.remote.requestClientId);if(t==null)throw new Error(`requestClientId "${e.remote.requestClientId}" not registered for file "${e.id}"`);return t}async restore(e){this.log(`Core: Running restored upload "${e}"`);let t=await v(this,x,pt).call(this,e);return this.emit("complete",t),t}[Symbol.for("uppy test: createUpload")](...e){return v(this,x,ft).call(this,...e)}addResultData(e,t){if(!v(this,x,On).call(this,e)){this.log(`Not setting result for an upload that has been removed: ${e}`);return}let{currentUploads:o}=this.getState(),n={...o[e],result:{...o[e].result,...t}};this.setState({currentUploads:{...o,[e]:n}})}async upload(){f(this,re).uploader?.length||this.log("No uploader type plugins are used","warning");let{files:e}=this.getState();if(v(this,x,Ir).call(this).length>0){let n=await v(this,x,Nr).call(this);if(!(this.getFiles().filter(s=>s.progress.uploadStarted==null).length>0))return this.emit("complete",n),n;({files:e}=this.getState())}let o=this.opts.onBeforeUpload(e);if(o===!1)throw new Error("Not starting the upload because onBeforeUpload returned false");o&&typeof o=="object"&&(e=o,this.setState({files:e}));try{if(f(this,oe).validateMinNumberOfFiles(e),!v(this,x,_n).call(this,e))throw new Y(this.i18n("missingRequiredMetaField"));let{currentUploads:n}=this.getState(),i=Object.values(n).flatMap(p=>p.fileIDs),s=Object.keys(e).filter(p=>{let m=this.getFile(p);return m&&!m.progress.uploadStarted&&!i.includes(p)}),a=v(this,x,ft).call(this,s),l=await v(this,x,pt).call(this,a);return this.emit("complete",l),l}catch(n){throw v(this,x,Ee).call(this,[n]),n}}};re=new WeakMap,oe=new WeakMap,ht=new WeakMap,Pe=new WeakMap,We=new WeakMap,Ve=new WeakMap,Oe=new WeakMap,x=new WeakSet,Ee=function(e){for(let s of e)s.isRestriction?this.emit("restriction-failed",s.file,s):this.emit("error",s,s.file),this.log(s,"warning");let t=e.filter(s=>s.isUserFacing),o=4,n=t.slice(0,o),i=t.slice(o);n.forEach(({message:s,details:a=""})=>{this.info({message:s,details:a},"error",this.opts.infoTimeout)}),i.length>0&&this.info({message:this.i18n("additionalRestrictionsFailed",{count:i.length})})},Ar=function(e){let{missingFields:t,error:o}=f(this,oe).getMissingRequiredMetaFields(e);return t.length>0?(this.setFileState(e.id,{missingRequiredMetaFields:t,error:o.message}),this.log(o.message),this.emit("restriction-failed",e,o),!1):(t.length===0&&e.missingRequiredMetaFields&&this.setFileState(e.id,{missingRequiredMetaFields:[]}),!0)},_n=function(e){let t=!0;for(let o of Object.values(e))v(this,x,Ar).call(this,o)||(t=!1);return t},Un=function(e){let{allowNewUpload:t}=this.getState();if(t===!1){let o=new Y(this.i18n("noMoreFilesAllowed"),{file:e});throw v(this,x,Ee).call(this,[o]),o}},Fn=function(e){let t=e instanceof File?{name:e.name,type:e.type,size:e.size,data:e,meta:{},isRemote:!1,source:void 0,preview:void 0}:e,o=Be(t),n=Tr(o,t),i=ze(n).extension,s=Sr(t,this.getID()),a={...t.meta,name:n,type:o},l=Number.isFinite(t.data.size)?t.data.size:null;return{source:t.source||"",id:s,name:n,extension:i||"",meta:{...this.getState().meta,...a},type:o,progress:{percentage:0,bytesUploaded:!1,bytesTotal:l,uploadComplete:!1,uploadStarted:null},size:l,isGhost:!1,...t.isRemote?{isRemote:!0,remote:t.remote,data:t.data}:{isRemote:!1,data:t.data},preview:t.preview}},Cr=function(){this.opts.autoProceed&&!this.scheduledAutoProceed&&(this.scheduledAutoProceed=setTimeout(()=>{this.scheduledAutoProceed=null,this.upload().catch(e=>{e.isRestriction||this.log(e.stack||e.message||e)})},4))},Lr=function(e){let{files:t}=this.getState(),o={...t},n=[],i=[];for(let s of e)try{let a=v(this,x,Fn).call(this,s);v(this,x,Un).call(this,a);let l=t[a.id],p=l?.isGhost;if(p&&!a.isRemote){if(a.data==null)throw new Error("File data is missing");a={...l,isGhost:!1,data:a.data},this.log(`Replaced the blob in the restored ghost file: ${a.name}, ${a.id}`)}let m=this.opts.onBeforeFileAdded(a,o);if(t=this.getState().files,o={...t,...o},!m&&this.checkIfFileAlreadyExists(a.id))throw new Y(this.i18n("noDuplicates",{fileName:a.name??this.i18n("unnamed")}),{file:a});if(m===!1&&!p)throw new Y("Cannot add the file because onBeforeFileAdded returned false.",{isUserFacing:!1,file:a});typeof m=="object"&&m!==null&&(a=m),f(this,oe).validateSingleFile(a),o[a.id]=a,n.push(a)}catch(a){i.push(a)}try{f(this,oe).validateAggregateRestrictions(Object.values(t),n)}catch(s){return i.push(s),{nextFilesState:t,validFilesToAdd:[],errors:i}}return{nextFilesState:o,validFilesToAdd:n,errors:i}},Ir=function(){let{files:e}=this.getState();return Object.keys(e).filter(t=>{let o=e[t];return o.error&&(!o.missingRequiredMetaFields||o.missingRequiredMetaFields.length===0)})},Nr=async function(){let e=v(this,x,Ir).call(this),t={...this.getState().files};if(e.forEach(n=>{t[n]={...t[n],isPaused:!1,error:null}}),this.setState({files:t,error:null}),this.emit("retry-all",this.getFilesByIds(e)),e.length===0)return{successful:[],failed:[]};let o=v(this,x,ft).call(this,e,{forceAllowNewUpload:!0});return v(this,x,pt).call(this,o)},Ht=new WeakMap,qr=function(){let e=v(this,x,En).call(this),t=null;e!=null&&(t=Math.round(e*100),t>100?t=100:t<0&&(t=0)),this.emit("progress",t??0),this.setState({totalProgress:t??0})},Re=new WeakMap,En=function(){let t=this.getFiles().filter(l=>l.progress.uploadStarted||l.progress.preprocess||l.progress.postprocess);if(t.length===0)return 0;if(t.every(l=>l.progress.uploadComplete))return 1;let o=l=>l.progress.bytesTotal!=null&&l.progress.bytesTotal!==0,n=t.filter(o),i=t.filter(l=>!o(l));if(n.every(l=>l.progress.uploadComplete)&&i.length>0&&!i.every(l=>l.progress.uploadComplete))return null;let s=n.reduce((l,p)=>l+(p.progress.bytesTotal??0),0),a=n.reduce((l,p)=>l+(p.progress.bytesUploaded||0),0);return s===0?0:a/s},Pn=function(){let e=(n,i,s)=>{let a=n.message||"Unknown error";n.details&&(a+=` ${n.details}`),this.setState({error:a}),i!=null&&i.id in this.getState().files&&this.setFileState(i.id,{error:a,response:s})};this.on("error",e),this.on("upload-error",(n,i,s)=>{if(e(i,n,s),typeof i=="object"&&i.message){this.log(i.message,"error");let a=new Error(this.i18n("failedToUpload",{file:n?.name??""}));a.isUserFacing=!0,a.details=i.message,i.details&&(a.details+=` ${i.details}`),v(this,x,Ee).call(this,[a])}else v(this,x,Ee).call(this,[i])});let t=null;this.on("upload-stalled",(n,i)=>{let{message:s}=n,a=i.map(l=>l.meta.name).join(", ");t||(this.info({message:s,details:a},"warning",this.opts.infoTimeout),t=setTimeout(()=>{t=null},this.opts.infoTimeout)),this.log(`${s} ${a}`.trim(),"warning")}),this.on("upload",()=>{this.setState({error:null})});let o=n=>{let i=n.filter(a=>{let l=a!=null&&this.getFile(a.id);return l||this.log(`Not setting progress for a file that has been removed: ${a?.id}`),l}),s=Object.fromEntries(i.map(a=>[a.id,{progress:{uploadStarted:Date.now(),uploadComplete:!1,bytesUploaded:0,bytesTotal:a.size}}]));this.patchFilesState(s)};this.on("upload-start",o),this.on("upload-progress",f(this,Ht)),this.on("upload-success",(n,i)=>{if(n==null||!this.getFile(n.id)){this.log(`Not setting progress for a file that has been removed: ${n?.id}`);return}let s=this.getFile(n.id).progress,a=f(this,Oe).size>0;this.setFileState(n.id,{progress:{...s,postprocess:a?{mode:"indeterminate"}:void 0,uploadComplete:!0,...!a&&{complete:!0},percentage:100,bytesUploaded:s.bytesTotal},response:i,uploadURL:i.uploadURL,isPaused:!1}),n.size==null&&this.setFileState(n.id,{size:i.bytesUploaded||s.bytesTotal}),f(this,Re).call(this)}),this.on("preprocess-progress",(n,i)=>{if(n==null||!this.getFile(n.id)){this.log(`Not setting progress for a file that has been removed: ${n?.id}`);return}this.setFileState(n.id,{progress:{...this.getFile(n.id).progress,preprocess:i}})}),this.on("preprocess-complete",n=>{if(n==null||!this.getFile(n.id)){this.log(`Not setting progress for a file that has been removed: ${n?.id}`);return}let i={...this.getState().files};i[n.id]={...i[n.id],progress:{...i[n.id].progress}},delete i[n.id].progress.preprocess,this.setState({files:i})}),this.on("postprocess-progress",(n,i)=>{if(n==null||!this.getFile(n.id)){this.log(`Not setting progress for a file that has been removed: ${n?.id}`);return}this.setFileState(n.id,{progress:{...this.getState().files[n.id].progress,postprocess:i}})}),this.on("postprocess-complete",n=>{let i=n&&this.getFile(n.id);if(i==null){this.log(`Not setting progress for a file that has been removed: ${n?.id}`);return}let{postprocess:s,...a}=i.progress;this.patchFilesState({[i.id]:{progress:{...a,complete:!0}}})}),this.on("restored",()=>{f(this,Re).call(this)}),this.on("dashboard:file-edit-complete",n=>{n&&v(this,x,Ar).call(this,n)}),typeof window<"u"&&window.addEventListener&&(window.addEventListener("online",f(this,he)),window.addEventListener("offline",f(this,he)),setTimeout(f(this,he),3e3))},he=new WeakMap,mt=new WeakMap,ft=function(e,t={}){let{forceAllowNewUpload:o=!1}=t,{allowNewUpload:n,currentUploads:i}=this.getState();if(!n&&!o)throw new Error("Cannot create a new upload: already uploading.");let s=bn();return this.emit("upload",s,this.getFilesByIds(e)),this.setState({allowNewUpload:this.opts.allowMultipleUploadBatches!==!1&&this.opts.allowMultipleUploads!==!1,currentUploads:{...i,[s]:{fileIDs:e,step:0,result:{}}}}),s},On=function(e){let{currentUploads:t}=this.getState();return t[e]},zr=function(e){let{[e]:t,...o}=this.getState().currentUploads;this.setState({currentUploads:o})},pt=async function(e){let t=()=>{let{currentUploads:s}=this.getState();return s[e]},o=t();if(!o)throw new Error("Nonexistent upload");let n=[...f(this,We),...f(this,Ve),...f(this,Oe)];try{for(let s=o.step||0;s{let m=this.getFile(p);m?.progress.postprocess&&this.emit("postprocess-complete",m)});let s=o.fileIDs.map(p=>this.getFile(p)),a=s.filter(p=>!p.error),l=s.filter(p=>p.error);this.addResultData(e,{successful:a,failed:l,uploadID:e}),o=t()}let i;return o&&(i=o.result,v(this,x,zr).call(this,e)),i==null&&(this.log(`Not setting result for an upload that has been removed: ${e}`),i={successful:[],failed:[],uploadID:e}),i},w(Wt,"VERSION",vn.version);var kr=Wt,Vt=kr;var Rn={name:"@uppy/golden-retriever",description:"The GoldenRetriever Uppy plugin saves selected files in browser cache to seamlessly resume uploding after browser crash or accidentally closed tab",version:"5.2.1",license:"MIT",type:"module",sideEffects:["lib/ServiceWorker.js"],scripts:{build:"tsc --build tsconfig.build.json",typecheck:"tsc --build",test:"vitest run --silent='passed-only'","test:e2e":"vitest run --project browser"},keywords:["file uploader","restore files","crash recovery","golden retriever","resumable uploads","uppy","uppy-plugin"],homepage:"https://uppy.io",bugs:{url:"https://github.com/transloadit/uppy/issues"},repository:{type:"git",url:"git+https://github.com/transloadit/uppy.git"},files:["src","lib","dist","CHANGELOG.md"],exports:{".":"./lib/index.js","./lib/ServiceWorker.js":"./lib/ServiceWorker.js","./package.json":"./package.json"},dependencies:{"@uppy/utils":"^7.1.5",lodash:"^4.17.21"},peerDependencies:{"@uppy/core":"^5.2.0"},devDependencies:{"@uppy/core":"^5.2.0","@uppy/dashboard":"^5.1.0","@uppy/xhr-upload":"^5.1.1","@vitest/browser":"^3.2.4",typescript:"^5.8.3",vitest:"^3.2.4"}};var Cn=typeof window<"u"&&(window.indexedDB||window.webkitIndexedDB||window.mozIndexedDB||window.OIndexedDB||window.msIndexedDB),Ta=!!Cn,Tn="uppy-blobs",J="files",Ln=1440*60*1e3,ja=3,jn=1048576;function ka(r){let e=r.openCursor();e.onsuccess=t=>{let o=t.target.result;if(!o)return;let n=o.value;n.expires=Date.now()+Ln,o.update(n)}}function kn(r){let e=Cn.open(r,ja);return new Promise((t,o)=>{e.onupgradeneeded=n=>{let i=n.target.result,s=n.currentTarget.transaction;if(n.oldVersion<2&&i.createObjectStore(J,{keyPath:"id"}).createIndex("store","store",{unique:!1}),n.oldVersion<3){let a=s.objectStore(J);a.createIndex("expires","expires",{unique:!1}),ka(a)}s.oncomplete=()=>{t(i)}},e.onsuccess=n=>{t(n.target.result)},e.onerror=o})}function Xt(r){return new Promise((e,t)=>{r.onsuccess=o=>{e(o.target.result)},r.onerror=t})}var An=!1,Q,Yt=class Yt{constructor(e){S(this,Q);w(this,"opts");w(this,"name");this.opts={dbName:Tn,storeName:"default",expires:Ln,maxFileSize:10*jn,maxTotalSize:300*jn,...e},this.name=this.opts.storeName;let t=async()=>{let o=await kn(this.opts.dbName);return P(this,Q,o),o};An?P(this,Q,t()):(An=!0,P(this,Q,Yt.cleanup().then(t,t)))}get ready(){return Promise.resolve(f(this,Q))}key(e){return`${this.name}!${e}`}async list(){let n=(await f(this,Q)).transaction([J],"readonly").objectStore(J).index("store").getAll(IDBKeyRange.only(this.name)),i=await Xt(n);return Object.fromEntries(i.map(s=>[s.fileID,s.data]))}async get(e){let n=(await f(this,Q)).transaction([J],"readonly").objectStore(J).get(this.key(e)),{data:i}=await Xt(n);return{id:i.fileID,data:i.data}}async getSize(){let n=(await f(this,Q)).transaction([J],"readonly").objectStore(J).index("store").openCursor(IDBKeyRange.only(this.name));return new Promise((i,s)=>{let a=0;n.onsuccess=l=>{let p=l.target.result;p?(a+=p.value.data.size,p.continue()):i(a)},n.onerror=()=>{s(new Error("Could not retrieve stored blobs size"))}})}async put(e){if(e.data.size!=null&&e.data.size>this.opts.maxFileSize)throw new Error("File is too big to store.");if(await this.getSize()>this.opts.maxTotalSize)throw new Error("No space left");let i=(await f(this,Q)).transaction([J],"readwrite").objectStore(J).add({id:this.key(e.id),fileID:e.id,store:this.name,expires:Date.now()+this.opts.expires,data:e.data});return Xt(i)}async delete(e){let n=(await f(this,Q)).transaction([J],"readwrite").objectStore(J).delete(this.key(e));return Xt(n)}static async cleanup(){let e=await kn(Tn),n=e.transaction([J],"readwrite").objectStore(J).index("expires").openCursor(IDBKeyRange.upperBound(Date.now()));await new Promise((i,s)=>{n.onsuccess=a=>{let l=a.target.result;l?(l.delete(),l.continue()):i()},n.onerror=s}),e.close()}};Q=new WeakMap,w(Yt,"isSupported");var Kt=Yt;Kt.isSupported=Ta;var In=Kt;var Nn=Ne(Rr(),1);function qn(r){try{return JSON.parse(r)}catch{return null}}var zn="uppyState:",Aa=r=>`${zn}${r}`;function Ca(){let r=[];for(let t=0;t{let o=localStorage.getItem(t);if(!o)return;let n=qn(o);n?.expires&&n.expires{Ca();let e=localStorage.getItem(this.name);if(!e)return;let t=qn(e);if(t)return P(this,me,t),t.metadata});w(this,"get",()=>f(this,me)?.metadata);w(this,"save",()=>{if(f(this,me)===null){localStorage.removeItem(this.name);return}let e=JSON.stringify(f(this,me));localStorage.setItem(this.name,e)});w(this,"set",e=>{P(this,me,e===null?null:{metadata:e,expires:Date.now()+this.opts.expires}),f(this,yt).call(this)});this.opts={expires:1440*60*1e3,throttleTime:500,...e},this.name=Aa(e.storeName),P(this,yt,this.opts.throttleTime===0?this.save:(0,Nn.default)(this.save,this.opts.throttleTime,{leading:!0,trailing:!0}))}};yt=new WeakMap,me=new WeakMap;var Bn=typeof navigator<"u"&&"serviceWorker"in navigator;function La(){return new Promise((r,e)=>{Bn?navigator.serviceWorker.controller?r():navigator.serviceWorker.addEventListener("controllerchange",()=>{r()}):e(new Error("Unsupported"))})}var de,bt=class{constructor(e){S(this,de);w(this,"name");P(this,de,La().then(t=>{P(this,de,t)})),this.name=e.storeName}get ready(){return Promise.resolve(f(this,de))}async list(){return await f(this,de),new Promise((e,t)=>{let o=n=>{n.data.store===this.name&&(n.data.type==="uppy/ALL_FILES"?(e(n.data.files),navigator.serviceWorker.removeEventListener("message",o)):t())};navigator.serviceWorker.addEventListener("message",o),navigator.serviceWorker.controller.postMessage({type:"uppy/GET_FILES",store:this.name})})}async put(e){await f(this,de),navigator.serviceWorker.controller.postMessage({type:"uppy/ADD_FILE",store:this.name,file:e})}async delete(e){await f(this,de),navigator.serviceWorker.controller.postMessage({type:"uppy/REMOVE_FILE",store:this.name,fileID:e})}};de=new WeakMap,w(bt,"isSupported");bt.isSupported=Bn;var Mn=bt;var Ia={expires:1440*60*1e3,serviceWorker:!1},Dn,Te,ge,je,ee,$n,Xe,Gn,Hn,Jt,Zt,St,xt,_t,vt=class vt extends pe{constructor(t,o){super(t,{...Ia,...o});S(this,ee);S(this,Te);S(this,ge);S(this,je);S(this,Xe,({pluginData:t,...o})=>{let n=f(this,Te).get();f(this,Te).set({...n??{currentUploads:{},files:{}},...o,pluginData:{...n?.pluginData,...t}})});S(this,Zt,async t=>{let{id:o,data:n,isRemote:i}=t;i||n==null||await Promise.all([f(this,ge)?.put({id:o,data:n}).catch(s=>{this.uppy.log("[GoldenRetriever] Could not store file in Service Worker","warning"),this.uppy.log(s)}),f(this,je).put({id:o,data:n}).catch(s=>{s instanceof Event&&s.target instanceof IDBRequest&&s.target.error?.name==="ConstraintError"||(this.uppy.log("[GoldenRetriever] Could not store file in IndexedDB","warning"),this.uppy.log(s))})])});S(this,St,(t,o,n)=>{if(o.currentUploads!==t.currentUploads){let{currentUploads:i}=this.uppy.getState();f(this,Xe).call(this,{currentUploads:i})}if(o.files!==t.files){Object.values(t.files).some(h=>!h.progress.complete)&&(Object.values(o.files).length===0||Object.values(o.files).every(h=>h.progress.complete&&!h.error))&&(this.uppy.log("[GoldenRetriever] All files have been uploaded and processed successfully, clearing recovery state"),this.uppy.setState({recoveredState:null}));let i=Object.fromEntries(Object.entries(o.files).map(([h,{data:U,preview:j,...O}])=>[h,O]));f(this,Xe).call(this,{files:i});let s=Object.values(o.files).filter(h=>t.files[h.id]==null),a=Object.values(o.files).flatMap(h=>{let U=t.files[h.id];return U!=null&&h.data!==U.data?[h]:[]}),p=[...Object.values(t.files).filter(h=>{let U=o.files[h.id];return U==null||U.progress.uploadComplete&&!h.progress.uploadComplete}),...a],m=[...s,...a];(async()=>{await v(this,ee,Jt).call(this,p.map(h=>h.id));for(let h of m)await f(this,Zt).call(this,h);m.length>0&&this.uppy.log(`[GoldenRetriever] Added ${m.length} blobs`)})()}});S(this,xt,()=>{this.uppy.log("[GoldenRetriever] Restore confirmed, proceeding...");let{currentUploads:t}=this.uppy.getState();Object.keys(t).length>0?(this.uppy.resumeAll(),Object.keys(t).forEach(o=>{this.uppy.restore(o)})):this.uppy.upload(),this.uppy.setState({recoveredState:null})});S(this,_t,t=>{f(this,Xe).call(this,{pluginData:t})});this.type="debugger",this.id=this.opts.id||"GoldenRetriever",P(this,Te,new gt({expires:this.opts.expires,storeName:t.getID(),throttleTime:vt[Symbol.for("uppy test: throttleTime")]??void 0})),this.opts.serviceWorker&&P(this,ge,new Mn({storeName:t.getID()})),P(this,je,new In({expires:this.opts.expires,...this.opts.indexedDB||{},storeName:t.getID()}))}async[(Dn=Symbol.for("uppy test: throttleTime"),Symbol.for("uppy test: deleteBlobs"))](t){return v(this,ee,Jt).call(this,t)}install(){v(this,ee,$n).call(this),this.uppy.on("state-update",f(this,St)),this.uppy.on("restore-confirmed",f(this,xt)),this.uppy.on("restore:plugin-data-changed",f(this,_t))}uninstall(){this.uppy.off("state-update",f(this,St)),this.uppy.off("restore-confirmed",f(this,xt)),this.uppy.off("restore:plugin-data-changed",f(this,_t))}};Te=new WeakMap,ge=new WeakMap,je=new WeakMap,ee=new WeakSet,$n=async function(){let t=f(this,Te).load();if(!t)return;let o=t.currentUploads||{},n=Object.entries(t.files||{}),i=Object.fromEntries(n.every(([,j])=>j.progress.complete&&!j.error)?[]:n),s=Object.entries(i);this.uppy.log(`[GoldenRetriever] Recovered ${Object.keys(o).length} current uploads and ${s.length} files from Local Storage`);let a=s.length>0;a||this.uppy.log("[GoldenRetriever] No files need to be loaded, restored only processing state...");let[l,p]=await Promise.all([v(this,ee,Gn).call(this),v(this,ee,Hn).call(this)]),m={...l,...p},h=Object.fromEntries(s.map(([j,O])=>{if(O.isRemote)return[j,{...O,isRestored:!0,data:{size:null}}];let T=m[j];return[j,!O.progress.uploadComplete&&T==null?{...O,isRestored:!0,isGhost:!0,data:void 0}:{...O,isRestored:!0,isGhost:!1,data:T}]}));this.uppy.setState({recoveredState:a?t:null,currentUploads:a?o:{},files:h}),this.uppy.emit("restored",t.pluginData);let U=Object.keys(m).filter(j=>!i[j]);if(U.length)try{this.uppy.log(`[GoldenRetriever] Cleaning up ${U.length} old files`),await v(this,ee,Jt).call(this,U)}catch(j){this.uppy.log(`[GoldenRetriever] Could not clean up ${U.length} old files`,"warning"),this.uppy.log(j)}},Xe=new WeakMap,Gn=async function(){if(!f(this,ge))return{};try{let t=await f(this,ge).list(),o=Object.keys(t).length;return this.uppy.log(o>0?`[GoldenRetriever] Successfully recovered ${o} blobs from Service Worker!`:"[GoldenRetriever] No blobs found in Service Worker"),t}catch(t){return this.uppy.log("[GoldenRetriever] Failed to recover blobs from Service Worker","warning"),this.uppy.log(t),{}}},Hn=async function(){try{let t=await f(this,je).list(),o=Object.keys(t).length;return this.uppy.log(o>0?`[GoldenRetriever] Successfully recovered ${o} blobs from IndexedDB!`:"[GoldenRetriever] No blobs found in IndexedDB"),t}catch(t){return this.uppy.log("[GoldenRetriever] Failed to recover blobs from IndexedDB","warning"),this.uppy.log(t),{}}},Jt=async function(t){await Promise.all(t.map(async o=>{try{await Promise.all([f(this,ge)?.delete(o),f(this,je).delete(o)])}catch(n){this.uppy.log(`[GoldenRetriever] Could not remove file ${o} from all stores`,"warning"),this.uppy.log(n)}})),t.length>0&&this.uppy.log(`[GoldenRetriever] Removed ${t.length} blobs`)},Zt=new WeakMap,St=new WeakMap,xt=new WeakMap,_t=new WeakMap,w(vt,"VERSION",Rn.version),w(vt,Dn);var wt=vt;function Ke(r){"@babel/helpers - typeof";return Ke=typeof Symbol=="function"&&typeof Symbol.iterator=="symbol"?function(e){return typeof e}:function(e){return e&&typeof Symbol=="function"&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e},Ke(r)}function Wn(r,e){for(var t=0;t1&&arguments[1]!==void 0?arguments[1]:null,i=arguments.length>2&&arguments[2]!==void 0?arguments[2]:null,s=arguments.length>3&&arguments[3]!==void 0?arguments[3]:null;if(Ba(this,e),o=Ma(this,e,[t]),o.originalRequest=i,o.originalResponse=s,o.causingError=n,n!=null&&(t+=", caused by ".concat(n.toString())),i!=null){var a=i.getHeader("X-Request-ID")||"n/a",l=i.getMethod(),p=i.getURL(),m=s?s.getStatus():"n/a",h=s?s.getBody()||"":"n/a";t+=", originated from request (method: ".concat(l,", url: ").concat(p,", response code: ").concat(m,", response text: ").concat(h,", request id: ").concat(a,")")}return o.message=t,o}return Ga(e,r),Na(e)})(Br(Error)),Ye=Va;var Xa=!1;function ye(r){Xa&&console.log(r)}function Et(r){"@babel/helpers - typeof";return Et=typeof Symbol=="function"&&typeof Symbol.iterator=="symbol"?function(e){return typeof e}:function(e){return e&&typeof Symbol=="function"&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e},Et(r)}function Ka(r,e){if(!(r instanceof e))throw new TypeError("Cannot call a class as a function")}function Vn(r,e){for(var t=0;t{let e={};return r.forEach((t,o)=>e[t]=o),e})(Pt),tl=/^(?:[A-Za-z\d+\/]{4})*?(?:[A-Za-z\d+\/]{2}(?:==)?|[A-Za-z\d+\/]{3}=?)?$/,$=String.fromCharCode.bind(String),rl=typeof Uint8Array.from=="function"?Uint8Array.from.bind(Uint8Array):r=>new Uint8Array(Array.prototype.slice.call(r,0)),ei=r=>r.replace(/=/g,"").replace(/[+\/]/g,e=>e=="+"?"-":"_"),ti=r=>r.replace(/[^A-Za-z0-9\+\/]/g,""),ri=r=>{let e,t,o,n,i="",s=r.length%3;for(let a=0;a255||(o=r.charCodeAt(a++))>255||(n=r.charCodeAt(a++))>255)throw new TypeError("invalid character found");e=t<<16|o<<8|n,i+=Pt[e>>18&63]+Pt[e>>12&63]+Pt[e>>6&63]+Pt[e&63]}return s?i.slice(0,s-3)+"===".substring(s):i},Gr=typeof btoa=="function"?r=>btoa(r):ri,Dr=typeof Uint8Array.prototype.toBase64=="function"?r=>r.toBase64():r=>{let t=[];for(let o=0,n=r.length;oe?ei(Dr(r)):Dr(r),ol=r=>{if(r.length<2){var e=r.charCodeAt(0);return e<128?r:e<2048?$(192|e>>>6)+$(128|e&63):$(224|e>>>12&15)+$(128|e>>>6&63)+$(128|e&63)}else{var e=65536+(r.charCodeAt(0)-55296)*1024+(r.charCodeAt(1)-56320);return $(240|e>>>18&7)+$(128|e>>>12&63)+$(128|e>>>6&63)+$(128|e&63)}},nl=/[\uD800-\uDBFF][\uDC00-\uDFFFF]|[^\x00-\x7F]/g,oi=r=>r.replace(nl,ol),Jn=Yn?r=>Dr(Yn.encode(r)):r=>Gr(oi(r)),Je=(r,e=!1)=>e?ei(Jn(r)):Jn(r),Zn=r=>Je(r,!0),il=/[\xC0-\xDF][\x80-\xBF]|[\xE0-\xEF][\x80-\xBF]{2}|[\xF0-\xF7][\x80-\xBF]{3}/g,sl=r=>{switch(r.length){case 4:var e=(7&r.charCodeAt(0))<<18|(63&r.charCodeAt(1))<<12|(63&r.charCodeAt(2))<<6|63&r.charCodeAt(3),t=e-65536;return $((t>>>10)+55296)+$((t&1023)+56320);case 3:return $((15&r.charCodeAt(0))<<12|(63&r.charCodeAt(1))<<6|63&r.charCodeAt(2));default:return $((31&r.charCodeAt(0))<<6|63&r.charCodeAt(1))}},ni=r=>r.replace(il,sl),ii=r=>{if(r=r.replace(/\s+/g,""),!tl.test(r))throw new TypeError("malformed base64.");r+="==".slice(2-(r.length&3));let e,t,o,n=[];for(let i=0;i>16&255)):o===64?n.push($(e>>16&255,e>>8&255)):n.push($(e>>16&255,e>>8&255,e&255));return n.join("")},Hr=typeof atob=="function"?r=>atob(ti(r)):ii,si=typeof Uint8Array.fromBase64=="function"?r=>Uint8Array.fromBase64(r):r=>rl(Hr(r).split("").map(e=>e.charCodeAt(0))),ai=r=>si(li(r)),al=Kn?r=>Kn.decode(si(r)):r=>ni(Hr(r)),li=r=>ti(r.replace(/[-_]/g,e=>e=="-"?"+":"/")),$r=r=>al(li(r)),ll=r=>{if(typeof r!="string")return!1;let e=r.replace(/\s+/g,"").replace(/={0,2}$/,"");return!/[^\s0-9a-zA-Z\+/]/.test(e)||!/[^\s0-9a-zA-Z\-_]/.test(e)},ui=r=>({value:r,enumerable:!1,writable:!0,configurable:!0}),ci=function(){let r=(e,t)=>Object.defineProperty(String.prototype,e,ui(t));r("fromBase64",function(){return $r(this)}),r("toBase64",function(e){return Je(this,e)}),r("toBase64URI",function(){return Je(this,!0)}),r("toBase64URL",function(){return Je(this,!0)}),r("toUint8Array",function(){return ai(this)})},di=function(){let r=(e,t)=>Object.defineProperty(Uint8Array.prototype,e,ui(t));r("toBase64",function(e){return er(this,e)}),r("toBase64URI",function(){return er(this,!0)}),r("toBase64URL",function(){return er(this,!0)})},ul=()=>{ci(),di()},fi={version:Qn,VERSION:Qa,atob:Hr,atobPolyfill:ii,btoa:Gr,btoaPolyfill:ri,fromBase64:$r,toBase64:Je,encode:Je,encodeURI:Zn,encodeURL:Zn,utob:oi,btou:ni,decode:$r,isValid:ll,fromUint8Array:er,toUint8Array:ai,extendString:ci,extendUint8Array:di,extendBuiltins:ul};var qi=Ne(Fi());function Kr(){return"xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g,function(r){var e=Math.random()*16|0,t=r==="x"?e:e&3|8;return t.toString(16)})}function Yr(){"use strict";Yr=function(){return e};var r,e={},t=Object.prototype,o=t.hasOwnProperty,n=Object.defineProperty||function(d,u,c){d[u]=c.value},i=typeof Symbol=="function"?Symbol:{},s=i.iterator||"@@iterator",a=i.asyncIterator||"@@asyncIterator",l=i.toStringTag||"@@toStringTag";function p(d,u,c){return Object.defineProperty(d,u,{value:c,enumerable:!0,configurable:!0,writable:!0}),d[u]}try{p({},"")}catch{p=function(c,g,b){return c[g]=b}}function m(d,u,c,g){var b=u&&u.prototype instanceof B?u:B,y=Object.create(b.prototype),E=new xe(g||[]);return n(y,"_invoke",{value:dr(d,c,E)}),y}function h(d,u,c){try{return{type:"normal",arg:d.call(u,c)}}catch(g){return{type:"throw",arg:g}}}e.wrap=m;var U="suspendedStart",j="suspendedYield",O="executing",T="completed",I={};function B(){}function _(){}function F(){}var L={};p(L,s,function(){return this});var K=Object.getPrototypeOf,k=K&&K(K(_e([])));k&&k!==t&&o.call(k,s)&&(L=k);var N=F.prototype=B.prototype=Object.create(L);function te(d){["next","throw","return"].forEach(function(u){p(d,u,function(c){return this._invoke(u,c)})})}function Z(d,u){function c(b,y,E,A){var C=h(d[b],d,y);if(C.type!=="throw"){var V=C.arg,D=V.value;return D&&ke(D)=="object"&&o.call(D,"__await")?u.resolve(D.__await).then(function(X){c("next",X,E,A)},function(X){c("throw",X,E,A)}):u.resolve(D).then(function(X){V.value=X,E(V)},function(X){return c("throw",X,E,A)})}A(C.arg)}var g;n(this,"_invoke",{value:function(y,E){function A(){return new u(function(C,V){c(y,E,C,V)})}return g=g?g.then(A,A):A()}})}function dr(d,u,c){var g=U;return function(b,y){if(g===O)throw Error("Generator is already running");if(g===T){if(b==="throw")throw y;return{value:r,done:!0}}for(c.method=b,c.arg=y;;){var E=c.delegate;if(E){var A=nt(E,c);if(A){if(A===I)continue;return A}}if(c.method==="next")c.sent=c._sent=c.arg;else if(c.method==="throw"){if(g===U)throw g=T,c.arg;c.dispatchException(c.arg)}else c.method==="return"&&c.abrupt("return",c.arg);g=O;var C=h(d,u,c);if(C.type==="normal"){if(g=c.done?T:j,C.arg===I)continue;return{value:C.arg,done:c.done}}C.type==="throw"&&(g=T,c.method="throw",c.arg=C.arg)}}}function nt(d,u){var c=u.method,g=d.iterator[c];if(g===r)return u.delegate=null,c==="throw"&&d.iterator.return&&(u.method="return",u.arg=r,nt(d,u),u.method==="throw")||c!=="return"&&(u.method="throw",u.arg=new TypeError("The iterator does not provide a '"+c+"' method")),I;var b=h(g,d.iterator,u.arg);if(b.type==="throw")return u.method="throw",u.arg=b.arg,u.delegate=null,I;var y=b.arg;return y?y.done?(u[d.resultName]=y.value,u.next=d.nextLoc,u.method!=="return"&&(u.method="next",u.arg=r),u.delegate=null,I):y:(u.method="throw",u.arg=new TypeError("iterator result is not an object"),u.delegate=null,I)}function fr(d){var u={tryLoc:d[0]};1 in d&&(u.catchLoc=d[1]),2 in d&&(u.finallyLoc=d[2],u.afterLoc=d[3]),this.tryEntries.push(u)}function Se(d){var u=d.completion||{};u.type="normal",delete u.arg,d.completion=u}function xe(d){this.tryEntries=[{tryLoc:"root"}],d.forEach(fr,this),this.reset(!0)}function _e(d){if(d||d===""){var u=d[s];if(u)return u.call(d);if(typeof d.next=="function")return d;if(!isNaN(d.length)){var c=-1,g=function b(){for(;++c=0;--b){var y=this.tryEntries[b],E=y.completion;if(y.tryLoc==="root")return g("end");if(y.tryLoc<=this.prev){var A=o.call(y,"catchLoc"),C=o.call(y,"finallyLoc");if(A&&C){if(this.prev=0;--g){var b=this.tryEntries[g];if(b.tryLoc<=this.prev&&o.call(b,"finallyLoc")&&this.prev=0;--c){var g=this.tryEntries[c];if(g.finallyLoc===u)return this.complete(g.completion,g.afterLoc),Se(g),I}},catch:function(u){for(var c=this.tryEntries.length-1;c>=0;--c){var g=this.tryEntries[c];if(g.tryLoc===u){var b=g.completion;if(b.type==="throw"){var y=b.arg;Se(g)}return y}}throw Error("illegal catch attempt")},delegateYield:function(u,c,g){return this.delegate={iterator:_e(u),resultName:c,nextLoc:g},this.method==="next"&&(this.arg=r),I}},e}function Ei(r,e,t,o,n,i,s){try{var a=r[i](s),l=a.value}catch(p){t(p);return}a.done?e(l):Promise.resolve(l).then(o,n)}function Sl(r){return function(){var e=this,t=arguments;return new Promise(function(o,n){var i=r.apply(e,t);function s(l){Ei(i,o,n,s,a,"next",l)}function a(l){Ei(i,o,n,s,a,"throw",l)}s(void 0)})}}function Li(r,e){return Ul(r)||_l(r,e)||Ii(r,e)||xl()}function xl(){throw new TypeError(`Invalid attempt to destructure non-iterable instance. +In order to be iterable, non-array objects must have a [Symbol.iterator]() method.`)}function _l(r,e){var t=r==null?null:typeof Symbol<"u"&&r[Symbol.iterator]||r["@@iterator"];if(t!=null){var o,n,i,s,a=[],l=!0,p=!1;try{if(i=(t=t.call(r)).next,e===0){if(Object(t)!==t)return;l=!1}else for(;!(l=(o=i.call(t)).done)&&(a.push(o.value),a.length!==e);l=!0);}catch(m){p=!0,n=m}finally{try{if(!l&&t.return!=null&&(s=t.return(),Object(s)!==s))return}finally{if(p)throw n}}return a}}function Ul(r){if(Array.isArray(r))return r}function ke(r){"@babel/helpers - typeof";return ke=typeof Symbol=="function"&&typeof Symbol.iterator=="symbol"?function(e){return typeof e}:function(e){return e&&typeof Symbol=="function"&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e},ke(r)}function Fl(r,e){var t=typeof Symbol<"u"&&r[Symbol.iterator]||r["@@iterator"];if(!t){if(Array.isArray(r)||(t=Ii(r))||e&&r&&typeof r.length=="number"){t&&(r=t);var o=0,n=function(){};return{s:n,n:function(){return o>=r.length?{done:!0}:{done:!1,value:r[o++]}},e:function(p){throw p},f:n}}throw new TypeError(`Invalid attempt to iterate non-iterable instance. +In order to be iterable, non-array objects must have a [Symbol.iterator]() method.`)}var i=!0,s=!1,a;return{s:function(){t=t.call(r)},n:function(){var p=t.next();return i=p.done,p},e:function(p){s=!0,a=p},f:function(){try{!i&&t.return!=null&&t.return()}finally{if(s)throw a}}}}function Ii(r,e){if(r){if(typeof r=="string")return Pi(r,e);var t=Object.prototype.toString.call(r).slice(8,-1);if(t==="Object"&&r.constructor&&(t=r.constructor.name),t==="Map"||t==="Set")return Array.from(r);if(t==="Arguments"||/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(t))return Pi(r,e)}}function Pi(r,e){(e==null||e>r.length)&&(e=r.length);for(var t=0,o=new Array(e);t1)for(var i=0,s=["uploadUrl","uploadSize","uploadLengthDeferred"];i1||t._parallelUploadUrls!=null?t._startParallelUpload():t._startSingleUpload()}).catch(function(l){t._emitError(l)})}},{key:"_startParallelUpload",value:function(){var t,o=this,n=this._size,i=0;this._parallelUploads=[];var s=this._parallelUploadUrls!=null?this._parallelUploadUrls.length:this.options.parallelUploads,a=(t=this.options.parallelUploadBoundaries)!==null&&t!==void 0?t:kl(this._source.size,s);this._parallelUploadUrls&&a.forEach(function(m,h){m.uploadUrl=o._parallelUploadUrls[h]||null}),this._parallelUploadUrls=new Array(a.length);var l=a.map(function(m,h){var U=0;return o._source.slice(m.start,m.end).then(function(j){var O=j.value;return new Promise(function(T,I){var B=Ze(Ze({},o.options),{},{uploadUrl:m.uploadUrl||null,storeFingerprintForResuming:!1,removeFingerprintOnSuccess:!1,parallelUploads:1,parallelUploadBoundaries:null,metadata:o.options.metadataForPartialUploads,headers:Ze(Ze({},o.options.headers),{},{"Upload-Concat":"partial"}),onSuccess:T,onError:I,onProgress:function(L){i=i-U+L,U=L,o._emitProgress(i,n)},onUploadUrlAvailable:function(){o._parallelUploadUrls[h]=_.url,o._parallelUploadUrls.filter(function(L){return!!L}).length===a.length&&o._saveUploadInUrlStorage()}}),_=new r(O,B);_.start(),o._parallelUploads.push(_)})})}),p;Promise.all(l).then(function(){p=o._openRequest("POST",o.options.endpoint),p.setHeader("Upload-Concat","final;".concat(o._parallelUploadUrls.join(" ")));var m=Ti(o.options.metadata);return m!==""&&p.setHeader("Upload-Metadata",m),o._sendRequest(p,null)}).then(function(m){if(!Qe(m.getStatus(),200)){o._emitHttpError(p,m,"tus: unexpected response while creating upload");return}var h=m.getHeader("Location");if(h==null){o._emitHttpError(p,m,"tus: invalid or missing Location header");return}o.url=Ci(o.options.endpoint,h),ye("Created upload at ".concat(o.url)),o._emitSuccess(m)}).catch(function(m){o._emitError(m)})}},{key:"_startSingleUpload",value:function(){if(this._aborted=!1,this.url!=null){ye("Resuming upload from previous URL: ".concat(this.url)),this._resumeUpload();return}if(this.options.uploadUrl!=null){ye("Resuming upload from provided URL: ".concat(this.options.uploadUrl)),this.url=this.options.uploadUrl,this._resumeUpload();return}ye("Creating a new upload"),this._createUpload()}},{key:"abort",value:function(t){var o=this;if(this._parallelUploads!=null){var n=Fl(this._parallelUploads),i;try{for(n.s();!(i=n.n()).done;){var s=i.value;s.abort(t)}}catch(a){n.e(a)}finally{n.f()}}return this._req!==null&&this._req.abort(),this._aborted=!0,this._retryTimeout!=null&&(clearTimeout(this._retryTimeout),this._retryTimeout=null),!t||this.url==null?Promise.resolve():r.terminate(this.url,this.options).then(function(){return o._removeFromUrlStorage()})}},{key:"_emitHttpError",value:function(t,o,n,i){this._emitError(new Ye(n,i,t,o))}},{key:"_emitError",value:function(t){var o=this;if(!this._aborted){if(this.options.retryDelays!=null){var n=this._offset!=null&&this._offset>this._offsetBeforeRetry;if(n&&(this._retryAttempt=0),Ai(t,this._retryAttempt,this.options)){var i=this.options.retryDelays[this._retryAttempt++];this._offsetBeforeRetry=this._offset,this._retryTimeout=setTimeout(function(){o.start()},i);return}}if(typeof this.options.onError=="function")this.options.onError(t);else throw t}}},{key:"_emitSuccess",value:function(t){this.options.removeFingerprintOnSuccess&&this._removeFromUrlStorage(),typeof this.options.onSuccess=="function"&&this.options.onSuccess({lastResponse:t})}},{key:"_emitProgress",value:function(t,o){typeof this.options.onProgress=="function"&&this.options.onProgress(t,o)}},{key:"_emitChunkComplete",value:function(t,o,n){typeof this.options.onChunkComplete=="function"&&this.options.onChunkComplete(t,o,n)}},{key:"_createUpload",value:function(){var t=this;if(!this.options.endpoint){this._emitError(new Error("tus: unable to create upload because no endpoint is provided"));return}var o=this._openRequest("POST",this.options.endpoint);this.options.uploadLengthDeferred?o.setHeader("Upload-Defer-Length","1"):o.setHeader("Upload-Length","".concat(this._size));var n=Ti(this.options.metadata);n!==""&&o.setHeader("Upload-Metadata",n);var i;this.options.uploadDataDuringCreation&&!this.options.uploadLengthDeferred?(this._offset=0,i=this._addChunkToRequest(o)):((this.options.protocol===or||this.options.protocol===Ot)&&o.setHeader("Upload-Complete","?0"),i=this._sendRequest(o,null)),i.then(function(s){if(!Qe(s.getStatus(),200)){t._emitHttpError(o,s,"tus: unexpected response while creating upload");return}var a=s.getHeader("Location");if(a==null){t._emitHttpError(o,s,"tus: invalid or missing Location header");return}if(t.url=Ci(t.options.endpoint,a),ye("Created upload at ".concat(t.url)),typeof t.options.onUploadUrlAvailable=="function"&&t.options.onUploadUrlAvailable(),t._size===0){t._emitSuccess(s),t._source.close();return}t._saveUploadInUrlStorage().then(function(){t.options.uploadDataDuringCreation?t._handleUploadResponse(o,s):(t._offset=0,t._performUpload())})}).catch(function(s){t._emitHttpError(o,null,"tus: failed to create upload",s)})}},{key:"_resumeUpload",value:function(){var t=this,o=this._openRequest("HEAD",this.url),n=this._sendRequest(o,null);n.then(function(i){var s=i.getStatus();if(!Qe(s,200)){if(s===423){t._emitHttpError(o,i,"tus: upload is currently locked; retry later");return}if(Qe(s,400)&&t._removeFromUrlStorage(),!t.options.endpoint){t._emitHttpError(o,i,"tus: unable to resume upload (new upload cannot be created without an endpoint)");return}t.url=null,t._createUpload();return}var a=Number.parseInt(i.getHeader("Upload-Offset"),10);if(Number.isNaN(a)){t._emitHttpError(o,i,"tus: invalid or missing offset value");return}var l=Number.parseInt(i.getHeader("Upload-Length"),10);if(Number.isNaN(l)&&!t.options.uploadLengthDeferred&&t.options.protocol===rr){t._emitHttpError(o,i,"tus: invalid or missing length value");return}typeof t.options.onUploadUrlAvailable=="function"&&t.options.onUploadUrlAvailable(),t._saveUploadInUrlStorage().then(function(){if(a===l){t._emitProgress(l,l),t._emitSuccess(i);return}t._offset=a,t._performUpload()})}).catch(function(i){t._emitHttpError(o,null,"tus: failed to resume upload",i)})}},{key:"_performUpload",value:function(){var t=this;if(!this._aborted){var o;this.options.overridePatchMethod?(o=this._openRequest("POST",this.url),o.setHeader("X-HTTP-Method-Override","PATCH")):o=this._openRequest("PATCH",this.url),o.setHeader("Upload-Offset","".concat(this._offset));var n=this._addChunkToRequest(o);n.then(function(i){if(!Qe(i.getStatus(),200)){t._emitHttpError(o,i,"tus: unexpected response while uploading chunk");return}t._handleUploadResponse(o,i)}).catch(function(i){t._aborted||t._emitHttpError(o,null,"tus: failed to upload chunk at offset ".concat(t._offset),i)})}}},{key:"_addChunkToRequest",value:function(t){var o=this,n=this._offset,i=this._offset+this.options.chunkSize;return t.setProgressHandler(function(s){o._emitProgress(n+s,o._size)}),this.options.protocol===rr?t.setHeader("Content-Type","application/offset+octet-stream"):this.options.protocol===Ot&&t.setHeader("Content-Type","application/partial-upload"),(i===Number.POSITIVE_INFINITY||i>this._size)&&!this.options.uploadLengthDeferred&&(i=this._size),this._source.slice(n,i).then(function(s){var a=s.value,l=s.done,p=a!=null&&a.size?a.size:0;o.options.uploadLengthDeferred&&l&&(o._size=o._offset+p,t.setHeader("Upload-Length","".concat(o._size)));var m=o._offset+p;return!o.options.uploadLengthDeferred&&l&&m!==o._size?Promise.reject(new Error("upload was configured with a size of ".concat(o._size," bytes, but the source is done after ").concat(m," bytes"))):a===null?o._sendRequest(t):((o.options.protocol===or||o.options.protocol===Ot)&&t.setHeader("Upload-Complete",l?"?1":"?0"),o._emitProgress(o._offset,o._size),o._sendRequest(t,a))})}},{key:"_handleUploadResponse",value:function(t,o){var n=Number.parseInt(o.getHeader("Upload-Offset"),10);if(Number.isNaN(n)){this._emitHttpError(t,o,"tus: invalid or missing offset value");return}if(this._emitProgress(n,this._size),this._emitChunkComplete(n-this._offset,n,this._size),this._offset=n,n===this._size){this._emitSuccess(o),this._source.close();return}this._performUpload()}},{key:"_openRequest",value:function(t,o){var n=ji(t,o,this.options);return this._req=n,n}},{key:"_removeFromUrlStorage",value:function(){var t=this;this._urlStorageKey&&(this._urlStorage.removeUpload(this._urlStorageKey).catch(function(o){t._emitError(o)}),this._urlStorageKey=null)}},{key:"_saveUploadInUrlStorage",value:function(){var t=this;if(!this.options.storeFingerprintForResuming||!this._fingerprint||this._urlStorageKey!==null)return Promise.resolve();var o={size:this._size,metadata:this.options.metadata,creationTime:new Date().toString()};return this._parallelUploads?o.parallelUploadUrls=this._parallelUploadUrls:o.uploadUrl=this.url,this._urlStorage.addUpload(this._fingerprint,o).then(function(n){t._urlStorageKey=n})}},{key:"_sendRequest",value:function(t){var o=arguments.length>1&&arguments[1]!==void 0?arguments[1]:null;return ki(t,o,this.options)}}],[{key:"terminate",value:function(t){var o=arguments.length>1&&arguments[1]!==void 0?arguments[1]:{},n=ji("DELETE",t,o);return ki(n,null,o).then(function(i){if(i.getStatus()!==204)throw new Ye("tus: unexpected response while terminating upload",null,n,i)}).catch(function(i){if(i instanceof Ye||(i=new Ye("tus: failed to terminate upload",i,n,null)),!Ai(i,0,o))throw i;var s=o.retryDelays[0],a=o.retryDelays.slice(1),l=Ze(Ze({},o),{},{retryDelays:a});return new Promise(function(p){return setTimeout(p,s)}).then(function(){return r.terminate(t,l)})})}}])})();function Ti(r){return Object.entries(r).map(function(e){var t=Li(e,2),o=t[0],n=t[1];return"".concat(o," ").concat(fi.encode(String(n)))}).join(",")}function Qe(r,e){return r>=e&&r=t.retryDelays.length||r.originalRequest==null?!1:t&&typeof t.onShouldRetry=="function"?t.onShouldRetry(r,e,t):Bi(r)}function Bi(r){var e=r.originalResponse?r.originalResponse.getStatus():0;return(!Qe(e,400)||e===409||e===423)&&jl()}function Ci(r,e){return new qi.default(e,r).toString()}function kl(r,e){for(var t=Math.floor(r/e),o=[],n=0;n=this.size;return Promise.resolve({value:n,done:i})}},{key:"close",value:function(){}}])})();function Tt(r){"@babel/helpers - typeof";return Tt=typeof Symbol=="function"&&typeof Symbol.iterator=="symbol"?function(e){return typeof e}:function(e){return e&&typeof Symbol=="function"&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e},Tt(r)}function zl(r,e){if(!(r instanceof e))throw new TypeError("Cannot call a class as a function")}function $i(r,e){for(var t=0;tthis._bufferOffset&&(this._buffer=this._buffer.slice(t-this._bufferOffset),this._bufferOffset=t);var n=Gi(this._buffer)===0;return this._done&&n?null:this._buffer.slice(0,o-t)}},{key:"close",value:function(){this._reader.cancel&&this._reader.cancel()}}])})();function Ae(r){"@babel/helpers - typeof";return Ae=typeof Symbol=="function"&&typeof Symbol.iterator=="symbol"?function(e){return typeof e}:function(e){return e&&typeof Symbol=="function"&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e},Ae(r)}function to(){"use strict";to=function(){return e};var r,e={},t=Object.prototype,o=t.hasOwnProperty,n=Object.defineProperty||function(d,u,c){d[u]=c.value},i=typeof Symbol=="function"?Symbol:{},s=i.iterator||"@@iterator",a=i.asyncIterator||"@@asyncIterator",l=i.toStringTag||"@@toStringTag";function p(d,u,c){return Object.defineProperty(d,u,{value:c,enumerable:!0,configurable:!0,writable:!0}),d[u]}try{p({},"")}catch{p=function(c,g,b){return c[g]=b}}function m(d,u,c,g){var b=u&&u.prototype instanceof B?u:B,y=Object.create(b.prototype),E=new xe(g||[]);return n(y,"_invoke",{value:dr(d,c,E)}),y}function h(d,u,c){try{return{type:"normal",arg:d.call(u,c)}}catch(g){return{type:"throw",arg:g}}}e.wrap=m;var U="suspendedStart",j="suspendedYield",O="executing",T="completed",I={};function B(){}function _(){}function F(){}var L={};p(L,s,function(){return this});var K=Object.getPrototypeOf,k=K&&K(K(_e([])));k&&k!==t&&o.call(k,s)&&(L=k);var N=F.prototype=B.prototype=Object.create(L);function te(d){["next","throw","return"].forEach(function(u){p(d,u,function(c){return this._invoke(u,c)})})}function Z(d,u){function c(b,y,E,A){var C=h(d[b],d,y);if(C.type!=="throw"){var V=C.arg,D=V.value;return D&&Ae(D)=="object"&&o.call(D,"__await")?u.resolve(D.__await).then(function(X){c("next",X,E,A)},function(X){c("throw",X,E,A)}):u.resolve(D).then(function(X){V.value=X,E(V)},function(X){return c("throw",X,E,A)})}A(C.arg)}var g;n(this,"_invoke",{value:function(y,E){function A(){return new u(function(C,V){c(y,E,C,V)})}return g=g?g.then(A,A):A()}})}function dr(d,u,c){var g=U;return function(b,y){if(g===O)throw Error("Generator is already running");if(g===T){if(b==="throw")throw y;return{value:r,done:!0}}for(c.method=b,c.arg=y;;){var E=c.delegate;if(E){var A=nt(E,c);if(A){if(A===I)continue;return A}}if(c.method==="next")c.sent=c._sent=c.arg;else if(c.method==="throw"){if(g===U)throw g=T,c.arg;c.dispatchException(c.arg)}else c.method==="return"&&c.abrupt("return",c.arg);g=O;var C=h(d,u,c);if(C.type==="normal"){if(g=c.done?T:j,C.arg===I)continue;return{value:C.arg,done:c.done}}C.type==="throw"&&(g=T,c.method="throw",c.arg=C.arg)}}}function nt(d,u){var c=u.method,g=d.iterator[c];if(g===r)return u.delegate=null,c==="throw"&&d.iterator.return&&(u.method="return",u.arg=r,nt(d,u),u.method==="throw")||c!=="return"&&(u.method="throw",u.arg=new TypeError("The iterator does not provide a '"+c+"' method")),I;var b=h(g,d.iterator,u.arg);if(b.type==="throw")return u.method="throw",u.arg=b.arg,u.delegate=null,I;var y=b.arg;return y?y.done?(u[d.resultName]=y.value,u.next=d.nextLoc,u.method!=="return"&&(u.method="next",u.arg=r),u.delegate=null,I):y:(u.method="throw",u.arg=new TypeError("iterator result is not an object"),u.delegate=null,I)}function fr(d){var u={tryLoc:d[0]};1 in d&&(u.catchLoc=d[1]),2 in d&&(u.finallyLoc=d[2],u.afterLoc=d[3]),this.tryEntries.push(u)}function Se(d){var u=d.completion||{};u.type="normal",delete u.arg,d.completion=u}function xe(d){this.tryEntries=[{tryLoc:"root"}],d.forEach(fr,this),this.reset(!0)}function _e(d){if(d||d===""){var u=d[s];if(u)return u.call(d);if(typeof d.next=="function")return d;if(!isNaN(d.length)){var c=-1,g=function b(){for(;++c=0;--b){var y=this.tryEntries[b],E=y.completion;if(y.tryLoc==="root")return g("end");if(y.tryLoc<=this.prev){var A=o.call(y,"catchLoc"),C=o.call(y,"finallyLoc");if(A&&C){if(this.prev=0;--g){var b=this.tryEntries[g];if(b.tryLoc<=this.prev&&o.call(b,"finallyLoc")&&this.prev=0;--c){var g=this.tryEntries[c];if(g.finallyLoc===u)return this.complete(g.completion,g.afterLoc),Se(g),I}},catch:function(u){for(var c=this.tryEntries.length-1;c>=0;--c){var g=this.tryEntries[c];if(g.tryLoc===u){var b=g.completion;if(b.type==="throw"){var y=b.arg;Se(g)}return y}}throw Error("illegal catch attempt")},delegateYield:function(u,c,g){return this.delegate={iterator:_e(u),resultName:c,nextLoc:g},this.method==="next"&&(this.arg=r),I}},e}function Wi(r,e,t,o,n,i,s){try{var a=r[i](s),l=a.value}catch(p){t(p);return}a.done?e(l):Promise.resolve(l).then(o,n)}function Gl(r){return function(){var e=this,t=arguments;return new Promise(function(o,n){var i=r.apply(e,t);function s(l){Wi(i,o,n,s,a,"next",l)}function a(l){Wi(i,o,n,s,a,"throw",l)}s(void 0)})}}function Hl(r,e){if(!(r instanceof e))throw new TypeError("Cannot call a class as a function")}function Vi(r,e){for(var t=0;t0&&arguments[0]!==void 0?arguments[0]:null;return new Promise(function(n,i){t._xhr.onload=function(){n(new eu(t._xhr))},t._xhr.onerror=function(s){i(s)},t._xhr.send(o)})}},{key:"abort",value:function(){return this._xhr.abort(),Promise.resolve()}},{key:"getUnderlyingObject",value:function(){return this._xhr}}])})(),eu=(function(){function r(e){oo(this,r),this._xhr=e}return no(r,[{key:"getStatus",value:function(){return this._xhr.status}},{key:"getHeader",value:function(t){return this._xhr.getResponseHeader(t)}},{key:"getBody",value:function(){return this._xhr.responseText}},{key:"getUnderlyingObject",value:function(){return this._xhr}}])})();function kt(r){"@babel/helpers - typeof";return kt=typeof Symbol=="function"&&typeof Symbol.iterator=="symbol"?function(e){return typeof e}:function(e){return e&&typeof Symbol=="function"&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e},kt(r)}function tu(r,e){if(!(r instanceof e))throw new TypeError("Cannot call a class as a function")}function Ji(r,e){for(var t=0;t0&&arguments[0]!==void 0?arguments[0]:null,o=arguments.length>1&&arguments[1]!==void 0?arguments[1]:{};return iu(this,e),o=et(et({},lr),o),au(this,e,[t,o])}return cu(e,r),su(e,null,[{key:"terminate",value:function(o){var n=arguments.length>1&&arguments[1]!==void 0?arguments[1]:{};return n=et(et({},lr),n),nr.terminate(o,n)}}])})(nr),lf=typeof XMLHttpRequest=="function"&&typeof Blob=="function"&&typeof Blob.prototype.slice=="function";var ss={name:"@uppy/tus",description:"Resumable uploads for Uppy using Tus.io",version:"5.1.1",license:"MIT",type:"module",sideEffects:!1,scripts:{build:"tsc --build tsconfig.build.json",typecheck:"tsc --build",test:"vitest run --environment=jsdom --silent='passed-only'"},keywords:["file uploader","uppy","uppy-plugin","upload","resumable","tus"],homepage:"https://uppy.io",bugs:{url:"https://github.com/transloadit/uppy/issues"},repository:{type:"git",url:"git+https://github.com/transloadit/uppy.git"},files:["src","lib","dist","CHANGELOG.md"],exports:{".":"./lib/index.js","./package.json":"./package.json"},dependencies:{"@uppy/companion-client":"^5.1.1","@uppy/utils":"^7.1.5","tus-js-client":"^4.2.3"},devDependencies:{"@uppy/core":"^5.2.0",jsdom:"^26.1.0",typescript:"^5.8.3",vitest:"^3.2.4"},peerDependencies:{"@uppy/core":"^5.2.0"}};function hu(){return typeof window<"u"&&(typeof window.PhoneGap<"u"||typeof window.Cordova<"u"||typeof window.cordova<"u")}function mu(){return typeof navigator<"u"&&typeof navigator.product=="string"&&navigator.product.toLowerCase()==="reactnative"}function lo(r){return(e,t)=>{if(hu()||mu())return lr.fingerprint(e,t);let o=["tus",r.id,t.endpoint].join("-");return Promise.resolve(o)}}var as={endpoint:"",uploadUrl:null,metadata:{},uploadSize:null,onProgress:null,onChunkComplete:null,onSuccess:null,onError:null,overridePatchMethod:!1,headers:{},addRequestId:!1,chunkSize:1/0,retryDelays:[100,1e3,3e3,5e3],parallelUploads:1,removeFingerprintOnSuccess:!1,uploadLengthDeferred:!1,uploadDataDuringCreation:!1},gu={limit:20,retryDelays:as.retryDelays,withCredentials:!1,allowedMetaFields:!0},At,be,ls,us,cs,Ct,rt=class extends pe{constructor(t,o){super(t,{...gu,...o});S(this,be);S(this,At);w(this,"requests");w(this,"uploaders");w(this,"uploaderEvents");S(this,Ct,async t=>{if(t.length===0){this.uppy.log("[Tus] No files to upload");return}this.opts.limit===0&&this.uppy.log("[Tus] When uploading multiple files at once, consider setting the `limit` option (to `10` for example), to limit the number of concurrent uploads, which helps prevent memory and network issues: https://uppy.io/docs/tus/#limit-0","warning"),this.uppy.log("[Tus] Uploading...");let o=this.uppy.getFilesByIds(t);await v(this,be,cs).call(this,o)});if(this.type="uploader",this.id=this.opts.id||"Tus",o?.allowedMetaFields===void 0&&"metaFields"in this.opts)throw new Error("The `metaFields` option has been renamed to `allowedMetaFields`.");if("autoRetry"in o)throw new Error("The `autoRetry` option was deprecated and has been removed.");this.requests=this.opts.rateLimitedQueue??new st(this.opts.limit),P(this,At,this.opts.retryDelays?.values()),this.uploaders=Object.create(null),this.uploaderEvents=Object.create(null)}resetUploaderReferences(t,o){let n=this.uploaders[t];n&&(n.abort(),o?.abort&&n.abort(!0),this.uploaders[t]=null),this.uploaderEvents[t]&&(this.uploaderEvents[t].remove(),this.uploaderEvents[t]=null)}onReceiveUploadUrl(t,o){let n=this.uppy.getFile(t.id);n&&(!n.tus||n.tus.uploadUrl!==o)&&(this.uppy.log("[Tus] Storing upload url"),this.uppy.setFileState(n.id,{tus:{...n.tus,uploadUrl:o}}))}install(){this.uppy.setState({capabilities:{...this.uppy.getState().capabilities,resumableUploads:!0}}),this.uppy.addUploader(f(this,Ct))}uninstall(){this.uppy.setState({capabilities:{...this.uppy.getState().capabilities,resumableUploads:!1}}),this.uppy.removeUploader(f(this,Ct))}};At=new WeakMap,be=new WeakSet,ls=async function(t){return this.resetUploaderReferences(t.id),new Promise((o,n)=>{let i,s,a,l={...this.opts,...t.tus||{}};typeof l.headers=="function"&&(l.headers=l.headers(t));let{onShouldRetry:p,onBeforeRequest:m,...h}=l,U={...as,...h};U.fingerprint=lo(t),U.onBeforeRequest=async _=>{let F=_.getUnderlyingObject();F&&(F.withCredentials=!!l.withCredentials);let L;if(typeof m=="function"&&(L=m(_,t)),qe(i,"shouldBeRequeued")){if(!i.shouldBeRequeued)return Promise.reject();let K,k=new Promise(N=>{K=N});i=this.requests.run(()=>(t.isPaused&&i.abort(),K(),()=>{})),await Promise.all([k,L]);return}return L},U.onError=_=>{this.uppy.log(_);let F=_.originalRequest!=null?_.originalRequest.getUnderlyingObject():null;_r(F)&&(_=new gr(_,F)),this.resetUploaderReferences(t.id),i?.abort(),typeof l.onError=="function"&&l.onError(_),n(_)},U.onProgress=(_,F)=>{this.onReceiveUploadUrl(t,a.url),typeof l.onProgress=="function"&&l.onProgress(_,F);let L=this.uppy.getFile(t.id);this.uppy.emit("upload-progress",L,{uploadStarted:L.progress.uploadStarted??0,bytesUploaded:_,bytesTotal:F})},U.onSuccess=_=>{let F={uploadURL:a.url??void 0,status:200,body:{xhr:_.lastResponse.getUnderlyingObject()}};this.uppy.emit("upload-success",this.uppy.getFile(t.id),F),this.resetUploaderReferences(t.id),i.done(),a.url&&this.uppy.log(`Download ${a.url}`),typeof l.onSuccess=="function"&&l.onSuccess(_),o(a)};let j=_=>{let F=_?.originalResponse?.getStatus();if(F===429){if(!this.requests.isPaused){let L=f(this,At)?.next();if(L==null||L.done)return!1;this.requests.rateLimit(L.value)}}else{if(F!=null&&F>=400&&F<500&&F!==409&&F!==423)return!1;typeof navigator<"u"&&navigator.onLine===!1&&(this.requests.isPaused||(this.requests.pause(),window.addEventListener("online",()=>{this.requests.resume()},{once:!0})))}return i.abort(),i={shouldBeRequeued:!0,abort(){this.shouldBeRequeued=!1},done(){throw new Error("Cannot mark a queued request as done: this indicates a bug")},fn(){throw new Error("Cannot run a queued request: this indicates a bug")}},!0};p!=null?U.onShouldRetry=(_,F)=>p(_,F,l,j):U.onShouldRetry=j;let O=(_,F,L)=>{qe(_,F)&&!qe(_,L)&&(_[L]=_[F])},T={};if(qt(l.allowedMetaFields,t.meta).forEach(_=>{T[_]=String(t.meta[_])}),O(T,"type","filetype"),O(T,"name","filename"),U.metadata=T,t.data==null)throw new Error("File data is empty");a=new ns(t.data,U),this.uploaders[t.id]=a;let B=new Ge(this.uppy);this.uploaderEvents[t.id]=B,s=()=>(t.isPaused||a.start(),()=>{}),a.findPreviousUploads().then(_=>{let F=_[0];F&&(this.uppy.log(`[Tus] Resuming upload of ${t.id} started at ${F.creationTime}`),a.resumeFromPreviousUpload(F)),i=this.requests.run(s)}),B.onFileRemove(t.id,_=>{i.abort(),this.resetUploaderReferences(t.id,{abort:!!a.url}),o(`upload ${_} was removed`)}),B.onPause(t.id,_=>{i.abort(),_?a.abort():i=this.requests.run(s)}),B.onPauseAll(t.id,()=>{i.abort(),a.abort()}),B.onCancelAll(t.id,()=>{i.abort(),this.resetUploaderReferences(t.id,{abort:!!a.url}),o(`upload ${t.id} was canceled`)}),B.onResumeAll(t.id,()=>{i.abort(),t.error&&a.abort(),i=this.requests.run(s)})}).catch(o=>{throw this.uppy.emit("upload-error",t,o),o})},us=function(t){let o={...this.opts};return t.tus&&Object.assign(o,t.tus),typeof o.headers=="function"&&(o.headers=o.headers(t)),{..."remote"in t&&t.remote.body,endpoint:o.endpoint,uploadUrl:o.uploadUrl,protocol:"tus",size:t.data.size,headers:o.headers,metadata:t.meta}},cs=async function(t){let o=yr(t),n=br(o);this.uppy.emit("upload-start",n),await Promise.allSettled(o.map(i=>{if(i.isRemote){let s=()=>this.requests,a=new AbortController,l=m=>{m.id===i.id&&a.abort()};this.uppy.on("file-removed",l);let p=this.uppy.getRequestClientForFile(i).uploadRemoteFile(i,v(this,be,us).call(this,i),{signal:a.signal,getQueue:s});return this.requests.wrapSyncFunction(()=>{this.uppy.off("file-removed",l)},{priority:-1})(),p}return v(this,be,ls).call(this,i)}))},Ct=new WeakMap,w(rt,"VERSION",ss.version);var R={summary:document.querySelector("#summary"),expectedFilename:document.querySelector("#expected-filename"),expectedSize:document.querySelector("#expected-size"),maximumSize:document.querySelector("#maximum-size"),expiresAt:document.querySelector("#expires-at"),fileInput:document.querySelector("#file-input"),selectedFile:document.querySelector("#selected-file"),progress:document.querySelector("#upload-progress"),uploadState:document.querySelector("#upload-state"),uploadButton:document.querySelector("#upload-button"),pauseButton:document.querySelector("#pause-button"),retryButton:document.querySelector("#retry-button"),intentState:document.querySelector("#intent-state"),nextAction:document.querySelector("#next-action"),intentId:document.querySelector("#intent-id"),jobId:document.querySelector("#job-id"),mediaId:document.querySelector("#media-id")},ps=new Set(["ready","failed","expired"]),yu=new Set(["pending","accepted","uploading"]),co=2e3,M,q,po,Le,Ie,ds,uo=!1,fo="pending";function G(r,e){r.textContent=e==null||e===""?"—":String(e)}function H(r,e=""){G(R.uploadState,r),R.uploadState.classList.remove("error","success"),e&&R.uploadState.classList.add(e)}function ot(r){if(!Number.isFinite(r))return"—";if(r===0)return"0 bytes";let e=["bytes","KiB","MiB","GiB","TiB"],t=Math.min(Math.floor(Math.log(r)/Math.log(1024)),e.length-1),o=r/1024**t,n=t===0||o>=10?0:1;return`${o.toFixed(n)} ${e[t]}`}function bu(r){if(!r)return"—";let e=new Date(r);return Number.isNaN(e.getTime())?String(r):new Intl.DateTimeFormat(void 0,{dateStyle:"medium",timeStyle:"short"}).format(e)}function vu(){let r=window.location.hash.slice(1);if(!r)return null;let t=new URLSearchParams(r).get("capability");if(t)return t;if(!r.includes("="))try{return decodeURIComponent(r)}catch{return null}return null}function wu(){window.history.replaceState(null,document.title,`${window.location.pathname}${window.location.search}`)}function ur(r){let e=new URL(`${window.location.pathname}/`,window.location.origin);return new URL(r.replace(/^\.\//,""),e).href}function Su(r,e){let t=r?.error?.message??r?.error?.detail?.message??r?.message??e;return typeof t=="string"&&t.length<=300?t:e}async function cr(r,e={}){let t=await fetch(r,{cache:"no-store",credentials:"same-origin",...e,headers:{Accept:"application/json",...e.body?{"Content-Type":"application/json"}:{},...e.headers}}),o;try{o=await t.json()}catch{o=null}if(!t.ok)throw new Error(Su(o,`VidXP rejected the request (${t.status}).`));return o??{}}function hs(r){let e=r?.status??r??{};return e.intent??e}function Ce(r,e){for(let t of e)if(r?.[t]!=null)return r[t];return null}function xu(r){let e=hs(r),t=Number(Ce(e,["byte_size","declared_byte_size","size"])),o=Number(Ce(e,["configured_maximum_bytes","maximum_bytes","max_bytes"]));return{intentId:Ce(e,["intent_id","id"]),filename:Ce(e,["original_filename","filename","declared_filename"]),byteSize:t,maximumBytes:o,expiresAt:Ce(e,["expires_at","expiry","expires"])}}function _u(){G(R.expectedFilename,q.filename),G(R.expectedSize,ot(q.byteSize)),G(R.maximumSize,ot(q.maximumBytes)),G(R.expiresAt,bu(q.expiresAt)),G(R.summary,`Select ${q.filename} (${ot(q.byteSize)}) to upload directly to VidXP.`)}function Uu(r){return Ce(r,["next_action","guidance","message","status_message"])}function ho(r){let e=hs(r),t=String(e.state??e.status??"pending").toLowerCase();return fo=t,G(R.intentState,t),G(R.intentId,Ce(e,["intent_id","id"])),G(R.jobId,e.job_id),G(R.mediaId,e.media_id),G(R.nextAction,Uu(e)??"VidXP is waiting for the upload to continue."),e.media_id?H("The video is ready in VidXP.","success"):(t==="failed"||t==="expired")&&H(t==="expired"?"This upload handoff has expired.":"VidXP could not finish importing this upload.","error"),we(),t}function Nt(){return M?.getFiles()[0]??null}function ve(r=Nt()){if(!r){G(R.selectedFile,"No file selected."),R.progress.value=0,R.progress.textContent="0%",we();return}let e=r.isGhost?" — re-select this file to recover the interrupted upload.":"";G(R.selectedFile,`${r.name} · ${ot(r.size)}${e}`);let t=Number(r.progress?.percentage??0);R.progress.value=Number.isFinite(t)?t:0,R.progress.textContent=`${Math.round(R.progress.value)}%`,r.isGhost&&H("The resumable upload was found. Re-select the same local file to continue."),we()}function we(){let r=Nt(),e=ps.has(fo),t=yu.has(fo),o=r&&!r.isGhost,n=!!r?.progress?.uploadComplete,i=!!r?.progress?.uploadStarted&&!n;R.fileInput.disabled=!M||!t||e||n,R.uploadButton.disabled=!o||!t||e||n||i,R.pauseButton.disabled=!o||!t||e||n||!i,R.retryButton.disabled=!o||!t||e||!r?.error,G(R.pauseButton,r?.isPaused?"Resume":"Pause")}function ms(r){return r.name!==q.filename?`Choose the expected file named ${q.filename}.`:r.size!==q.byteSize?`The selected file is ${ot(r.size)}; VidXP expects ${ot(q.byteSize)}.`:null}function Lt(r){let e=new URL(r,window.location.href);return e.hash="",e.href}function fs(r){return r.getMethod().toUpperCase()==="POST"&&Lt(r.getURL())===po}function gs(r){return r?.grant??r?.creation_grant??r?.capability??null}async function Fu(){if(Ie)return Ie;let r=await cr(ur("./creation-grant"),{method:"POST",body:"{}"});r.status&&ho(r);let e=gs(r);if(typeof e!="string"||!e)throw new Error("VidXP did not issue an upload creation grant.");return Ie=e,e}function Eu(){let r=new Date(q.expiresAt).getTime();return Number.isFinite(r)?Math.max(1e3,r-Date.now()):3600*1e3}function Pu(r){let e=r?.originalResponse?.getHeader?.("X-VidXP-Error");return typeof e=="string"&&/^[a-z0-9_-]{1,80}$/i.test(e)?`VidXP rejected the upload (${e}).`:"The transfer was interrupted. Check the connection and retry."}function Ou(r){if(!r||!M)return;Le=Lt(r),M.getPlugin("Tus")?.setOptions({uploadUrl:Le});let e=Nt();e&&e.tus?.uploadUrl!==Le&&M.setFileState(e.id,{tus:{...e.tus,uploadUrl:Le}})}function Ru(){let r=`vidxp-upload-${String(q.intentId).replace(/[^a-z0-9_-]/gi,"_")}`;M=new Vt({id:r,autoProceed:!1,meta:{intent_id:String(q.intentId)},restrictions:{maxNumberOfFiles:1,minFileSize:q.byteSize,maxFileSize:q.byteSize},onBeforeFileAdded(e){let t=ms(e);return t&&H(t,"error"),t==null}}),M.on("file-added",ve),M.on("file-removed",()=>ve()),M.on("restriction-failed",()=>{H("The selected file does not match this upload handoff.","error")}),M.on("upload-progress",e=>ve(e)),M.on("upload-pause",(e,t)=>{H(t?"Upload paused.":"Upload resumed."),ve(e)}),M.on("upload-error",(e,t)=>{Ie=null,H(Pu(t),"error"),e&&ve(e)}),M.on("upload-success",e=>{H("Transfer complete. VidXP is validating and importing the video.","success"),ve(e),It(0)}),M.on("restored",()=>{ve(),It(0)}),M.use(rt,{endpoint:po,...Le?{uploadUrl:Lt(Le)}:{},allowedMetaFields:["intent_id"],parallelUploads:1,overridePatchMethod:!1,uploadDataDuringCreation:!1,withCredentials:!1,removeFingerprintOnSuccess:!0,async onBeforeRequest(e){if(!fs(e))return;let t=await Fu();e.setHeader("Authorization",`VidXP-Handoff ${t}`)},onAfterResponse(e){fs(e)&&(Ie=null)}}),M.use(wt,{id:`GoldenRetriever:${r}`,expires:Eu(),serviceWorker:!1}),ve()}async function Tu(){if(!uo){uo=!0;try{let r=await cr(ur("./status"));Ou(r.resume_url);let e=ho(r);ps.has(e)||It(co)}catch{G(R.nextAction,"The status check failed. This page will try again automatically."),It(co)}finally{uo=!1}}}function It(r=co){window.clearTimeout(ds),ds=window.setTimeout(Tu,r)}async function ju(){let r=vu();wu();let e=r?await cr(ur("./bootstrap"),{method:"POST",body:JSON.stringify({capability:r})}):await cr(ur("./status"));if(q=xu(e),!q.intentId||!q.filename||!Number.isFinite(q.byteSize)||q.byteSize<0)throw new Error("VidXP returned an incomplete upload handoff.");if(Number.isFinite(q.maximumBytes)||(q.maximumBytes=q.byteSize),!e.creation_url)throw new Error("VidXP did not provide the tus creation endpoint.");po=Lt(e.creation_url),Le=e.resume_url?Lt(e.resume_url):null,Ie=gs(e),_u(),ho(e),Ru(),It(0)}R.fileInput.addEventListener("change",()=>{let r=R.fileInput.files?.[0];if(R.fileInput.value="",!r||!M)return;let e=ms(r);if(e){H(e,"error");return}try{M.addFile({source:"VidXP upload page",name:r.name,type:r.type||"application/octet-stream",data:r,meta:{intent_id:String(q.intentId)}}),H("The expected file is ready to upload.")}catch{H("The file could not be selected. Re-open it and try again.","error")}});R.uploadButton.addEventListener("click",async()=>{H("Starting the resumable upload…"),we();try{(await M.upload())?.failed?.length&&H("The transfer did not finish. Retry when ready.","error")}catch{H("The transfer did not finish. Retry when ready.","error")}we()});R.pauseButton.addEventListener("click",()=>{let r=Nt();r&&M.pauseResume(r.id),we()});R.retryButton.addEventListener("click",async()=>{let r=Nt();if(r){Ie=null,H("Retrying the resumable upload…");try{await M.retryUpload(r.id)}catch{H("The retry did not finish. Check the connection and try again.","error")}we()}});ju().catch(r=>{let e=r instanceof Error?r.message:"The upload page could not start.";G(R.summary,e),H(e,"error"),G(R.nextAction,"Request a new upload handoff from VidXP."),we()}); diff --git a/src/vidxp/core/uploads.py b/src/vidxp/core/uploads.py index d373a2b..c3d8c0f 100644 --- a/src/vidxp/core/uploads.py +++ b/src/vidxp/core/uploads.py @@ -55,25 +55,73 @@ def _validate_state(self) -> "UploadIntentRecord": raise ValueError("upload expiry must follow creation") if self.state == UploadState.pending and self.upload_id is not None: raise ValueError("pending uploads cannot have an upload identifier") - if self.state in { - UploadState.accepted, - UploadState.processing, - UploadState.failed, - } and self.upload_id is None: + if ( + self.state + in { + UploadState.accepted, + UploadState.processing, + UploadState.failed, + } + and self.upload_id is None + ): raise ValueError(f"{self.state} uploads require an upload identifier") - if self.state in { - UploadState.processing, - UploadState.failed, - UploadState.ready, - } and self.job_id is None: + if ( + self.state + in { + UploadState.processing, + UploadState.failed, + UploadState.ready, + } + and self.job_id is None + ): raise ValueError(f"{self.state} uploads require a job identifier") - if self.state in { - UploadState.pending, - UploadState.accepted, - } and self.job_id is not None: + if ( + self.state + in { + UploadState.pending, + UploadState.accepted, + } + and self.job_id is not None + ): raise ValueError(f"{self.state} uploads cannot have a job identifier") if self.state == UploadState.ready and self.media_id is None: raise ValueError("ready uploads require a media identifier") if self.media_id is not None and self.state != UploadState.ready: raise ValueError("only ready uploads may reference media") return self + + +class UploadHandoffRecord(BaseModel): + """Transport authorization state for one existing upload intent.""" + + model_config = ConfigDict( + extra="forbid", + frozen=True, + allow_inf_nan=False, + ) + + selector: str = Field(pattern=r"^[0-9a-f]{32}$") + intent_id: Uuid4Hex + repository_binding: str = Field(pattern=r"^[0-9a-f]{64}$") + byte_size: int = Field(gt=0) + created_at: AwareDatetime + expires_at: AwareDatetime + session_digest: str | None = Field( + default=None, + pattern=r"^[0-9a-f]{64}$", + ) + creation_grant_digest: str | None = Field( + default=None, + pattern=r"^[0-9a-f]{64}$", + ) + creation_grant_expires_at: AwareDatetime | None = None + + @model_validator(mode="after") + def _validate_handoff(self) -> "UploadHandoffRecord": + if self.expires_at <= self.created_at: + raise ValueError("handoff expiry must follow creation") + if (self.creation_grant_digest is None) != ( + self.creation_grant_expires_at is None + ): + raise ValueError("creation grant digest and expiry must be stored together") + return self diff --git a/src/vidxp/infrastructure/sql_catalog.py b/src/vidxp/infrastructure/sql_catalog.py index 695505a..50b451f 100644 --- a/src/vidxp/infrastructure/sql_catalog.py +++ b/src/vidxp/infrastructure/sql_catalog.py @@ -5,20 +5,34 @@ from datetime import datetime from typing import Any -from sqlalchemy import Engine, create_engine, delete, event, func, insert, select, update +from sqlalchemy import ( + Engine, + create_engine, + delete, + event, + func, + insert, + select, + update, +) from sqlalchemy.engine import Connection from sqlalchemy.exc import IntegrityError from sqlalchemy.pool import NullPool from vidxp.core.artifacts import ArtifactRecord, ArtifactState from vidxp.core.media import MediaRecord, utc_now -from vidxp.core.uploads import UploadIntentRecord, UploadState +from vidxp.core.uploads import ( + UploadHandoffRecord, + UploadIntentRecord, + UploadState, +) from vidxp.infrastructure.sql_tables import ( artifact_requests, artifacts, media, media_import_requests, metadata, + upload_handoffs, upload_intents, upload_quota, ) @@ -62,6 +76,24 @@ def _upload_record(row: Any) -> UploadIntentRecord: ) +def _upload_handoff_record(row: Any) -> UploadHandoffRecord: + return UploadHandoffRecord( + selector=row.selector, + intent_id=row.intent_id, + repository_binding=row.repository_binding, + byte_size=row.byte_size, + created_at=datetime.fromisoformat(row.created_at), + expires_at=datetime.fromisoformat(row.expires_at), + session_digest=row.session_digest, + creation_grant_digest=row.creation_grant_digest, + creation_grant_expires_at=( + datetime.fromisoformat(row.creation_grant_expires_at) + if row.creation_grant_expires_at is not None + else None + ), + ) + + class SQLCatalog: """SQLAlchemy Core catalog shared by SQLite and PostgreSQL adapters.""" @@ -134,22 +166,14 @@ def get_media(self, media_id: str) -> MediaRecord | None: payload = connection.execute( select(media.c.payload).where(media.c.media_id == media_id) ).scalar_one_or_none() - return ( - None - if payload is None - else _record(MediaRecord, payload) - ) + return None if payload is None else _record(MediaRecord, payload) def get_media_by_checksum(self, sha256: str) -> MediaRecord | None: with self.engine.connect() as connection: payload = connection.execute( select(media.c.payload).where(media.c.sha256 == sha256) ).scalar_one_or_none() - return ( - None - if payload is None - else _record(MediaRecord, payload) - ) + return None if payload is None else _record(MediaRecord, payload) def put_media(self, record: MediaRecord) -> MediaRecord: values = { @@ -193,11 +217,7 @@ def _media_by_id( payload = connection.execute( select(media.c.payload).where(media.c.media_id == media_id) ).scalar_one_or_none() - return ( - None - if payload is None - else _record(MediaRecord, payload) - ) + return None if payload is None else _record(MediaRecord, payload) @staticmethod def _media_by_checksum( @@ -207,11 +227,7 @@ def _media_by_checksum( payload = connection.execute( select(media.c.payload).where(media.c.sha256 == sha256) ).scalar_one_or_none() - return ( - None - if payload is None - else _record(MediaRecord, payload) - ) + return None if payload is None else _record(MediaRecord, payload) def list_media( self, @@ -228,17 +244,12 @@ def list_media( .limit(limit) .offset(offset) ).scalars() - return tuple( - _record(MediaRecord, payload) - for payload in payloads - ) + return tuple(_record(MediaRecord, payload) for payload in payloads) def count_media(self) -> int: with self.engine.connect() as connection: return int( - connection.execute( - select(func.count()).select_from(media) - ).scalar_one() + connection.execute(select(func.count()).select_from(media)).scalar_one() ) def reserve_media_import( @@ -271,9 +282,7 @@ def reserve_media_import( media_import_requests.c.request_fingerprint, media_import_requests.c.media_id, ) - .where( - media_import_requests.c.request_key == request_key - ) + .where(media_import_requests.c.request_key == request_key) .with_for_update() ).one() if row.request_fingerprint != request_fingerprint: @@ -284,9 +293,7 @@ def reserve_media_import( return None stored = self._media_by_id(connection, row.media_id) if stored is None: - raise RuntimeError( - "A completed media import references missing media." - ) + raise RuntimeError("A completed media import references missing media.") return stored def complete_media_import( @@ -309,9 +316,7 @@ def complete_media_import( "The media import reservation does not match the content." ) if row.media_id is not None and row.media_id != record.media_id: - raise FileExistsError( - "The media import key is already completed." - ) + raise FileExistsError("The media import key is already completed.") connection.execute( update(media_import_requests) .where(media_import_requests.c.request_key == request_key) @@ -325,11 +330,7 @@ def get_artifact(self, artifact_id: str) -> ArtifactRecord | None: artifacts.c.artifact_id == artifact_id ) ).scalar_one_or_none() - return ( - None - if payload is None - else _record(ArtifactRecord, payload) - ) + return None if payload is None else _record(ArtifactRecord, payload) def get_artifact_by_request( self, @@ -341,8 +342,7 @@ def get_artifact_by_request( .select_from( artifact_requests.join( artifacts, - artifact_requests.c.artifact_id - == artifacts.c.artifact_id, + artifact_requests.c.artifact_id == artifacts.c.artifact_id, ) ) .where(artifact_requests.c.request_key == request_key) @@ -350,12 +350,8 @@ def get_artifact_by_request( if payload is None: return None record = _record(ArtifactRecord, payload) - if ( - record.state != ArtifactState.ready - or ( - record.expires_at is not None - and record.expires_at <= utc_now() - ) + if record.state != ArtifactState.ready or ( + record.expires_at is not None and record.expires_at <= utc_now() ): return None return record @@ -367,12 +363,9 @@ def put_artifact(self, record: ArtifactRecord) -> ArtifactRecord: record.request_key, ) if request_match is not None: - if ( - request_match.state == ArtifactState.ready - and ( - request_match.expires_at is None - or request_match.expires_at > utc_now() - ) + if request_match.state == ArtifactState.ready and ( + request_match.expires_at is None + or request_match.expires_at > utc_now() ): return request_match connection.execute( @@ -412,15 +405,9 @@ def _artifact_by_id( artifact_id: str, ) -> ArtifactRecord | None: payload = connection.execute( - select(artifacts.c.payload).where( - artifacts.c.artifact_id == artifact_id - ) + select(artifacts.c.payload).where(artifacts.c.artifact_id == artifact_id) ).scalar_one_or_none() - return ( - None - if payload is None - else _record(ArtifactRecord, payload) - ) + return None if payload is None else _record(ArtifactRecord, payload) @staticmethod def _artifact_by_request( @@ -437,11 +424,7 @@ def _artifact_by_request( ) .where(artifact_requests.c.request_key == request_key) ).scalar_one_or_none() - return ( - None - if payload is None - else _record(ArtifactRecord, payload) - ) + return None if payload is None else _record(ArtifactRecord, payload) def invalidate_artifact_request( self, @@ -485,6 +468,119 @@ def create_upload_intent( ) return record + def create_upload_handoff( + self, + record: UploadHandoffRecord, + ) -> UploadHandoffRecord: + with self._write_transaction() as connection: + connection.execute( + insert(upload_handoffs).values( + selector=record.selector, + intent_id=record.intent_id, + repository_binding=record.repository_binding, + byte_size=record.byte_size, + created_at=record.created_at.isoformat(), + expires_at=record.expires_at.isoformat(), + session_digest=record.session_digest, + creation_grant_digest=record.creation_grant_digest, + creation_grant_expires_at=( + record.creation_grant_expires_at.isoformat() + if record.creation_grant_expires_at is not None + else None + ), + ) + ) + return record + + def get_upload_handoff_by_intent( + self, + intent_id: str, + *, + connection: Connection | None = None, + for_update: bool = False, + ) -> UploadHandoffRecord | None: + statement = select(upload_handoffs).where( + upload_handoffs.c.intent_id == intent_id + ) + if for_update: + statement = statement.with_for_update() + if connection is not None: + row = connection.execute(statement).one_or_none() + else: + with self.engine.connect() as active: + row = active.execute(statement).one_or_none() + return None if row is None else _upload_handoff_record(row) + + def get_upload_handoff( + self, + selector: str, + *, + connection: Connection | None = None, + for_update: bool = False, + ) -> UploadHandoffRecord | None: + statement = select(upload_handoffs).where( + upload_handoffs.c.selector == selector + ) + if for_update: + statement = statement.with_for_update() + if connection is not None: + row = connection.execute(statement).one_or_none() + else: + with self.engine.connect() as active: + row = active.execute(statement).one_or_none() + return None if row is None else _upload_handoff_record(row) + + def set_upload_handoff_session( + self, + selector: str, + *, + session_digest: str, + connection: Connection, + ) -> None: + result = connection.execute( + update(upload_handoffs) + .where(upload_handoffs.c.selector == selector) + .values(session_digest=session_digest) + ) + if result.rowcount != 1: + raise RuntimeError("The upload handoff session update was lost.") + + def set_upload_creation_grant( + self, + selector: str, + *, + digest: str, + expires_at: datetime, + connection: Connection, + ) -> None: + result = connection.execute( + update(upload_handoffs) + .where(upload_handoffs.c.selector == selector) + .values( + creation_grant_digest=digest, + creation_grant_expires_at=expires_at.isoformat(), + ) + ) + if result.rowcount != 1: + raise RuntimeError("The upload creation grant update was lost.") + + def consume_upload_creation_grant( + self, + selector: str, + *, + connection: Connection, + ) -> None: + result = connection.execute( + update(upload_handoffs) + .where(upload_handoffs.c.selector == selector) + .values( + creation_grant_digest=None, + creation_grant_expires_at=None, + ) + ) + if result.rowcount != 1: + raise RuntimeError("The upload creation grant update was lost.") + @staticmethod def _reserve_upload_quota( connection: Connection, @@ -631,9 +727,7 @@ def update_upload( .where( upload_quota.c.singleton_id == _UPLOAD_QUOTA_ID, ) - .values( - reserved_bytes=max(0, int(quota) - current.byte_size) - ) + .values(reserved_bytes=max(0, int(quota) - current.byte_size)) ) return True diff --git a/src/vidxp/infrastructure/sql_tables.py b/src/vidxp/infrastructure/sql_tables.py index 9e1030f..37ab16e 100644 --- a/src/vidxp/infrastructure/sql_tables.py +++ b/src/vidxp/infrastructure/sql_tables.py @@ -85,6 +85,26 @@ ), ) +upload_handoffs = Table( + "upload_handoffs", + metadata, + Column("selector", String(32), primary_key=True), + Column( + "intent_id", + String(32), + ForeignKey("upload_intents.intent_id", ondelete="CASCADE"), + nullable=False, + unique=True, + ), + Column("repository_binding", String(64), nullable=False), + Column("byte_size", BigInteger, nullable=False), + Column("created_at", Text, nullable=False), + Column("expires_at", Text, nullable=False), + Column("session_digest", String(64), nullable=True), + Column("creation_grant_digest", String(64), nullable=True), + Column("creation_grant_expires_at", Text, nullable=True), +) + upload_quota = Table( "upload_quota", metadata, diff --git a/src/vidxp/mcp.py b/src/vidxp/mcp.py index 89f9ae4..2788f19 100644 --- a/src/vidxp/mcp.py +++ b/src/vidxp/mcp.py @@ -6,6 +6,7 @@ from dataclasses import dataclass from functools import partial from typing import Annotated, Callable, TypeVar +from urllib.parse import quote import anyio from mcp.server import MCPServer @@ -18,6 +19,7 @@ from mcp.server.mcpserver.exceptions import ToolError from mcp.types import Icon, ResourceLink, ToolAnnotations from pydantic import Field +from pydantic import AwareDatetime from starlette.requests import Request from starlette.types import ASGIApp, Receive, Scope, Send @@ -28,6 +30,7 @@ CapabilityList, CreateSnippetCommand, CreateIndexCommand, + CreateUploadIntentCommand, ErrorCategory, Identifier, IndexStatus, @@ -39,12 +42,14 @@ MediaAsset, MediaId, MediaPage, + MediaUploadStatus, Principal, PrepareModelsCommand, QueryVideoCommand, RuntimeReadiness, SearchCommand, WorkspaceOverview, + UploadIntentId, ) from vidxp.authentication import ( OIDCBearerAuthenticator, @@ -58,9 +63,14 @@ icon_data_uri, ) from vidxp.composition import ControlPlaneContext, HttpApplicationContext -from vidxp.idempotency import IdempotencyKey, scoped_job_id +from vidxp.idempotency import ( + IdempotencyKey, + scoped_job_id, + scoped_request_key, +) from vidxp.core.identifiers import ArtifactId from vidxp.settings import HttpAuthMode +from vidxp.upload_service import RemoteUploadService _LOGGER = logging.getLogger(__name__) @@ -157,6 +167,28 @@ class RemoteMCP: transport_security: TransportSecuritySettings +class MediaUploadHandoff(MediaUploadStatus): + upload_page_url: str = Field( + min_length=1, + max_length=4096, + description=( + "HTTPS page for the user to open; the fragment carries the " + "short-lived handoff capability." + ), + ) + handoff_expires_at: AwareDatetime + + +def _uploads(context: ControlPlaneContext) -> RemoteUploadService: + if context.uploads is None: + raise ApplicationError( + "remote_upload_unavailable", + ErrorCategory.unavailable, + "Remote resumable uploads are not configured.", + ) + return context.uploads + + def _principal(default: Principal | None) -> Principal: current = _REQUEST_PRINCIPAL.get() if current is not None: @@ -282,10 +314,7 @@ async def __call__( receive: Receive, send: Send, ) -> None: - if ( - scope["type"] == "http" - and str(scope.get("path", "")) in {"/mcp", "/mcp/"} - ): + if scope["type"] == "http" and str(scope.get("path", "")) in {"/mcp", "/mcp/"}: response = await self.security.validate_request( Request(scope, receive=receive), is_post=str(scope.get("method", "")).upper() == "POST", @@ -308,12 +337,9 @@ def create_mcp_server( if oidc_authentication: assert settings.http_oidc_issuer is not None assert settings.mcp_public_url is not None - if ( - isinstance(context, HttpApplicationContext) - and isinstance( - context.authenticator, - OIDCBearerAuthenticator, - ) + if isinstance(context, HttpApplicationContext) and isinstance( + context.authenticator, + OIDCBearerAuthenticator, ): authenticator = context.authenticator.for_audience( settings.mcp_public_url, @@ -351,8 +377,9 @@ def create_mcp_server( "get_runtime_readiness before indexing. If selected model " "artifacts are missing, submit prepare_models and poll get_job " "until it completes. " - "Register and upload new video through the HTTP/tus API, then use " - "its media_id with start_indexing. get_index_status identifies the " + "Use create_media_upload to give the user a secure upload page, " + "then poll get_media_upload until it returns a media_id. Use that " + "media_id with start_indexing. get_index_status identifies the " "media included in the active index snapshot. For search_moments " "and query_video, provide command.media_id to restrict work to one " "video, or omit it to search/query across every media item in that " @@ -375,8 +402,8 @@ async def artifact_bytes( context, default_principal=default_principal, permission=RepositoryPermission.read, - operation=lambda _actor: ( - context.application.open_artifact_content(artifact_id) + operation=lambda _actor: context.application.open_artifact_content( + artifact_id ), ) if resource.mime_type != expected_mime_type: @@ -395,9 +422,7 @@ async def artifact_bytes( "vidxp://artifacts/{artifact_id}/content.mp4", name="vidxp_artifact_mp4", title="VidXP MP4 artifact", - description=( - "Binary content for a generated VidXP clip or video artifact." - ), + description=("Binary content for a generated VidXP clip or video artifact."), mime_type="video/mp4", ) async def read_mp4_artifact(artifact_id: ArtifactId) -> bytes: @@ -410,9 +435,7 @@ async def read_mp4_artifact(artifact_id: ArtifactId) -> bytes: "vidxp://artifacts/{artifact_id}/content.mkv", name="vidxp_artifact_matroska", title="VidXP Matroska artifact", - description=( - "Binary content for a source-profile VidXP clip artifact." - ), + description=("Binary content for a source-profile VidXP clip artifact."), mime_type="video/x-matroska", ) async def read_matroska_artifact(artifact_id: ArtifactId) -> bytes: @@ -531,6 +554,72 @@ async def get_media(media_id: MediaId) -> MediaAsset: operation=lambda _actor: context.application.get_media(media_id), ) + @server.tool( + description=( + "Create an idempotent, metadata-only tus upload handoff. Return a " + "normal HTTPS page for the user; video bytes never pass through " + "MCP." + ), + annotations=_SUBMIT, + structured_output=True, + ) + async def create_media_upload( + command: CreateUploadIntentCommand, + idempotency_key: IdempotencyKey, + ) -> MediaUploadHandoff: + def create(actor: Principal) -> MediaUploadHandoff: + service = _uploads(context) + handoff = service.create_handoff( + command, + principal=actor, + request_key=scoped_request_key( + principal=actor, + transport="mcp", + operation="media-upload", + idempotency_key=idempotency_key, + ), + ) + assert context.settings.upload_handoff_public_url is not None + page_url = ( + f"{context.settings.upload_handoff_public_url}/" + f"{handoff.status.intent_id}#capability=" + f"{quote(handoff.capability, safe='')}" + ) + return MediaUploadHandoff( + **handoff.status.model_dump(), + upload_page_url=page_url, + handoff_expires_at=handoff.expires_at, + ) + + return await _invoke_async( + context, + default_principal=default_principal, + permission=RepositoryPermission.write, + operation=create, + ) + + @server.tool( + description=( + "Get the current server-owned media upload state and the next " + "valid action. Processing or failed imports include a job_id for " + "get_job; ready uploads include a media_id for start_indexing." + ), + annotations=_READ_ONLY, + structured_output=True, + ) + async def get_media_upload( + intent_id: UploadIntentId, + ) -> MediaUploadStatus: + return await _invoke_async( + context, + default_principal=default_principal, + permission=RepositoryPermission.read, + operation=lambda actor: _uploads(context).get_status( + intent_id, + principal=actor, + ), + ) + @server.tool( description=( "Inspect the active index snapshot, including its indexed media " @@ -718,23 +807,14 @@ async def get_artifact_download( context, default_principal=default_principal, permission=RepositoryPermission.read, - operation=lambda _actor: ( - context.application.get_artifact(artifact_id) - ), - ) - suffix = ( - "mp4" - if artifact.mime_type == "video/mp4" - else "mkv" + operation=lambda _actor: context.application.get_artifact(artifact_id), ) + suffix = "mp4" if artifact.mime_type == "video/mp4" else "mkv" filename = f"{artifact.kind.value}-{artifact.artifact_id}.{suffix}" return ResourceLink( name=filename, title=f"VidXP {artifact.kind.value.replace('_', ' ')}", - uri=( - f"vidxp://artifacts/{artifact.artifact_id}/" - f"content.{suffix}" - ), + uri=(f"vidxp://artifacts/{artifact.artifact_id}/content.{suffix}"), description=( f"Generated from media {artifact.media_id}; " f"{artifact.byte_size:,} bytes." @@ -821,9 +901,7 @@ async def cancel_job(job_id: JobId) -> Job: def create_remote_mcp(context: ControlPlaneContext) -> RemoteMCP: - owns_authentication = ( - context.settings.http_auth_mode == HttpAuthMode.oidc - ) + owns_authentication = context.settings.http_auth_mode == HttpAuthMode.oidc server = create_mcp_server( context, oidc_authentication=owns_authentication, @@ -837,9 +915,7 @@ def create_remote_mcp(context: ControlPlaneContext) -> RemoteMCP: streamable_http_path="/mcp", stateless_http=True, json_response=False, - max_request_body_size=( - context.settings.mcp_max_request_body_bytes - ), + max_request_body_size=(context.settings.mcp_max_request_body_bytes), transport_security=transport_security, host=context.settings.http_bind_host, ) diff --git a/src/vidxp/migrations/versions/20260801_01_upload_handoffs.py b/src/vidxp/migrations/versions/20260801_01_upload_handoffs.py new file mode 100644 index 0000000..b003432 --- /dev/null +++ b/src/vidxp/migrations/versions/20260801_01_upload_handoffs.py @@ -0,0 +1,49 @@ +"""Add bounded browser upload handoff authorization. + +Revision ID: 20260801_01 +Revises: 20260729_01 +Create Date: 2026-08-01 +""" + +from alembic import op +import sqlalchemy as sa + + +revision = "20260801_01" +down_revision = "20260729_01" +branch_labels = None +depends_on = None + + +def upgrade() -> None: + op.create_table( + "upload_handoffs", + sa.Column("selector", sa.String(length=32), nullable=False), + sa.Column("intent_id", sa.String(length=32), nullable=False), + sa.Column( + "repository_binding", + sa.String(length=64), + nullable=False, + ), + sa.Column("byte_size", sa.BigInteger(), nullable=False), + sa.Column("created_at", sa.Text(), nullable=False), + sa.Column("expires_at", sa.Text(), nullable=False), + sa.Column("session_digest", sa.String(length=64), nullable=True), + sa.Column( + "creation_grant_digest", + sa.String(length=64), + nullable=True, + ), + sa.Column("creation_grant_expires_at", sa.Text(), nullable=True), + sa.ForeignKeyConstraint( + ["intent_id"], + ["upload_intents.intent_id"], + ondelete="CASCADE", + ), + sa.PrimaryKeyConstraint("selector"), + sa.UniqueConstraint("intent_id"), + ) + + +def downgrade() -> None: + op.drop_table("upload_handoffs") diff --git a/src/vidxp/settings.py b/src/vidxp/settings.py index 8cc0922..f1f7a58 100644 --- a/src/vidxp/settings.py +++ b/src/vidxp/settings.py @@ -51,13 +51,9 @@ class VidXPSettings(BaseSettings): mode: ApplicationMode = ApplicationMode.local data_dir: Path = Field(default_factory=default_data_directory) - repository_root: Path = Field( - default_factory=default_repository_directory - ) + repository_root: Path = Field(default_factory=default_repository_directory) runtime_backend: str = "auto" - model_cache: Path = Field( - default_factory=default_model_directory - ) + model_cache: Path = Field(default_factory=default_model_directory) allow_model_downloads: bool = True max_loaded_models: int = Field(default=3, gt=0, le=16) max_concurrent_indexing: int = Field(default=1, gt=0, le=16) @@ -166,6 +162,17 @@ class VidXPSettings(BaseSettings): ) upload_quarantine_root: Path | None = None upload_cleanup_token: SecretStr | None = None + upload_handoff_public_url: str | None = Field( + default=None, + min_length=1, + max_length=2048, + ) + upload_handoff_secret: SecretStr | None = None + upload_handoff_ttl_seconds: int = Field( + default=15 * 60, + ge=300, + le=60 * 60, + ) slm_base_url: str | None = Field( default=None, min_length=1, @@ -218,14 +225,22 @@ def _normalize_empty_optional_slm( ) -> str | None: return None if value == "" else value + @field_validator( + "upload_handoff_public_url", + "upload_handoff_secret", + mode="before", + ) + @classmethod + def _normalize_empty_handoff_setting(cls, value): + return None if value == "" else value + @field_validator("runtime_backend") @classmethod def _validate_runtime_backend(cls, value: str) -> str: backend = value.strip().lower() if not re.fullmatch(r"(auto|cpu|mps|cuda(?::[0-9]+)?)", backend): raise ValueError( - "runtime_backend must be auto, cpu, mps, cuda, " - "or cuda:." + "runtime_backend must be auto, cpu, mps, cuda, or cuda:." ) return backend @@ -243,8 +258,7 @@ def _clean_allowlist(cls, values: tuple[str, ...]) -> tuple[str, ...]: ] if invalid: raise ValueError( - "capability_allowlist entries must use " - "DISTRIBUTION:ENTRY_POINT." + "capability_allowlist entries must use DISTRIBUTION:ENTRY_POINT." ) return cleaned @@ -257,9 +271,7 @@ def _clean_allowlist(cls, values: tuple[str, ...]) -> tuple[str, ...]: ) @classmethod def _clean_http_lists(cls, values: tuple[str, ...]) -> tuple[str, ...]: - return tuple( - dict.fromkeys(value.strip() for value in values if value.strip()) - ) + return tuple(dict.fromkeys(value.strip() for value in values if value.strip())) @field_validator("http_trusted_hosts") @classmethod @@ -269,17 +281,10 @@ def _validate_trusted_hosts( ) -> tuple[str, ...]: normalized = tuple(value.lower() for value in values) for value in normalized: - if ( - "*" in value[1:] - or ( - value.startswith("*") - and value != "*" - and not value.startswith("*.") - ) + if "*" in value[1:] or ( + value.startswith("*") and value != "*" and not value.startswith("*.") ): - raise ValueError( - "Trusted-host wildcards must use *.example.com." - ) + raise ValueError("Trusted-host wildcards must use *.example.com.") return normalized @field_validator("mcp_allowed_hosts") @@ -291,13 +296,9 @@ def _validate_mcp_hosts( normalized = tuple(value.lower() for value in values) for value in normalized: if "*" in value and not value.endswith(":*"): - raise ValueError( - "MCP host wildcards are supported only as host:*." - ) + raise ValueError("MCP host wildcards are supported only as host:*.") if "/" in value or "://" in value: - raise ValueError( - "MCP allowed hosts must be Host header values." - ) + raise ValueError("MCP allowed hosts must be Host header values.") return normalized @field_validator("mcp_allowed_origins") @@ -319,9 +320,7 @@ def _validate_mcp_origins( or parsed.query or parsed.fragment ): - raise ValueError( - "MCP allowed origins must be serialized HTTP origins." - ) + raise ValueError("MCP allowed origins must be serialized HTTP origins.") try: parsed.port except ValueError as exc: @@ -329,9 +328,7 @@ def _validate_mcp_origins( "An MCP allowed origin contains an invalid port." ) from exc if "*" in value and not value.endswith(":*"): - raise ValueError( - "MCP origin wildcards are supported only as origin:*." - ) + raise ValueError("MCP origin wildcards are supported only as origin:*.") return values @field_validator("http_oidc_algorithms") @@ -369,23 +366,17 @@ def _validate_oidc_url( if value != value.strip(): raise ValueError(f"{info.field_name} must not contain whitespace.") if "\\" in value or any( - character.isspace() - or ord(character) < 32 - or ord(character) == 127 + character.isspace() or ord(character) < 32 or ord(character) == 127 for character in value ): - raise ValueError( - f"{info.field_name} contains an unsafe URL character." - ) + raise ValueError(f"{info.field_name} contains an unsafe URL character.") parsed = urlsplit(value) if parsed.scheme not in {"http", "https"} or parsed.hostname is None: raise ValueError(f"{info.field_name} must be an HTTP URL.") try: parsed.port except ValueError as exc: - raise ValueError( - f"{info.field_name} contains an invalid port." - ) from exc + raise ValueError(f"{info.field_name} contains an invalid port.") from exc if parsed.username is not None or parsed.password is not None: raise ValueError(f"{info.field_name} must not contain credentials.") if "#" in value: @@ -395,9 +386,7 @@ def _validate_oidc_url( "127.0.0.1", "::1", }: - raise ValueError( - f"{info.field_name} must use HTTPS outside loopback." - ) + raise ValueError(f"{info.field_name} must use HTTPS outside loopback.") if info.field_name == "http_oidc_issuer" and "?" in value: raise ValueError("http_oidc_issuer must not contain a query.") return value @@ -430,32 +419,24 @@ def _validate_service_url( try: parsed.port except ValueError as exc: - raise ValueError( - f"{info.field_name} contains an invalid port." - ) from exc + raise ValueError(f"{info.field_name} contains an invalid port.") from exc if ( info.field_name == "upload_public_endpoint" and parsed.scheme != "https" - and parsed.hostname.lower() not in { + and parsed.hostname.lower() + not in { "localhost", "127.0.0.1", "::1", } ): - raise ValueError( - "upload_public_endpoint must use HTTPS outside loopback." - ) + raise ValueError("upload_public_endpoint must use HTTPS outside loopback.") if info.field_name == "slm_base_url": if parsed.path.rstrip("/") != "/v1": raise ValueError("slm_base_url must end with /v1.") if parsed.hostname.lower() in {"ollama.com", "www.ollama.com"}: - raise ValueError( - "slm_base_url must use a self-hosted Ollama service." - ) - if ( - info.field_name != "slm_base_url" - and not value.endswith("/") - ): + raise ValueError("slm_base_url must use a self-hosted Ollama service.") + if info.field_name != "slm_base_url" and not value.endswith("/"): raise ValueError(f"{info.field_name} must end with a slash.") return value @@ -476,17 +457,45 @@ def _validate_mcp_public_url(cls, value: str | None) -> str | None: or parsed.fragment or parsed.path.rstrip("/") != "/mcp" ): - raise ValueError( - "mcp_public_url must be a plain HTTP URL ending in /mcp." - ) + raise ValueError("mcp_public_url must be a plain HTTP URL ending in /mcp.") + if parsed.scheme != "https" and parsed.hostname.lower() not in { + "localhost", + "127.0.0.1", + "::1", + }: + raise ValueError("mcp_public_url must use HTTPS outside loopback.") + return value.rstrip("/") + + @field_validator("upload_handoff_public_url") + @classmethod + def _validate_upload_handoff_public_url( + cls, + value: str | None, + ) -> str | None: + if value is None: + return None + if value != value.strip() or "\\" in value: + raise ValueError("upload_handoff_public_url contains unsafe characters.") + parsed = urlsplit(value) if ( parsed.scheme != "https" - and parsed.hostname.lower() - not in {"localhost", "127.0.0.1", "::1"} + or parsed.hostname is None + or parsed.username is not None + or parsed.password is not None + or parsed.query + or parsed.fragment + or parsed.path.rstrip("/") != "/upload-handoff" ): raise ValueError( - "mcp_public_url must use HTTPS outside loopback." + "upload_handoff_public_url must be an HTTPS URL ending in " + "/upload-handoff." ) + try: + parsed.port + except ValueError as exc: + raise ValueError( + "upload_handoff_public_url contains an invalid port." + ) from exc return value.rstrip("/") @field_validator("trusted_local_import_roots") @@ -495,9 +504,7 @@ def _clean_import_roots( cls, values: tuple[Path, ...], ) -> tuple[Path, ...]: - return tuple( - dict.fromkeys(path.expanduser() for path in values) - ) + return tuple(dict.fromkeys(path.expanduser() for path in values)) @model_validator(mode="after") def _require_explicit_server_backend(self) -> "VidXPSettings": @@ -507,17 +514,17 @@ def _require_explicit_server_backend(self) -> "VidXPSettings": "Connect agents to the remote MCP endpoint or use the HTTP " "API directly." ) - if ( - self.mode == ApplicationMode.server - and not re.fullmatch(r"(cpu|cuda(?::[0-9]+)?)", self.runtime_backend) + if self.mode == ApplicationMode.server and not re.fullmatch( + r"(cpu|cuda(?::[0-9]+)?)", self.runtime_backend ): raise ValueError( "Server mode requires an explicit cpu or cuda runtime backend." ) if self.http_auth_mode == HttpAuthMode.static: - if self.http_static_bearer_token is None or len( - self.http_static_bearer_token.get_secret_value() - ) < 32: + if ( + self.http_static_bearer_token is None + or len(self.http_static_bearer_token.get_secret_value()) < 32 + ): raise ValueError( "Static HTTP authentication requires a bearer token of " "at least 32 characters." @@ -572,10 +579,26 @@ def _require_explicit_server_backend(self) -> "VidXPSettings": "Remote uploads require an internal tusd endpoint and " "a cleanup token of at least 32 characters." ) - if (self.slm_base_url is None) != (self.slm_model is None): + handoff_configured = any( + value is not None + for value in ( + self.upload_handoff_public_url, + self.upload_handoff_secret, + ) + ) + if handoff_configured and ( + self.upload_public_endpoint is None + or self.upload_handoff_public_url is None + or self.upload_handoff_secret is None + or len(self.upload_handoff_secret.get_secret_value()) < 32 + ): raise ValueError( - "slm_base_url and slm_model must be configured together." + "Upload handoffs require remote uploads, an HTTPS public " + "handoff URL, and a dedicated secret of at least 32 " + "characters." ) + if (self.slm_base_url is None) != (self.slm_model is None): + raise ValueError("slm_base_url and slm_model must be configured together.") if self.slm_model is not None and self.slm_model.endswith("-cloud"): raise ValueError("slm_model must be a self-hosted Ollama model.") return self @@ -586,13 +609,13 @@ def validate_http_server(self) -> None: and self.http_auth_mode == HttpAuthMode.none ): raise ValueError( - "Server-mode HTTP requires static bearer or OIDC " - "authentication." + "Server-mode HTTP requires static bearer or OIDC authentication." ) - if ( - self.http_auth_mode == HttpAuthMode.none - and self.http_bind_host not in {"127.0.0.1", "::1", "localhost"} - ): + if self.http_auth_mode == HttpAuthMode.none and self.http_bind_host not in { + "127.0.0.1", + "::1", + "localhost", + }: raise ValueError( "Unauthenticated HTTP may bind only to a loopback address." ) @@ -600,13 +623,8 @@ def validate_http_server(self) -> None: raise ValueError("At least one trusted HTTP host is required.") if not self.mcp_allowed_hosts: raise ValueError("At least one allowed MCP host is required.") - if ( - self.http_auth_mode == HttpAuthMode.oidc - and self.mcp_public_url is None - ): - raise ValueError( - "OIDC MCP authentication requires mcp_public_url." - ) + if self.http_auth_mode == HttpAuthMode.oidc and self.mcp_public_url is None: + raise ValueError("OIDC MCP authentication requires mcp_public_url.") @property def layout(self) -> RepositoryLayout: @@ -667,6 +685,8 @@ def application_settings(self) -> VidXPSettings: defaults["upload_public_endpoint"] = None defaults["upload_internal_endpoint"] = None defaults["upload_cleanup_token"] = None + defaults["upload_handoff_public_url"] = None + defaults["upload_handoff_secret"] = None defaults["http_auth_mode"] = HttpAuthMode.none defaults["http_static_bearer_token"] = None defaults["http_oidc_issuer"] = None diff --git a/src/vidxp/tusd_hooks.py b/src/vidxp/tusd_hooks.py index d891b4a..80447dd 100644 --- a/src/vidxp/tusd_hooks.py +++ b/src/vidxp/tusd_hooks.py @@ -45,14 +45,14 @@ def _status(error: ApplicationError) -> int: }.get(error.detail.category, 500) -def _bearer(request) -> str | None: +def _authorization(request) -> tuple[str, str] | None: values = request.header("authorization") if len(values) != 1: return None scheme, separator, token = values[0].partition(" ") - if separator != " " or scheme.lower() != "bearer" or not token: + if separator != " " or not scheme or not token: return None - return token + return scheme.lower(), token class TusdHookService: @@ -105,15 +105,28 @@ def _pre_create(self, hook: TusdHookRequest) -> TusdHookResponse: ErrorCategory.validation, "Deferred-length and concatenated uploads are not supported.", ) - principal = self.authorization.require( - self.authenticator.authenticate(_bearer(hook.event.request)), - RepositoryPermission.write, - ) - record = self.uploads.accept_creation( - upload.metadata["intent_id"], - principal=principal, - byte_size=upload.size, - ) + authorization = _authorization(hook.event.request) + if authorization is not None and authorization[0] == "vidxp-handoff": + record = self.uploads.accept_handoff_creation( + upload.metadata["intent_id"], + grant=authorization[1], + byte_size=upload.size, + ) + else: + bearer = ( + authorization[1] + if authorization is not None and authorization[0] == "bearer" + else None + ) + principal = self.authorization.require( + self.authenticator.authenticate(bearer), + RepositoryPermission.write, + ) + record = self.uploads.accept_creation( + upload.metadata["intent_id"], + principal=principal, + byte_size=upload.size, + ) assert record.upload_id is not None return TusdHookResponse( change_file_info=TusdChangeFileInfo(upload_id=record.upload_id) diff --git a/src/vidxp/upload_page.py b/src/vidxp/upload_page.py new file mode 100644 index 0000000..839031d --- /dev/null +++ b/src/vidxp/upload_page.py @@ -0,0 +1,184 @@ +from __future__ import annotations + +from importlib.resources import files +from typing import Annotated +from urllib.parse import urlsplit + +from fastapi import APIRouter, Cookie, Depends, Request, Response + +from vidxp.api_models import ( + UploadCreationGrantResponse, + UploadHandoffBootstrapRequest, + UploadPageSessionResponse, +) +from vidxp.api_routes.dependencies import context +from vidxp.application_models import ( + ApplicationError, + ErrorCategory, + UploadIntentId, +) +from vidxp.composition import HttpApplicationContext +from vidxp.upload_service import RemoteUploadService, UploadBrowserSession + + +router = APIRouter(prefix="/upload-handoff", tags=["upload-handoff"]) +_SESSION_COOKIE = "__Secure-vidxp-upload" +_ASSETS = { + "upload-page.js": "text/javascript; charset=utf-8", + "upload-page.css": "text/css; charset=utf-8", + "THIRD_PARTY_NOTICES.txt": "text/plain; charset=utf-8", +} + + +def _uploads(service: HttpApplicationContext) -> RemoteUploadService: + if service.uploads is None: + raise ApplicationError( + "remote_upload_unavailable", + ErrorCategory.unavailable, + "Remote resumable uploads are not configured.", + ) + return service.uploads + + +def _asset(name: str) -> bytes: + if name not in _ASSETS: + raise ApplicationError( + "resource_not_found", + ErrorCategory.not_found, + "The requested upload-page asset was not found.", + ) + return files("vidxp").joinpath("assets", "upload_page", name).read_bytes() + + +def _require_same_origin( + request: Request, + service: HttpApplicationContext, +) -> None: + public_url = service.settings.upload_handoff_public_url + if public_url is None: + raise ApplicationError( + "remote_upload_handoff_unavailable", + ErrorCategory.unavailable, + "Browser upload handoffs are not configured.", + ) + parsed = urlsplit(public_url) + expected = f"{parsed.scheme}://{parsed.netloc}" + if ( + request.headers.get("origin") != expected + or request.headers.get( + "sec-fetch-site", + "same-origin", + ) + != "same-origin" + ): + raise ApplicationError( + "upload_handoff_origin_forbidden", + ErrorCategory.authorization, + "The upload handoff request must come from its VidXP page.", + ) + + +def _session_response( + session: UploadBrowserSession, +) -> UploadPageSessionResponse: + return UploadPageSessionResponse( + status=session.status, + creation_url=session.creation_url, + resume_url=session.resume_url, + ) + + +@router.get("/assets/{asset_name}", include_in_schema=False) +def upload_page_asset(asset_name: str) -> Response: + return Response(content=_asset(asset_name), media_type=_ASSETS[asset_name]) + + +@router.get("/{intent_id}", include_in_schema=False) +def upload_page(intent_id: UploadIntentId) -> Response: + del intent_id + content = ( + files("vidxp").joinpath("assets", "upload_page", "index.html").read_bytes() + ) + return Response(content=content, media_type="text/html; charset=utf-8") + + +@router.post( + "/{intent_id}/bootstrap", + response_model=UploadPageSessionResponse, + include_in_schema=False, +) +def bootstrap_upload_page( + intent_id: UploadIntentId, + command: UploadHandoffBootstrapRequest, + request: Request, + response: Response, + service: Annotated[HttpApplicationContext, Depends(context)], + current_session: Annotated[ + str | None, + Cookie(alias=_SESSION_COOKIE), + ] = None, +) -> UploadPageSessionResponse: + _require_same_origin(request, service) + session = _uploads(service).exchange_handoff( + intent_id, + capability=command.capability, + current_session=current_session, + ) + max_age = service.settings.upload_intent_ttl_seconds + response.set_cookie( + _SESSION_COOKIE, + session.session_token, + max_age=max_age, + expires=session.session_expires_at, + path=f"/upload-handoff/{intent_id}", + secure=True, + httponly=True, + samesite="strict", + ) + return _session_response(session) + + +@router.get( + "/{intent_id}/status", + response_model=UploadPageSessionResponse, + include_in_schema=False, +) +def upload_page_status( + intent_id: UploadIntentId, + service: Annotated[HttpApplicationContext, Depends(context)], + session_token: Annotated[ + str | None, + Cookie(alias=_SESSION_COOKIE), + ] = None, +) -> UploadPageSessionResponse: + return _session_response( + _uploads(service).browser_session( + intent_id, + session_token=session_token, + ) + ) + + +@router.post( + "/{intent_id}/creation-grant", + response_model=UploadCreationGrantResponse, + include_in_schema=False, +) +def create_upload_grant( + intent_id: UploadIntentId, + request: Request, + service: Annotated[HttpApplicationContext, Depends(context)], + session_token: Annotated[ + str | None, + Cookie(alias=_SESSION_COOKIE), + ] = None, +) -> UploadCreationGrantResponse: + _require_same_origin(request, service) + grant = _uploads(service).issue_creation_grant( + intent_id, + session_token=session_token, + ) + return UploadCreationGrantResponse( + grant=grant.token, + expires_at=grant.expires_at, + ) diff --git a/src/vidxp/upload_service.py b/src/vidxp/upload_service.py index 2a0bb90..a89454c 100644 --- a/src/vidxp/upload_service.py +++ b/src/vidxp/upload_service.py @@ -1,9 +1,13 @@ from __future__ import annotations +import base64 import hashlib +import hmac import json import logging -from datetime import timedelta +import secrets +from dataclasses import dataclass +from datetime import datetime, timedelta from hmac import compare_digest from pathlib import Path from typing import Any @@ -19,11 +23,16 @@ CreateUploadIntentCommand, ErrorCategory, JobState, + MediaUploadStatus, Principal, UploadIntent, ) from vidxp.core.media import QuarantinedMedia, utc_now -from vidxp.core.uploads import UploadIntentRecord, UploadState +from vidxp.core.uploads import ( + UploadHandoffRecord, + UploadIntentRecord, + UploadState, +) from vidxp.infrastructure.sql_catalog import ( SQLCatalog, UploadQuotaExceededError, @@ -32,6 +41,37 @@ from vidxp.settings import VidXPSettings LOGGER = logging.getLogger(__name__) +_CREATION_GRANT_TTL_SECONDS = 5 * 60 + + +@dataclass(frozen=True) +class UploadHandoff: + status: MediaUploadStatus + capability: str + expires_at: datetime + + +@dataclass(frozen=True) +class UploadBrowserSession: + status: MediaUploadStatus + session_token: str + session_expires_at: datetime + creation_url: str + resume_url: str | None + + +@dataclass(frozen=True) +class UploadCreationGrant: + token: str + expires_at: datetime + + +def _token_digest(token: str) -> str: + return hashlib.sha256(token.encode("ascii")).hexdigest() + + +def _base64url(value: bytes) -> str: + return base64.urlsafe_b64encode(value).rstrip(b"=").decode("ascii") def _public_intent(record: UploadIntentRecord) -> UploadIntent: @@ -91,8 +131,7 @@ def create_intent( declared_mime_type=command.declared_mime_type, state=UploadState.pending, created_at=now, - expires_at=now - + timedelta(seconds=self.settings.upload_intent_ttl_seconds), + expires_at=now + timedelta(seconds=self.settings.upload_intent_ttl_seconds), ) try: return _public_intent( @@ -105,7 +144,7 @@ def create_intent( raise ApplicationError( "upload_quota_exceeded", ErrorCategory.resource_limit, - "The principal upload quota would be exceeded.", + "The deployment upload quota would be exceeded.", ) from exc except IntegrityError: replay = self.catalog.get_upload_intent_by_request(request_key) @@ -114,6 +153,246 @@ def create_intent( return _public_intent(replay) raise + def create_handoff( + self, + command: CreateUploadIntentCommand, + *, + principal: Principal, + request_key: str, + ) -> UploadHandoff: + self._require_handoff_configured() + intent = self.create_intent( + command, + principal=principal, + request_key=request_key, + ) + record = self.catalog.get_upload_handoff_by_intent(intent.intent_id) + if record is None: + now = utc_now() + record = UploadHandoffRecord( + selector=secrets.token_hex(16), + intent_id=intent.intent_id, + repository_binding=self._repository_binding(), + byte_size=intent.byte_size, + created_at=now, + expires_at=min( + intent.expires_at, + now + timedelta(seconds=self.settings.upload_handoff_ttl_seconds), + ), + ) + try: + self.catalog.create_upload_handoff(record) + except IntegrityError: + replay = self.catalog.get_upload_handoff_by_intent(intent.intent_id) + if replay is None: + raise + record = replay + self._require_handoff_binding(record, intent) + return UploadHandoff( + status=self.status(intent), + capability=self._handoff_capability(record), + expires_at=record.expires_at, + ) + + def status(self, intent: UploadIntent) -> MediaUploadStatus: + status, next_action = { + UploadState.pending: ( + "Waiting for the expected video to be selected.", + "Open the upload page and upload the declared file, then " + "call get_media_upload again.", + ), + UploadState.accepted: ( + "The tus upload is accepted or still transferring.", + "Resume or finish the upload in the browser, then call " + "get_media_upload again.", + ), + UploadState.processing: ( + "The uploaded video is being validated and imported.", + "Call get_job with job_id to inspect import progress, and " + "continue polling get_media_upload.", + ), + UploadState.ready: ( + "The video is registered and ready for indexing.", + "Call start_indexing with media_id.", + ), + UploadState.failed: ( + "Validation or durable import failed.", + "Call get_job with job_id to inspect the failure, then create " + "a new media upload with a new idempotency key.", + ), + UploadState.expired: ( + "The upload intent expired before it became ready.", + "Call create_media_upload with a new idempotency key.", + ), + }[intent.state] + return MediaUploadStatus( + intent_id=intent.intent_id, + state=intent.state, + original_filename=intent.original_filename, + byte_size=intent.byte_size, + declared_mime_type=intent.declared_mime_type, + maximum_bytes=self.settings.upload_max_bytes, + expires_at=intent.expires_at, + job_id=intent.job_id, + media_id=intent.media_id, + status=status, + next_action=next_action, + ) + + def get_status( + self, + intent_id: str, + *, + principal: Principal, + ) -> MediaUploadStatus: + return self.status(self.get_intent(intent_id, principal=principal)) + + def exchange_handoff( + self, + intent_id: str, + *, + capability: str, + current_session: str | None = None, + ) -> UploadBrowserSession: + now = utc_now() + + def exchange(connection: Connection) -> tuple[str, UploadIntentRecord]: + record = self._handoff_from_capability( + capability, + connection=connection, + for_update=True, + ) + intent = self.catalog.get_upload_intent( + intent_id, + connection=connection, + for_update=True, + ) + self._verify_handoff( + record, + capability=capability, + intent=intent, + expected_intent_id=intent_id, + now=now, + ) + assert intent is not None + if intent.state != UploadState.pending: + raise ApplicationError( + "upload_handoff_replayed", + ErrorCategory.conflict, + "This handoff already started an upload. Reopen the " + "existing browser session or inspect its status through " + "get_media_upload.", + ) + if ( + current_session is not None + and record.session_digest is not None + and compare_digest( + _token_digest(current_session), + record.session_digest, + ) + ): + return current_session, intent + session = secrets.token_urlsafe(32) + self.catalog.set_upload_handoff_session( + record.selector, + session_digest=_token_digest(session), + connection=connection, + ) + return session, intent + + session, record = self.catalog.with_upload_transaction(exchange) + intent = _public_intent(record) + assert self.settings.upload_public_endpoint is not None + return UploadBrowserSession( + status=self.status(intent), + session_token=session, + session_expires_at=intent.expires_at, + creation_url=self.settings.upload_public_endpoint, + resume_url=None, + ) + + def browser_session( + self, + intent_id: str, + *, + session_token: str | None, + ) -> UploadBrowserSession: + record, intent = self._require_browser_session( + intent_id, + session_token=session_token, + ) + del record + public = _public_intent(intent) + assert self.settings.upload_public_endpoint is not None + resume_url = ( + self.settings.upload_public_endpoint + (intent.upload_id or "") + if self._tusd_creation_exists(intent) + else None + ) + return UploadBrowserSession( + status=self.status(public), + session_token=session_token or "", + session_expires_at=intent.expires_at, + creation_url=self.settings.upload_public_endpoint, + resume_url=resume_url, + ) + + def issue_creation_grant( + self, + intent_id: str, + *, + session_token: str | None, + ) -> UploadCreationGrant: + now = utc_now() + + def issue(connection: Connection) -> UploadCreationGrant: + record = self.catalog.get_upload_handoff_by_intent( + intent_id, + connection=connection, + for_update=True, + ) + intent = self.catalog.get_upload_intent( + intent_id, + connection=connection, + for_update=True, + ) + self._validate_browser_session( + record, + intent, + session_token=session_token, + now=now, + ) + assert record is not None and intent is not None + if intent.state != UploadState.pending and self._tusd_creation_exists( + intent + ): + raise ApplicationError( + "upload_handoff_replayed", + ErrorCategory.conflict, + "The tus upload was already created. Resume its existing " + "URL from the page status response.", + ) + expires_at = min( + intent.expires_at, + now + timedelta(seconds=_CREATION_GRANT_TTL_SECONDS), + ) + nonce = secrets.token_urlsafe(24) + token = self._creation_grant_token( + record, + session_digest=record.session_digest or "", + expires_at=expires_at, + nonce=nonce, + ) + self.catalog.set_upload_creation_grant( + record.selector, + digest=_token_digest(token), + expires_at=expires_at, + connection=connection, + ) + return UploadCreationGrant(token=token, expires_at=expires_at) + + return self.catalog.with_upload_transaction(issue) + def get_intent( self, intent_id: str, @@ -160,60 +439,91 @@ def accept_creation( now = utc_now() def accept(connection: Connection) -> UploadIntentRecord | None: - record = self.catalog.get_upload_intent( + return self._accept_creation_in_transaction( intent_id, + byte_size=byte_size, + now=now, + connection=connection, + ) + + accepted = self.catalog.with_upload_transaction(accept) + if accepted is None: + raise ApplicationError( + "upload_intent_expired", + ErrorCategory.validation, + "The upload intent has expired.", + ) + return accepted + + def accept_handoff_creation( + self, + intent_id: str, + *, + grant: str, + byte_size: int, + ) -> UploadIntentRecord: + now = utc_now() + + def accept(connection: Connection) -> UploadIntentRecord | None: + selector = self._creation_grant_selector(grant) + handoff = self.catalog.get_upload_handoff( + selector, connection=connection, for_update=True, ) - if record is None: - raise ApplicationError( - "upload_intent_invalid", - ErrorCategory.validation, - "The upload intent is invalid.", - ) - if record.expires_at <= now: - self.catalog.update_upload( - record.intent_id, - state=UploadState.expired, - connection=connection, + intent = self.catalog.get_upload_intent( + intent_id, + connection=connection, + for_update=True, + ) + try: + self._verify_creation_grant( + handoff, + intent, + grant=grant, + expected_intent_id=intent_id, + byte_size=byte_size, + now=now, ) - return None - if byte_size != record.byte_size: - raise ApplicationError( - "upload_size_mismatch", - ErrorCategory.validation, - "The upload size does not match its intent.", + except ApplicationError as exc: + if ( + intent is None + or intent.state == UploadState.pending + or exc.detail.code != "upload_creation_grant_invalid" + ): + raise + self._raise_creation_grant_replay( + handoff, + intent, + grant=grant, + expected_intent_id=intent_id, + byte_size=byte_size, + now=now, ) - if record.state == UploadState.accepted: - assert record.upload_id is not None - if self._quarantine_path( - f"{record.upload_id}.info" - ).exists(): - raise ApplicationError( - "upload_already_created", - ErrorCategory.conflict, - "The upload was already created; resume its existing URL.", - ) - return record - if record.state != UploadState.pending: - raise ApplicationError( - "upload_intent_consumed", - ErrorCategory.conflict, - "The upload intent has already been consumed.", + assert intent is not None + if intent.state != UploadState.pending and self._tusd_creation_exists( + intent + ): + self._raise_creation_grant_replay( + handoff, + intent, + grant=grant, + expected_intent_id=intent_id, + byte_size=byte_size, + now=now, ) - upload_id = uuid4().hex - self.catalog.update_upload( - record.intent_id, - state=UploadState.accepted, + assert handoff is not None + accepted = self._accept_creation_in_transaction( + intent_id, + byte_size=byte_size, + now=now, connection=connection, - upload_id=upload_id, ) - return record.model_copy( - update={ - "state": UploadState.accepted, - "upload_id": upload_id, - } + self.catalog.consume_upload_creation_grant( + handoff.selector, + connection=connection, ) + return accepted accepted = self.catalog.with_upload_transaction(accept) if accepted is None: @@ -224,6 +534,13 @@ def accept(connection: Connection) -> UploadIntentRecord | None: ) return accepted + def _tusd_creation_exists(self, intent: UploadIntentRecord) -> bool: + return ( + intent.state != UploadState.pending + and intent.upload_id is not None + and self._quarantine_path(f"{intent.upload_id}.info").exists() + ) + def complete_upload( self, *, @@ -241,10 +558,7 @@ def complete(connection: Connection) -> str: connection=connection, for_update=True, ) - if ( - record is None - or record.upload_id != upload_id - ): + if record is None or record.upload_id != upload_id: raise ApplicationError( "upload_completion_invalid", ErrorCategory.validation, @@ -385,15 +699,13 @@ def reconcile(self) -> dict[str, int]: JobState.recovery_exhausted, }: changed = self.catalog.with_upload_transaction( - lambda connection, item=record: ( - self.catalog.update_upload( - item.intent_id, - state=UploadState.failed, - connection=connection, - expected_states={ - UploadState.processing, - }, - ) + lambda connection, item=record: self.catalog.update_upload( + item.intent_id, + state=UploadState.failed, + connection=connection, + expected_states={ + UploadState.processing, + }, ) ) failed += int(changed) @@ -582,8 +894,7 @@ def _quarantine_path(self, name: str) -> Path: not name or len(name) > 260 or any( - character - not in "abcdefghijklmnopqrstuvwxyz" + character not in "abcdefghijklmnopqrstuvwxyz" "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789._~-" for character in name ) @@ -634,6 +945,382 @@ def _cleanup_after_import(self, upload_id: str) -> None: exc_info=True, ) + def _accept_creation_in_transaction( + self, + intent_id: str, + *, + byte_size: int, + now: datetime, + connection: Connection, + ) -> UploadIntentRecord | None: + record = self.catalog.get_upload_intent( + intent_id, + connection=connection, + for_update=True, + ) + if record is None: + raise ApplicationError( + "upload_intent_invalid", + ErrorCategory.validation, + "The upload intent is invalid.", + ) + if record.expires_at <= now: + self.catalog.update_upload( + record.intent_id, + state=UploadState.expired, + connection=connection, + ) + return None + if byte_size != record.byte_size: + raise ApplicationError( + "upload_size_mismatch", + ErrorCategory.validation, + "The upload size does not match its intent.", + ) + if record.state == UploadState.accepted: + assert record.upload_id is not None + if self._quarantine_path(f"{record.upload_id}.info").exists(): + raise ApplicationError( + "upload_already_created", + ErrorCategory.conflict, + "The upload was already created; resume its existing URL.", + ) + return record + if record.state != UploadState.pending: + raise ApplicationError( + "upload_intent_consumed", + ErrorCategory.conflict, + "The upload intent has already been consumed.", + ) + upload_id = uuid4().hex + self.catalog.update_upload( + record.intent_id, + state=UploadState.accepted, + connection=connection, + upload_id=upload_id, + ) + return record.model_copy( + update={ + "state": UploadState.accepted, + "upload_id": upload_id, + } + ) + + def _secret(self) -> bytes: + secret = self.settings.upload_handoff_secret + if secret is None: + raise ApplicationError( + "remote_upload_handoff_unavailable", + ErrorCategory.unavailable, + "Browser upload handoffs are not configured.", + ) + return secret.get_secret_value().encode("utf-8") + + def _repository_binding(self) -> str: + root = str(self.settings.repository_root.resolve()).replace("\\", "/") + return hashlib.sha256( + f"vidxp-upload-repository-v1\0{root}".encode("utf-8") + ).hexdigest() + + def _handoff_capability(self, record: UploadHandoffRecord) -> str: + material = "\0".join( + ( + "vidxp-upload-handoff-v1", + record.selector, + record.intent_id, + record.repository_binding, + str(record.byte_size), + record.expires_at.isoformat(), + ) + ).encode("utf-8") + signature = _base64url( + hmac.new(self._secret(), material, hashlib.sha256).digest() + ) + return f"v1.{record.selector}.{signature}" + + def _handoff_from_capability( + self, + capability: str, + *, + connection: Connection, + for_update: bool, + ) -> UploadHandoffRecord | None: + parts = capability.split(".") + if ( + len(parts) != 3 + or parts[0] != "v1" + or len(parts[1]) != 32 + or any(character not in "0123456789abcdef" for character in parts[1]) + ): + raise self._invalid_handoff() + return self.catalog.get_upload_handoff( + parts[1], + connection=connection, + for_update=for_update, + ) + + def _verify_handoff( + self, + record: UploadHandoffRecord | None, + *, + capability: str, + intent: UploadIntentRecord | None, + expected_intent_id: str, + now: datetime, + ) -> None: + if ( + record is None + or intent is None + or record.intent_id != expected_intent_id + or intent.intent_id != expected_intent_id + or record.repository_binding != self._repository_binding() + or record.byte_size != intent.byte_size + or not compare_digest( + self._handoff_capability(record), + capability, + ) + ): + raise self._invalid_handoff() + if record.expires_at <= now or intent.expires_at <= now: + raise ApplicationError( + "upload_handoff_expired", + ErrorCategory.authentication, + "The upload handoff expired. Create a new media upload.", + ) + + def _require_handoff_binding( + self, + record: UploadHandoffRecord, + intent: UploadIntent, + ) -> None: + if ( + record.intent_id != intent.intent_id + or record.repository_binding != self._repository_binding() + or record.byte_size != intent.byte_size + ): + raise RuntimeError("The stored upload handoff binding is invalid.") + + def _require_browser_session( + self, + intent_id: str, + *, + session_token: str | None, + ) -> tuple[UploadHandoffRecord, UploadIntentRecord]: + record = self.catalog.get_upload_handoff_by_intent(intent_id) + intent = self.catalog.get_upload_intent(intent_id) + self._validate_browser_session( + record, + intent, + session_token=session_token, + now=utc_now(), + ) + assert record is not None and intent is not None + return record, intent + + def _validate_browser_session( + self, + record: UploadHandoffRecord | None, + intent: UploadIntentRecord | None, + *, + session_token: str | None, + now: datetime, + ) -> None: + if ( + record is None + or intent is None + or session_token is None + or record.session_digest is None + or record.intent_id != intent.intent_id + or record.repository_binding != self._repository_binding() + or record.byte_size != intent.byte_size + or not compare_digest( + record.session_digest, + _token_digest(session_token), + ) + ): + raise ApplicationError( + "upload_handoff_session_invalid", + ErrorCategory.authentication, + "The browser upload session is missing or invalid. Reopen " + "the original handoff URL.", + ) + if intent.expires_at <= now or intent.state == UploadState.expired: + raise ApplicationError( + "upload_handoff_expired", + ErrorCategory.authentication, + "The browser upload session expired. Create a new media upload.", + ) + + def _creation_grant_token( + self, + record: UploadHandoffRecord, + *, + session_digest: str, + expires_at: datetime, + nonce: str, + ) -> str: + expires = str(int(expires_at.timestamp())) + material = "\0".join( + ( + "vidxp-upload-creation-v1", + record.selector, + record.intent_id, + record.repository_binding, + str(record.byte_size), + expires, + session_digest, + nonce, + ) + ).encode("utf-8") + signature = _base64url( + hmac.new(self._secret(), material, hashlib.sha256).digest() + ) + return f"v1.{record.selector}.{expires}.{nonce}.{signature}" + + def _creation_grant_selector(self, grant: str) -> str: + parts = grant.split(".") + if ( + len(parts) != 5 + or parts[0] != "v1" + or len(parts[1]) != 32 + or any(character not in "0123456789abcdef" for character in parts[1]) + or not parts[2].isdigit() + or not parts[3] + or not parts[4] + ): + raise self._invalid_creation_grant() + return parts[1] + + def _verify_creation_grant( + self, + record: UploadHandoffRecord | None, + intent: UploadIntentRecord | None, + *, + grant: str, + expected_intent_id: str, + byte_size: int, + now: datetime, + ) -> None: + parts = grant.split(".") + if len(parts) != 5: + raise self._invalid_creation_grant() + try: + expires = int(parts[2]) + except ValueError as exc: + raise self._invalid_creation_grant() from exc + if ( + not self._creation_grant_matches( + record, + intent, + grant=grant, + expected_intent_id=expected_intent_id, + byte_size=byte_size, + ) + or record is None + or intent is None + or record.creation_grant_digest is None + or record.creation_grant_expires_at is None + or expires != int(record.creation_grant_expires_at.timestamp()) + or not compare_digest( + record.creation_grant_digest, + _token_digest(grant), + ) + ): + raise self._invalid_creation_grant() + if record.creation_grant_expires_at <= now or intent.expires_at <= now: + raise ApplicationError( + "upload_creation_grant_expired", + ErrorCategory.authentication, + "The upload creation grant expired. Request a new grant from " + "the browser page.", + ) + + def _creation_grant_matches( + self, + record: UploadHandoffRecord | None, + intent: UploadIntentRecord | None, + *, + grant: str, + expected_intent_id: str, + byte_size: int, + ) -> bool: + parts = grant.split(".") + if ( + len(parts) != 5 + or not parts[2].isdigit() + or record is None + or intent is None + or record.session_digest is None + or record.intent_id != expected_intent_id + or intent.intent_id != expected_intent_id + or record.repository_binding != self._repository_binding() + or record.byte_size != byte_size + or intent.byte_size != byte_size + ): + return False + expires_at = datetime.fromtimestamp( + int(parts[2]), + tz=record.expires_at.tzinfo, + ) + return compare_digest( + self._creation_grant_token( + record, + session_digest=record.session_digest, + expires_at=expires_at, + nonce=parts[3], + ), + grant, + ) + + def _raise_creation_grant_replay( + self, + record: UploadHandoffRecord | None, + intent: UploadIntentRecord, + *, + grant: str, + expected_intent_id: str, + byte_size: int, + now: datetime, + ) -> None: + if not self._creation_grant_matches( + record, + intent, + grant=grant, + expected_intent_id=expected_intent_id, + byte_size=byte_size, + ): + raise self._invalid_creation_grant() + if int(grant.split(".")[2]) <= int(now.timestamp()): + raise ApplicationError( + "upload_creation_grant_expired", + ErrorCategory.authentication, + "The upload creation grant expired. Request a new grant from " + "the browser page.", + ) + raise ApplicationError( + "upload_handoff_replayed", + ErrorCategory.conflict, + "The upload creation grant was already used. Resume the existing " + "upload from its browser page.", + ) + + @staticmethod + def _invalid_handoff() -> ApplicationError: + return ApplicationError( + "upload_handoff_invalid", + ErrorCategory.authentication, + "The upload handoff is invalid for this intent or repository.", + ) + + @staticmethod + def _invalid_creation_grant() -> ApplicationError: + return ApplicationError( + "upload_creation_grant_invalid", + ErrorCategory.authentication, + "The upload creation grant is invalid for this intent, size, or " + "repository.", + ) + def _require_configured(self) -> None: if self.settings.upload_public_endpoint is None: raise ApplicationError( @@ -642,6 +1329,18 @@ def _require_configured(self) -> None: "Remote resumable uploads are not configured.", ) + def _require_handoff_configured(self) -> None: + self._require_configured() + if ( + self.settings.upload_handoff_public_url is None + or self.settings.upload_handoff_secret is None + ): + raise ApplicationError( + "remote_upload_handoff_unavailable", + ErrorCategory.unavailable, + "Browser upload handoffs are not configured.", + ) + @staticmethod def _require_same_request( record: UploadIntentRecord, diff --git a/tests/test_database_cli.py b/tests/test_database_cli.py index 48c7284..dd45387 100644 --- a/tests/test_database_cli.py +++ b/tests/test_database_cli.py @@ -21,7 +21,7 @@ def test_database_url_command_line_override_is_not_supported(self): self.assertEqual(raised.exception.code, 2) - def test_single_repository_baseline_is_the_only_migration_head(self): + def test_upload_handoff_migration_is_the_only_head(self): config = Config() config.set_main_option( "script_location", @@ -30,7 +30,7 @@ def test_single_repository_baseline_is_the_only_migration_head(self): scripts = ScriptDirectory.from_config(config) - self.assertEqual(scripts.get_heads(), ["20260729_01"]) + self.assertEqual(scripts.get_heads(), ["20260801_01"]) if __name__ == "__main__": diff --git a/tests/test_mcp.py b/tests/test_mcp.py index 2639628..5fdbaaa 100644 --- a/tests/test_mcp.py +++ b/tests/test_mcp.py @@ -6,10 +6,12 @@ import socket import sys import unittest -from datetime import datetime, timezone +from dataclasses import replace +from datetime import datetime, timedelta, timezone from pathlib import Path from tempfile import TemporaryDirectory from unittest.mock import Mock +from urllib.parse import urlsplit import httpx2 import uvicorn @@ -32,6 +34,7 @@ JobQueue, JobState, MediaPage, + MediaUploadStatus, Principal, QueryVideoCommand, WorkspaceOverview, @@ -52,17 +55,40 @@ from vidxp.composition import HttpApplicationContext from vidxp.control_plane import ControlPlaneApplication from vidxp.core.artifacts import ArtifactKind, ArtifactState +from vidxp.core.uploads import UploadState from vidxp.job_service import JobService from vidxp.mcp import VidXPTokenVerifier, create_mcp_server from vidxp.mcp_cli import main as mcp_main from vidxp.mcp_cli import stdio_client_config from vidxp.ports import LocalFileResource from vidxp.settings import VidXPSettings +from vidxp.upload_service import RemoteUploadService, UploadHandoff MEDIA_ID = "123456781234423481234567890abcde" JOB_ID = "223456781234423481234567890abcde" ARTIFACT_ID = "323456781234423481234567890abcde" +MCP_TOOL_NAMES = [ + "get_workspace", + "list_capabilities", + "get_capability", + "get_runtime_readiness", + "list_media", + "get_media", + "create_media_upload", + "get_media_upload", + "get_index_status", + "start_indexing", + "prepare_models", + "search_moments", + "query_video", + "create_clip", + "get_artifact_download", + "list_jobs", + "get_job", + "retry_job", + "cancel_job", +] def queued_job() -> Job: @@ -121,6 +147,31 @@ def context( settings=settings, ) + def upload_context( + self, + root: Path, + ) -> tuple[HttpApplicationContext, Mock]: + context = self.context(root) + settings = VidXPSettings( + repository_root=root, + runtime_backend="cpu", + upload_public_endpoint="https://uploads.example/uploads/", + upload_internal_endpoint="http://tusd:8080/uploads/", + upload_cleanup_token="c" * 32, + upload_handoff_public_url=("https://vidxp.example/upload-handoff"), + upload_handoff_secret="h" * 32, + ) + uploads = Mock(spec=RemoteUploadService) + return ( + replace( + context, + settings=settings, + authenticator=create_authenticator(settings), + uploads=uploads, + ), + uploads, + ) + async def test_curated_tools_have_structured_output_schemas(self): with TemporaryDirectory() as directory: context = self.context(Path(directory)) @@ -137,42 +188,159 @@ async def test_curated_tools_have_structured_output_schemas(self): self.assertEqual( [tool.name for tool in discovered.tools], - [ - "get_workspace", - "list_capabilities", - "get_capability", - "get_runtime_readiness", - "list_media", - "get_media", - "get_index_status", - "start_indexing", - "prepare_models", - "search_moments", - "query_video", - "create_clip", - "get_artifact_download", - "list_jobs", - "get_job", - "retry_job", - "cancel_job", - ], + MCP_TOOL_NAMES, ) self.assertTrue( all(tool.output_schema is not None for tool in discovered.tools) ) tools = {tool.name: tool for tool in discovered.tools} + self.assertFalse(tools["create_media_upload"].annotations.read_only_hint) + self.assertTrue(tools["create_media_upload"].annotations.idempotent_hint) + self.assertTrue(tools["get_media_upload"].annotations.read_only_hint) + upload_properties = tools["create_media_upload"].input_schema["$defs"][ + "CreateUploadIntentCommand" + ]["properties"] + self.assertEqual( + set(upload_properties), + {"original_filename", "byte_size", "declared_mime_type"}, + ) + serialized_upload_schema = json.dumps( + tools["create_media_upload"].input_schema + ).lower() + for forbidden in ( + '"base64"', + '"blob"', + '"chunk"', + '"chunks"', + '"content"', + '"data"', + ): + self.assertNotIn(forbidden, serialized_upload_schema) for name in ("search_moments", "query_video"): schema = tools[name].input_schema command_ref = schema["properties"]["command"]["$ref"] command_name = command_ref.rsplit("/", 1)[-1] - media_description = schema["$defs"][command_name]["properties"][ - "media_id" - ]["description"] + media_description = schema["$defs"][command_name]["properties"]["media_id"][ + "description" + ] self.assertIn("omit it", media_description) self.assertIn("active index snapshot", media_description) self.assertEqual(result.structured_content, {"items": []}) self.assertFalse(result.is_error) + async def test_media_upload_tools_return_bounded_idempotent_handoff(self): + with TemporaryDirectory() as directory: + context, uploads = self.upload_context(Path(directory)) + now = datetime.now(timezone.utc) + status = MediaUploadStatus( + intent_id=MEDIA_ID, + state=UploadState.pending, + original_filename="sample.mp4", + byte_size=5 * 1024 * 1024, + declared_mime_type="video/mp4", + maximum_bytes=50 * 1024 * 1024 * 1024, + expires_at=now + timedelta(hours=24), + status="Waiting for the expected video to be selected.", + next_action="Open the upload page.", + ) + uploads.create_handoff.return_value = UploadHandoff( + status=status, + capability="v1.selector.signature", + expires_at=now + timedelta(minutes=15), + ) + uploads.get_status.return_value = status + server = create_mcp_server( + context, + default_principal=Principal( + subject="agent", + scopes=frozenset({"*"}), + ), + ) + arguments = { + "command": { + "original_filename": "sample.mp4", + "byte_size": 5 * 1024 * 1024, + "declared_mime_type": "video/mp4", + }, + "idempotency_key": "agent-upload-0001", + } + async with Client(server) as client: + first = await client.call_tool( + "create_media_upload", + arguments, + ) + second = await client.call_tool( + "create_media_upload", + arguments, + ) + current = await client.call_tool( + "get_media_upload", + {"intent_id": MEDIA_ID}, + ) + + self.assertEqual(first.structured_content, second.structured_content) + self.assertEqual(first.structured_content["transport"], "tus") + page_url = urlsplit(first.structured_content["upload_page_url"]) + self.assertEqual(page_url.scheme, "https") + self.assertEqual(page_url.query, "") + self.assertTrue(page_url.fragment.startswith("capability=")) + self.assertEqual(current.structured_content["state"], "pending") + request_keys = [ + call.kwargs["request_key"] for call in uploads.create_handoff.call_args_list + ] + self.assertEqual(len(request_keys), 2) + self.assertEqual(request_keys[0], request_keys[1]) + self.assertRegex(request_keys[0], r"^[0-9a-f]{64}$") + + async def test_media_upload_tools_enforce_permissions_and_availability(self): + with TemporaryDirectory() as directory: + context = self.context(Path(directory)) + arguments = { + "command": { + "original_filename": "sample.mp4", + "byte_size": 1024, + }, + "idempotency_key": "agent-upload-0001", + } + write_server = create_mcp_server( + context, + default_principal=Principal( + subject="reader", + scopes=frozenset({"vidxp.read"}), + ), + ) + async with Client(write_server) as client: + denied = await client.call_tool( + "create_media_upload", + arguments, + ) + unavailable_server = create_mcp_server( + context, + default_principal=Principal( + subject="writer", + scopes=frozenset({"vidxp.write"}), + ), + ) + async with Client(unavailable_server) as client: + unavailable = await client.call_tool( + "create_media_upload", + arguments, + ) + read_denied = await client.call_tool( + "get_media_upload", + {"intent_id": MEDIA_ID}, + ) + + self.assertIn('"required_scope":"vidxp.write"', denied.content[0].text) + self.assertIn( + '"code":"remote_upload_unavailable"', + unavailable.content[0].text, + ) + self.assertIn( + '"required_scope":"vidxp.read"', + read_denied.content[0].text, + ) + async def test_workspace_tool_projects_actionable_repository_state(self): with TemporaryDirectory() as directory: context = self.context(Path(directory)) @@ -244,7 +412,7 @@ def test_stdio_check_performs_handshake_and_tool_probe(self): rendered = output.getvalue() self.assertIn("OK VidXP MCP", rendered) self.assertIn("Index state: missing", rendered) - self.assertIn("Tools: 17", rendered) + self.assertIn("Tools: 19", rendered) self.assertIn("get_index_status", rendered) async def test_server_info_exposes_vidxp_branding(self): @@ -499,8 +667,8 @@ async def test_clip_submission_and_lazy_artifact_download(self): clip = root / "clip.mp4" clip.write_bytes(b"clip-content") context = self.context(root) - context.jobs.submit_snippet.return_value = ( - queued_job().model_copy(update={"kind": JobKind.snippet}) + context.jobs.submit_snippet.return_value = queued_job().model_copy( + update={"kind": JobKind.snippet} ) context.application.get_artifact.return_value = Artifact( artifact_id=ARTIFACT_ID, @@ -513,14 +681,12 @@ async def test_clip_submission_and_lazy_artifact_download(self): state=ArtifactState.ready, created_at=datetime.now(timezone.utc), ) - context.application.open_artifact_content.return_value = ( - LocalFileResource( - path=clip, - filename=f"snippet-{ARTIFACT_ID}.mp4", - mime_type="video/mp4", - byte_size=12, - etag="1" * 64, - ) + context.application.open_artifact_content.return_value = LocalFileResource( + path=clip, + filename=f"snippet-{ARTIFACT_ID}.mp4", + mime_type="video/mp4", + byte_size=12, + etag="1" * 64, ) server = create_mcp_server( context, @@ -651,25 +817,7 @@ async def test_stdio_entrypoint_serves_the_same_curated_surface(self): self.assertEqual( [tool.name for tool in discovered.tools], - [ - "get_workspace", - "list_capabilities", - "get_capability", - "get_runtime_readiness", - "list_media", - "get_media", - "get_index_status", - "start_indexing", - "prepare_models", - "search_moments", - "query_video", - "create_clip", - "get_artifact_download", - "list_jobs", - "get_job", - "retry_job", - "cancel_job", - ], + MCP_TOOL_NAMES, ) self.assertEqual( [item["name"] for item in result.structured_content["items"]], @@ -722,7 +870,7 @@ async def test_streamable_http_works_with_the_official_remote_client(self): server.should_exit = True await serving - self.assertEqual(len(discovered.tools), 17) + self.assertEqual(len(discovered.tools), 19) self.assertEqual(result.structured_content, {"items": []}) async def test_oidc_verifier_projects_the_shared_validated_token(self): diff --git a/tests/test_packaging.py b/tests/test_packaging.py index ba7c0ce..348ccb7 100644 --- a/tests/test_packaging.py +++ b/tests/test_packaging.py @@ -13,6 +13,68 @@ class PackagingTests(unittest.TestCase): + def test_upload_page_assets_are_pinned_self_hosted_and_packaged(self): + package = json.loads( + (ROOT / "web" / "upload-page" / "package.json").read_text(encoding="utf-8") + ) + self.assertEqual( + package["dependencies"], + { + "@uppy/core": "5.2.0", + "@uppy/golden-retriever": "5.2.1", + "@uppy/tus": "5.1.1", + }, + ) + self.assertEqual(package["devDependencies"], {"esbuild": "0.28.1"}) + + project = tomllib.loads((ROOT / "pyproject.toml").read_text(encoding="utf-8")) + self.assertIn( + "assets/upload_page/*", + project["tool"]["setuptools"]["package-data"]["vidxp"], + ) + manifest = (ROOT / "MANIFEST.in").read_text(encoding="utf-8") + self.assertIn("prune web/upload-page/node_modules", manifest) + self.assertNotIn("recursive-include web/upload-page", manifest) + assets = ROOT / "src" / "vidxp" / "assets" / "upload_page" + expected = { + "index.html", + "upload-page.js", + "upload-page.css", + "THIRD_PARTY_NOTICES.txt", + } + self.assertEqual( + {path.name for path in assets.iterdir() if path.is_file()}, + expected, + ) + html = (assets / "index.html").read_text(encoding="utf-8") + self.assertIn("./assets/upload-page.js", html) + self.assertIn("./assets/upload-page.css", html) + self.assertNotRegex(html, r"https?://") + + source = (ROOT / "web" / "upload-page" / "src" / "app.js").read_text( + encoding="utf-8" + ) + for contract in ( + "allowedMetaFields: ['intent_id']", + "parallelUploads: 1", + "overridePatchMethod: false", + "uploadDataDuringCreation: false", + "withCredentials: false", + "removeFingerprintOnSuccess: true", + "VidXP-Handoff", + "pauseResume", + "retryUpload", + "history.replaceState", + ): + self.assertIn(contract, source) + self.assertNotIn("8 * 1024 * 1024", source) + self.assertNotIn("innerHTML", source) + self.assertNotIn("outerHTML", source) + self.assertIn( + "tus-js-client@4.3.1", + (assets / "THIRD_PARTY_NOTICES.txt").read_text(encoding="utf-8"), + ) + def test_canonical_icon_is_packaged_and_desktop_derivatives_are_wired(self): icon = ROOT / "docs" / "images" / "logo.png" self.assertTrue(icon.is_file()) @@ -22,9 +84,7 @@ def test_canonical_icon_is_packaged_and_desktop_derivatives_are_wired(self): "include docs/images/logo.png", (ROOT / "MANIFEST.in").read_text(encoding="utf-8"), ) - project = tomllib.loads( - (ROOT / "pyproject.toml").read_text(encoding="utf-8") - ) + project = tomllib.loads((ROOT / "pyproject.toml").read_text(encoding="utf-8")) self.assertEqual( project["project"]["urls"]["Homepage"], "https://github.com/grayhatdevelopers/vidxp", @@ -60,16 +120,14 @@ def test_canonical_icon_is_packaged_and_desktop_derivatives_are_wired(self): "--output src-tauri/icons" ), ) - sync_script = ( - ROOT / "desktop" / "scripts" / "sync-branding.mjs" - ).read_text(encoding="utf-8") + sync_script = (ROOT / "desktop" / "scripts" / "sync-branding.mjs").read_text( + encoding="utf-8" + ) self.assertIn("../docs/images/logo.png", sync_script) self.assertIn("web/icon.png", sync_script) self.assertIn( 'href="icon.png"', - (ROOT / "desktop" / "web" / "index.html").read_text( - encoding="utf-8" - ), + (ROOT / "desktop" / "web" / "index.html").read_text(encoding="utf-8"), ) def test_cpu_lock_uses_explicit_pytorch_index_without_cuda_packages(self): @@ -78,14 +136,14 @@ def test_cpu_lock_uses_explicit_pytorch_index_without_cuda_packages(self): self.assertIn("https://download.pytorch.org/whl/cpu", lock) self.assertNotIn('name = "nvidia-', lock) - self.assertIn('explicit = true', project) + self.assertIn("explicit = true", project) self.assertIn("sys_platform == 'linux'", project) self.assertIn("sys_platform == 'win32'", project) def test_publishable_metadata_contains_no_direct_url_requirements(self): - project = tomllib.loads( - (ROOT / "pyproject.toml").read_text(encoding="utf-8") - )["project"] + project = tomllib.loads((ROOT / "pyproject.toml").read_text(encoding="utf-8"))[ + "project" + ] requirements = list(project["dependencies"]) requirements.extend( line.strip() @@ -141,9 +199,9 @@ def test_capability_extras_read_capability_owned_requirements(self): ) self.assertIn( "chromadb-client", - ( - ROOT / "src" / "vidxp" / "requirements" / "server-storage.txt" - ).read_text(encoding="utf-8"), + (ROOT / "src" / "vidxp" / "requirements" / "server-storage.txt").read_text( + encoding="utf-8" + ), ) self.assertNotIn("benchmarks/requirements.txt", all_block) self.assertNotIn("requirements/frontend.txt", all_block) @@ -178,9 +236,9 @@ def test_install_profiles_keep_server_free_of_ml_dependencies(self): '"src/vidxp/requirements/mcp.txt"', pyproject, ) - server = ( - ROOT / "src" / "vidxp" / "requirements" / "server.txt" - ).read_text(encoding="utf-8") + server = (ROOT / "src" / "vidxp" / "requirements" / "server.txt").read_text( + encoding="utf-8" + ) for distribution in ( "chromadb", "faster-whisper", @@ -222,9 +280,7 @@ def test_install_profiles_keep_server_free_of_ml_dependencies(self): self.assertNotIn("httpx2", test_requirements) def test_optional_ollama_profile_never_pulls_a_model_implicitly(self): - compose = (ROOT / "compose.coolify.yaml").read_text( - encoding="utf-8" - ) + compose = (ROOT / "compose.coolify.yaml").read_text(encoding="utf-8") self.assertIn( "ollama/ollama:0.32.5@sha256:" @@ -243,33 +299,24 @@ def test_optional_ollama_profile_never_pulls_a_model_implicitly(self): self.assertNotIn("ollama pull", compose.lower()) def test_desktop_manifest_matches_published_package_contract(self): - project = tomllib.loads( - (ROOT / "pyproject.toml").read_text(encoding="utf-8") - ) + project = tomllib.loads((ROOT / "pyproject.toml").read_text(encoding="utf-8")) self.assertEqual( project["project"]["scripts"]["vidxp"], "vidxp.entrypoint:main", ) manifest = json.loads( - (ROOT / "desktop" / "runtime-manifest.json").read_text( - encoding="utf-8" - ) + (ROOT / "desktop" / "runtime-manifest.json").read_text(encoding="utf-8") ) package = json.loads( (ROOT / "desktop" / "package.json").read_text(encoding="utf-8") ) tauri = json.loads( - ( - ROOT - / "desktop" - / "src-tauri" - / "tauri.conf.json" - ).read_text(encoding="utf-8") + (ROOT / "desktop" / "src-tauri" / "tauri.conf.json").read_text( + encoding="utf-8" + ) ) cargo = tomllib.loads( - ( - ROOT / "desktop" / "src-tauri" / "Cargo.toml" - ).read_text(encoding="utf-8") + (ROOT / "desktop" / "src-tauri" / "Cargo.toml").read_text(encoding="utf-8") ) self.assertEqual(manifest["package_name"], project["project"]["name"]) @@ -284,9 +331,9 @@ def test_desktop_manifest_matches_published_package_contract(self): tauri["bundle"]["windows"]["nsis"]["installerHooks"], "nsis-hooks.nsh", ) - nsis_hooks = ( - ROOT / "desktop" / "src-tauri" / "nsis-hooks.nsh" - ).read_text(encoding="utf-8") + nsis_hooks = (ROOT / "desktop" / "src-tauri" / "nsis-hooks.nsh").read_text( + encoding="utf-8" + ) self.assertIn( r"$LOCALAPPDATA\Programs\${PRODUCTNAME}", nsis_hooks, @@ -334,9 +381,7 @@ def test_release_please_preserves_desktop_manifests_and_links_versions(self): "release-please-config.json", "release-please-config.stable.json", ): - config = json.loads( - (ROOT / filename).read_text(encoding="utf-8") - ) + config = json.loads((ROOT / filename).read_text(encoding="utf-8")) self.assertNotIn("group-pull-request-title-pattern", config) linked_versions = [ plugin @@ -364,9 +409,7 @@ def test_release_please_preserves_desktop_manifests_and_links_versions(self): self.assertIn("src-tauri/Cargo.lock", generic_files, filename) stable = json.loads( - (ROOT / "release-please-config.stable.json").read_text( - encoding="utf-8" - ) + (ROOT / "release-please-config.stable.json").read_text(encoding="utf-8") ) beta_manifest_updates = { (entry["path"], entry.get("jsonpath")) @@ -383,35 +426,29 @@ def test_release_please_preserves_desktop_manifests_and_links_versions(self): ) manifest = json.loads( - (ROOT / "desktop" / "runtime-manifest.json").read_text( - encoding="utf-8" - ) + (ROOT / "desktop" / "runtime-manifest.json").read_text(encoding="utf-8") ) package = json.loads( (ROOT / "desktop" / "package.json").read_text(encoding="utf-8") ) cargo = tomllib.loads( - ( - ROOT / "desktop" / "src-tauri" / "Cargo.toml" - ).read_text(encoding="utf-8") + (ROOT / "desktop" / "src-tauri" / "Cargo.toml").read_text(encoding="utf-8") ) version_file = ( - ROOT / "desktop" / "VERSION" - ).read_text(encoding="utf-8").strip() + (ROOT / "desktop" / "VERSION").read_text(encoding="utf-8").strip() + ) self.assertEqual(version_file, manifest["desktop_version"]) self.assertEqual(version_file, package["version"]) self.assertEqual(version_file, cargo["package"]["version"]) self.assertIn( f'version = "{version_file}" # x-release-please-version', - ( - ROOT / "desktop" / "src-tauri" / "Cargo.toml" - ).read_text(encoding="utf-8"), + (ROOT / "desktop" / "src-tauri" / "Cargo.toml").read_text(encoding="utf-8"), ) self.assertNotIn( f"vidxp=={version_file}", - ( - ROOT / "desktop" / "src-tauri" / "src" / "lib.rs" - ).read_text(encoding="utf-8"), + (ROOT / "desktop" / "src-tauri" / "src" / "lib.rs").read_text( + encoding="utf-8" + ), ) build_command = "bash utils/build_package.sh" @@ -445,9 +482,9 @@ def test_release_please_preserves_desktop_manifests_and_links_versions(self): "publish-desktop.yml": 4, } for workflow, expected in publisher_repo_flags.items(): - contents = ( - ROOT / ".github" / "workflows" / workflow - ).read_text(encoding="utf-8") + contents = (ROOT / ".github" / "workflows" / workflow).read_text( + encoding="utf-8" + ) self.assertEqual( contents.count('--repo "$GITHUB_REPOSITORY"'), expected, @@ -466,23 +503,23 @@ def test_release_please_preserves_desktop_manifests_and_links_versions(self): self.assertNotIn("--latest", core_publish) for workflow in ("ci.yml", "desktop.yml", "security.yml"): - contents = ( - ROOT / ".github" / "workflows" / workflow - ).read_text(encoding="utf-8") + contents = (ROOT / ".github" / "workflows" / workflow).read_text( + encoding="utf-8" + ) self.assertIn(" - release", contents, workflow) self.assertIn("github.base_ref != 'release'", contents, workflow) self.assertIn("github.head_ref != 'main'", contents, workflow) - desktop_ci = ( - ROOT / ".github" / "workflows" / "desktop.yml" - ).read_text(encoding="utf-8") + desktop_ci = (ROOT / ".github" / "workflows" / "desktop.yml").read_text( + encoding="utf-8" + ) self.assertIn( "!startsWith(github.head_ref, 'release-please--branches--')", desktop_ci, ) - promotion = ( - ROOT / ".github" / "workflows" / "promotion-pr.yml" - ).read_text(encoding="utf-8") + promotion = (ROOT / ".github" / "workflows" / "promotion-pr.yml").read_text( + encoding="utf-8" + ) self.assertIn('"$status" == "diverged"', promotion) synchronization = ( ROOT / ".github" / "workflows" / "sync-channels.yml" @@ -501,9 +538,9 @@ def test_release_please_preserves_desktop_manifests_and_links_versions(self): ) def test_windows_release_binary_uses_the_gui_subsystem(self): - main = ( - ROOT / "desktop" / "src-tauri" / "src" / "main.rs" - ).read_text(encoding="utf-8") + main = (ROOT / "desktop" / "src-tauri" / "src" / "main.rs").read_text( + encoding="utf-8" + ) self.assertIn( '#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]', diff --git a/tests/test_settings.py b/tests/test_settings.py index c089a17..c87f0f1 100644 --- a/tests/test_settings.py +++ b/tests/test_settings.py @@ -116,6 +116,35 @@ def test_unimplemented_remote_mode_is_rejected_explicitly(self): ): VidXPSettings(mode=ApplicationMode.remote) + def test_upload_handoff_requires_https_url_and_dedicated_secret(self): + base = { + "upload_public_endpoint": "https://uploads.example/uploads/", + "upload_internal_endpoint": "http://tusd:8080/uploads/", + "upload_cleanup_token": "c" * 32, + } + with self.assertRaisesRegex(ValidationError, "HTTPS URL"): + VidXPSettings( + **base, + upload_handoff_public_url=("http://vidxp.example/upload-handoff"), + upload_handoff_secret="h" * 32, + ) + with self.assertRaisesRegex(ValidationError, "dedicated secret"): + VidXPSettings( + **base, + upload_handoff_public_url=("https://vidxp.example/upload-handoff"), + ) + + settings = VidXPSettings( + **base, + upload_handoff_public_url=("https://vidxp.example/upload-handoff/"), + upload_handoff_secret="h" * 32, + ) + self.assertEqual( + settings.upload_handoff_public_url, + "https://vidxp.example/upload-handoff", + ) + self.assertEqual(settings.mcp_max_request_body_bytes, 4 * 1024 * 1024) + if __name__ == "__main__": unittest.main() diff --git a/tests/test_tusd_hooks.py b/tests/test_tusd_hooks.py index 3915851..d10f7cd 100644 --- a/tests/test_tusd_hooks.py +++ b/tests/test_tusd_hooks.py @@ -38,6 +38,8 @@ def _hooks(tmp_path: Path): upload_public_endpoint="http://localhost:8080/uploads/", upload_internal_endpoint="http://localhost:8080/uploads/", upload_cleanup_token="c" * 32, + upload_handoff_public_url="https://upload.example/upload-handoff", + upload_handoff_secret="h" * 32, ) uploads = RemoteUploadService( settings=settings, @@ -69,21 +71,27 @@ def _hooks(tmp_path: Path): ) -def _pre_create(intent_id: str, token: str) -> TusdHookRequest: +def _pre_create( + intent_id: str, + token: str, + *, + size: int = 20, + scheme: str = "Bearer", +) -> TusdHookRequest: return TusdHookRequest.model_validate( { "Type": "pre-create", "Event": { "Upload": { "ID": "", - "Size": 20, + "Size": size, "Offset": 0, "MetaData": {"intent_id": intent_id}, }, "HTTPRequest": { "Method": "POST", "URI": "/uploads/", - "Header": {"Authorization": [f"Bearer {token}"]}, + "Header": {"Authorization": [f"{scheme} {token}"]}, }, }, } @@ -104,6 +112,72 @@ def test_pre_create_authenticates_and_assigns_stored_id( catalog.close() +def test_pre_create_accepts_bounded_handoff_for_five_mib( + tmp_path: Path, +) -> None: + hooks, catalog, _, _ = _hooks(tmp_path) + size = 5 * 1024 * 1024 + handoff = hooks.uploads.create_handoff( + CreateUploadIntentCommand( + original_filename="five-mib.mp4", + byte_size=size, + declared_mime_type="video/mp4", + ), + principal=Principal(subject="agent", scopes=frozenset({"*"})), + request_key="b" * 64, + ) + session = hooks.uploads.exchange_handoff( + handoff.status.intent_id, + capability=handoff.capability, + ) + grant = hooks.uploads.issue_creation_grant( + handoff.status.intent_id, + session_token=session.session_token, + ) + + response = hooks.handle( + _pre_create( + handoff.status.intent_id, + grant.token, + size=size, + scheme="VidXP-Handoff", + ) + ) + + assert not response.reject_upload + stored = catalog.get_upload_intent(handoff.status.intent_id) + assert stored is not None and stored.state.value == "accepted" + assert response.change_file_info is not None + assert grant.token not in stored.model_dump_json() + + replay = hooks.handle( + _pre_create( + handoff.status.intent_id, + grant.token, + size=size, + scheme="VidXP-Handoff", + ) + ) + assert replay.reject_upload + assert replay.http_response is not None + assert replay.http_response.status_code == 409 + assert replay.http_response.headers["X-VidXP-Error"] == ("upload_handoff_replayed") + catalog.close() + + +def test_pre_create_never_falls_back_unknown_authorization_scheme( + tmp_path: Path, +) -> None: + hooks, catalog, intent, token = _hooks(tmp_path) + + response = hooks.handle(_pre_create(intent.intent_id, token, scheme="Unknown")) + + assert response.reject_upload + assert response.http_response is not None + assert response.http_response.status_code == 401 + catalog.close() + + def test_pre_create_rejects_unsupported_tus_modes(tmp_path: Path) -> None: hooks, catalog, intent, token = _hooks(tmp_path) request = _pre_create(intent.intent_id, token).model_copy( @@ -113,9 +187,7 @@ def test_pre_create_rejects_unsupported_tus_modes(tmp_path: Path) -> None: "upload": _pre_create( intent.intent_id, token, - ).event.upload.model_copy( - update={"size_is_deferred": True} - ) + ).event.upload.model_copy(update={"size_is_deferred": True}) } ) } diff --git a/tests/test_tusd_live.py b/tests/test_tusd_live.py new file mode 100644 index 0000000..32b316a --- /dev/null +++ b/tests/test_tusd_live.py @@ -0,0 +1,243 @@ +from __future__ import annotations + +import base64 +import os +import socket +import subprocess +import threading +import time +from pathlib import Path +from urllib.parse import urljoin + +import httpx +import pytest +import uvicorn + +from vidxp.application_models import CreateUploadIntentCommand, Principal +from vidxp.authentication import StaticBearerAuthenticator +from vidxp.authorization import AuthorizationPolicy +from vidxp.composition import UploadHookContext +from vidxp.core.uploads import UploadState +from vidxp.hook_app import create_hook_app +from vidxp.infrastructure.sql_catalog import SQLCatalog +from vidxp.settings import VidXPSettings +from vidxp.upload_service import RemoteUploadService + + +class _Jobs: + def enqueue_media_import_in_transaction( + self, + upload_id: str, + *, + connection, + job_id: str, + ) -> str: + del upload_id, connection + return job_id + + def close(self) -> None: + pass + + +def _free_port() -> int: + with socket.socket() as listener: + listener.bind(("127.0.0.1", 0)) + return int(listener.getsockname()[1]) + + +def _wait_for_http(url: str, *, process: subprocess.Popen[str] | None = None) -> None: + deadline = time.monotonic() + 15 + while time.monotonic() < deadline: + if process is not None and process.poll() is not None: + stdout, stderr = process.communicate() + raise AssertionError( + f"tusd exited during startup ({process.returncode}):\n" + f"{stdout}\n{stderr}" + ) + try: + if httpx.get(url, timeout=0.5).status_code < 500: + return + except httpx.HTTPError: + pass + time.sleep(0.05) + raise AssertionError(f"Timed out waiting for {url}") + + +def test_live_tusd_handoff_resumes_five_mib(tmp_path: Path) -> None: + executable_value = os.environ.get("VIDXP_TUSD_EXECUTABLE") + if not executable_value: + pytest.skip("set VIDXP_TUSD_EXECUTABLE to run the live tusd integration") + executable = Path(executable_value) + if not executable.is_file(): + pytest.fail(f"VIDXP_TUSD_EXECUTABLE is not a file: {executable}") + + hook_port = _free_port() + tusd_port = _free_port() + size = 5 * 1024 * 1024 + settings = VidXPSettings( + repository_root=tmp_path, + upload_public_endpoint=f"http://127.0.0.1:{tusd_port}/uploads/", + upload_internal_endpoint=f"http://127.0.0.1:{tusd_port}/uploads/", + upload_cleanup_token="c" * 32, + upload_handoff_public_url="https://upload.example/upload-handoff", + upload_handoff_secret="h" * 32, + upload_max_bytes=2 * size, + upload_quota_bytes=2 * size, + upload_recovery_interval_seconds=3600, + ) + catalog = SQLCatalog( + f"sqlite:///{(tmp_path / 'server.sqlite3').resolve().as_posix()}", + initialize=True, + ) + jobs = _Jobs() + uploads = RemoteUploadService( + settings=settings, + catalog=catalog, + media=object(), + jobs=jobs, + ) + authenticator = StaticBearerAuthenticator("t" * 32) + authorization = AuthorizationPolicy() + hook_context = UploadHookContext( + jobs=jobs, # type: ignore[arg-type] + authenticator=authenticator, + authorization=authorization, + settings=settings, + catalog=catalog, + uploads=uploads, + ) + hook_server = uvicorn.Server( + uvicorn.Config( + create_hook_app(context=hook_context), + host="127.0.0.1", + port=hook_port, + log_level="error", + ) + ) + hook_thread = threading.Thread(target=hook_server.run, daemon=True) + hook_thread.start() + _wait_for_http(f"http://127.0.0.1:{hook_port}/health") + + settings.quarantine_root.mkdir(parents=True, exist_ok=True) + tusd = subprocess.Popen( + [ + str(executable), + "-host=127.0.0.1", + f"-port={tusd_port}", + f"-upload-dir={settings.quarantine_root}", + "-base-path=/uploads/", + f"-max-size={2 * size}", + "-disable-download", + "-disable-concatenation", + f"-hooks-http=http://127.0.0.1:{hook_port}/hooks", + "-hooks-enabled-events=pre-create,post-finish", + "-hooks-http-timeout=5s", + "-show-startup-logs=false", + ], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + text=True, + ) + try: + _wait_for_http( + f"http://127.0.0.1:{tusd_port}/metrics", + process=tusd, + ) + handoff = uploads.create_handoff( + CreateUploadIntentCommand( + original_filename="five-mib.mp4", + byte_size=size, + declared_mime_type="video/mp4", + ), + principal=Principal(subject="agent", scopes=frozenset({"*"})), + request_key="a" * 64, + ) + session = uploads.exchange_handoff( + handoff.status.intent_id, + capability=handoff.capability, + ) + grant = uploads.issue_creation_grant( + handoff.status.intent_id, + session_token=session.session_token, + ) + metadata = base64.b64encode(handoff.status.intent_id.encode()).decode() + + with httpx.Client(timeout=15) as client: + created = client.post( + f"http://127.0.0.1:{tusd_port}/uploads/", + headers={ + "Tus-Resumable": "1.0.0", + "Upload-Length": str(size), + "Upload-Metadata": f"intent_id {metadata}", + "Authorization": f"VidXP-Handoff {grant.token}", + }, + ) + assert created.status_code == 201, created.text + upload_url = urljoin(str(created.url), created.headers["Location"]) + + first_chunk = 1024 * 1024 + first_patch = client.patch( + upload_url, + headers={ + "Tus-Resumable": "1.0.0", + "Upload-Offset": "0", + "Content-Type": "application/offset+octet-stream", + }, + content=b"a" * first_chunk, + ) + assert first_patch.status_code == 204, first_patch.text + + paused = client.head( + upload_url, + headers={"Tus-Resumable": "1.0.0"}, + ) + assert paused.status_code == 200 + assert paused.headers["Upload-Offset"] == str(first_chunk) + + resumed = client.patch( + upload_url, + headers={ + "Tus-Resumable": "1.0.0", + "Upload-Offset": str(first_chunk), + "Content-Type": "application/offset+octet-stream", + }, + content=b"b" * (size - first_chunk), + ) + assert resumed.status_code == 204, resumed.text + + completed = client.head( + upload_url, + headers={"Tus-Resumable": "1.0.0"}, + ) + assert completed.status_code == 200 + assert completed.headers["Upload-Offset"] == str(size) + + deadline = time.monotonic() + 5 + record = catalog.get_upload_intent(handoff.status.intent_id) + while record is not None and record.state != UploadState.processing: + if time.monotonic() >= deadline: + break + time.sleep(0.05) + record = catalog.get_upload_intent(handoff.status.intent_id) + + assert record is not None + assert record.state == UploadState.processing + assert record.job_id == record.upload_id + assert record.upload_id is not None + assert (settings.quarantine_root / record.upload_id).stat().st_size == size + info = (settings.quarantine_root / f"{record.upload_id}.info").read_text( + encoding="utf-8" + ) + assert grant.token not in info + assert session.session_token not in info + assert "Authorization" not in info + finally: + tusd.terminate() + try: + tusd.communicate(timeout=5) + except subprocess.TimeoutExpired: + tusd.kill() + tusd.communicate(timeout=5) + hook_server.should_exit = True + hook_thread.join(timeout=5) + catalog.close() diff --git a/tests/test_upload_page.py b/tests/test_upload_page.py new file mode 100644 index 0000000..4fb7e89 --- /dev/null +++ b/tests/test_upload_page.py @@ -0,0 +1,194 @@ +from __future__ import annotations + +from pathlib import Path +from unittest.mock import Mock + +from fastapi.testclient import TestClient + +from vidxp.api import create_app +from vidxp.application_models import CreateUploadIntentCommand, Principal +from vidxp.authentication import create_authenticator +from vidxp.authorization import AuthorizationPolicy +from vidxp.composition import HttpApplicationContext +from vidxp.control_plane import ControlPlaneApplication +from vidxp.infrastructure.sql_catalog import SQLCatalog +from vidxp.job_service import JobService +from vidxp.settings import VidXPSettings +from vidxp.upload_service import RemoteUploadService + + +def _fixture(root: Path): + settings = VidXPSettings( + repository_root=root, + runtime_backend="cpu", + http_auth_mode="static", + http_static_bearer_token="a" * 32, + http_trusted_hosts=("testserver",), + mcp_allowed_hosts=("testserver",), + upload_public_endpoint="https://uploads.example/uploads/", + upload_internal_endpoint="http://tusd:8080/uploads/", + upload_cleanup_token="c" * 32, + upload_handoff_public_url="https://testserver/upload-handoff", + upload_handoff_secret="h" * 32, + ) + catalog = SQLCatalog( + f"sqlite:///{(root / 'server.sqlite3').resolve().as_posix()}", + initialize=True, + ) + jobs = Mock(spec=JobService) + uploads = RemoteUploadService( + settings=settings, + catalog=catalog, + media=object(), + jobs=jobs, + ) + application = Mock(spec=ControlPlaneApplication) + readiness = Mock() + readiness.ready.return_value = True + context = HttpApplicationContext( + application=application, + jobs=jobs, + authorization=AuthorizationPolicy(), + settings=settings, + catalog=catalog, + uploads=uploads, + readiness=readiness, + authenticator=create_authenticator(settings), + ) + handoff = uploads.create_handoff( + CreateUploadIntentCommand( + original_filename="sample.mp4", + byte_size=5 * 1024 * 1024, + declared_mime_type="video/mp4", + ), + principal=Principal(subject="agent", scopes=frozenset({"*"})), + request_key="a" * 64, + ) + return context, catalog, handoff + + +def _assert_security_headers(response) -> None: + assert response.headers["cache-control"] == "private, no-store" + assert response.headers["referrer-policy"] == "no-referrer" + assert response.headers["x-content-type-options"] == "nosniff" + assert response.headers["x-frame-options"] == "DENY" + assert response.headers["cross-origin-opener-policy"] == "same-origin" + csp = response.headers["content-security-policy"] + assert "default-src 'none'" in csp + assert "script-src 'self'" in csp + assert "style-src 'self'" in csp + assert "connect-src 'self' https://uploads.example" in csp + assert "frame-ancestors 'none'" in csp + assert "unsafe-inline" not in csp + + +def test_upload_page_bootstrap_session_assets_and_security(tmp_path: Path) -> None: + context, catalog, handoff = _fixture(tmp_path) + path = f"/upload-handoff/{handoff.status.intent_id}" + + with TestClient( + create_app(context=context), + base_url="https://testserver", + ) as client: + page = client.get(path) + _assert_security_headers(page) + assert page.status_code == 200 + assert handoff.capability not in page.text + assert "./assets/upload-page.js" in page.text + assert "./assets/upload-page.css" in page.text + + script = client.get("/upload-handoff/assets/upload-page.js") + stylesheet = client.get("/upload-handoff/assets/upload-page.css") + assert script.status_code == stylesheet.status_code == 200 + assert script.headers["content-type"].startswith("text/javascript") + assert stylesheet.headers["content-type"].startswith("text/css") + + bootstrap = client.post( + f"{path}/bootstrap", + headers={ + "Origin": "https://testserver", + "Sec-Fetch-Site": "same-origin", + }, + json={"capability": handoff.capability}, + ) + _assert_security_headers(bootstrap) + assert bootstrap.status_code == 200 + payload = bootstrap.json() + assert payload["status"]["original_filename"] == "sample.mp4" + assert payload["status"]["byte_size"] == 5 * 1024 * 1024 + assert payload["status"]["maximum_bytes"] == 50 * 1024 * 1024 * 1024 + assert payload["creation_url"] == "https://uploads.example/uploads/" + cookie = bootstrap.headers["set-cookie"] + assert "__Secure-vidxp-upload=" in cookie + assert "HttpOnly" in cookie + assert "Secure" in cookie + assert "SameSite=strict" in cookie + assert f"Path={path}" in cookie + + status = client.get(f"{path}/status") + assert status.status_code == 200 + assert status.json()["status"]["state"] == "pending" + + grant = client.post( + f"{path}/creation-grant", + headers={ + "Origin": "https://testserver", + "Sec-Fetch-Site": "same-origin", + }, + json={}, + ) + assert grant.status_code == 200 + assert grant.json()["scheme"] == "VidXP-Handoff" + assert grant.json()["grant"].startswith("v1.") + + protected = client.get(f"/upload-handoff/{handoff.status.intent_id}evil") + assert protected.status_code == 401 + normal_api = client.get("/api/v1/workspace") + assert normal_api.status_code == 401 + + stored = catalog.get_upload_handoff_by_intent(handoff.status.intent_id) + assert stored is not None + assert stored.session_digest is not None + assert handoff.capability not in stored.model_dump_json() + catalog.close() + + +def test_upload_page_rejects_tamper_and_cross_origin_with_headers( + tmp_path: Path, +) -> None: + context, catalog, handoff = _fixture(tmp_path) + path = f"/upload-handoff/{handoff.status.intent_id}/bootstrap" + tampered = handoff.capability[:-1] + ("A" if handoff.capability[-1] != "A" else "B") + + with TestClient( + create_app(context=context), + base_url="https://testserver", + ) as client: + wrong_origin = client.post( + path, + headers={ + "Origin": "https://evil.example", + "Sec-Fetch-Site": "cross-site", + }, + json={"capability": handoff.capability}, + ) + _assert_security_headers(wrong_origin) + assert wrong_origin.status_code == 403 + assert wrong_origin.json()["error"]["code"] == ( + "upload_handoff_origin_forbidden" + ) + + invalid = client.post( + path, + headers={ + "Origin": "https://testserver", + "Sec-Fetch-Site": "same-origin", + }, + json={"capability": tampered}, + ) + _assert_security_headers(invalid) + assert invalid.status_code == 401 + assert invalid.json()["error"]["code"] == "upload_handoff_invalid" + assert tampered not in invalid.text + + catalog.close() diff --git a/tests/test_upload_service.py b/tests/test_upload_service.py index fc82b59..2f4e438 100644 --- a/tests/test_upload_service.py +++ b/tests/test_upload_service.py @@ -62,6 +62,8 @@ def _service( upload_public_endpoint="http://localhost:8080/uploads/", upload_internal_endpoint="http://localhost:8080/uploads/", upload_cleanup_token="x" * 32, + upload_handoff_public_url="https://upload.example/upload-handoff", + upload_handoff_secret="h" * 32, upload_max_bytes=quota, upload_quota_bytes=quota, ) @@ -114,6 +116,187 @@ def test_upload_intent_is_idempotent_and_repository_shared( catalog.close() +def test_upload_handoff_is_idempotent_and_bound_to_intent( + tmp_path: Path, +) -> None: + service, catalog, _ = _service(tmp_path) + owner = Principal(subject="owner") + + first = service.create_handoff( + _command(), + principal=owner, + request_key="a" * 64, + ) + replay = service.create_handoff( + _command(), + principal=owner, + request_key="a" * 64, + ) + + assert replay == first + assert first.status.state == UploadState.pending + assert first.status.maximum_bytes == 100 + stored = catalog.get_upload_handoff_by_intent(first.status.intent_id) + assert stored is not None + assert stored.session_digest is None + assert first.capability not in stored.model_dump_json() + catalog.close() + + +def test_handoff_rejects_tamper_wrong_intent_and_expiry( + tmp_path: Path, +) -> None: + service, catalog, _ = _service(tmp_path) + handoff = service.create_handoff( + _command(), + principal=Principal(subject="owner"), + request_key="a" * 64, + ) + tampered = handoff.capability[:-1] + ("A" if handoff.capability[-1] != "A" else "B") + + with pytest.raises(ApplicationError) as invalid: + service.exchange_handoff( + handoff.status.intent_id, + capability=tampered, + ) + assert invalid.value.detail.code == "upload_handoff_invalid" + + with pytest.raises(ApplicationError) as wrong_intent: + service.exchange_handoff( + uuid4().hex, + capability=handoff.capability, + ) + assert wrong_intent.value.detail.code == "upload_handoff_invalid" + + with patch( + "vidxp.upload_service.utc_now", + return_value=handoff.expires_at + timedelta(seconds=1), + ): + with pytest.raises(ApplicationError) as expired: + service.exchange_handoff( + handoff.status.intent_id, + capability=handoff.capability, + ) + assert expired.value.detail.code == "upload_handoff_expired" + catalog.close() + + +def test_browser_session_grant_accepts_five_mib_and_rejects_replay( + tmp_path: Path, +) -> None: + size = 5 * 1024 * 1024 + service, catalog, _ = _service(tmp_path, quota=2 * size) + command = _command(size) + handoff = service.create_handoff( + command, + principal=Principal(subject="owner"), + request_key="a" * 64, + ) + session = service.exchange_handoff( + handoff.status.intent_id, + capability=handoff.capability, + ) + grant = service.issue_creation_grant( + handoff.status.intent_id, + session_token=session.session_token, + ) + + with pytest.raises(ApplicationError) as wrong_size: + service.accept_handoff_creation( + handoff.status.intent_id, + grant=grant.token, + byte_size=size + 1, + ) + assert wrong_size.value.detail.code == "upload_creation_grant_invalid" + + accepted = service.accept_handoff_creation( + handoff.status.intent_id, + grant=grant.token, + byte_size=size, + ) + assert accepted.state == UploadState.accepted + page = service.browser_session( + handoff.status.intent_id, + session_token=session.session_token, + ) + assert page.resume_url is None + + with pytest.raises(ApplicationError) as replayed: + service.accept_handoff_creation( + handoff.status.intent_id, + grant=grant.token, + byte_size=size, + ) + assert replayed.value.detail.code == "upload_handoff_replayed" + + retry_grant = service.issue_creation_grant( + handoff.status.intent_id, + session_token=session.session_token, + ) + retried = service.accept_handoff_creation( + handoff.status.intent_id, + grant=retry_grant.token, + byte_size=size, + ) + assert retried.upload_id == accepted.upload_id + + info_path = service.settings.quarantine_root / f"{accepted.upload_id}.info" + info_path.parent.mkdir(parents=True, exist_ok=True) + info_path.write_text("{}", encoding="utf-8") + resumed_page = service.browser_session( + handoff.status.intent_id, + session_token=session.session_token, + ) + assert resumed_page.resume_url is not None + assert resumed_page.resume_url.endswith(accepted.upload_id or "") + + with pytest.raises(ApplicationError) as already_created: + service.issue_creation_grant( + handoff.status.intent_id, + session_token=session.session_token, + ) + assert already_created.value.detail.code == "upload_handoff_replayed" + catalog.close() + + +def test_upload_status_projects_processing_failed_and_ready_actions( + tmp_path: Path, +) -> None: + service, catalog, _ = _service(tmp_path) + created = service.create_intent( + _command(), + principal=Principal(subject="owner"), + request_key="a" * 64, + ) + + processing = service.status( + created.model_copy( + update={"state": UploadState.processing, "job_id": uuid4().hex} + ) + ) + assert processing.job_id is not None + assert "get_job" in processing.next_action + + failed = service.status( + created.model_copy(update={"state": UploadState.failed, "job_id": uuid4().hex}) + ) + assert failed.job_id is not None + assert "get_job" in failed.next_action + + ready = service.status( + created.model_copy( + update={ + "state": UploadState.ready, + "job_id": uuid4().hex, + "media_id": uuid4().hex, + } + ) + ) + assert ready.media_id is not None + assert "start_indexing" in ready.next_action + catalog.close() + + def test_upload_quota_is_reserved_and_released_atomically( tmp_path: Path, ) -> None: @@ -311,9 +494,7 @@ def test_active_resumable_upload_is_not_expired(tmp_path: Path) -> None: catalog.create_upload_intent(record, quota_limit=100) service.settings.quarantine_root.mkdir(parents=True, exist_ok=True) (service.settings.quarantine_root / f"{upload_id}.info").write_text( - '{"MetaData":{"intent_id":"' - + record.intent_id - + '"},"Size":60,"Offset":10}', + '{"MetaData":{"intent_id":"' + record.intent_id + '"},"Size":60,"Offset":10}', encoding="utf-8", ) diff --git a/web/upload-page/package-lock.json b/web/upload-page/package-lock.json new file mode 100644 index 0000000..f473439 --- /dev/null +++ b/web/upload-page/package-lock.json @@ -0,0 +1,866 @@ +{ + "name": "@vidxp/upload-page", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "@vidxp/upload-page", + "version": "0.0.0", + "dependencies": { + "@uppy/core": "5.2.0", + "@uppy/golden-retriever": "5.2.1", + "@uppy/tus": "5.1.1" + }, + "devDependencies": { + "esbuild": "0.28.1" + }, + "engines": { + "node": ">=22 <23" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.1.tgz", + "integrity": "sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.1.tgz", + "integrity": "sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.1.tgz", + "integrity": "sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.1.tgz", + "integrity": "sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.1.tgz", + "integrity": "sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.1.tgz", + "integrity": "sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.1.tgz", + "integrity": "sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.1.tgz", + "integrity": "sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.1.tgz", + "integrity": "sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.1.tgz", + "integrity": "sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.1.tgz", + "integrity": "sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.1.tgz", + "integrity": "sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.1.tgz", + "integrity": "sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.1.tgz", + "integrity": "sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.1.tgz", + "integrity": "sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.1.tgz", + "integrity": "sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.1.tgz", + "integrity": "sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.1.tgz", + "integrity": "sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.1.tgz", + "integrity": "sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.1.tgz", + "integrity": "sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.1.tgz", + "integrity": "sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.1.tgz", + "integrity": "sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.1.tgz", + "integrity": "sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.1.tgz", + "integrity": "sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.1.tgz", + "integrity": "sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.1.tgz", + "integrity": "sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@transloadit/prettier-bytes": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/@transloadit/prettier-bytes/-/prettier-bytes-0.3.5.tgz", + "integrity": "sha512-xF4A3d/ZyX2LJWeQZREZQw+qFX4TGQ8bGVP97OLRt6sPO6T0TNHBFTuRHOJh7RNmYOBmQ9MHxpolD9bXihpuVA==", + "license": "MIT" + }, + "node_modules/@types/retry": { + "version": "0.12.2", + "resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.2.tgz", + "integrity": "sha512-XISRgDJ2Tc5q4TRqvgJtzsRkFYNJzZrhTdtMoGVBttwzzQJkPnS3WWTFc7kuDRoPtPakl+T+OfdEUjYJj7Jbow==", + "license": "MIT" + }, + "node_modules/@uppy/companion-client": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/@uppy/companion-client/-/companion-client-5.1.1.tgz", + "integrity": "sha512-DzrOWTbIZHvtgAFXBMYHk2wD27NjpBSVhY2tEiEIUhPd2CxbFRZjHM/N3HOt3VwZEAP471QWFLlJRWPcIY3A2Q==", + "license": "MIT", + "dependencies": { + "@uppy/utils": "^7.1.1", + "namespace-emitter": "^2.0.1", + "p-retry": "^6.1.0" + }, + "peerDependencies": { + "@uppy/core": "^5.1.1" + } + }, + "node_modules/@uppy/core": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@uppy/core/-/core-5.2.0.tgz", + "integrity": "sha512-uvfNyz4cnaplt7LYJmEZHuqOuav0tKp4a9WKJIaH6iIj7XiqYvS2J5SEByexAlUFlzefOAyjzj4Ja2dd/8aMrw==", + "license": "MIT", + "dependencies": { + "@transloadit/prettier-bytes": "^0.3.4", + "@uppy/store-default": "^5.0.0", + "@uppy/utils": "^7.1.4", + "lodash": "^4.17.21", + "mime-match": "^1.0.2", + "namespace-emitter": "^2.0.1", + "nanoid": "^5.0.9", + "preact": "^10.5.13" + } + }, + "node_modules/@uppy/golden-retriever": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/@uppy/golden-retriever/-/golden-retriever-5.2.1.tgz", + "integrity": "sha512-R+Lzesfmgu35ViSOxqYxwOVo4ylkIS9xP2ClnhD8k2VTbqdc6RRimaPRqxxI3Vf42qWiRBm4WaG3uHls+wOCqQ==", + "license": "MIT", + "dependencies": { + "@uppy/utils": "^7.1.5", + "lodash": "^4.17.21" + }, + "peerDependencies": { + "@uppy/core": "^5.2.0" + } + }, + "node_modules/@uppy/store-default": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@uppy/store-default/-/store-default-5.0.0.tgz", + "integrity": "sha512-hQtCSQ1yGiaval/wVYUWquYGDJ+bpQ7e4FhUUAsRQz1x1K+o7NBtjfp63O9I4Ks1WRoKunpkarZ+as09l02cPw==", + "license": "MIT" + }, + "node_modules/@uppy/tus": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/@uppy/tus/-/tus-5.1.1.tgz", + "integrity": "sha512-316kLQfO5H/uUJIMhBYhBrTpeN0Q+d6ykW3pomCvdTkFGCvg20rF3oH/owE3lf2UZZN7ZqBk+wHO0WlQePoklg==", + "license": "MIT", + "dependencies": { + "@uppy/companion-client": "^5.1.1", + "@uppy/utils": "^7.1.5", + "tus-js-client": "^4.2.3" + }, + "peerDependencies": { + "@uppy/core": "^5.2.0" + } + }, + "node_modules/@uppy/utils": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@uppy/utils/-/utils-7.2.0.tgz", + "integrity": "sha512-6lC246qszMv6bTyl/+QyHwrudgeguWkA94ME1wHn+a6uRAvmtAEaUManIfGqTJfoKvWAiCJqdJPl5xRJjhAloQ==", + "license": "MIT", + "dependencies": { + "lodash": "^4.17.23", + "preact": "^10.26.10" + } + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "license": "MIT" + }, + "node_modules/combine-errors": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/combine-errors/-/combine-errors-3.0.3.tgz", + "integrity": "sha512-C8ikRNRMygCwaTx+Ek3Yr+OuZzgZjduCOfSQBjbM8V3MfgcjSTeto/GXP6PAwKvJz/v15b7GHZvx5rOlczFw/Q==", + "dependencies": { + "custom-error-instance": "2.1.1", + "lodash.uniqby": "4.5.0" + } + }, + "node_modules/custom-error-instance": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/custom-error-instance/-/custom-error-instance-2.1.1.tgz", + "integrity": "sha512-p6JFxJc3M4OTD2li2qaHkDCw9SfMw82Ldr6OC9Je1aXiGfhx2W8p3GaoeaGrPJTUN9NirTM/KTxHWMUdR1rsUg==", + "license": "ISC" + }, + "node_modules/esbuild": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.1.tgz", + "integrity": "sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.28.1", + "@esbuild/android-arm": "0.28.1", + "@esbuild/android-arm64": "0.28.1", + "@esbuild/android-x64": "0.28.1", + "@esbuild/darwin-arm64": "0.28.1", + "@esbuild/darwin-x64": "0.28.1", + "@esbuild/freebsd-arm64": "0.28.1", + "@esbuild/freebsd-x64": "0.28.1", + "@esbuild/linux-arm": "0.28.1", + "@esbuild/linux-arm64": "0.28.1", + "@esbuild/linux-ia32": "0.28.1", + "@esbuild/linux-loong64": "0.28.1", + "@esbuild/linux-mips64el": "0.28.1", + "@esbuild/linux-ppc64": "0.28.1", + "@esbuild/linux-riscv64": "0.28.1", + "@esbuild/linux-s390x": "0.28.1", + "@esbuild/linux-x64": "0.28.1", + "@esbuild/netbsd-arm64": "0.28.1", + "@esbuild/netbsd-x64": "0.28.1", + "@esbuild/openbsd-arm64": "0.28.1", + "@esbuild/openbsd-x64": "0.28.1", + "@esbuild/openharmony-arm64": "0.28.1", + "@esbuild/sunos-x64": "0.28.1", + "@esbuild/win32-arm64": "0.28.1", + "@esbuild/win32-ia32": "0.28.1", + "@esbuild/win32-x64": "0.28.1" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "license": "ISC" + }, + "node_modules/is-network-error": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/is-network-error/-/is-network-error-1.3.2.tgz", + "integrity": "sha512-PhBY86zaxNZUuWP6h13Vu5oFe0XY6/UlKzQnYFELzGVHygP3MxmvTfYSG7GN3aIab/iWudSMgjSnG9Dq+nHrgA==", + "license": "MIT", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/js-base64": { + "version": "3.9.2", + "resolved": "https://registry.npmjs.org/js-base64/-/js-base64-3.9.2.tgz", + "integrity": "sha512-6zayE8QlUdiweYI6cETD/XBSqFcoCUlufn/29PJR99r82x1yDnIprRca0YvAYpAW+ez0GuQkVBC6xG5QkD7OjA==", + "license": "BSD-3-Clause" + }, + "node_modules/lodash": { + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz", + "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==", + "license": "MIT" + }, + "node_modules/lodash._baseiteratee": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/lodash._baseiteratee/-/lodash._baseiteratee-4.7.0.tgz", + "integrity": "sha512-nqB9M+wITz0BX/Q2xg6fQ8mLkyfF7MU7eE+MNBNjTHFKeKaZAPEzEg+E8LWxKWf1DQVflNEn9N49yAuqKh2mWQ==", + "license": "MIT", + "dependencies": { + "lodash._stringtopath": "~4.8.0" + } + }, + "node_modules/lodash._basetostring": { + "version": "4.12.0", + "resolved": "https://registry.npmjs.org/lodash._basetostring/-/lodash._basetostring-4.12.0.tgz", + "integrity": "sha512-SwcRIbyxnN6CFEEK4K1y+zuApvWdpQdBHM/swxP962s8HIxPO3alBH5t3m/dl+f4CMUug6sJb7Pww8d13/9WSw==", + "license": "MIT" + }, + "node_modules/lodash._baseuniq": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/lodash._baseuniq/-/lodash._baseuniq-4.6.0.tgz", + "integrity": "sha512-Ja1YevpHZctlI5beLA7oc5KNDhGcPixFhcqSiORHNsp/1QTv7amAXzw+gu4YOvErqVlMVyIJGgtzeepCnnur0A==", + "license": "MIT", + "dependencies": { + "lodash._createset": "~4.0.0", + "lodash._root": "~3.0.0" + } + }, + "node_modules/lodash._createset": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/lodash._createset/-/lodash._createset-4.0.3.tgz", + "integrity": "sha512-GTkC6YMprrJZCYU3zcqZj+jkXkrXzq3IPBcF/fIPpNEAB4hZEtXU8zp/RwKOvZl43NUmwDbyRk3+ZTbeRdEBXA==", + "license": "MIT" + }, + "node_modules/lodash._root": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/lodash._root/-/lodash._root-3.0.1.tgz", + "integrity": "sha512-O0pWuFSK6x4EXhM1dhZ8gchNtG7JMqBtrHdoUFUWXD7dJnNSUze1GuyQr5sOs0aCvgGeI3o/OJW8f4ca7FDxmQ==", + "license": "MIT" + }, + "node_modules/lodash._stringtopath": { + "version": "4.8.0", + "resolved": "https://registry.npmjs.org/lodash._stringtopath/-/lodash._stringtopath-4.8.0.tgz", + "integrity": "sha512-SXL66C731p0xPDC5LZg4wI5H+dJo/EO4KTqOMwLYCH3+FmmfAKJEZCm6ohGpI+T1xwsDsJCfL4OnhorllvlTPQ==", + "license": "MIT", + "dependencies": { + "lodash._basetostring": "~4.12.0" + } + }, + "node_modules/lodash.throttle": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.throttle/-/lodash.throttle-4.1.1.tgz", + "integrity": "sha512-wIkUCfVKpVsWo3JSZlc+8MB5it+2AN5W8J7YVMST30UrvcQNZ1Okbj+rbVniijTWE6FGYy4XJq/rHkas8qJMLQ==", + "license": "MIT" + }, + "node_modules/lodash.uniqby": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.uniqby/-/lodash.uniqby-4.5.0.tgz", + "integrity": "sha512-IRt7cfTtHy6f1aRVA5n7kT8rgN3N1nH6MOWLcHfpWG2SH19E3JksLK38MktLxZDhlAjCP9jpIXkOnRXlu6oByQ==", + "license": "MIT", + "dependencies": { + "lodash._baseiteratee": "~4.7.0", + "lodash._baseuniq": "~4.6.0" + } + }, + "node_modules/mime-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/mime-match/-/mime-match-1.0.2.tgz", + "integrity": "sha512-VXp/ugGDVh3eCLOBCiHZMYWQaTNUHv2IJrut+yXA6+JbLPXHglHwfS/5A5L0ll+jkCY7fIzRJcH6OIunF+c6Cg==", + "license": "ISC", + "dependencies": { + "wildcard": "^1.1.0" + } + }, + "node_modules/namespace-emitter": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/namespace-emitter/-/namespace-emitter-2.0.1.tgz", + "integrity": "sha512-N/sMKHniSDJBjfrkbS/tpkPj4RAbvW3mr8UAzvlMHyun93XEm83IAvhWtJVHo+RHn/oO8Job5YN4b+wRjSVp5g==", + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "5.1.16", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-5.1.16.tgz", + "integrity": "sha512-kVrnsrJqMR8+oLJnGEmSWw9BivK5mt7H3FZatVRjrc5wGqFYuBxX1yG7+A7Gi5AefkX6t/oCkizcQgpu0cY1dQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.js" + }, + "engines": { + "node": "^18 || >=20" + } + }, + "node_modules/p-retry": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/p-retry/-/p-retry-6.2.1.tgz", + "integrity": "sha512-hEt02O4hUct5wtwg4H4KcWgDdm+l1bOaEy/hWzd8xtXB9BqxTWBBhb+2ImAtH4Cv4rPjV76xN3Zumqk3k3AhhQ==", + "license": "MIT", + "dependencies": { + "@types/retry": "0.12.2", + "is-network-error": "^1.0.0", + "retry": "^0.13.1" + }, + "engines": { + "node": ">=16.17" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/preact": { + "version": "10.29.7", + "resolved": "https://registry.npmjs.org/preact/-/preact-10.29.7.tgz", + "integrity": "sha512-DCHYrK/B10yUD3ZjLfhZ3WIE/9Vf9VFUODcRE2dRomTYDpJk6z6L9wecSfhfE6M9ZTHUdyQkoC46arIDhEV84Q==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/preact" + }, + "peerDependencies": { + "preact-render-to-string": ">=5" + }, + "peerDependenciesMeta": { + "preact-render-to-string": { + "optional": true + } + } + }, + "node_modules/proper-lockfile": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/proper-lockfile/-/proper-lockfile-4.1.2.tgz", + "integrity": "sha512-TjNPblN4BwAWMXU8s9AEz4JmQxnD1NNL7bNOY/AKUzyamc379FWASUhc/K1pL2noVb+XmZKLL68cjzLsiOAMaA==", + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "retry": "^0.12.0", + "signal-exit": "^3.0.2" + } + }, + "node_modules/proper-lockfile/node_modules/retry": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", + "integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==", + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/querystringify": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz", + "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==", + "license": "MIT" + }, + "node_modules/requires-port": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", + "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==", + "license": "MIT" + }, + "node_modules/retry": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz", + "integrity": "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==", + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "license": "ISC" + }, + "node_modules/tus-js-client": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/tus-js-client/-/tus-js-client-4.3.1.tgz", + "integrity": "sha512-ZLeYmjrkaU1fUsKbIi8JML52uAocjEZtBx4DKjRrqzrZa0O4MYwT6db+oqePlspV+FxXJAyFBc/L5gwUi2OFsg==", + "license": "MIT", + "dependencies": { + "buffer-from": "^1.1.2", + "combine-errors": "^3.0.3", + "is-stream": "^2.0.0", + "js-base64": "^3.7.2", + "lodash.throttle": "^4.1.1", + "proper-lockfile": "^4.1.2", + "url-parse": "^1.5.7" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/url-parse": { + "version": "1.5.10", + "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz", + "integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==", + "license": "MIT", + "dependencies": { + "querystringify": "^2.1.1", + "requires-port": "^1.0.0" + } + }, + "node_modules/wildcard": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/wildcard/-/wildcard-1.1.2.tgz", + "integrity": "sha512-DXukZJxpHA8LuotRwL0pP1+rS6CS7FF2qStDDE1C7DDg2rLud2PXRMuEDYIPhgEezwnlHNL4c+N6MfMTjCGTng==", + "license": "MIT" + } + } +} diff --git a/web/upload-page/package.json b/web/upload-page/package.json new file mode 100644 index 0000000..c011c7e --- /dev/null +++ b/web/upload-page/package.json @@ -0,0 +1,20 @@ +{ + "name": "@vidxp/upload-page", + "private": true, + "version": "0.0.0", + "type": "module", + "engines": { + "node": ">=22 <23" + }, + "scripts": { + "build": "node scripts/build.mjs" + }, + "dependencies": { + "@uppy/core": "5.2.0", + "@uppy/golden-retriever": "5.2.1", + "@uppy/tus": "5.1.1" + }, + "devDependencies": { + "esbuild": "0.28.1" + } +} diff --git a/web/upload-page/scripts/build.mjs b/web/upload-page/scripts/build.mjs new file mode 100644 index 0000000..19b3278 --- /dev/null +++ b/web/upload-page/scripts/build.mjs @@ -0,0 +1,104 @@ +import { copyFile, mkdir, readFile, readdir, writeFile } from 'node:fs/promises' +import { dirname, join, resolve } from 'node:path' +import { fileURLToPath } from 'node:url' + +import { build } from 'esbuild' + +const packageRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..') +const repositoryRoot = resolve(packageRoot, '..', '..') +const outputRoot = join(repositoryRoot, 'src', 'vidxp', 'assets', 'upload_page') + +await mkdir(outputRoot, { recursive: true }) + +const bundle = await build({ + entryPoints: [join(packageRoot, 'src', 'app.js')], + outfile: join(outputRoot, 'upload-page.js'), + bundle: true, + charset: 'utf8', + format: 'esm', + legalComments: 'none', + metafile: true, + minify: true, + sourcemap: false, + target: ['es2020'], +}) + +await copyFile( + join(packageRoot, 'src', 'index.html'), + join(outputRoot, 'index.html'), +) + +const lock = JSON.parse( + await readFile(join(packageRoot, 'package-lock.json'), 'utf8'), +) +const notices = [ + 'VidXP upload page third-party notices', + '', + 'This file covers JavaScript packages bundled into upload-page.js.', + 'Build-only development packages are not included.', +] + +const bundledPackagePaths = new Set() +for (const inputPath of Object.keys(bundle.metafile.inputs)) { + const normalized = inputPath.replaceAll('\\', '/') + const marker = 'node_modules/' + const markerIndex = normalized.lastIndexOf(marker) + if (markerIndex === -1) continue + + const packageParts = normalized.slice(markerIndex + marker.length).split('/') + const packageName = packageParts[0].startsWith('@') + ? packageParts.slice(0, 2).join('/') + : packageParts[0] + bundledPackagePaths.add(`node_modules/${packageName}`) +} + +for (const [packagePath, locked] of Object.entries(lock.packages)) { + if ( + !packagePath || + locked.dev || + !locked.version || + !bundledPackagePaths.has(packagePath.replaceAll('\\', '/')) + ) { + continue + } + + const installedRoot = join(packageRoot, packagePath) + const manifest = JSON.parse( + await readFile(join(installedRoot, 'package.json'), 'utf8'), + ) + const entries = await readdir(installedRoot) + const licenseFiles = entries + .filter((name) => /^(licen[cs]e|notice)(\.|$)/i.test(name)) + .sort((left, right) => left.localeCompare(right)) + + notices.push('', '='.repeat(72), '') + notices.push(`${manifest.name}@${locked.version}`) + notices.push(`Declared license: ${manifest.license ?? locked.license ?? 'unknown'}`) + + if (licenseFiles.length > 0) { + for (const filename of licenseFiles) { + notices.push('', `--- ${filename} ---`, '') + notices.push((await readFile(join(installedRoot, filename), 'utf8')).trim()) + } + } else { + const readmeName = entries.find((name) => /^readme(\.|$)/i.test(name)) + const readme = readmeName + ? await readFile(join(installedRoot, readmeName), 'utf8') + : '' + const licenseHeading = readme.search(/^#{1,3} licen[cs]e(?:\(s\))?\s*$/im) + const embeddedLicense = + licenseHeading === -1 ? '' : readme.slice(licenseHeading).trim() + if (embeddedLicense.length < 200) { + throw new Error(`No complete license notice found for ${manifest.name}`) + } + notices.push('', `--- ${readmeName} license section ---`, '') + notices.push(embeddedLicense) + } +} + +notices.push('') +await writeFile( + join(outputRoot, 'THIRD_PARTY_NOTICES.txt'), + notices.join('\n'), + 'utf8', +) diff --git a/web/upload-page/src/app.css b/web/upload-page/src/app.css new file mode 100644 index 0000000..f99e646 --- /dev/null +++ b/web/upload-page/src/app.css @@ -0,0 +1,211 @@ +:root { + color-scheme: light dark; + font-family: + Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", + sans-serif; + background: #08101f; + color: #e8eefc; +} + +* { + box-sizing: border-box; +} + +body { + min-height: 100vh; + margin: 0; + background: + radial-gradient(circle at top left, #1b3261 0, transparent 38rem), + #08101f; +} + +button, +input { + font: inherit; +} + +.shell { + width: min(48rem, calc(100% - 2rem)); + margin: 0 auto; + padding: 3rem 0 5rem; +} + +header { + margin-bottom: 2rem; +} + +.eyebrow, +.label { + color: #96b6ff; + font-size: 0.78rem; + font-weight: 700; + letter-spacing: 0.08em; + text-transform: uppercase; +} + +h1, +h2, +p { + margin-top: 0; +} + +h1 { + margin-bottom: 0.65rem; + font-size: clamp(2rem, 8vw, 3.6rem); + line-height: 1; +} + +h2 { + font-size: 1.15rem; +} + +.lede, +.hint, +#next-action { + color: #b6c4df; + line-height: 1.55; +} + +.card { + margin-top: 1rem; + padding: 1.4rem; + border: 1px solid #2c3d60; + border-radius: 1rem; + background: #101a2dbf; + box-shadow: 0 1rem 3rem #0004; +} + +.facts { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 0.85rem 1.25rem; + margin: 1rem 0; +} + +.facts div { + min-width: 0; +} + +.facts dt { + margin-bottom: 0.2rem; + color: #8fa0bd; + font-size: 0.8rem; +} + +.facts dd { + margin: 0; + overflow-wrap: anywhere; + font-weight: 650; +} + +.compact { + grid-template-columns: 1fr; +} + +.field-label { + display: block; + margin-bottom: 0.45rem; + color: #b6c4df; + font-weight: 700; +} + +button, +input::file-selector-button { + display: inline-flex; + min-height: 2.75rem; + align-items: center; + justify-content: center; + padding: 0.7rem 1rem; + border: 1px solid #6d96ee; + border-radius: 0.7rem; + background: #386ee0; + color: white; + cursor: pointer; + font-weight: 700; +} + +button:focus-visible, +input:focus-visible { + outline: 3px solid #a9c3ff; + outline-offset: 3px; +} + +input[type="file"] { + display: block; + width: 100%; + margin: 0.8rem 0; + color: #b6c4df; +} + +input::file-selector-button { + margin-right: 0.7rem; +} + +.selected-file { + min-height: 1.5rem; + overflow-wrap: anywhere; +} + +progress { + width: 100%; + height: 1rem; + accent-color: #6392f5; +} + +.state { + min-height: 1.5rem; + margin: 0.8rem 0; +} + +.state.error { + color: #ffaaa2; +} + +.state.success { + color: #91e6bc; +} + +.actions { + display: flex; + flex-wrap: wrap; + gap: 0.65rem; +} + +button.secondary { + border-color: #52678c; + background: #1b2943; +} + +button:disabled, +input:disabled { + cursor: not-allowed; + opacity: 0.5; +} + +noscript { + display: block; + margin-top: 1rem; + color: #ffaaa2; +} + +@media (max-width: 36rem) { + .shell { + padding-top: 1.5rem; + } + + .facts { + grid-template-columns: 1fr; + } + + .actions button { + width: 100%; + } +} + +@media (prefers-reduced-motion: reduce) { + *, + *::before, + *::after { + scroll-behavior: auto !important; + } +} diff --git a/web/upload-page/src/app.js b/web/upload-page/src/app.js new file mode 100644 index 0000000..722e248 --- /dev/null +++ b/web/upload-page/src/app.js @@ -0,0 +1,542 @@ +import Uppy from '@uppy/core' +import GoldenRetriever from '@uppy/golden-retriever' +import Tus from '@uppy/tus' + +import './app.css' + +const elements = { + summary: document.querySelector('#summary'), + expectedFilename: document.querySelector('#expected-filename'), + expectedSize: document.querySelector('#expected-size'), + maximumSize: document.querySelector('#maximum-size'), + expiresAt: document.querySelector('#expires-at'), + fileInput: document.querySelector('#file-input'), + selectedFile: document.querySelector('#selected-file'), + progress: document.querySelector('#upload-progress'), + uploadState: document.querySelector('#upload-state'), + uploadButton: document.querySelector('#upload-button'), + pauseButton: document.querySelector('#pause-button'), + retryButton: document.querySelector('#retry-button'), + intentState: document.querySelector('#intent-state'), + nextAction: document.querySelector('#next-action'), + intentId: document.querySelector('#intent-id'), + jobId: document.querySelector('#job-id'), + mediaId: document.querySelector('#media-id'), +} + +const TERMINAL_STATES = new Set(['ready', 'failed', 'expired']) +const TRANSFER_STATES = new Set(['pending', 'accepted', 'uploading']) +const POLL_INTERVAL_MS = 2000 + +let uppy +let expected +let creationUrl +let resumeUrl +let creationGrant +let pollTimer +let pollInFlight = false +let currentIntentState = 'pending' + +function setText(element, value) { + element.textContent = value == null || value === '' ? '—' : String(value) +} + +function setUploadMessage(message, kind = '') { + setText(elements.uploadState, message) + elements.uploadState.classList.remove('error', 'success') + if (kind) elements.uploadState.classList.add(kind) +} + +function formatBytes(value) { + if (!Number.isFinite(value)) return '—' + if (value === 0) return '0 bytes' + + const units = ['bytes', 'KiB', 'MiB', 'GiB', 'TiB'] + const exponent = Math.min( + Math.floor(Math.log(value) / Math.log(1024)), + units.length - 1, + ) + const amount = value / 1024 ** exponent + const digits = exponent === 0 || amount >= 10 ? 0 : 1 + return `${amount.toFixed(digits)} ${units[exponent]}` +} + +function formatDate(value) { + if (!value) return '—' + const date = new Date(value) + if (Number.isNaN(date.getTime())) return String(value) + return new Intl.DateTimeFormat(undefined, { + dateStyle: 'medium', + timeStyle: 'short', + }).format(date) +} + +function readCapabilityFragment() { + const fragment = window.location.hash.slice(1) + if (!fragment) return null + + const parameters = new URLSearchParams(fragment) + const named = parameters.get('capability') + if (named) return named + + if (!fragment.includes('=')) { + try { + return decodeURIComponent(fragment) + } catch { + return null + } + } + return null +} + +function clearFragment() { + window.history.replaceState( + null, + document.title, + `${window.location.pathname}${window.location.search}`, + ) +} + +function apiUrl(relative) { + const pageBase = new URL(`${window.location.pathname}/`, window.location.origin) + return new URL(relative.replace(/^\.\//, ''), pageBase).href +} + +function safeServerMessage(payload, fallback) { + const candidate = + payload?.error?.message ?? + payload?.error?.detail?.message ?? + payload?.message ?? + fallback + return typeof candidate === 'string' && candidate.length <= 300 + ? candidate + : fallback +} + +async function requestJson(url, options = {}) { + const response = await fetch(url, { + cache: 'no-store', + credentials: 'same-origin', + ...options, + headers: { + Accept: 'application/json', + ...(options.body ? { 'Content-Type': 'application/json' } : {}), + ...options.headers, + }, + }) + + let payload + try { + payload = await response.json() + } catch { + payload = null + } + + if (!response.ok) { + throw new Error( + safeServerMessage(payload, `VidXP rejected the request (${response.status}).`), + ) + } + return payload ?? {} +} + +function statusRecord(envelope) { + const status = envelope?.status ?? envelope ?? {} + return status.intent ?? status +} + +function firstDefined(record, names) { + for (const name of names) { + if (record?.[name] != null) return record[name] + } + return null +} + +function expectedRecord(envelope) { + const record = statusRecord(envelope) + const byteSize = Number( + firstDefined(record, ['byte_size', 'declared_byte_size', 'size']), + ) + const maximumBytes = Number( + firstDefined(record, [ + 'configured_maximum_bytes', + 'maximum_bytes', + 'max_bytes', + ]), + ) + return { + intentId: firstDefined(record, ['intent_id', 'id']), + filename: firstDefined(record, [ + 'original_filename', + 'filename', + 'declared_filename', + ]), + byteSize, + maximumBytes, + expiresAt: firstDefined(record, ['expires_at', 'expiry', 'expires']), + } +} + +function renderExpectedFile() { + setText(elements.expectedFilename, expected.filename) + setText(elements.expectedSize, formatBytes(expected.byteSize)) + setText(elements.maximumSize, formatBytes(expected.maximumBytes)) + setText(elements.expiresAt, formatDate(expected.expiresAt)) + setText( + elements.summary, + `Select ${expected.filename} (${formatBytes(expected.byteSize)}) to upload directly to VidXP.`, + ) +} + +function statusGuidance(record) { + return firstDefined(record, [ + 'next_action', + 'guidance', + 'message', + 'status_message', + ]) +} + +function applyStatus(envelope) { + const record = statusRecord(envelope) + const state = String(record.state ?? record.status ?? 'pending').toLowerCase() + currentIntentState = state + + setText(elements.intentState, state) + setText(elements.intentId, firstDefined(record, ['intent_id', 'id'])) + setText(elements.jobId, record.job_id) + setText(elements.mediaId, record.media_id) + setText( + elements.nextAction, + statusGuidance(record) ?? 'VidXP is waiting for the upload to continue.', + ) + + if (record.media_id) { + setUploadMessage('The video is ready in VidXP.', 'success') + } else if (state === 'failed' || state === 'expired') { + setUploadMessage( + state === 'expired' + ? 'This upload handoff has expired.' + : 'VidXP could not finish importing this upload.', + 'error', + ) + } + updateControls() + return state +} + +function activeFile() { + return uppy?.getFiles()[0] ?? null +} + +function renderFile(file = activeFile()) { + if (!file) { + setText(elements.selectedFile, 'No file selected.') + elements.progress.value = 0 + elements.progress.textContent = '0%' + updateControls() + return + } + + const suffix = file.isGhost + ? ' — re-select this file to recover the interrupted upload.' + : '' + setText( + elements.selectedFile, + `${file.name} · ${formatBytes(file.size)}${suffix}`, + ) + + const percentage = Number(file.progress?.percentage ?? 0) + elements.progress.value = Number.isFinite(percentage) ? percentage : 0 + elements.progress.textContent = `${Math.round(elements.progress.value)}%` + + if (file.isGhost) { + setUploadMessage( + 'The resumable upload was found. Re-select the same local file to continue.', + ) + } + updateControls() +} + +function updateControls() { + const file = activeFile() + const terminal = TERMINAL_STATES.has(currentIntentState) + const transferAllowed = TRANSFER_STATES.has(currentIntentState) + const usableFile = file && !file.isGhost + const complete = Boolean(file?.progress?.uploadComplete) + const uploading = Boolean(file?.progress?.uploadStarted) && !complete + + elements.fileInput.disabled = !uppy || !transferAllowed || terminal || complete + elements.uploadButton.disabled = + !usableFile || !transferAllowed || terminal || complete || uploading + elements.pauseButton.disabled = + !usableFile || !transferAllowed || terminal || complete || !uploading + elements.retryButton.disabled = + !usableFile || !transferAllowed || terminal || !file?.error + setText(elements.pauseButton, file?.isPaused ? 'Resume' : 'Pause') +} + +function validateExpectedFile(file) { + if (file.name !== expected.filename) { + return `Choose the expected file named ${expected.filename}.` + } + if (file.size !== expected.byteSize) { + return `The selected file is ${formatBytes(file.size)}; VidXP expects ${formatBytes(expected.byteSize)}.` + } + return null +} + +function normalizedUrl(value) { + const url = new URL(value, window.location.href) + url.hash = '' + return url.href +} + +function isCreationRequest(request) { + return ( + request.getMethod().toUpperCase() === 'POST' && + normalizedUrl(request.getURL()) === creationUrl + ) +} + +function responseGrant(payload) { + return payload?.grant ?? payload?.creation_grant ?? payload?.capability ?? null +} + +async function getCreationGrant() { + if (creationGrant) return creationGrant + + const payload = await requestJson(apiUrl('./creation-grant'), { + method: 'POST', + body: '{}', + }) + if (payload.status) applyStatus(payload) + const grant = responseGrant(payload) + if (typeof grant !== 'string' || !grant) { + throw new Error('VidXP did not issue an upload creation grant.') + } + creationGrant = grant + return grant +} + +function recoveryLifetime() { + const expiry = new Date(expected.expiresAt).getTime() + if (!Number.isFinite(expiry)) return 60 * 60 * 1000 + return Math.max(1000, expiry - Date.now()) +} + +function safeUploadError(error) { + const code = error?.originalResponse?.getHeader?.('X-VidXP-Error') + if (typeof code === 'string' && /^[a-z0-9_-]{1,80}$/i.test(code)) { + return `VidXP rejected the upload (${code}).` + } + return 'The transfer was interrupted. Check the connection and retry.' +} + +function applyResumeUrl(value) { + if (!value || !uppy) return + resumeUrl = normalizedUrl(value) + uppy.getPlugin('Tus')?.setOptions({ uploadUrl: resumeUrl }) + + const file = activeFile() + if (file && file.tus?.uploadUrl !== resumeUrl) { + uppy.setFileState(file.id, { + tus: { ...file.tus, uploadUrl: resumeUrl }, + }) + } +} + +function configureUppy() { + const scopedId = `vidxp-upload-${String(expected.intentId).replace(/[^a-z0-9_-]/gi, '_')}` + uppy = new Uppy({ + id: scopedId, + autoProceed: false, + meta: { intent_id: String(expected.intentId) }, + restrictions: { + maxNumberOfFiles: 1, + minFileSize: expected.byteSize, + maxFileSize: expected.byteSize, + }, + onBeforeFileAdded(file) { + const problem = validateExpectedFile(file) + if (problem) setUploadMessage(problem, 'error') + return problem == null + }, + }) + + uppy.on('file-added', renderFile) + uppy.on('file-removed', () => renderFile()) + uppy.on('restriction-failed', () => { + setUploadMessage('The selected file does not match this upload handoff.', 'error') + }) + uppy.on('upload-progress', (file) => renderFile(file)) + uppy.on('upload-pause', (file, isPaused) => { + setUploadMessage(isPaused ? 'Upload paused.' : 'Upload resumed.') + renderFile(file) + }) + uppy.on('upload-error', (file, error) => { + creationGrant = null + setUploadMessage(safeUploadError(error), 'error') + if (file) renderFile(file) + }) + uppy.on('upload-success', (file) => { + setUploadMessage('Transfer complete. VidXP is validating and importing the video.', 'success') + renderFile(file) + scheduleStatusPoll(0) + }) + uppy.on('restored', () => { + renderFile() + scheduleStatusPoll(0) + }) + + uppy.use(Tus, { + endpoint: creationUrl, + ...(resumeUrl ? { uploadUrl: normalizedUrl(resumeUrl) } : {}), + allowedMetaFields: ['intent_id'], + parallelUploads: 1, + overridePatchMethod: false, + uploadDataDuringCreation: false, + withCredentials: false, + removeFingerprintOnSuccess: true, + async onBeforeRequest(request) { + if (!isCreationRequest(request)) return + const grant = await getCreationGrant() + request.setHeader('Authorization', `VidXP-Handoff ${grant}`) + }, + onAfterResponse(request) { + if (isCreationRequest(request)) creationGrant = null + }, + }) + uppy.use(GoldenRetriever, { + id: `GoldenRetriever:${scopedId}`, + expires: recoveryLifetime(), + serviceWorker: false, + }) + + renderFile() +} + +async function pollStatus() { + if (pollInFlight) return + pollInFlight = true + try { + const payload = await requestJson(apiUrl('./status')) + applyResumeUrl(payload.resume_url) + const state = applyStatus(payload) + if (!TERMINAL_STATES.has(state)) scheduleStatusPoll(POLL_INTERVAL_MS) + } catch { + setText( + elements.nextAction, + 'The status check failed. This page will try again automatically.', + ) + scheduleStatusPoll(POLL_INTERVAL_MS) + } finally { + pollInFlight = false + } +} + +function scheduleStatusPoll(delay = POLL_INTERVAL_MS) { + window.clearTimeout(pollTimer) + pollTimer = window.setTimeout(pollStatus, delay) +} + +async function bootstrap() { + const capability = readCapabilityFragment() + clearFragment() + const payload = capability + ? await requestJson(apiUrl('./bootstrap'), { + method: 'POST', + body: JSON.stringify({ capability }), + }) + : await requestJson(apiUrl('./status')) + expected = expectedRecord(payload) + if ( + !expected.intentId || + !expected.filename || + !Number.isFinite(expected.byteSize) || + expected.byteSize < 0 + ) { + throw new Error('VidXP returned an incomplete upload handoff.') + } + if (!Number.isFinite(expected.maximumBytes)) { + expected.maximumBytes = expected.byteSize + } + if (!payload.creation_url) { + throw new Error('VidXP did not provide the tus creation endpoint.') + } + + creationUrl = normalizedUrl(payload.creation_url) + resumeUrl = payload.resume_url ? normalizedUrl(payload.resume_url) : null + creationGrant = responseGrant(payload) + + renderExpectedFile() + applyStatus(payload) + configureUppy() + scheduleStatusPoll(0) +} + +elements.fileInput.addEventListener('change', () => { + const file = elements.fileInput.files?.[0] + elements.fileInput.value = '' + if (!file || !uppy) return + + const problem = validateExpectedFile(file) + if (problem) { + setUploadMessage(problem, 'error') + return + } + + try { + uppy.addFile({ + source: 'VidXP upload page', + name: file.name, + type: file.type || 'application/octet-stream', + data: file, + meta: { intent_id: String(expected.intentId) }, + }) + setUploadMessage('The expected file is ready to upload.') + } catch { + setUploadMessage('The file could not be selected. Re-open it and try again.', 'error') + } +}) + +elements.uploadButton.addEventListener('click', async () => { + setUploadMessage('Starting the resumable upload…') + updateControls() + try { + const result = await uppy.upload() + if (result?.failed?.length) { + setUploadMessage('The transfer did not finish. Retry when ready.', 'error') + } + } catch { + setUploadMessage('The transfer did not finish. Retry when ready.', 'error') + } + updateControls() +}) + +elements.pauseButton.addEventListener('click', () => { + const file = activeFile() + if (file) uppy.pauseResume(file.id) + updateControls() +}) + +elements.retryButton.addEventListener('click', async () => { + const file = activeFile() + if (!file) return + creationGrant = null + setUploadMessage('Retrying the resumable upload…') + try { + await uppy.retryUpload(file.id) + } catch { + setUploadMessage('The retry did not finish. Check the connection and try again.', 'error') + } + updateControls() +}) + +bootstrap().catch((error) => { + const message = error instanceof Error ? error.message : 'The upload page could not start.' + setText(elements.summary, message) + setUploadMessage(message, 'error') + setText(elements.nextAction, 'Request a new upload handoff from VidXP.') + updateControls() +}) diff --git a/web/upload-page/src/index.html b/web/upload-page/src/index.html new file mode 100644 index 0000000..cf4657a --- /dev/null +++ b/web/upload-page/src/index.html @@ -0,0 +1,69 @@ + + + + + + + Upload video · VidXP + + + + +
+
+

VidXP media intake

+

Upload your video

+

Opening the secure upload handoff…

+
+ +
+

Expected file

+
+
Filename
+
Size
+
Maximum
+
Expires
+
+

+ VidXP checks the selected name and size before transfer. After upload, + the server probes and validates the media before it becomes available. +

+
+ +
+

Choose and upload

+ + +

No file selected.

+ + 0% +

+ Waiting for a valid handoff. +

+ +
+ + + +
+
+ +
+

VidXP status

+

State

+

The page will refresh the server-owned status automatically.

+
+
Upload intent
+
Import job
+
Media ID
+
+
+ + +
+ + From 2e3ac100178195bc5edd9c01234c14f068a39f83 Mon Sep 17 00:00:00 2001 From: Talha Date: Sat, 1 Aug 2026 16:49:27 +0500 Subject: [PATCH 02/21] fix: resolve upload handoff review feedback --- compose.coolify.yaml | 1 + docs/architecture/platform.md | 58 +-- docs/deployment/coolify.md | 5 +- src/vidxp/api.py | 3 +- src/vidxp/api_middleware.py | 21 +- src/vidxp/application_models.py | 65 +++- .../upload_page/THIRD_PARTY_NOTICES.txt | 237 ++++++++++++ src/vidxp/assets/upload_page/index.html | 16 +- src/vidxp/assets/upload_page/upload-page.css | 2 +- src/vidxp/assets/upload_page/upload-page.js | 19 +- src/vidxp/core/uploads.py | 46 ++- src/vidxp/infrastructure/sql_catalog.py | 132 +++++-- src/vidxp/infrastructure/sql_tables.py | 3 +- src/vidxp/mcp.py | 115 +++++- .../versions/20260801_01_upload_handoffs.py | 2 + src/vidxp/settings.py | 177 ++++++--- src/vidxp/upload_service.py | 340 +++++++----------- tests/fixtures/mcp_upload_server.py | 64 ++++ tests/test_mcp.py | 181 +++++++++- tests/test_packaging.py | 167 ++++++--- tests/test_settings.py | 12 + tests/test_tusd_hooks.py | 5 +- tests/test_tusd_live.py | 79 ++-- tests/test_upload_page.py | 12 +- tests/test_upload_service.py | 32 +- uv.lock | 34 +- web/upload-page/package-lock.json | 103 ++++++ web/upload-page/package.json | 1 + web/upload-page/scripts/build.mjs | 8 + web/upload-page/src/app.css | 75 +--- web/upload-page/src/app.js | 153 ++------ web/upload-page/src/index.html | 16 +- 32 files changed, 1456 insertions(+), 728 deletions(-) create mode 100644 tests/fixtures/mcp_upload_server.py diff --git a/compose.coolify.yaml b/compose.coolify.yaml index 7a2d5a9..f1c2694 100644 --- a/compose.coolify.yaml +++ b/compose.coolify.yaml @@ -14,6 +14,7 @@ x-vidxp-environment: &vidxp-environment VIDXP_UPLOAD_CLEANUP_TOKEN: ${VIDXP_UPLOAD_CLEANUP_TOKEN:?Set a cleanup token of at least 32 characters} VIDXP_UPLOAD_HANDOFF_PUBLIC_URL: ${VIDXP_UPLOAD_HANDOFF_PUBLIC_URL:?Set the HTTPS VidXP URL ending in /upload-handoff} VIDXP_UPLOAD_HANDOFF_SECRET: ${VIDXP_UPLOAD_HANDOFF_SECRET:?Set a dedicated handoff secret of at least 32 characters} + VIDXP_UPLOAD_CORS_ORIGIN_REGEX: ${VIDXP_UPLOAD_CORS_ORIGIN_REGEX:?Set an anchored policy that permits the handoff origin} VIDXP_UPLOAD_QUARANTINE_ROOT: /var/lib/vidxp/uploads VIDXP_UPLOAD_MAX_BYTES: ${VIDXP_UPLOAD_MAX_BYTES:-53687091200} VIDXP_UPLOAD_QUOTA_BYTES: ${VIDXP_UPLOAD_QUOTA_BYTES:-107374182400} diff --git a/docs/architecture/platform.md b/docs/architecture/platform.md index cf28a00..a09e77a 100644 --- a/docs/architecture/platform.md +++ b/docs/architecture/platform.md @@ -327,21 +327,34 @@ Remote upload is a four-stage protocol: Remote MCP adds a user-completable handoff in front of the same protocol. The `create_media_upload` write tool idempotently creates the authoritative upload -intent and returns an HTTPS page whose fragment contains a short-lived HMAC -capability bound to that intent, repository, declared size, purpose, version, -and expiry. The page exchanges the fragment for a digest-backed `HttpOnly`, -`Secure`, `SameSite=Strict` cookie, then requests a separate one-time creation -grant for the exact tus `POST`. Neither the raw handoff capability nor session -secret is stored, and the user's MCP bearer is never exposed to the browser. -The existing bearer-authenticated HTTP/tus route remains available. - -The packaged page uses Uppy Core, Tus, and Golden Retriever with native controls. -It validates the expected filename and byte size, limits tus metadata to the -intent identifier, supports progress/pause/resume/retry, and recovers interrupted -uploads from browser storage. It does not choose a tus chunk size, so a few-MiB -file follows the same resumable path rather than a separate MCP or small-upload -path. Scripts and styles are self-hosted under a strict CSP; the handoff API is -same-origin and tus CORS remains restricted to configured origins. +intent and returns an HTTPS page whose fragment contains a short-lived HS256 JWT +capability bound to that intent, repository, declared size, purpose, issuer, +audience, and expiry. PyJWT supplies the maintained token format and verification +instead of a private framing protocol. The page exchanges the fragment for a +digest-backed `HttpOnly`, `Secure`, `SameSite=Strict` cookie, then requests a +separate one-time creation grant for the exact tus `POST`. That grant is an opaque +384-bit verifier: only its digest, expiry, and consumption time are stored, +because its mandatory database round trip already provides expiry and replay +state and does not need a second signed-token format. Neither raw credential is +stored, and the user's MCP bearer is never exposed to the browser. The existing +bearer-authenticated HTTP/tus route remains available. + +The packaged page uses Uppy Dashboard, Tus, and Golden Retriever. Dashboard owns +selection, restrictions, progress, pause/resume, retry, accessibility, and ghost +recovery; custom UI remains only for VidXP's server-side import state and next +action. The measured self-hosted production assets grow from 40.1 KiB to 102.9 +KiB gzip by adopting Dashboard, a deliberate cost for removing duplicated upload +controls and accessibility behavior. The page validates the expected filename +and byte size, limits tus metadata to the intent identifier, and does not choose +a tus chunk size, so a few-MiB file follows the same resumable path rather than a +separate MCP or small-upload path. Scripts and styles are self-hosted under a +strict CSP; the handoff API is same-origin and tus CORS remains restricted to a +startup-validated origin policy. + +When the MCP client advertises `elicitation.url`, `create_media_upload` also uses +URL-mode elicitation and records accept, decline, or cancel in its structured +result. The structured `upload_page_url` is always retained for clients without +URL mode or for a later manual attempt. Application responses carrying upload URLs use `private, no-store` and `no-referrer`; hook payloads and MCP results do not persist them. The opaque path is @@ -357,10 +370,12 @@ metadata. Only the tus upload route is public. A completed upload is not a `MediaAsset` until durable probe/import succeeds. The supported server topology uses tusd filestore on a named quarantine volume -shared read-only with the hook service and worker. Managed media and artifacts use -the stack's named content volume. The whole deployment is single-node and one -deployed stack is one repository boundary. Clients upload directly to tusd; -FastAPI does not proxy large video bodies. +shared read-only with the hook service and worker. The API intentionally does not +mount that volume: it discovers created resumable resources with an internal tusd +`HEAD` and combines that result with the authoritative intent state in Postgres. +Managed media and artifacts use the stack's named content volume. The whole +deployment is single-node and one deployed stack is one repository boundary. +Clients upload directly to tusd; FastAPI does not proxy large video bodies. S3-compatible storage is deferred and not implemented. If revisited, it would apply only to upload quarantine, source media, and generated artifacts—never to @@ -918,7 +933,8 @@ Use the official MCP Python SDK v2 high-level server. Primary remote transport: - Streamable HTTP -- explicit `stateless_http=True` +- stateful transport sessions so capability-negotiated URL elicitation has a + request back-channel - `json_response=False` so disconnect cancellation uses the streaming/SSE path - application state persisted outside MCP sessions - explicit lifecycle through the application composition root @@ -976,7 +992,7 @@ sibling adapters. The MCP ASGI app is created with: - Streamable HTTP path `/mcp` -- stateless HTTP enabled +- stateful HTTP sessions enabled for server-to-client URL elicitation - JSON-only responses disabled - configured MCP body limit - explicit transport-security settings diff --git a/docs/deployment/coolify.md b/docs/deployment/coolify.md index a6c9afe..54e0b8a 100644 --- a/docs/deployment/coolify.md +++ b/docs/deployment/coolify.md @@ -27,7 +27,7 @@ VIDXP_PUBLIC_API_HOST=api.example.com VIDXP_UPLOAD_PUBLIC_ENDPOINT=https://uploads.example.com/uploads/ VIDXP_UPLOAD_HANDOFF_PUBLIC_URL=https://api.example.com/upload-handoff VIDXP_UPLOAD_HANDOFF_SECRET= -VIDXP_UPLOAD_CORS_ORIGIN_REGEX=^https://app\.example\.com$ +VIDXP_UPLOAD_CORS_ORIGIN_REGEX=^https://(api|app)\.example\.com$ ``` `VIDXP_UPLOAD_HANDOFF_PUBLIC_URL` must be the externally reachable HTTPS API @@ -57,7 +57,8 @@ an upstream proxy. Video bytes do not travel through MCP. A remote MCP client calls `create_media_upload` with the expected filename, size, and MIME type and gives the returned HTTPS page to the user. The page validates that exact file and -uploads it directly to tusd with pause, resume, retry, and browser recovery. +uploads it directly to tusd through Uppy Dashboard with pause, resume, retry, +accessible controls, and browser recovery. The client polls `get_media_upload`; once it returns a `media_id`, pass that ID to `start_indexing`. diff --git a/src/vidxp/api.py b/src/vidxp/api.py index 533767f..ee5e7d1 100644 --- a/src/vidxp/api.py +++ b/src/vidxp/api.py @@ -30,7 +30,8 @@ auto_error=False, scheme_name="BearerAuth", description=( - "Bearer access token. Authentication is enforced once by the server middleware." + "Bearer access token. Authentication is enforced once by the " + "server middleware." ), ) diff --git a/src/vidxp/api_middleware.py b/src/vidxp/api_middleware.py index cc5a693..143362d 100644 --- a/src/vidxp/api_middleware.py +++ b/src/vidxp/api_middleware.py @@ -117,7 +117,10 @@ async def __call__( receive: Receive, send: Send, ) -> None: - if scope["type"] == "http" and str(scope.get("path", "")).startswith("/api/"): + if ( + scope["type"] == "http" + and str(scope.get("path", "")).startswith("/api/") + ): headers = Headers(scope=scope) origin = headers.get("origin") requested_method = headers.get("access-control-request-method") @@ -130,7 +133,10 @@ async def __call__( ).split(",") if value.strip() } - if "*" not in self.allow_origins and origin not in self.allow_origins: + if ( + "*" not in self.allow_origins + and origin not in self.allow_origins + ): await public_error_response( ErrorDetail( code="cors_origin_forbidden", @@ -180,7 +186,11 @@ async def __call__( return host = _host_name(Headers(scope=scope).get("host", "")) if any( - host == pattern or (pattern.startswith("*.") and host.endswith(pattern[1:])) + host == pattern + or ( + pattern.startswith("*.") + and host.endswith(pattern[1:]) + ) for pattern in self.allowed_hosts ): await self.app(scope, receive, send) @@ -202,7 +212,10 @@ def _host_name(header: str) -> str: if closing < 0: return "" remainder = value[closing + 1 :] - if remainder and (not remainder.startswith(":") or not remainder[1:].isdigit()): + if remainder and ( + not remainder.startswith(":") + or not remainder[1:].isdigit() + ): return "" return value[1:closing] if value.count(":") == 1: diff --git a/src/vidxp/application_models.py b/src/vidxp/application_models.py index 95ba663..f92e121 100644 --- a/src/vidxp/application_models.py +++ b/src/vidxp/application_models.py @@ -172,7 +172,8 @@ def __init__( super().__init__( "dependency_unavailable", ErrorCategory.unavailable, - f"Dependencies for the {label} capability are unavailable. {install_hint}", + f"Dependencies for the {label} capability are unavailable. " + f"{install_hint}", details={ "capabilities": list(capabilities), "install_hint": install_hint, @@ -191,7 +192,9 @@ def __init__(self, capability: str) -> None: f"`vidxp prepare --modalities {modality}` before retrying.", details={ "capability": capability, - "remediation": (f"vidxp prepare --modalities {modality}"), + "remediation": ( + f"vidxp prepare --modalities {modality}" + ), }, ) @@ -210,7 +213,8 @@ def __init__( modality = capability.split(".", 1)[0] remediation = f"vidxp prepare --modalities {modality}" retry_message = ( - "Partial files were kept and the next preparation attempt will resume them." + "Partial files were kept and the next preparation attempt will " + "resume them." if resumable else "The next preparation attempt will restart this file." ) @@ -307,7 +311,9 @@ class CapabilitySummary(ApplicationModel): supports_indexing: bool prepares_models: bool roles: tuple[CapabilityRole, ...] = () - identity_mode: CapabilityIdentityMode = CapabilityIdentityMode.not_applicable + identity_mode: CapabilityIdentityMode = ( + CapabilityIdentityMode.not_applicable + ) provenance: CapabilityProvenance | None = None @@ -492,8 +498,13 @@ def _canonicalize_scene_sampling(cls, value: Any) -> Any: @model_validator(mode="after") def _scene_sampling_requires_scene(self) -> "CreateIndexCommand": - if self.scene_sample_fps is not None and "scene" not in self.modalities: - raise ValueError("scene_sample_fps requires the scene modality.") + if ( + self.scene_sample_fps is not None + and "scene" not in self.modalities + ): + raise ValueError( + "scene_sample_fps requires the scene modality." + ) return self @@ -510,7 +521,9 @@ class RemoveIndexCommand(ApplicationModel): class Artifact(ApplicationModel): - schema_version: Literal[ARTIFACT_SCHEMA_VERSION] = ARTIFACT_SCHEMA_VERSION + schema_version: Literal[ARTIFACT_SCHEMA_VERSION] = ( + ARTIFACT_SCHEMA_VERSION + ) artifact_id: ArtifactId media_id: MediaId generation_id: IndexGenerationId | None = None @@ -541,7 +554,9 @@ class SnippetProfile(StrEnum): class CreateSnippetCommand(ApplicationModel): media_id: MediaId = Field( - description=("Source video ID, normally copied from a search or query result.") + description=( + "Source video ID, normally copied from a search or query result." + ) ) start_seconds: float = Field( ge=0, @@ -598,7 +613,9 @@ class IndexStatusSummary(ApplicationModel): def _validate_media_id_window(self) -> "IndexStatusSummary": if self.media_count < len(self.media_ids): raise ValueError("media_count must cover every returned media ID") - if self.media_ids_truncated != (self.media_count > len(self.media_ids)): + if self.media_ids_truncated != ( + self.media_count > len(self.media_ids) + ): raise ValueError( "media_ids_truncated must reflect the returned media-ID window" ) @@ -623,7 +640,9 @@ class WorkspaceMediaCapability(ApplicationModel): default=(), description="Capability roles currently usable for this media item.", ) - identity_mode: CapabilityIdentityMode = CapabilityIdentityMode.not_applicable + identity_mode: CapabilityIdentityMode = ( + CapabilityIdentityMode.not_applicable + ) class WorkspaceMedia(ApplicationModel): @@ -721,7 +740,9 @@ def inspect(item: JsonValue) -> None: inspect(value) if forbidden: - raise ValueError("Search metadata contains internal location fields.") + raise ValueError( + "Search metadata contains internal location fields." + ) return value @model_validator(mode="after") @@ -745,11 +766,17 @@ def to_dict(self) -> dict[str, Any]: return self.model_dump(mode="json") def to_prediction(self) -> dict[str, list[dict[str, Any]]]: - return {self.query_id: [hit.model_dump(mode="json") for hit in self.hits]} + return { + self.query_id: [ + hit.model_dump(mode="json") for hit in self.hits + ] + } class FusionProvenance(ApplicationModel): - profile: Literal[FusionProfile.reciprocal_rank] = FusionProfile.reciprocal_rank + profile: Literal[FusionProfile.reciprocal_rank] = ( + FusionProfile.reciprocal_rank + ) rank_constant: int = Field(default=60, gt=0) overlap_rule: Literal["connected_intervals"] = "connected_intervals" requested_modalities: tuple[Identifier, ...] = () @@ -962,7 +989,9 @@ class QueryAnswer(ApplicationModel): def _validate_answer_grounding(self) -> "QueryAnswer": evidence_ids = {item.evidence_id for item in self.evidence} cited_ids = { - evidence_id for claim in self.claims for evidence_id in claim.evidence_ids + evidence_id + for claim in self.claims + for evidence_id in claim.evidence_ids } if not cited_ids.issubset(evidence_ids): raise ValueError("Every claim citation must resolve to evidence.") @@ -1101,7 +1130,9 @@ class PrepareModelsJobResult(ApplicationModel): class JobProgress(ApplicationModel): - schema_version: Literal[JOB_PROGRESS_SCHEMA_VERSION] = JOB_PROGRESS_SCHEMA_VERSION + schema_version: Literal[JOB_PROGRESS_SCHEMA_VERSION] = ( + JOB_PROGRESS_SCHEMA_VERSION + ) stage: str = Field(min_length=1, max_length=128) message: str = Field(min_length=1, max_length=512) current: int | None = Field(default=None, ge=0) @@ -1136,7 +1167,9 @@ class Job(ApplicationModel): def _validate_terminal_payload(self) -> "Job": if self.state == JobState.succeeded: if self.result is None or self.error is not None: - raise ValueError("succeeded jobs require a result and no error") + raise ValueError( + "succeeded jobs require a result and no error" + ) if self.result.kind != self.kind: raise ValueError("job result kind must match job kind") elif self.result is not None: diff --git a/src/vidxp/assets/upload_page/THIRD_PARTY_NOTICES.txt b/src/vidxp/assets/upload_page/THIRD_PARTY_NOTICES.txt index c5254b1..571db94 100644 --- a/src/vidxp/assets/upload_page/THIRD_PARTY_NOTICES.txt +++ b/src/vidxp/assets/upload_page/THIRD_PARTY_NOTICES.txt @@ -63,6 +63,35 @@ SOFTWARE. ======================================================================== +@uppy/dashboard@5.1.1 +Declared license: MIT + +--- LICENSE --- + +The MIT License (MIT) + +Copyright (c) 2018 Transloadit + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +======================================================================== + @uppy/golden-retriever@5.2.1 Declared license: MIT @@ -92,6 +121,35 @@ SOFTWARE. ======================================================================== +@uppy/provider-views@5.2.2 +Declared license: MIT + +--- LICENSE --- + +The MIT License (MIT) + +Copyright (c) 2018 Transloadit + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +======================================================================== + @uppy/store-default@5.0.0 Declared license: MIT @@ -121,6 +179,35 @@ SOFTWARE. ======================================================================== +@uppy/thumbnail-generator@5.1.0 +Declared license: MIT + +--- LICENSE --- + +The MIT License (MIT) + +Copyright (c) 2018 Transloadit + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +======================================================================== + @uppy/tus@5.1.1 Declared license: MIT @@ -179,6 +266,93 @@ SOFTWARE. ======================================================================== +classnames@2.5.1 +Declared license: MIT + +--- LICENSE --- + +The MIT License (MIT) + +Copyright (c) 2018 Jed Watson + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +======================================================================== + +eventemitter3@5.0.4 +Declared license: MIT + +--- LICENSE --- + +The MIT License (MIT) + +Copyright (c) 2014 Arnout Kazemier + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +======================================================================== + +exifr@7.1.3 +Declared license: MIT + +--- LICENSE --- + +MIT License + +Copyright (c) 2020 Mike Kovařík, Mutiny.cz + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +======================================================================== + js-base64@3.9.2 Declared license: BSD-3-Clause @@ -351,6 +525,40 @@ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ======================================================================== +p-queue@8.1.1 +Declared license: MIT + +--- license --- + +MIT License + +Copyright (c) Sindre Sorhus (https://sindresorhus.com) + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +======================================================================== + +p-timeout@6.1.4 +Declared license: MIT + +--- license --- + +MIT License + +Copyright (c) Sindre Sorhus (https://sindresorhus.com) + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +======================================================================== + preact@10.29.7 Declared license: MIT @@ -438,6 +646,35 @@ SOFTWARE. ======================================================================== +shallow-equal@3.1.0 +Declared license: MIT + +--- LICENSE --- + +The MIT License (MIT) + +Copyright © 2023 Misha Moroshko and Philip Su + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the “Software”), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +of the Software, and to permit persons to whom the Software is furnished to do +so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +======================================================================== + tus-js-client@4.3.1 Declared license: MIT diff --git a/src/vidxp/assets/upload_page/index.html b/src/vidxp/assets/upload_page/index.html index cf4657a..4a0e3d8 100644 --- a/src/vidxp/assets/upload_page/index.html +++ b/src/vidxp/assets/upload_page/index.html @@ -32,24 +32,10 @@

Expected file

Choose and upload

- - -

No file selected.

- - 0% +

Waiting for a valid handoff.

- -
- - - -
diff --git a/src/vidxp/assets/upload_page/upload-page.css b/src/vidxp/assets/upload_page/upload-page.css index f0b5fcb..40ce2bf 100644 --- a/src/vidxp/assets/upload_page/upload-page.css +++ b/src/vidxp/assets/upload_page/upload-page.css @@ -1 +1 @@ -:root{color-scheme:light dark;font-family:Inter,ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,Segoe UI,sans-serif;background:#08101f;color:#e8eefc}*{box-sizing:border-box}body{min-height:100vh;margin:0;background:radial-gradient(circle at top left,#1b3261 0,transparent 38rem),#08101f}button,input{font:inherit}.shell{width:min(48rem,calc(100% - 2rem));margin:0 auto;padding:3rem 0 5rem}header{margin-bottom:2rem}.eyebrow,.label{color:#96b6ff;font-size:.78rem;font-weight:700;letter-spacing:.08em;text-transform:uppercase}h1,h2,p{margin-top:0}h1{margin-bottom:.65rem;font-size:clamp(2rem,8vw,3.6rem);line-height:1}h2{font-size:1.15rem}.lede,.hint,#next-action{color:#b6c4df;line-height:1.55}.card{margin-top:1rem;padding:1.4rem;border:1px solid #2c3d60;border-radius:1rem;background:#101a2dbf;box-shadow:0 1rem 3rem #0004}.facts{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:.85rem 1.25rem;margin:1rem 0}.facts div{min-width:0}.facts dt{margin-bottom:.2rem;color:#8fa0bd;font-size:.8rem}.facts dd{margin:0;overflow-wrap:anywhere;font-weight:650}.compact{grid-template-columns:1fr}.field-label{display:block;margin-bottom:.45rem;color:#b6c4df;font-weight:700}button,input::file-selector-button{display:inline-flex;min-height:2.75rem;align-items:center;justify-content:center;padding:.7rem 1rem;border:1px solid #6d96ee;border-radius:.7rem;background:#386ee0;color:#fff;cursor:pointer;font-weight:700}button:focus-visible,input:focus-visible{outline:3px solid #a9c3ff;outline-offset:3px}input[type=file]{display:block;width:100%;margin:.8rem 0;color:#b6c4df}input::file-selector-button{margin-right:.7rem}.selected-file{min-height:1.5rem;overflow-wrap:anywhere}progress{width:100%;height:1rem;accent-color:#6392f5}.state{min-height:1.5rem;margin:.8rem 0}.state.error{color:#ffaaa2}.state.success{color:#91e6bc}.actions{display:flex;flex-wrap:wrap;gap:.65rem}button.secondary{border-color:#52678c;background:#1b2943}button:disabled,input:disabled{cursor:not-allowed;opacity:.5}noscript{display:block;margin-top:1rem;color:#ffaaa2}@media(max-width:36rem){.shell{padding-top:1.5rem}.facts{grid-template-columns:1fr}.actions button{width:100%}}@media(prefers-reduced-motion:reduce){*,*:before,*:after{scroll-behavior:auto!important}} +@charset "UTF-8";.uppy-Root{box-sizing:border-box;color:#333;font-family:-apple-system,system-ui,BlinkMacSystemFont,Segoe UI,Segoe UI Symbol,Segoe UI Emoji,Apple Color Emoji,Roboto,Helvetica,Arial,sans-serif;line-height:1;position:relative;text-align:left;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.uppy-Root[dir=rtl],[dir=rtl] .uppy-Root{text-align:right}.uppy-Root *,.uppy-Root :after,.uppy-Root :before{box-sizing:inherit}.uppy-Root [hidden]{display:none}.uppy-u-reset{all:initial;appearance:none;box-sizing:border-box;font-family:-apple-system,system-ui,BlinkMacSystemFont,Segoe UI,Segoe UI Symbol,Segoe UI Emoji,Apple Color Emoji,Roboto,Helvetica,Arial,sans-serif;line-height:1}[dir=rtl] .uppy-u-reset{text-align:right}.uppy-truncate-text{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.uppy-c-textInput{background-color:#fff;border:1px solid #ddd;border-radius:4px;font-family:inherit;font-size:14px;line-height:1.5;padding:6px 8px}.uppy-size--md .uppy-c-textInput{padding:8px 10px}.uppy-c-textInput:focus{border-color:#1269cf99;box-shadow:0 0 0 3px #1269cf26;outline:none}[data-uppy-theme=dark] .uppy-c-textInput{background-color:#333;border-color:#333;color:#eaeaea}[data-uppy-theme=dark] .uppy-c-textInput:focus{border-color:#525252;box-shadow:none}.uppy-c-icon{display:inline-block;max-height:100%;max-width:100%;overflow:hidden;fill:currentColor}.uppy-c-btn{align-items:center;color:inherit;display:inline-flex;font-family:inherit;font-size:inherit;font-weight:500;justify-content:center;line-height:1;transition-duration:.3s;transition-property:background-color,color;user-select:none;white-space:nowrap}.uppy-c-btn,[dir=rtl] .uppy-c-btn{text-align:center}.uppy-c-btn:not(:disabled):not(.disabled){cursor:pointer}.uppy-c-btn::-moz-focus-inner{border:0}.uppy-c-btn-primary{background-color:#1269cf;border-radius:4px;color:#fff;font-size:14px;padding:10px 18px}.uppy-c-btn-primary:not(:disabled):hover{background-color:#0e51a0}.uppy-c-btn-primary:focus{box-shadow:0 0 0 3px #1269cf66;outline:none}.uppy-size--md .uppy-c-btn-primary{padding:13px 22px}[data-uppy-theme=dark] .uppy-c-btn-primary{color:#eaeaea}[data-uppy-theme=dark] .uppy-c-btn-primary:focus{outline:none}[data-uppy-theme=dark] .uppy-c-btn-primary::-moz-focus-inner{border:0}[data-uppy-theme=dark] .uppy-c-btn-primary:focus{box-shadow:0 0 0 2px #aae1ffd9}.uppy-c-btn-primary.uppy-c-btn--disabled{background-color:#8eb2db}.uppy-c-btn-link{background-color:initial;border-radius:4px;color:#525252;font-size:14px;line-height:1;padding:10px 15px}.uppy-c-btn-link:hover{color:#333}.uppy-c-btn-link:focus{box-shadow:0 0 0 3px #1269cf40;outline:none}.uppy-size--md .uppy-c-btn-link{padding:13px 18px}[data-uppy-theme=dark] .uppy-c-btn-link{color:#eaeaea}[data-uppy-theme=dark] .uppy-c-btn-link:focus{outline:none}[data-uppy-theme=dark] .uppy-c-btn-link::-moz-focus-inner{border:0}[data-uppy-theme=dark] .uppy-c-btn-link:focus{box-shadow:0 0 0 2px #aae1ffd9}[data-uppy-theme=dark] .uppy-c-btn-link:hover{color:#939393}.uppy-ProviderBrowser-viewType--grid ul.uppy-ProviderBrowser-list,.uppy-ProviderBrowser-viewType--unsplash ul.uppy-ProviderBrowser-list{align-items:flex-start;display:flex;flex-direction:row;flex-wrap:wrap;justify-content:space-between;padding:6px}.uppy-ProviderBrowser-viewType--grid ul.uppy-ProviderBrowser-list:after,.uppy-ProviderBrowser-viewType--unsplash ul.uppy-ProviderBrowser-list:after{content:"";flex:auto}.uppy-ProviderBrowser-viewType--grid li.uppy-ProviderBrowserItem,.uppy-ProviderBrowser-viewType--unsplash li.uppy-ProviderBrowserItem{margin:0;position:relative;width:50%}.uppy-size--md .uppy-ProviderBrowser-viewType--grid li.uppy-ProviderBrowserItem,.uppy-size--md .uppy-ProviderBrowser-viewType--unsplash li.uppy-ProviderBrowserItem{width:33.3333%}.uppy-size--lg .uppy-ProviderBrowser-viewType--grid li.uppy-ProviderBrowserItem,.uppy-size--lg .uppy-ProviderBrowser-viewType--unsplash li.uppy-ProviderBrowserItem{width:25%}.uppy-ProviderBrowser-viewType--grid li.uppy-ProviderBrowserItem:before,.uppy-ProviderBrowser-viewType--unsplash li.uppy-ProviderBrowserItem:before{content:"";display:block;padding-top:100%}.uppy-ProviderBrowser-viewType--grid li.uppy-ProviderBrowserItem--selected img,.uppy-ProviderBrowser-viewType--grid li.uppy-ProviderBrowserItem--selected svg,.uppy-ProviderBrowser-viewType--unsplash li.uppy-ProviderBrowserItem--selected img,.uppy-ProviderBrowser-viewType--unsplash li.uppy-ProviderBrowserItem--selected svg{opacity:.85}.uppy-ProviderBrowser-viewType--grid li.uppy-ProviderBrowserItem--disabled,.uppy-ProviderBrowser-viewType--unsplash li.uppy-ProviderBrowserItem--disabled{opacity:.5}.uppy-ProviderBrowser-viewType--grid li.uppy-ProviderBrowserItem--noPreview .uppy-ProviderBrowserItem-inner,.uppy-ProviderBrowser-viewType--unsplash li.uppy-ProviderBrowserItem--noPreview .uppy-ProviderBrowserItem-inner{background-color:#93939333}[data-uppy-theme=dark] .uppy-ProviderBrowser-viewType--grid li.uppy-ProviderBrowserItem--noPreview .uppy-ProviderBrowserItem-inner,[data-uppy-theme=dark] .uppy-ProviderBrowser-viewType--unsplash li.uppy-ProviderBrowserItem--noPreview .uppy-ProviderBrowserItem-inner{background-color:#eaeaea33}.uppy-ProviderBrowser-viewType--grid li.uppy-ProviderBrowserItem--noPreview svg,.uppy-ProviderBrowser-viewType--unsplash li.uppy-ProviderBrowserItem--noPreview svg{height:30%;width:30%;fill:#000000b3}[data-uppy-theme=dark] .uppy-ProviderBrowser-viewType--grid li.uppy-ProviderBrowserItem--noPreview svg,[data-uppy-theme=dark] .uppy-ProviderBrowser-viewType--unsplash li.uppy-ProviderBrowserItem--noPreview svg{fill:#fffc}.uppy-ProviderBrowser-viewType--grid .uppy-ProviderBrowserItem-inner,.uppy-ProviderBrowser-viewType--unsplash .uppy-ProviderBrowserItem-inner{border-radius:4px;height:calc(100% - 14px);inset:7px;overflow:hidden;position:absolute;text-align:center;width:calc(100% - 14px)}@media(hover:none){.uppy-ProviderBrowser-viewType--grid .uppy-ProviderBrowserItem-inner .uppy-ProviderBrowserItem-author,.uppy-ProviderBrowser-viewType--unsplash .uppy-ProviderBrowserItem-inner .uppy-ProviderBrowserItem-author{display:block}}[data-uppy-theme=dark] .uppy-ProviderBrowser-viewType--grid .uppy-ProviderBrowserItem-inner,[data-uppy-theme=dark] .uppy-ProviderBrowser-viewType--unsplash .uppy-ProviderBrowserItem-inner{box-shadow:0 0 0 3px #aae1ffb3}.uppy-ProviderBrowser-viewType--grid .uppy-ProviderBrowserItem-inner img,.uppy-ProviderBrowser-viewType--unsplash .uppy-ProviderBrowserItem-inner img{border-radius:4px;height:100%;object-fit:cover;width:100%}.uppy-ProviderBrowser-viewType--grid .uppy-ProviderBrowserItem-author,.uppy-ProviderBrowser-viewType--unsplash .uppy-ProviderBrowserItem-author{background:#0000004d;bottom:0;color:#fff;display:none;font-size:12px;font-weight:500;left:0;margin:0;padding:5px;position:absolute;text-decoration:none;width:100%}.uppy-ProviderBrowser-viewType--grid .uppy-ProviderBrowserItem-author:hover,.uppy-ProviderBrowser-viewType--unsplash .uppy-ProviderBrowserItem-author:hover{background:#0006;text-decoration:underline}.uppy-ProviderBrowser-viewType--grid .uppy-ProviderBrowserItem-checkbox,.uppy-ProviderBrowser-viewType--unsplash .uppy-ProviderBrowserItem-checkbox{background-color:#1269cf;border-radius:50%;height:26px;opacity:0;position:absolute;right:16px;top:16px;width:26px;z-index:1002}.uppy-ProviderBrowser-viewType--grid .uppy-ProviderBrowserItem-checkbox:after,.uppy-ProviderBrowser-viewType--unsplash .uppy-ProviderBrowserItem-checkbox:after{height:7px;inset-inline-start:7px;top:8px;width:12px}.uppy-ProviderBrowser-viewType--grid .uppy-ProviderBrowserItem--is-checked .uppy-ProviderBrowserItem-checkbox,.uppy-ProviderBrowser-viewType--unsplash .uppy-ProviderBrowserItem--is-checked .uppy-ProviderBrowserItem-checkbox{opacity:1}.uppy-ProviderBrowser-viewType--grid .uppy-ProviderBrowserItem-checkbox--grid:focus+label .uppy-ProviderBrowserItem-author,.uppy-ProviderBrowser-viewType--grid .uppy-ProviderBrowserItem-checkbox--grid:hover+label .uppy-ProviderBrowserItem-author,.uppy-ProviderBrowser-viewType--unsplash .uppy-ProviderBrowserItem-checkbox--grid:focus+label .uppy-ProviderBrowserItem-author,.uppy-ProviderBrowser-viewType--unsplash .uppy-ProviderBrowserItem-checkbox--grid:hover+label .uppy-ProviderBrowserItem-author{display:block}.uppy-ProviderBrowser-viewType--grid .uppy-ProviderBrowserItem-checkbox--grid:focus+label,.uppy-ProviderBrowser-viewType--unsplash .uppy-ProviderBrowserItem-checkbox--grid:focus+label{box-shadow:0 0 0 3px #1269cf80}.uppy-ProviderBrowser-viewType--grid .uppy-ProviderBrowserItem-checkbox--grid:focus+label:focus,.uppy-ProviderBrowser-viewType--unsplash .uppy-ProviderBrowserItem-checkbox--grid:focus+label:focus{outline:none}.uppy-ProviderBrowser-viewType--grid .uppy-ProviderBrowserItem-checkbox--grid:focus+label::-moz-focus-inner,.uppy-ProviderBrowser-viewType--unsplash .uppy-ProviderBrowserItem-checkbox--grid:focus+label::-moz-focus-inner{border:0}.uppy-ProviderBrowser-viewType--list{background-color:#fff}[data-uppy-theme=dark] .uppy-ProviderBrowser-viewType--list{background-color:#1f1f1f}.uppy-ProviderBrowser-viewType--list li.uppy-ProviderBrowserItem{align-items:center;display:flex;margin:0;padding:7px 15px}[data-uppy-theme=dark] .uppy-ProviderBrowser-viewType--list li.uppy-ProviderBrowserItem{color:#eaeaea}.uppy-ProviderBrowser-viewType--list li.uppy-ProviderBrowserItem--disabled{opacity:.6}.uppy-ProviderBrowser-viewType--list .uppy-ProviderBrowserItem-checkbox{background-color:#fff;border:1px solid #cfcfcf;border-radius:3px;height:17px;margin-inline-end:15px;width:17px}.uppy-ProviderBrowser-viewType--list .uppy-ProviderBrowserItem-checkbox:focus{border:1px solid #1269cf;box-shadow:0 0 0 3px #1269cf40;outline:none}.uppy-ProviderBrowser-viewType--list .uppy-ProviderBrowserItem-checkbox:after{height:5px;inset-inline-start:3px;opacity:0;top:4px;width:9px}[data-uppy-theme=dark] .uppy-ProviderBrowser-viewType--list .uppy-ProviderBrowserItem-checkbox:focus{border-color:#02baf2b3;box-shadow:0 0 0 3px #02baf233}.uppy-ProviderBrowser-viewType--list .uppy-ProviderBrowserItem--is-checked .uppy-ProviderBrowserItem-checkbox,.uppy-ProviderBrowser-viewType--list .uppy-ProviderBrowserItem--is-partial .uppy-ProviderBrowserItem-checkbox{background-color:#1269cf;border-color:#1269cf}.uppy-ProviderBrowser-viewType--list .uppy-ProviderBrowserItem--is-checked .uppy-ProviderBrowserItem-checkbox:after,.uppy-ProviderBrowser-viewType--list .uppy-ProviderBrowserItem--is-partial .uppy-ProviderBrowserItem-checkbox:after{opacity:1}.uppy-ProviderBrowser-viewType--list .uppy-ProviderBrowserItem-inner{align-items:center;color:inherit;display:flex;font-family:-apple-system,system-ui,BlinkMacSystemFont,Segoe UI,Segoe UI Symbol,Segoe UI Emoji,Apple Color Emoji,Roboto,Helvetica,Arial,sans-serif;overflow:hidden;padding:2px;text-overflow:ellipsis;white-space:nowrap}.uppy-ProviderBrowser-viewType--list .uppy-ProviderBrowserItem-inner:focus{outline:none;text-decoration:underline}.uppy-ProviderBrowser-viewType--list .uppy-ProviderBrowserItem-inner img,.uppy-ProviderBrowser-viewType--list .uppy-ProviderBrowserItem-inner svg{margin-inline-end:8px}.uppy-ProviderBrowser-viewType--list .uppy-ProviderBrowserItem-inner span{line-height:1.2;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.uppy-ProviderBrowser-viewType--list .uppy-ProviderBrowserItem--disabled .uppy-ProviderBrowserItem-inner{cursor:default}.uppy-ProviderBrowser-viewType--list .uppy-ProviderBrowserItem-iconWrap{margin-inline-end:7px;width:20px}.uppy-ProviderBrowserItem-checkbox{cursor:pointer;flex-shrink:0;position:relative}.uppy-ProviderBrowserItem-checkbox:disabled,.uppy-ProviderBrowserItem-checkbox:disabled:after{cursor:default}[data-uppy-theme=dark] .uppy-ProviderBrowserItem-checkbox{background-color:#1f1f1f;border-color:#939393}[data-uppy-theme=dark] .uppy-ProviderBrowserItem--is-checked .uppy-ProviderBrowserItem-checkbox{background-color:#333}.uppy-ProviderBrowserItem--is-checked .uppy-ProviderBrowserItem-checkbox:after{border-bottom:2px solid #eaeaea;border-left:2px solid #eaeaea;content:"";cursor:pointer;position:absolute;transform:rotate(-45deg)}.uppy-ProviderBrowserItem--is-partial .uppy-ProviderBrowserItem-checkbox:after{background-color:#eaeaea!important;content:""!important;height:2px!important;left:20%!important;position:absolute!important;right:20%!important;top:50%!important;transform:translateY(-50%)!important}.uppy-SearchProvider{align-items:center;display:flex;flex:1;flex-direction:column;height:100%;justify-content:center;width:100%}[data-uppy-theme=dark] .uppy-SearchProvider{background-color:#1f1f1f}.uppy-SearchProvider-input{margin-bottom:15px;max-width:650px;width:90%}.uppy-size--md .uppy-SearchProvider-input{margin-bottom:20px}.uppy-SearchProvider-input::-webkit-search-cancel-button{display:none}.uppy-SearchProvider-searchButton{padding:13px 25px}.uppy-size--md .uppy-SearchProvider-searchButton{padding:13px 30px}.uppy-DashboardContent-panelBody{align-items:center;display:flex;flex:1;justify-content:center}[data-uppy-theme=dark] .uppy-DashboardContent-panelBody{background-color:#1f1f1f}.uppy-Provider-auth,.uppy-Provider-empty,.uppy-Provider-error,.uppy-Provider-loading{align-items:center;color:#939393;display:flex;flex:1;flex-flow:column wrap;justify-content:center}.uppy-Provider-empty{color:#939393}.uppy-Provider-authIcon svg{height:75px;width:100px}.uppy-Provider-authTitle{color:#757575;font-size:17px;font-weight:400;line-height:1.4;margin-bottom:30px;max-width:500px;padding:0 15px;text-align:center}.uppy-size--md .uppy-Provider-authTitle{font-size:20px}[data-uppy-theme=dark] .uppy-Provider-authTitle{color:#cfcfcf}.uppy-Provider-btn-google{align-items:center;background:#4285f4;display:flex;padding:8px 12px!important}.uppy-Provider-btn-google:hover{background-color:#1266f1}.uppy-Provider-btn-google:focus{box-shadow:0 0 0 3px #4285f466;outline:none}.uppy-Provider-btn-google svg{margin-right:8px}.uppy-Provider-breadcrumbs{color:#525252;flex:1;font-size:12px;margin-bottom:10px;text-align:start}.uppy-size--md .uppy-Provider-breadcrumbs{margin-bottom:0}[data-uppy-theme=dark] .uppy-Provider-breadcrumbs{color:#eaeaea}.uppy-Provider-breadcrumbsIcon{color:#525252;display:inline-block;line-height:1;margin-inline-end:4px;vertical-align:middle}.uppy-Provider-breadcrumbsIcon svg{height:13px;width:13px;fill:#525252}.uppy-Provider-breadcrumbs button{border-radius:3px;display:inline-block;line-height:inherit;padding:4px}.uppy-Provider-breadcrumbs button:focus{outline:none}.uppy-Provider-breadcrumbs button::-moz-focus-inner{border:0}.uppy-Provider-breadcrumbs button:hover{color:#0e51a0}.uppy-Provider-breadcrumbs button:focus{background-color:#dfe6f1}[data-uppy-theme=dark] .uppy-Provider-breadcrumbs button:focus{background-color:#333}.uppy-Provider-breadcrumbs button:not(:last-of-type){text-decoration:underline}.uppy-Provider-breadcrumbs button:last-of-type{color:#333;cursor:normal;font-weight:500;pointer-events:none}.uppy-Provider-breadcrumbs button:hover{cursor:pointer}[data-uppy-theme=dark] .uppy-Provider-breadcrumbs button{color:#eaeaea}.uppy-ProviderBrowser{display:flex;flex:1;flex-direction:column;font-size:14px;font-weight:400;height:100%}.uppy-ProviderBrowser-user{color:#333;font-weight:500;margin:0 8px 0 0}[data-uppy-theme=dark] .uppy-ProviderBrowser-user{color:#eaeaea}.uppy-ProviderBrowser-user:after{color:#939393;content:"·";font-weight:400;inset-inline-start:4px;position:relative}.uppy-ProviderBrowser-header{border-bottom:1px solid #eaeaea;position:relative;z-index:1001}[data-uppy-theme=dark] .uppy-ProviderBrowser-header{border-bottom:1px solid #333}.uppy-ProviderBrowser-headerBar{background-color:#fafafa;color:#757575;font-size:12px;line-height:1.4;padding:7px 15px;z-index:1001}.uppy-size--md .uppy-ProviderBrowser-headerBar{align-items:center;display:flex}[data-uppy-theme=dark] .uppy-ProviderBrowser-headerBar{background-color:#1f1f1f}.uppy-ProviderBrowser-headerBar--simple{display:block;justify-content:center;text-align:center}.uppy-ProviderBrowser-headerBar--simple .uppy-Provider-breadcrumbsWrap{display:inline-block;flex:none;vertical-align:middle}.uppy-ProviderBrowser-searchFilter{align-items:center;display:flex;height:30px;margin-bottom:15px;margin-top:15px;padding-left:8px;padding-right:8px;position:relative;width:100%}.uppy-ProviderBrowser-searchFilterInput{background-color:#eaeaea;border:0;border-radius:4px;color:#333;font-family:-apple-system,system-ui,BlinkMacSystemFont,Segoe UI,Segoe UI Symbol,Segoe UI Emoji,Apple Color Emoji,Roboto,Helvetica,Arial,sans-serif;font-size:13px;height:30px;line-height:1.4;outline:0;padding-inline-end:30px;padding-inline-start:30px;width:100%;z-index:1001}.uppy-ProviderBrowser-searchFilterInput::-webkit-search-cancel-button{display:none}[data-uppy-theme=dark] .uppy-ProviderBrowser-searchFilterInput{background-color:#1f1f1f;color:#eaeaea}.uppy-ProviderBrowser-searchFilterInput:focus{background-color:#cfcfcf;border:0}[data-uppy-theme=dark] .uppy-ProviderBrowser-searchFilterInput:focus{background-color:#333}.uppy-ProviderBrowser-searchFilterIcon{color:#757575;height:12px;inset-inline-start:16px;position:absolute;width:12px;z-index:1002}.uppy-ProviderBrowser-searchFilterInput::placeholder{color:#939393;opacity:1}.uppy-ProviderBrowser-searchFilterReset{border-radius:3px;color:#939393;cursor:pointer;height:22px;inset-inline-end:16px;padding:6px;position:absolute;width:22px;z-index:1002}.uppy-ProviderBrowser-searchFilterReset:focus{outline:none}.uppy-ProviderBrowser-searchFilterReset::-moz-focus-inner{border:0}.uppy-ProviderBrowser-searchFilterReset:focus{box-shadow:0 0 0 3px #1269cf80}.uppy-ProviderBrowser-searchFilterReset:hover{color:#757575}.uppy-ProviderBrowser-searchFilterReset svg{vertical-align:text-top}.uppy-ProviderBrowser-userLogout{border-radius:3px;color:#1269cf;cursor:pointer;line-height:inherit;padding:4px}.uppy-ProviderBrowser-userLogout:focus{outline:none}.uppy-ProviderBrowser-userLogout::-moz-focus-inner{border:0}.uppy-ProviderBrowser-userLogout:hover{color:#0e51a0}.uppy-ProviderBrowser-userLogout:focus{background-color:#dfe6f1}[data-uppy-theme=dark] .uppy-ProviderBrowser-userLogout:focus{background-color:#333}.uppy-ProviderBrowser-userLogout:hover{text-decoration:underline}[data-uppy-theme=dark] .uppy-ProviderBrowser-userLogout{color:#eaeaea}.uppy-ProviderBrowser-body{flex:1;position:relative}.uppy-ProviderBrowser-list{background-color:#fff;border-spacing:0;display:block;flex:1;height:100%;inset:0;list-style:none;margin:0;overflow-x:hidden;overflow-y:auto;padding:0;position:absolute;width:100%;-webkit-overflow-scrolling:touch}[data-uppy-theme=dark] .uppy-ProviderBrowser-list{background-color:#1f1f1f}.uppy-ProviderBrowser-list:focus{outline:none}.uppy-ProviderBrowserItem-inner{cursor:pointer;font-size:13px;font-weight:500}.uppy-ProviderBrowser-footer{align-items:center;background-color:#fff;border-top:1px solid #eaeaea;display:flex;justify-content:space-between;padding:15px}.uppy-ProviderBrowser-footer button{margin-inline-end:8px}[data-uppy-theme=dark] .uppy-ProviderBrowser-footer{background-color:#1f1f1f;border-top:1px solid #333}.uppy-ProviderBrowser-footer-buttons{flex-shrink:0}.uppy-ProviderBrowser-footer-error{color:#e32437;line-height:18px}@media(max-width:426px){.uppy-ProviderBrowser-footer{align-items:stretch;flex-direction:column-reverse}.uppy-ProviderBrowser-footer-error{padding-bottom:10px}}.picker-dialog-bg{z-index:20000!important}.picker-dialog{z-index:20001!important}.uppy-Dashboard-Item-previewInnerWrap{align-items:center;border-radius:3px;box-shadow:0 0 2px #0006;display:flex;flex-direction:column;height:100%;justify-content:center;overflow:hidden;position:relative;width:100%}.uppy-size--md .uppy-Dashboard-Item-previewInnerWrap{box-shadow:0 1px 2px #00000026}.uppy-Dashboard--singleFile .uppy-Dashboard-Item-previewInnerWrap{box-shadow:none}.uppy-Dashboard-Item-previewInnerWrap:after{background-color:#000000a6;content:"";display:none;inset:0;position:absolute;z-index:1001}.uppy-Dashboard-Item-previewLink{inset:0;position:absolute;z-index:1002}.uppy-Dashboard-Item-previewLink:focus{box-shadow:inset 0 0 0 3px #579df0}[data-uppy-theme=dark] .uppy-Dashboard-Item-previewLink:focus{box-shadow:inset 0 0 0 3px #016c8d}.uppy-Dashboard-Item-preview img.uppy-Dashboard-Item-previewImg{border-radius:3px;height:100%;object-fit:cover;transform:translateZ(0);width:100%}.uppy-Dashboard--singleFile .uppy-Dashboard-Item-preview img.uppy-Dashboard-Item-previewImg{height:auto;max-height:100%;max-width:100%;object-fit:contain;padding:10px;width:auto}.uppy-Dashboard-Item-progress{color:#fff;left:50%;position:absolute;text-align:center;top:50%;transform:translate(-50%,-50%);transition:all .35 ease;width:120px;z-index:1002}.uppy-Dashboard-Item-progressIndicator{color:#fff;display:inline-block;height:38px;opacity:.9;width:38px}.uppy-size--md .uppy-Dashboard-Item-progressIndicator{height:55px;width:55px}button.uppy-Dashboard-Item-progressIndicator{cursor:pointer}button.uppy-Dashboard-Item-progressIndicator:focus{outline:none}button.uppy-Dashboard-Item-progressIndicator::-moz-focus-inner{border:0}button.uppy-Dashboard-Item-progressIndicator:focus .uppy-Dashboard-Item-progressIcon--bg,button.uppy-Dashboard-Item-progressIndicator:focus .uppy-Dashboard-Item-progressIcon--retry{fill:#579df0}.uppy-Dashboard-Item-progressIcon--circle{height:100%;width:100%}.uppy-Dashboard-Item-progressIcon--bg{stroke:#fff6}.uppy-Dashboard-Item-progressIcon--progress{transition:stroke-dashoffset .5s ease-out;stroke:#fff}.uppy-Dashboard-Item-progressIcon--play{transition:all .2s;fill:#fff;stroke:#fff}.uppy-Dashboard-Item-progressIcon--cancel{transition:all .2s;fill:#fff}.uppy-Dashboard-Item-progressIcon--pause{transition:all .2s;fill:#fff;stroke:#fff}.uppy-Dashboard-Item-progressIcon--check{transition:all .2s;fill:#fff}.uppy-Dashboard-Item-progressIcon--retry{fill:#fff}.uppy-Dashboard-Item.is-complete .uppy-Dashboard-Item-progress{inset-inline-end:-8px;inset-inline-start:auto;top:-9px;transform:none;width:auto}.uppy-Dashboard-Item.is-error .uppy-Dashboard-Item-progressIndicator{height:18px;width:18px}.uppy-size--md .uppy-Dashboard-Item.is-error .uppy-Dashboard-Item-progressIndicator{height:28px;width:28px}.uppy-Dashboard-Item.is-complete .uppy-Dashboard-Item-progressIndicator{height:18px;opacity:1;width:18px}.uppy-size--md .uppy-Dashboard-Item.is-complete .uppy-Dashboard-Item-progressIndicator{height:22px;width:22px}.uppy-Dashboard-Item.is-processing .uppy-Dashboard-Item-progress{opacity:0}.uppy-Dashboard-Item-fileInfo{padding-inline-end:5px}.uppy-Dashboard--singleFile .uppy-Dashboard-Item-fileInfo{padding-inline-end:10px}.uppy-size--md.uppy-Dashboard--singleFile .uppy-Dashboard-Item-fileInfo{padding-inline-end:15px}.uppy-Dashboard-Item-name{font-size:12px;font-weight:500;line-height:1.3;margin-bottom:5px;word-wrap:anywhere;word-break:break-all}[data-uppy-theme=dark] .uppy-Dashboard-Item-name{color:#eaeaea}.uppy-size--md.uppy-Dashboard--singleFile .uppy-Dashboard-Item-name{font-size:14px;line-height:1.4}.uppy-Dashboard-Item-fileName{align-items:baseline;display:flex}.uppy-Dashboard-Item-fileName button{margin-left:5px}.uppy-Dashboard-Item-author{color:#757575;display:inline-block;font-size:11px;font-weight:400;line-height:1;margin-bottom:5px;vertical-align:bottom}.uppy-Dashboard-Item-author a{color:#757575}.uppy-Dashboard-Item-status{color:#757575;font-size:11px;font-weight:400;line-height:1}[data-uppy-theme=dark] .uppy-Dashboard-Item-status{color:#bbb}.uppy-Dashboard-Item-statusSize{display:inline-block;margin-bottom:5px;text-transform:uppercase;vertical-align:bottom}.uppy-Dashboard-Item-reSelect{color:#1269cf;font-family:inherit;font-size:inherit;font-weight:600}.uppy-Dashboard-Item-errorMessage{background-color:#fdeff1;color:#a51523;font-size:11px;font-weight:500;line-height:1.3;padding:5px 6px}.uppy-Dashboard-Item-errorMessageBtn{color:#a51523;cursor:pointer;font-size:11px;font-weight:500;text-decoration:underline}.uppy-Dashboard-Item-preview .uppy-Dashboard-Item-errorMessage{display:none}.uppy-size--md .uppy-Dashboard-Item-preview .uppy-Dashboard-Item-errorMessage{border-bottom-left-radius:3px;border-bottom-right-radius:3px;border-top:1px solid #f7c2c8;bottom:0;display:block;left:0;line-height:1.4;padding:6px 8px;position:absolute;right:0}.uppy-Dashboard-Item-fileInfo .uppy-Dashboard-Item-errorMessage{border:1px solid #f7c2c8;border-radius:3px;display:inline-block;position:static}.uppy-size--md .uppy-Dashboard-Item-fileInfo .uppy-Dashboard-Item-errorMessage{display:none}.uppy-Dashboard-Item-action{color:#939393;cursor:pointer}.uppy-Dashboard-Item-action:focus{outline:none}.uppy-Dashboard-Item-action::-moz-focus-inner{border:0}.uppy-Dashboard-Item-action:focus{box-shadow:0 0 0 3px #1269cf80}.uppy-Dashboard-Item-action:hover{color:#1f1f1f;opacity:1}[data-uppy-theme=dark] .uppy-Dashboard-Item-action{color:#cfcfcf}[data-uppy-theme=dark] .uppy-Dashboard-Item-action:focus{outline:none}[data-uppy-theme=dark] .uppy-Dashboard-Item-action::-moz-focus-inner{border:0}[data-uppy-theme=dark] .uppy-Dashboard-Item-action:focus{box-shadow:0 0 0 2px #aae1ffd9}[data-uppy-theme=dark] .uppy-Dashboard-Item-action:hover{color:#eaeaea}.uppy-Dashboard-Item-action--remove{color:#1f1f1f;opacity:.95}.uppy-Dashboard-Item-action--remove:hover{color:#000;opacity:1}.uppy-size--md .uppy-Dashboard-Item-action--remove{height:18px;inset-inline-end:-8px;padding:0;position:absolute;top:-8px;width:18px;z-index:1002}.uppy-size--md .uppy-Dashboard-Item-action--remove:focus{border-radius:50%}.uppy-Dashboard--singleFile.uppy-size--height-md .uppy-Dashboard-Item-action--remove{inset-inline-end:8px;position:absolute;top:8px}[data-uppy-theme=dark] .uppy-Dashboard-Item-action--remove{color:#525252}[data-uppy-theme=dark] .uppy-Dashboard-Item-action--remove:hover{color:#333}.uppy-Dashboard:not(.uppy-size--md):not(.uppy-Dashboard--singleFile.uppy-size--height-md) .uppy-Dashboard-Item-actionWrapper{align-items:center;display:flex}.uppy-Dashboard:not(.uppy-size--md):not(.uppy-Dashboard--singleFile.uppy-size--height-md) .uppy-Dashboard-Item-action{height:22px;margin-left:3px;padding:3px;width:22px}.uppy-Dashboard:not(.uppy-size--md):not(.uppy-Dashboard--singleFile.uppy-size--height-md) .uppy-Dashboard-Item-action:focus{border-radius:3px}.uppy-size--md .uppy-Dashboard-Item-action--copyLink,.uppy-size--md .uppy-Dashboard-Item-action--edit{height:16px;padding:0;width:16px}.uppy-size--md .uppy-Dashboard-Item-action--copyLink:focus,.uppy-size--md .uppy-Dashboard-Item-action--edit:focus{border-radius:3px}.uppy-Dashboard-Item{align-items:center;border-bottom:1px solid #eaeaea;display:flex;padding:10px}.uppy-Dashboard:not(.uppy-Dashboard--singleFile) .uppy-Dashboard-Item{padding-inline-end:0}[data-uppy-theme=dark] .uppy-Dashboard-Item{border-bottom:1px solid #333}.uppy-size--md .uppy-Dashboard-Item{border-bottom:0;display:block;float:inline-start;height:215px;margin:5px 15px;padding:0;position:relative;width:calc(33.333% - 30px)}.uppy-size--lg .uppy-Dashboard-Item{height:190px;margin:5px 15px;padding:0;width:calc(25% - 30px)}.uppy-size--xl .uppy-Dashboard-Item{height:210px;padding:0;width:calc(20% - 30px)}.uppy-Dashboard--singleFile .uppy-Dashboard-Item{border-bottom:0;display:flex;flex-direction:column;height:100%;max-width:400px;padding:15px;position:relative;width:100%}.uppy-Dashboard-Item.is-ghost .uppy-Dashboard-Item-previewInnerWrap{opacity:.2}.uppy-Dashboard-Item.is-ghost .uppy-Dashboard-Item-name{opacity:.7}.uppy-Dashboard-Item.is-ghost .uppy-Dashboard-Item-preview:before{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='35' height='39' viewBox='0 0 35 39'%3E%3Cpath fill='%2523000' d='M1.708 38.66c1.709 0 3.417-3.417 6.834-3.417s5.125 3.417 8.61 3.417c3.348 0 5.056-3.417 8.473-3.417 4.305 0 5.125 3.417 6.833 3.417.889 0 1.709-.889 1.709-1.709v-19.68C34.167-5.757 0-5.757 0 17.271v19.68c0 .82.888 1.709 1.708 1.709m8.542-17.084a3.383 3.383 0 0 1-3.417-3.416 3.383 3.383 0 0 1 3.417-3.417 3.383 3.383 0 0 1 3.417 3.417 3.383 3.383 0 0 1-3.417 3.416m13.667 0A3.383 3.383 0 0 1 20.5 18.16a3.383 3.383 0 0 1 3.417-3.417 3.383 3.383 0 0 1 3.416 3.417 3.383 3.383 0 0 1-3.416 3.416'/%3E%3C/svg%3E");background-position:50% 10px;background-repeat:no-repeat;background-size:25px;content:"";inset:0;opacity:.5;position:absolute;z-index:1005}.uppy-size--md .uppy-Dashboard-Item.is-ghost .uppy-Dashboard-Item-preview:before{background-position:50% 50%;background-size:40px}.uppy-Dashboard--singleFile .uppy-Dashboard-Item.is-ghost .uppy-Dashboard-Item-preview:before{background-position:50% 50%;background-size:30%}.uppy-Dashboard-Item-preview{flex-grow:0;flex-shrink:0;height:50px;position:relative;width:50px}.uppy-size--md .uppy-Dashboard-Item-preview{height:140px;width:100%}.uppy-size--lg .uppy-Dashboard-Item-preview{height:120px}.uppy-size--xl .uppy-Dashboard-Item-preview{height:140px}.uppy-Dashboard--singleFile .uppy-Dashboard-Item-preview{flex-grow:1;max-height:75%;width:100%}.uppy-Dashboard--singleFile.uppy-size--md .uppy-Dashboard-Item-preview{max-height:100%}.uppy-Dashboard-Item-fileInfoAndButtons{align-items:center;display:flex;flex-grow:1;justify-content:space-between;padding-inline-end:8px;padding-inline-start:12px}.uppy-Dashboard--singleFile .uppy-Dashboard-Item-fileInfoAndButtons,.uppy-size--md .uppy-Dashboard-Item-fileInfoAndButtons{align-items:flex-start;padding:9px 0 0}.uppy-Dashboard--singleFile .uppy-Dashboard-Item-fileInfoAndButtons{flex-grow:0;width:100%}.uppy-Dashboard-Item-fileInfo{flex-grow:1;flex-shrink:1}.uppy-Dashboard-Item-actionWrapper{flex-grow:0;flex-shrink:0}.uppy-Dashboard-Item.is-error .uppy-Dashboard-Item-previewInnerWrap:after,.uppy-Dashboard-Item.is-inprogress .uppy-Dashboard-Item-previewInnerWrap:after{display:block}.uppy-Dashboard-Item-errorDetails{appearance:none;background-color:#939393;border:none;border-radius:50%;color:#fff;cursor:help;flex-shrink:0;font-size:10px;font-weight:600;height:13px;inset-inline-start:2px;line-height:12px;position:relative;text-align:center;top:0;width:13px}.uppy-Dashboard-Item-errorDetails:after{line-height:1.3;word-wrap:break-word}.uppy-Dashboard-FileCard{background-color:#fff;border-radius:5px;box-shadow:0 0 10px 4px #0000001a;display:flex;flex-direction:column;height:100%;inset:0;position:absolute;width:100%;z-index:1005}.uppy-Dashboard-FileCard .uppy-DashboardContent-bar{border-top-left-radius:5px;border-top-right-radius:5px}.uppy-Dashboard-FileCard .uppy-Dashboard-FileCard-actions{border-bottom-left-radius:5px;border-bottom-right-radius:5px}.uppy-Dashboard-FileCard-inner{display:flex;flex-direction:column;flex-grow:1;flex-shrink:1;height:100%;min-height:0}.uppy-Dashboard-FileCard-preview{align-items:center;border-bottom:1px solid #eaeaea;display:flex;flex-grow:0;flex-shrink:1;height:60%;justify-content:center;min-height:0;position:relative}[data-uppy-theme=dark] .uppy-Dashboard-FileCard-preview{background-color:#333;border-bottom:0}.uppy-Dashboard-FileCard-preview img.uppy-Dashboard-Item-previewImg{border-radius:3px;box-shadow:0 3px 20px #00000026;flex:0 0 auto;max-height:90%;max-width:90%;object-fit:cover}.uppy-Dashboard-FileCard-edit{background-color:#00000080;border-radius:50px;color:#fff;font-size:13px;inset-inline-end:10px;padding:7px 15px;position:absolute;top:10px}.uppy-Dashboard-FileCard-edit:focus{outline:none}.uppy-Dashboard-FileCard-edit::-moz-focus-inner{border:0}.uppy-Dashboard-FileCard-edit:focus{box-shadow:0 0 0 3px #1269cf80}.uppy-Dashboard-FileCard-edit:hover{background-color:#000c}.uppy-Dashboard-FileCard-info{flex-grow:0;flex-shrink:0;height:40%;overflow-y:auto;padding:30px 20px 20px;-webkit-overflow-scrolling:touch}[data-uppy-theme=dark] .uppy-Dashboard-FileCard-info{background-color:#1f1f1f}.uppy-Dashboard-FileCard-fieldset{border:0;font-size:0;margin:auto auto 12px;max-width:640px;padding:0}.uppy-Dashboard-FileCard-label{color:#525252;display:inline-block;font-size:12px;vertical-align:middle;width:22%}.uppy-size--md .uppy-Dashboard-FileCard-label{font-size:14px}[data-uppy-theme=dark] .uppy-Dashboard-FileCard-label{color:#eaeaea}.uppy-Dashboard-FileCard-input{display:inline-block;vertical-align:middle;width:78%}.uppy-Dashboard-FileCard-actions{align-items:center;background-color:#fafafa;border-top:1px solid #eaeaea;display:flex;flex-grow:0;flex-shrink:0;height:55px;padding:0 15px}.uppy-size--md .uppy-Dashboard-FileCard-actions{height:65px}[data-uppy-theme=dark] .uppy-Dashboard-FileCard-actions{background-color:#1f1f1f;border-top:1px solid #333}.uppy-Dashboard-FileCard-actionsBtn{margin-inline-end:10px}.uppy-Informer{bottom:60px;left:0;position:absolute;right:0;text-align:center;z-index:1005}.uppy-Informer span>div{margin-bottom:6px}.uppy-Informer-animated{opacity:0;transform:translateY(350%);transition:all .3s ease-in;z-index:-1000}.uppy-Informer p{background-color:#757575;border-radius:18px;color:#fff;display:inline-block;font-size:12px;font-weight:400;line-height:1.4;margin:0;max-width:90%;padding:6px 15px}.uppy-size--md .uppy-Informer p{font-size:14px;line-height:1.3;max-width:500px;padding:10px 20px}[data-uppy-theme=dark] .uppy-Informer p{background-color:#333}.uppy-Informer p span{background-color:#fff;border-radius:50%;color:#525252;display:inline-block;font-size:10px;height:13px;inset-inline-start:3px;line-height:12px;margin-inline-start:-1px;position:relative;top:-1px;vertical-align:middle;width:13px}.uppy-Informer p span:hover{cursor:help}.uppy-Informer p span:after{line-height:1.3;word-wrap:break-word}.uppy-Root [aria-label][role~=tooltip]{position:relative}.uppy-Root [aria-label][role~=tooltip]:after,.uppy-Root [aria-label][role~=tooltip]:before{-webkit-backface-visibility:hidden;backface-visibility:hidden;box-sizing:border-box;opacity:0;pointer-events:none;position:absolute;transform:translateZ(0);transform-origin:top;transition:all var(--microtip-transition-duration,.18s) var(--microtip-transition-easing,ease-in-out) var(--microtip-transition-delay,0s);will-change:transform;z-index:10}.uppy-Root [aria-label][role~=tooltip]:before{background-size:100% auto!important;content:""}.uppy-Root [aria-label][role~=tooltip]:after{background:#111111e6;border-radius:4px;box-sizing:initial;color:#fff;content:attr(aria-label);font-size:var(--microtip-font-size,13px);font-weight:var(--microtip-font-weight,normal);padding:.5em 1em;text-transform:var(--microtip-text-transform,none);white-space:nowrap}.uppy-Root [aria-label][role~=tooltip]:focus:after,.uppy-Root [aria-label][role~=tooltip]:focus:before,.uppy-Root [aria-label][role~=tooltip]:hover:after,.uppy-Root [aria-label][role~=tooltip]:hover:before{opacity:1;pointer-events:auto}.uppy-Root [role~=tooltip][data-microtip-position|=top]:before{background:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='36' height='12'%3E%3Cpath fill='rgba(17, 17, 17, 0.9)' d='M2.658 0h32.004c-6 0-11.627 12.002-16.002 12.002S8.594 0 2.658 0'/%3E%3C/svg%3E") no-repeat;bottom:100%;height:6px;left:50%;margin-bottom:5px;transform:translate3d(-50%,0,0);width:18px}.uppy-Root [role~=tooltip][data-microtip-position|=top]:after{bottom:100%;left:50%;margin-bottom:11px;transform:translate3d(-50%,0,0)}.uppy-Root [role~=tooltip][data-microtip-position=top]:hover:after,.uppy-Root [role~=tooltip][data-microtip-position|=top]:hover:before{transform:translate3d(-50%,-5px,0)}.uppy-Root [role~=tooltip][data-microtip-position=top-left]:after{bottom:100%;transform:translate3d(calc(-100% + 16px),0,0)}.uppy-Root [role~=tooltip][data-microtip-position=top-left]:hover:after{transform:translate3d(calc(-100% + 16px),-5px,0)}.uppy-Root [role~=tooltip][data-microtip-position=top-right]:after{bottom:100%;transform:translate3d(-16px,0,0)}.uppy-Root [role~=tooltip][data-microtip-position=top-right]:hover:after{transform:translate3d(-16px,-5px,0)}.uppy-Root [role~=tooltip][data-microtip-position|=bottom]:before{background:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='36' height='12'%3E%3Cpath fill='rgba(17, 17, 17, 0.9)' d='M33.342 12H1.338c6 0 11.627-12.002 16.002-12.002S27.406 12 33.342 12'/%3E%3C/svg%3E") no-repeat;bottom:auto;height:6px;left:50%;margin-bottom:0;margin-top:5px;top:100%;transform:translate3d(-50%,-10px,0);width:18px}.uppy-Root [role~=tooltip][data-microtip-position|=bottom]:after{left:50%;margin-top:11px;top:100%;transform:translate3d(-50%,-10px,0)}.uppy-Root [role~=tooltip][data-microtip-position=bottom]:hover:after,.uppy-Root [role~=tooltip][data-microtip-position|=bottom]:hover:before{transform:translate3d(-50%,0,0)}.uppy-Root [role~=tooltip][data-microtip-position=bottom-left]:after{top:100%;transform:translate3d(calc(-100% + 16px),-10px,0)}.uppy-Root [role~=tooltip][data-microtip-position=bottom-left]:hover:after{transform:translate3d(calc(-100% + 16px),0,0)}.uppy-Root [role~=tooltip][data-microtip-position=bottom-right]:after{top:100%;transform:translate3d(-16px,-10px,0)}.uppy-Root [role~=tooltip][data-microtip-position=bottom-right]:hover:after{transform:translate3d(-16px,0,0)}.uppy-Root [role~=tooltip][data-microtip-position=left]:after,.uppy-Root [role~=tooltip][data-microtip-position=left]:before{inset:50% 100% auto auto;transform:translate3d(10px,-50%,0)}.uppy-Root [role~=tooltip][data-microtip-position=left]:before{background:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='36'%3E%3Cpath fill='rgba(17, 17, 17, 0.9)' d='M0 33.342V1.338c0 6 12.002 11.627 12.002 16.002S0 27.406 0 33.342'/%3E%3C/svg%3E") no-repeat;height:18px;margin-bottom:0;margin-right:5px;width:6px}.uppy-Root [role~=tooltip][data-microtip-position=left]:after{margin-right:11px}.uppy-Root [role~=tooltip][data-microtip-position=left]:hover:after,.uppy-Root [role~=tooltip][data-microtip-position=left]:hover:before{transform:translate3d(0,-50%,0)}.uppy-Root [role~=tooltip][data-microtip-position=right]:after,.uppy-Root [role~=tooltip][data-microtip-position=right]:before{bottom:auto;left:100%;top:50%;transform:translate3d(-10px,-50%,0)}.uppy-Root [role~=tooltip][data-microtip-position=right]:before{background:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='36'%3E%3Cpath fill='rgba(17, 17, 17, 0.9)' d='M12 2.658v32.004c0-6-12.002-11.627-12.002-16.002S12 8.594 12 2.658'/%3E%3C/svg%3E") no-repeat;height:18px;margin-bottom:0;margin-left:5px;width:6px}.uppy-Root [role~=tooltip][data-microtip-position=right]:after{margin-left:11px}.uppy-Root [role~=tooltip][data-microtip-position=right]:hover:after,.uppy-Root [role~=tooltip][data-microtip-position=right]:hover:before{transform:translate3d(0,-50%,0)}.uppy-Root [role~=tooltip][data-microtip-size=small]:after{white-space:normal;width:80px}.uppy-Root [role~=tooltip][data-microtip-size=medium]:after{white-space:normal;width:150px}.uppy-Root [role~=tooltip][data-microtip-size=large]:after{white-space:normal;width:260px}.uppy-StatusBar{background-color:#fff;color:#fff;display:flex;font-size:12px;font-weight:400;height:46px;line-height:40px;position:relative;transition:height .2s;z-index:1001}[data-uppy-theme=dark] .uppy-StatusBar{background-color:#1f1f1f}.uppy-StatusBar:before{background-color:#eaeaea;content:"";height:2px;inset:0;position:absolute;width:100%}[data-uppy-theme=dark] .uppy-StatusBar:before{background-color:#757575}.uppy-StatusBar[aria-hidden=true]{height:0;overflow-y:hidden}.uppy-StatusBar.is-complete .uppy-StatusBar-progress{background-color:#1bb240}.uppy-StatusBar.is-error .uppy-StatusBar-progress{background-color:#e32437}.uppy-StatusBar.is-complete .uppy-StatusBar-statusIndicator{color:#1bb240}.uppy-StatusBar.is-error .uppy-StatusBar-statusIndicator{color:#e32437}.uppy-StatusBar:not([aria-hidden=true]).is-waiting{background-color:#fff;border-top:1px solid #eaeaea;height:65px}[data-uppy-theme=dark] .uppy-StatusBar:not([aria-hidden=true]).is-waiting{background-color:#1f1f1f;border-top:1px solid #333}.uppy-StatusBar-progress{background-color:#1269cf;height:2px;position:absolute;transition:background-color,width .3s ease-out;z-index:1001}.uppy-StatusBar-progress.is-indeterminate{animation:uppy-StatusBar-ProgressStripes 1s linear infinite;background-image:linear-gradient(45deg,#0000004d 25%,#0000 0 50%,#0000004d 0 75%,#0000 0,#0000);background-size:64px 64px}@keyframes uppy-StatusBar-ProgressStripes{0%{background-position:0 0}to{background-position:64px 0}}.uppy-StatusBar.is-postprocessing .uppy-StatusBar-progress,.uppy-StatusBar.is-preprocessing .uppy-StatusBar-progress{background-color:#f6a623}.uppy-StatusBar.is-waiting .uppy-StatusBar-progress{display:none}.uppy-StatusBar-content{align-items:center;color:#333;display:flex;height:100%;padding-inline-start:10px;position:relative;text-overflow:ellipsis;white-space:nowrap;z-index:1002}.uppy-size--md .uppy-StatusBar-content{padding-inline-start:15px}[data-uppy-theme=dark] .uppy-StatusBar-content{color:#eaeaea}.uppy-StatusBar-status{display:flex;flex-direction:column;font-weight:400;justify-content:center;line-height:1.4;padding-inline-end:.3em}.uppy-StatusBar-statusPrimary{display:flex;font-weight:500;line-height:1}.uppy-StatusBar-statusPrimary button.uppy-StatusBar-details{margin-left:5px}[data-uppy-theme=dark] .uppy-StatusBar-statusPrimary{color:#eaeaea}.uppy-StatusBar-statusSecondary{color:#757575;display:inline-block;font-size:11px;line-height:1.2;margin-top:1px;white-space:nowrap}[data-uppy-theme=dark] .uppy-StatusBar-statusSecondary{color:#bbb}.uppy-StatusBar-statusSecondaryHint{display:inline-block;line-height:1;margin-inline-end:5px;vertical-align:middle}.uppy-size--md .uppy-StatusBar-statusSecondaryHint{margin-inline-end:8px}.uppy-StatusBar-statusIndicator{color:#525252;margin-inline-end:7px;position:relative;top:1px}.uppy-StatusBar-statusIndicator svg{vertical-align:text-bottom}.uppy-StatusBar-actions{align-items:center;bottom:0;display:flex;inset-inline-end:10px;position:absolute;top:0;z-index:1004}.uppy-StatusBar.is-waiting .uppy-StatusBar-actions{background-color:#fafafa;height:100%;padding:0 15px;position:static;width:100%}[data-uppy-theme=dark] .uppy-StatusBar.is-waiting .uppy-StatusBar-actions{background-color:#1f1f1f}.uppy-StatusBar:not([aria-hidden=true]).is-waiting.has-ghosts{flex-direction:column;height:90px}.uppy-size--md .uppy-StatusBar:not([aria-hidden=true]).is-waiting.has-ghosts{flex-direction:row;height:65px}.uppy-StatusBar:not([aria-hidden=true]).is-waiting.has-ghosts .uppy-StatusBar-actions{flex-direction:column;justify-content:center}.uppy-size--md .uppy-StatusBar:not([aria-hidden=true]).is-waiting.has-ghosts .uppy-StatusBar-actions{flex-direction:row;justify-content:normal}.uppy-StatusBar-actionCircleBtn{cursor:pointer;line-height:1;margin:3px;opacity:.9}.uppy-StatusBar-actionCircleBtn:focus{outline:none}.uppy-StatusBar-actionCircleBtn::-moz-focus-inner{border:0}.uppy-StatusBar-actionCircleBtn:focus{box-shadow:0 0 0 3px #1269cf80}[data-uppy-theme=dark] .uppy-StatusBar-actionCircleBtn:focus{outline:none}[data-uppy-theme=dark] .uppy-StatusBar-actionCircleBtn::-moz-focus-inner{border:0}[data-uppy-theme=dark] .uppy-StatusBar-actionCircleBtn:focus{box-shadow:0 0 0 2px #aae1ffd9}.uppy-StatusBar-actionCircleBtn:hover{opacity:1}.uppy-StatusBar-actionCircleBtn:focus{border-radius:50%}.uppy-StatusBar-actionCircleBtn svg{vertical-align:bottom}.uppy-StatusBar-actionBtn{color:#1269cf;display:inline-block;font-size:10px;line-height:inherit;vertical-align:middle}.uppy-size--md .uppy-StatusBar-actionBtn{font-size:11px}.uppy-StatusBar-actionBtn--disabled{opacity:.4}[data-uppy-theme=dark] .uppy-StatusBar-actionBtn--disabled{opacity:.7}.uppy-StatusBar-actionBtn--retry{background-color:#ff4b23;border-radius:8px;color:#fff;height:16px;line-height:1;margin-inline-end:6px;padding:1px 6px 3px 18px;position:relative}.uppy-StatusBar-actionBtn--retry:focus{outline:none}.uppy-StatusBar-actionBtn--retry::-moz-focus-inner{border:0}.uppy-StatusBar-actionBtn--retry:focus{box-shadow:0 0 0 3px #1269cf80}[data-uppy-theme=dark] .uppy-StatusBar-actionBtn--retry:focus{outline:none}[data-uppy-theme=dark] .uppy-StatusBar-actionBtn--retry::-moz-focus-inner{border:0}[data-uppy-theme=dark] .uppy-StatusBar-actionBtn--retry:focus{box-shadow:0 0 0 2px #aae1ffd9}.uppy-StatusBar-actionBtn--retry:hover{background-color:#f92d00}.uppy-StatusBar-actionBtn--retry svg{inset-inline-start:6px;position:absolute;top:3px}.uppy-StatusBar.is-waiting .uppy-StatusBar-actionBtn--upload{background-color:#1bb240;color:#fff;font-size:14px;line-height:1;padding:15px 10px;width:100%}.uppy-StatusBar.is-waiting .uppy-StatusBar-actionBtn--upload:hover{background-color:#189c38}[data-uppy-theme=dark] .uppy-StatusBar.is-waiting .uppy-StatusBar-actionBtn--upload{background-color:#1c8b37}[data-uppy-theme=dark] .uppy-StatusBar.is-waiting .uppy-StatusBar-actionBtn--upload:hover{background-color:#18762f}.uppy-size--md .uppy-StatusBar.is-waiting .uppy-StatusBar-actionBtn--upload{padding:13px 22px;width:auto}.uppy-StatusBar.is-waiting .uppy-StatusBar-actionBtn--upload.uppy-StatusBar-actionBtn--disabled:hover{background-color:#1bb240;cursor:not-allowed}[data-uppy-theme=dark] .uppy-StatusBar.is-waiting .uppy-StatusBar-actionBtn--upload.uppy-StatusBar-actionBtn--disabled:hover{background-color:#1c8b37}.uppy-StatusBar:not(.is-waiting) .uppy-StatusBar-actionBtn--upload{background-color:initial;color:#1269cf}.uppy-StatusBar-actionBtn--uploadNewlyAdded{border-radius:3px;padding-inline-end:3px;padding-bottom:1px;padding-inline-start:3px}.uppy-StatusBar-actionBtn--uploadNewlyAdded:focus{outline:none}.uppy-StatusBar-actionBtn--uploadNewlyAdded::-moz-focus-inner{border:0}.uppy-StatusBar-actionBtn--uploadNewlyAdded:focus{box-shadow:0 0 0 3px #1269cf80}[data-uppy-theme=dark] .uppy-StatusBar-actionBtn--uploadNewlyAdded:focus{outline:none}[data-uppy-theme=dark] .uppy-StatusBar-actionBtn--uploadNewlyAdded::-moz-focus-inner{border:0}[data-uppy-theme=dark] .uppy-StatusBar-actionBtn--uploadNewlyAdded:focus{box-shadow:0 0 0 2px #aae1ffd9}.uppy-StatusBar.is-postprocessing .uppy-StatusBar-actionBtn--uploadNewlyAdded,.uppy-StatusBar.is-preprocessing .uppy-StatusBar-actionBtn--uploadNewlyAdded{display:none}.uppy-StatusBar-actionBtn--done{border-radius:3px;line-height:1;padding:7px 8px}.uppy-StatusBar-actionBtn--done:focus{outline:none}.uppy-StatusBar-actionBtn--done::-moz-focus-inner{border:0}.uppy-StatusBar-actionBtn--done:hover{color:#0e51a0}.uppy-StatusBar-actionBtn--done:focus{background-color:#dfe6f1}[data-uppy-theme=dark] .uppy-StatusBar-actionBtn--done:focus{background-color:#333}[data-uppy-theme=dark] .uppy-StatusBar-actionBtn--done{color:#02baf2}.uppy-size--md .uppy-StatusBar-actionBtn--done{font-size:14px}.uppy-StatusBar-serviceMsg{color:#000;font-size:11px;line-height:1.1;padding-left:10px}.uppy-size--md .uppy-StatusBar-serviceMsg{font-size:14px;padding-left:15px}[data-uppy-theme=dark] .uppy-StatusBar-serviceMsg{color:#eaeaea}.uppy-StatusBar-serviceMsg-ghostsIcon{left:6px;opacity:.5;position:relative;top:2px;vertical-align:text-bottom;width:10px}.uppy-size--md .uppy-StatusBar-serviceMsg-ghostsIcon{left:10px;top:1px;width:15px}.uppy-StatusBar-details{appearance:none;background-color:#939393;border-radius:50%;color:#fff;cursor:help;display:inline-block;font-size:10px;font-weight:600;height:13px;inset-inline-start:2px;line-height:12px;position:relative;text-align:center;top:0;vertical-align:middle;width:13px}.uppy-StatusBar-details:after{line-height:1.3;word-wrap:break-word}.uppy-StatusBar-spinner{animation-duration:1s;animation-iteration-count:infinite;animation-name:uppy-StatusBar-spinnerAnimation;animation-timing-function:linear;fill:#1269cf;margin-inline-end:10px}.uppy-StatusBar.is-postprocessing .uppy-StatusBar-spinner,.uppy-StatusBar.is-preprocessing .uppy-StatusBar-spinner{fill:#f6a623}@keyframes uppy-StatusBar-spinnerAnimation{0%{transform:rotate(0)}to{transform:rotate(1turn)}}.uppy-transition-slideDownUp-enter{opacity:.01;transform:translate3d(0,-105%,0);transition:transform .25s ease-in-out,opacity .25s ease-in-out}.uppy-transition-slideDownUp-enter.uppy-transition-slideDownUp-enter-active{opacity:1;transform:translateZ(0)}.uppy-transition-slideDownUp-leave{opacity:1;transform:translateZ(0);transition:transform .25s ease-in-out,opacity .25s ease-in-out}.uppy-transition-slideDownUp-leave.uppy-transition-slideDownUp-leave-active{opacity:.01;transform:translate3d(0,-105%,0)}@keyframes uppy-Dashboard-fadeIn{0%{opacity:0}to{opacity:1}}@keyframes uppy-Dashboard-fadeOut{0%{opacity:1}to{opacity:0}}@keyframes uppy-Dashboard-slideDownAndFadeIn{0%{opacity:0;transform:translate3d(-50%,-70%,0)}to{opacity:1;transform:translate3d(-50%,-50%,0)}}@keyframes uppy-Dashboard-slideDownAndFadeIn--small{0%{opacity:0;transform:translate3d(0,-20%,0)}to{opacity:1;transform:translateZ(0)}}@keyframes uppy-Dashboard-slideUpFadeOut{0%{opacity:1;transform:translate3d(-50%,-50%,0)}to{opacity:0;transform:translate3d(-50%,-70%,0)}}@keyframes uppy-Dashboard-slideUpFadeOut--small{0%{opacity:1;transform:translateZ(0)}to{opacity:0;transform:translate3d(0,-20%,0)}}.uppy-Dashboard--modal{z-index:1001}.uppy-Dashboard--modal[aria-hidden=true]{display:none}.uppy-Dashboard--modal.uppy-Dashboard--animateOpenClose>.uppy-Dashboard-inner{animation:uppy-Dashboard-slideDownAndFadeIn--small .3s cubic-bezier(0,0,.2,1)}@media only screen and (min-width:820px){.uppy-Dashboard--modal.uppy-Dashboard--animateOpenClose>.uppy-Dashboard-inner{animation:uppy-Dashboard-slideDownAndFadeIn .3s cubic-bezier(0,0,.2,1)}}.uppy-Dashboard--modal.uppy-Dashboard--animateOpenClose>.uppy-Dashboard-overlay{animation:uppy-Dashboard-fadeIn .3s cubic-bezier(0,0,.2,1)}.uppy-Dashboard--modal.uppy-Dashboard--animateOpenClose.uppy-Dashboard--isClosing>.uppy-Dashboard-inner{animation:uppy-Dashboard-slideUpFadeOut--small .3s cubic-bezier(0,0,.2,1)}@media only screen and (min-width:820px){.uppy-Dashboard--modal.uppy-Dashboard--animateOpenClose.uppy-Dashboard--isClosing>.uppy-Dashboard-inner{animation:uppy-Dashboard-slideUpFadeOut .3s cubic-bezier(0,0,.2,1)}}.uppy-Dashboard--modal.uppy-Dashboard--animateOpenClose.uppy-Dashboard--isClosing>.uppy-Dashboard-overlay{animation:uppy-Dashboard-fadeOut .3s cubic-bezier(0,0,.2,1)}.uppy-Dashboard-isFixed{height:100vh;overflow:hidden}.uppy-Dashboard--modal .uppy-Dashboard-overlay{background-color:#00000080;inset:0;position:fixed;z-index:1001}.uppy-Dashboard-inner{background-color:#f4f4f4;border:1px solid #eaeaea;border-radius:5px;max-height:100%;max-width:100%;outline:none;position:relative}.uppy-size--md .uppy-Dashboard-inner{min-height:auto}@media only screen and (min-width:820px){.uppy-Dashboard-inner{height:500px;width:650px}}.uppy-Dashboard--modal .uppy-Dashboard-inner{z-index:1002}[data-uppy-theme=dark] .uppy-Dashboard-inner{background-color:#1f1f1f}.uppy-Dashboard--isDisabled .uppy-Dashboard-inner{cursor:not-allowed}.uppy-Dashboard-innerWrap{border-radius:5px;display:flex;flex-direction:column;height:100%;opacity:0;overflow:hidden;position:relative}.uppy-Dashboard--isInnerWrapVisible .uppy-Dashboard-innerWrap{opacity:1}.uppy-Dashboard--isDisabled .uppy-Dashboard-innerWrap{cursor:not-allowed;filter:grayscale(100%);opacity:.6;user-select:none}.uppy-Dashboard--isDisabled .uppy-ProviderIconBg{fill:#9f9f9f}.uppy-Dashboard--isDisabled [aria-disabled],.uppy-Dashboard--isDisabled [disabled]{cursor:not-allowed;pointer-events:none}.uppy-Dashboard--modal .uppy-Dashboard-inner{border:none;inset:35px 15px 15px;position:fixed}@media only screen and (min-width:820px){.uppy-Dashboard--modal .uppy-Dashboard-inner{box-shadow:0 5px 15px 4px #00000026;left:50%;right:auto;top:50%;transform:translate(-50%,-50%)}}.uppy-Dashboard-close{color:#ffffffe6;cursor:pointer;display:block;font-size:27px;inset-inline-end:-2px;position:absolute;top:-33px;z-index:1005}.uppy-Dashboard-close:focus{outline:none}.uppy-Dashboard-close::-moz-focus-inner{border:0}.uppy-Dashboard-close:focus{color:#6eabf2}@media only screen and (min-width:820px){.uppy-Dashboard-close{font-size:35px;inset-inline-end:-35px;top:-10px}}.uppy-Dashboard-serviceMsg{background-color:#fffbf7;border-bottom:1px solid #edd4b9;border-top:1px solid #edd4b9;font-size:12px;font-weight:500;line-height:1.3;padding:12px 0;position:relative;top:-1px;z-index:1004}.uppy-size--md .uppy-Dashboard-serviceMsg{font-size:14px;line-height:1.4}[data-uppy-theme=dark] .uppy-Dashboard-serviceMsg{background-color:#1f1f1f;border-bottom:1px solid #333;border-top:1px solid #333;color:#eaeaea}.uppy-Dashboard-serviceMsg-title{display:block;line-height:1;margin-bottom:4px;padding-left:42px}.uppy-Dashboard-serviceMsg-text{padding:0 15px}.uppy-Dashboard-serviceMsg-actionBtn{color:#1269cf;font-size:inherit;font-weight:inherit;vertical-align:initial}[data-uppy-theme=dark] .uppy-Dashboard-serviceMsg-actionBtn{color:#02baf2e6}.uppy-Dashboard-serviceMsg-icon{left:15px;position:absolute;top:10px}.uppy-Dashboard-AddFiles{align-items:center;display:flex;flex-direction:column;height:100%;justify-content:center;position:relative;text-align:center}[data-uppy-drag-drop-supported=true] .uppy-Dashboard-AddFiles{border:1px dashed #dfdfdf;border-radius:3px;height:calc(100% - 14px);margin:7px}.uppy-Dashboard-AddFilesPanel .uppy-Dashboard-AddFiles{border:none;height:calc(100% - 54px)}.uppy-Dashboard--modal .uppy-Dashboard-AddFiles{border-color:#cfcfcf}[data-uppy-theme=dark] .uppy-Dashboard-AddFiles{border-color:#757575}.uppy-Dashboard-AddFiles-info{display:none;margin-top:auto;padding-bottom:15px;padding-top:15px}.uppy-size--height-md .uppy-Dashboard-AddFiles-info{display:block}.uppy-size--md .uppy-Dashboard-AddFiles-info{bottom:25px;left:0;padding-bottom:0;padding-top:30px;position:absolute;right:0}[data-uppy-num-acquirers="0"] .uppy-Dashboard-AddFiles-info{margin-top:0}.uppy-Dashboard-browse{color:#1269cf;cursor:pointer}.uppy-Dashboard-browse:focus{outline:none}.uppy-Dashboard-browse::-moz-focus-inner{border:0}.uppy-Dashboard-browse:focus,.uppy-Dashboard-browse:hover{border-bottom:1px solid #1269cf}[data-uppy-theme=dark] .uppy-Dashboard-browse{color:#02baf2e6}[data-uppy-theme=dark] .uppy-Dashboard-browse:focus,[data-uppy-theme=dark] .uppy-Dashboard-browse:hover{border-bottom:1px solid #02baf2}.uppy-Dashboard-browseBtn{display:block;font-size:14px;font-weight:500;margin-bottom:5px;margin-top:8px;width:100%}.uppy-size--md .uppy-Dashboard-browseBtn{font-size:15px;margin:15px auto;padding:13px 44px;width:auto}.uppy-Dashboard-AddFiles-list{display:flex;flex:1;flex-direction:column;margin-top:2px;overflow-y:auto;padding:2px 0;width:100%;-webkit-overflow-scrolling:touch}.uppy-size--md .uppy-Dashboard-AddFiles-list{flex:none;flex-direction:row;flex-wrap:wrap;justify-content:center;margin-top:15px;max-width:600px;overflow-y:visible;padding-top:0}.uppy-DashboardTab{border-bottom:1px solid #eaeaea;text-align:center;width:100%}[data-uppy-theme=dark] .uppy-DashboardTab{border-bottom:1px solid #333}.uppy-size--md .uppy-DashboardTab{border-bottom:none;display:inline-block;margin-bottom:10px;width:auto}.uppy-DashboardTab-btn{align-items:center;-webkit-appearance:none;appearance:none;background-color:initial;color:#525252;cursor:pointer;flex-direction:row;height:100%;justify-content:left;padding:12px 15px;width:100%}.uppy-DashboardTab-btn:focus{outline:none}.uppy-size--md .uppy-DashboardTab-btn{border-radius:5px;flex-direction:column;margin-inline-end:1px;padding:10px 3px;width:86px}[data-uppy-theme=dark] .uppy-DashboardTab-btn{color:#eaeaea}.uppy-DashboardTab-btn::-moz-focus-inner{border:0}.uppy-DashboardTab-btn:hover{background-color:#e9ecef}[data-uppy-theme=dark] .uppy-DashboardTab-btn:hover{background-color:#333}.uppy-DashboardTab-btn:active,.uppy-DashboardTab-btn:focus{background-color:#dfe6f1}[data-uppy-theme=dark] .uppy-DashboardTab-btn:active,[data-uppy-theme=dark] .uppy-DashboardTab-btn:focus{background-color:#525252}.uppy-DashboardTab-btn svg{display:inline-block;max-height:100%;max-width:100%;overflow:hidden;transition:transform .15s ease-in-out;vertical-align:text-top}.uppy-DashboardTab-inner{align-items:center;background-color:#fff;border-radius:8px;box-shadow:0 1px 1px #0000001a,0 1px 2px #0000001a,0 2px 3px #00000005;display:flex;height:32px;justify-content:center;margin-inline-end:10px;width:32px}.uppy-size--md .uppy-DashboardTab-inner{margin-inline-end:0}[data-uppy-theme=dark] .uppy-DashboardTab-inner{background-color:#323232;box-shadow:0 1px 1px #0003,0 1px 2px #0003,0 2px 3px #00000014}.uppy-DashboardTab-name{font-size:14px;font-weight:400}.uppy-size--md .uppy-DashboardTab-name{font-size:12px;line-height:15px;margin-bottom:0;margin-top:8px}.uppy-DashboardTab-iconMyDevice{color:#1269cf}[data-uppy-theme=dark] .uppy-DashboardTab-iconMyDevice{color:#02baf2}.uppy-DashboardTab-iconBox{color:#0061d5}[data-uppy-theme=dark] .uppy-DashboardTab-iconBox{color:#eaeaea}.uppy-DashboardTab-iconDropbox{color:#0061fe}[data-uppy-theme=dark] .uppy-DashboardTab-iconDropbox{color:#eaeaea}.uppy-DashboardTab-iconUnsplash{color:#111}[data-uppy-theme=dark] .uppy-DashboardTab-iconUnsplash{color:#eaeaea}.uppy-DashboardTab-iconWebdav{color:#111}[data-uppy-theme=dark] .uppy-DashboardTab-iconWebdav{color:#eaeaea}.uppy-DashboardTab-iconScreenRec{color:#2c3e50}[data-uppy-theme=dark] .uppy-DashboardTab-iconScreenRec{color:#eaeaea}.uppy-DashboardTab-iconAudio{color:#8030a3}[data-uppy-theme=dark] .uppy-DashboardTab-iconAudio{color:#bf6ee3}.uppy-Dashboard-input{height:.1px;opacity:0;overflow:hidden;position:absolute;width:.1px;z-index:-1}.uppy-DashboardContent-bar{align-items:center;background-color:#fafafa;border-bottom:1px solid #eaeaea;display:flex;flex-shrink:0;height:40px;justify-content:space-between;padding:0 10px;position:relative;width:100%;z-index:1004}.uppy-size--md .uppy-DashboardContent-bar{height:50px;padding:0 15px}[data-uppy-theme=dark] .uppy-DashboardContent-bar{background-color:#1f1f1f;border-bottom:1px solid #333}.uppy-DashboardContent-title{font-size:12px;font-weight:500;left:0;line-height:40px;margin:auto;max-width:170px;overflow-x:hidden;position:absolute;right:0;text-align:center;text-overflow:ellipsis;top:0;white-space:nowrap;width:100%}.uppy-size--md .uppy-DashboardContent-title{font-size:14px;line-height:50px;max-width:300px}[data-uppy-theme=dark] .uppy-DashboardContent-title{color:#eaeaea}.uppy-DashboardContent-back,.uppy-DashboardContent-save{-webkit-appearance:none;background:none;border:0;border-radius:3px;color:inherit;color:#1269cf;cursor:pointer;font-family:inherit;font-size:inherit;font-size:12px;font-weight:400;line-height:1;margin:0;margin-inline-start:-6px;padding:7px 6px}.uppy-DashboardContent-back:focus,.uppy-DashboardContent-save:focus{outline:none}.uppy-DashboardContent-back::-moz-focus-inner,.uppy-DashboardContent-save::-moz-focus-inner{border:0}.uppy-DashboardContent-back:hover,.uppy-DashboardContent-save:hover{color:#0e51a0}.uppy-DashboardContent-back:focus,.uppy-DashboardContent-save:focus{background-color:#dfe6f1}[data-uppy-theme=dark] .uppy-DashboardContent-back:focus,[data-uppy-theme=dark] .uppy-DashboardContent-save:focus{background-color:#333}.uppy-size--md .uppy-DashboardContent-back,.uppy-size--md .uppy-DashboardContent-save{font-size:14px}[data-uppy-theme=dark] .uppy-DashboardContent-back,[data-uppy-theme=dark] .uppy-DashboardContent-save{color:#02baf2}.uppy-DashboardContent-addMore{-webkit-appearance:none;background:none;border:0;border-radius:3px;color:inherit;color:#1269cf;cursor:pointer;font-family:inherit;font-size:inherit;font-weight:500;height:29px;line-height:1;margin:0;margin-inline-end:-5px;padding:7px 8px;width:29px}.uppy-DashboardContent-addMore:focus{outline:none}.uppy-DashboardContent-addMore::-moz-focus-inner{border:0}.uppy-DashboardContent-addMore:hover{color:#0e51a0}.uppy-DashboardContent-addMore:focus{background-color:#dfe6f1}[data-uppy-theme=dark] .uppy-DashboardContent-addMore:focus{background-color:#333}.uppy-size--md .uppy-DashboardContent-addMore{font-size:14px;height:auto;margin-inline-end:-8px;width:auto}[data-uppy-theme=dark] .uppy-DashboardContent-addMore{color:#02baf2}.uppy-DashboardContent-addMore svg{margin-inline-end:4px;vertical-align:initial}.uppy-size--md .uppy-DashboardContent-addMore svg{height:11px;width:11px}.uppy-DashboardContent-addMoreCaption{display:none}.uppy-size--md .uppy-DashboardContent-addMoreCaption{display:inline}.uppy-DashboardContent-panel{background-color:#f5f5f5;flex:1}.uppy-Dashboard-AddFilesPanel,.uppy-DashboardContent-panel{border-radius:5px;display:flex;flex-direction:column;inset:0;overflow:hidden;position:absolute;z-index:1005}.uppy-Dashboard-AddFilesPanel{background:#fafafa;background:linear-gradient(0deg,#fafafa 35%,#fafafad9);box-shadow:0 0 10px 5px #00000026}[data-uppy-theme=dark] .uppy-Dashboard-AddFilesPanel{background-color:#333;background-image:linear-gradient(0deg,#1f1f1f 35%,#1f1f1fd9)}.uppy-Dashboard--isAddFilesPanelVisible .uppy-Dashboard-files{filter:blur(2px)}.uppy-Dashboard-progress{bottom:0;height:12%;left:0;position:absolute;width:100%}.uppy-Dashboard-progressBarContainer.is-active{height:100%;left:0;position:absolute;top:0;width:100%;z-index:1004}.uppy-Dashboard-filesContainer{flex:1;margin:0;overflow-y:hidden;position:relative}.uppy-Dashboard-filesContainer:after{clear:both;content:"";display:table}.uppy-Dashboard-files{flex:1;margin:0;overflow-y:auto;padding:0 0 10px;-webkit-overflow-scrolling:touch}.uppy-size--md .uppy-Dashboard-files{padding-top:10px}.uppy-Dashboard--singleFile .uppy-Dashboard-filesInner{align-items:center;display:flex;height:100%;justify-content:center}.uppy-Dashboard-dropFilesHereHint{align-items:center;background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='48' height='48'%3E%3Cpath fill='%231269CF' d='M24 1v1C11.85 2 2 11.85 2 24s9.85 22 22 22 22-9.85 22-22S36.15 2 24 2zm0 0V0c13.254 0 24 10.746 24 24S37.254 48 24 48 0 37.254 0 24 10.746 0 24 0zm7.707 19.293a.999.999 0 1 1-1.414 1.414L25 16.414V34a1 1 0 1 1-2 0V16.414l-5.293 5.293a.999.999 0 1 1-1.414-1.414l7-7a1 1 0 0 1 1.414 0z'/%3E%3C/svg%3E");background-position:50% 50%;background-repeat:no-repeat;border:1px dashed #1269cf;border-radius:3px;color:#757575;display:flex;font-size:16px;justify-content:center;inset:7px;padding-top:90px;position:absolute;text-align:center;visibility:hidden;z-index:2000}[data-uppy-theme=dark] .uppy-Dashboard-dropFilesHereHint{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='48' height='48'%3E%3Cpath fill='%2302BAF2' d='M24 1v1C11.85 2 2 11.85 2 24s9.85 22 22 22 22-9.85 22-22S36.15 2 24 2zm0 0V0c13.254 0 24 10.746 24 24S37.254 48 24 48 0 37.254 0 24 10.746 0 24 0zm7.707 19.293a.999.999 0 1 1-1.414 1.414L25 16.414V34a1 1 0 1 1-2 0V16.414l-5.293 5.293a.999.999 0 1 1-1.414-1.414l7-7a1 1 0 0 1 1.414 0z'/%3E%3C/svg%3E");border-color:#02baf2;color:#bbb}.uppy-Dashboard.uppy-Dashboard--isDraggingOver .uppy-Dashboard-dropFilesHereHint{pointer-events:none;visibility:visible}.uppy-Dashboard.uppy-Dashboard--isDraggingOver .uppy-Dashboard-files,.uppy-Dashboard.uppy-Dashboard--isDraggingOver .uppy-Dashboard-progressindicators,.uppy-Dashboard.uppy-Dashboard--isDraggingOver .uppy-Dashboard-serviceMsg,.uppy-Dashboard.uppy-Dashboard--isDraggingOver .uppy-DashboardContent-bar{opacity:.15}.uppy-Dashboard.uppy-Dashboard--isDraggingOver .uppy-Dashboard-AddFiles{opacity:.03}.uppy-Dashboard-AddFiles-title{color:#000;font-size:17px;font-weight:500;line-height:1.35;margin-bottom:5px;margin-top:15px;padding:0 15px;text-align:inline-start;width:100%}.uppy-size--md .uppy-Dashboard-AddFiles-title{font-size:21px;font-weight:400;margin-top:5px;max-width:480px;padding:0 35px;text-align:center}[data-uppy-num-acquirers="0"] .uppy-Dashboard-AddFiles-title{text-align:center}[data-uppy-theme=dark] .uppy-Dashboard-AddFiles-title{color:#eaeaea}.uppy-Dashboard-AddFiles-title button{font-weight:500}.uppy-size--md .uppy-Dashboard-AddFiles-title button{font-weight:400}.uppy-Dashboard-note{color:#757575;font-size:14px;line-height:1.25;margin:auto;max-width:350px;padding:0 15px;text-align:center}.uppy-size--md .uppy-Dashboard-note{line-height:1.35;max-width:600px}[data-uppy-theme=dark] .uppy-Dashboard-note{color:#cfcfcf}a.uppy-Dashboard-poweredBy{color:#939393;display:inline-block;font-size:11px;margin-top:8px;text-align:center;text-decoration:none}.uppy-Dashboard-poweredByIcon{margin-left:1px;margin-right:1px;opacity:.9;position:relative;top:1px;vertical-align:text-top;fill:none;stroke:#939393}.uppy-Dashboard-Item-previewIcon{height:25px;left:50%;position:absolute;top:50%;transform:translate(-50%,-50%);width:25px;z-index:100}.uppy-size--md .uppy-Dashboard-Item-previewIcon{height:38px;width:38px}.uppy-Dashboard-Item-previewIcon svg{height:100%;width:100%}.uppy-Dashboard--singleFile .uppy-Dashboard-Item-previewIcon{height:100%;max-height:60%;max-width:60%;width:100%}.uppy-Dashboard-Item-previewIconWrap{height:76px;max-height:75%;position:relative}.uppy-Dashboard--singleFile .uppy-Dashboard-Item-previewIconWrap{height:100%;width:100%}.uppy-Dashboard-Item-previewIconBg{filter:drop-shadow(rgba(0,0,0,.1) 0 1px 1px);height:100%;width:100%}.uppy-Dashboard-upload{height:50px;position:relative;width:50px}.uppy-size--md .uppy-Dashboard-upload{height:60px;width:60px}.uppy-Dashboard-upload .uppy-c-icon{position:relative;top:1px;width:50%}.uppy-Dashboard-uploadCount{background-color:#1bb240;border-radius:50%;color:#fff;font-size:8px;height:16px;inset-inline-end:-12px;line-height:16px;position:absolute;top:-12px;width:16px}.uppy-size--md .uppy-Dashboard-uploadCount{font-size:9px;height:18px;line-height:18px;width:18px}:root{color-scheme:light dark;font-family:Inter,ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,Segoe UI,sans-serif;background:#08101f;color:#e8eefc}*{box-sizing:border-box}body{min-height:100vh;margin:0;background:radial-gradient(circle at top left,#1b3261 0,transparent 38rem),#08101f}.shell{width:min(48rem,calc(100% - 2rem));margin:0 auto;padding:3rem 0 5rem}header{margin-bottom:2rem}.eyebrow,.label{color:#96b6ff;font-size:.78rem;font-weight:700;letter-spacing:.08em;text-transform:uppercase}h1,h2,p{margin-top:0}h1{margin-bottom:.65rem;font-size:clamp(2rem,8vw,3.6rem);line-height:1}h2{font-size:1.15rem}.lede,.hint,#next-action{color:#b6c4df;line-height:1.55}.card{margin-top:1rem;padding:1.4rem;border:1px solid #2c3d60;border-radius:1rem;background:#101a2dbf;box-shadow:0 1rem 3rem #0004}.facts{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:.85rem 1.25rem;margin:1rem 0}.facts div{min-width:0}.facts dt{margin-bottom:.2rem;color:#8fa0bd;font-size:.8rem}.facts dd{margin:0;overflow-wrap:anywhere;font-weight:650}.compact{grid-template-columns:1fr}#uppy-dashboard{margin-top:1rem}.state{min-height:1.5rem;margin:.8rem 0}.state.error{color:#ffaaa2}.state.success{color:#91e6bc}noscript{display:block;margin-top:1rem;color:#ffaaa2}@media(max-width:36rem){.shell{padding-top:1.5rem}.facts{grid-template-columns:1fr}}@media(prefers-reduced-motion:reduce){*,*:before,*:after{scroll-behavior:auto!important}} diff --git a/src/vidxp/assets/upload_page/upload-page.js b/src/vidxp/assets/upload_page/upload-page.js index 74e5d10..398cb72 100644 --- a/src/vidxp/assets/upload_page/upload-page.js +++ b/src/vidxp/assets/upload_page/upload-page.js @@ -1,8 +1,13 @@ -var ys=Object.create;var pr=Object.defineProperty;var bs=Object.getOwnPropertyDescriptor;var vs=Object.getOwnPropertyNames;var ws=Object.getPrototypeOf,Ss=Object.prototype.hasOwnProperty;var mo=r=>{throw TypeError(r)};var xs=(r,e,t)=>e in r?pr(r,e,{enumerable:!0,configurable:!0,writable:!0,value:t}):r[e]=t;var z=(r,e)=>()=>{try{return e||r((e={exports:{}}).exports,e),e.exports}catch(t){throw e=0,t}};var _s=(r,e,t,o)=>{if(e&&typeof e=="object"||typeof e=="function")for(let n of vs(e))!Ss.call(r,n)&&n!==t&&pr(r,n,{get:()=>e[n],enumerable:!(o=bs(e,n))||o.enumerable});return r};var Ne=(r,e,t)=>(t=r!=null?ys(ws(r)):{},_s(e||!r||!r.__esModule?pr(t,"default",{value:r,enumerable:!0}):t,r));var w=(r,e,t)=>xs(r,typeof e!="symbol"?e+"":e,t),hr=(r,e,t)=>e.has(r)||mo("Cannot "+t);var f=(r,e,t)=>(hr(r,e,"read from private field"),t?t.call(r):e.get(r)),S=(r,e,t)=>e.has(r)?mo("Cannot add the same private member more than once"):e instanceof WeakSet?e.add(r):e.set(r,t),P=(r,e,t,o)=>(hr(r,e,"write to private field"),o?o.call(r,t):e.set(r,t),t),v=(r,e,t)=>(hr(r,e,"access private method"),t);var Fo=z((yc,Uo)=>{"use strict";Uo.exports=function(e){if(typeof e!="number"||Number.isNaN(e))throw new TypeError(`Expected a number, got ${typeof e}`);let t=e<0,o=Math.abs(e);if(t&&(o=-o),o===0)return"0 B";let n=["B","KB","MB","GB","TB","PB","EB","ZB","YB"],i=Math.min(Math.floor(Math.log(o)/Math.log(1024)),n.length-1),s=Number(o/1024**i),a=n[i];return`${s>=10||s%1===0?Math.round(s):s.toFixed(1)} ${a}`}});var Oo=z((bc,Po)=>{"use strict";function Eo(r,e){this.text=r=r||"",this.hasWild=~r.indexOf("*"),this.separator=e,this.parts=r.split(e)}Eo.prototype.match=function(r){var e=!0,t=this.parts,o,n=t.length,i;if(typeof r=="string"||r instanceof String)if(!this.hasWild&&this.text!=r)e=!1;else{for(i=(r||"").split(this.separator),o=0;e&&o{var ks=Oo(),As=/[\/\+\.]/;Ro.exports=function(r,e){function t(o){var n=ks(o,r,As);return n&&n.length>=2}return e?t(e.split(";")[0]):t}});var $t=z((Ec,Io)=>{function Ls(r){var e=typeof r;return r!=null&&(e=="object"||e=="function")}Io.exports=Ls});var qo=z((Pc,No)=>{var Is=typeof global=="object"&&global&&global.Object===Object&&global;No.exports=Is});var Er=z((Oc,zo)=>{var Ns=qo(),qs=typeof self=="object"&&self&&self.Object===Object&&self,zs=Ns||qs||Function("return this")();zo.exports=zs});var Mo=z((Rc,Bo)=>{var Bs=Er(),Ms=function(){return Bs.Date.now()};Bo.exports=Ms});var $o=z((Tc,Do)=>{var Ds=/\s/;function $s(r){for(var e=r.length;e--&&Ds.test(r.charAt(e)););return e}Do.exports=$s});var Ho=z((jc,Go)=>{var Gs=$o(),Hs=/^\s+/;function Ws(r){return r&&r.slice(0,Gs(r)+1).replace(Hs,"")}Go.exports=Ws});var Pr=z((kc,Wo)=>{var Vs=Er(),Xs=Vs.Symbol;Wo.exports=Xs});var Yo=z((Ac,Ko)=>{var Vo=Pr(),Xo=Object.prototype,Ks=Xo.hasOwnProperty,Ys=Xo.toString,dt=Vo?Vo.toStringTag:void 0;function Js(r){var e=Ks.call(r,dt),t=r[dt];try{r[dt]=void 0;var o=!0}catch{}var n=Ys.call(r);return o&&(e?r[dt]=t:delete r[dt]),n}Ko.exports=Js});var Zo=z((Cc,Jo)=>{var Zs=Object.prototype,Qs=Zs.toString;function ea(r){return Qs.call(r)}Jo.exports=ea});var rn=z((Lc,tn)=>{var Qo=Pr(),ta=Yo(),ra=Zo(),oa="[object Null]",na="[object Undefined]",en=Qo?Qo.toStringTag:void 0;function ia(r){return r==null?r===void 0?na:oa:en&&en in Object(r)?ta(r):ra(r)}tn.exports=ia});var nn=z((Ic,on)=>{function sa(r){return r!=null&&typeof r=="object"}on.exports=sa});var an=z((Nc,sn)=>{var aa=rn(),la=nn(),ua="[object Symbol]";function ca(r){return typeof r=="symbol"||la(r)&&aa(r)==ua}sn.exports=ca});var dn=z((qc,cn)=>{var da=Ho(),ln=$t(),fa=an(),un=NaN,pa=/^[-+]0x[0-9a-f]+$/i,ha=/^0b[01]+$/i,ma=/^0o[0-7]+$/i,ga=parseInt;function ya(r){if(typeof r=="number")return r;if(fa(r))return un;if(ln(r)){var e=typeof r.valueOf=="function"?r.valueOf():r;r=ln(e)?e+"":e}if(typeof r!="string")return r===0?r:+r;r=da(r);var t=ha.test(r);return t||ma.test(r)?ga(r.slice(2),t?2:8):pa.test(r)?un:+r}cn.exports=ya});var hn=z((zc,pn)=>{var ba=$t(),Or=Mo(),fn=dn(),va="Expected a function",wa=Math.max,Sa=Math.min;function xa(r,e,t){var o,n,i,s,a,l,p=0,m=!1,h=!1,U=!0;if(typeof r!="function")throw new TypeError(va);e=fn(e)||0,ba(t)&&(m=!!t.leading,h="maxWait"in t,i=h?wa(fn(t.maxWait)||0,e):i,U="trailing"in t?!!t.trailing:U);function j(k){var N=o,te=n;return o=n=void 0,p=k,s=r.apply(te,N),s}function O(k){return p=k,a=setTimeout(B,e),m?j(k):s}function T(k){var N=k-l,te=k-p,Z=e-N;return h?Sa(Z,i-te):Z}function I(k){var N=k-l,te=k-p;return l===void 0||N>=e||N<0||h&&te>=i}function B(){var k=Or();if(I(k))return _(k);a=setTimeout(B,T(k))}function _(k){return a=void 0,U&&o?j(k):(o=n=void 0,s)}function F(){a!==void 0&&clearTimeout(a),p=0,o=l=n=a=void 0}function L(){return a===void 0?s:_(Or())}function K(){var k=Or(),N=I(k);if(o=arguments,n=this,l=k,N){if(a===void 0)return O(l);if(h)return clearTimeout(a),a=setTimeout(B,e),j(l)}return a===void 0&&(a=setTimeout(B,e)),s}return K.cancel=F,K.flush=L,K}pn.exports=xa});var Rr=z((Bc,mn)=>{var _a=hn(),Ua=$t(),Fa="Expected a function";function Ea(r,e,t){var o=!0,n=!0;if(typeof r!="function")throw new TypeError(Fa);return Ua(t)&&(o="leading"in t?!!t.leading:o,n="trailing"in t?!!t.trailing:n),_a(r,e,{leading:o,maxWait:e,trailing:n})}mn.exports=Ea});var yn=z((Mc,gn)=>{gn.exports=function(){var e={},t=e._fns={};e.emit=function(s,a,l,p,m,h,U){var j=o(s);j.length&&n(s,j,[a,l,p,m,h,U])},e.on=function(s,a){t[s]||(t[s]=[]),t[s].push(a)},e.once=function(s,a){function l(){a.apply(this,arguments),e.off(s,l)}this.on(s,l)},e.off=function(s,a){var l=[];if(s&&a){var p=this._fns[s],m=0,h=p?p.length:0;for(m;m{"use strict";pi.exports=function(e,t){if(t=t.split(":")[0],e=+e,!e)return!1;switch(t){case"http":case"ws":return e!==80;case"https":case"wss":return e!==443;case"ftp":return e!==21;case"gopher":return e!==70;case"file":return!1}return e!==0}});var yi=z(Wr=>{"use strict";var cl=Object.prototype.hasOwnProperty,dl;function mi(r){try{return decodeURIComponent(r.replace(/\+/g," "))}catch{return null}}function gi(r){try{return encodeURIComponent(r)}catch{return null}}function fl(r){for(var e=/([^=?#&]+)=?([^&]*)/g,t={},o;o=e.exec(r);){var n=mi(o[1]),i=mi(o[2]);n===null||i===null||n in t||(t[n]=i)}return t}function pl(r,e){e=e||"";var t=[],o,n;typeof e!="string"&&(e="?");for(n in r)if(cl.call(r,n)){if(o=r[n],!o&&(o===null||o===dl||isNaN(o))&&(o=""),n=gi(n),o=gi(o),n===null||o===null)continue;t.push(n+"="+o)}return t.length?e+t.join("&"):""}Wr.stringify=pl;Wr.parse=fl});var Fi=z((Od,Ui)=>{"use strict";var vi=hi(),tr=yi(),hl=/^[\x00-\x20\u00a0\u1680\u2000-\u200a\u2028\u2029\u202f\u205f\u3000\ufeff]+/,wi=/[\n\r\t]/g,ml=/^[A-Za-z][A-Za-z0-9+-.]*:\/\//,Si=/:\d+$/,gl=/^([a-z][a-z0-9.+-]*:)?(\/\/)?([\\/]+)?([\S\s]*)/i,yl=/^[a-zA-Z]:/;function Xr(r){return(r||"").toString().replace(hl,"")}var Vr=[["#","hash"],["?","query"],function(e,t){return ae(t.protocol)?e.replace(/\\/g,"/"):e},["/","pathname"],["@","auth",1],[NaN,"host",void 0,1,1],[/:(\d*)$/,"port",void 0,1],[NaN,"hostname",void 0,1,1]],bi={hash:1,query:1};function xi(r){var e;typeof window<"u"?e=window:typeof global<"u"?e=global:typeof self<"u"?e=self:e={};var t=e.location||{};r=r||t;var o={},n=typeof r,i;if(r.protocol==="blob:")o=new le(unescape(r.pathname),{});else if(n==="string"){o=new le(r,{});for(i in bi)delete o[i]}else if(n==="object"){for(i in r)i in bi||(o[i]=r[i]);o.slashes===void 0&&(o.slashes=ml.test(r.href))}return o}function ae(r){return r==="file:"||r==="ftp:"||r==="http:"||r==="https:"||r==="ws:"||r==="wss:"}function _i(r,e){r=Xr(r),r=r.replace(wi,""),e=e||{};var t=gl.exec(r),o=t[1]?t[1].toLowerCase():"",n=!!t[2],i=!!t[3],s=0,a;return n?i?(a=t[2]+t[3]+t[4],s=t[2].length+t[3].length):(a=t[2]+t[4],s=t[2].length):i?(a=t[3]+t[4],s=t[3].length):a=t[4],o==="file:"?s>=2&&(a=a.slice(2)):ae(o)?a=t[4]:o?n&&(a=a.slice(2)):s>=2&&ae(e.protocol)&&(a=t[4]),{protocol:o,slashes:n||ae(o),slashesCount:s,rest:a}}function bl(r,e){if(r==="")return e;for(var t=(e||"/").split("/").slice(0,-1).concat(r.split("/")),o=t.length,n=t[o-1],i=!1,s=0;o--;)t[o]==="."?t.splice(o,1):t[o]===".."?(t.splice(o,1),s++):s&&(o===0&&(i=!0),t.splice(o,1),s--);return i&&t.unshift(""),(n==="."||n==="..")&&t.push(""),t.join("/")}function le(r,e,t){if(r=Xr(r),r=r.replace(wi,""),!(this instanceof le))return new le(r,e,t);var o,n,i,s,a,l,p=Vr.slice(),m=typeof e,h=this,U=0;for(m!=="object"&&m!=="string"&&(t=e,e=null),t&&typeof t!="function"&&(t=tr.parse),e=xi(e),n=_i(r||"",e),o=!n.protocol&&!n.slashes,h.slashes=n.slashes||o&&e.slashes,h.protocol=n.protocol||e.protocol||"",r=n.rest,(n.protocol==="file:"&&(n.slashesCount!==2||yl.test(r))||!n.slashes&&(n.protocol||n.slashesCount<2||!ae(h.protocol)))&&(p[3]=[/(.*)/,"pathname"]);U"error"in r&&!!r.error,Fs=r=>r.progress.uploadComplete;function yr(r){return r.filter(e=>!Us(e)&&!Fs(e))}function br(r){return r.filter(e=>!e.progress?.uploadStarted||!e.isRestored)}function ze(r){let e=r.lastIndexOf(".");return e===-1||e===r.length-1?{name:r,extension:void 0}:{name:r.slice(0,e),extension:r.slice(e+1)}}var vr={__proto__:null,md:"text/markdown",markdown:"text/markdown",mp4:"video/mp4",mp3:"audio/mp3",svg:"image/svg+xml",jpg:"image/jpeg",png:"image/png",webp:"image/webp",gif:"image/gif",heic:"image/heic",heif:"image/heif",yaml:"text/yaml",yml:"text/yaml",csv:"text/csv",tsv:"text/tab-separated-values",tab:"text/tab-separated-values",avi:"video/x-msvideo",mks:"video/x-matroska",mkv:"video/x-matroska",mov:"video/quicktime",dicom:"application/dicom",doc:"application/msword",msg:"application/vnd.ms-outlook",docm:"application/vnd.ms-word.document.macroenabled.12",docx:"application/vnd.openxmlformats-officedocument.wordprocessingml.document",dot:"application/msword",dotm:"application/vnd.ms-word.template.macroenabled.12",dotx:"application/vnd.openxmlformats-officedocument.wordprocessingml.template",xla:"application/vnd.ms-excel",xlam:"application/vnd.ms-excel.addin.macroenabled.12",xlc:"application/vnd.ms-excel",xlf:"application/x-xliff+xml",xlm:"application/vnd.ms-excel",xls:"application/vnd.ms-excel",xlsb:"application/vnd.ms-excel.sheet.binary.macroenabled.12",xlsm:"application/vnd.ms-excel.sheet.macroenabled.12",xlsx:"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",xlt:"application/vnd.ms-excel",xltm:"application/vnd.ms-excel.template.macroenabled.12",xltx:"application/vnd.openxmlformats-officedocument.spreadsheetml.template",xlw:"application/vnd.ms-excel",txt:"text/plain",text:"text/plain",conf:"text/plain",log:"text/plain",pdf:"application/pdf",zip:"application/zip","7z":"application/x-7z-compressed",rar:"application/x-rar-compressed",tar:"application/x-tar",gz:"application/gzip",dmg:"application/x-apple-diskimage"};function Be(r){if(r.type)return r.type;let e=r.name?ze(r.name).extension?.toLowerCase():null;return e&&e in vr?vr[e]:"application/octet-stream"}function Es(r){return r.charCodeAt(0).toString(32)}function go(r){let e="";return r.replace(/[^A-Z0-9]/gi,t=>(e+=`-${Es(t)}`,"/"))+e}function wr(r,e){let t=e||"uppy";return typeof r.name=="string"&&(t+=`-${go(r.name.toLowerCase())}`),r.type!==void 0&&(t+=`-${r.type}`),r.meta&&typeof r.meta.relativePath=="string"&&(t+=`-${go(r.meta.relativePath.toLowerCase())}`),r.data?.size!==void 0&&(t+=`-${r.data.size}`),r.data.lastModified!==void 0&&(t+=`-${r.data.lastModified}`),t}function Ps(r){return!r.isRemote||!r.remote?!1:new Set(["box","dropbox","drive","facebook","unsplash"]).has(r.remote.provider)}function Sr(r,e){if(Ps(r))return r.id;let t=Be(r);return wr({...r,type:t},e)}function qt(r,e){return r===!0?Object.keys(e):Array.isArray(r)?r:[]}function xr(r){return r<10?`0${r}`:r.toString()}function Ue(){let r=new Date,e=xr(r.getHours()),t=xr(r.getMinutes()),o=xr(r.getSeconds());return`${e}:${t}:${o}`}function Os(r){return r?r.readyState===4&&r.status===0:!1}var _r=Os;function Rs(r){return new Error("Cancelled",{cause:r})}function yo(r){if(r!=null){let e=()=>this.abort(r.reason);r.addEventListener("abort",e,{once:!0});let t=()=>{r.removeEventListener("abort",e)};this.then?.(t,t)}return this}var ue,ne,ce,Me,ie,De,Fe,W,Ur,it,bo,vo,wo,zt,$e,st=class{constructor(e){S(this,W);S(this,ue,0);S(this,ne,[]);S(this,ce,!1);S(this,Me);S(this,ie,1);S(this,De);S(this,Fe);w(this,"limit");S(this,zt,()=>this.resume());S(this,$e,()=>{if(f(this,ce)){P(this,Fe,setTimeout(f(this,$e),0));return}P(this,ie,this.limit),this.limit=Math.ceil((f(this,De)+f(this,ie))/2);for(let e=f(this,ie);e<=this.limit;e++)v(this,W,it).call(this);f(this,De)-f(this,ie)>3?P(this,Fe,setTimeout(f(this,$e),2e3)):P(this,ie,Math.floor(f(this,ie)/2))});typeof e!="number"||e===0?this.limit=1/0:this.limit=e}run(e,t){return!f(this,ce)&&f(this,ue){let n=this.run(()=>(e(...o),queueMicrotask(()=>n.done()),()=>{}),t);return{abortOn:yo,abort(){n.abort()}}}}wrapPromiseFunction(e,t){return(...o)=>{let n,i=new Promise((s,a)=>{n=this.run(()=>{let l,p;try{p=Promise.resolve(e(...o))}catch(m){p=Promise.reject(m)}return p.then(m=>{l?a(l):(n.done(),s(m))},m=>{l?a(l):(n.done(),a(m))}),m=>{l=Rs(m)}},t)});return i.abort=s=>{n.abort(s)},i.abortOn=yo,i}}resume(){P(this,ce,!1),clearTimeout(f(this,Me));for(let e=0;e1&&Number.isFinite(this.limit)&&(P(this,De,this.limit-1),this.limit=f(this,ie),P(this,Fe,setTimeout(f(this,$e),e)))}get isPaused(){return f(this,ce)}};ue=new WeakMap,ne=new WeakMap,ce=new WeakMap,Me=new WeakMap,ie=new WeakMap,De=new WeakMap,Fe=new WeakMap,W=new WeakSet,Ur=function(e){P(this,ue,f(this,ue)+1);let t=!1,o;try{o=e()}catch(n){throw P(this,ue,f(this,ue)-1),n}return{abort:n=>{t||(t=!0,P(this,ue,f(this,ue)-1),o?.(n),v(this,W,it).call(this))},done:()=>{t||(t=!0,P(this,ue,f(this,ue)-1),v(this,W,it).call(this))}}},it=function(){queueMicrotask(()=>v(this,W,bo).call(this))},bo=function(){if(f(this,ce)||f(this,ue)>=this.limit||f(this,ne).length===0)return;let e=f(this,ne).shift();if(e==null)throw new Error("Invariant violation: next is null");let t=v(this,W,Ur).call(this,e.fn);e.abort=t.abort,e.done=t.done},vo=function(e,t){let o={fn:e,priority:t?.priority||0,abort:()=>{v(this,W,wo).call(this,o)},done:()=>{throw new Error("Cannot mark a queued request as done: this indicates a bug")}},n=f(this,ne).findIndex(i=>o.priority>i.priority);return n===-1?f(this,ne).push(o):f(this,ne).splice(n,0,o),o},wo=function(e){let t=f(this,ne).indexOf(e);t!==-1&&f(this,ne).splice(t,1)},zt=new WeakMap,$e=new WeakMap;function Ts(r,e,t){let o=[];return r.forEach(n=>typeof n!="string"?o.push(n):e[Symbol.split](n).forEach((i,s,a)=>{i!==""&&o.push(i),s{throw new Error(`missing string: ${r}`)},at,lt,Fr,fe=class{constructor(e,{onMissingKey:t=js}={}){S(this,lt);w(this,"locale");S(this,at);this.locale={strings:{},pluralize(o){return o===1?0:1}},Array.isArray(e)?e.forEach(v(this,lt,Fr),this):v(this,lt,Fr).call(this,e),P(this,at,t)}translate(e,t){return this.translateArray(e,t).join("")}translateArray(e,t){let o=this.locale.strings[e];if(o==null&&(f(this,at).call(this,e),o=e),typeof o=="object"){if(t&&typeof t.smart_count<"u"){let i=this.locale.pluralize(t.smart_count);return So(o[i],t)}throw new Error("Attempted to use a string with plural forms, but no value was given for %{smart_count}")}if(typeof o!="string")throw new Error("string was not a string");return So(o,t)}};at=new WeakMap,lt=new WeakSet,Fr=function(e){if(!e?.strings)return;let t=this.locale;Object.assign(this.locale,{strings:{...t.strings,...e.strings},pluralize:e.pluralize||t.pluralize})};var pe=class{constructor(e,t){w(this,"uppy");w(this,"opts");w(this,"id");w(this,"defaultLocale");w(this,"i18n");w(this,"i18nArray");w(this,"type");w(this,"VERSION");this.uppy=e,this.opts=t??{}}getPluginState(){let{plugins:e}=this.uppy.getState();return e?.[this.id]||{}}setPluginState(e){let{plugins:t}=this.uppy.getState();this.uppy.setState({plugins:{...t,[this.id]:{...t[this.id],...e}}})}setOptions(e){this.opts={...this.opts,...e},this.setPluginState(void 0),this.i18nInit()}i18nInit(){let e=new fe([this.defaultLocale,this.uppy.locale,this.opts.locale]);this.i18n=e.translate.bind(e),this.i18nArray=e.translateArray.bind(e),this.setPluginState(void 0)}addTarget(e){throw new Error("Extend the addTarget method to add your plugin to another plugin's target")}install(){}uninstall(){}update(e){}afterUpdate(){}};var se,ut,Ge=class{constructor(e){S(this,se);S(this,ut,[]);P(this,se,e)}on(e,t){return f(this,ut).push([e,t]),f(this,se).on(e,t)}remove(){for(let[e,t]of f(this,ut).splice(0))f(this,se).off(e,t)}onFilePause(e,t){this.on("upload-pause",(o,n)=>{e===o?.id&&t(n)})}onFileRemove(e,t){this.on("file-removed",o=>{e===o.id&&t(o.id)})}onPause(e,t){this.on("upload-pause",(o,n)=>{e===o?.id&&t(n)})}onRetry(e,t){this.on("upload-retry",o=>{e===o?.id&&t()})}onRetryAll(e,t){this.on("retry-all",()=>{f(this,se).getFile(e)&&t()})}onPauseAll(e,t){this.on("pause-all",()=>{f(this,se).getFile(e)&&t()})}onCancelAll(e,t){this.on("cancel-all",(...o)=>{f(this,se).getFile(e)&&t(...o)})}onResumeAll(e,t){this.on("resume-all",()=>{f(this,se).getFile(e)&&t()})}};se=new WeakMap,ut=new WeakMap;var xo={debug:()=>{},warn:()=>{},error:(...r)=>console.error(`[Uppy] [${Ue()}]`,...r)},_o={debug:(...r)=>console.debug(`[Uppy] [${Ue()}]`,...r),warn:(...r)=>console.warn(`[Uppy] [${Ue()}]`,...r),error:(...r)=>console.error(`[Uppy] [${Ue()}]`,...r)};var ct=Ne(Fo(),1),jo=Ne(To(),1);var ko={maxFileSize:null,minFileSize:null,maxTotalFileSize:null,maxNumberOfFiles:null,minNumberOfFiles:null,allowedFileTypes:null,requiredMetaFields:[]},Y=class extends Error{constructor(t,o){super(t);w(this,"isUserFacing");w(this,"file");w(this,"isRestriction",!0);this.isUserFacing=o?.isUserFacing??!0,o?.file&&(this.file=o.file)}},Bt=class{constructor(e,t){w(this,"getI18n");w(this,"getOpts");this.getI18n=t,this.getOpts=()=>{let o=e();if(o.restrictions?.allowedFileTypes!=null&&!Array.isArray(o.restrictions.allowedFileTypes))throw new TypeError("`restrictions.allowedFileTypes` must be an array");return o}}validateAggregateRestrictions(e,t){let{maxTotalFileSize:o,maxNumberOfFiles:n}=this.getOpts().restrictions;if(n&&e.filter(s=>!s.isGhost).length+t.length>n)throw new Y(`${this.getI18n()("youCanOnlyUploadX",{smart_count:n})}`);if(o){let i=[...e,...t].reduce((s,a)=>s+(a.size??0),0);if(i>o)throw new Y(this.getI18n()("aggregateExceedsSize",{sizeAllowed:(0,ct.default)(o),size:(0,ct.default)(i)}))}}validateSingleFile(e){let{maxFileSize:t,minFileSize:o,allowedFileTypes:n}=this.getOpts().restrictions;if(n&&!n.some(s=>s.includes("/")?e.type?(0,jo.default)(e.type.replace(/;.*?$/,""),s):!1:s[0]==="."&&e.extension?e.extension.toLowerCase()===s.slice(1).toLowerCase():!1)){let s=n.join(", ");throw new Y(this.getI18n()("youCanOnlyUploadFileTypes",{types:s}),{file:e})}if(t&&e.size!=null&&e.size>t)throw new Y(this.getI18n()("exceedsSize",{size:(0,ct.default)(t),file:e.name??this.getI18n()("unnamed")}),{file:e});if(o&&e.size!=null&&e.size{this.validateSingleFile(o)}),this.validateAggregateRestrictions(e,t)}validateMinNumberOfFiles(e){let{minNumberOfFiles:t}=this.getOpts().restrictions;if(t&&Object.keys(e).length{f(this,He).delete(e)}}};He=new WeakMap,Dt=new WeakSet,Co=function(...e){f(this,He).forEach(t=>{t(...e)})},w(Mt,"VERSION",Ao.version);var Lo=Mt;var Sn=Ne(Rr(),1),xn=Ne(yn(),1);var Pa="useandom-26T198340PX75pxJACKVERYMINDBUSHWOLF_GQZbfghjklqvwyzrict";var bn=(r=21)=>{let e="",t=r|0;for(;t-- >0;)e+=Pa[Math.random()*64|0];return e};var vn={name:"@uppy/core",description:"Core module for the extensible JavaScript file upload widget with support for drag&drop, resumable uploads, previews, restrictions, file processing/encoding, remote providers like Instagram, Dropbox, Google Drive, S3 and more :dog:",version:"5.2.0",license:"MIT",style:"dist/style.min.css",type:"module",sideEffects:["*.css"],scripts:{build:"tsc --build tsconfig.build.json","build:css":"sass --load-path=../../ src/style.scss dist/style.css && postcss dist/style.css -u cssnano -o dist/style.min.css",typecheck:"tsc --build",test:"vitest run --environment=jsdom --silent='passed-only'"},keywords:["file uploader","uppy","uppy-plugin"],homepage:"https://uppy.io",bugs:{url:"https://github.com/transloadit/uppy/issues"},repository:{type:"git",url:"git+https://github.com/transloadit/uppy.git"},files:["src","lib","dist","CHANGELOG.md"],exports:{".":"./lib/index.js","./css/style.min.css":"./dist/style.min.css","./css/style.css":"./dist/style.css","./css/style.scss":"./src/style.scss","./package.json":"./package.json"},dependencies:{"@transloadit/prettier-bytes":"^0.3.4","@uppy/store-default":"^5.0.0","@uppy/utils":"^7.1.4",lodash:"^4.17.21","mime-match":"^1.0.2","namespace-emitter":"^2.0.1",nanoid:"^5.0.9",preact:"^10.5.13"},devDependencies:{"@types/deep-freeze":"^0",cssnano:"^7.0.7","deep-freeze":"^0.0.1",jsdom:"^26.1.0",postcss:"^8.5.6","postcss-cli":"^11.0.1",sass:"^1.89.2",typescript:"^5.8.3",vitest:"^3.2.4"}};function Tr(r,e){return e.name?e.name:r.split("/")[0]==="image"?`${r.split("/")[0]}.${r.split("/")[1]}`:"noname"}var wn={strings:{addBulkFilesFailed:{0:"Failed to add %{smart_count} file due to an internal error",1:"Failed to add %{smart_count} files due to internal errors"},youCanOnlyUploadX:{0:"You can only upload %{smart_count} file",1:"You can only upload %{smart_count} files"},youHaveToAtLeastSelectX:{0:"You have to select at least %{smart_count} file",1:"You have to select at least %{smart_count} files"},aggregateExceedsSize:"You selected %{size} of files, but maximum allowed size is %{sizeAllowed}",exceedsSize:"%{file} exceeds maximum allowed size of %{size}",missingRequiredMetaField:"Missing required meta fields",missingRequiredMetaFieldOnFile:"Missing required meta fields in %{fileName}",inferiorSize:"This file is smaller than the allowed size of %{size}",youCanOnlyUploadFileTypes:"You can only upload: %{types}",noMoreFilesAllowed:"Cannot add more files",noDuplicates:"Cannot add the duplicate file '%{fileName}', it already exists",companionError:"Connection with Companion failed",authAborted:"Authentication aborted",companionUnauthorizeHint:"To unauthorize to your %{provider} account, please go to %{url}",failedToUpload:"Failed to upload %{file}",noInternetConnection:"No Internet connection",connectedToInternet:"Connected to the Internet",noFilesFound:"You have no files or folders here",noSearchResults:"Unfortunately, there are no results for this search",selectX:{0:"Select %{smart_count}",1:"Select %{smart_count}"},allFilesFromFolderNamed:"All files from folder %{name}",openFolderNamed:"Open folder %{name}",cancel:"Cancel",logOut:"Log out",logIn:"Log in",pickFiles:"Pick files",pickPhotos:"Pick photos",filter:"Filter",resetFilter:"Reset filter",loading:"Loading...",loadedXFiles:"Loaded %{numFiles} files",authenticateWithTitle:"Please authenticate with %{pluginName} to select files",authenticateWith:"Connect to %{pluginName}",signInWithGoogle:"Sign in with Google",searchImages:"Search for images",enterTextToSearch:"Enter text to search for images",search:"Search",resetSearch:"Reset search",emptyFolderAdded:"No files were added from empty folder",addedNumFiles:"Added %{numFiles} file(s)",folderAlreadyAdded:'The folder "%{folder}" was already added',folderAdded:{0:"Added %{smart_count} file from %{folder}",1:"Added %{smart_count} files from %{folder}"},additionalRestrictionsFailed:"%{count} additional restrictions were not fulfilled",unnamed:"Unnamed",pleaseWait:"Please wait"}};function jr(r){if(r==null&&typeof navigator<"u"&&(r=navigator.userAgent),!r)return!0;let e=/Edge\/(\d+\.\d+)/.exec(r);if(!e)return!0;let o=e[1].split(".",2),n=parseInt(o[0],10),i=parseInt(o[1],10);return n<15||n===15&&i<15063||n>18||n===18&&i>=18218}var Gt={totalProgress:0,allowNewUpload:!0,error:null,recoveredState:null},re,oe,ht,Pe,We,Ve,Oe,x,Ee,Ar,_n,Un,Fn,Cr,Lr,Ir,Nr,Ht,qr,Re,En,Pn,he,mt,ft,On,zr,pt,Wt=class Wt{constructor(e){S(this,x);S(this,re,Object.create(null));S(this,oe);S(this,ht);S(this,Pe,(0,xn.default)());S(this,We,new Set);S(this,Ve,new Set);S(this,Oe,new Set);w(this,"defaultLocale");w(this,"locale");w(this,"opts");w(this,"store");w(this,"i18n");w(this,"i18nArray");w(this,"scheduledAutoProceed",null);w(this,"wasOffline",!1);S(this,Ht,(e,t)=>{let o=e?this.getFile(e.id):void 0;if(e==null||!o){this.log(`Not setting progress for a file that has been removed: ${e?.id}`);return}if(o.progress.percentage===100){this.log(`Not setting progress for a file that has been already uploaded: ${e.id}`);return}let n={bytesTotal:t.bytesTotal,percentage:t.bytesTotal!=null&&Number.isFinite(t.bytesTotal)&&t.bytesTotal>0?Math.round(t.bytesUploaded/t.bytesTotal*100):void 0};o.progress.uploadStarted!=null?this.setFileState(e.id,{progress:{...o.progress,...n,bytesUploaded:t.bytesUploaded}}):this.setFileState(e.id,{progress:{...o.progress,...n}}),f(this,Re).call(this)});S(this,Re,(0,Sn.default)(()=>v(this,x,qr).call(this),500,{leading:!0,trailing:!0}));S(this,he,this.updateOnlineStatus.bind(this));S(this,mt,new Map);this.defaultLocale=wn;let t={id:"uppy",autoProceed:!1,allowMultipleUploadBatches:!0,debug:!1,restrictions:ko,meta:{},onBeforeFileAdded:(n,i)=>!Object.hasOwn(i,n.id),onBeforeUpload:n=>n,store:new Lo,logger:xo,infoTimeout:5e3},o={...t,...e};this.opts={...o,restrictions:{...t.restrictions,...e?.restrictions}},e?.logger&&e.debug?this.log("You are using a custom `logger`, but also set `debug: true`, which uses built-in logger to output logs to console. Ignoring `debug: true` and using your custom `logger`.","warning"):e?.debug&&(this.opts.logger=_o),this.log(`Using Core v${Wt.VERSION}`),this.i18nInit(),this.store=this.opts.store,this.setState({...Gt,plugins:{},files:{},currentUploads:{},capabilities:{uploadProgress:jr(),individualCancellation:!0,resumableUploads:!1},meta:{...this.opts.meta},info:[]}),P(this,oe,new Bt(()=>this.opts,()=>this.i18n)),P(this,ht,this.store.subscribe((n,i,s)=>{this.emit("state-update",n,i,s),this.updateAll(i)})),this.opts.debug&&typeof window<"u"&&(window[this.opts.id]=this),v(this,x,Pn).call(this)}emit(e,...t){f(this,Pe).emit(e,...t)}on(e,t){return f(this,Pe).on(e,t),this}once(e,t){return f(this,Pe).once(e,t),this}off(e,t){return f(this,Pe).off(e,t),this}updateAll(e){this.iteratePlugins(t=>{t.update(e)})}setState(e){this.store.setState(e)}getState(){return this.store.getState()}patchFilesState(e){let t=this.getState().files;this.setState({files:{...t,...Object.fromEntries(Object.entries(e).map(([o,n])=>[o,{...t[o],...n}]))}})}setFileState(e,t){if(!this.getState().files[e])throw new Error(`Can’t set state for ${e} (the file could have been removed)`);this.patchFilesState({[e]:t})}i18nInit(){let e=o=>this.log(`Missing i18n string: ${o}`,"error"),t=new fe([this.defaultLocale,this.opts.locale],{onMissingKey:e});this.i18n=t.translate.bind(t),this.i18nArray=t.translateArray.bind(t),this.locale=t.locale}setOptions(e){this.opts={...this.opts,...e,restrictions:{...this.opts.restrictions,...e?.restrictions}},e.meta&&this.setMeta(e.meta),this.i18nInit(),e.locale&&this.iteratePlugins(t=>{t.setOptions(e)}),this.setState(void 0)}resetProgress(){let e={percentage:0,bytesUploaded:!1,uploadComplete:!1,uploadStarted:null},t={...this.getState().files},o=Object.create(null);Object.keys(t).forEach(n=>{o[n]={...t[n],progress:{...t[n].progress,...e},tus:void 0,transloadit:void 0}}),this.setState({files:o,...Gt})}clear(){let{capabilities:e,currentUploads:t}=this.getState();if(Object.keys(t).length>0&&!e.individualCancellation)throw new Error("The installed uploader plugin does not allow removing files during an upload.");this.setState({...Gt,files:{}})}addPreProcessor(e){f(this,We).add(e)}removePreProcessor(e){return f(this,We).delete(e)}addPostProcessor(e){f(this,Oe).add(e)}removePostProcessor(e){return f(this,Oe).delete(e)}addUploader(e){f(this,Ve).add(e)}removeUploader(e){return f(this,Ve).delete(e)}setMeta(e){let t={...this.getState().meta,...e},o={...this.getState().files};Object.keys(o).forEach(n=>{o[n]={...o[n],meta:{...o[n].meta,...e}}}),this.log("Adding metadata:"),this.log(e),this.setState({meta:t,files:o})}setFileMeta(e,t){let o={...this.getState().files};if(!o[e]){this.log(`Was trying to set metadata for a file that has been removed: ${e}`);return}let n={...o[e].meta,...t};o[e]={...o[e],meta:n},this.setState({files:o})}getFile(e){return this.getState().files[e]}getFiles(){let{files:e}=this.getState();return Object.values(e)}getFilesByIds(e){return e.map(t=>this.getFile(t))}getObjectOfFilesPerState(){let{files:e,totalProgress:t,error:o}=this.getState(),n=Object.values(e),i=[],s=[],a=[],l=[],p=[],m=[],h=[],U=[],j=[];for(let O of n){let{progress:T}=O;!T.uploadComplete&&T.uploadStarted&&(i.push(O),O.isPaused||U.push(O)),T.uploadStarted||s.push(O),(T.uploadStarted||T.preprocess||T.postprocess)&&a.push(O),T.uploadStarted&&l.push(O),O.isPaused&&p.push(O),T.uploadComplete&&m.push(O),O.error&&h.push(O),(T.preprocess||T.postprocess)&&j.push(O)}return{newFiles:s,startedFiles:a,uploadStartedFiles:l,pausedFiles:p,completeFiles:m,erroredFiles:h,inProgressFiles:i,inProgressNotPausedFiles:U,processingFiles:j,isUploadStarted:l.length>0,isAllComplete:t===100&&m.length===n.length&&j.length===0,isAllErrored:!!o&&h.length===n.length,isAllPaused:i.length!==0&&p.length===i.length,isUploadInProgress:i.length>0,isSomeGhost:n.some(O=>O.isGhost)}}validateRestrictions(e,t=this.getFiles()){try{f(this,oe).validate(t,[e])}catch(o){return o}return null}validateSingleFile(e){try{f(this,oe).validateSingleFile(e)}catch(t){return t.message}return null}validateAggregateRestrictions(e){let t=this.getFiles();try{f(this,oe).validateAggregateRestrictions(t,e)}catch(o){return o.message}return null}checkIfFileAlreadyExists(e){let{files:t}=this.getState();return!!(t[e]&&!t[e].isGhost)}addFile(e){let{nextFilesState:t,validFilesToAdd:o,errors:n}=v(this,x,Lr).call(this,[e]),i=n.filter(a=>a.isRestriction);if(v(this,x,Ee).call(this,i),n.length>0)throw n[0];this.setState({files:t});let[s]=o;return this.emit("file-added",s),this.emit("files-added",o),this.log(`Added file: ${s.name}, ${s.id}, mime type: ${s.type}`),v(this,x,Cr).call(this),s.id}addFiles(e){let{nextFilesState:t,validFilesToAdd:o,errors:n}=v(this,x,Lr).call(this,e),i=n.filter(a=>a.isRestriction);v(this,x,Ee).call(this,i);let s=n.filter(a=>!a.isRestriction);if(s.length>0){let a=`Multiple errors occurred while adding files: -`;if(s.forEach(l=>{a+=` - * ${l.message}`}),this.info({message:this.i18n("addBulkFilesFailed",{smart_count:s.length}),details:a},"error",this.opts.infoTimeout),typeof AggregateError=="function")throw new AggregateError(s,a);{let l=new Error(a);throw l.errors=s,l}}this.setState({files:t}),o.forEach(a=>{this.emit("file-added",a)}),this.emit("files-added",o),o.length>5?this.log(`Added batch of ${o.length} files`):Object.values(o).forEach(a=>{this.log(`Added file: ${a.name} +var Hc=Object.create;var co=Object.defineProperty;var Jc=Object.getOwnPropertyDescriptor;var Zc=Object.getOwnPropertyNames;var Wc=Object.getPrototypeOf,Yc=Object.prototype.hasOwnProperty;var wa=r=>{throw TypeError(r)};var qc=(r,e,t)=>e in r?co(r,e,{enumerable:!0,configurable:!0,writable:!0,value:t}):r[e]=t;var Z=(r,e)=>()=>{try{return e||r((e={exports:{}}).exports,e),e.exports}catch(t){throw e=0,t}};var Kc=(r,e,t,i)=>{if(e&&typeof e=="object"||typeof e=="function")for(let s of Zc(e))!Yc.call(r,s)&&s!==t&&co(r,s,{get:()=>e[s],enumerable:!(i=Jc(e,s))||i.enumerable});return r};var G=(r,e,t)=>(t=r!=null?Hc(Wc(r)):{},Kc(e||!r||!r.__esModule?co(t,"default",{value:r,enumerable:!0}):t,r));var y=(r,e,t)=>qc(r,typeof e!="symbol"?e+"":e,t),ho=(r,e,t)=>e.has(r)||wa("Cannot "+t);var h=(r,e,t)=>(ho(r,e,"read from private field"),t?t.call(r):e.get(r)),U=(r,e,t)=>e.has(r)?wa("Cannot add the same private member more than once"):e instanceof WeakSet?e.add(r):e.set(r,t),R=(r,e,t,i)=>(ho(r,e,"write to private field"),i?i.call(r,t):e.set(r,t),t),E=(r,e,t)=>(ho(r,e,"access private method"),t);var Fr=(r,e,t,i)=>({set _(s){R(r,e,s,t)},get _(){return h(r,e,i)}});var ds=Z((Gb,dl)=>{"use strict";dl.exports=function(e){if(typeof e!="number"||Number.isNaN(e))throw new TypeError(`Expected a number, got ${typeof e}`);let t=e<0,i=Math.abs(e);if(t&&(i=-i),i===0)return"0 B";let s=["B","KB","MB","GB","TB","PB","EB","ZB","YB"],o=Math.min(Math.floor(Math.log(i)/Math.log(1024)),s.length-1),n=Number(i/1024**o),a=s[o];return`${n>=10||n%1===0?Math.round(n):n.toFixed(1)} ${a}`}});var fl=Z((Hb,hl)=>{"use strict";function cl(r,e){this.text=r=r||"",this.hasWild=~r.indexOf("*"),this.separator=e,this.parts=r.split(e)}cl.prototype.match=function(r){var e=!0,t=this.parts,i,s=t.length,o;if(typeof r=="string"||r instanceof String)if(!this.hasWild&&this.text!=r)e=!1;else{for(o=(r||"").split(this.separator),i=0;e&&i{var Eh=fl(),Sh=/[\/\+\.]/;ml.exports=function(r,e){function t(i){var s=Eh(i,r,Sh);return s&&s.length>=2}return e?t(e.split(";")[0]):t}});var ys=Z((sx,Ul)=>{function Rh(r){var e=typeof r;return r!=null&&(e=="object"||e=="function")}Ul.exports=Rh});var vl=Z((ox,Bl)=>{var Lh=typeof global=="object"&&global&&global.Object===Object&&global;Bl.exports=Lh});var jo=Z((nx,El)=>{var Nh=vl(),kh=typeof self=="object"&&self&&self.Object===Object&&self,_h=Nh||kh||Function("return this")();El.exports=_h});var wl=Z((ax,Sl)=>{var Ch=jo(),Ph=function(){return Ch.Date.now()};Sl.exports=Ph});var Rl=Z((lx,Dl)=>{var Th=/\s/;function Ih(r){for(var e=r.length;e--&&Th.test(r.charAt(e)););return e}Dl.exports=Ih});var Nl=Z((px,Ll)=>{var Vh=Rl(),Oh=/^\s+/;function Ah(r){return r&&r.slice(0,Vh(r)+1).replace(Oh,"")}Ll.exports=Ah});var Mo=Z((ux,kl)=>{var zh=jo(),jh=zh.Symbol;kl.exports=jh});var Tl=Z((dx,Pl)=>{var _l=Mo(),Cl=Object.prototype,Mh=Cl.hasOwnProperty,Gh=Cl.toString,Gr=_l?_l.toStringTag:void 0;function Hh(r){var e=Mh.call(r,Gr),t=r[Gr];try{r[Gr]=void 0;var i=!0}catch{}var s=Gh.call(r);return i&&(e?r[Gr]=t:delete r[Gr]),s}Pl.exports=Hh});var Vl=Z((cx,Il)=>{var Jh=Object.prototype,Zh=Jh.toString;function Wh(r){return Zh.call(r)}Il.exports=Wh});var jl=Z((hx,zl)=>{var Ol=Mo(),Yh=Tl(),qh=Vl(),Kh="[object Null]",$h="[object Undefined]",Al=Ol?Ol.toStringTag:void 0;function Xh(r){return r==null?r===void 0?$h:Kh:Al&&Al in Object(r)?Yh(r):qh(r)}zl.exports=Xh});var Gl=Z((fx,Ml)=>{function ef(r){return r!=null&&typeof r=="object"}Ml.exports=ef});var Jl=Z((mx,Hl)=>{var tf=jl(),rf=Gl(),sf="[object Symbol]";function of(r){return typeof r=="symbol"||rf(r)&&tf(r)==sf}Hl.exports=of});var ql=Z((yx,Yl)=>{var nf=Nl(),Zl=ys(),af=Jl(),Wl=NaN,lf=/^[-+]0x[0-9a-f]+$/i,pf=/^0b[01]+$/i,uf=/^0o[0-7]+$/i,df=parseInt;function cf(r){if(typeof r=="number")return r;if(af(r))return Wl;if(Zl(r)){var e=typeof r.valueOf=="function"?r.valueOf():r;r=Zl(e)?e+"":e}if(typeof r!="string")return r===0?r:+r;r=nf(r);var t=pf.test(r);return t||uf.test(r)?df(r.slice(2),t?2:8):lf.test(r)?Wl:+r}Yl.exports=cf});var gs=Z((gx,$l)=>{var hf=ys(),Go=wl(),Kl=ql(),ff="Expected a function",mf=Math.max,yf=Math.min;function gf(r,e,t){var i,s,o,n,a,p,u=0,d=!1,c=!1,f=!0;if(typeof r!="function")throw new TypeError(ff);e=Kl(e)||0,hf(t)&&(d=!!t.leading,c="maxWait"in t,o=c?mf(Kl(t.maxWait)||0,e):o,f="trailing"in t?!!t.trailing:f);function x(N){var I=i,P=s;return i=s=void 0,u=N,n=r.apply(P,I),n}function Q(N){return u=N,a=setTimeout(k,e),d?x(N):n}function D(N){var I=N-p,P=N-u,J=e-I;return c?yf(J,o-P):J}function L(N){var I=N-p,P=N-u;return p===void 0||I>=e||I<0||c&&P>=o}function k(){var N=Go();if(L(N))return B(N);a=setTimeout(k,D(N))}function B(N){return a=void 0,f&&i?x(N):(i=s=void 0,n)}function S(){a!==void 0&&clearTimeout(a),u=0,i=p=s=a=void 0}function _(){return a===void 0?n:B(Go())}function V(){var N=Go(),I=L(N);if(i=arguments,s=this,p=N,I){if(a===void 0)return Q(p);if(c)return clearTimeout(a),a=setTimeout(k,e),x(p)}return a===void 0&&(a=setTimeout(k,e)),n}return V.cancel=S,V.flush=_,V}$l.exports=gf});var Ho=Z((bx,Xl)=>{var bf=gs(),xf=ys(),Qf="Expected a function";function Ff(r,e,t){var i=!0,s=!0;if(typeof r!="function")throw new TypeError(Qf);return xf(t)&&(i="leading"in t?!!t.leading:i,s="trailing"in t?!!t.trailing:s),bf(r,e,{leading:i,maxWait:e,trailing:s})}Xl.exports=Ff});var tp=Z((xx,ep)=>{ep.exports=function(){var e={},t=e._fns={};e.emit=function(n,a,p,u,d,c,f){var x=i(n);x.length&&s(n,x,[a,p,u,d,c,f])},e.on=function(n,a){t[n]||(t[n]=[]),t[n].push(a)},e.once=function(n,a){function p(){a.apply(this,arguments),e.off(n,p)}this.on(n,p)},e.off=function(n,a){var p=[];if(n&&a){var u=this._fns[n],d=0,c=u?u.length:0;for(d;d{(function(){"use strict";var r={}.hasOwnProperty;function e(){for(var s="",o=0;o{"use strict";var zf=Object.prototype.hasOwnProperty,de="~";function Yr(){}Object.create&&(Yr.prototype=Object.create(null),new Yr().__proto__||(de=!1));function jf(r,e,t){this.fn=r,this.context=e,this.once=t||!1}function vp(r,e,t,i,s){if(typeof t!="function")throw new TypeError("The listener must be a function");var o=new jf(t,i||r,s),n=de?de+e:e;return r._events[n]?r._events[n].fn?r._events[n]=[r._events[n],o]:r._events[n].push(o):(r._events[n]=o,r._eventsCount++),r}function vs(r,e){--r._eventsCount===0?r._events=new Yr:delete r._events[e]}function ae(){this._events=new Yr,this._eventsCount=0}ae.prototype.eventNames=function(){var e=[],t,i;if(this._eventsCount===0)return e;for(i in t=this._events)zf.call(t,i)&&e.push(de?i.slice(1):i);return Object.getOwnPropertySymbols?e.concat(Object.getOwnPropertySymbols(t)):e};ae.prototype.listeners=function(e){var t=de?de+e:e,i=this._events[t];if(!i)return[];if(i.fn)return[i.fn];for(var s=0,o=i.length,n=new Array(o);s{"use strict";zd.exports=function(e,t){if(t=t.split(":")[0],e=+e,!e)return!1;switch(t){case"http":case"ws":return e!==80;case"https":case"wss":return e!==443;case"ftp":return e!==21;case"gopher":return e!==70;case"file":return!1}return e!==0}});var Hd=Z(na=>{"use strict";var V0=Object.prototype.hasOwnProperty,O0;function Md(r){try{return decodeURIComponent(r.replace(/\+/g," "))}catch{return null}}function Gd(r){try{return encodeURIComponent(r)}catch{return null}}function A0(r){for(var e=/([^=?#&]+)=?([^&]*)/g,t={},i;i=e.exec(r);){var s=Md(i[1]),o=Md(i[2]);s===null||o===null||s in t||(t[s]=o)}return t}function z0(r,e){e=e||"";var t=[],i,s;typeof e!="string"&&(e="?");for(s in r)if(V0.call(r,s)){if(i=r[s],!i&&(i===null||i===O0||isNaN(i))&&(i=""),s=Gd(s),i=Gd(i),s===null||i===null)continue;t.push(s+"="+i)}return t.length?e+t.join("&"):""}na.stringify=z0;na.parse=A0});var Xd=Z((j1,$d)=>{"use strict";var Zd=jd(),to=Hd(),j0=/^[\x00-\x20\u00a0\u1680\u2000-\u200a\u2028\u2029\u202f\u205f\u3000\ufeff]+/,Wd=/[\n\r\t]/g,M0=/^[A-Za-z][A-Za-z0-9+-.]*:\/\//,Yd=/:\d+$/,G0=/^([a-z][a-z0-9.+-]*:)?(\/\/)?([\\/]+)?([\S\s]*)/i,H0=/^[a-zA-Z]:/;function la(r){return(r||"").toString().replace(j0,"")}var aa=[["#","hash"],["?","query"],function(e,t){return Me(t.protocol)?e.replace(/\\/g,"/"):e},["/","pathname"],["@","auth",1],[NaN,"host",void 0,1,1],[/:(\d*)$/,"port",void 0,1],[NaN,"hostname",void 0,1,1]],Jd={hash:1,query:1};function qd(r){var e;typeof window<"u"?e=window:typeof global<"u"?e=global:typeof self<"u"?e=self:e={};var t=e.location||{};r=r||t;var i={},s=typeof r,o;if(r.protocol==="blob:")i=new Ge(unescape(r.pathname),{});else if(s==="string"){i=new Ge(r,{});for(o in Jd)delete i[o]}else if(s==="object"){for(o in r)o in Jd||(i[o]=r[o]);i.slashes===void 0&&(i.slashes=M0.test(r.href))}return i}function Me(r){return r==="file:"||r==="ftp:"||r==="http:"||r==="https:"||r==="ws:"||r==="wss:"}function Kd(r,e){r=la(r),r=r.replace(Wd,""),e=e||{};var t=G0.exec(r),i=t[1]?t[1].toLowerCase():"",s=!!t[2],o=!!t[3],n=0,a;return s?o?(a=t[2]+t[3]+t[4],n=t[2].length+t[3].length):(a=t[2]+t[4],n=t[2].length):o?(a=t[3]+t[4],n=t[3].length):a=t[4],i==="file:"?n>=2&&(a=a.slice(2)):Me(i)?a=t[4]:i?s&&(a=a.slice(2)):n>=2&&Me(e.protocol)&&(a=t[4]),{protocol:i,slashes:s||Me(i),slashesCount:n,rest:a}}function J0(r,e){if(r==="")return e;for(var t=(e||"/").split("/").slice(0,-1).concat(r.split("/")),i=t.length,s=t[i-1],o=!1,n=0;i--;)t[i]==="."?t.splice(i,1):t[i]===".."?(t.splice(i,1),n++):n&&(i===0&&(o=!0),t.splice(i,1),n--);return o&&t.unshift(""),(s==="."||s==="..")&&t.push(""),t.join("/")}function Ge(r,e,t){if(r=la(r),r=r.replace(Wd,""),!(this instanceof Ge))return new Ge(r,e,t);var i,s,o,n,a,p,u=aa.slice(),d=typeof e,c=this,f=0;for(d!=="object"&&d!=="string"&&(t=e,e=null),t&&typeof t!="function"&&(t=to.parse),e=qd(e),s=Kd(r||"",e),i=!s.protocol&&!s.slashes,c.slashes=s.slashes||i&&e.slashes,c.protocol=s.protocol||e.protocol||"",r=s.rest,(s.protocol==="file:"&&(s.slashesCount!==2||H0.test(r))||!s.slashes&&(s.protocol||s.slashesCount<2||!Me(c.protocol)))&&(u[3]=[/(.*)/,"pathname"]);f"error"in r&&!!r.error,th=r=>r.progress.uploadComplete;function go(r){return r.filter(e=>!eh(e)&&!th(e))}function bo(r){return r.filter(e=>!e.progress?.uploadStarted||!e.isRestored)}function vr(r){return typeof r!="object"||r===null||!("nodeType"in r)?!1:r.nodeType===Node.ELEMENT_NODE}function rh(r){if(typeof r=="string"){let e=document.querySelectorAll(r);return e.length===0?null:Array.from(e)}return typeof r=="object"&&vr(r)?[r]:null}var qi=rh;function ih(r,e=document){return typeof r=="string"?e.querySelector(r):vr(r)?r:null}var xo=ih;function $e(r){let e=r.lastIndexOf(".");return e===-1||e===r.length-1?{name:r,extension:void 0}:{name:r.slice(0,e),extension:r.slice(e+1)}}var Qo={__proto__:null,md:"text/markdown",markdown:"text/markdown",mp4:"video/mp4",mp3:"audio/mp3",svg:"image/svg+xml",jpg:"image/jpeg",png:"image/png",webp:"image/webp",gif:"image/gif",heic:"image/heic",heif:"image/heif",yaml:"text/yaml",yml:"text/yaml",csv:"text/csv",tsv:"text/tab-separated-values",tab:"text/tab-separated-values",avi:"video/x-msvideo",mks:"video/x-matroska",mkv:"video/x-matroska",mov:"video/quicktime",dicom:"application/dicom",doc:"application/msword",msg:"application/vnd.ms-outlook",docm:"application/vnd.ms-word.document.macroenabled.12",docx:"application/vnd.openxmlformats-officedocument.wordprocessingml.document",dot:"application/msword",dotm:"application/vnd.ms-word.template.macroenabled.12",dotx:"application/vnd.openxmlformats-officedocument.wordprocessingml.template",xla:"application/vnd.ms-excel",xlam:"application/vnd.ms-excel.addin.macroenabled.12",xlc:"application/vnd.ms-excel",xlf:"application/x-xliff+xml",xlm:"application/vnd.ms-excel",xls:"application/vnd.ms-excel",xlsb:"application/vnd.ms-excel.sheet.binary.macroenabled.12",xlsm:"application/vnd.ms-excel.sheet.macroenabled.12",xlsx:"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",xlt:"application/vnd.ms-excel",xltm:"application/vnd.ms-excel.template.macroenabled.12",xltx:"application/vnd.openxmlformats-officedocument.spreadsheetml.template",xlw:"application/vnd.ms-excel",txt:"text/plain",text:"text/plain",conf:"text/plain",log:"text/plain",pdf:"application/pdf",zip:"application/zip","7z":"application/x-7z-compressed",rar:"application/x-rar-compressed",tar:"application/x-tar",gz:"application/gzip",dmg:"application/x-apple-diskimage"};function Vt(r){if(r.type)return r.type;let e=r.name?$e(r.name).extension?.toLowerCase():null;return e&&e in Qo?Qo[e]:"application/octet-stream"}function sh(r){return r.charCodeAt(0).toString(32)}function Da(r){let e="";return r.replace(/[^A-Z0-9]/gi,t=>(e+=`-${sh(t)}`,"/"))+e}function Fo(r,e){let t=e||"uppy";return typeof r.name=="string"&&(t+=`-${Da(r.name.toLowerCase())}`),r.type!==void 0&&(t+=`-${r.type}`),r.meta&&typeof r.meta.relativePath=="string"&&(t+=`-${Da(r.meta.relativePath.toLowerCase())}`),r.data?.size!==void 0&&(t+=`-${r.data.size}`),r.data.lastModified!==void 0&&(t+=`-${r.data.lastModified}`),t}function oh(r){return!r.isRemote||!r.remote?!1:new Set(["box","dropbox","drive","facebook","unsplash"]).has(r.remote.provider)}function Er(r,e){if(oh(r))return r.id;let t=Vt(r);return Fo({...r,type:t},e)}function Ki(r,e){return r===!0?Object.keys(e):Array.isArray(r)?r:[]}var Je=Array.from;function Uo(r){let e=Je(r.files);return Promise.resolve(e)}function $i(r,e,t,{onSuccess:i}){r.readEntries(s=>{let o=[...e,...s];s.length?queueMicrotask(()=>{$i(r,o,t,{onSuccess:i})}):i(o)},s=>{t(s),i(e)})}function Ra(r,e){return r==null?r:{kind:r.isFile?"file":r.isDirectory?"directory":void 0,name:r.name,getFile(){return new Promise((t,i)=>r.file(t,i))},async*values(){let t=r.createReader();yield*await new Promise(s=>{$i(t,[],e,{onSuccess:o=>s(o.map(n=>Ra(n,e)))})})},isSameEntry:void 0}}async function*La(r,e,t=void 0){let i=()=>`${e}/${r.name}`;if(r.kind==="file"){let s=await r.getFile();s!=null?(s.relativePath=e?i():null,yield s):t!=null&&(yield t)}else if(r.kind==="directory")for await(let s of r.values())yield*La(s,e?i():r.name);else t!=null&&(yield t)}async function*Bo(r,e){let t=await Promise.all(Array.from(r.items,async i=>{let s;return s??(s=Ra(typeof i.getAsEntry=="function"?i.getAsEntry():i.webkitGetAsEntry(),e)),{fileSystemHandle:s,lastResortFile:i.getAsFile()}}));for(let{lastResortFile:i,fileSystemHandle:s}of t)if(s!=null)try{yield*La(s,"",i)}catch(o){i!=null?yield i:e(o)}else i!=null&&(yield i)}async function Xi(r,e){let t=e?.logDropError??Function.prototype;try{let i=[];for await(let s of Bo(r,t))i.push(s);return i}catch{return Uo(r)}}function nh(r){for(;r&&!r.dir;)r=r.parentNode;return r?.dir}var vo=nh;function Eo(r){return r<10?`0${r}`:r.toString()}function ht(){let r=new Date,e=Eo(r.getHours()),t=Eo(r.getMinutes()),i=Eo(r.getSeconds());return`${e}:${t}:${i}`}function es(){if(typeof window>"u")return!1;let r=document.body;return!(r==null||window==null||!("draggable"in r)||!("ondragstart"in r)||!("ondrop"in r)||!("FormData"in window)||!("FileReader"in window))}function ah(r){return r?r.readyState===4&&r.status===0:!1}var So=ah;function Sr(r){return r.startsWith("blob:")}function wr(r){return r?/^[^/]+\/(jpe?g|gif|png|svg|svg\+xml|bmp|webp|avif)$/.test(r):!1}function wo(r){let e=Math.floor(r/3600)%24,t=Math.floor(r/60)%60,i=Math.floor(r%60);return{hours:e,minutes:t,seconds:i}}function ts(r){let e=wo(r),t=e.hours===0?"":`${e.hours}h`,i=e.minutes===0?"":`${e.hours===0?e.minutes:` ${e.minutes.toString(10).padStart(2,"0")}`}m`,s=e.hours!==0?"":`${e.minutes===0?e.seconds:` ${e.seconds.toString(10).padStart(2,"0")}`}s`;return`${t}${i}${s}`}function lh(r){return new Error("Cancelled",{cause:r})}function Na(r){if(r!=null){let e=()=>this.abort(r.reason);r.addEventListener("abort",e,{once:!0});let t=()=>{r.removeEventListener("abort",e)};this.then?.(t,t)}return this}var Ze,_e,We,Ot,Ce,At,ft,oe,Do,Dr,ka,_a,Ca,rs,zt,Rr=class{constructor(e){U(this,oe);U(this,Ze,0);U(this,_e,[]);U(this,We,!1);U(this,Ot);U(this,Ce,1);U(this,At);U(this,ft);y(this,"limit");U(this,rs,()=>this.resume());U(this,zt,()=>{if(h(this,We)){R(this,ft,setTimeout(h(this,zt),0));return}R(this,Ce,this.limit),this.limit=Math.ceil((h(this,At)+h(this,Ce))/2);for(let e=h(this,Ce);e<=this.limit;e++)E(this,oe,Dr).call(this);h(this,At)-h(this,Ce)>3?R(this,ft,setTimeout(h(this,zt),2e3)):R(this,Ce,Math.floor(h(this,Ce)/2))});typeof e!="number"||e===0?this.limit=1/0:this.limit=e}run(e,t){return!h(this,We)&&h(this,Ze){let s=this.run(()=>(e(...i),queueMicrotask(()=>s.done()),()=>{}),t);return{abortOn:Na,abort(){s.abort()}}}}wrapPromiseFunction(e,t){return(...i)=>{let s,o=new Promise((n,a)=>{s=this.run(()=>{let p,u;try{u=Promise.resolve(e(...i))}catch(d){u=Promise.reject(d)}return u.then(d=>{p?a(p):(s.done(),n(d))},d=>{p?a(p):(s.done(),a(d))}),d=>{p=lh(d)}},t)});return o.abort=n=>{s.abort(n)},o.abortOn=Na,o}}resume(){R(this,We,!1),clearTimeout(h(this,Ot));for(let e=0;e1&&Number.isFinite(this.limit)&&(R(this,At,this.limit-1),this.limit=h(this,Ce),R(this,ft,setTimeout(h(this,zt),e)))}get isPaused(){return h(this,We)}};Ze=new WeakMap,_e=new WeakMap,We=new WeakMap,Ot=new WeakMap,Ce=new WeakMap,At=new WeakMap,ft=new WeakMap,oe=new WeakSet,Do=function(e){R(this,Ze,h(this,Ze)+1);let t=!1,i;try{i=e()}catch(s){throw R(this,Ze,h(this,Ze)-1),s}return{abort:s=>{t||(t=!0,R(this,Ze,h(this,Ze)-1),i?.(s),E(this,oe,Dr).call(this))},done:()=>{t||(t=!0,R(this,Ze,h(this,Ze)-1),E(this,oe,Dr).call(this))}}},Dr=function(){queueMicrotask(()=>E(this,oe,ka).call(this))},ka=function(){if(h(this,We)||h(this,Ze)>=this.limit||h(this,_e).length===0)return;let e=h(this,_e).shift();if(e==null)throw new Error("Invariant violation: next is null");let t=E(this,oe,Do).call(this,e.fn);e.abort=t.abort,e.done=t.done},_a=function(e,t){let i={fn:e,priority:t?.priority||0,abort:()=>{E(this,oe,Ca).call(this,i)},done:()=>{throw new Error("Cannot mark a queued request as done: this indicates a bug")}},s=h(this,_e).findIndex(o=>i.priority>o.priority);return s===-1?h(this,_e).push(i):h(this,_e).splice(s,0,i),i},Ca=function(e){let t=h(this,_e).indexOf(e);t!==-1&&h(this,_e).splice(t,1)},rs=new WeakMap,zt=new WeakMap;function is(r){return{...r,type:r.mimeType,extension:r.name?$e(r.name).extension:null}}function ph(r,e,t){let i=[];return r.forEach(s=>typeof s!="string"?i.push(s):e[Symbol.split](s).forEach((o,n,a)=>{o!==""&&i.push(o),n{throw new Error(`missing string: ${r}`)},Lr,Nr,Ro,Xe=class{constructor(e,{onMissingKey:t=uh}={}){U(this,Nr);y(this,"locale");U(this,Lr);this.locale={strings:{},pluralize(i){return i===1?0:1}},Array.isArray(e)?e.forEach(E(this,Nr,Ro),this):E(this,Nr,Ro).call(this,e),R(this,Lr,t)}translate(e,t){return this.translateArray(e,t).join("")}translateArray(e,t){let i=this.locale.strings[e];if(i==null&&(h(this,Lr).call(this,e),i=e),typeof i=="object"){if(t&&typeof t.smart_count<"u"){let o=this.locale.pluralize(t.smart_count);return Pa(i[o],t)}throw new Error("Attempted to use a string with plural forms, but no value was given for %{smart_count}")}if(typeof i!="string")throw new Error("string was not a string");return Pa(i,t)}};Lr=new WeakMap,Nr=new WeakSet,Ro=function(e){if(!e?.strings)return;let t=this.locale;Object.assign(this.locale,{strings:{...t.strings,...e.strings},pluralize:e.pluralize||t.pluralize})};function kr(r,e){if(e===0)return"";if(r.length<=e)return r;if(e<=4)return`${r.slice(0,e-1)}…`;let t=e-3,i=Math.ceil(t/2),s=Math.floor(t/2);return r.slice(0,i)+"..."+r.slice(-s)}var Pr,M,Aa,dh,et,Ta,za,ja,Lo,os,_r,Ma,_o,No,ko,ch,ns={},as=[],hh=/acit|ex(?:s|g|n|p|$)|rph|grid|ows|mnc|ntw|ine[ch]|zoo|^ord|itera/i,Tr=Array.isArray;function Pe(r,e){for(var t in e)r[t]=e[t];return r}function Co(r){r&&r.parentNode&&r.parentNode.removeChild(r)}function Ir(r,e,t){var i,s,o,n={};for(o in e)o=="key"?i=e[o]:o=="ref"?s=e[o]:n[o]=e[o];if(arguments.length>2&&(n.children=arguments.length>3?Pr.call(arguments,2):t),typeof r=="function"&&r.defaultProps!=null)for(o in r.defaultProps)n[o]===void 0&&(n[o]=r.defaultProps[o]);return Cr(r,n,i,s,null)}function Cr(r,e,t,i,s){var o={type:r,props:e,key:t,ref:i,__k:null,__:null,__b:0,__e:null,__c:null,constructor:void 0,__v:s??++Aa,__i:-1,__u:0};return s==null&&M.vnode!=null&&M.vnode(o),o}function Ga(){return{current:null}}function ee(r){return r.children}function ne(r,e){this.props=r,this.context=e}function jt(r,e){if(e==null)return r.__?jt(r.__,r.__i+1):null;for(var t;ee&&et.sort(ja),r=et.shift(),e=et.length,fh(r)}finally{et.length=ls.__r=0}}function Ja(r,e,t,i,s,o,n,a,p,u,d){var c,f,x,Q,D,L,k,B=i&&i.__k||as,S=e.length;for(p=mh(t,e,B,p,S),c=0;c0?n=r.__k[o]=Cr(n.type,n.props,n.key,n.ref?n.ref:null,n.__v):r.__k[o]=n,p=o+f,n.__=r,n.__b=r.__b+1,a=null,(u=n.__i=yh(n,t,p,c))!=-1&&(c--,(a=t[u])&&(a.__u|=2)),a==null||a.__v==null?(u==-1&&(s>d?f--:sp?f--:f++,n.__u|=4))):r.__k[o]=null;if(c)for(o=0;o(d?1:0)){for(s=t-1,o=t+1;s>=0||o=0?s--:o++])!=null&&(2&u.__u)==0&&a==u.key&&p==u.type)return n}return-1}function Va(r,e,t){e[0]=="-"?r.setProperty(e,t??""):r[e]=t==null?"":typeof t!="number"||hh.test(e)?t:t+"px"}function ss(r,e,t,i,s){var o,n;e:if(e=="style")if(typeof t=="string")r.style.cssText=t;else{if(typeof i=="string"&&(r.style.cssText=i=""),i)for(e in i)t&&e in t||Va(r.style,e,"");if(t)for(e in t)i&&t[e]==i[e]||Va(r.style,e,t[e])}else if(e[0]=="o"&&e[1]=="n")o=e!=(e=e.replace(Ma,"$1")),n=e.toLowerCase(),e=n in r||e=="onFocusOut"||e=="onFocusIn"?n.slice(2):e.slice(2),r.l||(r.l={}),r.l[e+o]=t,t?i?t[_r]=i[_r]:(t[_r]=_o,r.addEventListener(e,o?ko:No,o)):r.removeEventListener(e,o?ko:No,o);else{if(s=="http://www.w3.org/2000/svg")e=e.replace(/xlink(H|:h)/,"h").replace(/sName$/,"s");else if(e!="width"&&e!="height"&&e!="href"&&e!="list"&&e!="form"&&e!="tabIndex"&&e!="download"&&e!="rowSpan"&&e!="colSpan"&&e!="role"&&e!="popover"&&e in r)try{r[e]=t??"";break e}catch{}typeof t=="function"||(t==null||t===!1&&e[4]!="-"?r.removeAttribute(e):r.setAttribute(e,e=="popover"&&t==1?"":t))}}function Oa(r){return function(e){if(this.l){var t=this.l[e.type+r];if(e[os]==null)e[os]=_o++;else if(e[os]0?r:Tr(r)?r.map(qa):r.constructor!==void 0?null:Pe({},r)}function gh(r,e,t,i,s,o,n,a,p){var u,d,c,f,x,Q,D,L=t.props||ns,k=e.props,B=e.type;if(B=="svg"?s="http://www.w3.org/2000/svg":B=="math"?s="http://www.w3.org/1998/Math/MathML":s||(s="http://www.w3.org/1999/xhtml"),o!=null){for(u=0;u2&&(a.children=arguments.length>3?Pr.call(arguments,2):t),Cr(r.type,a,i||r.key,s||r.ref,null)}Pr=as.slice,M={__e:function(r,e,t,i){for(var s,o,n;e=e.__;)if((s=e.__c)&&!s.__)try{if((o=s.constructor)&&o.getDerivedStateFromError!=null&&(s.setState(o.getDerivedStateFromError(r)),n=s.__d),s.componentDidCatch!=null&&(s.componentDidCatch(r,i||{}),n=s.__d),n)return s.__E=s}catch(a){r=a}throw r}},Aa=0,dh=function(r){return r!=null&&r.constructor===void 0},ne.prototype.setState=function(r,e){var t;t=this.__s!=null&&this.__s!=this.state?this.__s:this.__s=Pe({},this.state),typeof r=="function"&&(r=r(Pe({},t),this.props)),r&&Pe(t,r),r!=null&&this.__v&&(e&&this._sb.push(e),Ia(this))},ne.prototype.forceUpdate=function(r){this.__v&&(this.__e=!0,r&&this.__h.push(r),Ia(this))},ne.prototype.render=ee,et=[],za=typeof Promise=="function"?Promise.prototype.then.bind(Promise.resolve()):setTimeout,ja=function(r,e){return r.__v.__b-e.__v.__b},ls.__r=0,Lo=Math.random().toString(8),os="__d"+Lo,_r="__a"+Lo,Ma=/(PointerCapture)$|Capture$/i,_o=0,No=Oa(!1),ko=Oa(!0),ch=0;var xh=0;function l(r,e,t,i,s,o){e||(e={});var n,a,p=e;if("ref"in p)for(a in p={},e)a=="ref"?n=e[a]:p[a]=e[a];var u={type:r,props:p,key:t,ref:n,__k:null,__:null,__b:0,__e:null,__c:null,constructor:void 0,__v:--xh,__i:-1,__u:0,__source:s,__self:o};if(typeof r=="function"&&(n=r.defaultProps))for(a in n)p[a]===void 0&&(p[a]=n[a]);return M.vnode&&M.vnode(u),u}var Vr,Y,Vo,$a,Or=0,nl=[],K=M,Xa=K.__b,el=K.__r,tl=K.diffed,rl=K.__c,il=K.unmount,sl=K.__;function Ao(r,e){K.__h&&K.__h(Y,r,Or||e),Or=0;var t=Y.__H||(Y.__H={__:[],__h:[]});return r>=t.__.length&&t.__.push({}),t.__[r]}function ge(r){return Or=1,Qh(ll,r)}function Qh(r,e,t){var i=Ao(Vr++,2);if(i.t=r,!i.__c&&(i.__=[t?t(e):ll(void 0,e),function(a){var p=i.__N?i.__N[0]:i.__[0],u=i.t(p,a);p!==u&&(i.__N=[u,i.__[1]],i.__c.setState({}))}],i.__c=Y,!Y.__f)){var s=function(a,p,u){if(!i.__c.__H)return!0;var d=!1,c=i.__c.props!==a;if(i.__c.__H.__.some(function(x){if(x.__N){d=!0;var Q=x.__[0];x.__=x.__N,x.__N=void 0,Q!==x.__[0]&&(c=!0)}}),o){var f=o.call(this,a,p,u);return d?f||c:f}return!d||c};Y.__f=!0;var o=Y.shouldComponentUpdate,n=Y.componentWillUpdate;Y.componentWillUpdate=function(a,p,u){if(this.__e){var d=o;o=void 0,s(a,p,u),o=d}n&&n.call(this,a,p,u)},Y.shouldComponentUpdate=s}return i.__N||i.__}function we(r,e){var t=Ao(Vr++,3);!K.__s&&al(t.__H,e)&&(t.__=r,t.u=e,Y.__H.__h.push(t))}function Ie(r){return Or=5,mt(function(){return{current:r}},[])}function mt(r,e){var t=Ao(Vr++,7);return al(t.__H,e)&&(t.__=r(),t.__H=e,t.__h=r),t.__}function De(r,e){return Or=8,mt(function(){return r},e)}function Fh(){for(var r;r=nl.shift();){var e=r.__H;if(r.__P&&e)try{e.__h.some(us),e.__h.some(Oo),e.__h=[]}catch(t){e.__h=[],K.__e(t,r.__v)}}}K.__b=function(r){Y=null,Xa&&Xa(r)},K.__=function(r,e){r&&e.__k&&e.__k.__m&&(r.__m=e.__k.__m),sl&&sl(r,e)},K.__r=function(r){el&&el(r),Vr=0;var e=(Y=r.__c).__H;e&&(Vo===Y?(e.__h=[],Y.__h=[],e.__.some(function(t){t.__N&&(t.__=t.__N),t.u=t.__N=void 0})):(e.__h.some(us),e.__h.some(Oo),e.__h=[],Vr=0)),Vo=Y},K.diffed=function(r){tl&&tl(r);var e=r.__c;e&&e.__H&&(e.__H.__h.length&&(nl.push(e)!==1&&$a===K.requestAnimationFrame||(($a=K.requestAnimationFrame)||Uh)(Fh)),e.__H.__.some(function(t){t.u&&(t.__H=t.u,t.u=void 0)})),Vo=Y=null},K.__c=function(r,e){e.some(function(t){try{t.__h.some(us),t.__h=t.__h.filter(function(i){return!i.__||Oo(i)})}catch(i){e.some(function(s){s.__h&&(s.__h=[])}),e=[],K.__e(i,t.__v)}}),rl&&rl(r,e)},K.unmount=function(r){il&&il(r);var e,t=r.__c;t&&t.__H&&(t.__H.__.some(function(i){try{us(i)}catch(s){e=s}}),t.__H=void 0,e&&K.__e(e,t.__v))};var ol=typeof requestAnimationFrame=="function";function Uh(r){var e,t=function(){clearTimeout(i),ol&&cancelAnimationFrame(e),setTimeout(r)},i=setTimeout(t,35);ol&&(e=requestAnimationFrame(t))}function us(r){var e=Y,t=r.__c;typeof t=="function"&&(r.__c=void 0,t()),Y=e}function Oo(r){var e=Y;r.__c=r.__(),Y=e}function al(r,e){return!r||r.length!==e.length||e.some(function(t,i){return t!==r[i]})}function ll(r,e){return typeof e=="function"?e(r):e}var Bh={position:"relative",width:"100%",minHeight:"100%"},vh={position:"absolute",top:0,left:0,width:"100%",overflow:"visible"};function Mt({data:r,rowHeight:e,renderRow:t,overscanCount:i=10,padding:s=4,...o}){let n=Ie(null),[a,p]=ge(0),[u,d]=ge(0);we(()=>{function B(){n.current!=null&&u!==n.current.offsetHeight&&d(n.current.offsetHeight)}return B(),window.addEventListener("resize",B),()=>{window.removeEventListener("resize",B)}},[u]);let c=De(()=>{n.current&&p(n.current.scrollTop)},[]),f=Math.floor(a/e),x=Math.floor(u/e);i&&(f=Math.max(0,f-f%i),x+=i);let Q=f+x+s,D=r.slice(f,Q),L={...Bh,height:r.length*e},k={...vh,top:f*e};return l("div",{onScroll:c,ref:n,...o,children:l("div",{role:"presentation",style:L,children:l("div",{role:"presentation",style:k,children:D.map(t)})})})}var Ve=class{constructor(e,t){y(this,"uppy");y(this,"opts");y(this,"id");y(this,"defaultLocale");y(this,"i18n");y(this,"i18nArray");y(this,"type");y(this,"VERSION");this.uppy=e,this.opts=t??{}}getPluginState(){let{plugins:e}=this.uppy.getState();return e?.[this.id]||{}}setPluginState(e){let{plugins:t}=this.uppy.getState();this.uppy.setState({plugins:{...t,[this.id]:{...t[this.id],...e}}})}setOptions(e){this.opts={...this.opts,...e},this.setPluginState(void 0),this.i18nInit()}i18nInit(){let e=new Xe([this.defaultLocale,this.uppy.locale,this.opts.locale]);this.i18n=e.translate.bind(e),this.i18nArray=e.translateArray.bind(e),this.setPluginState(void 0)}addTarget(e){throw new Error("Extend the addTarget method to add your plugin to another plugin's target")}install(){}uninstall(){}update(e){}afterUpdate(){}};var Oe,Ar,Gt=class{constructor(e){U(this,Oe);U(this,Ar,[]);R(this,Oe,e)}on(e,t){return h(this,Ar).push([e,t]),h(this,Oe).on(e,t)}remove(){for(let[e,t]of h(this,Ar).splice(0))h(this,Oe).off(e,t)}onFilePause(e,t){this.on("upload-pause",(i,s)=>{e===i?.id&&t(s)})}onFileRemove(e,t){this.on("file-removed",i=>{e===i.id&&t(i.id)})}onPause(e,t){this.on("upload-pause",(i,s)=>{e===i?.id&&t(s)})}onRetry(e,t){this.on("upload-retry",i=>{e===i?.id&&t()})}onRetryAll(e,t){this.on("retry-all",()=>{h(this,Oe).getFile(e)&&t()})}onPauseAll(e,t){this.on("pause-all",()=>{h(this,Oe).getFile(e)&&t()})}onCancelAll(e,t){this.on("cancel-all",(...i)=>{h(this,Oe).getFile(e)&&t(...i)})}onResumeAll(e,t){this.on("resume-all",()=>{h(this,Oe).getFile(e)&&t()})}};Oe=new WeakMap,Ar=new WeakMap;var pl={debug:()=>{},warn:()=>{},error:(...r)=>console.error(`[Uppy] [${ht()}]`,...r)},ul={debug:(...r)=>console.debug(`[Uppy] [${ht()}]`,...r),warn:(...r)=>console.warn(`[Uppy] [${ht()}]`,...r),error:(...r)=>console.error(`[Uppy] [${ht()}]`,...r)};var zr=G(ds(),1),gl=G(yl(),1);var bl={maxFileSize:null,minFileSize:null,maxTotalFileSize:null,maxNumberOfFiles:null,minNumberOfFiles:null,allowedFileTypes:null,requiredMetaFields:[]},fe=class extends Error{constructor(t,i){super(t);y(this,"isUserFacing");y(this,"file");y(this,"isRestriction",!0);this.isUserFacing=i?.isUserFacing??!0,i?.file&&(this.file=i.file)}},cs=class{constructor(e,t){y(this,"getI18n");y(this,"getOpts");this.getI18n=t,this.getOpts=()=>{let i=e();if(i.restrictions?.allowedFileTypes!=null&&!Array.isArray(i.restrictions.allowedFileTypes))throw new TypeError("`restrictions.allowedFileTypes` must be an array");return i}}validateAggregateRestrictions(e,t){let{maxTotalFileSize:i,maxNumberOfFiles:s}=this.getOpts().restrictions;if(s&&e.filter(n=>!n.isGhost).length+t.length>s)throw new fe(`${this.getI18n()("youCanOnlyUploadX",{smart_count:s})}`);if(i){let o=[...e,...t].reduce((n,a)=>n+(a.size??0),0);if(o>i)throw new fe(this.getI18n()("aggregateExceedsSize",{sizeAllowed:(0,zr.default)(i),size:(0,zr.default)(o)}))}}validateSingleFile(e){let{maxFileSize:t,minFileSize:i,allowedFileTypes:s}=this.getOpts().restrictions;if(s&&!s.some(n=>n.includes("/")?e.type?(0,gl.default)(e.type.replace(/;.*?$/,""),n):!1:n[0]==="."&&e.extension?e.extension.toLowerCase()===n.slice(1).toLowerCase():!1)){let n=s.join(", ");throw new fe(this.getI18n()("youCanOnlyUploadFileTypes",{types:n}),{file:e})}if(t&&e.size!=null&&e.size>t)throw new fe(this.getI18n()("exceedsSize",{size:(0,zr.default)(t),file:e.name??this.getI18n()("unnamed")}),{file:e});if(i&&e.size!=null&&e.size{this.validateSingleFile(i)}),this.validateAggregateRestrictions(e,t)}validateMinNumberOfFiles(e){let{minNumberOfFiles:t}=this.getOpts().restrictions;if(t&&Object.keys(e).length(t=i,e||(e=Promise.resolve().then(()=>(e=null,r(...t)))),e)}var jr,hs=class hs extends Ve{constructor(){super(...arguments);U(this,jr);y(this,"isTargetDOMEl");y(this,"el");y(this,"parent");y(this,"title")}getTargetPlugin(t){let i;if(typeof t?.addTarget=="function")i=t,i instanceof hs||console.warn(new Error("The provided plugin is not an instance of UIPlugin. This is an indication of a bug with the way Uppy is bundled.",{cause:{targetPlugin:i,UIPlugin:hs}}));else if(typeof t=="function"){let s=t;this.uppy.iteratePlugins(o=>{o instanceof s&&(i=o)})}return i}mount(t,i){let s=i.id,o=xo(t);if(o){this.isTargetDOMEl=!0;let p=document.createElement("div");return p.classList.add("uppy-Root"),R(this,jr,wh(u=>{this.uppy.getPlugin(this.id)&&(Io(this.render(u,p),p),this.afterUpdate())})),this.uppy.log(`Installing ${s} to a DOM element '${t}'`),this.opts.replaceTargetContent&&(o.innerHTML=""),Io(this.render(this.uppy.getState(),p),p),this.el=p,o.appendChild(p),p.dir=this.opts.direction||vo(p)||"ltr",this.onMount(),this.el}let n=this.getTargetPlugin(t);if(n)return this.uppy.log(`Installing ${s} to ${n.id}`),this.parent=n,this.el=n.addTarget(i),this.onMount(),this.el;this.uppy.log(`Not installing ${s}`);let a=`Invalid target option given to ${s}.`;throw typeof t=="function"?a+=" The given target is not a Plugin class. Please check that you're not specifying a React Component instead of a plugin. If you are using @uppy/* packages directly, make sure you have only 1 version of @uppy/core installed: run `npm ls @uppy/core` on the command line and verify that all the versions match and are deduped correctly.":a+="If you meant to target an HTML element, please make sure that the element exists. Check that the @@ -12,69 +12,45 @@

VidXP media intake

-

Upload your video

-

Opening the secure upload handoff…

+

Upload your videos

+

Opening the secure upload session…

- - -
-

Expected file

+
+
+

Upload session

+ +
-
Filename
-
Size
-
Maximum
+
State
+
Files
+
Per-file maximum
+
Session maximum
+
Reserved
+
Uploaded
Expires

- VidXP checks the selected name and size before transfer. After upload, - the server probes and validates the media before it becomes available. + The complete link authorizes this session. VidXP binds each selected + file and reserves quota before its bytes travel directly to tusd.

-

Choose and upload

+

Choose and upload videos

- Waiting for a valid handoff. + Waiting for a valid upload session.

-

VidXP status

-

State

+

Per-file results

The page will refresh the server-owned status automatically.

-
-
Upload intent
-
Import job
-
Media ID
-
+
    - +
    diff --git a/src/vidxp/assets/upload_page/upload-page.css b/src/vidxp/assets/upload_page/upload-page.css index ac4ff62..59e0830 100644 --- a/src/vidxp/assets/upload_page/upload-page.css +++ b/src/vidxp/assets/upload_page/upload-page.css @@ -1 +1 @@ -@charset "UTF-8";.uppy-Root{box-sizing:border-box;color:#333;font-family:-apple-system,system-ui,BlinkMacSystemFont,Segoe UI,Segoe UI Symbol,Segoe UI Emoji,Apple Color Emoji,Roboto,Helvetica,Arial,sans-serif;line-height:1;position:relative;text-align:left;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.uppy-Root[dir=rtl],[dir=rtl] .uppy-Root{text-align:right}.uppy-Root *,.uppy-Root :after,.uppy-Root :before{box-sizing:inherit}.uppy-Root [hidden]{display:none}.uppy-u-reset{all:initial;appearance:none;box-sizing:border-box;font-family:-apple-system,system-ui,BlinkMacSystemFont,Segoe UI,Segoe UI Symbol,Segoe UI Emoji,Apple Color Emoji,Roboto,Helvetica,Arial,sans-serif;line-height:1}[dir=rtl] .uppy-u-reset{text-align:right}.uppy-truncate-text{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.uppy-c-textInput{background-color:#fff;border:1px solid #ddd;border-radius:4px;font-family:inherit;font-size:14px;line-height:1.5;padding:6px 8px}.uppy-size--md .uppy-c-textInput{padding:8px 10px}.uppy-c-textInput:focus{border-color:#1269cf99;box-shadow:0 0 0 3px #1269cf26;outline:none}[data-uppy-theme=dark] .uppy-c-textInput{background-color:#333;border-color:#333;color:#eaeaea}[data-uppy-theme=dark] .uppy-c-textInput:focus{border-color:#525252;box-shadow:none}.uppy-c-icon{display:inline-block;max-height:100%;max-width:100%;overflow:hidden;fill:currentColor}.uppy-c-btn{align-items:center;color:inherit;display:inline-flex;font-family:inherit;font-size:inherit;font-weight:500;justify-content:center;line-height:1;transition-duration:.3s;transition-property:background-color,color;user-select:none;white-space:nowrap}.uppy-c-btn,[dir=rtl] .uppy-c-btn{text-align:center}.uppy-c-btn:not(:disabled):not(.disabled){cursor:pointer}.uppy-c-btn::-moz-focus-inner{border:0}.uppy-c-btn-primary{background-color:#1269cf;border-radius:4px;color:#fff;font-size:14px;padding:10px 18px}.uppy-c-btn-primary:not(:disabled):hover{background-color:#0e51a0}.uppy-c-btn-primary:focus{box-shadow:0 0 0 3px #1269cf66;outline:none}.uppy-size--md .uppy-c-btn-primary{padding:13px 22px}[data-uppy-theme=dark] .uppy-c-btn-primary{color:#eaeaea}[data-uppy-theme=dark] .uppy-c-btn-primary:focus{outline:none}[data-uppy-theme=dark] .uppy-c-btn-primary::-moz-focus-inner{border:0}[data-uppy-theme=dark] .uppy-c-btn-primary:focus{box-shadow:0 0 0 2px #aae1ffd9}.uppy-c-btn-primary.uppy-c-btn--disabled{background-color:#8eb2db}.uppy-c-btn-link{background-color:initial;border-radius:4px;color:#525252;font-size:14px;line-height:1;padding:10px 15px}.uppy-c-btn-link:hover{color:#333}.uppy-c-btn-link:focus{box-shadow:0 0 0 3px #1269cf40;outline:none}.uppy-size--md .uppy-c-btn-link{padding:13px 18px}[data-uppy-theme=dark] .uppy-c-btn-link{color:#eaeaea}[data-uppy-theme=dark] .uppy-c-btn-link:focus{outline:none}[data-uppy-theme=dark] .uppy-c-btn-link::-moz-focus-inner{border:0}[data-uppy-theme=dark] .uppy-c-btn-link:focus{box-shadow:0 0 0 2px #aae1ffd9}[data-uppy-theme=dark] .uppy-c-btn-link:hover{color:#939393}.uppy-ProviderBrowser-viewType--grid ul.uppy-ProviderBrowser-list,.uppy-ProviderBrowser-viewType--unsplash ul.uppy-ProviderBrowser-list{align-items:flex-start;display:flex;flex-direction:row;flex-wrap:wrap;justify-content:space-between;padding:6px}.uppy-ProviderBrowser-viewType--grid ul.uppy-ProviderBrowser-list:after,.uppy-ProviderBrowser-viewType--unsplash ul.uppy-ProviderBrowser-list:after{content:"";flex:auto}.uppy-ProviderBrowser-viewType--grid li.uppy-ProviderBrowserItem,.uppy-ProviderBrowser-viewType--unsplash li.uppy-ProviderBrowserItem{margin:0;position:relative;width:50%}.uppy-size--md .uppy-ProviderBrowser-viewType--grid li.uppy-ProviderBrowserItem,.uppy-size--md .uppy-ProviderBrowser-viewType--unsplash li.uppy-ProviderBrowserItem{width:33.3333%}.uppy-size--lg .uppy-ProviderBrowser-viewType--grid li.uppy-ProviderBrowserItem,.uppy-size--lg .uppy-ProviderBrowser-viewType--unsplash li.uppy-ProviderBrowserItem{width:25%}.uppy-ProviderBrowser-viewType--grid li.uppy-ProviderBrowserItem:before,.uppy-ProviderBrowser-viewType--unsplash li.uppy-ProviderBrowserItem:before{content:"";display:block;padding-top:100%}.uppy-ProviderBrowser-viewType--grid li.uppy-ProviderBrowserItem--selected img,.uppy-ProviderBrowser-viewType--grid li.uppy-ProviderBrowserItem--selected svg,.uppy-ProviderBrowser-viewType--unsplash li.uppy-ProviderBrowserItem--selected img,.uppy-ProviderBrowser-viewType--unsplash li.uppy-ProviderBrowserItem--selected svg{opacity:.85}.uppy-ProviderBrowser-viewType--grid li.uppy-ProviderBrowserItem--disabled,.uppy-ProviderBrowser-viewType--unsplash li.uppy-ProviderBrowserItem--disabled{opacity:.5}.uppy-ProviderBrowser-viewType--grid li.uppy-ProviderBrowserItem--noPreview .uppy-ProviderBrowserItem-inner,.uppy-ProviderBrowser-viewType--unsplash li.uppy-ProviderBrowserItem--noPreview .uppy-ProviderBrowserItem-inner{background-color:#93939333}[data-uppy-theme=dark] .uppy-ProviderBrowser-viewType--grid li.uppy-ProviderBrowserItem--noPreview .uppy-ProviderBrowserItem-inner,[data-uppy-theme=dark] .uppy-ProviderBrowser-viewType--unsplash li.uppy-ProviderBrowserItem--noPreview .uppy-ProviderBrowserItem-inner{background-color:#eaeaea33}.uppy-ProviderBrowser-viewType--grid li.uppy-ProviderBrowserItem--noPreview svg,.uppy-ProviderBrowser-viewType--unsplash li.uppy-ProviderBrowserItem--noPreview svg{height:30%;width:30%;fill:#000000b3}[data-uppy-theme=dark] .uppy-ProviderBrowser-viewType--grid li.uppy-ProviderBrowserItem--noPreview svg,[data-uppy-theme=dark] .uppy-ProviderBrowser-viewType--unsplash li.uppy-ProviderBrowserItem--noPreview svg{fill:#fffc}.uppy-ProviderBrowser-viewType--grid .uppy-ProviderBrowserItem-inner,.uppy-ProviderBrowser-viewType--unsplash .uppy-ProviderBrowserItem-inner{border-radius:4px;height:calc(100% - 14px);inset:7px;overflow:hidden;position:absolute;text-align:center;width:calc(100% - 14px)}@media(hover:none){.uppy-ProviderBrowser-viewType--grid .uppy-ProviderBrowserItem-inner .uppy-ProviderBrowserItem-author,.uppy-ProviderBrowser-viewType--unsplash .uppy-ProviderBrowserItem-inner .uppy-ProviderBrowserItem-author{display:block}}[data-uppy-theme=dark] .uppy-ProviderBrowser-viewType--grid .uppy-ProviderBrowserItem-inner,[data-uppy-theme=dark] .uppy-ProviderBrowser-viewType--unsplash .uppy-ProviderBrowserItem-inner{box-shadow:0 0 0 3px #aae1ffb3}.uppy-ProviderBrowser-viewType--grid .uppy-ProviderBrowserItem-inner img,.uppy-ProviderBrowser-viewType--unsplash .uppy-ProviderBrowserItem-inner img{border-radius:4px;height:100%;object-fit:cover;width:100%}.uppy-ProviderBrowser-viewType--grid .uppy-ProviderBrowserItem-author,.uppy-ProviderBrowser-viewType--unsplash .uppy-ProviderBrowserItem-author{background:#0000004d;bottom:0;color:#fff;display:none;font-size:12px;font-weight:500;left:0;margin:0;padding:5px;position:absolute;text-decoration:none;width:100%}.uppy-ProviderBrowser-viewType--grid .uppy-ProviderBrowserItem-author:hover,.uppy-ProviderBrowser-viewType--unsplash .uppy-ProviderBrowserItem-author:hover{background:#0006;text-decoration:underline}.uppy-ProviderBrowser-viewType--grid .uppy-ProviderBrowserItem-checkbox,.uppy-ProviderBrowser-viewType--unsplash .uppy-ProviderBrowserItem-checkbox{background-color:#1269cf;border-radius:50%;height:26px;opacity:0;position:absolute;right:16px;top:16px;width:26px;z-index:1002}.uppy-ProviderBrowser-viewType--grid .uppy-ProviderBrowserItem-checkbox:after,.uppy-ProviderBrowser-viewType--unsplash .uppy-ProviderBrowserItem-checkbox:after{height:7px;inset-inline-start:7px;top:8px;width:12px}.uppy-ProviderBrowser-viewType--grid .uppy-ProviderBrowserItem--is-checked .uppy-ProviderBrowserItem-checkbox,.uppy-ProviderBrowser-viewType--unsplash .uppy-ProviderBrowserItem--is-checked .uppy-ProviderBrowserItem-checkbox{opacity:1}.uppy-ProviderBrowser-viewType--grid .uppy-ProviderBrowserItem-checkbox--grid:focus+label .uppy-ProviderBrowserItem-author,.uppy-ProviderBrowser-viewType--grid .uppy-ProviderBrowserItem-checkbox--grid:hover+label .uppy-ProviderBrowserItem-author,.uppy-ProviderBrowser-viewType--unsplash .uppy-ProviderBrowserItem-checkbox--grid:focus+label .uppy-ProviderBrowserItem-author,.uppy-ProviderBrowser-viewType--unsplash .uppy-ProviderBrowserItem-checkbox--grid:hover+label .uppy-ProviderBrowserItem-author{display:block}.uppy-ProviderBrowser-viewType--grid .uppy-ProviderBrowserItem-checkbox--grid:focus+label,.uppy-ProviderBrowser-viewType--unsplash .uppy-ProviderBrowserItem-checkbox--grid:focus+label{box-shadow:0 0 0 3px #1269cf80}.uppy-ProviderBrowser-viewType--grid .uppy-ProviderBrowserItem-checkbox--grid:focus+label:focus,.uppy-ProviderBrowser-viewType--unsplash .uppy-ProviderBrowserItem-checkbox--grid:focus+label:focus{outline:none}.uppy-ProviderBrowser-viewType--grid .uppy-ProviderBrowserItem-checkbox--grid:focus+label::-moz-focus-inner,.uppy-ProviderBrowser-viewType--unsplash .uppy-ProviderBrowserItem-checkbox--grid:focus+label::-moz-focus-inner{border:0}.uppy-ProviderBrowser-viewType--list{background-color:#fff}[data-uppy-theme=dark] .uppy-ProviderBrowser-viewType--list{background-color:#1f1f1f}.uppy-ProviderBrowser-viewType--list li.uppy-ProviderBrowserItem{align-items:center;display:flex;margin:0;padding:7px 15px}[data-uppy-theme=dark] .uppy-ProviderBrowser-viewType--list li.uppy-ProviderBrowserItem{color:#eaeaea}.uppy-ProviderBrowser-viewType--list li.uppy-ProviderBrowserItem--disabled{opacity:.6}.uppy-ProviderBrowser-viewType--list .uppy-ProviderBrowserItem-checkbox{background-color:#fff;border:1px solid #cfcfcf;border-radius:3px;height:17px;margin-inline-end:15px;width:17px}.uppy-ProviderBrowser-viewType--list .uppy-ProviderBrowserItem-checkbox:focus{border:1px solid #1269cf;box-shadow:0 0 0 3px #1269cf40;outline:none}.uppy-ProviderBrowser-viewType--list .uppy-ProviderBrowserItem-checkbox:after{height:5px;inset-inline-start:3px;opacity:0;top:4px;width:9px}[data-uppy-theme=dark] .uppy-ProviderBrowser-viewType--list .uppy-ProviderBrowserItem-checkbox:focus{border-color:#02baf2b3;box-shadow:0 0 0 3px #02baf233}.uppy-ProviderBrowser-viewType--list .uppy-ProviderBrowserItem--is-checked .uppy-ProviderBrowserItem-checkbox,.uppy-ProviderBrowser-viewType--list .uppy-ProviderBrowserItem--is-partial .uppy-ProviderBrowserItem-checkbox{background-color:#1269cf;border-color:#1269cf}.uppy-ProviderBrowser-viewType--list .uppy-ProviderBrowserItem--is-checked .uppy-ProviderBrowserItem-checkbox:after,.uppy-ProviderBrowser-viewType--list .uppy-ProviderBrowserItem--is-partial .uppy-ProviderBrowserItem-checkbox:after{opacity:1}.uppy-ProviderBrowser-viewType--list .uppy-ProviderBrowserItem-inner{align-items:center;color:inherit;display:flex;font-family:-apple-system,system-ui,BlinkMacSystemFont,Segoe UI,Segoe UI Symbol,Segoe UI Emoji,Apple Color Emoji,Roboto,Helvetica,Arial,sans-serif;overflow:hidden;padding:2px;text-overflow:ellipsis;white-space:nowrap}.uppy-ProviderBrowser-viewType--list .uppy-ProviderBrowserItem-inner:focus{outline:none;text-decoration:underline}.uppy-ProviderBrowser-viewType--list .uppy-ProviderBrowserItem-inner img,.uppy-ProviderBrowser-viewType--list .uppy-ProviderBrowserItem-inner svg{margin-inline-end:8px}.uppy-ProviderBrowser-viewType--list .uppy-ProviderBrowserItem-inner span{line-height:1.2;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.uppy-ProviderBrowser-viewType--list .uppy-ProviderBrowserItem--disabled .uppy-ProviderBrowserItem-inner{cursor:default}.uppy-ProviderBrowser-viewType--list .uppy-ProviderBrowserItem-iconWrap{margin-inline-end:7px;width:20px}.uppy-ProviderBrowserItem-checkbox{cursor:pointer;flex-shrink:0;position:relative}.uppy-ProviderBrowserItem-checkbox:disabled,.uppy-ProviderBrowserItem-checkbox:disabled:after{cursor:default}[data-uppy-theme=dark] .uppy-ProviderBrowserItem-checkbox{background-color:#1f1f1f;border-color:#939393}[data-uppy-theme=dark] .uppy-ProviderBrowserItem--is-checked .uppy-ProviderBrowserItem-checkbox{background-color:#333}.uppy-ProviderBrowserItem--is-checked .uppy-ProviderBrowserItem-checkbox:after{border-bottom:2px solid #eaeaea;border-left:2px solid #eaeaea;content:"";cursor:pointer;position:absolute;transform:rotate(-45deg)}.uppy-ProviderBrowserItem--is-partial .uppy-ProviderBrowserItem-checkbox:after{background-color:#eaeaea!important;content:""!important;height:2px!important;left:20%!important;position:absolute!important;right:20%!important;top:50%!important;transform:translateY(-50%)!important}.uppy-SearchProvider{align-items:center;display:flex;flex:1;flex-direction:column;height:100%;justify-content:center;width:100%}[data-uppy-theme=dark] .uppy-SearchProvider{background-color:#1f1f1f}.uppy-SearchProvider-input{margin-bottom:15px;max-width:650px;width:90%}.uppy-size--md .uppy-SearchProvider-input{margin-bottom:20px}.uppy-SearchProvider-input::-webkit-search-cancel-button{display:none}.uppy-SearchProvider-searchButton{padding:13px 25px}.uppy-size--md .uppy-SearchProvider-searchButton{padding:13px 30px}.uppy-DashboardContent-panelBody{align-items:center;display:flex;flex:1;justify-content:center}[data-uppy-theme=dark] .uppy-DashboardContent-panelBody{background-color:#1f1f1f}.uppy-Provider-auth,.uppy-Provider-empty,.uppy-Provider-error,.uppy-Provider-loading{align-items:center;color:#939393;display:flex;flex:1;flex-flow:column wrap;justify-content:center}.uppy-Provider-empty{color:#939393}.uppy-Provider-authIcon svg{height:75px;width:100px}.uppy-Provider-authTitle{color:#757575;font-size:17px;font-weight:400;line-height:1.4;margin-bottom:30px;max-width:500px;padding:0 15px;text-align:center}.uppy-size--md .uppy-Provider-authTitle{font-size:20px}[data-uppy-theme=dark] .uppy-Provider-authTitle{color:#cfcfcf}.uppy-Provider-btn-google{align-items:center;background:#4285f4;display:flex;padding:8px 12px!important}.uppy-Provider-btn-google:hover{background-color:#1266f1}.uppy-Provider-btn-google:focus{box-shadow:0 0 0 3px #4285f466;outline:none}.uppy-Provider-btn-google svg{margin-right:8px}.uppy-Provider-breadcrumbs{color:#525252;flex:1;font-size:12px;margin-bottom:10px;text-align:start}.uppy-size--md .uppy-Provider-breadcrumbs{margin-bottom:0}[data-uppy-theme=dark] .uppy-Provider-breadcrumbs{color:#eaeaea}.uppy-Provider-breadcrumbsIcon{color:#525252;display:inline-block;line-height:1;margin-inline-end:4px;vertical-align:middle}.uppy-Provider-breadcrumbsIcon svg{height:13px;width:13px;fill:#525252}.uppy-Provider-breadcrumbs button{border-radius:3px;display:inline-block;line-height:inherit;padding:4px}.uppy-Provider-breadcrumbs button:focus{outline:none}.uppy-Provider-breadcrumbs button::-moz-focus-inner{border:0}.uppy-Provider-breadcrumbs button:hover{color:#0e51a0}.uppy-Provider-breadcrumbs button:focus{background-color:#dfe6f1}[data-uppy-theme=dark] .uppy-Provider-breadcrumbs button:focus{background-color:#333}.uppy-Provider-breadcrumbs button:not(:last-of-type){text-decoration:underline}.uppy-Provider-breadcrumbs button:last-of-type{color:#333;cursor:normal;font-weight:500;pointer-events:none}.uppy-Provider-breadcrumbs button:hover{cursor:pointer}[data-uppy-theme=dark] .uppy-Provider-breadcrumbs button{color:#eaeaea}.uppy-ProviderBrowser{display:flex;flex:1;flex-direction:column;font-size:14px;font-weight:400;height:100%}.uppy-ProviderBrowser-user{color:#333;font-weight:500;margin:0 8px 0 0}[data-uppy-theme=dark] .uppy-ProviderBrowser-user{color:#eaeaea}.uppy-ProviderBrowser-user:after{color:#939393;content:"·";font-weight:400;inset-inline-start:4px;position:relative}.uppy-ProviderBrowser-header{border-bottom:1px solid #eaeaea;position:relative;z-index:1001}[data-uppy-theme=dark] .uppy-ProviderBrowser-header{border-bottom:1px solid #333}.uppy-ProviderBrowser-headerBar{background-color:#fafafa;color:#757575;font-size:12px;line-height:1.4;padding:7px 15px;z-index:1001}.uppy-size--md .uppy-ProviderBrowser-headerBar{align-items:center;display:flex}[data-uppy-theme=dark] .uppy-ProviderBrowser-headerBar{background-color:#1f1f1f}.uppy-ProviderBrowser-headerBar--simple{display:block;justify-content:center;text-align:center}.uppy-ProviderBrowser-headerBar--simple .uppy-Provider-breadcrumbsWrap{display:inline-block;flex:none;vertical-align:middle}.uppy-ProviderBrowser-searchFilter{align-items:center;display:flex;height:30px;margin-bottom:15px;margin-top:15px;padding-left:8px;padding-right:8px;position:relative;width:100%}.uppy-ProviderBrowser-searchFilterInput{background-color:#eaeaea;border:0;border-radius:4px;color:#333;font-family:-apple-system,system-ui,BlinkMacSystemFont,Segoe UI,Segoe UI Symbol,Segoe UI Emoji,Apple Color Emoji,Roboto,Helvetica,Arial,sans-serif;font-size:13px;height:30px;line-height:1.4;outline:0;padding-inline-end:30px;padding-inline-start:30px;width:100%;z-index:1001}.uppy-ProviderBrowser-searchFilterInput::-webkit-search-cancel-button{display:none}[data-uppy-theme=dark] .uppy-ProviderBrowser-searchFilterInput{background-color:#1f1f1f;color:#eaeaea}.uppy-ProviderBrowser-searchFilterInput:focus{background-color:#cfcfcf;border:0}[data-uppy-theme=dark] .uppy-ProviderBrowser-searchFilterInput:focus{background-color:#333}.uppy-ProviderBrowser-searchFilterIcon{color:#757575;height:12px;inset-inline-start:16px;position:absolute;width:12px;z-index:1002}.uppy-ProviderBrowser-searchFilterInput::placeholder{color:#939393;opacity:1}.uppy-ProviderBrowser-searchFilterReset{border-radius:3px;color:#939393;cursor:pointer;height:22px;inset-inline-end:16px;padding:6px;position:absolute;width:22px;z-index:1002}.uppy-ProviderBrowser-searchFilterReset:focus{outline:none}.uppy-ProviderBrowser-searchFilterReset::-moz-focus-inner{border:0}.uppy-ProviderBrowser-searchFilterReset:focus{box-shadow:0 0 0 3px #1269cf80}.uppy-ProviderBrowser-searchFilterReset:hover{color:#757575}.uppy-ProviderBrowser-searchFilterReset svg{vertical-align:text-top}.uppy-ProviderBrowser-userLogout{border-radius:3px;color:#1269cf;cursor:pointer;line-height:inherit;padding:4px}.uppy-ProviderBrowser-userLogout:focus{outline:none}.uppy-ProviderBrowser-userLogout::-moz-focus-inner{border:0}.uppy-ProviderBrowser-userLogout:hover{color:#0e51a0}.uppy-ProviderBrowser-userLogout:focus{background-color:#dfe6f1}[data-uppy-theme=dark] .uppy-ProviderBrowser-userLogout:focus{background-color:#333}.uppy-ProviderBrowser-userLogout:hover{text-decoration:underline}[data-uppy-theme=dark] .uppy-ProviderBrowser-userLogout{color:#eaeaea}.uppy-ProviderBrowser-body{flex:1;position:relative}.uppy-ProviderBrowser-list{background-color:#fff;border-spacing:0;display:block;flex:1;height:100%;inset:0;list-style:none;margin:0;overflow-x:hidden;overflow-y:auto;padding:0;position:absolute;width:100%;-webkit-overflow-scrolling:touch}[data-uppy-theme=dark] .uppy-ProviderBrowser-list{background-color:#1f1f1f}.uppy-ProviderBrowser-list:focus{outline:none}.uppy-ProviderBrowserItem-inner{cursor:pointer;font-size:13px;font-weight:500}.uppy-ProviderBrowser-footer{align-items:center;background-color:#fff;border-top:1px solid #eaeaea;display:flex;justify-content:space-between;padding:15px}.uppy-ProviderBrowser-footer button{margin-inline-end:8px}[data-uppy-theme=dark] .uppy-ProviderBrowser-footer{background-color:#1f1f1f;border-top:1px solid #333}.uppy-ProviderBrowser-footer-buttons{flex-shrink:0}.uppy-ProviderBrowser-footer-error{color:#e32437;line-height:18px}@media(max-width:426px){.uppy-ProviderBrowser-footer{align-items:stretch;flex-direction:column-reverse}.uppy-ProviderBrowser-footer-error{padding-bottom:10px}}.picker-dialog-bg{z-index:20000!important}.picker-dialog{z-index:20001!important}.uppy-Dashboard-Item-previewInnerWrap{align-items:center;border-radius:3px;box-shadow:0 0 2px #0006;display:flex;flex-direction:column;height:100%;justify-content:center;overflow:hidden;position:relative;width:100%}.uppy-size--md .uppy-Dashboard-Item-previewInnerWrap{box-shadow:0 1px 2px #00000026}.uppy-Dashboard--singleFile .uppy-Dashboard-Item-previewInnerWrap{box-shadow:none}.uppy-Dashboard-Item-previewInnerWrap:after{background-color:#000000a6;content:"";display:none;inset:0;position:absolute;z-index:1001}.uppy-Dashboard-Item-previewLink{inset:0;position:absolute;z-index:1002}.uppy-Dashboard-Item-previewLink:focus{box-shadow:inset 0 0 0 3px #579df0}[data-uppy-theme=dark] .uppy-Dashboard-Item-previewLink:focus{box-shadow:inset 0 0 0 3px #016c8d}.uppy-Dashboard-Item-preview img.uppy-Dashboard-Item-previewImg{border-radius:3px;height:100%;object-fit:cover;transform:translateZ(0);width:100%}.uppy-Dashboard--singleFile .uppy-Dashboard-Item-preview img.uppy-Dashboard-Item-previewImg{height:auto;max-height:100%;max-width:100%;object-fit:contain;padding:10px;width:auto}.uppy-Dashboard-Item-progress{color:#fff;left:50%;position:absolute;text-align:center;top:50%;transform:translate(-50%,-50%);transition:all .35 ease;width:120px;z-index:1002}.uppy-Dashboard-Item-progressIndicator{color:#fff;display:inline-block;height:38px;opacity:.9;width:38px}.uppy-size--md .uppy-Dashboard-Item-progressIndicator{height:55px;width:55px}button.uppy-Dashboard-Item-progressIndicator{cursor:pointer}button.uppy-Dashboard-Item-progressIndicator:focus{outline:none}button.uppy-Dashboard-Item-progressIndicator::-moz-focus-inner{border:0}button.uppy-Dashboard-Item-progressIndicator:focus .uppy-Dashboard-Item-progressIcon--bg,button.uppy-Dashboard-Item-progressIndicator:focus .uppy-Dashboard-Item-progressIcon--retry{fill:#579df0}.uppy-Dashboard-Item-progressIcon--circle{height:100%;width:100%}.uppy-Dashboard-Item-progressIcon--bg{stroke:#fff6}.uppy-Dashboard-Item-progressIcon--progress{transition:stroke-dashoffset .5s ease-out;stroke:#fff}.uppy-Dashboard-Item-progressIcon--play{transition:all .2s;fill:#fff;stroke:#fff}.uppy-Dashboard-Item-progressIcon--cancel{transition:all .2s;fill:#fff}.uppy-Dashboard-Item-progressIcon--pause{transition:all .2s;fill:#fff;stroke:#fff}.uppy-Dashboard-Item-progressIcon--check{transition:all .2s;fill:#fff}.uppy-Dashboard-Item-progressIcon--retry{fill:#fff}.uppy-Dashboard-Item.is-complete .uppy-Dashboard-Item-progress{inset-inline-end:-8px;inset-inline-start:auto;top:-9px;transform:none;width:auto}.uppy-Dashboard-Item.is-error .uppy-Dashboard-Item-progressIndicator{height:18px;width:18px}.uppy-size--md .uppy-Dashboard-Item.is-error .uppy-Dashboard-Item-progressIndicator{height:28px;width:28px}.uppy-Dashboard-Item.is-complete .uppy-Dashboard-Item-progressIndicator{height:18px;opacity:1;width:18px}.uppy-size--md .uppy-Dashboard-Item.is-complete .uppy-Dashboard-Item-progressIndicator{height:22px;width:22px}.uppy-Dashboard-Item.is-processing .uppy-Dashboard-Item-progress{opacity:0}.uppy-Dashboard-Item-fileInfo{padding-inline-end:5px}.uppy-Dashboard--singleFile .uppy-Dashboard-Item-fileInfo{padding-inline-end:10px}.uppy-size--md.uppy-Dashboard--singleFile .uppy-Dashboard-Item-fileInfo{padding-inline-end:15px}.uppy-Dashboard-Item-name{font-size:12px;font-weight:500;line-height:1.3;margin-bottom:5px;word-wrap:anywhere;word-break:break-all}[data-uppy-theme=dark] .uppy-Dashboard-Item-name{color:#eaeaea}.uppy-size--md.uppy-Dashboard--singleFile .uppy-Dashboard-Item-name{font-size:14px;line-height:1.4}.uppy-Dashboard-Item-fileName{align-items:baseline;display:flex}.uppy-Dashboard-Item-fileName button{margin-left:5px}.uppy-Dashboard-Item-author{color:#757575;display:inline-block;font-size:11px;font-weight:400;line-height:1;margin-bottom:5px;vertical-align:bottom}.uppy-Dashboard-Item-author a{color:#757575}.uppy-Dashboard-Item-status{color:#757575;font-size:11px;font-weight:400;line-height:1}[data-uppy-theme=dark] .uppy-Dashboard-Item-status{color:#bbb}.uppy-Dashboard-Item-statusSize{display:inline-block;margin-bottom:5px;text-transform:uppercase;vertical-align:bottom}.uppy-Dashboard-Item-reSelect{color:#1269cf;font-family:inherit;font-size:inherit;font-weight:600}.uppy-Dashboard-Item-errorMessage{background-color:#fdeff1;color:#a51523;font-size:11px;font-weight:500;line-height:1.3;padding:5px 6px}.uppy-Dashboard-Item-errorMessageBtn{color:#a51523;cursor:pointer;font-size:11px;font-weight:500;text-decoration:underline}.uppy-Dashboard-Item-preview .uppy-Dashboard-Item-errorMessage{display:none}.uppy-size--md .uppy-Dashboard-Item-preview .uppy-Dashboard-Item-errorMessage{border-bottom-left-radius:3px;border-bottom-right-radius:3px;border-top:1px solid #f7c2c8;bottom:0;display:block;left:0;line-height:1.4;padding:6px 8px;position:absolute;right:0}.uppy-Dashboard-Item-fileInfo .uppy-Dashboard-Item-errorMessage{border:1px solid #f7c2c8;border-radius:3px;display:inline-block;position:static}.uppy-size--md .uppy-Dashboard-Item-fileInfo .uppy-Dashboard-Item-errorMessage{display:none}.uppy-Dashboard-Item-action{color:#939393;cursor:pointer}.uppy-Dashboard-Item-action:focus{outline:none}.uppy-Dashboard-Item-action::-moz-focus-inner{border:0}.uppy-Dashboard-Item-action:focus{box-shadow:0 0 0 3px #1269cf80}.uppy-Dashboard-Item-action:hover{color:#1f1f1f;opacity:1}[data-uppy-theme=dark] .uppy-Dashboard-Item-action{color:#cfcfcf}[data-uppy-theme=dark] .uppy-Dashboard-Item-action:focus{outline:none}[data-uppy-theme=dark] .uppy-Dashboard-Item-action::-moz-focus-inner{border:0}[data-uppy-theme=dark] .uppy-Dashboard-Item-action:focus{box-shadow:0 0 0 2px #aae1ffd9}[data-uppy-theme=dark] .uppy-Dashboard-Item-action:hover{color:#eaeaea}.uppy-Dashboard-Item-action--remove{color:#1f1f1f;opacity:.95}.uppy-Dashboard-Item-action--remove:hover{color:#000;opacity:1}.uppy-size--md .uppy-Dashboard-Item-action--remove{height:18px;inset-inline-end:-8px;padding:0;position:absolute;top:-8px;width:18px;z-index:1002}.uppy-size--md .uppy-Dashboard-Item-action--remove:focus{border-radius:50%}.uppy-Dashboard--singleFile.uppy-size--height-md .uppy-Dashboard-Item-action--remove{inset-inline-end:8px;position:absolute;top:8px}[data-uppy-theme=dark] .uppy-Dashboard-Item-action--remove{color:#525252}[data-uppy-theme=dark] .uppy-Dashboard-Item-action--remove:hover{color:#333}.uppy-Dashboard:not(.uppy-size--md):not(.uppy-Dashboard--singleFile.uppy-size--height-md) .uppy-Dashboard-Item-actionWrapper{align-items:center;display:flex}.uppy-Dashboard:not(.uppy-size--md):not(.uppy-Dashboard--singleFile.uppy-size--height-md) .uppy-Dashboard-Item-action{height:22px;margin-left:3px;padding:3px;width:22px}.uppy-Dashboard:not(.uppy-size--md):not(.uppy-Dashboard--singleFile.uppy-size--height-md) .uppy-Dashboard-Item-action:focus{border-radius:3px}.uppy-size--md .uppy-Dashboard-Item-action--copyLink,.uppy-size--md .uppy-Dashboard-Item-action--edit{height:16px;padding:0;width:16px}.uppy-size--md .uppy-Dashboard-Item-action--copyLink:focus,.uppy-size--md .uppy-Dashboard-Item-action--edit:focus{border-radius:3px}.uppy-Dashboard-Item{align-items:center;border-bottom:1px solid #eaeaea;display:flex;padding:10px}.uppy-Dashboard:not(.uppy-Dashboard--singleFile) .uppy-Dashboard-Item{padding-inline-end:0}[data-uppy-theme=dark] .uppy-Dashboard-Item{border-bottom:1px solid #333}.uppy-size--md .uppy-Dashboard-Item{border-bottom:0;display:block;float:inline-start;height:215px;margin:5px 15px;padding:0;position:relative;width:calc(33.333% - 30px)}.uppy-size--lg .uppy-Dashboard-Item{height:190px;margin:5px 15px;padding:0;width:calc(25% - 30px)}.uppy-size--xl .uppy-Dashboard-Item{height:210px;padding:0;width:calc(20% - 30px)}.uppy-Dashboard--singleFile .uppy-Dashboard-Item{border-bottom:0;display:flex;flex-direction:column;height:100%;max-width:400px;padding:15px;position:relative;width:100%}.uppy-Dashboard-Item.is-ghost .uppy-Dashboard-Item-previewInnerWrap{opacity:.2}.uppy-Dashboard-Item.is-ghost .uppy-Dashboard-Item-name{opacity:.7}.uppy-Dashboard-Item.is-ghost .uppy-Dashboard-Item-preview:before{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='35' height='39' viewBox='0 0 35 39'%3E%3Cpath fill='%2523000' d='M1.708 38.66c1.709 0 3.417-3.417 6.834-3.417s5.125 3.417 8.61 3.417c3.348 0 5.056-3.417 8.473-3.417 4.305 0 5.125 3.417 6.833 3.417.889 0 1.709-.889 1.709-1.709v-19.68C34.167-5.757 0-5.757 0 17.271v19.68c0 .82.888 1.709 1.708 1.709m8.542-17.084a3.383 3.383 0 0 1-3.417-3.416 3.383 3.383 0 0 1 3.417-3.417 3.383 3.383 0 0 1 3.417 3.417 3.383 3.383 0 0 1-3.417 3.416m13.667 0A3.383 3.383 0 0 1 20.5 18.16a3.383 3.383 0 0 1 3.417-3.417 3.383 3.383 0 0 1 3.416 3.417 3.383 3.383 0 0 1-3.416 3.416'/%3E%3C/svg%3E");background-position:50% 10px;background-repeat:no-repeat;background-size:25px;content:"";inset:0;opacity:.5;position:absolute;z-index:1005}.uppy-size--md .uppy-Dashboard-Item.is-ghost .uppy-Dashboard-Item-preview:before{background-position:50% 50%;background-size:40px}.uppy-Dashboard--singleFile .uppy-Dashboard-Item.is-ghost .uppy-Dashboard-Item-preview:before{background-position:50% 50%;background-size:30%}.uppy-Dashboard-Item-preview{flex-grow:0;flex-shrink:0;height:50px;position:relative;width:50px}.uppy-size--md .uppy-Dashboard-Item-preview{height:140px;width:100%}.uppy-size--lg .uppy-Dashboard-Item-preview{height:120px}.uppy-size--xl .uppy-Dashboard-Item-preview{height:140px}.uppy-Dashboard--singleFile .uppy-Dashboard-Item-preview{flex-grow:1;max-height:75%;width:100%}.uppy-Dashboard--singleFile.uppy-size--md .uppy-Dashboard-Item-preview{max-height:100%}.uppy-Dashboard-Item-fileInfoAndButtons{align-items:center;display:flex;flex-grow:1;justify-content:space-between;padding-inline-end:8px;padding-inline-start:12px}.uppy-Dashboard--singleFile .uppy-Dashboard-Item-fileInfoAndButtons,.uppy-size--md .uppy-Dashboard-Item-fileInfoAndButtons{align-items:flex-start;padding:9px 0 0}.uppy-Dashboard--singleFile .uppy-Dashboard-Item-fileInfoAndButtons{flex-grow:0;width:100%}.uppy-Dashboard-Item-fileInfo{flex-grow:1;flex-shrink:1}.uppy-Dashboard-Item-actionWrapper{flex-grow:0;flex-shrink:0}.uppy-Dashboard-Item.is-error .uppy-Dashboard-Item-previewInnerWrap:after,.uppy-Dashboard-Item.is-inprogress .uppy-Dashboard-Item-previewInnerWrap:after{display:block}.uppy-Dashboard-Item-errorDetails{appearance:none;background-color:#939393;border:none;border-radius:50%;color:#fff;cursor:help;flex-shrink:0;font-size:10px;font-weight:600;height:13px;inset-inline-start:2px;line-height:12px;position:relative;text-align:center;top:0;width:13px}.uppy-Dashboard-Item-errorDetails:after{line-height:1.3;word-wrap:break-word}.uppy-Dashboard-FileCard{background-color:#fff;border-radius:5px;box-shadow:0 0 10px 4px #0000001a;display:flex;flex-direction:column;height:100%;inset:0;position:absolute;width:100%;z-index:1005}.uppy-Dashboard-FileCard .uppy-DashboardContent-bar{border-top-left-radius:5px;border-top-right-radius:5px}.uppy-Dashboard-FileCard .uppy-Dashboard-FileCard-actions{border-bottom-left-radius:5px;border-bottom-right-radius:5px}.uppy-Dashboard-FileCard-inner{display:flex;flex-direction:column;flex-grow:1;flex-shrink:1;height:100%;min-height:0}.uppy-Dashboard-FileCard-preview{align-items:center;border-bottom:1px solid #eaeaea;display:flex;flex-grow:0;flex-shrink:1;height:60%;justify-content:center;min-height:0;position:relative}[data-uppy-theme=dark] .uppy-Dashboard-FileCard-preview{background-color:#333;border-bottom:0}.uppy-Dashboard-FileCard-preview img.uppy-Dashboard-Item-previewImg{border-radius:3px;box-shadow:0 3px 20px #00000026;flex:0 0 auto;max-height:90%;max-width:90%;object-fit:cover}.uppy-Dashboard-FileCard-edit{background-color:#00000080;border-radius:50px;color:#fff;font-size:13px;inset-inline-end:10px;padding:7px 15px;position:absolute;top:10px}.uppy-Dashboard-FileCard-edit:focus{outline:none}.uppy-Dashboard-FileCard-edit::-moz-focus-inner{border:0}.uppy-Dashboard-FileCard-edit:focus{box-shadow:0 0 0 3px #1269cf80}.uppy-Dashboard-FileCard-edit:hover{background-color:#000c}.uppy-Dashboard-FileCard-info{flex-grow:0;flex-shrink:0;height:40%;overflow-y:auto;padding:30px 20px 20px;-webkit-overflow-scrolling:touch}[data-uppy-theme=dark] .uppy-Dashboard-FileCard-info{background-color:#1f1f1f}.uppy-Dashboard-FileCard-fieldset{border:0;font-size:0;margin:auto auto 12px;max-width:640px;padding:0}.uppy-Dashboard-FileCard-label{color:#525252;display:inline-block;font-size:12px;vertical-align:middle;width:22%}.uppy-size--md .uppy-Dashboard-FileCard-label{font-size:14px}[data-uppy-theme=dark] .uppy-Dashboard-FileCard-label{color:#eaeaea}.uppy-Dashboard-FileCard-input{display:inline-block;vertical-align:middle;width:78%}.uppy-Dashboard-FileCard-actions{align-items:center;background-color:#fafafa;border-top:1px solid #eaeaea;display:flex;flex-grow:0;flex-shrink:0;height:55px;padding:0 15px}.uppy-size--md .uppy-Dashboard-FileCard-actions{height:65px}[data-uppy-theme=dark] .uppy-Dashboard-FileCard-actions{background-color:#1f1f1f;border-top:1px solid #333}.uppy-Dashboard-FileCard-actionsBtn{margin-inline-end:10px}.uppy-Informer{bottom:60px;left:0;position:absolute;right:0;text-align:center;z-index:1005}.uppy-Informer span>div{margin-bottom:6px}.uppy-Informer-animated{opacity:0;transform:translateY(350%);transition:all .3s ease-in;z-index:-1000}.uppy-Informer p{background-color:#757575;border-radius:18px;color:#fff;display:inline-block;font-size:12px;font-weight:400;line-height:1.4;margin:0;max-width:90%;padding:6px 15px}.uppy-size--md .uppy-Informer p{font-size:14px;line-height:1.3;max-width:500px;padding:10px 20px}[data-uppy-theme=dark] .uppy-Informer p{background-color:#333}.uppy-Informer p span{background-color:#fff;border-radius:50%;color:#525252;display:inline-block;font-size:10px;height:13px;inset-inline-start:3px;line-height:12px;margin-inline-start:-1px;position:relative;top:-1px;vertical-align:middle;width:13px}.uppy-Informer p span:hover{cursor:help}.uppy-Informer p span:after{line-height:1.3;word-wrap:break-word}.uppy-Root [aria-label][role~=tooltip]{position:relative}.uppy-Root [aria-label][role~=tooltip]:after,.uppy-Root [aria-label][role~=tooltip]:before{-webkit-backface-visibility:hidden;backface-visibility:hidden;box-sizing:border-box;opacity:0;pointer-events:none;position:absolute;transform:translateZ(0);transform-origin:top;transition:all var(--microtip-transition-duration,.18s) var(--microtip-transition-easing,ease-in-out) var(--microtip-transition-delay,0s);will-change:transform;z-index:10}.uppy-Root [aria-label][role~=tooltip]:before{background-size:100% auto!important;content:""}.uppy-Root [aria-label][role~=tooltip]:after{background:#111111e6;border-radius:4px;box-sizing:initial;color:#fff;content:attr(aria-label);font-size:var(--microtip-font-size,13px);font-weight:var(--microtip-font-weight,normal);padding:.5em 1em;text-transform:var(--microtip-text-transform,none);white-space:nowrap}.uppy-Root [aria-label][role~=tooltip]:focus:after,.uppy-Root [aria-label][role~=tooltip]:focus:before,.uppy-Root [aria-label][role~=tooltip]:hover:after,.uppy-Root [aria-label][role~=tooltip]:hover:before{opacity:1;pointer-events:auto}.uppy-Root [role~=tooltip][data-microtip-position|=top]:before{background:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='36' height='12'%3E%3Cpath fill='rgba(17, 17, 17, 0.9)' d='M2.658 0h32.004c-6 0-11.627 12.002-16.002 12.002S8.594 0 2.658 0'/%3E%3C/svg%3E") no-repeat;bottom:100%;height:6px;left:50%;margin-bottom:5px;transform:translate3d(-50%,0,0);width:18px}.uppy-Root [role~=tooltip][data-microtip-position|=top]:after{bottom:100%;left:50%;margin-bottom:11px;transform:translate3d(-50%,0,0)}.uppy-Root [role~=tooltip][data-microtip-position=top]:hover:after,.uppy-Root [role~=tooltip][data-microtip-position|=top]:hover:before{transform:translate3d(-50%,-5px,0)}.uppy-Root [role~=tooltip][data-microtip-position=top-left]:after{bottom:100%;transform:translate3d(calc(-100% + 16px),0,0)}.uppy-Root [role~=tooltip][data-microtip-position=top-left]:hover:after{transform:translate3d(calc(-100% + 16px),-5px,0)}.uppy-Root [role~=tooltip][data-microtip-position=top-right]:after{bottom:100%;transform:translate3d(-16px,0,0)}.uppy-Root [role~=tooltip][data-microtip-position=top-right]:hover:after{transform:translate3d(-16px,-5px,0)}.uppy-Root [role~=tooltip][data-microtip-position|=bottom]:before{background:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='36' height='12'%3E%3Cpath fill='rgba(17, 17, 17, 0.9)' d='M33.342 12H1.338c6 0 11.627-12.002 16.002-12.002S27.406 12 33.342 12'/%3E%3C/svg%3E") no-repeat;bottom:auto;height:6px;left:50%;margin-bottom:0;margin-top:5px;top:100%;transform:translate3d(-50%,-10px,0);width:18px}.uppy-Root [role~=tooltip][data-microtip-position|=bottom]:after{left:50%;margin-top:11px;top:100%;transform:translate3d(-50%,-10px,0)}.uppy-Root [role~=tooltip][data-microtip-position=bottom]:hover:after,.uppy-Root [role~=tooltip][data-microtip-position|=bottom]:hover:before{transform:translate3d(-50%,0,0)}.uppy-Root [role~=tooltip][data-microtip-position=bottom-left]:after{top:100%;transform:translate3d(calc(-100% + 16px),-10px,0)}.uppy-Root [role~=tooltip][data-microtip-position=bottom-left]:hover:after{transform:translate3d(calc(-100% + 16px),0,0)}.uppy-Root [role~=tooltip][data-microtip-position=bottom-right]:after{top:100%;transform:translate3d(-16px,-10px,0)}.uppy-Root [role~=tooltip][data-microtip-position=bottom-right]:hover:after{transform:translate3d(-16px,0,0)}.uppy-Root [role~=tooltip][data-microtip-position=left]:after,.uppy-Root [role~=tooltip][data-microtip-position=left]:before{inset:50% 100% auto auto;transform:translate3d(10px,-50%,0)}.uppy-Root [role~=tooltip][data-microtip-position=left]:before{background:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='36'%3E%3Cpath fill='rgba(17, 17, 17, 0.9)' d='M0 33.342V1.338c0 6 12.002 11.627 12.002 16.002S0 27.406 0 33.342'/%3E%3C/svg%3E") no-repeat;height:18px;margin-bottom:0;margin-right:5px;width:6px}.uppy-Root [role~=tooltip][data-microtip-position=left]:after{margin-right:11px}.uppy-Root [role~=tooltip][data-microtip-position=left]:hover:after,.uppy-Root [role~=tooltip][data-microtip-position=left]:hover:before{transform:translate3d(0,-50%,0)}.uppy-Root [role~=tooltip][data-microtip-position=right]:after,.uppy-Root [role~=tooltip][data-microtip-position=right]:before{bottom:auto;left:100%;top:50%;transform:translate3d(-10px,-50%,0)}.uppy-Root [role~=tooltip][data-microtip-position=right]:before{background:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='36'%3E%3Cpath fill='rgba(17, 17, 17, 0.9)' d='M12 2.658v32.004c0-6-12.002-11.627-12.002-16.002S12 8.594 12 2.658'/%3E%3C/svg%3E") no-repeat;height:18px;margin-bottom:0;margin-left:5px;width:6px}.uppy-Root [role~=tooltip][data-microtip-position=right]:after{margin-left:11px}.uppy-Root [role~=tooltip][data-microtip-position=right]:hover:after,.uppy-Root [role~=tooltip][data-microtip-position=right]:hover:before{transform:translate3d(0,-50%,0)}.uppy-Root [role~=tooltip][data-microtip-size=small]:after{white-space:normal;width:80px}.uppy-Root [role~=tooltip][data-microtip-size=medium]:after{white-space:normal;width:150px}.uppy-Root [role~=tooltip][data-microtip-size=large]:after{white-space:normal;width:260px}.uppy-StatusBar{background-color:#fff;color:#fff;display:flex;font-size:12px;font-weight:400;height:46px;line-height:40px;position:relative;transition:height .2s;z-index:1001}[data-uppy-theme=dark] .uppy-StatusBar{background-color:#1f1f1f}.uppy-StatusBar:before{background-color:#eaeaea;content:"";height:2px;inset:0;position:absolute;width:100%}[data-uppy-theme=dark] .uppy-StatusBar:before{background-color:#757575}.uppy-StatusBar[aria-hidden=true]{height:0;overflow-y:hidden}.uppy-StatusBar.is-complete .uppy-StatusBar-progress{background-color:#1bb240}.uppy-StatusBar.is-error .uppy-StatusBar-progress{background-color:#e32437}.uppy-StatusBar.is-complete .uppy-StatusBar-statusIndicator{color:#1bb240}.uppy-StatusBar.is-error .uppy-StatusBar-statusIndicator{color:#e32437}.uppy-StatusBar:not([aria-hidden=true]).is-waiting{background-color:#fff;border-top:1px solid #eaeaea;height:65px}[data-uppy-theme=dark] .uppy-StatusBar:not([aria-hidden=true]).is-waiting{background-color:#1f1f1f;border-top:1px solid #333}.uppy-StatusBar-progress{background-color:#1269cf;height:2px;position:absolute;transition:background-color,width .3s ease-out;z-index:1001}.uppy-StatusBar-progress.is-indeterminate{animation:uppy-StatusBar-ProgressStripes 1s linear infinite;background-image:linear-gradient(45deg,#0000004d 25%,#0000 0 50%,#0000004d 0 75%,#0000 0,#0000);background-size:64px 64px}@keyframes uppy-StatusBar-ProgressStripes{0%{background-position:0 0}to{background-position:64px 0}}.uppy-StatusBar.is-postprocessing .uppy-StatusBar-progress,.uppy-StatusBar.is-preprocessing .uppy-StatusBar-progress{background-color:#f6a623}.uppy-StatusBar.is-waiting .uppy-StatusBar-progress{display:none}.uppy-StatusBar-content{align-items:center;color:#333;display:flex;height:100%;padding-inline-start:10px;position:relative;text-overflow:ellipsis;white-space:nowrap;z-index:1002}.uppy-size--md .uppy-StatusBar-content{padding-inline-start:15px}[data-uppy-theme=dark] .uppy-StatusBar-content{color:#eaeaea}.uppy-StatusBar-status{display:flex;flex-direction:column;font-weight:400;justify-content:center;line-height:1.4;padding-inline-end:.3em}.uppy-StatusBar-statusPrimary{display:flex;font-weight:500;line-height:1}.uppy-StatusBar-statusPrimary button.uppy-StatusBar-details{margin-left:5px}[data-uppy-theme=dark] .uppy-StatusBar-statusPrimary{color:#eaeaea}.uppy-StatusBar-statusSecondary{color:#757575;display:inline-block;font-size:11px;line-height:1.2;margin-top:1px;white-space:nowrap}[data-uppy-theme=dark] .uppy-StatusBar-statusSecondary{color:#bbb}.uppy-StatusBar-statusSecondaryHint{display:inline-block;line-height:1;margin-inline-end:5px;vertical-align:middle}.uppy-size--md .uppy-StatusBar-statusSecondaryHint{margin-inline-end:8px}.uppy-StatusBar-statusIndicator{color:#525252;margin-inline-end:7px;position:relative;top:1px}.uppy-StatusBar-statusIndicator svg{vertical-align:text-bottom}.uppy-StatusBar-actions{align-items:center;bottom:0;display:flex;inset-inline-end:10px;position:absolute;top:0;z-index:1004}.uppy-StatusBar.is-waiting .uppy-StatusBar-actions{background-color:#fafafa;height:100%;padding:0 15px;position:static;width:100%}[data-uppy-theme=dark] .uppy-StatusBar.is-waiting .uppy-StatusBar-actions{background-color:#1f1f1f}.uppy-StatusBar:not([aria-hidden=true]).is-waiting.has-ghosts{flex-direction:column;height:90px}.uppy-size--md .uppy-StatusBar:not([aria-hidden=true]).is-waiting.has-ghosts{flex-direction:row;height:65px}.uppy-StatusBar:not([aria-hidden=true]).is-waiting.has-ghosts .uppy-StatusBar-actions{flex-direction:column;justify-content:center}.uppy-size--md .uppy-StatusBar:not([aria-hidden=true]).is-waiting.has-ghosts .uppy-StatusBar-actions{flex-direction:row;justify-content:normal}.uppy-StatusBar-actionCircleBtn{cursor:pointer;line-height:1;margin:3px;opacity:.9}.uppy-StatusBar-actionCircleBtn:focus{outline:none}.uppy-StatusBar-actionCircleBtn::-moz-focus-inner{border:0}.uppy-StatusBar-actionCircleBtn:focus{box-shadow:0 0 0 3px #1269cf80}[data-uppy-theme=dark] .uppy-StatusBar-actionCircleBtn:focus{outline:none}[data-uppy-theme=dark] .uppy-StatusBar-actionCircleBtn::-moz-focus-inner{border:0}[data-uppy-theme=dark] .uppy-StatusBar-actionCircleBtn:focus{box-shadow:0 0 0 2px #aae1ffd9}.uppy-StatusBar-actionCircleBtn:hover{opacity:1}.uppy-StatusBar-actionCircleBtn:focus{border-radius:50%}.uppy-StatusBar-actionCircleBtn svg{vertical-align:bottom}.uppy-StatusBar-actionBtn{color:#1269cf;display:inline-block;font-size:10px;line-height:inherit;vertical-align:middle}.uppy-size--md .uppy-StatusBar-actionBtn{font-size:11px}.uppy-StatusBar-actionBtn--disabled{opacity:.4}[data-uppy-theme=dark] .uppy-StatusBar-actionBtn--disabled{opacity:.7}.uppy-StatusBar-actionBtn--retry{background-color:#ff4b23;border-radius:8px;color:#fff;height:16px;line-height:1;margin-inline-end:6px;padding:1px 6px 3px 18px;position:relative}.uppy-StatusBar-actionBtn--retry:focus{outline:none}.uppy-StatusBar-actionBtn--retry::-moz-focus-inner{border:0}.uppy-StatusBar-actionBtn--retry:focus{box-shadow:0 0 0 3px #1269cf80}[data-uppy-theme=dark] .uppy-StatusBar-actionBtn--retry:focus{outline:none}[data-uppy-theme=dark] .uppy-StatusBar-actionBtn--retry::-moz-focus-inner{border:0}[data-uppy-theme=dark] .uppy-StatusBar-actionBtn--retry:focus{box-shadow:0 0 0 2px #aae1ffd9}.uppy-StatusBar-actionBtn--retry:hover{background-color:#f92d00}.uppy-StatusBar-actionBtn--retry svg{inset-inline-start:6px;position:absolute;top:3px}.uppy-StatusBar.is-waiting .uppy-StatusBar-actionBtn--upload{background-color:#1bb240;color:#fff;font-size:14px;line-height:1;padding:15px 10px;width:100%}.uppy-StatusBar.is-waiting .uppy-StatusBar-actionBtn--upload:hover{background-color:#189c38}[data-uppy-theme=dark] .uppy-StatusBar.is-waiting .uppy-StatusBar-actionBtn--upload{background-color:#1c8b37}[data-uppy-theme=dark] .uppy-StatusBar.is-waiting .uppy-StatusBar-actionBtn--upload:hover{background-color:#18762f}.uppy-size--md .uppy-StatusBar.is-waiting .uppy-StatusBar-actionBtn--upload{padding:13px 22px;width:auto}.uppy-StatusBar.is-waiting .uppy-StatusBar-actionBtn--upload.uppy-StatusBar-actionBtn--disabled:hover{background-color:#1bb240;cursor:not-allowed}[data-uppy-theme=dark] .uppy-StatusBar.is-waiting .uppy-StatusBar-actionBtn--upload.uppy-StatusBar-actionBtn--disabled:hover{background-color:#1c8b37}.uppy-StatusBar:not(.is-waiting) .uppy-StatusBar-actionBtn--upload{background-color:initial;color:#1269cf}.uppy-StatusBar-actionBtn--uploadNewlyAdded{border-radius:3px;padding-inline-end:3px;padding-bottom:1px;padding-inline-start:3px}.uppy-StatusBar-actionBtn--uploadNewlyAdded:focus{outline:none}.uppy-StatusBar-actionBtn--uploadNewlyAdded::-moz-focus-inner{border:0}.uppy-StatusBar-actionBtn--uploadNewlyAdded:focus{box-shadow:0 0 0 3px #1269cf80}[data-uppy-theme=dark] .uppy-StatusBar-actionBtn--uploadNewlyAdded:focus{outline:none}[data-uppy-theme=dark] .uppy-StatusBar-actionBtn--uploadNewlyAdded::-moz-focus-inner{border:0}[data-uppy-theme=dark] .uppy-StatusBar-actionBtn--uploadNewlyAdded:focus{box-shadow:0 0 0 2px #aae1ffd9}.uppy-StatusBar.is-postprocessing .uppy-StatusBar-actionBtn--uploadNewlyAdded,.uppy-StatusBar.is-preprocessing .uppy-StatusBar-actionBtn--uploadNewlyAdded{display:none}.uppy-StatusBar-actionBtn--done{border-radius:3px;line-height:1;padding:7px 8px}.uppy-StatusBar-actionBtn--done:focus{outline:none}.uppy-StatusBar-actionBtn--done::-moz-focus-inner{border:0}.uppy-StatusBar-actionBtn--done:hover{color:#0e51a0}.uppy-StatusBar-actionBtn--done:focus{background-color:#dfe6f1}[data-uppy-theme=dark] .uppy-StatusBar-actionBtn--done:focus{background-color:#333}[data-uppy-theme=dark] .uppy-StatusBar-actionBtn--done{color:#02baf2}.uppy-size--md .uppy-StatusBar-actionBtn--done{font-size:14px}.uppy-StatusBar-serviceMsg{color:#000;font-size:11px;line-height:1.1;padding-left:10px}.uppy-size--md .uppy-StatusBar-serviceMsg{font-size:14px;padding-left:15px}[data-uppy-theme=dark] .uppy-StatusBar-serviceMsg{color:#eaeaea}.uppy-StatusBar-serviceMsg-ghostsIcon{left:6px;opacity:.5;position:relative;top:2px;vertical-align:text-bottom;width:10px}.uppy-size--md .uppy-StatusBar-serviceMsg-ghostsIcon{left:10px;top:1px;width:15px}.uppy-StatusBar-details{appearance:none;background-color:#939393;border-radius:50%;color:#fff;cursor:help;display:inline-block;font-size:10px;font-weight:600;height:13px;inset-inline-start:2px;line-height:12px;position:relative;text-align:center;top:0;vertical-align:middle;width:13px}.uppy-StatusBar-details:after{line-height:1.3;word-wrap:break-word}.uppy-StatusBar-spinner{animation-duration:1s;animation-iteration-count:infinite;animation-name:uppy-StatusBar-spinnerAnimation;animation-timing-function:linear;fill:#1269cf;margin-inline-end:10px}.uppy-StatusBar.is-postprocessing .uppy-StatusBar-spinner,.uppy-StatusBar.is-preprocessing .uppy-StatusBar-spinner{fill:#f6a623}@keyframes uppy-StatusBar-spinnerAnimation{0%{transform:rotate(0)}to{transform:rotate(1turn)}}.uppy-transition-slideDownUp-enter{opacity:.01;transform:translate3d(0,-105%,0);transition:transform .25s ease-in-out,opacity .25s ease-in-out}.uppy-transition-slideDownUp-enter.uppy-transition-slideDownUp-enter-active{opacity:1;transform:translateZ(0)}.uppy-transition-slideDownUp-leave{opacity:1;transform:translateZ(0);transition:transform .25s ease-in-out,opacity .25s ease-in-out}.uppy-transition-slideDownUp-leave.uppy-transition-slideDownUp-leave-active{opacity:.01;transform:translate3d(0,-105%,0)}@keyframes uppy-Dashboard-fadeIn{0%{opacity:0}to{opacity:1}}@keyframes uppy-Dashboard-fadeOut{0%{opacity:1}to{opacity:0}}@keyframes uppy-Dashboard-slideDownAndFadeIn{0%{opacity:0;transform:translate3d(-50%,-70%,0)}to{opacity:1;transform:translate3d(-50%,-50%,0)}}@keyframes uppy-Dashboard-slideDownAndFadeIn--small{0%{opacity:0;transform:translate3d(0,-20%,0)}to{opacity:1;transform:translateZ(0)}}@keyframes uppy-Dashboard-slideUpFadeOut{0%{opacity:1;transform:translate3d(-50%,-50%,0)}to{opacity:0;transform:translate3d(-50%,-70%,0)}}@keyframes uppy-Dashboard-slideUpFadeOut--small{0%{opacity:1;transform:translateZ(0)}to{opacity:0;transform:translate3d(0,-20%,0)}}.uppy-Dashboard--modal{z-index:1001}.uppy-Dashboard--modal[aria-hidden=true]{display:none}.uppy-Dashboard--modal.uppy-Dashboard--animateOpenClose>.uppy-Dashboard-inner{animation:uppy-Dashboard-slideDownAndFadeIn--small .3s cubic-bezier(0,0,.2,1)}@media only screen and (min-width:820px){.uppy-Dashboard--modal.uppy-Dashboard--animateOpenClose>.uppy-Dashboard-inner{animation:uppy-Dashboard-slideDownAndFadeIn .3s cubic-bezier(0,0,.2,1)}}.uppy-Dashboard--modal.uppy-Dashboard--animateOpenClose>.uppy-Dashboard-overlay{animation:uppy-Dashboard-fadeIn .3s cubic-bezier(0,0,.2,1)}.uppy-Dashboard--modal.uppy-Dashboard--animateOpenClose.uppy-Dashboard--isClosing>.uppy-Dashboard-inner{animation:uppy-Dashboard-slideUpFadeOut--small .3s cubic-bezier(0,0,.2,1)}@media only screen and (min-width:820px){.uppy-Dashboard--modal.uppy-Dashboard--animateOpenClose.uppy-Dashboard--isClosing>.uppy-Dashboard-inner{animation:uppy-Dashboard-slideUpFadeOut .3s cubic-bezier(0,0,.2,1)}}.uppy-Dashboard--modal.uppy-Dashboard--animateOpenClose.uppy-Dashboard--isClosing>.uppy-Dashboard-overlay{animation:uppy-Dashboard-fadeOut .3s cubic-bezier(0,0,.2,1)}.uppy-Dashboard-isFixed{height:100vh;overflow:hidden}.uppy-Dashboard--modal .uppy-Dashboard-overlay{background-color:#00000080;inset:0;position:fixed;z-index:1001}.uppy-Dashboard-inner{background-color:#f4f4f4;border:1px solid #eaeaea;border-radius:5px;max-height:100%;max-width:100%;outline:none;position:relative}.uppy-size--md .uppy-Dashboard-inner{min-height:auto}@media only screen and (min-width:820px){.uppy-Dashboard-inner{height:500px;width:650px}}.uppy-Dashboard--modal .uppy-Dashboard-inner{z-index:1002}[data-uppy-theme=dark] .uppy-Dashboard-inner{background-color:#1f1f1f}.uppy-Dashboard--isDisabled .uppy-Dashboard-inner{cursor:not-allowed}.uppy-Dashboard-innerWrap{border-radius:5px;display:flex;flex-direction:column;height:100%;opacity:0;overflow:hidden;position:relative}.uppy-Dashboard--isInnerWrapVisible .uppy-Dashboard-innerWrap{opacity:1}.uppy-Dashboard--isDisabled .uppy-Dashboard-innerWrap{cursor:not-allowed;filter:grayscale(100%);opacity:.6;user-select:none}.uppy-Dashboard--isDisabled .uppy-ProviderIconBg{fill:#9f9f9f}.uppy-Dashboard--isDisabled [aria-disabled],.uppy-Dashboard--isDisabled [disabled]{cursor:not-allowed;pointer-events:none}.uppy-Dashboard--modal .uppy-Dashboard-inner{border:none;inset:35px 15px 15px;position:fixed}@media only screen and (min-width:820px){.uppy-Dashboard--modal .uppy-Dashboard-inner{box-shadow:0 5px 15px 4px #00000026;left:50%;right:auto;top:50%;transform:translate(-50%,-50%)}}.uppy-Dashboard-close{color:#ffffffe6;cursor:pointer;display:block;font-size:27px;inset-inline-end:-2px;position:absolute;top:-33px;z-index:1005}.uppy-Dashboard-close:focus{outline:none}.uppy-Dashboard-close::-moz-focus-inner{border:0}.uppy-Dashboard-close:focus{color:#6eabf2}@media only screen and (min-width:820px){.uppy-Dashboard-close{font-size:35px;inset-inline-end:-35px;top:-10px}}.uppy-Dashboard-serviceMsg{background-color:#fffbf7;border-bottom:1px solid #edd4b9;border-top:1px solid #edd4b9;font-size:12px;font-weight:500;line-height:1.3;padding:12px 0;position:relative;top:-1px;z-index:1004}.uppy-size--md .uppy-Dashboard-serviceMsg{font-size:14px;line-height:1.4}[data-uppy-theme=dark] .uppy-Dashboard-serviceMsg{background-color:#1f1f1f;border-bottom:1px solid #333;border-top:1px solid #333;color:#eaeaea}.uppy-Dashboard-serviceMsg-title{display:block;line-height:1;margin-bottom:4px;padding-left:42px}.uppy-Dashboard-serviceMsg-text{padding:0 15px}.uppy-Dashboard-serviceMsg-actionBtn{color:#1269cf;font-size:inherit;font-weight:inherit;vertical-align:initial}[data-uppy-theme=dark] .uppy-Dashboard-serviceMsg-actionBtn{color:#02baf2e6}.uppy-Dashboard-serviceMsg-icon{left:15px;position:absolute;top:10px}.uppy-Dashboard-AddFiles{align-items:center;display:flex;flex-direction:column;height:100%;justify-content:center;position:relative;text-align:center}[data-uppy-drag-drop-supported=true] .uppy-Dashboard-AddFiles{border:1px dashed #dfdfdf;border-radius:3px;height:calc(100% - 14px);margin:7px}.uppy-Dashboard-AddFilesPanel .uppy-Dashboard-AddFiles{border:none;height:calc(100% - 54px)}.uppy-Dashboard--modal .uppy-Dashboard-AddFiles{border-color:#cfcfcf}[data-uppy-theme=dark] .uppy-Dashboard-AddFiles{border-color:#757575}.uppy-Dashboard-AddFiles-info{display:none;margin-top:auto;padding-bottom:15px;padding-top:15px}.uppy-size--height-md .uppy-Dashboard-AddFiles-info{display:block}.uppy-size--md .uppy-Dashboard-AddFiles-info{bottom:25px;left:0;padding-bottom:0;padding-top:30px;position:absolute;right:0}[data-uppy-num-acquirers="0"] .uppy-Dashboard-AddFiles-info{margin-top:0}.uppy-Dashboard-browse{color:#1269cf;cursor:pointer}.uppy-Dashboard-browse:focus{outline:none}.uppy-Dashboard-browse::-moz-focus-inner{border:0}.uppy-Dashboard-browse:focus,.uppy-Dashboard-browse:hover{border-bottom:1px solid #1269cf}[data-uppy-theme=dark] .uppy-Dashboard-browse{color:#02baf2e6}[data-uppy-theme=dark] .uppy-Dashboard-browse:focus,[data-uppy-theme=dark] .uppy-Dashboard-browse:hover{border-bottom:1px solid #02baf2}.uppy-Dashboard-browseBtn{display:block;font-size:14px;font-weight:500;margin-bottom:5px;margin-top:8px;width:100%}.uppy-size--md .uppy-Dashboard-browseBtn{font-size:15px;margin:15px auto;padding:13px 44px;width:auto}.uppy-Dashboard-AddFiles-list{display:flex;flex:1;flex-direction:column;margin-top:2px;overflow-y:auto;padding:2px 0;width:100%;-webkit-overflow-scrolling:touch}.uppy-size--md .uppy-Dashboard-AddFiles-list{flex:none;flex-direction:row;flex-wrap:wrap;justify-content:center;margin-top:15px;max-width:600px;overflow-y:visible;padding-top:0}.uppy-DashboardTab{border-bottom:1px solid #eaeaea;text-align:center;width:100%}[data-uppy-theme=dark] .uppy-DashboardTab{border-bottom:1px solid #333}.uppy-size--md .uppy-DashboardTab{border-bottom:none;display:inline-block;margin-bottom:10px;width:auto}.uppy-DashboardTab-btn{align-items:center;-webkit-appearance:none;appearance:none;background-color:initial;color:#525252;cursor:pointer;flex-direction:row;height:100%;justify-content:left;padding:12px 15px;width:100%}.uppy-DashboardTab-btn:focus{outline:none}.uppy-size--md .uppy-DashboardTab-btn{border-radius:5px;flex-direction:column;margin-inline-end:1px;padding:10px 3px;width:86px}[data-uppy-theme=dark] .uppy-DashboardTab-btn{color:#eaeaea}.uppy-DashboardTab-btn::-moz-focus-inner{border:0}.uppy-DashboardTab-btn:hover{background-color:#e9ecef}[data-uppy-theme=dark] .uppy-DashboardTab-btn:hover{background-color:#333}.uppy-DashboardTab-btn:active,.uppy-DashboardTab-btn:focus{background-color:#dfe6f1}[data-uppy-theme=dark] .uppy-DashboardTab-btn:active,[data-uppy-theme=dark] .uppy-DashboardTab-btn:focus{background-color:#525252}.uppy-DashboardTab-btn svg{display:inline-block;max-height:100%;max-width:100%;overflow:hidden;transition:transform .15s ease-in-out;vertical-align:text-top}.uppy-DashboardTab-inner{align-items:center;background-color:#fff;border-radius:8px;box-shadow:0 1px 1px #0000001a,0 1px 2px #0000001a,0 2px 3px #00000005;display:flex;height:32px;justify-content:center;margin-inline-end:10px;width:32px}.uppy-size--md .uppy-DashboardTab-inner{margin-inline-end:0}[data-uppy-theme=dark] .uppy-DashboardTab-inner{background-color:#323232;box-shadow:0 1px 1px #0003,0 1px 2px #0003,0 2px 3px #00000014}.uppy-DashboardTab-name{font-size:14px;font-weight:400}.uppy-size--md .uppy-DashboardTab-name{font-size:12px;line-height:15px;margin-bottom:0;margin-top:8px}.uppy-DashboardTab-iconMyDevice{color:#1269cf}[data-uppy-theme=dark] .uppy-DashboardTab-iconMyDevice{color:#02baf2}.uppy-DashboardTab-iconBox{color:#0061d5}[data-uppy-theme=dark] .uppy-DashboardTab-iconBox{color:#eaeaea}.uppy-DashboardTab-iconDropbox{color:#0061fe}[data-uppy-theme=dark] .uppy-DashboardTab-iconDropbox{color:#eaeaea}.uppy-DashboardTab-iconUnsplash{color:#111}[data-uppy-theme=dark] .uppy-DashboardTab-iconUnsplash{color:#eaeaea}.uppy-DashboardTab-iconWebdav{color:#111}[data-uppy-theme=dark] .uppy-DashboardTab-iconWebdav{color:#eaeaea}.uppy-DashboardTab-iconScreenRec{color:#2c3e50}[data-uppy-theme=dark] .uppy-DashboardTab-iconScreenRec{color:#eaeaea}.uppy-DashboardTab-iconAudio{color:#8030a3}[data-uppy-theme=dark] .uppy-DashboardTab-iconAudio{color:#bf6ee3}.uppy-Dashboard-input{height:.1px;opacity:0;overflow:hidden;position:absolute;width:.1px;z-index:-1}.uppy-DashboardContent-bar{align-items:center;background-color:#fafafa;border-bottom:1px solid #eaeaea;display:flex;flex-shrink:0;height:40px;justify-content:space-between;padding:0 10px;position:relative;width:100%;z-index:1004}.uppy-size--md .uppy-DashboardContent-bar{height:50px;padding:0 15px}[data-uppy-theme=dark] .uppy-DashboardContent-bar{background-color:#1f1f1f;border-bottom:1px solid #333}.uppy-DashboardContent-title{font-size:12px;font-weight:500;left:0;line-height:40px;margin:auto;max-width:170px;overflow-x:hidden;position:absolute;right:0;text-align:center;text-overflow:ellipsis;top:0;white-space:nowrap;width:100%}.uppy-size--md .uppy-DashboardContent-title{font-size:14px;line-height:50px;max-width:300px}[data-uppy-theme=dark] .uppy-DashboardContent-title{color:#eaeaea}.uppy-DashboardContent-back,.uppy-DashboardContent-save{-webkit-appearance:none;background:none;border:0;border-radius:3px;color:inherit;color:#1269cf;cursor:pointer;font-family:inherit;font-size:inherit;font-size:12px;font-weight:400;line-height:1;margin:0;margin-inline-start:-6px;padding:7px 6px}.uppy-DashboardContent-back:focus,.uppy-DashboardContent-save:focus{outline:none}.uppy-DashboardContent-back::-moz-focus-inner,.uppy-DashboardContent-save::-moz-focus-inner{border:0}.uppy-DashboardContent-back:hover,.uppy-DashboardContent-save:hover{color:#0e51a0}.uppy-DashboardContent-back:focus,.uppy-DashboardContent-save:focus{background-color:#dfe6f1}[data-uppy-theme=dark] .uppy-DashboardContent-back:focus,[data-uppy-theme=dark] .uppy-DashboardContent-save:focus{background-color:#333}.uppy-size--md .uppy-DashboardContent-back,.uppy-size--md .uppy-DashboardContent-save{font-size:14px}[data-uppy-theme=dark] .uppy-DashboardContent-back,[data-uppy-theme=dark] .uppy-DashboardContent-save{color:#02baf2}.uppy-DashboardContent-addMore{-webkit-appearance:none;background:none;border:0;border-radius:3px;color:inherit;color:#1269cf;cursor:pointer;font-family:inherit;font-size:inherit;font-weight:500;height:29px;line-height:1;margin:0;margin-inline-end:-5px;padding:7px 8px;width:29px}.uppy-DashboardContent-addMore:focus{outline:none}.uppy-DashboardContent-addMore::-moz-focus-inner{border:0}.uppy-DashboardContent-addMore:hover{color:#0e51a0}.uppy-DashboardContent-addMore:focus{background-color:#dfe6f1}[data-uppy-theme=dark] .uppy-DashboardContent-addMore:focus{background-color:#333}.uppy-size--md .uppy-DashboardContent-addMore{font-size:14px;height:auto;margin-inline-end:-8px;width:auto}[data-uppy-theme=dark] .uppy-DashboardContent-addMore{color:#02baf2}.uppy-DashboardContent-addMore svg{margin-inline-end:4px;vertical-align:initial}.uppy-size--md .uppy-DashboardContent-addMore svg{height:11px;width:11px}.uppy-DashboardContent-addMoreCaption{display:none}.uppy-size--md .uppy-DashboardContent-addMoreCaption{display:inline}.uppy-DashboardContent-panel{background-color:#f5f5f5;flex:1}.uppy-Dashboard-AddFilesPanel,.uppy-DashboardContent-panel{border-radius:5px;display:flex;flex-direction:column;inset:0;overflow:hidden;position:absolute;z-index:1005}.uppy-Dashboard-AddFilesPanel{background:#fafafa;background:linear-gradient(0deg,#fafafa 35%,#fafafad9);box-shadow:0 0 10px 5px #00000026}[data-uppy-theme=dark] .uppy-Dashboard-AddFilesPanel{background-color:#333;background-image:linear-gradient(0deg,#1f1f1f 35%,#1f1f1fd9)}.uppy-Dashboard--isAddFilesPanelVisible .uppy-Dashboard-files{filter:blur(2px)}.uppy-Dashboard-progress{bottom:0;height:12%;left:0;position:absolute;width:100%}.uppy-Dashboard-progressBarContainer.is-active{height:100%;left:0;position:absolute;top:0;width:100%;z-index:1004}.uppy-Dashboard-filesContainer{flex:1;margin:0;overflow-y:hidden;position:relative}.uppy-Dashboard-filesContainer:after{clear:both;content:"";display:table}.uppy-Dashboard-files{flex:1;margin:0;overflow-y:auto;padding:0 0 10px;-webkit-overflow-scrolling:touch}.uppy-size--md .uppy-Dashboard-files{padding-top:10px}.uppy-Dashboard--singleFile .uppy-Dashboard-filesInner{align-items:center;display:flex;height:100%;justify-content:center}.uppy-Dashboard-dropFilesHereHint{align-items:center;background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='48' height='48'%3E%3Cpath fill='%231269CF' d='M24 1v1C11.85 2 2 11.85 2 24s9.85 22 22 22 22-9.85 22-22S36.15 2 24 2zm0 0V0c13.254 0 24 10.746 24 24S37.254 48 24 48 0 37.254 0 24 10.746 0 24 0zm7.707 19.293a.999.999 0 1 1-1.414 1.414L25 16.414V34a1 1 0 1 1-2 0V16.414l-5.293 5.293a.999.999 0 1 1-1.414-1.414l7-7a1 1 0 0 1 1.414 0z'/%3E%3C/svg%3E");background-position:50% 50%;background-repeat:no-repeat;border:1px dashed #1269cf;border-radius:3px;color:#757575;display:flex;font-size:16px;justify-content:center;inset:7px;padding-top:90px;position:absolute;text-align:center;visibility:hidden;z-index:2000}[data-uppy-theme=dark] .uppy-Dashboard-dropFilesHereHint{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='48' height='48'%3E%3Cpath fill='%2302BAF2' d='M24 1v1C11.85 2 2 11.85 2 24s9.85 22 22 22 22-9.85 22-22S36.15 2 24 2zm0 0V0c13.254 0 24 10.746 24 24S37.254 48 24 48 0 37.254 0 24 10.746 0 24 0zm7.707 19.293a.999.999 0 1 1-1.414 1.414L25 16.414V34a1 1 0 1 1-2 0V16.414l-5.293 5.293a.999.999 0 1 1-1.414-1.414l7-7a1 1 0 0 1 1.414 0z'/%3E%3C/svg%3E");border-color:#02baf2;color:#bbb}.uppy-Dashboard.uppy-Dashboard--isDraggingOver .uppy-Dashboard-dropFilesHereHint{pointer-events:none;visibility:visible}.uppy-Dashboard.uppy-Dashboard--isDraggingOver .uppy-Dashboard-files,.uppy-Dashboard.uppy-Dashboard--isDraggingOver .uppy-Dashboard-progressindicators,.uppy-Dashboard.uppy-Dashboard--isDraggingOver .uppy-Dashboard-serviceMsg,.uppy-Dashboard.uppy-Dashboard--isDraggingOver .uppy-DashboardContent-bar{opacity:.15}.uppy-Dashboard.uppy-Dashboard--isDraggingOver .uppy-Dashboard-AddFiles{opacity:.03}.uppy-Dashboard-AddFiles-title{color:#000;font-size:17px;font-weight:500;line-height:1.35;margin-bottom:5px;margin-top:15px;padding:0 15px;text-align:inline-start;width:100%}.uppy-size--md .uppy-Dashboard-AddFiles-title{font-size:21px;font-weight:400;margin-top:5px;max-width:480px;padding:0 35px;text-align:center}[data-uppy-num-acquirers="0"] .uppy-Dashboard-AddFiles-title{text-align:center}[data-uppy-theme=dark] .uppy-Dashboard-AddFiles-title{color:#eaeaea}.uppy-Dashboard-AddFiles-title button{font-weight:500}.uppy-size--md .uppy-Dashboard-AddFiles-title button{font-weight:400}.uppy-Dashboard-note{color:#757575;font-size:14px;line-height:1.25;margin:auto;max-width:350px;padding:0 15px;text-align:center}.uppy-size--md .uppy-Dashboard-note{line-height:1.35;max-width:600px}[data-uppy-theme=dark] .uppy-Dashboard-note{color:#cfcfcf}a.uppy-Dashboard-poweredBy{color:#939393;display:inline-block;font-size:11px;margin-top:8px;text-align:center;text-decoration:none}.uppy-Dashboard-poweredByIcon{margin-left:1px;margin-right:1px;opacity:.9;position:relative;top:1px;vertical-align:text-top;fill:none;stroke:#939393}.uppy-Dashboard-Item-previewIcon{height:25px;left:50%;position:absolute;top:50%;transform:translate(-50%,-50%);width:25px;z-index:100}.uppy-size--md .uppy-Dashboard-Item-previewIcon{height:38px;width:38px}.uppy-Dashboard-Item-previewIcon svg{height:100%;width:100%}.uppy-Dashboard--singleFile .uppy-Dashboard-Item-previewIcon{height:100%;max-height:60%;max-width:60%;width:100%}.uppy-Dashboard-Item-previewIconWrap{height:76px;max-height:75%;position:relative}.uppy-Dashboard--singleFile .uppy-Dashboard-Item-previewIconWrap{height:100%;width:100%}.uppy-Dashboard-Item-previewIconBg{filter:drop-shadow(rgba(0,0,0,.1) 0 1px 1px);height:100%;width:100%}.uppy-Dashboard-upload{height:50px;position:relative;width:50px}.uppy-size--md .uppy-Dashboard-upload{height:60px;width:60px}.uppy-Dashboard-upload .uppy-c-icon{position:relative;top:1px;width:50%}.uppy-Dashboard-uploadCount{background-color:#1bb240;border-radius:50%;color:#fff;font-size:8px;height:16px;inset-inline-end:-12px;line-height:16px;position:absolute;top:-12px;width:16px}.uppy-size--md .uppy-Dashboard-uploadCount{font-size:9px;height:18px;line-height:18px;width:18px}:root{color-scheme:light dark;font-family:Inter,ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,Segoe UI,sans-serif;background:#08101f;color:#e8eefc}*{box-sizing:border-box}body{min-height:100vh;margin:0;background:radial-gradient(circle at top left,#1b3261 0,transparent 38rem),#08101f}.shell{width:min(48rem,calc(100% - 2rem));margin:0 auto;padding:3rem 0 5rem}header{margin-bottom:2rem}.eyebrow,.label{color:#96b6ff;font-size:.78rem;font-weight:700;letter-spacing:.08em;text-transform:uppercase}h1,h2,p{margin-top:0}h1{margin-bottom:.65rem;font-size:clamp(2rem,8vw,3.6rem);line-height:1}h2{font-size:1.15rem}.lede,.hint,#next-action{color:#b6c4df;line-height:1.55}.card{margin-top:1rem;padding:1.4rem;border:1px solid #2c3d60;border-radius:1rem;background:#101a2dbf;box-shadow:0 1rem 3rem #0004}.facts{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:.85rem 1.25rem;margin:1rem 0}.facts div{min-width:0}.facts dt{margin-bottom:.2rem;color:#8fa0bd;font-size:.8rem}.facts dd{margin:0;overflow-wrap:anywhere;font-weight:650}.compact{grid-template-columns:1fr}#authentication-form label{display:block;margin-bottom:.45rem;color:#b6c4df;font-weight:650}.authentication-controls{display:flex;gap:.65rem}.authentication-controls input{min-width:0;flex:1;padding:.7rem .8rem;border:1px solid #49618d;border-radius:.55rem;background:#08101f;color:#e8eefc}.authentication-controls button{padding:.7rem 1rem;border:0;border-radius:.55rem;background:#78a2ff;color:#08101f;font-weight:750;cursor:pointer}#uppy-dashboard{margin-top:1rem}.state{min-height:1.5rem;margin:.8rem 0}.state.error{color:#ffaaa2}.state.success{color:#91e6bc}noscript{display:block;margin-top:1rem;color:#ffaaa2}@media(max-width:36rem){.shell{padding-top:1.5rem}.facts{grid-template-columns:1fr}.authentication-controls{align-items:stretch;flex-direction:column}}@media(prefers-reduced-motion:reduce){*,*:before,*:after{scroll-behavior:auto!important}} +@charset "UTF-8";.uppy-Root{box-sizing:border-box;color:#333;font-family:-apple-system,system-ui,BlinkMacSystemFont,Segoe UI,Segoe UI Symbol,Segoe UI Emoji,Apple Color Emoji,Roboto,Helvetica,Arial,sans-serif;line-height:1;position:relative;text-align:left;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.uppy-Root[dir=rtl],[dir=rtl] .uppy-Root{text-align:right}.uppy-Root *,.uppy-Root :after,.uppy-Root :before{box-sizing:inherit}.uppy-Root [hidden]{display:none}.uppy-u-reset{all:initial;appearance:none;box-sizing:border-box;font-family:-apple-system,system-ui,BlinkMacSystemFont,Segoe UI,Segoe UI Symbol,Segoe UI Emoji,Apple Color Emoji,Roboto,Helvetica,Arial,sans-serif;line-height:1}[dir=rtl] .uppy-u-reset{text-align:right}.uppy-truncate-text{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.uppy-c-textInput{background-color:#fff;border:1px solid #ddd;border-radius:4px;font-family:inherit;font-size:14px;line-height:1.5;padding:6px 8px}.uppy-size--md .uppy-c-textInput{padding:8px 10px}.uppy-c-textInput:focus{border-color:#1269cf99;box-shadow:0 0 0 3px #1269cf26;outline:none}[data-uppy-theme=dark] .uppy-c-textInput{background-color:#333;border-color:#333;color:#eaeaea}[data-uppy-theme=dark] .uppy-c-textInput:focus{border-color:#525252;box-shadow:none}.uppy-c-icon{display:inline-block;max-height:100%;max-width:100%;overflow:hidden;fill:currentColor}.uppy-c-btn{align-items:center;color:inherit;display:inline-flex;font-family:inherit;font-size:inherit;font-weight:500;justify-content:center;line-height:1;transition-duration:.3s;transition-property:background-color,color;user-select:none;white-space:nowrap}.uppy-c-btn,[dir=rtl] .uppy-c-btn{text-align:center}.uppy-c-btn:not(:disabled):not(.disabled){cursor:pointer}.uppy-c-btn::-moz-focus-inner{border:0}.uppy-c-btn-primary{background-color:#1269cf;border-radius:4px;color:#fff;font-size:14px;padding:10px 18px}.uppy-c-btn-primary:not(:disabled):hover{background-color:#0e51a0}.uppy-c-btn-primary:focus{box-shadow:0 0 0 3px #1269cf66;outline:none}.uppy-size--md .uppy-c-btn-primary{padding:13px 22px}[data-uppy-theme=dark] .uppy-c-btn-primary{color:#eaeaea}[data-uppy-theme=dark] .uppy-c-btn-primary:focus{outline:none}[data-uppy-theme=dark] .uppy-c-btn-primary::-moz-focus-inner{border:0}[data-uppy-theme=dark] .uppy-c-btn-primary:focus{box-shadow:0 0 0 2px #aae1ffd9}.uppy-c-btn-primary.uppy-c-btn--disabled{background-color:#8eb2db}.uppy-c-btn-link{background-color:initial;border-radius:4px;color:#525252;font-size:14px;line-height:1;padding:10px 15px}.uppy-c-btn-link:hover{color:#333}.uppy-c-btn-link:focus{box-shadow:0 0 0 3px #1269cf40;outline:none}.uppy-size--md .uppy-c-btn-link{padding:13px 18px}[data-uppy-theme=dark] .uppy-c-btn-link{color:#eaeaea}[data-uppy-theme=dark] .uppy-c-btn-link:focus{outline:none}[data-uppy-theme=dark] .uppy-c-btn-link::-moz-focus-inner{border:0}[data-uppy-theme=dark] .uppy-c-btn-link:focus{box-shadow:0 0 0 2px #aae1ffd9}[data-uppy-theme=dark] .uppy-c-btn-link:hover{color:#939393}.uppy-ProviderBrowser-viewType--grid ul.uppy-ProviderBrowser-list,.uppy-ProviderBrowser-viewType--unsplash ul.uppy-ProviderBrowser-list{align-items:flex-start;display:flex;flex-direction:row;flex-wrap:wrap;justify-content:space-between;padding:6px}.uppy-ProviderBrowser-viewType--grid ul.uppy-ProviderBrowser-list:after,.uppy-ProviderBrowser-viewType--unsplash ul.uppy-ProviderBrowser-list:after{content:"";flex:auto}.uppy-ProviderBrowser-viewType--grid li.uppy-ProviderBrowserItem,.uppy-ProviderBrowser-viewType--unsplash li.uppy-ProviderBrowserItem{margin:0;position:relative;width:50%}.uppy-size--md .uppy-ProviderBrowser-viewType--grid li.uppy-ProviderBrowserItem,.uppy-size--md .uppy-ProviderBrowser-viewType--unsplash li.uppy-ProviderBrowserItem{width:33.3333%}.uppy-size--lg .uppy-ProviderBrowser-viewType--grid li.uppy-ProviderBrowserItem,.uppy-size--lg .uppy-ProviderBrowser-viewType--unsplash li.uppy-ProviderBrowserItem{width:25%}.uppy-ProviderBrowser-viewType--grid li.uppy-ProviderBrowserItem:before,.uppy-ProviderBrowser-viewType--unsplash li.uppy-ProviderBrowserItem:before{content:"";display:block;padding-top:100%}.uppy-ProviderBrowser-viewType--grid li.uppy-ProviderBrowserItem--selected img,.uppy-ProviderBrowser-viewType--grid li.uppy-ProviderBrowserItem--selected svg,.uppy-ProviderBrowser-viewType--unsplash li.uppy-ProviderBrowserItem--selected img,.uppy-ProviderBrowser-viewType--unsplash li.uppy-ProviderBrowserItem--selected svg{opacity:.85}.uppy-ProviderBrowser-viewType--grid li.uppy-ProviderBrowserItem--disabled,.uppy-ProviderBrowser-viewType--unsplash li.uppy-ProviderBrowserItem--disabled{opacity:.5}.uppy-ProviderBrowser-viewType--grid li.uppy-ProviderBrowserItem--noPreview .uppy-ProviderBrowserItem-inner,.uppy-ProviderBrowser-viewType--unsplash li.uppy-ProviderBrowserItem--noPreview .uppy-ProviderBrowserItem-inner{background-color:#93939333}[data-uppy-theme=dark] .uppy-ProviderBrowser-viewType--grid li.uppy-ProviderBrowserItem--noPreview .uppy-ProviderBrowserItem-inner,[data-uppy-theme=dark] .uppy-ProviderBrowser-viewType--unsplash li.uppy-ProviderBrowserItem--noPreview .uppy-ProviderBrowserItem-inner{background-color:#eaeaea33}.uppy-ProviderBrowser-viewType--grid li.uppy-ProviderBrowserItem--noPreview svg,.uppy-ProviderBrowser-viewType--unsplash li.uppy-ProviderBrowserItem--noPreview svg{height:30%;width:30%;fill:#000000b3}[data-uppy-theme=dark] .uppy-ProviderBrowser-viewType--grid li.uppy-ProviderBrowserItem--noPreview svg,[data-uppy-theme=dark] .uppy-ProviderBrowser-viewType--unsplash li.uppy-ProviderBrowserItem--noPreview svg{fill:#fffc}.uppy-ProviderBrowser-viewType--grid .uppy-ProviderBrowserItem-inner,.uppy-ProviderBrowser-viewType--unsplash .uppy-ProviderBrowserItem-inner{border-radius:4px;height:calc(100% - 14px);inset:7px;overflow:hidden;position:absolute;text-align:center;width:calc(100% - 14px)}@media(hover:none){.uppy-ProviderBrowser-viewType--grid .uppy-ProviderBrowserItem-inner .uppy-ProviderBrowserItem-author,.uppy-ProviderBrowser-viewType--unsplash .uppy-ProviderBrowserItem-inner .uppy-ProviderBrowserItem-author{display:block}}[data-uppy-theme=dark] .uppy-ProviderBrowser-viewType--grid .uppy-ProviderBrowserItem-inner,[data-uppy-theme=dark] .uppy-ProviderBrowser-viewType--unsplash .uppy-ProviderBrowserItem-inner{box-shadow:0 0 0 3px #aae1ffb3}.uppy-ProviderBrowser-viewType--grid .uppy-ProviderBrowserItem-inner img,.uppy-ProviderBrowser-viewType--unsplash .uppy-ProviderBrowserItem-inner img{border-radius:4px;height:100%;object-fit:cover;width:100%}.uppy-ProviderBrowser-viewType--grid .uppy-ProviderBrowserItem-author,.uppy-ProviderBrowser-viewType--unsplash .uppy-ProviderBrowserItem-author{background:#0000004d;bottom:0;color:#fff;display:none;font-size:12px;font-weight:500;left:0;margin:0;padding:5px;position:absolute;text-decoration:none;width:100%}.uppy-ProviderBrowser-viewType--grid .uppy-ProviderBrowserItem-author:hover,.uppy-ProviderBrowser-viewType--unsplash .uppy-ProviderBrowserItem-author:hover{background:#0006;text-decoration:underline}.uppy-ProviderBrowser-viewType--grid .uppy-ProviderBrowserItem-checkbox,.uppy-ProviderBrowser-viewType--unsplash .uppy-ProviderBrowserItem-checkbox{background-color:#1269cf;border-radius:50%;height:26px;opacity:0;position:absolute;right:16px;top:16px;width:26px;z-index:1002}.uppy-ProviderBrowser-viewType--grid .uppy-ProviderBrowserItem-checkbox:after,.uppy-ProviderBrowser-viewType--unsplash .uppy-ProviderBrowserItem-checkbox:after{height:7px;inset-inline-start:7px;top:8px;width:12px}.uppy-ProviderBrowser-viewType--grid .uppy-ProviderBrowserItem--is-checked .uppy-ProviderBrowserItem-checkbox,.uppy-ProviderBrowser-viewType--unsplash .uppy-ProviderBrowserItem--is-checked .uppy-ProviderBrowserItem-checkbox{opacity:1}.uppy-ProviderBrowser-viewType--grid .uppy-ProviderBrowserItem-checkbox--grid:focus+label .uppy-ProviderBrowserItem-author,.uppy-ProviderBrowser-viewType--grid .uppy-ProviderBrowserItem-checkbox--grid:hover+label .uppy-ProviderBrowserItem-author,.uppy-ProviderBrowser-viewType--unsplash .uppy-ProviderBrowserItem-checkbox--grid:focus+label .uppy-ProviderBrowserItem-author,.uppy-ProviderBrowser-viewType--unsplash .uppy-ProviderBrowserItem-checkbox--grid:hover+label .uppy-ProviderBrowserItem-author{display:block}.uppy-ProviderBrowser-viewType--grid .uppy-ProviderBrowserItem-checkbox--grid:focus+label,.uppy-ProviderBrowser-viewType--unsplash .uppy-ProviderBrowserItem-checkbox--grid:focus+label{box-shadow:0 0 0 3px #1269cf80}.uppy-ProviderBrowser-viewType--grid .uppy-ProviderBrowserItem-checkbox--grid:focus+label:focus,.uppy-ProviderBrowser-viewType--unsplash .uppy-ProviderBrowserItem-checkbox--grid:focus+label:focus{outline:none}.uppy-ProviderBrowser-viewType--grid .uppy-ProviderBrowserItem-checkbox--grid:focus+label::-moz-focus-inner,.uppy-ProviderBrowser-viewType--unsplash .uppy-ProviderBrowserItem-checkbox--grid:focus+label::-moz-focus-inner{border:0}.uppy-ProviderBrowser-viewType--list{background-color:#fff}[data-uppy-theme=dark] .uppy-ProviderBrowser-viewType--list{background-color:#1f1f1f}.uppy-ProviderBrowser-viewType--list li.uppy-ProviderBrowserItem{align-items:center;display:flex;margin:0;padding:7px 15px}[data-uppy-theme=dark] .uppy-ProviderBrowser-viewType--list li.uppy-ProviderBrowserItem{color:#eaeaea}.uppy-ProviderBrowser-viewType--list li.uppy-ProviderBrowserItem--disabled{opacity:.6}.uppy-ProviderBrowser-viewType--list .uppy-ProviderBrowserItem-checkbox{background-color:#fff;border:1px solid #cfcfcf;border-radius:3px;height:17px;margin-inline-end:15px;width:17px}.uppy-ProviderBrowser-viewType--list .uppy-ProviderBrowserItem-checkbox:focus{border:1px solid #1269cf;box-shadow:0 0 0 3px #1269cf40;outline:none}.uppy-ProviderBrowser-viewType--list .uppy-ProviderBrowserItem-checkbox:after{height:5px;inset-inline-start:3px;opacity:0;top:4px;width:9px}[data-uppy-theme=dark] .uppy-ProviderBrowser-viewType--list .uppy-ProviderBrowserItem-checkbox:focus{border-color:#02baf2b3;box-shadow:0 0 0 3px #02baf233}.uppy-ProviderBrowser-viewType--list .uppy-ProviderBrowserItem--is-checked .uppy-ProviderBrowserItem-checkbox,.uppy-ProviderBrowser-viewType--list .uppy-ProviderBrowserItem--is-partial .uppy-ProviderBrowserItem-checkbox{background-color:#1269cf;border-color:#1269cf}.uppy-ProviderBrowser-viewType--list .uppy-ProviderBrowserItem--is-checked .uppy-ProviderBrowserItem-checkbox:after,.uppy-ProviderBrowser-viewType--list .uppy-ProviderBrowserItem--is-partial .uppy-ProviderBrowserItem-checkbox:after{opacity:1}.uppy-ProviderBrowser-viewType--list .uppy-ProviderBrowserItem-inner{align-items:center;color:inherit;display:flex;font-family:-apple-system,system-ui,BlinkMacSystemFont,Segoe UI,Segoe UI Symbol,Segoe UI Emoji,Apple Color Emoji,Roboto,Helvetica,Arial,sans-serif;overflow:hidden;padding:2px;text-overflow:ellipsis;white-space:nowrap}.uppy-ProviderBrowser-viewType--list .uppy-ProviderBrowserItem-inner:focus{outline:none;text-decoration:underline}.uppy-ProviderBrowser-viewType--list .uppy-ProviderBrowserItem-inner img,.uppy-ProviderBrowser-viewType--list .uppy-ProviderBrowserItem-inner svg{margin-inline-end:8px}.uppy-ProviderBrowser-viewType--list .uppy-ProviderBrowserItem-inner span{line-height:1.2;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.uppy-ProviderBrowser-viewType--list .uppy-ProviderBrowserItem--disabled .uppy-ProviderBrowserItem-inner{cursor:default}.uppy-ProviderBrowser-viewType--list .uppy-ProviderBrowserItem-iconWrap{margin-inline-end:7px;width:20px}.uppy-ProviderBrowserItem-checkbox{cursor:pointer;flex-shrink:0;position:relative}.uppy-ProviderBrowserItem-checkbox:disabled,.uppy-ProviderBrowserItem-checkbox:disabled:after{cursor:default}[data-uppy-theme=dark] .uppy-ProviderBrowserItem-checkbox{background-color:#1f1f1f;border-color:#939393}[data-uppy-theme=dark] .uppy-ProviderBrowserItem--is-checked .uppy-ProviderBrowserItem-checkbox{background-color:#333}.uppy-ProviderBrowserItem--is-checked .uppy-ProviderBrowserItem-checkbox:after{border-bottom:2px solid #eaeaea;border-left:2px solid #eaeaea;content:"";cursor:pointer;position:absolute;transform:rotate(-45deg)}.uppy-ProviderBrowserItem--is-partial .uppy-ProviderBrowserItem-checkbox:after{background-color:#eaeaea!important;content:""!important;height:2px!important;left:20%!important;position:absolute!important;right:20%!important;top:50%!important;transform:translateY(-50%)!important}.uppy-SearchProvider{align-items:center;display:flex;flex:1;flex-direction:column;height:100%;justify-content:center;width:100%}[data-uppy-theme=dark] .uppy-SearchProvider{background-color:#1f1f1f}.uppy-SearchProvider-input{margin-bottom:15px;max-width:650px;width:90%}.uppy-size--md .uppy-SearchProvider-input{margin-bottom:20px}.uppy-SearchProvider-input::-webkit-search-cancel-button{display:none}.uppy-SearchProvider-searchButton{padding:13px 25px}.uppy-size--md .uppy-SearchProvider-searchButton{padding:13px 30px}.uppy-DashboardContent-panelBody{align-items:center;display:flex;flex:1;justify-content:center}[data-uppy-theme=dark] .uppy-DashboardContent-panelBody{background-color:#1f1f1f}.uppy-Provider-auth,.uppy-Provider-empty,.uppy-Provider-error,.uppy-Provider-loading{align-items:center;color:#939393;display:flex;flex:1;flex-flow:column wrap;justify-content:center}.uppy-Provider-empty{color:#939393}.uppy-Provider-authIcon svg{height:75px;width:100px}.uppy-Provider-authTitle{color:#757575;font-size:17px;font-weight:400;line-height:1.4;margin-bottom:30px;max-width:500px;padding:0 15px;text-align:center}.uppy-size--md .uppy-Provider-authTitle{font-size:20px}[data-uppy-theme=dark] .uppy-Provider-authTitle{color:#cfcfcf}.uppy-Provider-btn-google{align-items:center;background:#4285f4;display:flex;padding:8px 12px!important}.uppy-Provider-btn-google:hover{background-color:#1266f1}.uppy-Provider-btn-google:focus{box-shadow:0 0 0 3px #4285f466;outline:none}.uppy-Provider-btn-google svg{margin-right:8px}.uppy-Provider-breadcrumbs{color:#525252;flex:1;font-size:12px;margin-bottom:10px;text-align:start}.uppy-size--md .uppy-Provider-breadcrumbs{margin-bottom:0}[data-uppy-theme=dark] .uppy-Provider-breadcrumbs{color:#eaeaea}.uppy-Provider-breadcrumbsIcon{color:#525252;display:inline-block;line-height:1;margin-inline-end:4px;vertical-align:middle}.uppy-Provider-breadcrumbsIcon svg{height:13px;width:13px;fill:#525252}.uppy-Provider-breadcrumbs button{border-radius:3px;display:inline-block;line-height:inherit;padding:4px}.uppy-Provider-breadcrumbs button:focus{outline:none}.uppy-Provider-breadcrumbs button::-moz-focus-inner{border:0}.uppy-Provider-breadcrumbs button:hover{color:#0e51a0}.uppy-Provider-breadcrumbs button:focus{background-color:#dfe6f1}[data-uppy-theme=dark] .uppy-Provider-breadcrumbs button:focus{background-color:#333}.uppy-Provider-breadcrumbs button:not(:last-of-type){text-decoration:underline}.uppy-Provider-breadcrumbs button:last-of-type{color:#333;cursor:normal;font-weight:500;pointer-events:none}.uppy-Provider-breadcrumbs button:hover{cursor:pointer}[data-uppy-theme=dark] .uppy-Provider-breadcrumbs button{color:#eaeaea}.uppy-ProviderBrowser{display:flex;flex:1;flex-direction:column;font-size:14px;font-weight:400;height:100%}.uppy-ProviderBrowser-user{color:#333;font-weight:500;margin:0 8px 0 0}[data-uppy-theme=dark] .uppy-ProviderBrowser-user{color:#eaeaea}.uppy-ProviderBrowser-user:after{color:#939393;content:"·";font-weight:400;inset-inline-start:4px;position:relative}.uppy-ProviderBrowser-header{border-bottom:1px solid #eaeaea;position:relative;z-index:1001}[data-uppy-theme=dark] .uppy-ProviderBrowser-header{border-bottom:1px solid #333}.uppy-ProviderBrowser-headerBar{background-color:#fafafa;color:#757575;font-size:12px;line-height:1.4;padding:7px 15px;z-index:1001}.uppy-size--md .uppy-ProviderBrowser-headerBar{align-items:center;display:flex}[data-uppy-theme=dark] .uppy-ProviderBrowser-headerBar{background-color:#1f1f1f}.uppy-ProviderBrowser-headerBar--simple{display:block;justify-content:center;text-align:center}.uppy-ProviderBrowser-headerBar--simple .uppy-Provider-breadcrumbsWrap{display:inline-block;flex:none;vertical-align:middle}.uppy-ProviderBrowser-searchFilter{align-items:center;display:flex;height:30px;margin-bottom:15px;margin-top:15px;padding-left:8px;padding-right:8px;position:relative;width:100%}.uppy-ProviderBrowser-searchFilterInput{background-color:#eaeaea;border:0;border-radius:4px;color:#333;font-family:-apple-system,system-ui,BlinkMacSystemFont,Segoe UI,Segoe UI Symbol,Segoe UI Emoji,Apple Color Emoji,Roboto,Helvetica,Arial,sans-serif;font-size:13px;height:30px;line-height:1.4;outline:0;padding-inline-end:30px;padding-inline-start:30px;width:100%;z-index:1001}.uppy-ProviderBrowser-searchFilterInput::-webkit-search-cancel-button{display:none}[data-uppy-theme=dark] .uppy-ProviderBrowser-searchFilterInput{background-color:#1f1f1f;color:#eaeaea}.uppy-ProviderBrowser-searchFilterInput:focus{background-color:#cfcfcf;border:0}[data-uppy-theme=dark] .uppy-ProviderBrowser-searchFilterInput:focus{background-color:#333}.uppy-ProviderBrowser-searchFilterIcon{color:#757575;height:12px;inset-inline-start:16px;position:absolute;width:12px;z-index:1002}.uppy-ProviderBrowser-searchFilterInput::placeholder{color:#939393;opacity:1}.uppy-ProviderBrowser-searchFilterReset{border-radius:3px;color:#939393;cursor:pointer;height:22px;inset-inline-end:16px;padding:6px;position:absolute;width:22px;z-index:1002}.uppy-ProviderBrowser-searchFilterReset:focus{outline:none}.uppy-ProviderBrowser-searchFilterReset::-moz-focus-inner{border:0}.uppy-ProviderBrowser-searchFilterReset:focus{box-shadow:0 0 0 3px #1269cf80}.uppy-ProviderBrowser-searchFilterReset:hover{color:#757575}.uppy-ProviderBrowser-searchFilterReset svg{vertical-align:text-top}.uppy-ProviderBrowser-userLogout{border-radius:3px;color:#1269cf;cursor:pointer;line-height:inherit;padding:4px}.uppy-ProviderBrowser-userLogout:focus{outline:none}.uppy-ProviderBrowser-userLogout::-moz-focus-inner{border:0}.uppy-ProviderBrowser-userLogout:hover{color:#0e51a0}.uppy-ProviderBrowser-userLogout:focus{background-color:#dfe6f1}[data-uppy-theme=dark] .uppy-ProviderBrowser-userLogout:focus{background-color:#333}.uppy-ProviderBrowser-userLogout:hover{text-decoration:underline}[data-uppy-theme=dark] .uppy-ProviderBrowser-userLogout{color:#eaeaea}.uppy-ProviderBrowser-body{flex:1;position:relative}.uppy-ProviderBrowser-list{background-color:#fff;border-spacing:0;display:block;flex:1;height:100%;inset:0;list-style:none;margin:0;overflow-x:hidden;overflow-y:auto;padding:0;position:absolute;width:100%;-webkit-overflow-scrolling:touch}[data-uppy-theme=dark] .uppy-ProviderBrowser-list{background-color:#1f1f1f}.uppy-ProviderBrowser-list:focus{outline:none}.uppy-ProviderBrowserItem-inner{cursor:pointer;font-size:13px;font-weight:500}.uppy-ProviderBrowser-footer{align-items:center;background-color:#fff;border-top:1px solid #eaeaea;display:flex;justify-content:space-between;padding:15px}.uppy-ProviderBrowser-footer button{margin-inline-end:8px}[data-uppy-theme=dark] .uppy-ProviderBrowser-footer{background-color:#1f1f1f;border-top:1px solid #333}.uppy-ProviderBrowser-footer-buttons{flex-shrink:0}.uppy-ProviderBrowser-footer-error{color:#e32437;line-height:18px}@media(max-width:426px){.uppy-ProviderBrowser-footer{align-items:stretch;flex-direction:column-reverse}.uppy-ProviderBrowser-footer-error{padding-bottom:10px}}.picker-dialog-bg{z-index:20000!important}.picker-dialog{z-index:20001!important}.uppy-Dashboard-Item-previewInnerWrap{align-items:center;border-radius:3px;box-shadow:0 0 2px #0006;display:flex;flex-direction:column;height:100%;justify-content:center;overflow:hidden;position:relative;width:100%}.uppy-size--md .uppy-Dashboard-Item-previewInnerWrap{box-shadow:0 1px 2px #00000026}.uppy-Dashboard--singleFile .uppy-Dashboard-Item-previewInnerWrap{box-shadow:none}.uppy-Dashboard-Item-previewInnerWrap:after{background-color:#000000a6;content:"";display:none;inset:0;position:absolute;z-index:1001}.uppy-Dashboard-Item-previewLink{inset:0;position:absolute;z-index:1002}.uppy-Dashboard-Item-previewLink:focus{box-shadow:inset 0 0 0 3px #579df0}[data-uppy-theme=dark] .uppy-Dashboard-Item-previewLink:focus{box-shadow:inset 0 0 0 3px #016c8d}.uppy-Dashboard-Item-preview img.uppy-Dashboard-Item-previewImg{border-radius:3px;height:100%;object-fit:cover;transform:translateZ(0);width:100%}.uppy-Dashboard--singleFile .uppy-Dashboard-Item-preview img.uppy-Dashboard-Item-previewImg{height:auto;max-height:100%;max-width:100%;object-fit:contain;padding:10px;width:auto}.uppy-Dashboard-Item-progress{color:#fff;left:50%;position:absolute;text-align:center;top:50%;transform:translate(-50%,-50%);transition:all .35 ease;width:120px;z-index:1002}.uppy-Dashboard-Item-progressIndicator{color:#fff;display:inline-block;height:38px;opacity:.9;width:38px}.uppy-size--md .uppy-Dashboard-Item-progressIndicator{height:55px;width:55px}button.uppy-Dashboard-Item-progressIndicator{cursor:pointer}button.uppy-Dashboard-Item-progressIndicator:focus{outline:none}button.uppy-Dashboard-Item-progressIndicator::-moz-focus-inner{border:0}button.uppy-Dashboard-Item-progressIndicator:focus .uppy-Dashboard-Item-progressIcon--bg,button.uppy-Dashboard-Item-progressIndicator:focus .uppy-Dashboard-Item-progressIcon--retry{fill:#579df0}.uppy-Dashboard-Item-progressIcon--circle{height:100%;width:100%}.uppy-Dashboard-Item-progressIcon--bg{stroke:#fff6}.uppy-Dashboard-Item-progressIcon--progress{transition:stroke-dashoffset .5s ease-out;stroke:#fff}.uppy-Dashboard-Item-progressIcon--play{transition:all .2s;fill:#fff;stroke:#fff}.uppy-Dashboard-Item-progressIcon--cancel{transition:all .2s;fill:#fff}.uppy-Dashboard-Item-progressIcon--pause{transition:all .2s;fill:#fff;stroke:#fff}.uppy-Dashboard-Item-progressIcon--check{transition:all .2s;fill:#fff}.uppy-Dashboard-Item-progressIcon--retry{fill:#fff}.uppy-Dashboard-Item.is-complete .uppy-Dashboard-Item-progress{inset-inline-end:-8px;inset-inline-start:auto;top:-9px;transform:none;width:auto}.uppy-Dashboard-Item.is-error .uppy-Dashboard-Item-progressIndicator{height:18px;width:18px}.uppy-size--md .uppy-Dashboard-Item.is-error .uppy-Dashboard-Item-progressIndicator{height:28px;width:28px}.uppy-Dashboard-Item.is-complete .uppy-Dashboard-Item-progressIndicator{height:18px;opacity:1;width:18px}.uppy-size--md .uppy-Dashboard-Item.is-complete .uppy-Dashboard-Item-progressIndicator{height:22px;width:22px}.uppy-Dashboard-Item.is-processing .uppy-Dashboard-Item-progress{opacity:0}.uppy-Dashboard-Item-fileInfo{padding-inline-end:5px}.uppy-Dashboard--singleFile .uppy-Dashboard-Item-fileInfo{padding-inline-end:10px}.uppy-size--md.uppy-Dashboard--singleFile .uppy-Dashboard-Item-fileInfo{padding-inline-end:15px}.uppy-Dashboard-Item-name{font-size:12px;font-weight:500;line-height:1.3;margin-bottom:5px;word-wrap:anywhere;word-break:break-all}[data-uppy-theme=dark] .uppy-Dashboard-Item-name{color:#eaeaea}.uppy-size--md.uppy-Dashboard--singleFile .uppy-Dashboard-Item-name{font-size:14px;line-height:1.4}.uppy-Dashboard-Item-fileName{align-items:baseline;display:flex}.uppy-Dashboard-Item-fileName button{margin-left:5px}.uppy-Dashboard-Item-author{color:#757575;display:inline-block;font-size:11px;font-weight:400;line-height:1;margin-bottom:5px;vertical-align:bottom}.uppy-Dashboard-Item-author a{color:#757575}.uppy-Dashboard-Item-status{color:#757575;font-size:11px;font-weight:400;line-height:1}[data-uppy-theme=dark] .uppy-Dashboard-Item-status{color:#bbb}.uppy-Dashboard-Item-statusSize{display:inline-block;margin-bottom:5px;text-transform:uppercase;vertical-align:bottom}.uppy-Dashboard-Item-reSelect{color:#1269cf;font-family:inherit;font-size:inherit;font-weight:600}.uppy-Dashboard-Item-errorMessage{background-color:#fdeff1;color:#a51523;font-size:11px;font-weight:500;line-height:1.3;padding:5px 6px}.uppy-Dashboard-Item-errorMessageBtn{color:#a51523;cursor:pointer;font-size:11px;font-weight:500;text-decoration:underline}.uppy-Dashboard-Item-preview .uppy-Dashboard-Item-errorMessage{display:none}.uppy-size--md .uppy-Dashboard-Item-preview .uppy-Dashboard-Item-errorMessage{border-bottom-left-radius:3px;border-bottom-right-radius:3px;border-top:1px solid #f7c2c8;bottom:0;display:block;left:0;line-height:1.4;padding:6px 8px;position:absolute;right:0}.uppy-Dashboard-Item-fileInfo .uppy-Dashboard-Item-errorMessage{border:1px solid #f7c2c8;border-radius:3px;display:inline-block;position:static}.uppy-size--md .uppy-Dashboard-Item-fileInfo .uppy-Dashboard-Item-errorMessage{display:none}.uppy-Dashboard-Item-action{color:#939393;cursor:pointer}.uppy-Dashboard-Item-action:focus{outline:none}.uppy-Dashboard-Item-action::-moz-focus-inner{border:0}.uppy-Dashboard-Item-action:focus{box-shadow:0 0 0 3px #1269cf80}.uppy-Dashboard-Item-action:hover{color:#1f1f1f;opacity:1}[data-uppy-theme=dark] .uppy-Dashboard-Item-action{color:#cfcfcf}[data-uppy-theme=dark] .uppy-Dashboard-Item-action:focus{outline:none}[data-uppy-theme=dark] .uppy-Dashboard-Item-action::-moz-focus-inner{border:0}[data-uppy-theme=dark] .uppy-Dashboard-Item-action:focus{box-shadow:0 0 0 2px #aae1ffd9}[data-uppy-theme=dark] .uppy-Dashboard-Item-action:hover{color:#eaeaea}.uppy-Dashboard-Item-action--remove{color:#1f1f1f;opacity:.95}.uppy-Dashboard-Item-action--remove:hover{color:#000;opacity:1}.uppy-size--md .uppy-Dashboard-Item-action--remove{height:18px;inset-inline-end:-8px;padding:0;position:absolute;top:-8px;width:18px;z-index:1002}.uppy-size--md .uppy-Dashboard-Item-action--remove:focus{border-radius:50%}.uppy-Dashboard--singleFile.uppy-size--height-md .uppy-Dashboard-Item-action--remove{inset-inline-end:8px;position:absolute;top:8px}[data-uppy-theme=dark] .uppy-Dashboard-Item-action--remove{color:#525252}[data-uppy-theme=dark] .uppy-Dashboard-Item-action--remove:hover{color:#333}.uppy-Dashboard:not(.uppy-size--md):not(.uppy-Dashboard--singleFile.uppy-size--height-md) .uppy-Dashboard-Item-actionWrapper{align-items:center;display:flex}.uppy-Dashboard:not(.uppy-size--md):not(.uppy-Dashboard--singleFile.uppy-size--height-md) .uppy-Dashboard-Item-action{height:22px;margin-left:3px;padding:3px;width:22px}.uppy-Dashboard:not(.uppy-size--md):not(.uppy-Dashboard--singleFile.uppy-size--height-md) .uppy-Dashboard-Item-action:focus{border-radius:3px}.uppy-size--md .uppy-Dashboard-Item-action--copyLink,.uppy-size--md .uppy-Dashboard-Item-action--edit{height:16px;padding:0;width:16px}.uppy-size--md .uppy-Dashboard-Item-action--copyLink:focus,.uppy-size--md .uppy-Dashboard-Item-action--edit:focus{border-radius:3px}.uppy-Dashboard-Item{align-items:center;border-bottom:1px solid #eaeaea;display:flex;padding:10px}.uppy-Dashboard:not(.uppy-Dashboard--singleFile) .uppy-Dashboard-Item{padding-inline-end:0}[data-uppy-theme=dark] .uppy-Dashboard-Item{border-bottom:1px solid #333}.uppy-size--md .uppy-Dashboard-Item{border-bottom:0;display:block;float:inline-start;height:215px;margin:5px 15px;padding:0;position:relative;width:calc(33.333% - 30px)}.uppy-size--lg .uppy-Dashboard-Item{height:190px;margin:5px 15px;padding:0;width:calc(25% - 30px)}.uppy-size--xl .uppy-Dashboard-Item{height:210px;padding:0;width:calc(20% - 30px)}.uppy-Dashboard--singleFile .uppy-Dashboard-Item{border-bottom:0;display:flex;flex-direction:column;height:100%;max-width:400px;padding:15px;position:relative;width:100%}.uppy-Dashboard-Item.is-ghost .uppy-Dashboard-Item-previewInnerWrap{opacity:.2}.uppy-Dashboard-Item.is-ghost .uppy-Dashboard-Item-name{opacity:.7}.uppy-Dashboard-Item.is-ghost .uppy-Dashboard-Item-preview:before{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='35' height='39' viewBox='0 0 35 39'%3E%3Cpath fill='%2523000' d='M1.708 38.66c1.709 0 3.417-3.417 6.834-3.417s5.125 3.417 8.61 3.417c3.348 0 5.056-3.417 8.473-3.417 4.305 0 5.125 3.417 6.833 3.417.889 0 1.709-.889 1.709-1.709v-19.68C34.167-5.757 0-5.757 0 17.271v19.68c0 .82.888 1.709 1.708 1.709m8.542-17.084a3.383 3.383 0 0 1-3.417-3.416 3.383 3.383 0 0 1 3.417-3.417 3.383 3.383 0 0 1 3.417 3.417 3.383 3.383 0 0 1-3.417 3.416m13.667 0A3.383 3.383 0 0 1 20.5 18.16a3.383 3.383 0 0 1 3.417-3.417 3.383 3.383 0 0 1 3.416 3.417 3.383 3.383 0 0 1-3.416 3.416'/%3E%3C/svg%3E");background-position:50% 10px;background-repeat:no-repeat;background-size:25px;content:"";inset:0;opacity:.5;position:absolute;z-index:1005}.uppy-size--md .uppy-Dashboard-Item.is-ghost .uppy-Dashboard-Item-preview:before{background-position:50% 50%;background-size:40px}.uppy-Dashboard--singleFile .uppy-Dashboard-Item.is-ghost .uppy-Dashboard-Item-preview:before{background-position:50% 50%;background-size:30%}.uppy-Dashboard-Item-preview{flex-grow:0;flex-shrink:0;height:50px;position:relative;width:50px}.uppy-size--md .uppy-Dashboard-Item-preview{height:140px;width:100%}.uppy-size--lg .uppy-Dashboard-Item-preview{height:120px}.uppy-size--xl .uppy-Dashboard-Item-preview{height:140px}.uppy-Dashboard--singleFile .uppy-Dashboard-Item-preview{flex-grow:1;max-height:75%;width:100%}.uppy-Dashboard--singleFile.uppy-size--md .uppy-Dashboard-Item-preview{max-height:100%}.uppy-Dashboard-Item-fileInfoAndButtons{align-items:center;display:flex;flex-grow:1;justify-content:space-between;padding-inline-end:8px;padding-inline-start:12px}.uppy-Dashboard--singleFile .uppy-Dashboard-Item-fileInfoAndButtons,.uppy-size--md .uppy-Dashboard-Item-fileInfoAndButtons{align-items:flex-start;padding:9px 0 0}.uppy-Dashboard--singleFile .uppy-Dashboard-Item-fileInfoAndButtons{flex-grow:0;width:100%}.uppy-Dashboard-Item-fileInfo{flex-grow:1;flex-shrink:1}.uppy-Dashboard-Item-actionWrapper{flex-grow:0;flex-shrink:0}.uppy-Dashboard-Item.is-error .uppy-Dashboard-Item-previewInnerWrap:after,.uppy-Dashboard-Item.is-inprogress .uppy-Dashboard-Item-previewInnerWrap:after{display:block}.uppy-Dashboard-Item-errorDetails{appearance:none;background-color:#939393;border:none;border-radius:50%;color:#fff;cursor:help;flex-shrink:0;font-size:10px;font-weight:600;height:13px;inset-inline-start:2px;line-height:12px;position:relative;text-align:center;top:0;width:13px}.uppy-Dashboard-Item-errorDetails:after{line-height:1.3;word-wrap:break-word}.uppy-Dashboard-FileCard{background-color:#fff;border-radius:5px;box-shadow:0 0 10px 4px #0000001a;display:flex;flex-direction:column;height:100%;inset:0;position:absolute;width:100%;z-index:1005}.uppy-Dashboard-FileCard .uppy-DashboardContent-bar{border-top-left-radius:5px;border-top-right-radius:5px}.uppy-Dashboard-FileCard .uppy-Dashboard-FileCard-actions{border-bottom-left-radius:5px;border-bottom-right-radius:5px}.uppy-Dashboard-FileCard-inner{display:flex;flex-direction:column;flex-grow:1;flex-shrink:1;height:100%;min-height:0}.uppy-Dashboard-FileCard-preview{align-items:center;border-bottom:1px solid #eaeaea;display:flex;flex-grow:0;flex-shrink:1;height:60%;justify-content:center;min-height:0;position:relative}[data-uppy-theme=dark] .uppy-Dashboard-FileCard-preview{background-color:#333;border-bottom:0}.uppy-Dashboard-FileCard-preview img.uppy-Dashboard-Item-previewImg{border-radius:3px;box-shadow:0 3px 20px #00000026;flex:0 0 auto;max-height:90%;max-width:90%;object-fit:cover}.uppy-Dashboard-FileCard-edit{background-color:#00000080;border-radius:50px;color:#fff;font-size:13px;inset-inline-end:10px;padding:7px 15px;position:absolute;top:10px}.uppy-Dashboard-FileCard-edit:focus{outline:none}.uppy-Dashboard-FileCard-edit::-moz-focus-inner{border:0}.uppy-Dashboard-FileCard-edit:focus{box-shadow:0 0 0 3px #1269cf80}.uppy-Dashboard-FileCard-edit:hover{background-color:#000c}.uppy-Dashboard-FileCard-info{flex-grow:0;flex-shrink:0;height:40%;overflow-y:auto;padding:30px 20px 20px;-webkit-overflow-scrolling:touch}[data-uppy-theme=dark] .uppy-Dashboard-FileCard-info{background-color:#1f1f1f}.uppy-Dashboard-FileCard-fieldset{border:0;font-size:0;margin:auto auto 12px;max-width:640px;padding:0}.uppy-Dashboard-FileCard-label{color:#525252;display:inline-block;font-size:12px;vertical-align:middle;width:22%}.uppy-size--md .uppy-Dashboard-FileCard-label{font-size:14px}[data-uppy-theme=dark] .uppy-Dashboard-FileCard-label{color:#eaeaea}.uppy-Dashboard-FileCard-input{display:inline-block;vertical-align:middle;width:78%}.uppy-Dashboard-FileCard-actions{align-items:center;background-color:#fafafa;border-top:1px solid #eaeaea;display:flex;flex-grow:0;flex-shrink:0;height:55px;padding:0 15px}.uppy-size--md .uppy-Dashboard-FileCard-actions{height:65px}[data-uppy-theme=dark] .uppy-Dashboard-FileCard-actions{background-color:#1f1f1f;border-top:1px solid #333}.uppy-Dashboard-FileCard-actionsBtn{margin-inline-end:10px}.uppy-Informer{bottom:60px;left:0;position:absolute;right:0;text-align:center;z-index:1005}.uppy-Informer span>div{margin-bottom:6px}.uppy-Informer-animated{opacity:0;transform:translateY(350%);transition:all .3s ease-in;z-index:-1000}.uppy-Informer p{background-color:#757575;border-radius:18px;color:#fff;display:inline-block;font-size:12px;font-weight:400;line-height:1.4;margin:0;max-width:90%;padding:6px 15px}.uppy-size--md .uppy-Informer p{font-size:14px;line-height:1.3;max-width:500px;padding:10px 20px}[data-uppy-theme=dark] .uppy-Informer p{background-color:#333}.uppy-Informer p span{background-color:#fff;border-radius:50%;color:#525252;display:inline-block;font-size:10px;height:13px;inset-inline-start:3px;line-height:12px;margin-inline-start:-1px;position:relative;top:-1px;vertical-align:middle;width:13px}.uppy-Informer p span:hover{cursor:help}.uppy-Informer p span:after{line-height:1.3;word-wrap:break-word}.uppy-Root [aria-label][role~=tooltip]{position:relative}.uppy-Root [aria-label][role~=tooltip]:after,.uppy-Root [aria-label][role~=tooltip]:before{-webkit-backface-visibility:hidden;backface-visibility:hidden;box-sizing:border-box;opacity:0;pointer-events:none;position:absolute;transform:translateZ(0);transform-origin:top;transition:all var(--microtip-transition-duration,.18s) var(--microtip-transition-easing,ease-in-out) var(--microtip-transition-delay,0s);will-change:transform;z-index:10}.uppy-Root [aria-label][role~=tooltip]:before{background-size:100% auto!important;content:""}.uppy-Root [aria-label][role~=tooltip]:after{background:#111111e6;border-radius:4px;box-sizing:initial;color:#fff;content:attr(aria-label);font-size:var(--microtip-font-size,13px);font-weight:var(--microtip-font-weight,normal);padding:.5em 1em;text-transform:var(--microtip-text-transform,none);white-space:nowrap}.uppy-Root [aria-label][role~=tooltip]:focus:after,.uppy-Root [aria-label][role~=tooltip]:focus:before,.uppy-Root [aria-label][role~=tooltip]:hover:after,.uppy-Root [aria-label][role~=tooltip]:hover:before{opacity:1;pointer-events:auto}.uppy-Root [role~=tooltip][data-microtip-position|=top]:before{background:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='36' height='12'%3E%3Cpath fill='rgba(17, 17, 17, 0.9)' d='M2.658 0h32.004c-6 0-11.627 12.002-16.002 12.002S8.594 0 2.658 0'/%3E%3C/svg%3E") no-repeat;bottom:100%;height:6px;left:50%;margin-bottom:5px;transform:translate3d(-50%,0,0);width:18px}.uppy-Root [role~=tooltip][data-microtip-position|=top]:after{bottom:100%;left:50%;margin-bottom:11px;transform:translate3d(-50%,0,0)}.uppy-Root [role~=tooltip][data-microtip-position=top]:hover:after,.uppy-Root [role~=tooltip][data-microtip-position|=top]:hover:before{transform:translate3d(-50%,-5px,0)}.uppy-Root [role~=tooltip][data-microtip-position=top-left]:after{bottom:100%;transform:translate3d(calc(-100% + 16px),0,0)}.uppy-Root [role~=tooltip][data-microtip-position=top-left]:hover:after{transform:translate3d(calc(-100% + 16px),-5px,0)}.uppy-Root [role~=tooltip][data-microtip-position=top-right]:after{bottom:100%;transform:translate3d(-16px,0,0)}.uppy-Root [role~=tooltip][data-microtip-position=top-right]:hover:after{transform:translate3d(-16px,-5px,0)}.uppy-Root [role~=tooltip][data-microtip-position|=bottom]:before{background:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='36' height='12'%3E%3Cpath fill='rgba(17, 17, 17, 0.9)' d='M33.342 12H1.338c6 0 11.627-12.002 16.002-12.002S27.406 12 33.342 12'/%3E%3C/svg%3E") no-repeat;bottom:auto;height:6px;left:50%;margin-bottom:0;margin-top:5px;top:100%;transform:translate3d(-50%,-10px,0);width:18px}.uppy-Root [role~=tooltip][data-microtip-position|=bottom]:after{left:50%;margin-top:11px;top:100%;transform:translate3d(-50%,-10px,0)}.uppy-Root [role~=tooltip][data-microtip-position=bottom]:hover:after,.uppy-Root [role~=tooltip][data-microtip-position|=bottom]:hover:before{transform:translate3d(-50%,0,0)}.uppy-Root [role~=tooltip][data-microtip-position=bottom-left]:after{top:100%;transform:translate3d(calc(-100% + 16px),-10px,0)}.uppy-Root [role~=tooltip][data-microtip-position=bottom-left]:hover:after{transform:translate3d(calc(-100% + 16px),0,0)}.uppy-Root [role~=tooltip][data-microtip-position=bottom-right]:after{top:100%;transform:translate3d(-16px,-10px,0)}.uppy-Root [role~=tooltip][data-microtip-position=bottom-right]:hover:after{transform:translate3d(-16px,0,0)}.uppy-Root [role~=tooltip][data-microtip-position=left]:after,.uppy-Root [role~=tooltip][data-microtip-position=left]:before{inset:50% 100% auto auto;transform:translate3d(10px,-50%,0)}.uppy-Root [role~=tooltip][data-microtip-position=left]:before{background:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='36'%3E%3Cpath fill='rgba(17, 17, 17, 0.9)' d='M0 33.342V1.338c0 6 12.002 11.627 12.002 16.002S0 27.406 0 33.342'/%3E%3C/svg%3E") no-repeat;height:18px;margin-bottom:0;margin-right:5px;width:6px}.uppy-Root [role~=tooltip][data-microtip-position=left]:after{margin-right:11px}.uppy-Root [role~=tooltip][data-microtip-position=left]:hover:after,.uppy-Root [role~=tooltip][data-microtip-position=left]:hover:before{transform:translate3d(0,-50%,0)}.uppy-Root [role~=tooltip][data-microtip-position=right]:after,.uppy-Root [role~=tooltip][data-microtip-position=right]:before{bottom:auto;left:100%;top:50%;transform:translate3d(-10px,-50%,0)}.uppy-Root [role~=tooltip][data-microtip-position=right]:before{background:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='36'%3E%3Cpath fill='rgba(17, 17, 17, 0.9)' d='M12 2.658v32.004c0-6-12.002-11.627-12.002-16.002S12 8.594 12 2.658'/%3E%3C/svg%3E") no-repeat;height:18px;margin-bottom:0;margin-left:5px;width:6px}.uppy-Root [role~=tooltip][data-microtip-position=right]:after{margin-left:11px}.uppy-Root [role~=tooltip][data-microtip-position=right]:hover:after,.uppy-Root [role~=tooltip][data-microtip-position=right]:hover:before{transform:translate3d(0,-50%,0)}.uppy-Root [role~=tooltip][data-microtip-size=small]:after{white-space:normal;width:80px}.uppy-Root [role~=tooltip][data-microtip-size=medium]:after{white-space:normal;width:150px}.uppy-Root [role~=tooltip][data-microtip-size=large]:after{white-space:normal;width:260px}.uppy-StatusBar{background-color:#fff;color:#fff;display:flex;font-size:12px;font-weight:400;height:46px;line-height:40px;position:relative;transition:height .2s;z-index:1001}[data-uppy-theme=dark] .uppy-StatusBar{background-color:#1f1f1f}.uppy-StatusBar:before{background-color:#eaeaea;content:"";height:2px;inset:0;position:absolute;width:100%}[data-uppy-theme=dark] .uppy-StatusBar:before{background-color:#757575}.uppy-StatusBar[aria-hidden=true]{height:0;overflow-y:hidden}.uppy-StatusBar.is-complete .uppy-StatusBar-progress{background-color:#1bb240}.uppy-StatusBar.is-error .uppy-StatusBar-progress{background-color:#e32437}.uppy-StatusBar.is-complete .uppy-StatusBar-statusIndicator{color:#1bb240}.uppy-StatusBar.is-error .uppy-StatusBar-statusIndicator{color:#e32437}.uppy-StatusBar:not([aria-hidden=true]).is-waiting{background-color:#fff;border-top:1px solid #eaeaea;height:65px}[data-uppy-theme=dark] .uppy-StatusBar:not([aria-hidden=true]).is-waiting{background-color:#1f1f1f;border-top:1px solid #333}.uppy-StatusBar-progress{background-color:#1269cf;height:2px;position:absolute;transition:background-color,width .3s ease-out;z-index:1001}.uppy-StatusBar-progress.is-indeterminate{animation:uppy-StatusBar-ProgressStripes 1s linear infinite;background-image:linear-gradient(45deg,#0000004d 25%,#0000 0 50%,#0000004d 0 75%,#0000 0,#0000);background-size:64px 64px}@keyframes uppy-StatusBar-ProgressStripes{0%{background-position:0 0}to{background-position:64px 0}}.uppy-StatusBar.is-postprocessing .uppy-StatusBar-progress,.uppy-StatusBar.is-preprocessing .uppy-StatusBar-progress{background-color:#f6a623}.uppy-StatusBar.is-waiting .uppy-StatusBar-progress{display:none}.uppy-StatusBar-content{align-items:center;color:#333;display:flex;height:100%;padding-inline-start:10px;position:relative;text-overflow:ellipsis;white-space:nowrap;z-index:1002}.uppy-size--md .uppy-StatusBar-content{padding-inline-start:15px}[data-uppy-theme=dark] .uppy-StatusBar-content{color:#eaeaea}.uppy-StatusBar-status{display:flex;flex-direction:column;font-weight:400;justify-content:center;line-height:1.4;padding-inline-end:.3em}.uppy-StatusBar-statusPrimary{display:flex;font-weight:500;line-height:1}.uppy-StatusBar-statusPrimary button.uppy-StatusBar-details{margin-left:5px}[data-uppy-theme=dark] .uppy-StatusBar-statusPrimary{color:#eaeaea}.uppy-StatusBar-statusSecondary{color:#757575;display:inline-block;font-size:11px;line-height:1.2;margin-top:1px;white-space:nowrap}[data-uppy-theme=dark] .uppy-StatusBar-statusSecondary{color:#bbb}.uppy-StatusBar-statusSecondaryHint{display:inline-block;line-height:1;margin-inline-end:5px;vertical-align:middle}.uppy-size--md .uppy-StatusBar-statusSecondaryHint{margin-inline-end:8px}.uppy-StatusBar-statusIndicator{color:#525252;margin-inline-end:7px;position:relative;top:1px}.uppy-StatusBar-statusIndicator svg{vertical-align:text-bottom}.uppy-StatusBar-actions{align-items:center;bottom:0;display:flex;inset-inline-end:10px;position:absolute;top:0;z-index:1004}.uppy-StatusBar.is-waiting .uppy-StatusBar-actions{background-color:#fafafa;height:100%;padding:0 15px;position:static;width:100%}[data-uppy-theme=dark] .uppy-StatusBar.is-waiting .uppy-StatusBar-actions{background-color:#1f1f1f}.uppy-StatusBar:not([aria-hidden=true]).is-waiting.has-ghosts{flex-direction:column;height:90px}.uppy-size--md .uppy-StatusBar:not([aria-hidden=true]).is-waiting.has-ghosts{flex-direction:row;height:65px}.uppy-StatusBar:not([aria-hidden=true]).is-waiting.has-ghosts .uppy-StatusBar-actions{flex-direction:column;justify-content:center}.uppy-size--md .uppy-StatusBar:not([aria-hidden=true]).is-waiting.has-ghosts .uppy-StatusBar-actions{flex-direction:row;justify-content:normal}.uppy-StatusBar-actionCircleBtn{cursor:pointer;line-height:1;margin:3px;opacity:.9}.uppy-StatusBar-actionCircleBtn:focus{outline:none}.uppy-StatusBar-actionCircleBtn::-moz-focus-inner{border:0}.uppy-StatusBar-actionCircleBtn:focus{box-shadow:0 0 0 3px #1269cf80}[data-uppy-theme=dark] .uppy-StatusBar-actionCircleBtn:focus{outline:none}[data-uppy-theme=dark] .uppy-StatusBar-actionCircleBtn::-moz-focus-inner{border:0}[data-uppy-theme=dark] .uppy-StatusBar-actionCircleBtn:focus{box-shadow:0 0 0 2px #aae1ffd9}.uppy-StatusBar-actionCircleBtn:hover{opacity:1}.uppy-StatusBar-actionCircleBtn:focus{border-radius:50%}.uppy-StatusBar-actionCircleBtn svg{vertical-align:bottom}.uppy-StatusBar-actionBtn{color:#1269cf;display:inline-block;font-size:10px;line-height:inherit;vertical-align:middle}.uppy-size--md .uppy-StatusBar-actionBtn{font-size:11px}.uppy-StatusBar-actionBtn--disabled{opacity:.4}[data-uppy-theme=dark] .uppy-StatusBar-actionBtn--disabled{opacity:.7}.uppy-StatusBar-actionBtn--retry{background-color:#ff4b23;border-radius:8px;color:#fff;height:16px;line-height:1;margin-inline-end:6px;padding:1px 6px 3px 18px;position:relative}.uppy-StatusBar-actionBtn--retry:focus{outline:none}.uppy-StatusBar-actionBtn--retry::-moz-focus-inner{border:0}.uppy-StatusBar-actionBtn--retry:focus{box-shadow:0 0 0 3px #1269cf80}[data-uppy-theme=dark] .uppy-StatusBar-actionBtn--retry:focus{outline:none}[data-uppy-theme=dark] .uppy-StatusBar-actionBtn--retry::-moz-focus-inner{border:0}[data-uppy-theme=dark] .uppy-StatusBar-actionBtn--retry:focus{box-shadow:0 0 0 2px #aae1ffd9}.uppy-StatusBar-actionBtn--retry:hover{background-color:#f92d00}.uppy-StatusBar-actionBtn--retry svg{inset-inline-start:6px;position:absolute;top:3px}.uppy-StatusBar.is-waiting .uppy-StatusBar-actionBtn--upload{background-color:#1bb240;color:#fff;font-size:14px;line-height:1;padding:15px 10px;width:100%}.uppy-StatusBar.is-waiting .uppy-StatusBar-actionBtn--upload:hover{background-color:#189c38}[data-uppy-theme=dark] .uppy-StatusBar.is-waiting .uppy-StatusBar-actionBtn--upload{background-color:#1c8b37}[data-uppy-theme=dark] .uppy-StatusBar.is-waiting .uppy-StatusBar-actionBtn--upload:hover{background-color:#18762f}.uppy-size--md .uppy-StatusBar.is-waiting .uppy-StatusBar-actionBtn--upload{padding:13px 22px;width:auto}.uppy-StatusBar.is-waiting .uppy-StatusBar-actionBtn--upload.uppy-StatusBar-actionBtn--disabled:hover{background-color:#1bb240;cursor:not-allowed}[data-uppy-theme=dark] .uppy-StatusBar.is-waiting .uppy-StatusBar-actionBtn--upload.uppy-StatusBar-actionBtn--disabled:hover{background-color:#1c8b37}.uppy-StatusBar:not(.is-waiting) .uppy-StatusBar-actionBtn--upload{background-color:initial;color:#1269cf}.uppy-StatusBar-actionBtn--uploadNewlyAdded{border-radius:3px;padding-inline-end:3px;padding-bottom:1px;padding-inline-start:3px}.uppy-StatusBar-actionBtn--uploadNewlyAdded:focus{outline:none}.uppy-StatusBar-actionBtn--uploadNewlyAdded::-moz-focus-inner{border:0}.uppy-StatusBar-actionBtn--uploadNewlyAdded:focus{box-shadow:0 0 0 3px #1269cf80}[data-uppy-theme=dark] .uppy-StatusBar-actionBtn--uploadNewlyAdded:focus{outline:none}[data-uppy-theme=dark] .uppy-StatusBar-actionBtn--uploadNewlyAdded::-moz-focus-inner{border:0}[data-uppy-theme=dark] .uppy-StatusBar-actionBtn--uploadNewlyAdded:focus{box-shadow:0 0 0 2px #aae1ffd9}.uppy-StatusBar.is-postprocessing .uppy-StatusBar-actionBtn--uploadNewlyAdded,.uppy-StatusBar.is-preprocessing .uppy-StatusBar-actionBtn--uploadNewlyAdded{display:none}.uppy-StatusBar-actionBtn--done{border-radius:3px;line-height:1;padding:7px 8px}.uppy-StatusBar-actionBtn--done:focus{outline:none}.uppy-StatusBar-actionBtn--done::-moz-focus-inner{border:0}.uppy-StatusBar-actionBtn--done:hover{color:#0e51a0}.uppy-StatusBar-actionBtn--done:focus{background-color:#dfe6f1}[data-uppy-theme=dark] .uppy-StatusBar-actionBtn--done:focus{background-color:#333}[data-uppy-theme=dark] .uppy-StatusBar-actionBtn--done{color:#02baf2}.uppy-size--md .uppy-StatusBar-actionBtn--done{font-size:14px}.uppy-StatusBar-serviceMsg{color:#000;font-size:11px;line-height:1.1;padding-left:10px}.uppy-size--md .uppy-StatusBar-serviceMsg{font-size:14px;padding-left:15px}[data-uppy-theme=dark] .uppy-StatusBar-serviceMsg{color:#eaeaea}.uppy-StatusBar-serviceMsg-ghostsIcon{left:6px;opacity:.5;position:relative;top:2px;vertical-align:text-bottom;width:10px}.uppy-size--md .uppy-StatusBar-serviceMsg-ghostsIcon{left:10px;top:1px;width:15px}.uppy-StatusBar-details{appearance:none;background-color:#939393;border-radius:50%;color:#fff;cursor:help;display:inline-block;font-size:10px;font-weight:600;height:13px;inset-inline-start:2px;line-height:12px;position:relative;text-align:center;top:0;vertical-align:middle;width:13px}.uppy-StatusBar-details:after{line-height:1.3;word-wrap:break-word}.uppy-StatusBar-spinner{animation-duration:1s;animation-iteration-count:infinite;animation-name:uppy-StatusBar-spinnerAnimation;animation-timing-function:linear;fill:#1269cf;margin-inline-end:10px}.uppy-StatusBar.is-postprocessing .uppy-StatusBar-spinner,.uppy-StatusBar.is-preprocessing .uppy-StatusBar-spinner{fill:#f6a623}@keyframes uppy-StatusBar-spinnerAnimation{0%{transform:rotate(0)}to{transform:rotate(1turn)}}.uppy-transition-slideDownUp-enter{opacity:.01;transform:translate3d(0,-105%,0);transition:transform .25s ease-in-out,opacity .25s ease-in-out}.uppy-transition-slideDownUp-enter.uppy-transition-slideDownUp-enter-active{opacity:1;transform:translateZ(0)}.uppy-transition-slideDownUp-leave{opacity:1;transform:translateZ(0);transition:transform .25s ease-in-out,opacity .25s ease-in-out}.uppy-transition-slideDownUp-leave.uppy-transition-slideDownUp-leave-active{opacity:.01;transform:translate3d(0,-105%,0)}@keyframes uppy-Dashboard-fadeIn{0%{opacity:0}to{opacity:1}}@keyframes uppy-Dashboard-fadeOut{0%{opacity:1}to{opacity:0}}@keyframes uppy-Dashboard-slideDownAndFadeIn{0%{opacity:0;transform:translate3d(-50%,-70%,0)}to{opacity:1;transform:translate3d(-50%,-50%,0)}}@keyframes uppy-Dashboard-slideDownAndFadeIn--small{0%{opacity:0;transform:translate3d(0,-20%,0)}to{opacity:1;transform:translateZ(0)}}@keyframes uppy-Dashboard-slideUpFadeOut{0%{opacity:1;transform:translate3d(-50%,-50%,0)}to{opacity:0;transform:translate3d(-50%,-70%,0)}}@keyframes uppy-Dashboard-slideUpFadeOut--small{0%{opacity:1;transform:translateZ(0)}to{opacity:0;transform:translate3d(0,-20%,0)}}.uppy-Dashboard--modal{z-index:1001}.uppy-Dashboard--modal[aria-hidden=true]{display:none}.uppy-Dashboard--modal.uppy-Dashboard--animateOpenClose>.uppy-Dashboard-inner{animation:uppy-Dashboard-slideDownAndFadeIn--small .3s cubic-bezier(0,0,.2,1)}@media only screen and (min-width:820px){.uppy-Dashboard--modal.uppy-Dashboard--animateOpenClose>.uppy-Dashboard-inner{animation:uppy-Dashboard-slideDownAndFadeIn .3s cubic-bezier(0,0,.2,1)}}.uppy-Dashboard--modal.uppy-Dashboard--animateOpenClose>.uppy-Dashboard-overlay{animation:uppy-Dashboard-fadeIn .3s cubic-bezier(0,0,.2,1)}.uppy-Dashboard--modal.uppy-Dashboard--animateOpenClose.uppy-Dashboard--isClosing>.uppy-Dashboard-inner{animation:uppy-Dashboard-slideUpFadeOut--small .3s cubic-bezier(0,0,.2,1)}@media only screen and (min-width:820px){.uppy-Dashboard--modal.uppy-Dashboard--animateOpenClose.uppy-Dashboard--isClosing>.uppy-Dashboard-inner{animation:uppy-Dashboard-slideUpFadeOut .3s cubic-bezier(0,0,.2,1)}}.uppy-Dashboard--modal.uppy-Dashboard--animateOpenClose.uppy-Dashboard--isClosing>.uppy-Dashboard-overlay{animation:uppy-Dashboard-fadeOut .3s cubic-bezier(0,0,.2,1)}.uppy-Dashboard-isFixed{height:100vh;overflow:hidden}.uppy-Dashboard--modal .uppy-Dashboard-overlay{background-color:#00000080;inset:0;position:fixed;z-index:1001}.uppy-Dashboard-inner{background-color:#f4f4f4;border:1px solid #eaeaea;border-radius:5px;max-height:100%;max-width:100%;outline:none;position:relative}.uppy-size--md .uppy-Dashboard-inner{min-height:auto}@media only screen and (min-width:820px){.uppy-Dashboard-inner{height:500px;width:650px}}.uppy-Dashboard--modal .uppy-Dashboard-inner{z-index:1002}[data-uppy-theme=dark] .uppy-Dashboard-inner{background-color:#1f1f1f}.uppy-Dashboard--isDisabled .uppy-Dashboard-inner{cursor:not-allowed}.uppy-Dashboard-innerWrap{border-radius:5px;display:flex;flex-direction:column;height:100%;opacity:0;overflow:hidden;position:relative}.uppy-Dashboard--isInnerWrapVisible .uppy-Dashboard-innerWrap{opacity:1}.uppy-Dashboard--isDisabled .uppy-Dashboard-innerWrap{cursor:not-allowed;filter:grayscale(100%);opacity:.6;user-select:none}.uppy-Dashboard--isDisabled .uppy-ProviderIconBg{fill:#9f9f9f}.uppy-Dashboard--isDisabled [aria-disabled],.uppy-Dashboard--isDisabled [disabled]{cursor:not-allowed;pointer-events:none}.uppy-Dashboard--modal .uppy-Dashboard-inner{border:none;inset:35px 15px 15px;position:fixed}@media only screen and (min-width:820px){.uppy-Dashboard--modal .uppy-Dashboard-inner{box-shadow:0 5px 15px 4px #00000026;left:50%;right:auto;top:50%;transform:translate(-50%,-50%)}}.uppy-Dashboard-close{color:#ffffffe6;cursor:pointer;display:block;font-size:27px;inset-inline-end:-2px;position:absolute;top:-33px;z-index:1005}.uppy-Dashboard-close:focus{outline:none}.uppy-Dashboard-close::-moz-focus-inner{border:0}.uppy-Dashboard-close:focus{color:#6eabf2}@media only screen and (min-width:820px){.uppy-Dashboard-close{font-size:35px;inset-inline-end:-35px;top:-10px}}.uppy-Dashboard-serviceMsg{background-color:#fffbf7;border-bottom:1px solid #edd4b9;border-top:1px solid #edd4b9;font-size:12px;font-weight:500;line-height:1.3;padding:12px 0;position:relative;top:-1px;z-index:1004}.uppy-size--md .uppy-Dashboard-serviceMsg{font-size:14px;line-height:1.4}[data-uppy-theme=dark] .uppy-Dashboard-serviceMsg{background-color:#1f1f1f;border-bottom:1px solid #333;border-top:1px solid #333;color:#eaeaea}.uppy-Dashboard-serviceMsg-title{display:block;line-height:1;margin-bottom:4px;padding-left:42px}.uppy-Dashboard-serviceMsg-text{padding:0 15px}.uppy-Dashboard-serviceMsg-actionBtn{color:#1269cf;font-size:inherit;font-weight:inherit;vertical-align:initial}[data-uppy-theme=dark] .uppy-Dashboard-serviceMsg-actionBtn{color:#02baf2e6}.uppy-Dashboard-serviceMsg-icon{left:15px;position:absolute;top:10px}.uppy-Dashboard-AddFiles{align-items:center;display:flex;flex-direction:column;height:100%;justify-content:center;position:relative;text-align:center}[data-uppy-drag-drop-supported=true] .uppy-Dashboard-AddFiles{border:1px dashed #dfdfdf;border-radius:3px;height:calc(100% - 14px);margin:7px}.uppy-Dashboard-AddFilesPanel .uppy-Dashboard-AddFiles{border:none;height:calc(100% - 54px)}.uppy-Dashboard--modal .uppy-Dashboard-AddFiles{border-color:#cfcfcf}[data-uppy-theme=dark] .uppy-Dashboard-AddFiles{border-color:#757575}.uppy-Dashboard-AddFiles-info{display:none;margin-top:auto;padding-bottom:15px;padding-top:15px}.uppy-size--height-md .uppy-Dashboard-AddFiles-info{display:block}.uppy-size--md .uppy-Dashboard-AddFiles-info{bottom:25px;left:0;padding-bottom:0;padding-top:30px;position:absolute;right:0}[data-uppy-num-acquirers="0"] .uppy-Dashboard-AddFiles-info{margin-top:0}.uppy-Dashboard-browse{color:#1269cf;cursor:pointer}.uppy-Dashboard-browse:focus{outline:none}.uppy-Dashboard-browse::-moz-focus-inner{border:0}.uppy-Dashboard-browse:focus,.uppy-Dashboard-browse:hover{border-bottom:1px solid #1269cf}[data-uppy-theme=dark] .uppy-Dashboard-browse{color:#02baf2e6}[data-uppy-theme=dark] .uppy-Dashboard-browse:focus,[data-uppy-theme=dark] .uppy-Dashboard-browse:hover{border-bottom:1px solid #02baf2}.uppy-Dashboard-browseBtn{display:block;font-size:14px;font-weight:500;margin-bottom:5px;margin-top:8px;width:100%}.uppy-size--md .uppy-Dashboard-browseBtn{font-size:15px;margin:15px auto;padding:13px 44px;width:auto}.uppy-Dashboard-AddFiles-list{display:flex;flex:1;flex-direction:column;margin-top:2px;overflow-y:auto;padding:2px 0;width:100%;-webkit-overflow-scrolling:touch}.uppy-size--md .uppy-Dashboard-AddFiles-list{flex:none;flex-direction:row;flex-wrap:wrap;justify-content:center;margin-top:15px;max-width:600px;overflow-y:visible;padding-top:0}.uppy-DashboardTab{border-bottom:1px solid #eaeaea;text-align:center;width:100%}[data-uppy-theme=dark] .uppy-DashboardTab{border-bottom:1px solid #333}.uppy-size--md .uppy-DashboardTab{border-bottom:none;display:inline-block;margin-bottom:10px;width:auto}.uppy-DashboardTab-btn{align-items:center;-webkit-appearance:none;appearance:none;background-color:initial;color:#525252;cursor:pointer;flex-direction:row;height:100%;justify-content:left;padding:12px 15px;width:100%}.uppy-DashboardTab-btn:focus{outline:none}.uppy-size--md .uppy-DashboardTab-btn{border-radius:5px;flex-direction:column;margin-inline-end:1px;padding:10px 3px;width:86px}[data-uppy-theme=dark] .uppy-DashboardTab-btn{color:#eaeaea}.uppy-DashboardTab-btn::-moz-focus-inner{border:0}.uppy-DashboardTab-btn:hover{background-color:#e9ecef}[data-uppy-theme=dark] .uppy-DashboardTab-btn:hover{background-color:#333}.uppy-DashboardTab-btn:active,.uppy-DashboardTab-btn:focus{background-color:#dfe6f1}[data-uppy-theme=dark] .uppy-DashboardTab-btn:active,[data-uppy-theme=dark] .uppy-DashboardTab-btn:focus{background-color:#525252}.uppy-DashboardTab-btn svg{display:inline-block;max-height:100%;max-width:100%;overflow:hidden;transition:transform .15s ease-in-out;vertical-align:text-top}.uppy-DashboardTab-inner{align-items:center;background-color:#fff;border-radius:8px;box-shadow:0 1px 1px #0000001a,0 1px 2px #0000001a,0 2px 3px #00000005;display:flex;height:32px;justify-content:center;margin-inline-end:10px;width:32px}.uppy-size--md .uppy-DashboardTab-inner{margin-inline-end:0}[data-uppy-theme=dark] .uppy-DashboardTab-inner{background-color:#323232;box-shadow:0 1px 1px #0003,0 1px 2px #0003,0 2px 3px #00000014}.uppy-DashboardTab-name{font-size:14px;font-weight:400}.uppy-size--md .uppy-DashboardTab-name{font-size:12px;line-height:15px;margin-bottom:0;margin-top:8px}.uppy-DashboardTab-iconMyDevice{color:#1269cf}[data-uppy-theme=dark] .uppy-DashboardTab-iconMyDevice{color:#02baf2}.uppy-DashboardTab-iconBox{color:#0061d5}[data-uppy-theme=dark] .uppy-DashboardTab-iconBox{color:#eaeaea}.uppy-DashboardTab-iconDropbox{color:#0061fe}[data-uppy-theme=dark] .uppy-DashboardTab-iconDropbox{color:#eaeaea}.uppy-DashboardTab-iconUnsplash{color:#111}[data-uppy-theme=dark] .uppy-DashboardTab-iconUnsplash{color:#eaeaea}.uppy-DashboardTab-iconWebdav{color:#111}[data-uppy-theme=dark] .uppy-DashboardTab-iconWebdav{color:#eaeaea}.uppy-DashboardTab-iconScreenRec{color:#2c3e50}[data-uppy-theme=dark] .uppy-DashboardTab-iconScreenRec{color:#eaeaea}.uppy-DashboardTab-iconAudio{color:#8030a3}[data-uppy-theme=dark] .uppy-DashboardTab-iconAudio{color:#bf6ee3}.uppy-Dashboard-input{height:.1px;opacity:0;overflow:hidden;position:absolute;width:.1px;z-index:-1}.uppy-DashboardContent-bar{align-items:center;background-color:#fafafa;border-bottom:1px solid #eaeaea;display:flex;flex-shrink:0;height:40px;justify-content:space-between;padding:0 10px;position:relative;width:100%;z-index:1004}.uppy-size--md .uppy-DashboardContent-bar{height:50px;padding:0 15px}[data-uppy-theme=dark] .uppy-DashboardContent-bar{background-color:#1f1f1f;border-bottom:1px solid #333}.uppy-DashboardContent-title{font-size:12px;font-weight:500;left:0;line-height:40px;margin:auto;max-width:170px;overflow-x:hidden;position:absolute;right:0;text-align:center;text-overflow:ellipsis;top:0;white-space:nowrap;width:100%}.uppy-size--md .uppy-DashboardContent-title{font-size:14px;line-height:50px;max-width:300px}[data-uppy-theme=dark] .uppy-DashboardContent-title{color:#eaeaea}.uppy-DashboardContent-back,.uppy-DashboardContent-save{-webkit-appearance:none;background:none;border:0;border-radius:3px;color:inherit;color:#1269cf;cursor:pointer;font-family:inherit;font-size:inherit;font-size:12px;font-weight:400;line-height:1;margin:0;margin-inline-start:-6px;padding:7px 6px}.uppy-DashboardContent-back:focus,.uppy-DashboardContent-save:focus{outline:none}.uppy-DashboardContent-back::-moz-focus-inner,.uppy-DashboardContent-save::-moz-focus-inner{border:0}.uppy-DashboardContent-back:hover,.uppy-DashboardContent-save:hover{color:#0e51a0}.uppy-DashboardContent-back:focus,.uppy-DashboardContent-save:focus{background-color:#dfe6f1}[data-uppy-theme=dark] .uppy-DashboardContent-back:focus,[data-uppy-theme=dark] .uppy-DashboardContent-save:focus{background-color:#333}.uppy-size--md .uppy-DashboardContent-back,.uppy-size--md .uppy-DashboardContent-save{font-size:14px}[data-uppy-theme=dark] .uppy-DashboardContent-back,[data-uppy-theme=dark] .uppy-DashboardContent-save{color:#02baf2}.uppy-DashboardContent-addMore{-webkit-appearance:none;background:none;border:0;border-radius:3px;color:inherit;color:#1269cf;cursor:pointer;font-family:inherit;font-size:inherit;font-weight:500;height:29px;line-height:1;margin:0;margin-inline-end:-5px;padding:7px 8px;width:29px}.uppy-DashboardContent-addMore:focus{outline:none}.uppy-DashboardContent-addMore::-moz-focus-inner{border:0}.uppy-DashboardContent-addMore:hover{color:#0e51a0}.uppy-DashboardContent-addMore:focus{background-color:#dfe6f1}[data-uppy-theme=dark] .uppy-DashboardContent-addMore:focus{background-color:#333}.uppy-size--md .uppy-DashboardContent-addMore{font-size:14px;height:auto;margin-inline-end:-8px;width:auto}[data-uppy-theme=dark] .uppy-DashboardContent-addMore{color:#02baf2}.uppy-DashboardContent-addMore svg{margin-inline-end:4px;vertical-align:initial}.uppy-size--md .uppy-DashboardContent-addMore svg{height:11px;width:11px}.uppy-DashboardContent-addMoreCaption{display:none}.uppy-size--md .uppy-DashboardContent-addMoreCaption{display:inline}.uppy-DashboardContent-panel{background-color:#f5f5f5;flex:1}.uppy-Dashboard-AddFilesPanel,.uppy-DashboardContent-panel{border-radius:5px;display:flex;flex-direction:column;inset:0;overflow:hidden;position:absolute;z-index:1005}.uppy-Dashboard-AddFilesPanel{background:#fafafa;background:linear-gradient(0deg,#fafafa 35%,#fafafad9);box-shadow:0 0 10px 5px #00000026}[data-uppy-theme=dark] .uppy-Dashboard-AddFilesPanel{background-color:#333;background-image:linear-gradient(0deg,#1f1f1f 35%,#1f1f1fd9)}.uppy-Dashboard--isAddFilesPanelVisible .uppy-Dashboard-files{filter:blur(2px)}.uppy-Dashboard-progress{bottom:0;height:12%;left:0;position:absolute;width:100%}.uppy-Dashboard-progressBarContainer.is-active{height:100%;left:0;position:absolute;top:0;width:100%;z-index:1004}.uppy-Dashboard-filesContainer{flex:1;margin:0;overflow-y:hidden;position:relative}.uppy-Dashboard-filesContainer:after{clear:both;content:"";display:table}.uppy-Dashboard-files{flex:1;margin:0;overflow-y:auto;padding:0 0 10px;-webkit-overflow-scrolling:touch}.uppy-size--md .uppy-Dashboard-files{padding-top:10px}.uppy-Dashboard--singleFile .uppy-Dashboard-filesInner{align-items:center;display:flex;height:100%;justify-content:center}.uppy-Dashboard-dropFilesHereHint{align-items:center;background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='48' height='48'%3E%3Cpath fill='%231269CF' d='M24 1v1C11.85 2 2 11.85 2 24s9.85 22 22 22 22-9.85 22-22S36.15 2 24 2zm0 0V0c13.254 0 24 10.746 24 24S37.254 48 24 48 0 37.254 0 24 10.746 0 24 0zm7.707 19.293a.999.999 0 1 1-1.414 1.414L25 16.414V34a1 1 0 1 1-2 0V16.414l-5.293 5.293a.999.999 0 1 1-1.414-1.414l7-7a1 1 0 0 1 1.414 0z'/%3E%3C/svg%3E");background-position:50% 50%;background-repeat:no-repeat;border:1px dashed #1269cf;border-radius:3px;color:#757575;display:flex;font-size:16px;justify-content:center;inset:7px;padding-top:90px;position:absolute;text-align:center;visibility:hidden;z-index:2000}[data-uppy-theme=dark] .uppy-Dashboard-dropFilesHereHint{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='48' height='48'%3E%3Cpath fill='%2302BAF2' d='M24 1v1C11.85 2 2 11.85 2 24s9.85 22 22 22 22-9.85 22-22S36.15 2 24 2zm0 0V0c13.254 0 24 10.746 24 24S37.254 48 24 48 0 37.254 0 24 10.746 0 24 0zm7.707 19.293a.999.999 0 1 1-1.414 1.414L25 16.414V34a1 1 0 1 1-2 0V16.414l-5.293 5.293a.999.999 0 1 1-1.414-1.414l7-7a1 1 0 0 1 1.414 0z'/%3E%3C/svg%3E");border-color:#02baf2;color:#bbb}.uppy-Dashboard.uppy-Dashboard--isDraggingOver .uppy-Dashboard-dropFilesHereHint{pointer-events:none;visibility:visible}.uppy-Dashboard.uppy-Dashboard--isDraggingOver .uppy-Dashboard-files,.uppy-Dashboard.uppy-Dashboard--isDraggingOver .uppy-Dashboard-progressindicators,.uppy-Dashboard.uppy-Dashboard--isDraggingOver .uppy-Dashboard-serviceMsg,.uppy-Dashboard.uppy-Dashboard--isDraggingOver .uppy-DashboardContent-bar{opacity:.15}.uppy-Dashboard.uppy-Dashboard--isDraggingOver .uppy-Dashboard-AddFiles{opacity:.03}.uppy-Dashboard-AddFiles-title{color:#000;font-size:17px;font-weight:500;line-height:1.35;margin-bottom:5px;margin-top:15px;padding:0 15px;text-align:inline-start;width:100%}.uppy-size--md .uppy-Dashboard-AddFiles-title{font-size:21px;font-weight:400;margin-top:5px;max-width:480px;padding:0 35px;text-align:center}[data-uppy-num-acquirers="0"] .uppy-Dashboard-AddFiles-title{text-align:center}[data-uppy-theme=dark] .uppy-Dashboard-AddFiles-title{color:#eaeaea}.uppy-Dashboard-AddFiles-title button{font-weight:500}.uppy-size--md .uppy-Dashboard-AddFiles-title button{font-weight:400}.uppy-Dashboard-note{color:#757575;font-size:14px;line-height:1.25;margin:auto;max-width:350px;padding:0 15px;text-align:center}.uppy-size--md .uppy-Dashboard-note{line-height:1.35;max-width:600px}[data-uppy-theme=dark] .uppy-Dashboard-note{color:#cfcfcf}a.uppy-Dashboard-poweredBy{color:#939393;display:inline-block;font-size:11px;margin-top:8px;text-align:center;text-decoration:none}.uppy-Dashboard-poweredByIcon{margin-left:1px;margin-right:1px;opacity:.9;position:relative;top:1px;vertical-align:text-top;fill:none;stroke:#939393}.uppy-Dashboard-Item-previewIcon{height:25px;left:50%;position:absolute;top:50%;transform:translate(-50%,-50%);width:25px;z-index:100}.uppy-size--md .uppy-Dashboard-Item-previewIcon{height:38px;width:38px}.uppy-Dashboard-Item-previewIcon svg{height:100%;width:100%}.uppy-Dashboard--singleFile .uppy-Dashboard-Item-previewIcon{height:100%;max-height:60%;max-width:60%;width:100%}.uppy-Dashboard-Item-previewIconWrap{height:76px;max-height:75%;position:relative}.uppy-Dashboard--singleFile .uppy-Dashboard-Item-previewIconWrap{height:100%;width:100%}.uppy-Dashboard-Item-previewIconBg{filter:drop-shadow(rgba(0,0,0,.1) 0 1px 1px);height:100%;width:100%}.uppy-Dashboard-upload{height:50px;position:relative;width:50px}.uppy-size--md .uppy-Dashboard-upload{height:60px;width:60px}.uppy-Dashboard-upload .uppy-c-icon{position:relative;top:1px;width:50%}.uppy-Dashboard-uploadCount{background-color:#1bb240;border-radius:50%;color:#fff;font-size:8px;height:16px;inset-inline-end:-12px;line-height:16px;position:absolute;top:-12px;width:16px}.uppy-size--md .uppy-Dashboard-uploadCount{font-size:9px;height:18px;line-height:18px;width:18px}:root{color-scheme:light dark;font-family:Inter,ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,Segoe UI,sans-serif;background:#08101f;color:#e8eefc}*{box-sizing:border-box}body{min-height:100vh;margin:0;background:radial-gradient(circle at top left,#1b3261 0,transparent 38rem),#08101f}.shell{width:min(60rem,calc(100% - 2rem));margin:0 auto;padding:3rem 0 5rem}header{margin-bottom:2rem}.eyebrow,.label{color:#96b6ff;font-size:.78rem;font-weight:700;letter-spacing:.08em;text-transform:uppercase}h1,h2,p{margin-top:0}h1{margin-bottom:.65rem;font-size:clamp(2rem,8vw,3.6rem);line-height:1}h2{font-size:1.15rem}.lede,.hint,#next-action{color:#b6c4df;line-height:1.55}.card{margin-top:1rem;padding:1.4rem;border:1px solid #2c3d60;border-radius:1rem;background:#101a2dbf;box-shadow:0 1rem 3rem #0004}.facts{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:.85rem 1.25rem;margin:1rem 0}.facts div{min-width:0}.facts dt{margin-bottom:.2rem;color:#8fa0bd;font-size:.8rem}.facts dd{margin:0;overflow-wrap:anywhere;font-weight:650}.compact{grid-template-columns:1fr}.section-heading{display:flex;align-items:center;justify-content:space-between;gap:1rem}.section-heading h2{margin-bottom:0}button{padding:.7rem .8rem;border:1px solid #49618d;border-radius:.55rem;color:#e8eefc;background:#182945;font-weight:750;cursor:pointer}button:hover{background:#233b62}button:focus-visible{outline:3px solid #96b6ff;outline-offset:2px}button:disabled{cursor:not-allowed;opacity:.55}#uppy-dashboard{margin-top:1rem}.session-files{display:grid;gap:.75rem;margin:1rem 0 0;padding:0;list-style:none}.session-file{display:grid;grid-template-columns:minmax(0,1fr) auto;gap:.5rem 1rem;padding:1rem;border:1px solid #2c3d60;border-radius:.75rem;background:#08101f99}.session-file strong,.session-file span{overflow-wrap:anywhere}.file-details{display:flex;flex-wrap:wrap;gap:.35rem .8rem;color:#b6c4df;font-size:.88rem}.file-state{color:#96b6ff;font-weight:700}.file-action{grid-column:1 / -1;margin:0;color:#b6c4df;font-size:.9rem;line-height:1.4}.empty-state{color:#8fa0bd}.state{min-height:1.5rem;margin:.8rem 0}.state.error{color:#ffaaa2}.state.success{color:#91e6bc}noscript{display:block;margin-top:1rem;color:#ffaaa2}@media(max-width:36rem){.shell{padding-top:1.5rem}.facts{grid-template-columns:1fr}.section-heading,.session-file{align-items:stretch;grid-template-columns:1fr}.section-heading{flex-direction:column}}@media(prefers-reduced-motion:reduce){*,*:before,*:after{scroll-behavior:auto!important}} diff --git a/src/vidxp/assets/upload_page/upload-page.js b/src/vidxp/assets/upload_page/upload-page.js index 4a42001..7742e9a 100644 --- a/src/vidxp/assets/upload_page/upload-page.js +++ b/src/vidxp/assets/upload_page/upload-page.js @@ -1,13 +1,13 @@ -var Jc=Object.create;var ho=Object.defineProperty;var Zc=Object.getOwnPropertyDescriptor;var Wc=Object.getOwnPropertyNames;var Yc=Object.getPrototypeOf,qc=Object.prototype.hasOwnProperty;var Da=r=>{throw TypeError(r)};var Kc=(r,e,t)=>e in r?ho(r,e,{enumerable:!0,configurable:!0,writable:!0,value:t}):r[e]=t;var W=(r,e)=>()=>{try{return e||r((e={exports:{}}).exports,e),e.exports}catch(t){throw e=0,t}};var $c=(r,e,t,i)=>{if(e&&typeof e=="object"||typeof e=="function")for(let s of Wc(e))!qc.call(r,s)&&s!==t&&ho(r,s,{get:()=>e[s],enumerable:!(i=Zc(e,s))||i.enumerable});return r};var H=(r,e,t)=>(t=r!=null?Jc(Yc(r)):{},$c(e||!r||!r.__esModule?ho(t,"default",{value:r,enumerable:!0}):t,r));var y=(r,e,t)=>Kc(r,typeof e!="symbol"?e+"":e,t),fo=(r,e,t)=>e.has(r)||Da("Cannot "+t);var h=(r,e,t)=>(fo(r,e,"read from private field"),t?t.call(r):e.get(r)),U=(r,e,t)=>e.has(r)?Da("Cannot add the same private member more than once"):e instanceof WeakSet?e.add(r):e.set(r,t),R=(r,e,t,i)=>(fo(r,e,"write to private field"),i?i.call(r,t):e.set(r,t),t),E=(r,e,t)=>(fo(r,e,"access private method"),t);var Fr=(r,e,t,i)=>({set _(s){R(r,e,s,t)},get _(){return h(r,e,i)}});var hs=W((Zb,cl)=>{"use strict";cl.exports=function(e){if(typeof e!="number"||Number.isNaN(e))throw new TypeError(`Expected a number, got ${typeof e}`);let t=e<0,i=Math.abs(e);if(t&&(i=-i),i===0)return"0 B";let s=["B","KB","MB","GB","TB","PB","EB","ZB","YB"],o=Math.min(Math.floor(Math.log(i)/Math.log(1024)),s.length-1),n=Number(i/1024**o),a=s[o];return`${n>=10||n%1===0?Math.round(n):n.toFixed(1)} ${a}`}});var ml=W((Wb,fl)=>{"use strict";function hl(r,e){this.text=r=r||"",this.hasWild=~r.indexOf("*"),this.separator=e,this.parts=r.split(e)}hl.prototype.match=function(r){var e=!0,t=this.parts,i,s=t.length,o;if(typeof r=="string"||r instanceof String)if(!this.hasWild&&this.text!=r)e=!1;else{for(o=(r||"").split(this.separator),i=0;e&&i{var Sh=ml(),wh=/[\/\+\.]/;yl.exports=function(r,e){function t(i){var s=Sh(i,r,wh);return s&&s.length>=2}return e?t(e.split(";")[0]):t}});var bs=W((ax,Bl)=>{function Lh(r){var e=typeof r;return r!=null&&(e=="object"||e=="function")}Bl.exports=Lh});var El=W((lx,vl)=>{var Nh=typeof global=="object"&&global&&global.Object===Object&&global;vl.exports=Nh});var Mo=W((px,Sl)=>{var kh=El(),Ch=typeof self=="object"&&self&&self.Object===Object&&self,_h=kh||Ch||Function("return this")();Sl.exports=_h});var Dl=W((ux,wl)=>{var Ph=Mo(),Th=function(){return Ph.Date.now()};wl.exports=Th});var Ll=W((dx,Rl)=>{var Ih=/\s/;function Vh(r){for(var e=r.length;e--&&Ih.test(r.charAt(e)););return e}Rl.exports=Vh});var kl=W((cx,Nl)=>{var Oh=Ll(),Ah=/^\s+/;function zh(r){return r&&r.slice(0,Oh(r)+1).replace(Ah,"")}Nl.exports=zh});var Go=W((hx,Cl)=>{var jh=Mo(),Mh=jh.Symbol;Cl.exports=Mh});var Il=W((fx,Tl)=>{var _l=Go(),Pl=Object.prototype,Gh=Pl.hasOwnProperty,Hh=Pl.toString,Gr=_l?_l.toStringTag:void 0;function Jh(r){var e=Gh.call(r,Gr),t=r[Gr];try{r[Gr]=void 0;var i=!0}catch{}var s=Hh.call(r);return i&&(e?r[Gr]=t:delete r[Gr]),s}Tl.exports=Jh});var Ol=W((mx,Vl)=>{var Zh=Object.prototype,Wh=Zh.toString;function Yh(r){return Wh.call(r)}Vl.exports=Yh});var Ml=W((yx,jl)=>{var Al=Go(),qh=Il(),Kh=Ol(),$h="[object Null]",Xh="[object Undefined]",zl=Al?Al.toStringTag:void 0;function ef(r){return r==null?r===void 0?Xh:$h:zl&&zl in Object(r)?qh(r):Kh(r)}jl.exports=ef});var Hl=W((gx,Gl)=>{function tf(r){return r!=null&&typeof r=="object"}Gl.exports=tf});var Zl=W((bx,Jl)=>{var rf=Ml(),sf=Hl(),of="[object Symbol]";function nf(r){return typeof r=="symbol"||sf(r)&&rf(r)==of}Jl.exports=nf});var Kl=W((xx,ql)=>{var af=kl(),Wl=bs(),lf=Zl(),Yl=NaN,pf=/^[-+]0x[0-9a-f]+$/i,uf=/^0b[01]+$/i,df=/^0o[0-7]+$/i,cf=parseInt;function hf(r){if(typeof r=="number")return r;if(lf(r))return Yl;if(Wl(r)){var e=typeof r.valueOf=="function"?r.valueOf():r;r=Wl(e)?e+"":e}if(typeof r!="string")return r===0?r:+r;r=af(r);var t=uf.test(r);return t||df.test(r)?cf(r.slice(2),t?2:8):pf.test(r)?Yl:+r}ql.exports=hf});var xs=W((Qx,Xl)=>{var ff=bs(),Ho=Dl(),$l=Kl(),mf="Expected a function",yf=Math.max,gf=Math.min;function bf(r,e,t){var i,s,o,n,a,p,u=0,d=!1,c=!1,f=!0;if(typeof r!="function")throw new TypeError(mf);e=$l(e)||0,ff(t)&&(d=!!t.leading,c="maxWait"in t,o=c?yf($l(t.maxWait)||0,e):o,f="trailing"in t?!!t.trailing:f);function x(N){var I=i,P=s;return i=s=void 0,u=N,n=r.apply(P,I),n}function Q(N){return u=N,a=setTimeout(k,e),d?x(N):n}function D(N){var I=N-p,P=N-u,Z=e-I;return c?gf(Z,o-P):Z}function L(N){var I=N-p,P=N-u;return p===void 0||I>=e||I<0||c&&P>=o}function k(){var N=Ho();if(L(N))return B(N);a=setTimeout(k,D(N))}function B(N){return a=void 0,f&&i?x(N):(i=s=void 0,n)}function S(){a!==void 0&&clearTimeout(a),u=0,i=p=s=a=void 0}function C(){return a===void 0?n:B(Ho())}function V(){var N=Ho(),I=L(N);if(i=arguments,s=this,p=N,I){if(a===void 0)return Q(p);if(c)return clearTimeout(a),a=setTimeout(k,e),x(p)}return a===void 0&&(a=setTimeout(k,e)),n}return V.cancel=S,V.flush=C,V}Xl.exports=bf});var Jo=W((Fx,ep)=>{var xf=xs(),Qf=bs(),Ff="Expected a function";function Uf(r,e,t){var i=!0,s=!0;if(typeof r!="function")throw new TypeError(Ff);return Qf(t)&&(i="leading"in t?!!t.leading:i,s="trailing"in t?!!t.trailing:s),xf(r,e,{leading:i,maxWait:e,trailing:s})}ep.exports=Uf});var rp=W((Ux,tp)=>{tp.exports=function(){var e={},t=e._fns={};e.emit=function(n,a,p,u,d,c,f){var x=i(n);x.length&&s(n,x,[a,p,u,d,c,f])},e.on=function(n,a){t[n]||(t[n]=[]),t[n].push(a)},e.once=function(n,a){function p(){a.apply(this,arguments),e.off(n,p)}this.on(n,p)},e.off=function(n,a){var p=[];if(n&&a){var u=this._fns[n],d=0,c=u?u.length:0;for(d;d{(function(){"use strict";var r={}.hasOwnProperty;function e(){for(var s="",o=0;o{"use strict";var jf=Object.prototype.hasOwnProperty,ce="~";function Yr(){}Object.create&&(Yr.prototype=Object.create(null),new Yr().__proto__||(ce=!1));function Mf(r,e,t){this.fn=r,this.context=e,this.once=t||!1}function Ep(r,e,t,i,s){if(typeof t!="function")throw new TypeError("The listener must be a function");var o=new Mf(t,i||r,s),n=ce?ce+e:e;return r._events[n]?r._events[n].fn?r._events[n]=[r._events[n],o]:r._events[n].push(o):(r._events[n]=o,r._eventsCount++),r}function Ss(r,e){--r._eventsCount===0?r._events=new Yr:delete r._events[e]}function le(){this._events=new Yr,this._eventsCount=0}le.prototype.eventNames=function(){var e=[],t,i;if(this._eventsCount===0)return e;for(i in t=this._events)jf.call(t,i)&&e.push(ce?i.slice(1):i);return Object.getOwnPropertySymbols?e.concat(Object.getOwnPropertySymbols(t)):e};le.prototype.listeners=function(e){var t=ce?ce+e:e,i=this._events[t];if(!i)return[];if(i.fn)return[i.fn];for(var s=0,o=i.length,n=new Array(o);s{"use strict";jd.exports=function(e,t){if(t=t.split(":")[0],e=+e,!e)return!1;switch(t){case"http":case"ws":return e!==80;case"https":case"wss":return e!==443;case"ftp":return e!==21;case"gopher":return e!==70;case"file":return!1}return e!==0}});var Jd=W(aa=>{"use strict";var O0=Object.prototype.hasOwnProperty,A0;function Gd(r){try{return decodeURIComponent(r.replace(/\+/g," "))}catch{return null}}function Hd(r){try{return encodeURIComponent(r)}catch{return null}}function z0(r){for(var e=/([^=?#&]+)=?([^&]*)/g,t={},i;i=e.exec(r);){var s=Gd(i[1]),o=Gd(i[2]);s===null||o===null||s in t||(t[s]=o)}return t}function j0(r,e){e=e||"";var t=[],i,s;typeof e!="string"&&(e="?");for(s in r)if(O0.call(r,s)){if(i=r[s],!i&&(i===null||i===A0||isNaN(i))&&(i=""),s=Hd(s),i=Hd(i),s===null||i===null)continue;t.push(s+"="+i)}return t.length?e+t.join("&"):""}aa.stringify=j0;aa.parse=z0});var ec=W((H1,Xd)=>{"use strict";var Wd=Md(),io=Jd(),M0=/^[\x00-\x20\u00a0\u1680\u2000-\u200a\u2028\u2029\u202f\u205f\u3000\ufeff]+/,Yd=/[\n\r\t]/g,G0=/^[A-Za-z][A-Za-z0-9+-.]*:\/\//,qd=/:\d+$/,H0=/^([a-z][a-z0-9.+-]*:)?(\/\/)?([\\/]+)?([\S\s]*)/i,J0=/^[a-zA-Z]:/;function pa(r){return(r||"").toString().replace(M0,"")}var la=[["#","hash"],["?","query"],function(e,t){return Me(t.protocol)?e.replace(/\\/g,"/"):e},["/","pathname"],["@","auth",1],[NaN,"host",void 0,1,1],[/:(\d*)$/,"port",void 0,1],[NaN,"hostname",void 0,1,1]],Zd={hash:1,query:1};function Kd(r){var e;typeof window<"u"?e=window:typeof global<"u"?e=global:typeof self<"u"?e=self:e={};var t=e.location||{};r=r||t;var i={},s=typeof r,o;if(r.protocol==="blob:")i=new Ge(unescape(r.pathname),{});else if(s==="string"){i=new Ge(r,{});for(o in Zd)delete i[o]}else if(s==="object"){for(o in r)o in Zd||(i[o]=r[o]);i.slashes===void 0&&(i.slashes=G0.test(r.href))}return i}function Me(r){return r==="file:"||r==="ftp:"||r==="http:"||r==="https:"||r==="ws:"||r==="wss:"}function $d(r,e){r=pa(r),r=r.replace(Yd,""),e=e||{};var t=H0.exec(r),i=t[1]?t[1].toLowerCase():"",s=!!t[2],o=!!t[3],n=0,a;return s?o?(a=t[2]+t[3]+t[4],n=t[2].length+t[3].length):(a=t[2]+t[4],n=t[2].length):o?(a=t[3]+t[4],n=t[3].length):a=t[4],i==="file:"?n>=2&&(a=a.slice(2)):Me(i)?a=t[4]:i?s&&(a=a.slice(2)):n>=2&&Me(e.protocol)&&(a=t[4]),{protocol:i,slashes:s||Me(i),slashesCount:n,rest:a}}function Z0(r,e){if(r==="")return e;for(var t=(e||"/").split("/").slice(0,-1).concat(r.split("/")),i=t.length,s=t[i-1],o=!1,n=0;i--;)t[i]==="."?t.splice(i,1):t[i]===".."?(t.splice(i,1),n++):n&&(i===0&&(o=!0),t.splice(i,1),n--);return o&&t.unshift(""),(s==="."||s==="..")&&t.push(""),t.join("/")}function Ge(r,e,t){if(r=pa(r),r=r.replace(Yd,""),!(this instanceof Ge))return new Ge(r,e,t);var i,s,o,n,a,p,u=la.slice(),d=typeof e,c=this,f=0;for(d!=="object"&&d!=="string"&&(t=e,e=null),t&&typeof t!="function"&&(t=io.parse),e=Kd(e),s=$d(r||"",e),i=!s.protocol&&!s.slashes,c.slashes=s.slashes||i&&e.slashes,c.protocol=s.protocol||e.protocol||"",r=s.rest,(s.protocol==="file:"&&(s.slashesCount!==2||J0.test(r))||!s.slashes&&(s.protocol||s.slashesCount<2||!Me(c.protocol)))&&(u[3]=[/(.*)/,"pathname"]);f"error"in r&&!!r.error,rh=r=>r.progress.uploadComplete;function bo(r){return r.filter(e=>!th(e)&&!rh(e))}function xo(r){return r.filter(e=>!e.progress?.uploadStarted||!e.isRestored)}function vr(r){return typeof r!="object"||r===null||!("nodeType"in r)?!1:r.nodeType===Node.ELEMENT_NODE}function ih(r){if(typeof r=="string"){let e=document.querySelectorAll(r);return e.length===0?null:Array.from(e)}return typeof r=="object"&&vr(r)?[r]:null}var $i=ih;function sh(r,e=document){return typeof r=="string"?e.querySelector(r):vr(r)?r:null}var Qo=sh;function $e(r){let e=r.lastIndexOf(".");return e===-1||e===r.length-1?{name:r,extension:void 0}:{name:r.slice(0,e),extension:r.slice(e+1)}}var Fo={__proto__:null,md:"text/markdown",markdown:"text/markdown",mp4:"video/mp4",mp3:"audio/mp3",svg:"image/svg+xml",jpg:"image/jpeg",png:"image/png",webp:"image/webp",gif:"image/gif",heic:"image/heic",heif:"image/heif",yaml:"text/yaml",yml:"text/yaml",csv:"text/csv",tsv:"text/tab-separated-values",tab:"text/tab-separated-values",avi:"video/x-msvideo",mks:"video/x-matroska",mkv:"video/x-matroska",mov:"video/quicktime",dicom:"application/dicom",doc:"application/msword",msg:"application/vnd.ms-outlook",docm:"application/vnd.ms-word.document.macroenabled.12",docx:"application/vnd.openxmlformats-officedocument.wordprocessingml.document",dot:"application/msword",dotm:"application/vnd.ms-word.template.macroenabled.12",dotx:"application/vnd.openxmlformats-officedocument.wordprocessingml.template",xla:"application/vnd.ms-excel",xlam:"application/vnd.ms-excel.addin.macroenabled.12",xlc:"application/vnd.ms-excel",xlf:"application/x-xliff+xml",xlm:"application/vnd.ms-excel",xls:"application/vnd.ms-excel",xlsb:"application/vnd.ms-excel.sheet.binary.macroenabled.12",xlsm:"application/vnd.ms-excel.sheet.macroenabled.12",xlsx:"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",xlt:"application/vnd.ms-excel",xltm:"application/vnd.ms-excel.template.macroenabled.12",xltx:"application/vnd.openxmlformats-officedocument.spreadsheetml.template",xlw:"application/vnd.ms-excel",txt:"text/plain",text:"text/plain",conf:"text/plain",log:"text/plain",pdf:"application/pdf",zip:"application/zip","7z":"application/x-7z-compressed",rar:"application/x-rar-compressed",tar:"application/x-tar",gz:"application/gzip",dmg:"application/x-apple-diskimage"};function Vt(r){if(r.type)return r.type;let e=r.name?$e(r.name).extension?.toLowerCase():null;return e&&e in Fo?Fo[e]:"application/octet-stream"}function oh(r){return r.charCodeAt(0).toString(32)}function Ra(r){let e="";return r.replace(/[^A-Z0-9]/gi,t=>(e+=`-${oh(t)}`,"/"))+e}function Uo(r,e){let t=e||"uppy";return typeof r.name=="string"&&(t+=`-${Ra(r.name.toLowerCase())}`),r.type!==void 0&&(t+=`-${r.type}`),r.meta&&typeof r.meta.relativePath=="string"&&(t+=`-${Ra(r.meta.relativePath.toLowerCase())}`),r.data?.size!==void 0&&(t+=`-${r.data.size}`),r.data.lastModified!==void 0&&(t+=`-${r.data.lastModified}`),t}function nh(r){return!r.isRemote||!r.remote?!1:new Set(["box","dropbox","drive","facebook","unsplash"]).has(r.remote.provider)}function Er(r,e){if(nh(r))return r.id;let t=Vt(r);return Uo({...r,type:t},e)}function Xi(r,e){return r===!0?Object.keys(e):Array.isArray(r)?r:[]}var Je=Array.from;function Bo(r){let e=Je(r.files);return Promise.resolve(e)}function es(r,e,t,{onSuccess:i}){r.readEntries(s=>{let o=[...e,...s];s.length?queueMicrotask(()=>{es(r,o,t,{onSuccess:i})}):i(o)},s=>{t(s),i(e)})}function La(r,e){return r==null?r:{kind:r.isFile?"file":r.isDirectory?"directory":void 0,name:r.name,getFile(){return new Promise((t,i)=>r.file(t,i))},async*values(){let t=r.createReader();yield*await new Promise(s=>{es(t,[],e,{onSuccess:o=>s(o.map(n=>La(n,e)))})})},isSameEntry:void 0}}async function*Na(r,e,t=void 0){let i=()=>`${e}/${r.name}`;if(r.kind==="file"){let s=await r.getFile();s!=null?(s.relativePath=e?i():null,yield s):t!=null&&(yield t)}else if(r.kind==="directory")for await(let s of r.values())yield*Na(s,e?i():r.name);else t!=null&&(yield t)}async function*vo(r,e){let t=await Promise.all(Array.from(r.items,async i=>{let s;return s??(s=La(typeof i.getAsEntry=="function"?i.getAsEntry():i.webkitGetAsEntry(),e)),{fileSystemHandle:s,lastResortFile:i.getAsFile()}}));for(let{lastResortFile:i,fileSystemHandle:s}of t)if(s!=null)try{yield*Na(s,"",i)}catch(o){i!=null?yield i:e(o)}else i!=null&&(yield i)}async function ts(r,e){let t=e?.logDropError??Function.prototype;try{let i=[];for await(let s of vo(r,t))i.push(s);return i}catch{return Bo(r)}}function ah(r){for(;r&&!r.dir;)r=r.parentNode;return r?.dir}var Eo=ah;function So(r){return r<10?`0${r}`:r.toString()}function ht(){let r=new Date,e=So(r.getHours()),t=So(r.getMinutes()),i=So(r.getSeconds());return`${e}:${t}:${i}`}function rs(){if(typeof window>"u")return!1;let r=document.body;return!(r==null||window==null||!("draggable"in r)||!("ondragstart"in r)||!("ondrop"in r)||!("FormData"in window)||!("FileReader"in window))}function lh(r){return r?r.readyState===4&&r.status===0:!1}var wo=lh;function Sr(r){return r.startsWith("blob:")}function wr(r){return r?/^[^/]+\/(jpe?g|gif|png|svg|svg\+xml|bmp|webp|avif)$/.test(r):!1}function Do(r){let e=Math.floor(r/3600)%24,t=Math.floor(r/60)%60,i=Math.floor(r%60);return{hours:e,minutes:t,seconds:i}}function is(r){let e=Do(r),t=e.hours===0?"":`${e.hours}h`,i=e.minutes===0?"":`${e.hours===0?e.minutes:` ${e.minutes.toString(10).padStart(2,"0")}`}m`,s=e.hours!==0?"":`${e.minutes===0?e.seconds:` ${e.seconds.toString(10).padStart(2,"0")}`}s`;return`${t}${i}${s}`}function ph(r){return new Error("Cancelled",{cause:r})}function ka(r){if(r!=null){let e=()=>this.abort(r.reason);r.addEventListener("abort",e,{once:!0});let t=()=>{r.removeEventListener("abort",e)};this.then?.(t,t)}return this}var Ze,Ce,We,Ot,_e,At,ft,ne,Ro,Dr,Ca,_a,Pa,ss,zt,Rr=class{constructor(e){U(this,ne);U(this,Ze,0);U(this,Ce,[]);U(this,We,!1);U(this,Ot);U(this,_e,1);U(this,At);U(this,ft);y(this,"limit");U(this,ss,()=>this.resume());U(this,zt,()=>{if(h(this,We)){R(this,ft,setTimeout(h(this,zt),0));return}R(this,_e,this.limit),this.limit=Math.ceil((h(this,At)+h(this,_e))/2);for(let e=h(this,_e);e<=this.limit;e++)E(this,ne,Dr).call(this);h(this,At)-h(this,_e)>3?R(this,ft,setTimeout(h(this,zt),2e3)):R(this,_e,Math.floor(h(this,_e)/2))});typeof e!="number"||e===0?this.limit=1/0:this.limit=e}run(e,t){return!h(this,We)&&h(this,Ze){let s=this.run(()=>(e(...i),queueMicrotask(()=>s.done()),()=>{}),t);return{abortOn:ka,abort(){s.abort()}}}}wrapPromiseFunction(e,t){return(...i)=>{let s,o=new Promise((n,a)=>{s=this.run(()=>{let p,u;try{u=Promise.resolve(e(...i))}catch(d){u=Promise.reject(d)}return u.then(d=>{p?a(p):(s.done(),n(d))},d=>{p?a(p):(s.done(),a(d))}),d=>{p=ph(d)}},t)});return o.abort=n=>{s.abort(n)},o.abortOn=ka,o}}resume(){R(this,We,!1),clearTimeout(h(this,Ot));for(let e=0;e1&&Number.isFinite(this.limit)&&(R(this,At,this.limit-1),this.limit=h(this,_e),R(this,ft,setTimeout(h(this,zt),e)))}get isPaused(){return h(this,We)}};Ze=new WeakMap,Ce=new WeakMap,We=new WeakMap,Ot=new WeakMap,_e=new WeakMap,At=new WeakMap,ft=new WeakMap,ne=new WeakSet,Ro=function(e){R(this,Ze,h(this,Ze)+1);let t=!1,i;try{i=e()}catch(s){throw R(this,Ze,h(this,Ze)-1),s}return{abort:s=>{t||(t=!0,R(this,Ze,h(this,Ze)-1),i?.(s),E(this,ne,Dr).call(this))},done:()=>{t||(t=!0,R(this,Ze,h(this,Ze)-1),E(this,ne,Dr).call(this))}}},Dr=function(){queueMicrotask(()=>E(this,ne,Ca).call(this))},Ca=function(){if(h(this,We)||h(this,Ze)>=this.limit||h(this,Ce).length===0)return;let e=h(this,Ce).shift();if(e==null)throw new Error("Invariant violation: next is null");let t=E(this,ne,Ro).call(this,e.fn);e.abort=t.abort,e.done=t.done},_a=function(e,t){let i={fn:e,priority:t?.priority||0,abort:()=>{E(this,ne,Pa).call(this,i)},done:()=>{throw new Error("Cannot mark a queued request as done: this indicates a bug")}},s=h(this,Ce).findIndex(o=>i.priority>o.priority);return s===-1?h(this,Ce).push(i):h(this,Ce).splice(s,0,i),i},Pa=function(e){let t=h(this,Ce).indexOf(e);t!==-1&&h(this,Ce).splice(t,1)},ss=new WeakMap,zt=new WeakMap;function os(r){return{...r,type:r.mimeType,extension:r.name?$e(r.name).extension:null}}function uh(r,e,t){let i=[];return r.forEach(s=>typeof s!="string"?i.push(s):e[Symbol.split](s).forEach((o,n,a)=>{o!==""&&i.push(o),n{throw new Error(`missing string: ${r}`)},Lr,Nr,Lo,Xe=class{constructor(e,{onMissingKey:t=dh}={}){U(this,Nr);y(this,"locale");U(this,Lr);this.locale={strings:{},pluralize(i){return i===1?0:1}},Array.isArray(e)?e.forEach(E(this,Nr,Lo),this):E(this,Nr,Lo).call(this,e),R(this,Lr,t)}translate(e,t){return this.translateArray(e,t).join("")}translateArray(e,t){let i=this.locale.strings[e];if(i==null&&(h(this,Lr).call(this,e),i=e),typeof i=="object"){if(t&&typeof t.smart_count<"u"){let o=this.locale.pluralize(t.smart_count);return Ta(i[o],t)}throw new Error("Attempted to use a string with plural forms, but no value was given for %{smart_count}")}if(typeof i!="string")throw new Error("string was not a string");return Ta(i,t)}};Lr=new WeakMap,Nr=new WeakSet,Lo=function(e){if(!e?.strings)return;let t=this.locale;Object.assign(this.locale,{strings:{...t.strings,...e.strings},pluralize:e.pluralize||t.pluralize})};function kr(r,e){if(e===0)return"";if(r.length<=e)return r;if(e<=4)return`${r.slice(0,e-1)}…`;let t=e-3,i=Math.ceil(t/2),s=Math.floor(t/2);return r.slice(0,i)+"..."+r.slice(-s)}var Pr,M,za,ch,et,Ia,ja,Ma,No,as,Cr,Ga,_o,ko,Co,hh,ls={},ps=[],fh=/acit|ex(?:s|g|n|p|$)|rph|grid|ows|mnc|ntw|ine[ch]|zoo|^ord|itera/i,Tr=Array.isArray;function Pe(r,e){for(var t in e)r[t]=e[t];return r}function Po(r){r&&r.parentNode&&r.parentNode.removeChild(r)}function Ir(r,e,t){var i,s,o,n={};for(o in e)o=="key"?i=e[o]:o=="ref"?s=e[o]:n[o]=e[o];if(arguments.length>2&&(n.children=arguments.length>3?Pr.call(arguments,2):t),typeof r=="function"&&r.defaultProps!=null)for(o in r.defaultProps)n[o]===void 0&&(n[o]=r.defaultProps[o]);return _r(r,n,i,s,null)}function _r(r,e,t,i,s){var o={type:r,props:e,key:t,ref:i,__k:null,__:null,__b:0,__e:null,__c:null,constructor:void 0,__v:s??++za,__i:-1,__u:0};return s==null&&M.vnode!=null&&M.vnode(o),o}function Ha(){return{current:null}}function te(r){return r.children}function ae(r,e){this.props=r,this.context=e}function jt(r,e){if(e==null)return r.__?jt(r.__,r.__i+1):null;for(var t;ee&&et.sort(Ma),r=et.shift(),e=et.length,mh(r)}finally{et.length=us.__r=0}}function Za(r,e,t,i,s,o,n,a,p,u,d){var c,f,x,Q,D,L,k,B=i&&i.__k||ps,S=e.length;for(p=yh(t,e,B,p,S),c=0;c0?n=r.__k[o]=_r(n.type,n.props,n.key,n.ref?n.ref:null,n.__v):r.__k[o]=n,p=o+f,n.__=r,n.__b=r.__b+1,a=null,(u=n.__i=gh(n,t,p,c))!=-1&&(c--,(a=t[u])&&(a.__u|=2)),a==null||a.__v==null?(u==-1&&(s>d?f--:sp?f--:f++,n.__u|=4))):r.__k[o]=null;if(c)for(o=0;o(d?1:0)){for(s=t-1,o=t+1;s>=0||o=0?s--:o++])!=null&&(2&u.__u)==0&&a==u.key&&p==u.type)return n}return-1}function Oa(r,e,t){e[0]=="-"?r.setProperty(e,t??""):r[e]=t==null?"":typeof t!="number"||fh.test(e)?t:t+"px"}function ns(r,e,t,i,s){var o,n;e:if(e=="style")if(typeof t=="string")r.style.cssText=t;else{if(typeof i=="string"&&(r.style.cssText=i=""),i)for(e in i)t&&e in t||Oa(r.style,e,"");if(t)for(e in t)i&&t[e]==i[e]||Oa(r.style,e,t[e])}else if(e[0]=="o"&&e[1]=="n")o=e!=(e=e.replace(Ga,"$1")),n=e.toLowerCase(),e=n in r||e=="onFocusOut"||e=="onFocusIn"?n.slice(2):e.slice(2),r.l||(r.l={}),r.l[e+o]=t,t?i?t[Cr]=i[Cr]:(t[Cr]=_o,r.addEventListener(e,o?Co:ko,o)):r.removeEventListener(e,o?Co:ko,o);else{if(s=="http://www.w3.org/2000/svg")e=e.replace(/xlink(H|:h)/,"h").replace(/sName$/,"s");else if(e!="width"&&e!="height"&&e!="href"&&e!="list"&&e!="form"&&e!="tabIndex"&&e!="download"&&e!="rowSpan"&&e!="colSpan"&&e!="role"&&e!="popover"&&e in r)try{r[e]=t??"";break e}catch{}typeof t=="function"||(t==null||t===!1&&e[4]!="-"?r.removeAttribute(e):r.setAttribute(e,e=="popover"&&t==1?"":t))}}function Aa(r){return function(e){if(this.l){var t=this.l[e.type+r];if(e[as]==null)e[as]=_o++;else if(e[as]0?r:Tr(r)?r.map(Ka):r.constructor!==void 0?null:Pe({},r)}function bh(r,e,t,i,s,o,n,a,p){var u,d,c,f,x,Q,D,L=t.props||ls,k=e.props,B=e.type;if(B=="svg"?s="http://www.w3.org/2000/svg":B=="math"?s="http://www.w3.org/1998/Math/MathML":s||(s="http://www.w3.org/1999/xhtml"),o!=null){for(u=0;u2&&(a.children=arguments.length>3?Pr.call(arguments,2):t),_r(r.type,a,i||r.key,s||r.ref,null)}Pr=ps.slice,M={__e:function(r,e,t,i){for(var s,o,n;e=e.__;)if((s=e.__c)&&!s.__)try{if((o=s.constructor)&&o.getDerivedStateFromError!=null&&(s.setState(o.getDerivedStateFromError(r)),n=s.__d),s.componentDidCatch!=null&&(s.componentDidCatch(r,i||{}),n=s.__d),n)return s.__E=s}catch(a){r=a}throw r}},za=0,ch=function(r){return r!=null&&r.constructor===void 0},ae.prototype.setState=function(r,e){var t;t=this.__s!=null&&this.__s!=this.state?this.__s:this.__s=Pe({},this.state),typeof r=="function"&&(r=r(Pe({},t),this.props)),r&&Pe(t,r),r!=null&&this.__v&&(e&&this._sb.push(e),Va(this))},ae.prototype.forceUpdate=function(r){this.__v&&(this.__e=!0,r&&this.__h.push(r),Va(this))},ae.prototype.render=te,et=[],ja=typeof Promise=="function"?Promise.prototype.then.bind(Promise.resolve()):setTimeout,Ma=function(r,e){return r.__v.__b-e.__v.__b},us.__r=0,No=Math.random().toString(8),as="__d"+No,Cr="__a"+No,Ga=/(PointerCapture)$|Capture$/i,_o=0,ko=Aa(!1),Co=Aa(!0),hh=0;var Qh=0;function l(r,e,t,i,s,o){e||(e={});var n,a,p=e;if("ref"in p)for(a in p={},e)a=="ref"?n=e[a]:p[a]=e[a];var u={type:r,props:p,key:t,ref:n,__k:null,__:null,__b:0,__e:null,__c:null,constructor:void 0,__v:--Qh,__i:-1,__u:0,__source:s,__self:o};if(typeof r=="function"&&(n=r.defaultProps))for(a in n)p[a]===void 0&&(p[a]=n[a]);return M.vnode&&M.vnode(u),u}var Vr,q,Oo,Xa,Or=0,al=[],$=M,el=$.__b,tl=$.__r,rl=$.diffed,il=$.__c,sl=$.unmount,ol=$.__;function zo(r,e){$.__h&&$.__h(q,r,Or||e),Or=0;var t=q.__H||(q.__H={__:[],__h:[]});return r>=t.__.length&&t.__.push({}),t.__[r]}function ge(r){return Or=1,Fh(pl,r)}function Fh(r,e,t){var i=zo(Vr++,2);if(i.t=r,!i.__c&&(i.__=[t?t(e):pl(void 0,e),function(a){var p=i.__N?i.__N[0]:i.__[0],u=i.t(p,a);p!==u&&(i.__N=[u,i.__[1]],i.__c.setState({}))}],i.__c=q,!q.__f)){var s=function(a,p,u){if(!i.__c.__H)return!0;var d=!1,c=i.__c.props!==a;if(i.__c.__H.__.some(function(x){if(x.__N){d=!0;var Q=x.__[0];x.__=x.__N,x.__N=void 0,Q!==x.__[0]&&(c=!0)}}),o){var f=o.call(this,a,p,u);return d?f||c:f}return!d||c};q.__f=!0;var o=q.shouldComponentUpdate,n=q.componentWillUpdate;q.componentWillUpdate=function(a,p,u){if(this.__e){var d=o;o=void 0,s(a,p,u),o=d}n&&n.call(this,a,p,u)},q.shouldComponentUpdate=s}return i.__N||i.__}function we(r,e){var t=zo(Vr++,3);!$.__s&&ll(t.__H,e)&&(t.__=r,t.u=e,q.__H.__h.push(t))}function Ie(r){return Or=5,mt(function(){return{current:r}},[])}function mt(r,e){var t=zo(Vr++,7);return ll(t.__H,e)&&(t.__=r(),t.__H=e,t.__h=r),t.__}function De(r,e){return Or=8,mt(function(){return r},e)}function Uh(){for(var r;r=al.shift();){var e=r.__H;if(r.__P&&e)try{e.__h.some(cs),e.__h.some(Ao),e.__h=[]}catch(t){e.__h=[],$.__e(t,r.__v)}}}$.__b=function(r){q=null,el&&el(r)},$.__=function(r,e){r&&e.__k&&e.__k.__m&&(r.__m=e.__k.__m),ol&&ol(r,e)},$.__r=function(r){tl&&tl(r),Vr=0;var e=(q=r.__c).__H;e&&(Oo===q?(e.__h=[],q.__h=[],e.__.some(function(t){t.__N&&(t.__=t.__N),t.u=t.__N=void 0})):(e.__h.some(cs),e.__h.some(Ao),e.__h=[],Vr=0)),Oo=q},$.diffed=function(r){rl&&rl(r);var e=r.__c;e&&e.__H&&(e.__H.__h.length&&(al.push(e)!==1&&Xa===$.requestAnimationFrame||((Xa=$.requestAnimationFrame)||Bh)(Uh)),e.__H.__.some(function(t){t.u&&(t.__H=t.u,t.u=void 0)})),Oo=q=null},$.__c=function(r,e){e.some(function(t){try{t.__h.some(cs),t.__h=t.__h.filter(function(i){return!i.__||Ao(i)})}catch(i){e.some(function(s){s.__h&&(s.__h=[])}),e=[],$.__e(i,t.__v)}}),il&&il(r,e)},$.unmount=function(r){sl&&sl(r);var e,t=r.__c;t&&t.__H&&(t.__H.__.some(function(i){try{cs(i)}catch(s){e=s}}),t.__H=void 0,e&&$.__e(e,t.__v))};var nl=typeof requestAnimationFrame=="function";function Bh(r){var e,t=function(){clearTimeout(i),nl&&cancelAnimationFrame(e),setTimeout(r)},i=setTimeout(t,35);nl&&(e=requestAnimationFrame(t))}function cs(r){var e=q,t=r.__c;typeof t=="function"&&(r.__c=void 0,t()),q=e}function Ao(r){var e=q;r.__c=r.__(),q=e}function ll(r,e){return!r||r.length!==e.length||e.some(function(t,i){return t!==r[i]})}function pl(r,e){return typeof e=="function"?e(r):e}var vh={position:"relative",width:"100%",minHeight:"100%"},Eh={position:"absolute",top:0,left:0,width:"100%",overflow:"visible"};function Mt({data:r,rowHeight:e,renderRow:t,overscanCount:i=10,padding:s=4,...o}){let n=Ie(null),[a,p]=ge(0),[u,d]=ge(0);we(()=>{function B(){n.current!=null&&u!==n.current.offsetHeight&&d(n.current.offsetHeight)}return B(),window.addEventListener("resize",B),()=>{window.removeEventListener("resize",B)}},[u]);let c=De(()=>{n.current&&p(n.current.scrollTop)},[]),f=Math.floor(a/e),x=Math.floor(u/e);i&&(f=Math.max(0,f-f%i),x+=i);let Q=f+x+s,D=r.slice(f,Q),L={...vh,height:r.length*e},k={...Eh,top:f*e};return l("div",{onScroll:c,ref:n,...o,children:l("div",{role:"presentation",style:L,children:l("div",{role:"presentation",style:k,children:D.map(t)})})})}var Ve=class{constructor(e,t){y(this,"uppy");y(this,"opts");y(this,"id");y(this,"defaultLocale");y(this,"i18n");y(this,"i18nArray");y(this,"type");y(this,"VERSION");this.uppy=e,this.opts=t??{}}getPluginState(){let{plugins:e}=this.uppy.getState();return e?.[this.id]||{}}setPluginState(e){let{plugins:t}=this.uppy.getState();this.uppy.setState({plugins:{...t,[this.id]:{...t[this.id],...e}}})}setOptions(e){this.opts={...this.opts,...e},this.setPluginState(void 0),this.i18nInit()}i18nInit(){let e=new Xe([this.defaultLocale,this.uppy.locale,this.opts.locale]);this.i18n=e.translate.bind(e),this.i18nArray=e.translateArray.bind(e),this.setPluginState(void 0)}addTarget(e){throw new Error("Extend the addTarget method to add your plugin to another plugin's target")}install(){}uninstall(){}update(e){}afterUpdate(){}};var Oe,Ar,Gt=class{constructor(e){U(this,Oe);U(this,Ar,[]);R(this,Oe,e)}on(e,t){return h(this,Ar).push([e,t]),h(this,Oe).on(e,t)}remove(){for(let[e,t]of h(this,Ar).splice(0))h(this,Oe).off(e,t)}onFilePause(e,t){this.on("upload-pause",(i,s)=>{e===i?.id&&t(s)})}onFileRemove(e,t){this.on("file-removed",i=>{e===i.id&&t(i.id)})}onPause(e,t){this.on("upload-pause",(i,s)=>{e===i?.id&&t(s)})}onRetry(e,t){this.on("upload-retry",i=>{e===i?.id&&t()})}onRetryAll(e,t){this.on("retry-all",()=>{h(this,Oe).getFile(e)&&t()})}onPauseAll(e,t){this.on("pause-all",()=>{h(this,Oe).getFile(e)&&t()})}onCancelAll(e,t){this.on("cancel-all",(...i)=>{h(this,Oe).getFile(e)&&t(...i)})}onResumeAll(e,t){this.on("resume-all",()=>{h(this,Oe).getFile(e)&&t()})}};Oe=new WeakMap,Ar=new WeakMap;var ul={debug:()=>{},warn:()=>{},error:(...r)=>console.error(`[Uppy] [${ht()}]`,...r)},dl={debug:(...r)=>console.debug(`[Uppy] [${ht()}]`,...r),warn:(...r)=>console.warn(`[Uppy] [${ht()}]`,...r),error:(...r)=>console.error(`[Uppy] [${ht()}]`,...r)};var zr=H(hs(),1),bl=H(gl(),1);var xl={maxFileSize:null,minFileSize:null,maxTotalFileSize:null,maxNumberOfFiles:null,minNumberOfFiles:null,allowedFileTypes:null,requiredMetaFields:[]},me=class extends Error{constructor(t,i){super(t);y(this,"isUserFacing");y(this,"file");y(this,"isRestriction",!0);this.isUserFacing=i?.isUserFacing??!0,i?.file&&(this.file=i.file)}},fs=class{constructor(e,t){y(this,"getI18n");y(this,"getOpts");this.getI18n=t,this.getOpts=()=>{let i=e();if(i.restrictions?.allowedFileTypes!=null&&!Array.isArray(i.restrictions.allowedFileTypes))throw new TypeError("`restrictions.allowedFileTypes` must be an array");return i}}validateAggregateRestrictions(e,t){let{maxTotalFileSize:i,maxNumberOfFiles:s}=this.getOpts().restrictions;if(s&&e.filter(n=>!n.isGhost).length+t.length>s)throw new me(`${this.getI18n()("youCanOnlyUploadX",{smart_count:s})}`);if(i){let o=[...e,...t].reduce((n,a)=>n+(a.size??0),0);if(o>i)throw new me(this.getI18n()("aggregateExceedsSize",{sizeAllowed:(0,zr.default)(i),size:(0,zr.default)(o)}))}}validateSingleFile(e){let{maxFileSize:t,minFileSize:i,allowedFileTypes:s}=this.getOpts().restrictions;if(s&&!s.some(n=>n.includes("/")?e.type?(0,bl.default)(e.type.replace(/;.*?$/,""),n):!1:n[0]==="."&&e.extension?e.extension.toLowerCase()===n.slice(1).toLowerCase():!1)){let n=s.join(", ");throw new me(this.getI18n()("youCanOnlyUploadFileTypes",{types:n}),{file:e})}if(t&&e.size!=null&&e.size>t)throw new me(this.getI18n()("exceedsSize",{size:(0,zr.default)(t),file:e.name??this.getI18n()("unnamed")}),{file:e});if(i&&e.size!=null&&e.size{this.validateSingleFile(i)}),this.validateAggregateRestrictions(e,t)}validateMinNumberOfFiles(e){let{minNumberOfFiles:t}=this.getOpts().restrictions;if(t&&Object.keys(e).length(t=i,e||(e=Promise.resolve().then(()=>(e=null,r(...t)))),e)}var jr,ms=class ms extends Ve{constructor(){super(...arguments);U(this,jr);y(this,"isTargetDOMEl");y(this,"el");y(this,"parent");y(this,"title")}getTargetPlugin(t){let i;if(typeof t?.addTarget=="function")i=t,i instanceof ms||console.warn(new Error("The provided plugin is not an instance of UIPlugin. This is an indication of a bug with the way Uppy is bundled.",{cause:{targetPlugin:i,UIPlugin:ms}}));else if(typeof t=="function"){let s=t;this.uppy.iteratePlugins(o=>{o instanceof s&&(i=o)})}return i}mount(t,i){let s=i.id,o=Qo(t);if(o){this.isTargetDOMEl=!0;let p=document.createElement("div");return p.classList.add("uppy-Root"),R(this,jr,Dh(u=>{this.uppy.getPlugin(this.id)&&(Vo(this.render(u,p),p),this.afterUpdate())})),this.uppy.log(`Installing ${s} to a DOM element '${t}'`),this.opts.replaceTargetContent&&(o.innerHTML=""),Vo(this.render(this.uppy.getState(),p),p),this.el=p,o.appendChild(p),p.dir=this.opts.direction||Eo(p)||"ltr",this.onMount(),this.el}let n=this.getTargetPlugin(t);if(n)return this.uppy.log(`Installing ${s} to ${n.id}`),this.parent=n,this.el=n.addTarget(i),this.onMount(),this.el;this.uppy.log(`Not installing ${s}`);let a=`Invalid target option given to ${s}.`;throw typeof t=="function"?a+=" The given target is not a Plugin class. Please check that you're not specifying a React Component instead of a plugin. If you are using @uppy/* packages directly, make sure you have only 1 version of @uppy/core installed: run `npm ls @uppy/core` on the command line and verify that all the versions match and are deduped correctly.":a+="If you meant to target an HTML element, please make sure that the element exists. Check that the @@ -12,69 +12,45 @@

    VidXP media intake

    -

    Upload your video

    -

    Opening the secure upload handoff…

    +

    Upload your videos

    +

    Opening the secure upload session…

    - - -
    -

    Expected file

    +
    +
    +

    Upload session

    + +
    -
    Filename
    -
    Size
    -
    Maximum
    +
    State
    +
    Files
    +
    Per-file maximum
    +
    Session maximum
    +
    Reserved
    +
    Uploaded
    Expires

    - VidXP checks the selected name and size before transfer. After upload, - the server probes and validates the media before it becomes available. + The complete link authorizes this session. VidXP binds each selected + file and reserves quota before its bytes travel directly to tusd.

    -

    Choose and upload

    +

    Choose and upload videos

    - Waiting for a valid handoff. + Waiting for a valid upload session.

    -

    VidXP status

    -

    State

    +

    Per-file results

    The page will refresh the server-owned status automatically.

    -
    -
    Upload intent
    -
    Import job
    -
    Media ID
    -
    +
      - +
      From 1fb02b3f50bd3fcdc45b66f7233b9e7d9174d4aa Mon Sep 17 00:00:00 2001 From: Talha Date: Sun, 2 Aug 2026 00:27:43 +0500 Subject: [PATCH 06/21] feat(artifacts): add transport-aware clip delivery --- MANIFEST.in | 1 + README.md | 2 + compose.coolify.yaml | 3 + docs/architecture/platform.md | 31 +- docs/deployment/coolify.md | 17 + pyproject.toml | 1 + src/vidxp/api.py | 6 +- src/vidxp/api_middleware.py | 74 ++++- src/vidxp/api_models.py | 9 + src/vidxp/application_boundary.py | 22 +- src/vidxp/application_models.py | 23 ++ src/vidxp/artifact_delivery.py | 272 ++++++++++++++++ src/vidxp/artifact_download.py | 156 +++++++++ src/vidxp/artifact_service.py | 14 +- .../artifact_download/artifact-download.js | 31 ++ src/vidxp/assets/artifact_download/index.html | 15 + src/vidxp/capability_security.py | 15 + src/vidxp/mcp.py | 118 ++++++- src/vidxp/settings.py | 119 +++++-- src/vidxp/upload_service.py | 6 +- tests/fixtures/mcp_upload_server.py | 57 +++- tests/test_application_boundary.py | 18 ++ tests/test_artifact_download.py | 306 ++++++++++++++++++ tests/test_mcp.py | 282 ++++++++++++++++ tests/test_packaging.py | 27 ++ tests/test_settings.py | 42 +++ 26 files changed, 1581 insertions(+), 86 deletions(-) create mode 100644 src/vidxp/artifact_delivery.py create mode 100644 src/vidxp/artifact_download.py create mode 100644 src/vidxp/assets/artifact_download/artifact-download.js create mode 100644 src/vidxp/assets/artifact_download/index.html create mode 100644 src/vidxp/capability_security.py create mode 100644 tests/test_artifact_download.py diff --git a/MANIFEST.in b/MANIFEST.in index e74fc57..7454bce 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -2,6 +2,7 @@ include README.md include LICENSE include docs/images/logo.png recursive-include src/vidxp/assets/upload_page * +recursive-include src/vidxp/assets/artifact_download * include web/upload-page/package.json include web/upload-page/package-lock.json include web/upload-page/scripts/build.mjs diff --git a/README.md b/README.md index 0781a7f..35f66e3 100644 --- a/README.md +++ b/README.md @@ -131,6 +131,8 @@ See the [Coolify guide](docs/deployment/coolify.md) for the complete setup. - Group recurring faces in a video and render a highlighted actor overlay. - Search one selected video or every video in the active library. - Open matching timestamps and export downloadable clips and overlays. +- Retrieve completed clips through native MCP resources, local stdio paths, or + short-lived resumable HTTPS downloads without embedding video bytes in tool JSON. - Keep personal, client, or project libraries separate. - Follow long indexing jobs, cancel them, and keep the last working index if a later run fails. diff --git a/compose.coolify.yaml b/compose.coolify.yaml index 5bf7916..27f4b09 100644 --- a/compose.coolify.yaml +++ b/compose.coolify.yaml @@ -9,6 +9,9 @@ x-vidxp-environment: &vidxp-environment VIDXP_HTTP_STATIC_BEARER_TOKEN: ${VIDXP_HTTP_STATIC_BEARER_TOKEN:?Set a bearer token of at least 32 characters} VIDXP_HTTP_TRUSTED_HOSTS: '["127.0.0.1","localhost","${VIDXP_PUBLIC_API_HOST:?Set the public API host}"]' VIDXP_MCP_ALLOWED_HOSTS: '["127.0.0.1:*","localhost:*","${VIDXP_PUBLIC_API_HOST:?Set the public API host}"]' + VIDXP_ARTIFACT_DOWNLOAD_PUBLIC_URL: https://${VIDXP_PUBLIC_API_HOST:?Set the public API host}/artifact-download + VIDXP_ARTIFACT_DOWNLOAD_SECRET: ${VIDXP_ARTIFACT_DOWNLOAD_SECRET:?Set a dedicated artifact-download secret of at least 32 characters} + VIDXP_ARTIFACT_DOWNLOAD_TTL_SECONDS: ${VIDXP_ARTIFACT_DOWNLOAD_TTL_SECONDS:-900} VIDXP_UPLOAD_PUBLIC_ENDPOINT: ${VIDXP_UPLOAD_PUBLIC_ENDPOINT:?Set the public tusd endpoint ending in /uploads/} VIDXP_UPLOAD_INTERNAL_ENDPOINT: http://tusd:8080/uploads/ VIDXP_UPLOAD_CLEANUP_TOKEN: ${VIDXP_UPLOAD_CLEANUP_TOKEN:?Set a cleanup token of at least 32 characters} diff --git a/docs/architecture/platform.md b/docs/architecture/platform.md index c2ceb11..4c80719 100644 --- a/docs/architecture/platform.md +++ b/docs/architecture/platform.md @@ -838,11 +838,17 @@ remains available when it is disabled. - creation/expiry Media and artifacts are served from the managed local content volume through -protected Starlette `FileResponse`, including byte-range support. - -Every delivery request is handled only after repository authorization. Artifact -delivery resolves an `ArtifactStore` key beneath its configured root, rejects -symlink/path escapes, and only then constructs `FileResponse`. +Starlette `FileResponse`, including GET, HEAD, byte ranges, resumable delivery, +strong ETags, safe attachment filenames, and correct 206/416 responses. + +Ordinary REST delivery is handled after repository authorization. Public artifact +downloads instead require a short-lived bearer capability bound to artifact ID, +repository, media type/extension, digest, expiry, and versioned purpose. The +fragment capability is exchanged on the existing API listener for a secure +short-lived cookie; neither the fragment nor a server path enters a request URL. +Both routes resolve the authoritative `ArtifactStore` key beneath its configured +root, reject symlink/path escapes, verify size/digest, and only then construct the +same `FileResponse`. Local source playback applies the identical configured-root and symlink/path-escape checks to the `MediaStore` key before constructing `FileResponse`. @@ -857,8 +863,15 @@ written to a temporary key, validated, and atomically published. Actor overlays and other rendered media use the same artifact workflow. Public rendering commands never accept output paths. The separate CLI artifact-download command may copy an already-authorized managed artifact to an explicit user -destination; API downloads remain protected responses and MCP returns a lazy -resource link instead of embedding video bytes in the tool result. +destination. MCP always returns a lazy native resource link instead of embedding +video bytes in JSON or text. Ordinary local stdio additionally returns the +verified absolute artifact path and encoded `file://` URI because the agent and +VidXP share a filesystem; `VIDXP_MCP_STDIO_FILESYSTEM_ACCESSIBLE=false` disables +that hint for Docker, WSL, SSH-proxy, or other isolated stdio deployments. Remote +Streamable HTTP never returns a server path and instead returns the expiring HTTPS +capability URL. Filesystem-isolated stdio can use that same HTTPS mode when a +public artifact-download origin is configured, otherwise the MCP resource remains +the portable fallback. ## 18. FastAPI adapter @@ -1306,7 +1319,9 @@ Capability discovery tests: - deterministic behavior on supported Python 3.11 through 3.14 Managed artifact delivery tests cover authorization, path/symlink containment, -missing or corrupt content, GET/HEAD/range headers, and MIME/disposition metadata. +missing or corrupt content, stdio and Streamable HTTP resources, fragment/cookie +capabilities, expiry and binding failures, GET/HEAD/repeated ranges, 206/416, +ETag, and MIME/disposition metadata for MP4 and Matroska artifacts. ### 24.7 Platforms and installation profiles diff --git a/docs/deployment/coolify.md b/docs/deployment/coolify.md index 02255ea..2f9e18c 100644 --- a/docs/deployment/coolify.md +++ b/docs/deployment/coolify.md @@ -22,6 +22,7 @@ VIDXP_CONTROL_IMAGE=ghcr.io/grayhatdevelopers/vidxp:-control VIDXP_WORKER_IMAGE=ghcr.io/grayhatdevelopers/vidxp:-worker POSTGRES_PASSWORD= VIDXP_HTTP_STATIC_BEARER_TOKEN= +VIDXP_ARTIFACT_DOWNLOAD_SECRET= VIDXP_UPLOAD_CLEANUP_TOKEN= VIDXP_PUBLIC_API_HOST=api.example.com VIDXP_UPLOAD_PUBLIC_ENDPOINT=https://uploads.example.com/uploads/ @@ -30,6 +31,22 @@ VIDXP_UPLOAD_HANDOFF_SECRET= VIDXP_UPLOAD_CORS_ORIGIN_REGEX=^(https://api\.example\.com|https://app\.example\.com)$ ``` +Compose derives `VIDXP_ARTIFACT_DOWNLOAD_PUBLIC_URL` as +`https://${VIDXP_PUBLIC_API_HOST}/artifact-download`. The API issues 15-minute +links by default (`VIDXP_ARTIFACT_DOWNLOAD_TTL_SECONDS=900`); deployments may +set a value from 60 seconds through 24 hours. Keep the artifact-download secret +distinct from API, upload, and cleanup credentials. + +After `create_clip` completes, `get_artifact_download` always returns the native +MCP resource. Streamable HTTP callers also receive a short-lived HTTPS link on +the API origin. Its bearer capability is carried in the URL fragment, exchanged +for a `Secure`, `HttpOnly`, `SameSite=Strict` cookie, and removed from browser +history before the content request. The public route requires neither an API +token nor browser login; possession of the complete unexpired link is authority +to download that one repository-bound MP4 or MKV. GET, HEAD, ranges, ETag, and +resume requests remain valid until expiry. Redact fragments in client-side +telemetry and do not rewrite the public URL to an internal service hostname. + `VIDXP_UPLOAD_HANDOFF_PUBLIC_URL` must be the externally reachable HTTPS API URL ending exactly in `/upload-handoff`. Keep its secret distinct from the MCP bearer and upload-cleanup credentials. `create_media_upload` returns the upload diff --git a/pyproject.toml b/pyproject.toml index d57e4bb..5ef9a40 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -69,6 +69,7 @@ include = ["vidxp*"] [tool.setuptools.package-data] vidxp = [ + "assets/artifact_download/*", "assets/upload_page/*", "benchmarks/requirements.txt", "capabilities/*/requirements.txt", diff --git a/src/vidxp/api.py b/src/vidxp/api.py index ee5e7d1..a82443e 100644 --- a/src/vidxp/api.py +++ b/src/vidxp/api.py @@ -11,10 +11,10 @@ from vidxp.api_middleware import ( ApiCORSMiddleware, BearerAuthenticationMiddleware, + BrowserCapabilitySecurityHeadersMiddleware, RequestBodyLimitMiddleware, RequestBodyTooLarge, TypedTrustedHostMiddleware, - UploadHandoffSecurityHeadersMiddleware, request_too_large_response, ) from vidxp.api_models import HealthResponse, ReadinessResponse @@ -24,6 +24,7 @@ from vidxp.mcp import MCPTransportSecurityBoundary, create_remote_mcp from vidxp.settings import VidXPSettings from vidxp.upload_page import router as upload_page_router +from vidxp.artifact_download import router as artifact_download_router _BEARER_SECURITY = HTTPBearer( @@ -130,6 +131,7 @@ def ready( dependencies=api_dependencies, ) app.include_router(upload_page_router) + app.include_router(artifact_download_router) app.mount("/", remote_mcp.app) app.add_exception_handler( RequestBodyTooLarge, @@ -147,7 +149,7 @@ def ready( delegated_paths=delegated_auth_paths, ) app.add_middleware( - UploadHandoffSecurityHeadersMiddleware, + BrowserCapabilitySecurityHeadersMiddleware, upload_endpoint=active_settings.upload_public_endpoint, ) if active_settings.http_allowed_origins: diff --git a/src/vidxp/api_middleware.py b/src/vidxp/api_middleware.py index dd31d0e..8d5cd09 100644 --- a/src/vidxp/api_middleware.py +++ b/src/vidxp/api_middleware.py @@ -36,6 +36,14 @@ "/upload-handoff/assets/THIRD_PARTY_NOTICES.txt", } ) +_ARTIFACT_DOWNLOAD_PAGE = re.compile(r"^/artifact-download/[0-9a-f]{32}$") +_ARTIFACT_DOWNLOAD_BOOTSTRAP = re.compile( + r"^/artifact-download/[0-9a-f]{32}/bootstrap$" +) +_ARTIFACT_DOWNLOAD_CONTENT = re.compile( + r"^/artifact-download/[0-9a-f]{32}/content$" +) +_ARTIFACT_DOWNLOAD_ASSET = "/artifact-download/assets/artifact-download.js" def _public_upload_handoff_request(scope: Scope) -> bool: @@ -59,9 +67,33 @@ def _public_upload_handoff_request(scope: Scope) -> bool: ) -def _upload_handoff_path(scope: Scope) -> bool: +def _public_artifact_download_request(scope: Scope) -> bool: path = str(scope.get("path", "")) - return path == "/upload-handoff" or path.startswith("/upload-handoff/") + method = str(scope.get("method", "")).upper() + return ( + method == "GET" + and ( + path == _ARTIFACT_DOWNLOAD_ASSET + or _ARTIFACT_DOWNLOAD_PAGE.fullmatch(path) is not None + or _ARTIFACT_DOWNLOAD_CONTENT.fullmatch(path) is not None + ) + ) or ( + method == "HEAD" + and _ARTIFACT_DOWNLOAD_CONTENT.fullmatch(path) is not None + ) or ( + method == "POST" + and _ARTIFACT_DOWNLOAD_BOOTSTRAP.fullmatch(path) is not None + ) + + +def _browser_capability_path(scope: Scope) -> bool: + path = str(scope.get("path", "")) + return ( + path == "/upload-handoff" + or path.startswith("/upload-handoff/") + or path == "/artifact-download" + or path.startswith("/artifact-download/") + ) class RequestBodyTooLarge(HTTPException, OSError): @@ -253,6 +285,7 @@ async def __call__( scope["type"] != "http" or str(scope.get("path", "")) in PUBLIC_HTTP_PATHS or _public_upload_handoff_request(scope) + or _public_artifact_download_request(scope) or str(scope.get("path", "")) in self.delegated_paths ): await self.app(scope, receive, send) @@ -276,8 +309,8 @@ async def __call__( await self.app(scope, receive, send) -class UploadHandoffSecurityHeadersMiddleware: - """Apply a no-store browser boundary to the exact handoff subtree.""" +class BrowserCapabilitySecurityHeadersMiddleware: + """Apply no-store browser security headers to capability subtrees.""" def __init__(self, app: ASGIApp, *, upload_endpoint: str | None) -> None: self.app = app @@ -286,7 +319,7 @@ def __init__(self, app: ASGIApp, *, upload_endpoint: str | None) -> None: parsed = urlsplit(upload_endpoint) origin = f"{parsed.scheme}://{parsed.netloc}" connect_sources = "'self'" if origin is None else f"'self' {origin}" - self.headers = { + self.common_headers = { "Cache-Control": "private, no-store", "Referrer-Policy": "no-referrer", "X-Content-Type-Options": "nosniff", @@ -296,14 +329,19 @@ def __init__(self, app: ASGIApp, *, upload_endpoint: str | None) -> None: "Permissions-Policy": ( "camera=(), microphone=(), geolocation=(), payment=(), usb=()" ), - "Content-Security-Policy": ( - "default-src 'none'; script-src 'self'; style-src 'self'; " - "style-src-elem 'self'; style-src-attr 'unsafe-inline'; " - f"connect-src {connect_sources}; img-src 'self' data:; " - "font-src 'self'; object-src 'none'; base-uri 'none'; " - "form-action 'none'; frame-ancestors 'none'; worker-src 'none'" - ), } + self.upload_csp = ( + "default-src 'none'; script-src 'self'; style-src 'self'; " + "style-src-elem 'self'; style-src-attr 'unsafe-inline'; " + f"connect-src {connect_sources}; img-src 'self' data:; " + "font-src 'self'; object-src 'none'; base-uri 'none'; " + "form-action 'none'; frame-ancestors 'none'; worker-src 'none'" + ) + self.artifact_csp = ( + "default-src 'none'; script-src 'self'; connect-src 'self'; " + "object-src 'none'; base-uri 'none'; form-action 'none'; " + "frame-ancestors 'none'" + ) async def __call__( self, @@ -311,15 +349,23 @@ async def __call__( receive: Receive, send: Send, ) -> None: - if scope["type"] != "http" or not _upload_handoff_path(scope): + if scope["type"] != "http" or not _browser_capability_path(scope): await self.app(scope, receive, send) return + path = str(scope.get("path", "")) + content_security_policy = ( + self.upload_csp + if path == "/upload-handoff" or path.startswith("/upload-handoff/") + else self.artifact_csp + ) + async def add_headers(message: Message) -> None: if message["type"] == "http.response.start": headers = MutableHeaders(scope=message) - for name, value in self.headers.items(): + for name, value in self.common_headers.items(): headers[name] = value + headers["Content-Security-Policy"] = content_security_policy await send(message) await self.app(scope, receive, add_headers) diff --git a/src/vidxp/api_models.py b/src/vidxp/api_models.py index c34888e..69e7c6d 100644 --- a/src/vidxp/api_models.py +++ b/src/vidxp/api_models.py @@ -37,6 +37,15 @@ class UploadHandoffBootstrapRequest(ApplicationModel): capability: str = Field(min_length=32, max_length=512) +class ArtifactDownloadBootstrapRequest(ApplicationModel): + capability: str = Field(min_length=32, max_length=2048) + + +class ArtifactDownloadBootstrapResponse(ApplicationModel): + content_url: str = Field(min_length=1, max_length=2048) + expires_at: AwareDatetime + + class UploadPageSessionResponse(ApplicationModel): status: MediaUploadSessionStatus creation_url: str = Field(min_length=1, max_length=2048) diff --git a/src/vidxp/application_boundary.py b/src/vidxp/application_boundary.py index 51c0ea1..3ce724c 100644 --- a/src/vidxp/application_boundary.py +++ b/src/vidxp/application_boundary.py @@ -12,6 +12,8 @@ ResourceNotFoundError, ) from vidxp.artifact_service import ( + ArtifactNotFoundError, + ArtifactNotReadyError, ArtifactRequestError, ArtifactUnavailableError, InvalidArtifactError, @@ -163,8 +165,26 @@ def wrapped(*args: Any, **kwargs: Any) -> Any: ) from exc except MediaUnavailableError as exc: raise ResourceNotFoundError("media") from exc + except ArtifactNotFoundError as exc: + raise ApplicationError( + "artifact_not_found", + ErrorCategory.not_found, + "The requested artifact was not found.", + ) from exc + except ArtifactNotReadyError as exc: + raise ApplicationError( + "artifact_not_ready", + ErrorCategory.conflict, + "The requested artifact is not ready for delivery.", + retryable=True, + ) from exc except ArtifactUnavailableError as exc: - raise ResourceNotFoundError("artifact") from exc + raise ApplicationError( + "artifact_not_ready", + ErrorCategory.conflict, + "The requested artifact is not ready for delivery.", + retryable=True, + ) from exc except ArtifactIntegrityError as exc: raise ApplicationError( "artifact_integrity_failed", diff --git a/src/vidxp/application_models.py b/src/vidxp/application_models.py index 58ca44c..433c8ca 100644 --- a/src/vidxp/application_models.py +++ b/src/vidxp/application_models.py @@ -583,6 +583,29 @@ class Artifact(ApplicationModel): expires_at: AwareDatetime | None = None +class ArtifactDeliveryMode(StrEnum): + local_file = "local_file" + https_download = "https_download" + mcp_resource = "mcp_resource" + + +class ArtifactDownload(ApplicationModel): + artifact_id: ArtifactId + filename: str = Field(min_length=1, max_length=255) + mime_type: MimeType + byte_size: int = Field(gt=0) + sha256: Sha256 + etag: str = Field(pattern=r'^"[0-9a-f]{64}"$') + state: ArtifactState + resource_uri: str = Field(min_length=1, max_length=2048) + delivery_mode: ArtifactDeliveryMode + local_path: Path | None = None + file_uri: str | None = Field(default=None, max_length=4096) + download_url: str | None = Field(default=None, max_length=4096) + download_expires_at: AwareDatetime | None = None + delivery_error: ErrorDetail | None = None + + class ActorOverlayProfile(StrEnum): default = "default" diff --git a/src/vidxp/artifact_delivery.py b/src/vidxp/artifact_delivery.py new file mode 100644 index 0000000..261a4bd --- /dev/null +++ b/src/vidxp/artifact_delivery.py @@ -0,0 +1,272 @@ +from __future__ import annotations + +from dataclasses import dataclass +from datetime import datetime, timedelta +from pathlib import Path +from urllib.parse import quote + +from vidxp.application_models import ( + ApplicationError, + Artifact, + ErrorCategory, +) +from vidxp.capability_security import repository_binding +from vidxp.core.media import utc_now +from vidxp.ports import LocalFileResource +from vidxp.settings import VidXPSettings + + +_LINK_AUDIENCE = "vidxp-artifact-download-link" +_SESSION_AUDIENCE = "vidxp-artifact-download-session" +_LINK_PURPOSE = "artifact-download-link-v1" +_SESSION_PURPOSE = "artifact-download-session-v1" +_SUPPORTED_MEDIA = { + "video/mp4": "mp4", + "video/x-matroska": "mkv", +} + + +@dataclass(frozen=True) +class ArtifactBinding: + artifact_id: str + filename: str + mime_type: str + extension: str + byte_size: int + sha256: str + + +@dataclass(frozen=True) +class IssuedArtifactDownload: + url: str + expires_at: datetime + + +def artifact_binding(artifact: Artifact) -> ArtifactBinding: + extension = _SUPPORTED_MEDIA.get(artifact.mime_type) + if extension is None: + raise ApplicationError( + "artifact_type_unsupported", + ErrorCategory.validation, + "Only completed MP4 and Matroska video artifacts can be delivered.", + details={"mime_type": artifact.mime_type}, + ) + return ArtifactBinding( + artifact_id=artifact.artifact_id, + filename=f"{artifact.kind.value}-{artifact.artifact_id}.{extension}", + mime_type=artifact.mime_type, + extension=extension, + byte_size=artifact.byte_size, + sha256=artifact.sha256, + ) + + +def require_resource_binding( + binding: ArtifactBinding, + resource: LocalFileResource, +) -> None: + if ( + resource.filename != binding.filename + or resource.mime_type != binding.mime_type + or resource.byte_size != binding.byte_size + or resource.etag != binding.sha256 + ): + raise ApplicationError( + "artifact_download_binding_mismatch", + ErrorCategory.conflict, + "The registered artifact metadata does not match its content.", + ) + + +class ArtifactDownloadCapabilities: + """Issue and validate stateless repository-bound browser downloads.""" + + def __init__(self, settings: VidXPSettings) -> None: + self.settings = settings + + def issue(self, artifact: Artifact) -> IssuedArtifactDownload: + public_url = self.settings.artifact_download_public_url + if public_url is None or self.settings.artifact_download_secret is None: + raise ApplicationError( + "public_download_origin_unavailable", + ErrorCategory.unavailable, + "Public artifact downloads are not configured for this deployment.", + ) + binding = artifact_binding(artifact) + now = utc_now() + expires_at = datetime.fromtimestamp( + int( + ( + now + + timedelta( + seconds=self.settings.artifact_download_ttl_seconds + ) + ).timestamp() + ), + tz=now.tzinfo, + ) + capability = self._encode( + binding, + audience=_LINK_AUDIENCE, + purpose=_LINK_PURPOSE, + issued_at=now, + expires_at=expires_at, + ) + return IssuedArtifactDownload( + url=( + f"{public_url}/{binding.artifact_id}#capability=" + f"{quote(capability, safe='')}" + ), + expires_at=expires_at, + ) + + def exchange(self, artifact: Artifact, capability: str) -> tuple[str, datetime]: + binding = artifact_binding(artifact) + claims = self._decode( + capability, + audience=_LINK_AUDIENCE, + purpose=_LINK_PURPOSE, + ) + self._require_binding(claims, binding) + expires_at = datetime.fromtimestamp(int(claims["exp"]), tz=utc_now().tzinfo) + session_token = self._encode( + binding, + audience=_SESSION_AUDIENCE, + purpose=_SESSION_PURPOSE, + issued_at=utc_now(), + expires_at=expires_at, + ) + return session_token, expires_at + + def authorize(self, artifact: Artifact, session_token: str | None) -> None: + if session_token is None: + raise self._invalid_capability() + binding = artifact_binding(artifact) + claims = self._decode( + session_token, + audience=_SESSION_AUDIENCE, + purpose=_SESSION_PURPOSE, + ) + self._require_binding(claims, binding) + + def _encode( + self, + binding: ArtifactBinding, + *, + audience: str, + purpose: str, + issued_at: datetime, + expires_at: datetime, + ) -> str: + import jwt + + return jwt.encode( + { + "iss": "vidxp", + "aud": audience, + "sub": binding.artifact_id, + "purpose": purpose, + "version": 1, + "repository": repository_binding(self.settings.repository_root), + "mime_type": binding.mime_type, + "extension": binding.extension, + "sha256": binding.sha256, + "iat": int(issued_at.timestamp()), + "exp": int(expires_at.timestamp()), + }, + self._secret(), + algorithm="HS256", + ) + + def _decode( + self, + token: str, + *, + audience: str, + purpose: str, + ) -> dict: + import jwt + + try: + claims = jwt.decode( + token, + self._secret(), + algorithms=["HS256"], + audience=audience, + issuer="vidxp", + options={ + "require": [ + "sub", + "purpose", + "version", + "repository", + "mime_type", + "extension", + "sha256", + "iat", + "exp", + ] + }, + ) + except jwt.ExpiredSignatureError as exc: + raise ApplicationError( + "artifact_download_capability_expired", + ErrorCategory.authentication, + "The artifact download capability expired.", + ) from exc + except jwt.PyJWTError as exc: + raise self._invalid_capability() from exc + if claims.get("purpose") != purpose or claims.get("version") != 1: + raise self._invalid_capability() + return claims + + def _require_binding(self, claims: dict, binding: ArtifactBinding) -> None: + if ( + claims.get("sub") != binding.artifact_id + or claims.get("repository") + != repository_binding(self.settings.repository_root) + or claims.get("mime_type") != binding.mime_type + or claims.get("extension") != binding.extension + or claims.get("sha256") != binding.sha256 + ): + raise ApplicationError( + "artifact_download_binding_mismatch", + ErrorCategory.authorization, + "The artifact download capability does not match this artifact.", + ) + + def _secret(self) -> str: + secret = self.settings.artifact_download_secret + if secret is None: + raise ApplicationError( + "public_download_origin_unavailable", + ErrorCategory.unavailable, + "Public artifact downloads are not configured for this deployment.", + ) + return secret.get_secret_value() + + @staticmethod + def _invalid_capability() -> ApplicationError: + return ApplicationError( + "artifact_download_capability_invalid", + ErrorCategory.authentication, + "The artifact download capability is invalid.", + ) + + +def verified_local_path(path: Path) -> Path: + try: + resolved = path.resolve(strict=True) + except OSError as exc: + raise _local_path_unavailable() from exc + if not resolved.is_file(): + raise _local_path_unavailable() + return resolved + + +def _local_path_unavailable() -> ApplicationError: + return ApplicationError( + "local_path_unavailable", + ErrorCategory.unavailable, + "The completed artifact is not available as a local file.", + ) diff --git a/src/vidxp/artifact_download.py b/src/vidxp/artifact_download.py new file mode 100644 index 0000000..7ab56e2 --- /dev/null +++ b/src/vidxp/artifact_download.py @@ -0,0 +1,156 @@ +from __future__ import annotations + +from importlib.resources import files +from typing import Annotated +from urllib.parse import urlsplit + +from fastapi import APIRouter, Cookie, Depends, Request, Response + +from vidxp.api_models import ( + ArtifactDownloadBootstrapRequest, + ArtifactDownloadBootstrapResponse, +) +from vidxp.api_routes.dependencies import context, file_response +from vidxp.application_models import ApplicationError, ErrorCategory +from vidxp.artifact_delivery import ( + ArtifactDownloadCapabilities, + artifact_binding, + require_resource_binding, +) +from vidxp.composition import HttpApplicationContext +from vidxp.core.identifiers import ArtifactId +from vidxp.core.media import utc_now + + +router = APIRouter(prefix="/artifact-download", tags=["artifact-download"]) +_SESSION_COOKIE = "__Secure-vidxp-artifact" +_SCRIPT = "artifact-download.js" + + +def _capabilities(service: HttpApplicationContext) -> ArtifactDownloadCapabilities: + return ArtifactDownloadCapabilities(service.settings) + + +def _require_same_origin( + request: Request, + service: HttpApplicationContext, +) -> None: + public_url = service.settings.artifact_download_public_url + if public_url is None: + raise ApplicationError( + "public_download_origin_unavailable", + ErrorCategory.unavailable, + "Public artifact downloads are not configured for this deployment.", + ) + parsed = urlsplit(public_url) + expected = f"{parsed.scheme}://{parsed.netloc}" + origin = request.headers.get("origin", "") + if ( + origin.lower() != expected.lower() + or request.headers.get("sec-fetch-site", "same-origin") != "same-origin" + ): + raise ApplicationError( + "artifact_download_origin_forbidden", + ErrorCategory.authorization, + "The capability exchange must come from its VidXP download page.", + ) + + +@router.get("/assets/{asset_name}", include_in_schema=False) +def artifact_download_asset(asset_name: str) -> Response: + if asset_name != _SCRIPT: + raise ApplicationError( + "resource_not_found", + ErrorCategory.not_found, + "The requested artifact-download asset was not found.", + ) + content = files("vidxp").joinpath( + "assets", "artifact_download", _SCRIPT + ).read_bytes() + return Response(content=content, media_type="text/javascript; charset=utf-8") + + +@router.get("/{artifact_id}", include_in_schema=False) +def artifact_download_page(artifact_id: ArtifactId) -> Response: + del artifact_id + content = files("vidxp").joinpath( + "assets", "artifact_download", "index.html" + ).read_bytes() + return Response(content=content, media_type="text/html; charset=utf-8") + + +@router.post( + "/{artifact_id}/bootstrap", + response_model=ArtifactDownloadBootstrapResponse, + include_in_schema=False, +) +def bootstrap_artifact_download( + artifact_id: ArtifactId, + command: ArtifactDownloadBootstrapRequest, + request: Request, + response: Response, + service: Annotated[HttpApplicationContext, Depends(context)], +) -> ArtifactDownloadBootstrapResponse: + _require_same_origin(request, service) + artifact = service.application.get_artifact(artifact_id) + session_token, expires_at = _capabilities(service).exchange( + artifact, + command.capability, + ) + response.set_cookie( + _SESSION_COOKIE, + session_token, + max_age=max(0, int((expires_at - utc_now()).total_seconds())), + expires=expires_at, + path=f"/artifact-download/{artifact_id}", + secure=True, + httponly=True, + samesite="strict", + ) + return ArtifactDownloadBootstrapResponse( + content_url=f"/artifact-download/{artifact_id}/content", + expires_at=expires_at, + ) + + +def _content( + artifact_id: ArtifactId, + request: Request, + service: HttpApplicationContext, + session_token: str | None, +) -> Response: + artifact = service.application.get_artifact(artifact_id) + _capabilities(service).authorize(artifact, session_token) + resource = service.application.open_artifact_content(artifact_id) + require_resource_binding(artifact_binding(artifact), resource) + return file_response( + request, + resource, + disposition="attachment", + ) + + +@router.get("/{artifact_id}/content", include_in_schema=False) +def get_artifact_download_content( + artifact_id: ArtifactId, + request: Request, + service: Annotated[HttpApplicationContext, Depends(context)], + session_token: Annotated[ + str | None, + Cookie(alias=_SESSION_COOKIE), + ] = None, +) -> Response: + return _content(artifact_id, request, service, session_token) + + +@router.head("/{artifact_id}/content", include_in_schema=False) +def head_artifact_download_content( + artifact_id: ArtifactId, + request: Request, + service: Annotated[HttpApplicationContext, Depends(context)], + session_token: Annotated[ + str | None, + Cookie(alias=_SESSION_COOKIE), + ] = None, +) -> Response: + return _content(artifact_id, request, service, session_token) diff --git a/src/vidxp/artifact_service.py b/src/vidxp/artifact_service.py index b326a81..24cc868 100644 --- a/src/vidxp/artifact_service.py +++ b/src/vidxp/artifact_service.py @@ -35,6 +35,14 @@ class ArtifactUnavailableError(FileNotFoundError): """Raised when an artifact is missing, expired, or fails integrity checks.""" +class ArtifactNotFoundError(ArtifactUnavailableError): + """Raised when no registered artifact exists for an identifier.""" + + +class ArtifactNotReadyError(ArtifactUnavailableError): + """Raised when a registered artifact cannot currently be delivered.""" + + class ArtifactRequestError(ValueError): """Raised when an artifact request is invalid for its source media.""" @@ -91,7 +99,7 @@ def get(self, artifact_id: str) -> Artifact: def require_record(self, artifact_id: str) -> ArtifactRecord: record = self.catalog.get_artifact(artifact_id) if record is None: - raise ArtifactUnavailableError("The artifact is unavailable.") + raise ArtifactNotFoundError("The artifact was not found.") self._require_ready(record) return record @@ -104,7 +112,7 @@ def _require_ready(record: ArtifactRecord) -> None: and record.expires_at <= utc_now() ) ): - raise ArtifactUnavailableError("The artifact is unavailable.") + raise ArtifactNotReadyError("The artifact is not ready.") def content(self, artifact_id: str) -> LocalFileResource: record = self.require_record(artifact_id) @@ -126,7 +134,7 @@ def _verified_content(self, record: ArtifactRecord) -> Path: byte_size=record.byte_size, ) except (FileNotFoundError, PermissionError) as exc: - raise ArtifactUnavailableError( + raise ArtifactNotReadyError( "The artifact content is unavailable." ) from exc except RuntimeError as exc: diff --git a/src/vidxp/assets/artifact_download/artifact-download.js b/src/vidxp/assets/artifact_download/artifact-download.js new file mode 100644 index 0000000..941ba28 --- /dev/null +++ b/src/vidxp/assets/artifact_download/artifact-download.js @@ -0,0 +1,31 @@ +const status = document.querySelector('#download-status') + +function readCapability() { + const parameters = new URLSearchParams(window.location.hash.slice(1)) + return parameters.get('capability') +} + +async function startDownload() { + const capability = readCapability() + if (!capability) throw new Error('The complete VidXP download link is required.') + const base = window.location.pathname.replace(/\/$/, '') + const response = await fetch(`${base}/bootstrap`, { + method: 'POST', + credentials: 'same-origin', + cache: 'no-store', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ capability }), + }) + if (!response.ok) throw new Error('The VidXP download link is invalid or expired.') + const payload = await response.json() + window.history.replaceState(null, document.title, base) + status.textContent = 'Your download is starting…' + window.location.replace(payload.content_url) +} + +startDownload().catch((error) => { + status.textContent = error instanceof Error ? error.message : 'The download failed.' +}) diff --git a/src/vidxp/assets/artifact_download/index.html b/src/vidxp/assets/artifact_download/index.html new file mode 100644 index 0000000..667abad --- /dev/null +++ b/src/vidxp/assets/artifact_download/index.html @@ -0,0 +1,15 @@ + + + + + + VidXP artifact download + + + +
      +

      Preparing your VidXP artifact

      +

      Verifying the download link…

      +
      + + diff --git a/src/vidxp/capability_security.py b/src/vidxp/capability_security.py new file mode 100644 index 0000000..27782ff --- /dev/null +++ b/src/vidxp/capability_security.py @@ -0,0 +1,15 @@ +from __future__ import annotations + +import hashlib +from pathlib import Path + + +def repository_binding(repository_root: Path) -> str: + """Return the stable repository binding used by bearer capabilities.""" + + root = str(repository_root.resolve()).replace("\\", "/") + # Preserve the deployed upload-session binding contract while sharing it + # with other repository-scoped browser capabilities. + return hashlib.sha256( + f"vidxp-upload-repository-v1\0{root}".encode("utf-8") + ).hexdigest() diff --git a/src/vidxp/mcp.py b/src/vidxp/mcp.py index 892c846..1a40de8 100644 --- a/src/vidxp/mcp.py +++ b/src/vidxp/mcp.py @@ -5,7 +5,7 @@ from contextvars import ContextVar from dataclasses import dataclass from functools import partial -from typing import Annotated, Callable, TypeVar +from typing import Annotated, Callable, Literal, TypeVar from urllib.parse import quote import anyio @@ -31,11 +31,14 @@ from vidxp import __version__ from vidxp.application_models import ( ApplicationError, + ArtifactDeliveryMode, + ArtifactDownload, CapabilityInfo, CapabilityList, CreateSnippetCommand, CreateIndexCommand, ErrorCategory, + ErrorDetail, Identifier, IndexStatus, Job, @@ -55,6 +58,12 @@ WorkspaceOverview, UploadSessionId, ) +from vidxp.artifact_delivery import ( + ArtifactDownloadCapabilities, + artifact_binding, + require_resource_binding, + verified_local_path, +) from vidxp.authentication import ( OIDCBearerAuthenticator, create_authenticator, @@ -251,6 +260,13 @@ def _application_error(exc: ApplicationError) -> ToolError: ) +def _translate_application_result(operation: Callable[[], _T]) -> _T: + try: + return operation() + except ApplicationError as exc: + raise _application_error(exc) from exc + + def _invoke( context: ControlPlaneContext, *, @@ -337,6 +353,7 @@ def create_mcp_server( *, default_principal: Principal | None = None, oidc_authentication: bool = False, + artifact_delivery: Literal["local_stdio", "streamable_http"] = "local_stdio", ) -> MCPServer: settings = context.settings token_verifier = None @@ -813,16 +830,17 @@ def submit(actor: Principal) -> Job: @server.tool( description=( - "Return a readable MCP resource link for a completed clip or video " - "artifact. The artifact_id is in the completed create_clip job " - "result; clients read the link only when they need the video bytes." + "Return a readable MCP resource link plus transport-appropriate " + "delivery metadata for a completed clip or video artifact. Local " + "stdio may expose its verified path; Streamable HTTP returns a " + "short-lived browser download without exposing server paths." ), annotations=_READ_ONLY, structured_output=True, ) async def get_artifact_download( artifact_id: ArtifactId, - ) -> ResourceLink: + ) -> Annotated[CallToolResult, ArtifactDownload]: artifact = await _invoke_async( context, default_principal=default_principal, @@ -831,19 +849,15 @@ async def get_artifact_download( context.application.get_artifact(artifact_id) ), ) - suffix = ( - "mp4" - if artifact.mime_type == "video/mp4" - else "mkv" + binding = _translate_application_result(lambda: artifact_binding(artifact)) + resource_uri = ( + f"vidxp://artifacts/{artifact.artifact_id}/" + f"content.{binding.extension}" ) - filename = f"{artifact.kind.value}-{artifact.artifact_id}.{suffix}" - return ResourceLink( - name=filename, + link = ResourceLink( + name=binding.filename, title=f"VidXP {artifact.kind.value.replace('_', ' ')}", - uri=( - f"vidxp://artifacts/{artifact.artifact_id}/" - f"content.{suffix}" - ), + uri=resource_uri, description=( f"Generated from media {artifact.media_id}; " f"{artifact.byte_size:,} bytes." @@ -851,6 +865,77 @@ async def get_artifact_download( mimeType=artifact.mime_type, size=artifact.byte_size, ) + local_path = None + file_uri = None + download_url = None + download_expires_at = None + delivery_error = None + if artifact_delivery == "streamable_http": + issued = _translate_application_result( + lambda: ArtifactDownloadCapabilities(context.settings).issue( + artifact + ) + ) + delivery_mode = ArtifactDeliveryMode.https_download + download_url = issued.url + download_expires_at = issued.expires_at + elif context.settings.mcp_stdio_filesystem_accessible: + resource = await _invoke_async( + context, + default_principal=default_principal, + permission=RepositoryPermission.read, + operation=lambda _actor: ( + context.application.open_artifact_content(artifact_id) + ), + ) + _translate_application_result( + lambda: require_resource_binding(binding, resource) + ) + resolved = _translate_application_result( + lambda: verified_local_path(resource.path) + ) + delivery_mode = ArtifactDeliveryMode.local_file + local_path = resolved + file_uri = resolved.as_uri() + elif context.settings.artifact_download_public_url is not None: + issued = _translate_application_result( + lambda: ArtifactDownloadCapabilities(context.settings).issue( + artifact + ) + ) + delivery_mode = ArtifactDeliveryMode.https_download + download_url = issued.url + download_expires_at = issued.expires_at + else: + delivery_mode = ArtifactDeliveryMode.mcp_resource + delivery_error = ErrorDetail( + code="local_path_unavailable", + category=ErrorCategory.unavailable, + message=( + "The stdio client is configured as filesystem-isolated; " + "read the MCP resource or configure a public download origin." + ), + ) + result = ArtifactDownload( + artifact_id=artifact.artifact_id, + filename=binding.filename, + mime_type=artifact.mime_type, + byte_size=artifact.byte_size, + sha256=artifact.sha256, + etag=f'"{artifact.sha256}"', + state=artifact.state, + resource_uri=resource_uri, + delivery_mode=delivery_mode, + local_path=local_path, + file_uri=file_uri, + download_url=download_url, + download_expires_at=download_expires_at, + delivery_error=delivery_error, + ) + return CallToolResult( + content=[link], + structured_content=result.model_dump(mode="json"), + ) @server.tool( description="List durable jobs so work can be recovered across sessions.", @@ -936,6 +1021,7 @@ def create_remote_mcp(context: ControlPlaneContext) -> RemoteMCP: server = create_mcp_server( context, oidc_authentication=owns_authentication, + artifact_delivery="streamable_http", ) transport_security = TransportSecuritySettings( enable_dns_rebinding_protection=True, diff --git a/src/vidxp/settings.py b/src/vidxp/settings.py index 8fc0027..5ac6c43 100644 --- a/src/vidxp/settings.py +++ b/src/vidxp/settings.py @@ -75,6 +75,42 @@ def _tusd_cors_origins(pattern: str) -> tuple[str, ...]: return tuple(origins) +def _validate_browser_public_url( + value: str | None, + *, + field_name: str, + required_path: str, +) -> str | None: + if value is None: + return None + if value != value.strip() or "\\" in value: + raise ValueError(f"{field_name} contains unsafe characters.") + parsed = urlsplit(value) + if ( + parsed.scheme not in {"http", "https"} + or parsed.hostname is None + or parsed.username is not None + or parsed.password is not None + or parsed.query + or parsed.fragment + or parsed.path.rstrip("/") != required_path + ): + raise ValueError( + f"{field_name} must be an HTTP(S) URL ending in {required_path}." + ) + if parsed.scheme != "https" and parsed.hostname.lower() not in { + "localhost", + "127.0.0.1", + "::1", + }: + raise ValueError(f"{field_name} must use HTTPS outside loopback.") + try: + parsed.port + except ValueError as exc: + raise ValueError(f"{field_name} contains an invalid port.") from exc + return value.rstrip("/") + + class ApplicationMode(StrEnum): local = "local" remote = "remote" @@ -182,6 +218,18 @@ class VidXPSettings(BaseSettings): "testserver", ) mcp_allowed_origins: tuple[str, ...] = () + mcp_stdio_filesystem_accessible: bool = True + artifact_download_public_url: str | None = Field( + default=None, + min_length=1, + max_length=2048, + ) + artifact_download_secret: SecretStr | None = None + artifact_download_ttl_seconds: int = Field( + default=15 * 60, + ge=60, + le=24 * 60 * 60, + ) upload_public_endpoint: str | None = Field( default=None, min_length=1, @@ -290,6 +338,8 @@ def _normalize_empty_optional_slm( return None if value == "" else value @field_validator( + "artifact_download_public_url", + "artifact_download_secret", "upload_handoff_public_url", "upload_handoff_secret", "upload_cors_origin_regex", @@ -576,41 +626,23 @@ def _validate_upload_handoff_public_url( cls, value: str | None, ) -> str | None: - if value is None: - return None - if value != value.strip() or "\\" in value: - raise ValueError( - "upload_handoff_public_url contains unsafe characters." - ) - parsed = urlsplit(value) - if ( - parsed.scheme not in {"http", "https"} - or parsed.hostname is None - or parsed.username is not None - or parsed.password is not None - or parsed.query - or parsed.fragment - or parsed.path.rstrip("/") != "/upload-handoff" - ): - raise ValueError( - "upload_handoff_public_url must be an HTTP(S) URL ending in " - "/upload-handoff." - ) - if parsed.scheme != "https" and parsed.hostname.lower() not in { - "localhost", - "127.0.0.1", - "::1", - }: - raise ValueError( - "upload_handoff_public_url must use HTTPS outside loopback." - ) - try: - parsed.port - except ValueError as exc: - raise ValueError( - "upload_handoff_public_url contains an invalid port." - ) from exc - return value.rstrip("/") + return _validate_browser_public_url( + value, + field_name="upload_handoff_public_url", + required_path="/upload-handoff", + ) + + @field_validator("artifact_download_public_url") + @classmethod + def _validate_artifact_download_public_url( + cls, + value: str | None, + ) -> str | None: + return _validate_browser_public_url( + value, + field_name="artifact_download_public_url", + required_path="/artifact-download", + ) @field_validator("trusted_local_import_roots") @classmethod @@ -715,6 +747,23 @@ def _require_explicit_server_backend(self) -> "VidXPSettings": "origin regex, and a " "dedicated secret of at least 32 characters." ) + download_configured = any( + value is not None + for value in ( + self.artifact_download_public_url, + self.artifact_download_secret, + ) + ) + if download_configured and ( + self.artifact_download_public_url is None + or self.artifact_download_secret is None + or len(self.artifact_download_secret.get_secret_value()) < 32 + ): + raise ValueError( + "Public artifact downloads require an HTTPS download URL " + "(or loopback HTTP) and a dedicated secret of at least 32 " + "characters." + ) if self.upload_session_max_bytes < self.upload_max_bytes: raise ValueError( "The upload session aggregate limit must be at least the " diff --git a/src/vidxp/upload_service.py b/src/vidxp/upload_service.py index 6d9b78b..96d8996 100644 --- a/src/vidxp/upload_service.py +++ b/src/vidxp/upload_service.py @@ -35,6 +35,7 @@ UploadSessionState, UploadState, ) +from vidxp.capability_security import repository_binding from vidxp.infrastructure.sql_catalog import ( SQLCatalog, UploadQuotaExceededError, @@ -1389,10 +1390,7 @@ def _secret(self) -> str: return secret.get_secret_value() def _repository_binding(self) -> str: - root = str(self.settings.repository_root.resolve()).replace("\\", "/") - return hashlib.sha256( - f"vidxp-upload-repository-v1\0{root}".encode("utf-8") - ).hexdigest() + return repository_binding(self.settings.repository_root) def _session_capability(self, record: UploadSessionRecord) -> str: import jwt diff --git a/tests/fixtures/mcp_upload_server.py b/tests/fixtures/mcp_upload_server.py index 4ce39cb..4da9f72 100644 --- a/tests/fixtures/mcp_upload_server.py +++ b/tests/fixtures/mcp_upload_server.py @@ -1,18 +1,31 @@ +import hashlib +import os +from contextlib import ExitStack from datetime import datetime, timedelta, timezone +from pathlib import Path from unittest.mock import Mock +from unittest.mock import patch -from vidxp.application_models import MediaUploadSessionStatus, Principal +from vidxp.application_models import ( + Artifact, + MediaUploadSessionStatus, + Principal, +) from vidxp.authorization import AuthorizationPolicy from vidxp.composition import ControlPlaneContext from vidxp.control_plane import ControlPlaneApplication +from vidxp.core.artifacts import ArtifactKind, ArtifactState from vidxp.core.uploads import UploadSessionState from vidxp.job_service import JobService from vidxp.mcp import create_mcp_server +from vidxp.ports import LocalFileResource from vidxp.settings import VidXPSettings from vidxp.upload_service import RemoteUploadService, UploadSessionLink UPLOAD_SESSION_ID = "423456781234423481234567890abcde" +ARTIFACT_ID = "323456781234423481234567890abcde" +MEDIA_ID = "123456781234423481234567890abcde" def main() -> None: @@ -57,8 +70,32 @@ def main() -> None: capability="fixture-capability", ) uploads.get_status.return_value = status + application = Mock(spec=ControlPlaneApplication) + artifact_path = os.environ.get("VIDXP_TEST_ARTIFACT_PATH") + if artifact_path is not None: + path = Path(artifact_path).resolve(strict=True) + content = path.read_bytes() + digest = hashlib.sha256(content).hexdigest() + application.get_artifact.return_value = Artifact( + artifact_id=ARTIFACT_ID, + media_id=MEDIA_ID, + kind=ArtifactKind.snippet, + profile="compatible_mp4", + mime_type="video/mp4", + byte_size=len(content), + sha256=digest, + state=ArtifactState.ready, + created_at=now, + ) + application.open_artifact_content.return_value = LocalFileResource( + path=path, + filename=f"snippet-{ARTIFACT_ID}.mp4", + mime_type="video/mp4", + byte_size=len(content), + etag=digest, + ) context = ControlPlaneContext( - application=Mock(spec=ControlPlaneApplication), + application=application, jobs=Mock(spec=JobService), authorization=AuthorizationPolicy(), settings=settings, @@ -72,7 +109,21 @@ def main() -> None: scopes=frozenset({"*"}), ), ) - server.run("stdio") + with ExitStack() as stack: + if os.environ.get("VIDXP_TEST_FORBID_HELPERS") == "1": + stack.enter_context( + patch( + "socket.create_server", + side_effect=AssertionError("stdio opened an HTTP listener"), + ) + ) + stack.enter_context( + patch( + "subprocess.Popen", + side_effect=AssertionError("stdio started a helper process"), + ) + ) + server.run("stdio") if __name__ == "__main__": diff --git a/tests/test_application_boundary.py b/tests/test_application_boundary.py index 21deedb..2316640 100644 --- a/tests/test_application_boundary.py +++ b/tests/test_application_boundary.py @@ -2,11 +2,29 @@ from vidxp.application_boundary import application_boundary from vidxp.application_models import ApplicationError +from vidxp.artifact_service import ArtifactNotFoundError, ArtifactNotReadyError from vidxp.core.artifacts import ArtifactRendererUnavailableError from vidxp.core.media import MediaProbeUnavailableError class ApplicationBoundaryTests(unittest.TestCase): + def test_artifact_delivery_distinguishes_not_found_from_not_ready(self): + cases = ( + (ArtifactNotFoundError("missing"), "artifact_not_found", False), + (ArtifactNotReadyError("pending"), "artifact_not_ready", True), + ) + for source, code, retryable in cases: + with self.subTest(code=code): + @application_boundary + def operation(): + raise source + + with self.assertRaises(ApplicationError) as raised: + operation() + + self.assertEqual(raised.exception.code, code) + self.assertEqual(raised.exception.retryable, retryable) + def test_missing_media_probe_carries_init_remediation(self): @application_boundary def operation(): diff --git a/tests/test_artifact_download.py b/tests/test_artifact_download.py new file mode 100644 index 0000000..84bcc02 --- /dev/null +++ b/tests/test_artifact_download.py @@ -0,0 +1,306 @@ +import hashlib +from dataclasses import replace +from datetime import datetime, timedelta, timezone +from pathlib import Path +from tempfile import TemporaryDirectory +from unittest.mock import Mock, patch +from urllib.parse import parse_qs, urlsplit + +import pytest +from fastapi.testclient import TestClient + +from vidxp.api import create_app +from vidxp.application_models import ApplicationError, Artifact +from vidxp.artifact_delivery import ( + ArtifactDownloadCapabilities, + verified_local_path, +) +from vidxp.authentication import create_authenticator +from vidxp.authorization import AuthorizationPolicy +from vidxp.composition import HttpApplicationContext +from vidxp.control_plane import ControlPlaneApplication +from vidxp.core.artifacts import ArtifactKind, ArtifactState +from vidxp.job_service import JobService +from vidxp.ports import LocalFileResource +from vidxp.settings import VidXPSettings + + +ARTIFACT_ID = "323456781234423481234567890abcde" +OTHER_ARTIFACT_ID = "423456781234423481234567890abcde" +MEDIA_ID = "123456781234423481234567890abcde" +PUBLIC_URL = "https://download.example/artifact-download" +SECRET = "d" * 32 + + +def test_missing_local_artifact_uses_stable_delivery_error(tmp_path: Path) -> None: + with pytest.raises(ApplicationError) as caught: + verified_local_path(tmp_path / "missing.mp4") + + assert caught.value.detail.code == "local_path_unavailable" + + +def _artifact(content: bytes, mime_type: str, *, artifact_id: str = ARTIFACT_ID): + return Artifact( + artifact_id=artifact_id, + media_id=MEDIA_ID, + kind=ArtifactKind.snippet, + profile="compatible_mp4" if mime_type == "video/mp4" else "source_mkv", + mime_type=mime_type, + byte_size=len(content), + sha256=hashlib.sha256(content).hexdigest(), + state=ArtifactState.ready, + created_at=datetime.now(timezone.utc), + ) + + +def _context(root: Path, artifact: Artifact, content_path: Path): + settings = VidXPSettings( + repository_root=root, + runtime_backend="cpu", + http_auth_mode="static", + http_static_bearer_token="a" * 32, + http_trusted_hosts=("download.example", "testserver"), + mcp_allowed_hosts=("download.example", "testserver"), + artifact_download_public_url=PUBLIC_URL, + artifact_download_secret=SECRET, + ) + application = Mock(spec=ControlPlaneApplication) + + def get_artifact(artifact_id: str): + if artifact_id == artifact.artifact_id: + return artifact + return artifact.model_copy(update={"artifact_id": artifact_id}) + + application.get_artifact.side_effect = get_artifact + extension = "mp4" if artifact.mime_type == "video/mp4" else "mkv" + application.open_artifact_content.return_value = LocalFileResource( + path=content_path, + filename=f"snippet-{artifact.artifact_id}.{extension}", + mime_type=artifact.mime_type, + byte_size=artifact.byte_size, + etag=artifact.sha256, + ) + jobs = Mock(spec=JobService) + jobs.start.return_value = None + readiness = Mock() + readiness.ready.return_value = True + return HttpApplicationContext( + application=application, + jobs=jobs, + readiness=readiness, + authenticator=create_authenticator(settings), + authorization=AuthorizationPolicy(), + settings=settings, + ) + + +def _bootstrap(client: TestClient, artifact: Artifact, capability: str): + return client.post( + f"/artifact-download/{artifact.artifact_id}/bootstrap", + headers={ + "Origin": "https://download.example", + "Sec-Fetch-Site": "same-origin", + }, + json={"capability": capability}, + ) + + +@pytest.mark.parametrize( + ("mime_type", "extension"), + (("video/mp4", "mp4"), ("video/x-matroska", "mkv")), +) +def test_capability_download_supports_full_head_ranges_and_resume( + mime_type: str, + extension: str, +): + content = b"0123456789abcdef" + with TemporaryDirectory() as directory: + root = Path(directory) + content_path = root / f"clip.{extension}" + content_path.write_bytes(content) + artifact = _artifact(content, mime_type) + context = _context(root, artifact, content_path) + issued = ArtifactDownloadCapabilities(context.settings).issue(artifact) + parsed = urlsplit(issued.url) + capability = parse_qs(parsed.fragment)["capability"][0] + with TestClient( + create_app(context=context), + base_url="https://download.example", + ) as client: + landing = client.get(f"/artifact-download/{ARTIFACT_ID}") + bootstrap = _bootstrap(client, artifact, capability) + head = client.head(f"/artifact-download/{ARTIFACT_ID}/content") + full = client.get(f"/artifact-download/{ARTIFACT_ID}/content") + cached = client.get( + f"/artifact-download/{ARTIFACT_ID}/content", + headers={"If-None-Match": f'"{artifact.sha256}"'}, + ) + first = client.get( + f"/artifact-download/{ARTIFACT_ID}/content", + headers={"Range": "bytes=2-5"}, + ) + second = client.get( + f"/artifact-download/{ARTIFACT_ID}/content", + headers={"Range": "bytes=6-"}, + ) + unsatisfied = client.get( + f"/artifact-download/{ARTIFACT_ID}/content", + headers={"Range": "bytes=99-100"}, + ) + + assert parsed.scheme == "https" + assert parsed.netloc == "download.example" + assert parsed.query == "" + assert capability not in landing.text + assert landing.status_code == 200 + assert "script-src 'self'" in landing.headers["content-security-policy"] + assert landing.headers["referrer-policy"] == "no-referrer" + assert bootstrap.status_code == 200 + assert bootstrap.json()["content_url"].endswith("/content") + assert "secure" in bootstrap.headers["set-cookie"].lower() + assert "httponly" in bootstrap.headers["set-cookie"].lower() + assert head.status_code == 200 + assert head.content == b"" + assert head.headers["content-length"] == str(len(content)) + assert head.headers["content-type"] == mime_type + assert head.headers["accept-ranges"] == "bytes" + assert head.headers["etag"] == f'"{artifact.sha256}"' + assert full.status_code == 200 + assert full.content == content + assert f"snippet-{ARTIFACT_ID}.{extension}" in full.headers[ + "content-disposition" + ] + assert cached.status_code == 304 + assert cached.content == b"" + assert first.status_code == 206 + assert first.content == content[2:6] + assert first.headers["content-range"] == f"bytes 2-5/{len(content)}" + assert second.status_code == 206 + assert second.content == content[6:] + assert unsatisfied.status_code == 416 + assert unsatisfied.headers["content-range"] == f"bytes */{len(content)}" + + +def test_capability_download_rejects_missing_auth_tampering_and_wrong_artifact(): + content = b"protected-artifact" + with TemporaryDirectory() as directory: + root = Path(directory) + path = root / "clip.mp4" + path.write_bytes(content) + artifact = _artifact(content, "video/mp4") + context = _context(root, artifact, path) + issued = ArtifactDownloadCapabilities(context.settings).issue(artifact) + capability = parse_qs(urlsplit(issued.url).fragment)["capability"][0] + with TestClient( + create_app(context=context), + base_url="https://download.example", + ) as client: + missing = client.get(f"/artifact-download/{ARTIFACT_ID}/content") + header, payload, signature = capability.split(".") + replacement = "A" if signature[0] != "A" else "B" + tampered_capability = ".".join( + (header, payload, replacement + signature[1:]) + ) + tampered = _bootstrap(client, artifact, tampered_capability) + wrong = client.post( + f"/artifact-download/{OTHER_ARTIFACT_ID}/bootstrap", + headers={ + "Origin": "https://download.example", + "Sec-Fetch-Site": "same-origin", + }, + json={"capability": capability}, + ) + traversal = client.get("/artifact-download/../../etc/passwd") + + assert missing.status_code == 401 + assert missing.json()["error"]["code"] == "artifact_download_capability_invalid" + assert tampered.status_code == 401 + assert tampered.json()["error"]["code"] == "artifact_download_capability_invalid" + assert wrong.status_code == 403 + assert wrong.json()["error"]["code"] == "artifact_download_binding_mismatch" + assert traversal.status_code in {401, 404} + context.application.open_artifact_content.assert_not_called() + + +def test_capability_download_rejects_expired_and_unsupported_artifacts(): + content = b"expired-artifact" + with TemporaryDirectory() as directory: + root = Path(directory) + path = root / "clip.mp4" + path.write_bytes(content) + artifact = _artifact(content, "video/mp4") + context = _context(root, artifact, path) + old_now = datetime.now(timezone.utc) - timedelta(hours=1) + with patch("vidxp.artifact_delivery.utc_now", return_value=old_now): + issued = ArtifactDownloadCapabilities(context.settings).issue(artifact) + capability = parse_qs(urlsplit(issued.url).fragment)["capability"][0] + with TestClient( + create_app(context=context), + base_url="https://download.example", + ) as client: + expired = _bootstrap(client, artifact, capability) + unsupported_artifact = artifact.model_copy( + update={"mime_type": "video/webm"} + ) + unsupported_context = replace( + context, + application=Mock(spec=ControlPlaneApplication), + ) + unsupported_context.application.get_artifact.return_value = ( + unsupported_artifact + ) + with TestClient( + create_app(context=unsupported_context), + base_url="https://download.example", + ) as unsupported_client: + unsupported = unsupported_client.post( + f"/artifact-download/{ARTIFACT_ID}/bootstrap", + headers={ + "Origin": "https://download.example", + "Sec-Fetch-Site": "same-origin", + }, + json={"capability": capability}, + ) + + assert expired.status_code == 401 + assert expired.json()["error"]["code"] == "artifact_download_capability_expired" + assert unsupported.status_code == 422 + assert unsupported.json()["error"]["code"] == "artifact_type_unsupported" + + +def test_public_download_requires_no_api_bearer_and_rejects_cross_origin_exchange(): + content = b"no-login-required" + with TemporaryDirectory() as directory: + root = Path(directory) + path = root / "clip.mp4" + path.write_bytes(content) + artifact = _artifact(content, "video/mp4") + context = _context(root, artifact, path) + issued = ArtifactDownloadCapabilities(context.settings).issue(artifact) + capability = parse_qs(urlsplit(issued.url).fragment)["capability"][0] + with TestClient( + create_app(context=context), + base_url="https://download.example", + ) as client: + accepted = _bootstrap(client, artifact, capability) + case_normalized = client.post( + f"/artifact-download/{ARTIFACT_ID}/bootstrap", + headers={ + "Origin": "https://DOWNLOAD.EXAMPLE", + "Sec-Fetch-Site": "same-origin", + }, + json={"capability": capability}, + ) + forbidden = client.post( + f"/artifact-download/{ARTIFACT_ID}/bootstrap", + headers={ + "Origin": "https://attacker.example", + "Sec-Fetch-Site": "cross-site", + }, + json={"capability": capability}, + ) + + assert accepted.status_code == 200 + assert case_normalized.status_code == 200 + assert forbidden.status_code == 403 + assert forbidden.json()["error"]["code"] == "artifact_download_origin_forbidden" diff --git a/tests/test_mcp.py b/tests/test_mcp.py index 68cdbdb..eecc238 100644 --- a/tests/test_mcp.py +++ b/tests/test_mcp.py @@ -1,6 +1,7 @@ import asyncio import base64 import contextlib +import hashlib import io import json import socket @@ -143,6 +144,9 @@ def context( ), mcp_allowed_hosts: tuple[str, ...] = ("127.0.0.1:*",), mcp_allowed_origins: tuple[str, ...] = (), + artifact_download_public_url: str | None = None, + artifact_download_secret: str | None = None, + mcp_stdio_filesystem_accessible: bool = True, ) -> HttpApplicationContext: settings = VidXPSettings( repository_root=root, @@ -152,6 +156,9 @@ def context( http_trusted_hosts=http_trusted_hosts, mcp_allowed_hosts=mcp_allowed_hosts, mcp_allowed_origins=mcp_allowed_origins, + artifact_download_public_url=artifact_download_public_url, + artifact_download_secret=artifact_download_secret, + mcp_stdio_filesystem_accessible=mcp_stdio_filesystem_accessible, ) application = Mock(spec=ControlPlaneApplication) application.list_capabilities.return_value = () @@ -261,6 +268,29 @@ async def test_curated_tools_have_structured_output_schemas(self): '"declared_mime_type"', ): self.assertNotIn(forbidden, serialized_upload_schema) + artifact_output = tools["get_artifact_download"].output_schema + self.assertEqual( + set(artifact_output["properties"]), + { + "artifact_id", + "filename", + "mime_type", + "byte_size", + "sha256", + "etag", + "state", + "resource_uri", + "delivery_mode", + "local_path", + "file_uri", + "download_url", + "download_expires_at", + "delivery_error", + }, + ) + serialized_artifact_output = json.dumps(artifact_output).lower() + for forbidden in ('"base64"', '"blob"', '"bytes"', '"content"'): + self.assertNotIn(forbidden, serialized_artifact_output) for name in ("search_moments", "query_video"): schema = tools[name].input_schema command_ref = schema["properties"]["command"]["$ref"] @@ -415,6 +445,43 @@ async def accept_url(_context, params): result.structured_content["upload_session_url"], ) + async def test_real_stdio_artifact_delivery_returns_local_file_and_resource(self): + with TemporaryDirectory() as directory: + artifact_path = Path(directory) / "rendered clip.mp4" + content = b"real-stdio-clip-content" + artifact_path.write_bytes(content) + fixture = Path(__file__).parent / "fixtures" / "mcp_upload_server.py" + parameters = StdioServerParameters( + command=sys.executable, + args=[str(fixture)], + cwd=Path(__file__).parents[1], + env={ + "VIDXP_TEST_ARTIFACT_PATH": str(artifact_path), + "VIDXP_TEST_FORBID_HELPERS": "1", + }, + ) + async with Client(stdio_client(parameters)) as client: + result = await client.call_tool( + "get_artifact_download", + {"artifact_id": ARTIFACT_ID}, + ) + link = result.content[0] + downloaded = await client.read_resource(str(link.uri)) + local_bytes = Path( + result.structured_content["local_path"] + ).read_bytes() + + self.assertFalse(result.is_error) + self.assertIsInstance(link, ResourceLink) + self.assertEqual(result.structured_content["delivery_mode"], "local_file") + self.assertEqual( + local_bytes, + content, + ) + self.assertIn("rendered%20clip.mp4", result.structured_content["file_uri"]) + self.assertIsNone(result.structured_content["download_url"]) + self.assertEqual(downloaded.contents[0].blob, base64.b64encode(content).decode()) + def test_remote_mcp_is_stateless(self): with TemporaryDirectory() as directory: context = self.context(Path(directory)) @@ -506,6 +573,94 @@ async def accept_url(_context, params): result.structured_content["upload_session_url"], ) + async def test_real_streamable_http_artifact_delivery_hides_local_path(self): + token = "s" * 32 + listener = socket.socket() + listener.bind(("127.0.0.1", 0)) + port = listener.getsockname()[1] + listener.close() + with TemporaryDirectory() as directory: + root = Path(directory) + clip = root / "remote-clip.mkv" + content = b"real-http-matroska-content" + clip.write_bytes(content) + digest = hashlib.sha256(content).hexdigest() + context = self.context( + root, + static_token=token, + artifact_download_public_url=( + "https://public.example/artifact-download" + ), + artifact_download_secret="d" * 32, + ) + context.application.get_artifact.return_value = Artifact( + artifact_id=ARTIFACT_ID, + media_id=MEDIA_ID, + kind=ArtifactKind.snippet, + profile="source_mkv", + mime_type="video/x-matroska", + byte_size=len(content), + sha256=digest, + state=ArtifactState.ready, + created_at=datetime.now(timezone.utc), + ) + context.application.open_artifact_content.return_value = ( + LocalFileResource( + path=clip, + filename=f"snippet-{ARTIFACT_ID}.mkv", + mime_type="video/x-matroska", + byte_size=len(content), + etag=digest, + ) + ) + server = uvicorn.Server( + uvicorn.Config( + create_app(context=context), + host="127.0.0.1", + port=port, + log_level="critical", + ) + ) + serving = asyncio.create_task(server.serve()) + try: + for _attempt in range(200): + if server.started: + break + if serving.done(): + await serving + await asyncio.sleep(0.01) + else: + self.fail("The MCP HTTP fixture did not start.") + async with httpx2.AsyncClient( + headers={"Authorization": f"Bearer {token}"} + ) as http_client: + transport = streamable_http_client( + f"http://127.0.0.1:{port}/mcp", + http_client=http_client, + ) + async with Client(transport) as client: + result = await client.call_tool( + "get_artifact_download", + {"artifact_id": ARTIFACT_ID}, + ) + link = result.content[0] + downloaded = await client.read_resource(str(link.uri)) + finally: + server.should_exit = True + await serving + + self.assertFalse(result.is_error) + self.assertIsInstance(link, ResourceLink) + self.assertEqual(result.structured_content["delivery_mode"], "https_download") + self.assertIsNone(result.structured_content["local_path"]) + self.assertIsNone(result.structured_content["file_uri"]) + public = urlsplit(result.structured_content["download_url"]) + self.assertEqual(public.scheme, "https") + self.assertEqual(public.netloc, "public.example") + self.assertEqual(public.query, "") + self.assertTrue(public.fragment.startswith("capability=")) + self.assertEqual(downloaded.contents[0].blob, base64.b64encode(content).decode()) + async def test_media_upload_tools_enforce_permissions_and_availability(self): with TemporaryDirectory() as directory: context = self.context(Path(directory)) @@ -932,8 +1087,135 @@ async def test_clip_submission_and_lazy_artifact_download(self): self.assertIsInstance(link, ResourceLink) self.assertEqual(link.mime_type, "video/mp4") self.assertEqual(link.size, 12) + self.assertEqual(link.name, f"snippet-{ARTIFACT_ID}.mp4") + self.assertEqual(linked.structured_content["artifact_id"], ARTIFACT_ID) + self.assertEqual(linked.structured_content["filename"], link.name) + self.assertEqual(linked.structured_content["mime_type"], "video/mp4") + self.assertEqual(linked.structured_content["byte_size"], 12) + self.assertEqual(linked.structured_content["sha256"], "1" * 64) + self.assertEqual(linked.structured_content["etag"], f'"{"1" * 64}"') + self.assertEqual(linked.structured_content["state"], "ready") + self.assertEqual(linked.structured_content["delivery_mode"], "local_file") + self.assertEqual(Path(linked.structured_content["local_path"]), clip) + self.assertIsNone(linked.structured_content["download_url"]) self.assertEqual(downloaded.contents[0].blob, "Y2xpcC1jb250ZW50") + async def test_isolated_stdio_preserves_resource_without_misleading_path(self): + with TemporaryDirectory() as directory: + root = Path(directory) + context = self.context( + root, + mcp_stdio_filesystem_accessible=False, + ) + context.application.get_artifact.return_value = Artifact( + artifact_id=ARTIFACT_ID, + media_id=MEDIA_ID, + kind=ArtifactKind.snippet, + profile="compatible_mp4", + mime_type="video/mp4", + byte_size=12, + sha256="1" * 64, + state=ArtifactState.ready, + created_at=datetime.now(timezone.utc), + ) + server = create_mcp_server( + context, + default_principal=Principal( + subject="isolated-agent", + scopes=frozenset({"*"}), + ), + ) + async with Client(server) as client: + result = await client.call_tool( + "get_artifact_download", + {"artifact_id": ARTIFACT_ID}, + ) + + self.assertFalse(result.is_error) + self.assertIsInstance(result.content[0], ResourceLink) + self.assertEqual(result.structured_content["delivery_mode"], "mcp_resource") + self.assertIsNone(result.structured_content["local_path"]) + self.assertIsNone(result.structured_content["file_uri"]) + self.assertEqual( + result.structured_content["delivery_error"]["code"], + "local_path_unavailable", + ) + context.application.open_artifact_content.assert_not_called() + + async def test_isolated_stdio_uses_configured_public_download(self): + with TemporaryDirectory() as directory: + context = self.context( + Path(directory), + mcp_stdio_filesystem_accessible=False, + artifact_download_public_url=( + "https://public.example/artifact-download" + ), + artifact_download_secret="d" * 32, + ) + context.application.get_artifact.return_value = Artifact( + artifact_id=ARTIFACT_ID, + media_id=MEDIA_ID, + kind=ArtifactKind.snippet, + profile="compatible_mp4", + mime_type="video/mp4", + byte_size=12, + sha256="1" * 64, + state=ArtifactState.ready, + created_at=datetime.now(timezone.utc), + ) + server = create_mcp_server( + context, + default_principal=Principal( + subject="isolated-agent", + scopes=frozenset({"*"}), + ), + ) + async with Client(server) as client: + result = await client.call_tool( + "get_artifact_download", + {"artifact_id": ARTIFACT_ID}, + ) + + self.assertFalse(result.is_error) + self.assertEqual(result.structured_content["delivery_mode"], "https_download") + self.assertIsNone(result.structured_content["local_path"]) + self.assertTrue( + result.structured_content["download_url"].startswith( + f"https://public.example/artifact-download/{ARTIFACT_ID}#" + ) + ) + + async def test_remote_artifact_delivery_requires_public_origin(self): + with TemporaryDirectory() as directory: + context = self.context(Path(directory)) + context.application.get_artifact.return_value = Artifact( + artifact_id=ARTIFACT_ID, + media_id=MEDIA_ID, + kind=ArtifactKind.snippet, + profile="compatible_mp4", + mime_type="video/mp4", + byte_size=12, + sha256="1" * 64, + state=ArtifactState.ready, + created_at=datetime.now(timezone.utc), + ) + server = create_mcp_server( + context, + default_principal=Principal( + subject="remote-agent", + scopes=frozenset({"*"}), + ), + artifact_delivery="streamable_http", + ) + async with Client(server) as client: + result = await client.call_tool( + "get_artifact_download", + {"artifact_id": ARTIFACT_ID}, + ) + + self.assertTrue(result.is_error) + self.assertIn("public_download_origin_unavailable", result.content[0].text) + async def test_application_errors_are_machine_readable_and_safe(self): with TemporaryDirectory() as directory: context = self.context(Path(directory)) diff --git a/tests/test_packaging.py b/tests/test_packaging.py index 5e76ac7..9a78e4e 100644 --- a/tests/test_packaging.py +++ b/tests/test_packaging.py @@ -126,6 +126,33 @@ def test_upload_page_assets_are_pinned_self_hosted_and_packaged(self): (assets / "THIRD_PARTY_NOTICES.txt").read_text(encoding="utf-8"), ) + def test_artifact_download_landing_assets_are_safe_and_packaged(self): + project = tomllib.loads( + (ROOT / "pyproject.toml").read_text(encoding="utf-8") + ) + self.assertIn( + "assets/artifact_download/*", + project["tool"]["setuptools"]["package-data"]["vidxp"], + ) + self.assertIn( + "recursive-include src/vidxp/assets/artifact_download *", + (ROOT / "MANIFEST.in").read_text(encoding="utf-8"), + ) + assets = ROOT / "src" / "vidxp" / "assets" / "artifact_download" + self.assertEqual( + {path.name for path in assets.iterdir() if path.is_file()}, + {"index.html", "artifact-download.js"}, + ) + html = (assets / "index.html").read_text(encoding="utf-8") + script = (assets / "artifact-download.js").read_text(encoding="utf-8") + self.assertIn("./assets/artifact-download.js", html) + self.assertNotRegex(html, r"https?://") + self.assertNotIn(" -
      -

      Preparing your VidXP artifact

      -

      Verifying the download link…

      +
      +
      + + VidXP +
      + +
      + +

      Secure artifact delivery

      +

      Verifying your link

      +

      + Checking that this artifact is ready and authorized for download… +

      + + + + + + + +

      + This private link is handled only by VidXP and is not stored by this page. +

      +
      + +
      diff --git a/src/vidxp/assets/artifact_download/vidxp-logo.png b/src/vidxp/assets/artifact_download/vidxp-logo.png new file mode 100644 index 0000000000000000000000000000000000000000..07e4996a94e4068c63e27b1e3f931e3e2e45512f GIT binary patch literal 21616 zcmV)qK$^daP)bG20WrD zs>)ATsZ=8V9fn~rckbLBJ3Bi^%4?(KZzJU8VL=cKmfr*9-+kqIZ~5IveplUL`p8eE zm9LYZ!2LGvd9(MG{dgt+vzySxm>5qN=^^x4;iRP@c&Z@V1k}X}F2wDXuLJs-;$}6uN zSg+Sl6QLe1C$?*k9zFV0t5s90)p&2}himf}>W=lkZrleYfM)MQ5OPhq=Peww==CxM zNZph4bNN7EQum8fus;>K|A8K~tFzOH@cCIPck@NLr+0*1U6T(w=%APR%mWWRP#Zsf zeBCe9ze@o)*kOkq*2VMLvu7VKC-(yp_yK+T^f9vX&HDB0jVMFpE|{C9+MPVUd(B_a z2O->#@=>TTt-RkWzZJSaLdp`v)TN0M@PpmdiLyjHmiK~>eZpV9y+sC`3Do~ z2W4S4#C6&qFUsumrhl`i?4L*Ox&5UI73xO_Hntn?^Dnp^m&$=`*@u@tC(;B2`rEo za#|T0)`8U@|JxddAn`X@?Ln=7|NdeTmOm>mo^$A-ht6rH1VNGTc^!ZM(n~M>vzYo{ ziiyYEcgpK+A45TJOzY6{bthIfPlBx9&sqkzs*@|6mCVz?9>RaF@!Y9EN?F`zy886) z-L_`!nzdb>(TPVMIpKjaB`8vW3c`Q+<(I!Bn)FjK_Z+yq~{q=yw!dYfxrM-E{^@GoM8Lji1nzyoUvxP=+K% zu)jlH;kjc*v0}O7%iC)mon2j(N|gxW*8A?e?@)w~j0-QuYX?ZbSFc{Tty{OwIDcQ+ zz>w%4!`8bM`9a~=qR=k4vO{OWyIJM_Z~R{vMaAx+{JB?#zzdWxNtnPmp!DnM`E<>)<|LA2APyf9Zsw}S0 zn)n###eXaI>p5jG4fCHAcpMEF2PEz1gOp;3jN3cfI|a)PTD>Z~THbYt6TVt=bpe9- zRiF~z|6;&^0Ye<%tCpD5U_(fxk3#4uFuz6f4TyNqph0Hv;KBMFp@VDQefM3nc=2N8 zGTyHh$eQA?wcj*?cacz*lG zdFP#Hw%TedBRt0jDMC4$mq6yGz%7)9e5PB#mp*R#x%2xOAenj|R;txXS68RFg09_8 zJn_WE7hQBwrRLcLx%hvB&h>u(A3^wD@Pb)X@N3qrG0!~njCty*r_92I3zeG23LxM; z_SnO0vBehp9819KTrZz_zrs%mH%siqg3bsw=?a=5dlTho;zHqv8Ll8D{KR((VJE`x z?ANdVpp`3@Unp<;KNBWQs05DU-Cu6VV{#)RcO!y-1i_2NQsJ}0r%s(}X3Usj`t|E) zcG+bYGiuZ*v*V6Cnju4msIajJn0g;IW?X=TQTV8V)c825j>rGg`kHrH)Vd4>* zgsbMJn{G0T7A-P+@4dJA@|VA?LKhsPt9rqL1?Gt-oy}`x~U2ttH8B_4Z3pyg~+~?z{vXXu&i{Yx;(bgr=6>0-W#$*Z_=PHh zNA6q*ihIi~x0uHsd(7;%+ivEFBaSdT?X;7T%%uwWwbx!V&p!LC{^s(qAh-xgZs~i# z04PzmPN5LBH(T+Uo8BtYPn&sr{IGyW$SPFw`w9SG>U~*C8V*G6-LlR~uxCwf%FdWh z6d)t;;Gefjd``aG%o*|@qG={L{d6|VXeC@W|4k}Eqe*b%iAJxj=Y_rYGZMWSPM%!yK=UGXX}P{blix{-SJ#X4a{^1VU5z5|GVD^`N9 zg!(v`eT0EuM)1N|nV|M;>`Z z%{<@`RL?a53?&Pu??YiE*`+QZNf*1?x;W8Un!qV#^s?3afR_a3!J)|rAd*Az>*>1b- z)X!tqQ4lc9_rL#rWdn)ziRaW5JezxxzT#WK4cK984khoC1`Az5<0S4D&LtC)`(y6Z)gKZ<2puIFyy;+b-~k7UGE^v@C~yZGfb64uI9G`JGpjr8 zYFe=X#W8=8r79w?Lj5uQzyLi9(xDr@Xc7~TsRr%;>%ac1Dg!GSq<`g=SDOF$kN;3h zfKuS^dEci${b@69+&CqFupB4~nBmk@Pc@@QkJjS>_D~u~BUnL9Kfq8bXO$n|@$}PA zn<-PKm=nc?7)_&x&B0u^wC#qPd=r1CDNp8RMvw>}6o1`y*Qw9vbrgly zeSAN8@?^CF(8W9Nyo=s!R=;8e`EINTcVMb0&(TL86Gs}xM5O?*bqY6=87$%UbMB(h zv^VAX^YnBHZV9zUF*Nlk>RCw?dVXX?4=Yq^64VM{&Jh-`W8&fWiSM}pn7m6axkRa7 zIDaAn2%W$D4_yDCqA#feM=j^l3*3Yb7y-EeFg&@w<;WqZ&XCFNVp(V%A z#+HJ7KmN2H*7f|l>Rs|%e!Bc~`#P(uk-K-t3A1_n{P9hgsSgOFoLh(rNS>&GyNJ&@ z%-0@ppgL|pjC$AnA3q8Z$oF!y-+JpUMf?|Ee6gBsg!iK#{m2}0$RP^g5Htew3c18(2ngggZ~`GF7J|?4{xG!J zL2e+z^wmT?diD?yS&hA9c4hNk6oBJ(bauqb!2270Zbj9+QB$8^T6hT&r|4)DVlL}w@ z7Zd~{B$R?&0ZE>7&plT`9u~#7SM}=EQ$G(7Y{vySs(IwDyDGi>{f zafMh4GJ`t`S;T6dHR~laP3j5Y2@X2wVBr)VHobcH(sP{s^2?e|ep?hObnXi9h~gsb zzJ2=W^+*A(St|;$WXZc`-THN=eSNzs2yShQEjAa0+QICw!w#YpL-f2+D9%$YL)<#o znqa#ZczOR}n5ZJgGSxon3?Bdu)J|*;NB`U3{?;6R_~B;g(4l&R;PXN7gn+DS7((0y zDdB(lmw!?B;DyhzI4=1D+ahHz93io)QMMg-+`;T4{=O!b0V`7%6TP2g4qp(G3C3{v z@NHFa-}>e^)z$1G^#WtY>}j^yW*Z|lb*4?ubDda*_3PFv7QmDSQdiTnXD{UwdiUuq z$F31Y8Pgi`yaSpl^#-R)TljFG1}WZ6!0c%?wcikS`l)yJchFX zzCR(TeH2X?e)W@@WtB@;tAeLlvu3G~FTC(VeVvs8BM9FQ2EbP%B*H!3!;M4Wn0<&J zo})~B)<5{nqmMqSQNdw{O%NdtFzduJwBy@Dic5N`n~+W#^M^nDK@_pg>^^FD;URu5 zuUGVZ779k0HEX5{6w_Z7mjGuF#33V$VqF!Ycdy>6D7dJtx7u1){qCoX(re*ZQhO<( z?bFI!ZnfoB<^Un8P&6zR7K;wxWIiBjg61PrmZC0dKuuQ`YxYxuudD(n8x+?Jd`P%} zFK4ymtEG1>ZY#g}=9`U_De1|xqW}EoKdaA&x5vx_7~upk`+VNV_#}XqE?uTTa0SAb z&vV18a@@P`zFXgWthf)_sxj04h5{Br{SzlnRGe^vgrHkW{lKfDWH;S(qgsN&gEuuJ zMvO2ANvH@GL2)?`ltvON^w8<8LP2>ySmfO$7Gpf|#N)c^aT&NAKqmYHKe)1|o_bPT z)@1!VR&TdmcM}D(uAveHO{_r+eFz14fQAu>tBfmI1qc$HK=5HAe`0*lKdT$zQ_sgL zppb*#{EgK@WPtDyG*mD}9+YwM8cHJ7Ow21#r69!f!Ux*iCSa0+5F!bbMZ->N2^|4+ z#T9>(?|4Zq5We{-0f3K*wb)wB{SikTsRCcQYNc5)e}Q@YvBzbFu8FM<$MLwnhbS04 z2H%CN06^kqIFEPVdsj=>$YWgjw=4C1D8OjB4kDC;4?e^^Ek&%8Z=P%(oc^FW>Ex3Y z*e0!5HvTUzJ?pM8HDet@`$IuUv@u-(HxFUnc;k%<{s`-M4Nf0c4^oEJ1=EMGBsyS4 zfCFIb0E?_NtOh9k>Z`9YXP$MY`gvVBZsp%DX=HK+d+aeXSwP)YVo7!o zUSPzwBh)056TsT^>)TIi3Fn(vWCe3xV38pLL{X4_y83%6s%P~R=6T=>7Fi-De);kh zqMYw4zS&`i9o3xx08u)W8mjyG=btmH?F zg|bv*B}fZBTd-0+PfJN{E-fVCIZbhqz$reh;*H7F{Pg(saSZBS{tX{R+yuJ8svwDq9Dpj2FZ8>a9 zi5zw@yUF$JEegGC*;4cBE3axE0&;3s@$=xGz4qKw6%A`}dzll@72o{qLU>hc@cZ)5Cs?b-7wHP>XkQuSv2s3<};ij($-qNXcOZe!Vq(B7S zLzJzjEhm@1aSa>`OU7~Dd-pxH9AE)3%N9eoP(~3qhBCh??^7@+N{)haJ$DHAfzq9R z#_3Y9GD7bc3+0Nx{?+Uwxai2Ej*1B_(_MbTG-%{(meC|!fDmENZgdO4O#>mL{j3-O zlJ%E6sIZCqLER_{f}K8nx(XhXj&Q*bcieHO`bkXiDp}>k;BSdJpEqx=0xLo_zKffS z!caa2DC{Z6M+ph_pn}wbxj2WBV%LoIn3{HS?slx87!JeVs@Eg6j3xUssGm7<}?6r>F}#Rovk$ zsb9GIs;kYxha9Z<044p#KmW72^wQs}HRC-i1Y-o1pfD5Jv^1=ssD<1^^&8*##>E>; z0W7y3s)>hnyHW#a3p)5GlYURXhD{|V)A39KTxfTLwa;G3ao0X!(mxN!%` z`+BIbJa9tztgzm~?4c061{Ppdqj)>-va`~)uf6uFGJ@;d*UNYJ7l62fn%+GmtqhjH z$77*LDDkt+*4x+u6k+{X`J}B;?B&as8D{n1Qg+^HX9b$9T#8f|OXxR5SWH^fungb` z%s&t67Y2#z8asBZ+><+%JmPy%V2WDkd|DvZWlwQ!>yQ%$$rY;GjVfiUOPz4SOxv@m>FocR*-a}&3c*NE9M+dJ-rncqp1McZ$`ovw0B zG(-U@vhESc1`GMw^ME7@P+fp`)WSW#*%-3&?^)KU6@_v+yDt z&jt8yUP2*OE@qB4Zu}z^zMyDs2(AG{KYR8Zy?Lx`1dFz#ERaQH^~lyCK)@VI0fkB! z$O`-AFMp+(I?}VC^-V-6FwcVr4^o!_$m3i{0gu{!l!lmO##p&nCC&xjAAfF}Mr;(N zwE!Y4P4{k2tNE6yI0`}~=`(BQEDa;E4k#KaV9h!T4zL8!^QuTSu`s+wV(C_ih=y-F zT<-~Z0f2d@;2xBKNP^#w3or%%kCgC|g#zHbNr*J?g5oMS8=x@eFQ_r_3eZjN`z z;BuFW<-~nahi}^8reP8}Q9v2KQ_a7;4oy2;{>{zq=vb*100r28zx`!pw5!SHn52$Z ztX!_3Pd_WGvVZ>p4pc@O26Eh$D^{AhVu8GG_ZWo&4mGQe-}kB7Hi;Y{iSX%!ikwrI z>nC6}B3RtgE3eFv3g!pJ$8WBsZ61)GB4{5am^krz;r)-$Ye8|Lo3Fd>T64;&r)eHy zoS1e%-e3OwC-a3bo~JwmE`TtW%BY>hLh%DQMF~Fjna}985E(?dxdBC`X)YRzY{~mZ zR{(rjKNnwP=JCP4xs&It1Sn8)1rRpvE-|uT!5iW>4px+o61+C|HPgSJ9sj~{P$VLT zC!TylZzK=CM_0cnL*IUV747r=tLD9~7z8W<@uKu@4_oQOjUs8p_p$=X9FprtC`;a5 zBBuLIO)bOBZ7D#KbN1JTphpWawaYFeHID&k^8gU#Kp}7gta?oO=+S%XYCiMKvxL`} zXg=|YGtEG`9u$P+*3CEHBqeg6P$fP4>~pjR0wutL@SaP5|2y-%fM!T183|~W)kI~h zKHn~8HlhOfmEo&z8j14U&Px za`8G50g=zm()oOj@C1AZY3)D$@egYL;XEL#zyXs5+kEES&#D_d=GbGEK*A#A%gPIi zQ*%^tXeuAzT)mcnV51n+R_3zuc#yB}EjOQ)wTxI=Of`b$rp=QTJ3xenazM>eox|t2 zaqDGu@VV=*zfK62d1@{2@2q|l4O5MI#$OUXY1go8C{)-~QG&^`=8I`Qa3t6JaFB+;h*p)M7#1LTYK0bUpo zb`o)bU#MV0akW6jBnwJSL6NDi3bJ(!wT&(O~`C2ucnp^A2$w*W3fU!wM|pMC=EUorjI|O zn1r0S-+r5!;#$v9CQ$jI<0mM9Mfr%u5jLjX@))nEBJiBbqV|sUst8+cxwVFiWcsjl zkU3p~-cOebXqs5CR?xqrB@sk|>H%RPH62|WU=EhN!IWF4n9qLpa{_ofB$;%SxR6^k zS^+SQ7XW?Twb!bZP%9fog|h~-a|av|xN(L-C22{Y#!VU?+u&(qeeVuf#eBw&q?Zs% zSVNv;@(CkZ)$4Ti_tw-h7KGJqr-bzsL5Dz4Je<{bEIJOzyIs!<|~rICT91&2klzhXB4v##x7d)mP9*`X%qln zp^gA0n=s)pEhB$O!da~e6(ywzg@4zG>r zH`9*<44F9id=f3b)Qe+4uM+e72MGNvNh}dpBY4cB7kpSSOE3AbmcZVUbuPg}H1XxH zd_|g4f2~Cvc38zybFFav7kurjlDPVv`h8puiit(oZ~p_7=7ytqbKx7Rlpgc!yYGII zKNzH(1{Mw6gXxD2tZHRcU6uF22dC?Kz#mKy+ME(H5>iwaLwS?UEa1|hXqixOp;JAdFn}s#NBwO

      IGO}*Fyz@2}Mx=fhuqTUjyW( z`Fj2b1ZlGp`5R{Wyz|c!(?3EL#{$pd!Hg!;d||TS$?lM%Hx+JR`JXSIaqLp{Q<}|N49l|FCX;NagOr@q_+pcqPgp5rtwsBlIsyK3v=q zzMWJt3JE>{08_gFMjE!|Fu9KdwN8;^VgV>nAy)v+9ED+KE^%1eK#~N3(T6jpwQ(}G z3uqIK|BY+>#N5NvbF!I)i zOU8RiJHryfNMa?pZdUcHLWScBI$V>=9AOz*3|IkD!i1J;F$F-69J!lX0nQLtu)WaQ zSOFOeoZ70V$&l(e3iv=rzm|B)#4vNN^afSby`hCHWL9t`NAjc zEU`X$iU0oZ|I+xsZ(p0uO6!0!=aRY3TxX5Npb zds!k2&;FrMuL#iOUXXWy1qE;LJ=+OojU^;{FhPP8iUD7~hA=F_0MQ30{D#)??Pxj| z#$%Dh(3my+DJO?Xqq4`gU{%%hMs8*RKn_2S;@~$C23k+XjAKF{!CYBT*w{V59}WPee&d+1({!?g6PrKrf!Vf0u1P5k3Fi# zg8#==fK@m$8Pa|B-AC&hbZ(J=MVLY&9+ZZ2LeW$q#F7B`_|Dga4dr`=Y-$H$E?&H- zv3ZwCjgyc3b%$XiGN=>?KKQ$bCWpm05Mh!^1u|nzs!ua3bRGJtdCR(e@l*8R2tl6khqm`MX zXmm4CZ1Mv{D-@cN^!lqbt!ma%goW8h0U(%2Un4}6eXS_dK2j@40SS~YE{t%J2Z}|6 z@wNyR>w^-Z9J~*3yPYTm3@Z1P@R;xyD`Xi>tzGo6KQO*GAcKD@e{TV3@lDEiuKJAL zJ7T8rbEHEz6AOSau?C=KKeUl#5FwG*N-owHlfwF85dda@AgW|AhZ^Jiaw(G*X3F;^ zKwzqu-n3u=B9psLFhIZlLOILL2hsCGL_ie~)k;@h`8Rb-B%^R~C;`g;?6c2kxT^Ds z0;3!Nq!6Z@Okrn7r&ft>I7C9+(LPdn#Qh;h zp^YvQIdHE4%ZWa~f(BIonu2ZZ1d2Aq_ct^1MRn^4o_P8ZF@0PB2$v8J00(hG;RY** z3K}YDl=GJrZM$>(s2QxgHf?+(7GTDV7qxnad^*uUkeZlX4EplcAn6*)Tv&;shYr}v zIikFgOLrrDLRRt!_+r2v`GiT5>_K5s2x=B7_d>Amkp-3O(Qr`G)A(=%yZG(J`rRlP zibiAu`p3n*xAZ-I4O*GBH;abFLX86;aHUi+l>y=8wS_JW^gDuV;BHtYn<1Dq0IaL& z@2Q25OC{u^RGJlw@NN^T6(EI)Tq^l@LZcT&DX9Xf-gh=XK{(#NobqCDIz%D)J`>K@dEQQ<_7(p4O`vb#q%Qb=BZ7>BBG6PRjD z`B2FWVuCT_kU3x(UgK|oG;#}I2L4S%gb61}WYgg7npxyGU=J6GvLp0WQXCUSwk9#q z_VRUlUEC+4jJm61Ae>%n_j^h!zT4O>N^D{0L12(f*%m26@}J3r#^FR}dRCaYNn9Xl zQ-q57M~UW2W{r?$!i4eWMrp#N@87!zQ2y(Jh^fFOAwv!y#EeDYCiyN}E)?zWa6Eir zV`OQ=L{zXLLm7q%%eQ`Uq!J2AT_B+=Eo%6AfGJ5WQnip>xB^x(*YVVoPbrsy5OHA& z`lvOOSPIM~QF&93P>s^9d~xjm|k- z-`=QVlJEATZUdLi^z#9mS~z2wz(UiXeKQAcb9MFJ-|(96lJ&7CU+P<@LuZ!JP@k{n zCIaNR>6rI%;|??v1t6U!B~kRzQ3FU#JjbG_bGGEncM@zsN*E>4sKC_`phP538hhp> zN3J=`6ow_{JmJ<_0a{LL`9&fN-*de5!iQQDUMwjj7(^t#J{PAGVpCdmJ_4G<-TZwJlse zq_e{2g07VIY)%qtmiDfBvMTX)AXh4Q?UaumTKGu;1#A_XOqi#Q453vX*fC5VtKisU zkJIWNOgZ)WvtOPq*{v~JAWG)!3Bd;2NKC!LZSw*`1`u)VhVaWWV5aE=t z^D7a8&;C&YridTwDENS`Bjm*2cb6j9p~HqYTw{I*v0O4J;X*BK7$$b23>^>7DyC&x zH4+pq!Uw&#x!yJ|#<)pZm_HN-&BZ#r_0}Q6;K z70duOUycSVNqF|I#OL%L%$1x4O{^>g>iMCA$4MJJjjUM3n)B-%gL^{q2Yl1ZO)tYm zl6UBGeFcEC%icWoW)^K7|7s8%#&Z*04%r2kXZ0pm?)m<1ezVtDBH3 zbh}icdDi#T)z@({TsS95;$n>yDO&bVI%Xw(Nl_=tK*<%XA2$cnMGQ|p{*D5MD1X{d z%;R0sA243r36+;;zpVZdWdbd;q7Xn?{@WPSQqm{P2sccPQY&!* zQ3C+#&2~0a1u04nJ^YTaa$8CA3y_Cffav005zqJ1nL_;(1My+v_nJ1O80|*#Q%wE+ z?{MUftr6&mE0Vd#8WK9?T8BO< zwbl_SYpu{}<^-j7sTKw*tl1(w3N(zO+Fl9@@rPgoH~^|%X|f4JgU0RcC)CZyA>eJ-{LHER?XgA;vZP)a!gmBcyVX6r0I1Wr0MHS=)=m)4dVE2`; zd{tKeU0P9d-@W(fv=GQ5nj`KPOn`r*O9l%yeG8Cfw zpRgb^8z$Uc#rgc$4aEN6bIDM-V&<8mg1sXOG_ab-+Gp;mx< ziY4UxX3m(YjA&Ehs)?dEQx21gYV37a>`G6s{N!pfE{%(c;%AXdL}2EjEY~~Mi85(G znqQR&jD!y-Zig${SfBPNE*9WO0|))}06u&p=5%N2$2&$M1M=g82HGv+LMu-u(C~v#kS^2mFO#TYj-3vCLWSo12v! zK~fI~Ql_zkP97Q|7}@ zvj95jBeSbW|56N-!euf`V z&+cdJ0^V4$bl^}o*GJ>{x}Nm7gT|ScWI_jt5i9{|Ql@Izv9BwI3wTqDDw)|woUF{5 z+*p(V>yg9)c0cSA6CRc`eMP%_WPK0^cg<7wKo;j}BkW`^BhnC2!wZ3)7}$oqP{p+qHJGnT0vx8_1m z2<@gRfe+FOtu<>>6$_dL{gbYx<7LjASL}W~q9lx2;V^(dPLw(TFaRahC=D6H{*FT+ zjH1|*Dj(k8Uuo54+A{sw&wXASKwi-a-|bp7u~br^FpxOoxnhO*j19r+E>}@x7Mqo? zg@qZ#WEmAV-q!N1mZ6n@)76I$_w8-VMX(6iH?$ypJHp0R784!#bJYGtQ5;i%e8I&s}(nvtS+fYerQ17Po=n~88%-cD4BvNf%IX8 zed$YIR&2n8tvAJ$z$BswxCmAym9-XI*f`vnB4LL4(5`Ir#{vL_>$3x{Z`Rs7+XOl$ zZgpF_%+3c=^bc_p1RCM^Jq>L!Xf;+4oAq0v!q*~@WI<<(KriX0O<0Cs@*p;Jd7FyK zQ=ozKpHb>&I5)7*K6{G-%!qf#U{%w>T5+Kq;Efu16a-?3En46R7@5MCV%8bn__QGZ zlTJKI`}$~WA|uxBwtJ)Bjdrl<+8qV`xAfxC0vZ$tNCCKe!uKJD7oS zFZ><9YxC^eYtJ6VpsXVJbs8c7kXVjC{qb_`;U_bvGkx)?VG-*W=4jt$b%0ow7sS#* zS9_kFW)3VJa>abNY4>tXzlSQF{@xwO#`G@2J$Qj*jyYDCyn9uga zA)CcWI6i}LxOw#Eb6k4gzzs~$ve`3ROOLSklTKXdQ!0l1W{>W9+`TqOwQ#zCz3`2ot5tB#mp1yRy_Yb^1RX+G* zB7`ZoOp%6(AzH7FSqIUxG6^TC37|=W@NR}MU|@wI5(=?uSji|2pTim;RGq6Qgb8I< zvuD2)r%CN@9X<8)3{YSYGqrYvo|Ii%?sm9C*c_XCgDWG z24oi&iE3OT0K&RYf9A7sct>ao_(cJ*2H+CDAAW#E0~a!Au;oRduc?S4dSI3j+y?o5 zRzDTdOeTv3WKvtb5A@F1V<$d0s6&L57SW5c3>?1?0(pCbnjzW@(Fg)lc1 zQb|#`rkaOo)i8`9gb<;Z?HsDAV5zDdCGA#h@GwEJ2klk-4U9l3IUTk$1;k>(pkiNs z>1Cbe+u!Xl4%mZ~;>T|QL6t2bDc0Z}G4&HjI&zcb0{LAfdbIf`S3%eYg_s91?0A+yE1Z0$??jB$GwZY&Y|y(6iA% z(qhC@!ZKUi#pS^PlR{-w?0M&%cX7e4l~J4yHbpb)ZQ&soHC4u$pjiMm1$ZK)>tz%g zBhg6~(FMawWx574Z`n`*%YX?66ao%;uj&b}6C+h34kqG!z84Jf0Fh4(t@-|%4X0joa_UucpfWL8n`2B)pG`E!M9)>)= zF3BtaB?^EtkUJsDvV$H>!ASTKDIw-o*(Mk%$7~ za^aw1Qj)?CG~C&2^Ud@*fDZzP3>TCzd+Y#y7^sBM7IJBK@S!5qIXM!PkOhHD+C2}0ApI+%D!NKfbjsCK7;@pC^pM^w&_Xm)=Y|h{;BRv~dHGD?i6yYX(tP6utKNbQDB4sS;O!ViAg= zaO5*E>j)R6qcRD9&A)xAHOD1|O^pJ(fv$JsYhW)qS3*%R5f5IY(#emCL--`Duy88y z8MX(4=m%@?lH@%AeO%Y8Voms7%PbZ(yO@G)VhezJ!UdR&LL~Y+B?<^^6d=>7FV2K| zfpY;tDjFymH|FsGX8!KG?pD;ir{?%q>gvDup1Wm*&DXXtfDZHcD2`yV7nQ!O0AgVV zE+Cw#Qy;JuGqTblamX+d*5TJ#G4pg_LS3tHxVc0i@aVV?#;>C^L?nK|GmV=hvA_~+ z=K_t)`JxydZej^_`P3((7<}iFcb2FHfzMwirXR&+uFoo0xr0)27x3$F0|=E7vC4sL zXSbXMpODW09{_rJQ2K?k_ao6IRuP22m?0GSnBwzGt<3yWQVk3=$KNd4z9tL)^-oE%s$*g#DG1z-P$`P1c>o69b})O_yq zpI6WWHH?6+mu5)>&z^Zi3s?_U#6u5F*VTe6K>>z}PiL1+%rfa>l!2@vAs4=t1Pqp; zM=!gLi@#WWDMS@a+-e1L%N2nG8ZpbPG&l&DIHC_$zaLNmJ*xv71mo-@{Rt>7s~Wen zM1W(=0*at<5r9-Xw|KpE2lH)p)aH^r!=$6!SQPLA?v#uw$^@36w~%8Ej?1|tJJX>_ zWY&E9dXws4ZV*Q%Jxp4Ds{izbFI-#<^t=Tq*q1(IZnQxHa3!P+tA*M3T+k>f_vXg2 zHx4%hUr+o`&HMoI$&5aQx`nJ^1!D~WbGk}orSdn-Dor7nN3w#{1QOaoCizH#L=1v~ z0Yo`4|L=;wU7_2@hDt7NB|C!e!E`QhExrN?D?kVeE`SplB6tShZmWG)>S|<^z3$pY z7SHWrfZr$L0c)Um)I2bv1;t_^aISz!{JGy~@;R{vu#ww{HRArwkmCSYY2rXJA-9+_ z9CH8#J&n2TUkY~$OU?pFu6mO!dyguL0m8+>^ame|a+?Vx5M$Y{)~2LY8fFZ{{hMF^ zN)i28XW0!Mxe-qYl}pZm5Rnn8tU!Pr$QTSjhYSP`Rm8At++e^F1P(We8>^9mm~+e} z$0UZY=#2h&OLUuUBU{NNF;NaIkxe7NsVfXnN&F9Y0l&{N5yp%eFUVFf!)0fW1^U|2 zV#QDhTh!5}X>9-~pbZj;mHw6}+*|>>N#`W z{F|wtSTN@Na1Tz9Y$p}L@j%O*nQt_!WIb z0TMC)o4B#B;0NO51#X9sH9C|zQZQ#DgqkXXX4iC_T!(9}v{lN`*9Z^GvOug6!zmC3 zs}$k}nimp=mAbts2RnND-9|ZgDv}8)2?eR~$Nc~zAC{1nUO%6L<~-Q*2Ta6w0xX}G zYa-W=LZO%dK6)VNZom24UPE85GP%5Cy2B0A0@p zQ>RUfvc5grUY-42R$rH#BcQRi_~yIszSBJN@FSYO1@Pbx`5Wn92p5nvJ7noN6}fqs zUbbu}y9c5t8UPoN8XhbwmyDyGEo_HNX?KyVa@`5s2>^isyoj~wW+E#I;WJxkUnXyi zknh}1YV)CWXGn1ig2&=8qX+JSI9_)fb;B%Ql<201%QyI)zImMi(0)`L`3g9JXT+j_ zIj{)2!>PEJn{S$=Glp61oF4@-+wZWwT$i2qJ4Fgict0*M#sJAZfV%PdMT!xo`WZ8$ z;;{hLTBWNl7CAjuUZ)2aA9=ub9EV2A><03dX9hYq>P)c(Vj6aLUY zhi?ZXP<*mT$Fa{*w7f=Qc1ozov9q%LM$UG^cs1s_5fI3nIR9O-Hp@hb5GGN@PIAL_ zl9spw+zK27762cPi#b79J}eF{3zLuKfxtrPcpm_dYvOxB^nQN;=wB29S3~PJA)_6O z(?@uZr*tw3<{v;xNO|9V_s9kjrz^b;ck!@*-jhx~%~lpQ+vg`LkI3>CAhU>5w6RkF zT!3`kWL!Xg0SgrL>(BaD6rgNZ>$(m-=%vpYr^<>0-IE^H-9hBRO6P{c5fD;QOUP=& zDuD0_$CxTYj{vkTG%YKxQ+wp>tYQDalwtumHXH$#1J;k#PN?^mn@C0;fenF);25?MehJtT!OD4Uvpu^4`md1(Ytz{uuu0VxZRckk5v;sV4)J`>z`|NZ4I zz~m1cHR(Gqqc203+@M1S0p25gKfkG`qoi!GWBRZN5K0INpA0%@m0}g(&H-j9gf@e? z>>#TK@M#j~fEA$Z3}KV0TdKnv`l{K7l!38(>ZvERy-iKCSQZ6)Y%wdsXH_z`71KV< zjd$^zUTH6%DByuo4}g;Rygwt}`8!b%76%J;rzkvD3+9t!&`WujB*soW`BbeHrk7X4A3~=v#_eW*nT`v5{nf+o`s+lza6tT+5 z@=+#+Iff8oGY4XETFwA2ScTUm<_EnK+EFi8_2iG3TLjuxZHsdfSOk7Z(&&smw>Ka^ z!vy?cEpR7LsO1@dxOfG%?CRxC(~xJqK7ZRzA!jtJQY}qtuvrvU-?# zRxvAlt{{8ff8t3etME~hJ4E;>JERx_dQtKOu#>n9-DxBUa^NntIb>mvU_Lz0j_=80 z0I_7cb*!BKQ#D^DQU(C?96;H&q-okRNk@jxocW5FEz4u08 z80F^77nzU7z<~|13`KmlE3>ZK=_6#sxS>o9gOKsX7_4*z3UI>(ux$*?ncq!>ygewJ z8_WA?AmKSCoN$VjjUoZ?#=CNJ;{Uf??Z6=I&RwsH!1ribyBy!o*kRRkyyu0G;`^z0 zSnmp3S<$#FGJ3@GekT-^@s`OqX{`ZSKLv=Qm^aE8oV~?8z+tcuX3d(R%;WJVo~%q_ z5bpw7<^qZ~fGV@tNef_#+_hQ&iH;tu32D>CG?RF%k-TuDWRH1j1Hv#_FHqiNSQO7H z$1KbtAg*JjO@7cFbGQtC#<#Od5E2_gXt9ReEMBwQ#?Yl?@_!ZmrBd_0c_g!k{0hU;+0lJapRBisCDoEpeq6=XH4>@##Tu(;{>NnkndPUE3 zqij4GP%!i>kX1-7V4Yk*e;pp4;rE^91*S9+loWE9W)E{Hv^<9O+K=z1?8>&I1)6=+ z(Q%H`OpG%1<<-{0dO_xYYGS*>0jciuF_0 z(t}A%HASC{bV0aShy&%gSQ|n^-O@(R>p@{H`R*f+I!5n-&IJ_VPgO!}w}j2hS?GdsuyMAoXn2Ov^&3<_^XbogPtKr6 zzIiY$q*h6wLhhi^gHz}ZqDCj53fmVkP)`RRfWOy`6FNKMrc15B5&!Jdsf?8Y;Zs-& z$pq(6RX0&nVd-AM?93?w!p&gv?QY^m6$?{uDcspw$phbyx%T5&NIzp;ri+l_A`TL! z(2m6EQsJ}8my41eeavw>?gfhi7lB0p3uqc!kUtjXDVcY`YudAr+1@qGMqLGY{TxqI zU=6}1t6wXEBp)o-^=eJt*j|mIexU^dL4MDfsBjQtGe2M2Zk4HMDQd>dmN5Cv5Cap$ zsMmv~F0lrna#FB_YjdPhhPjYiNkV6hOAsO0`z5xg)a=+3POu|wD}mw<23_FyHyvW<2SO-K zec(Rj0D8I}ig>(BYM~XZ_$&Y~;cPN~KgfZ>C~8awjG_)1E*q^+f;W@zjCB|wvk$$A zT9_+PCUn#N?$%M6)+eiyqIhg7^ewprfEpp?-Lmo^jQl>nR1#61Mar3E5I?vIgbPSx z#YS!py1uS^QB!nNkfWnnselLnc_#_u#iou}$-BGa^z!Z-}PqvOH+K|F1Y+({dR6f47 zv==Wd>2NJ*feIVo6G4&aVI>0!$pKJy1ua|EO&Hh32p$aJH=tnk5;A(>V`Zo&hOVW= zonn@ErCOP|pHeZ6{h4Rj-%c)_B50p&UJjA`0hY$@J7`q+!-dlrJ9ZxxKJO>gB_F|i z;VVpb3%jEJ|B|j|6BX}>Qr=2ta${wjBCHh`VAdpNMF+WtgG_@$o&(4Lk%G)sY3+J0 z%qc5GO{b@eS;$DkE~;=Ke<-`6E`SK&Fd=A&&57X|GIhUfMYvg*MS4@>@Hh4^+OnJR|g=Rw&z=pE38CBPye80z_TZ=NnY%8mk3sAdvX zgl>KpGyX$O&mgFcxQ!)R3 zxJFS*H?`Puf;FWBuDcrxTTob9m-|qasO!hU&66kJAe84BqTzM&Tm)G{^TbQ5?XS9F@qW%lnJ`H4=rk5x!O%et68eE z{N|WK)=~#0Gju_O1mY&eyF0sw%5#!H{EcyQm~z;@YPIU3j7rRK`bp$;DN*xJN!nYB z7HX{?^ze>CP5XWOz!?xxgn1l`72a3d;iYp}03Q*_gJKb238@RzO&qjdu{$A~ChovA zPrT(7BR>P$9Y3&P)gOaNlWw|D8XNytfRx)oBx-H}istW|X2`zS|C`+G{vshqE&9oH z4FWMfyZ{f-xte}K?+Afn69lIKP*_5{A7GbO;sV(0E*(A9T(g>KPovU^@J{;=g@q)# zfF1@f?6c2)W`u0?gwpwPa1u@=gHQ^xfj*&B9NJ~Cvw3LOly)z40VY*o;#$6Ksee7( zFSzNZn~tbdYxhdh!u}y|exoS$`i7eP_m|vg?&nJIMm*EN#PK;oB})fQOSIqzIDrGm z`uQdd+y@}9uiJKB`2kwBL%Y!<)&U*>5{bc7R0{(-5iIN>ZUMhPO-w$D0`SxUm_lN~ z3v`#mFRryHws<)g*hC>x3J|)^9|?8Bx-^EGa4)d{leU(H|D2rcrefY~Cs0#gpJDf+ z@|9hhE^fikm0^Iw97E!Jw9j5h7?67($=1<}H%sdOMhFj2&Oir7m_tizAt77pUo^;(+2dj^mc*CTrVm8N1vW4jc)%3y2%zQ~-dW+|K520HQ%Z^UAt4v)_C&5#R%M_N2eDxDH^kZpwA>``IXcdHmOd^ZT{#swqS1G zJyLoOWlMEVr)%S;+#GaH8##mwt>2w)v+5-6jQ=OMz)B}9q$R!TO4JGaYR)|n+tbjuqBx&l zOzt#s2Vt>)Z(-HPvU4sV7Ggzf0lVfb7&Kti)0bV=8C-w;_0_Y^I;(!|b=Oal82NNb za!{C7^UcDI%_B@{je65nUg*o4qx;HLJ=g?_EMkY74&LDL%cGfQU^Wk?8}x12&TXX% zkgdlEeLg4b6ztL_=Kq_MZ@B&!t34*j43H>o zODbj#TO>MKZ~@I?;B0R~Pgjk+KtLP~ow`$b-R^Kg2nhSnDD$7-OTg8&l*07`uy;)6ScYw&RAcV%M zr>>CjkJSt?q$$G=lr(M}oGmH0djR*-x_*=-(y=(5cK&aeS&0QjmrYCOTVOYvBK<7! zrepmYY5amAl92=3lz|EQ?>Oa_nYp}kT9*d~;u;3S$vnsFM- zF^EZVUy2qZd6TbwNEK{|CWVpPR3}A^r-T{!UD!hP&0!RVQRzxA+o#x_Z;-T7xv8eP zZDB}ig`L%ETbqQZ^QBMe1ccA(Pwp3;+c6v^IP9>)x-P%`FUJMd;8xc_&`Be1LBV3Y zzmPG$aHRXhO**VhuHtTKVE4K1EJMDXI>s#y&<2gIRpX(UMO(C#*WbHi6|CSC`4bjd zfgD!Ri5*U`_I-tkxD313Sn=*NamhVz?2?7wCMn+qVP`mD%9I=CdB30AE4W+-efJ9fZ4)^@v;R4i-`f#WvNS(8b#Yp`FUwpGP^Tq+T@41ez;5#g&%trHU#8v zUcof2ob}38J~tD7alRtTZsk^(axc6GCQb5qOOWDVOx*(CwyG4=K3}hz6KF{c;?XQwToj@D5q(aFXLl;+CCwok8^|qSs%oe5=Fp^O`;=ZhssGB5J zDTWEtowaUbC|^01Qk>ktjN$}NM)+lTrhq^yEV^mL`R;t^`BQdRBAh?=teOi1>~)k0 zzo`Q7xagvbYCrtp5A}nW{^9a(%2!<^q0uJ9$n`EwAM)kggi2gE8iSvtHE2fZ`Y@)I z@U8^uCs%T5$yp|UQD7OGMK_JyLm2si8D+}cDwW+DyO-*wYEZgX6kb^Q4f?R4U*`@)+jvCE78 zKNul!GSmIOp|t_6ZVQ@Oa-iS$s705u>Yd=jB;b2r3gF)-VPb_yfqz_b$t8orAUHe- z&2h3AWZ+-KoY%n>e^QB^G2%V_l1Yjv>Yl!gqSCUnjQJ~0JNvEUGo|i+d`eR2VaXr` z?{jlLKR#}gEM?TJaVFhs(JZX9T~Tfenb2?b5<){SlE1tvdhvw(?arY?UYmB=Wlwju zvg+TD0>svU(9ydE|C;pKZ7nYhuLLz2ffa5g2W5QdKsnL=av^=>g78bd6+4*5rUmlT zM=nSAEC{SyjR9g9&%?KS;iS2CzuQM+4qg~}`Z_-y+hi2QSLHTPwB0}{ajeYH<;IX@ zIp6p@T*fR&rd&f4rg2A@==zDRNf^)TWCA~3x~y;$#-U?}yaz6Ny*zK1L#&iH3XdHu zk;3$aQqeaz>M(CinR4yhN%=TmU+-?!Cm;VG9hGxZ?Y&e800000NkvXXu0mjffh!L; literal 0 HcmV?d00001 diff --git a/src/vidxp/mcp.py b/src/vidxp/mcp.py index 1a40de8..003b021 100644 --- a/src/vidxp/mcp.py +++ b/src/vidxp/mcp.py @@ -267,6 +267,17 @@ def _translate_application_result(operation: Callable[[], _T]) -> _T: raise _application_error(exc) from exc +def _public_download_unavailable() -> ErrorDetail: + return ErrorDetail( + code="public_download_origin_unavailable", + category=ErrorCategory.unavailable, + message=( + "Public artifact downloads are not configured; read the native " + "MCP resource instead." + ), + ) + + def _invoke( context: ControlPlaneContext, *, @@ -870,7 +881,10 @@ async def get_artifact_download( download_url = None download_expires_at = None delivery_error = None - if artifact_delivery == "streamable_http": + if ( + artifact_delivery == "streamable_http" + and context.settings.artifact_download_public_url is not None + ): issued = _translate_application_result( lambda: ArtifactDownloadCapabilities(context.settings).issue( artifact @@ -879,6 +893,9 @@ async def get_artifact_download( delivery_mode = ArtifactDeliveryMode.https_download download_url = issued.url download_expires_at = issued.expires_at + elif artifact_delivery == "streamable_http": + delivery_mode = ArtifactDeliveryMode.mcp_resource + delivery_error = _public_download_unavailable() elif context.settings.mcp_stdio_filesystem_accessible: resource = await _invoke_async( context, diff --git a/tests/test_artifact_download.py b/tests/test_artifact_download.py index 84bcc02..fe9f004 100644 --- a/tests/test_artifact_download.py +++ b/tests/test_artifact_download.py @@ -128,6 +128,10 @@ def test_capability_download_supports_full_head_ranges_and_resume( base_url="https://download.example", ) as client: landing = client.get(f"/artifact-download/{ARTIFACT_ID}") + stylesheet = client.get( + "/artifact-download/assets/artifact-download.css" + ) + logo = client.get("/artifact-download/assets/vidxp-logo.png") bootstrap = _bootstrap(client, artifact, capability) head = client.head(f"/artifact-download/{ARTIFACT_ID}/content") full = client.get(f"/artifact-download/{ARTIFACT_ID}/content") @@ -153,10 +157,30 @@ def test_capability_download_supports_full_head_ranges_and_resume( assert parsed.query == "" assert capability not in landing.text assert landing.status_code == 200 - assert "script-src 'self'" in landing.headers["content-security-policy"] + csp = landing.headers["content-security-policy"] + assert "script-src 'self'" in csp + assert "style-src 'self'" in csp + assert "img-src 'self'" in csp + assert "'unsafe-inline'" not in csp assert landing.headers["referrer-policy"] == "no-referrer" + assert stylesheet.status_code == 200 + assert stylesheet.headers["content-type"].startswith("text/css") + assert logo.status_code == 200 + assert logo.headers["content-type"] == "image/png" assert bootstrap.status_code == 200 - assert bootstrap.json()["content_url"].endswith("/content") + bootstrap_payload = bootstrap.json() + assert set(bootstrap_payload) == { + "content_url", + "filename", + "mime_type", + "byte_size", + "expires_at", + } + assert capability not in bootstrap.text + assert bootstrap_payload["content_url"].endswith("/content") + assert bootstrap_payload["filename"] == f"snippet-{ARTIFACT_ID}.{extension}" + assert bootstrap_payload["mime_type"] == mime_type + assert bootstrap_payload["byte_size"] == len(content) assert "secure" in bootstrap.headers["set-cookie"].lower() assert "httponly" in bootstrap.headers["set-cookie"].lower() assert head.status_code == 200 diff --git a/tests/test_mcp.py b/tests/test_mcp.py index eecc238..eacdab2 100644 --- a/tests/test_mcp.py +++ b/tests/test_mcp.py @@ -573,7 +573,11 @@ async def accept_url(_context, params): result.structured_content["upload_session_url"], ) - async def test_real_streamable_http_artifact_delivery_hides_local_path(self): + async def _real_streamable_http_artifact_delivery( + self, + *, + configure_download: bool, + ): token = "s" * 32 listener = socket.socket() listener.bind(("127.0.0.1", 0)) @@ -585,14 +589,17 @@ async def test_real_streamable_http_artifact_delivery_hides_local_path(self): content = b"real-http-matroska-content" clip.write_bytes(content) digest = hashlib.sha256(content).hexdigest() - context = self.context( - root, - static_token=token, - artifact_download_public_url=( - "https://public.example/artifact-download" - ), - artifact_download_secret="d" * 32, + download_settings = ( + { + "artifact_download_public_url": ( + "https://public.example/artifact-download" + ), + "artifact_download_secret": "d" * 32, + } + if configure_download + else {} ) + context = self.context(root, static_token=token, **download_settings) context.application.get_artifact.return_value = Artifact( artifact_id=ARTIFACT_ID, media_id=MEDIA_ID, @@ -649,6 +656,15 @@ async def test_real_streamable_http_artifact_delivery_hides_local_path(self): server.should_exit = True await serving + return result, link, downloaded, content + + async def test_real_streamable_http_artifact_delivery_hides_local_path(self): + result, link, downloaded, content = ( + await self._real_streamable_http_artifact_delivery( + configure_download=True + ) + ) + self.assertFalse(result.is_error) self.assertIsInstance(link, ResourceLink) self.assertEqual(result.structured_content["delivery_mode"], "https_download") @@ -661,6 +677,31 @@ async def test_real_streamable_http_artifact_delivery_hides_local_path(self): self.assertTrue(public.fragment.startswith("capability=")) self.assertEqual(downloaded.contents[0].blob, base64.b64encode(content).decode()) + async def test_real_streamable_http_preserves_resource_without_public_download( + self, + ): + result, link, downloaded, content = ( + await self._real_streamable_http_artifact_delivery( + configure_download=False + ) + ) + + self.assertFalse(result.is_error) + self.assertIsInstance(link, ResourceLink) + self.assertEqual(result.structured_content["delivery_mode"], "mcp_resource") + self.assertIsNone(result.structured_content["local_path"]) + self.assertIsNone(result.structured_content["file_uri"]) + self.assertIsNone(result.structured_content["download_url"]) + self.assertIsNone(result.structured_content["download_expires_at"]) + self.assertEqual( + result.structured_content["delivery_error"]["code"], + "public_download_origin_unavailable", + ) + self.assertEqual( + downloaded.contents[0].blob, + base64.b64encode(content).decode(), + ) + async def test_media_upload_tools_enforce_permissions_and_availability(self): with TemporaryDirectory() as directory: context = self.context(Path(directory)) @@ -1185,7 +1226,9 @@ async def test_isolated_stdio_uses_configured_public_download(self): ) ) - async def test_remote_artifact_delivery_requires_public_origin(self): + async def test_remote_artifact_delivery_preserves_resource_without_public_origin( + self, + ): with TemporaryDirectory() as directory: context = self.context(Path(directory)) context.application.get_artifact.return_value = Artifact( @@ -1213,8 +1256,17 @@ async def test_remote_artifact_delivery_requires_public_origin(self): {"artifact_id": ARTIFACT_ID}, ) - self.assertTrue(result.is_error) - self.assertIn("public_download_origin_unavailable", result.content[0].text) + self.assertFalse(result.is_error) + self.assertIsInstance(result.content[0], ResourceLink) + self.assertEqual(result.structured_content["delivery_mode"], "mcp_resource") + self.assertIsNone(result.structured_content["local_path"]) + self.assertIsNone(result.structured_content["file_uri"]) + self.assertIsNone(result.structured_content["download_url"]) + self.assertIsNone(result.structured_content["download_expires_at"]) + self.assertEqual( + result.structured_content["delivery_error"]["code"], + "public_download_origin_unavailable", + ) async def test_application_errors_are_machine_readable_and_safe(self): with TemporaryDirectory() as directory: diff --git a/tests/test_packaging.py b/tests/test_packaging.py index 9a78e4e..7239cd4 100644 --- a/tests/test_packaging.py +++ b/tests/test_packaging.py @@ -141,16 +141,36 @@ def test_artifact_download_landing_assets_are_safe_and_packaged(self): assets = ROOT / "src" / "vidxp" / "assets" / "artifact_download" self.assertEqual( {path.name for path in assets.iterdir() if path.is_file()}, - {"index.html", "artifact-download.js"}, + { + "index.html", + "artifact-download.css", + "artifact-download.js", + "vidxp-logo.png", + }, ) html = (assets / "index.html").read_text(encoding="utf-8") + stylesheet = (assets / "artifact-download.css").read_text( + encoding="utf-8" + ) script = (assets / "artifact-download.js").read_text(encoding="utf-8") self.assertIn("./assets/artifact-download.js", html) + self.assertIn("./assets/artifact-download.css", html) + self.assertIn("./assets/vidxp-logo.png", html) self.assertNotRegex(html, r"https?://") self.assertNotIn("