From ff32728cfee0c81ae43abf174bcf204f2608284f Mon Sep 17 00:00:00 2001 From: Preetam Dwivedi Date: Mon, 28 Sep 2026 09:55:09 -0700 Subject: [PATCH 1/5] docs(web): design gateway-backed web library ## Summary ### Why? SubmitQueue has gateway APIs and a terminal client but no reusable browser UX. Shipping one fixed Next application would couple the UI to a deployment's authentication, telemetry, routing, and process policy, forcing other deployers to fork it. ### What? Define a gateway-backed web package analogous to `submitqueue/client`: generated Connect clients, server-only gateway helpers, presentation models, components, and link helpers live in a pnpm workspace under `web/`, while host-owned Next applications provide routes, authorization, telemetry, configuration, transport, and deployment. The RFC specifies the Node/server boundary, Next 16 packaging and security constraints, TypeScript proto generation, additive gateway prerequisites, status and error compatibility, telemetry ownership, testing, CI isolation from Go/Bazel, a read-only first phase, and the promotion path for additional domain UX packages. Co-authored-by: Cursor --- doc/rfc/index.md | 1 + doc/rfc/web-library.md | 397 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 398 insertions(+) create mode 100644 doc/rfc/web-library.md diff --git a/doc/rfc/index.md b/doc/rfc/index.md index 11dcb60d9..92f80c017 100644 --- a/doc/rfc/index.md +++ b/doc/rfc/index.md @@ -11,6 +11,7 @@ Design documents and technical proposals, grouped by scope. Shared/cross-cutting - [Consumer Hold](consumer-hold.md) - Fourth delivery outcome letting a controller postpone its delivery: the message becomes a partition barrier that pauses consumption for a chosen delay, redelivers in order, and does not count as a failure toward dead-lettering - [Change URIs](change-uri.md) - Identity of a code change: `scheme://{host[:port]}/{path}` per provider (GitHub PR, Phabricator Diff, git ref/commit) and canonical-form rules - [Hooks Framework](hook-framework.md) - Fire-and-forget side effects off pipeline lifecycle events: one shared `HookEvent` contract (`api/base/hook/`) published to a durable per-domain hook topic, dispatched by a per-domain stage to a pluggable hook extension (`platform/extension/hook/`) for integrations like warehouse export and code-review notifications +- [Web as a Library](web-library.md) - Publishing gateway-backed components, presentation models, and server helpers as a reusable package while host-owned Next apps provide routes, authentication, telemetry, configuration, and gateway transport ## SubmitQueue diff --git a/doc/rfc/web-library.md b/doc/rfc/web-library.md new file mode 100644 index 000000000..5d8e5a01c --- /dev/null +++ b/doc/rfc/web-library.md @@ -0,0 +1,397 @@ +# Web as a Library + +SubmitQueue should publish its web UX as a library of components, gateway helpers, and presentation models. A deployer-owned Next application supplies authentication, telemetry, configuration, routes, and gateway connectivity. + +## Problem + +There is no browser UX today. The gateway already exposes the required read APIs: `GetRequestSummaryByID`, `GetRequestSummaryByChangeURI`, `List`, `GetRequestHistoryByID`, and `GetRequestHistoryByChangeURI` ([status/list](submitqueue/status-list-api.md), [history](submitqueue/history-api.md)). It also exposes `Cancel`, which needs an actor/audit contract before the web host uses it. `submitqueue/client/` already renders the read data in a terminal. + +A repository-owned Next application would couple the reusable UX to one deployment's environment variables, authentication, telemetry, and gateway routing. Unlike a Go deployer, who can import the library and replace `service/.../main.go`, a Next deployer would have to fork the application root (`app/`, `proxy.ts`, and `next.config.ts`). + +Hosts supply these deployment-specific inputs: + +| Concern | The host supplies | Wired with | +|---|---|---| +| Authentication and authorization | the caller session and allowed actions | host routes, server actions, and an optional optimistic `proxy.ts` redirect | +| Logging | a `pino.Logger`, or any structurally compatible object | `pino` plus `pino-pretty`, `pino-opentelemetry-transport`, or another host-selected transport | +| Metrics | a registered global OpenTelemetry `MeterProvider`, or none | `@opentelemetry/sdk-metrics` plus a Prometheus or OTLP exporter | +| Gateway transport | a generated gateway client for each queue | Connect transport configured for the host's gateway address, credentials, and edge | +| Configuration | branding, external-host mappings, and internal path templates | a serializable `WebConfig` populated by the host | + +Keeping these inputs outside the library allows the same UX to run in multiple hosts. This also matches the gateway's existing security boundary: `submitqueue/client/conn.go` can send a bearer token, but this repository does not validate it. Authentication belongs to the proxy, sidecar, ingress, or host in front of the gateway. + +Change and build links are concrete helpers over gateway data. Change URIs use `scheme://{host[:port]}/{path}` with a mandatory host and must be interpretable without deployment wiring ([change URIs](change-uri.md)). Build runners already write the provider's web URL into request metadata. Every rendered external link is validated against an HTTP/HTTPS allowlist; `git://` and deployments with different browser hosts use explicit `WebConfig` mappings. + +## Runtime boundary + +Unlike a Go service, a Next application runs in three environments: + +``` +build route discovery, bundling, and static generation; no host process +server Node server components, route handlers, server actions, and proxy +browser client components on another machine; serializable data and opaque + server-action references only +``` + +Next owns the request loop and bundler, so the library cannot own "no server" in the literal Go sense. Instead, it owns no process and no application root. The host owns the Next app root and resolves server dependencies once per process. Only serializable data, capabilities, and opaque server-action references cross into client components. + +## Design principles + +- **Use the gateway as the domain boundary.** Web code reads and mutates SubmitQueue only through the published gateway API. Missing domain data should extend that API. +- **Keep deployment policy in the host.** The library does not read environment variables, open listeners, import auth SDKs, choose telemetry exporters, hardcode external URLs, or exit the process. +- **Keep server dependencies on the server.** Host objects are never serialized into client components. +- **Compose with data and callbacks.** Host routes pass serializable data, capabilities, and authorized server actions to library components. +- **Keep routing private to the host.** Per-queue gateway selection is a plain host function, not a library interface. +- **Let the host choose framework versions.** `react`, `react-dom`, and `next` are peer dependencies. + +``` +┌───────────────────────────── HOST ──────────────────────────────┐ +│ app routes · proxy · auth · telemetry · config · transport │ +└──────────────────────────────┬──────────────────────────────────┘ + │ generated client, data, actions + ▼ +┌──────────────────────────── LIBRARY ────────────────────────────┐ +│ gateway helpers · param parsers · components · models · hrefs │ +│ NO env reads · NO listener · NO auth SDK · NO backend choices │ +└─────────────────────────────────────────────────────────────────┘ +``` + +## Go analogy and its limits + +The reusable web package most closely matches `submitqueue/client/`, not `pipeline.Construct`: + +| Web | Go analogue | +|---|---| +| Gateway query helpers, paging, presentation models | `submitqueue/client/query.go`, `view.go`, and `watch.go` | +| Host route, auth checks, transport, and telemetry wiring | `service/.../main.go` | +| `(queue) => generated client` | host-private `Factory.For(Config{QueueName})` routing | + +The analogy stops at the wire boundary. Go controllers take domain entities and keep protobuf mapping in the host. The web library intentionally consumes the published gateway contract, so gateway-to-presentation mapping and gRPC-status-to-display-error mapping belong in the library. All deeper domain extensions remain behind the gateway. + +## Proposal + +### Step 1 · Package layout + +Start with one domain package and promote shared code only after another domain needs it: + +``` +web/ # pnpm workspace root +├── go.mod # exclude Node dependencies from root Go ./... +├── .nvmrc +├── package.json +├── pnpm-lock.yaml +├── pnpm-workspace.yaml +├── api/ # committed protobuf-es/Connect output +│ ├── base/change/ +│ ├── base/mergestrategy/ +│ └── submitqueue/gateway/ # @submitqueue/api-submitqueue-gateway +├── submitqueue/ # @submitqueue/web-submitqueue +│ └── src/ +│ ├── gateway/ # ./server: server-only RPC helpers +│ ├── component/ # server and client UI components +│ ├── model/ # presentation models and capabilities +│ ├── params.ts # route/search parameter parsing +│ ├── href.ts # external and internal link helpers +│ └── testing/ # ./testing: fixtures and fake generated clients +├── service/ +│ └── submitqueue/ # reference host and Next application root + +api/ # existing proto source + committed Go output +``` + +`web/` is a root-level artifact tree, like `api/`, `service/`, and `test/`. It contains the complete Node workspace, including the reference host, so dependency installation has one workspace boundary. pnpm may create package-local links, but all stay below `web/`. The Go domain and service trees remain Go-only. + +TypeScript stubs are generated into the pnpm workspace so their runtime dependencies resolve there. Protobuf-ES and Connect-ES v2 use pinned `@bufbuild/buf` and `@bufbuild/protoc-gen-es` development dependencies; no separate Connect generator is needed. After the existing Bazel Go generation, `make proto` runs `pnpm --dir web exec buf generate` for TypeScript. A new `check-proto` target regenerates both Go and TypeScript output and fails on a dirty tree. The API package, `@bufbuild/protobuf`, and `@connectrpc/connect` use one compatible v2 line across the workspace. + +The web library depends on `web/api/submitqueue/gateway`, not the Go code under `submitqueue/`. The cost is that one domain spans `submitqueue/`, `api/submitqueue/`, and `web/submitqueue/`; ownership and the Go/web hierarchies must remain aligned by convention. + +`web/service/submitqueue/` is the reference composition root used by Docker Compose. Deployers reuse the package and maintain their own host. + +### Step 2 · Gateway client and presentation helpers + +The generated gateway client is the web library's only domain dependency. The library adds concrete helpers for presentation mapping, error classification, and telemetry; it does not redefine the gateway as an interface. + +```ts +// web/submitqueue/src/gateway/request.ts +import type { Client } from '@connectrpc/connect' +import type { SubmitQueueGateway } from '@submitqueue/api-submitqueue-gateway' + +export async function loadRequest( + gateway: Client, + query: ByID, + logger?: Logger, +): Promise + +export async function cancelRequest( + gateway: Client, + command: CancelCommand, + logger?: Logger, +): Promise +``` + +The library obtains its meter through `metrics.getMeter("@submitqueue/web-submitqueue", version)` and never accepts an SDK or exporter. A logger is optional and typed as `pino.Logger`; `pino` is an optional peer because the emitted declarations reference its type. + +The host constructs Node clients with `@connectrpc/connect-node` `createGrpcTransport`, which speaks gRPC over HTTP/2 to the existing grpc-go server. The transport is cached on `globalThis` so route and instrumentation bundles plus development hot reload share one instance. It is configured with TLS and bearer/deadline interceptors. The library exports an API-only Connect tracing interceptor so gateway calls nest under the active Next span. Host startup validates every configured gateway. Per-queue routing is a plain function: + +```ts +type GatewayForQueue = (queue: string) => Client +``` + +Cross-queue helpers take this function. Library tests use a fake generated client. + +If a view needs domain data that the gateway does not expose, the gateway contract should grow. Phase one adds `ListQueues`, backed by the gateway's existing `queueconfig.Store.List`. A host spanning several gateways fans out over its configured gateway endpoints and associates each returned queue with that client. + +Links are also concrete helpers: + +```ts +export function changeHref(uri: string, config: WebConfig): Href | null +export function buildHref(buildURL: string): Href | null +export function requestHref(id: ByID, paths: WebPaths): Href | null +``` + +`WebPaths` contains serializable templates such as `/q/{queue}/r/{sqid}`. A missing template means the host did not expose that route, so the component renders no link. The gateway contract adds `build_url` to `HistoryEvent`, where each build occurrence belongs, and may mirror the current build URL on `RequestSummary`. + +#### Telemetry + +`@opentelemetry/api` is stable, works in Node and browsers, and is a no-op until the host registers a provider. Library packages depend only on that API. SDKs, exporters, registries, transports, and collector addresses remain host dependencies. Connect interceptors create client spans so gateway calls nest under Next request traces. + +An incompatible OpenTelemetry API major silently becomes a no-op, so host startup asserts compatibility. Ephemeral/serverless hosts may flush through `after(...)`; the recommended long-running Node host does not flush per request. + +### Step 3 · Host route composition + +Next's filesystem is the route topology, so the library does not add a runtime engine or view registry. Each host route performs the composition work that Go service mains perform: + +1. Resolve and authorize the host session. +2. Select and call the generated gateway client through library helpers. +3. Derive serializable capabilities and data. +4. Render a library component. +5. Provide host-owned server actions for enabled mutations. + +```tsx +// web/service/submitqueue/app/request/[queue]/[sqid]/page.tsx +export default async function RequestRoute({ params }: RouteProps) { + const parsed = parseRequestParams(await params) // params is a Promise in Next 16 + if (!parsed.ok) { + return + } + const route = parsed.value + const session = await requireSession() + await requireAllowed(session, Action.ViewRequest, route) + + const result = await loadRequest(gatewayForQueue(route.queue), route, logger) + if (!result.ok) { + return + } + + return ( + + ) +} +``` + +Phase one is read-only. Cancel is enabled only after `CancelRequest` carries an additive actor field and the gateway records it in request history. A later host-owned action re-authenticates, authorizes the same queue/request resource, constructs identity fields from the trusted session rather than browser input, and returns a result union instead of throwing an expected error. Land remains out of scope until change-level authorization is defined. + +Hiding a button is not authorization. The library receives resource-bound capabilities for presentation and callbacks for actions, not sessions or an authorization interface. List rows carry their own queue/request capabilities. + +Routes remain hand-written host composition roots. A host may expose only a subset; missing `WebPaths` templates suppress links to unmounted routes. Separate route files preserve per-segment layouts, metadata, streaming, Suspense boundaries, and caching. + +The library ships built ESM that preserves `"use client"` directives, with package exports for the default component surface, `./server`, and `./testing`. The reference host targets `next >=16.3 <17`—above the Server Actions CSRF security floor—and compatible React 19 releases. Gateway loaders are `server-only`, never cached, and convert proto messages before data reaches client components. + +`proxy.ts` may perform an optimistic anonymous-user redirect, but every server component, route handler, and server action performs the real authorization check. Multi-instance hosts configure a shared `NEXT_SERVER_ACTIONS_ENCRYPTION_KEY` and `experimental.serverActions.allowedOrigins` when a proxy rewrites `Host`. + +### Step 4 · Browser boundary + +The host passes only serializable configuration, page data, and capabilities to client components. Sessions remain in the host: + +```tsx +// web/service/submitqueue/app/layout.tsx (host-owned) +export default async function RootLayout({ children }: { children: React.ReactNode }) { + return ( + + + + {children} + + + + ) +} +``` + +Phase one uses client `error.tsx` boundaries that show `error.digest`, plus `onRequestError`, which logs that digest with the active trace ID. Browser telemetry (`useReportWebVitals` or a dedicated endpoint) is added only when a concrete metric requires it; the library does not prescribe a second collector or browser credential path. + +### Step 5 · Data and wire contracts + +Gateway calls run only in server components, route handlers, or server actions. The browser holds a session cookie, never gateway credentials. The gateway is a private dependency reachable only by trusted hosts; otherwise host authorization can be bypassed. + +`make proto` generates and commits TypeScript stubs under `web/api/`, mirroring `protopb/`. Protobuf-ES maps `int64` timestamps to `bigint`; gateway helpers validate safe range and convert them to `number` milliseconds in presentation models. Proto messages never cross into client components. + +Gateway statuses are strings today, not an enum. The library owns a display table for color, ordering, and terminal state plus an explicit unknown-status fallback. A committed fixture under `api/submitqueue/gateway/testdata/` is checked by both Go and TypeScript tests so new `entity.RequestStatus*` values cannot silently miss web presentation; keeping it outside `web/` lets hermetic Go tests read it. + +Before implementation, additive gateway changes provide: + +- `ListQueues` +- typed gRPC status details for known errors +- typed `build_url` fields instead of an implicit metadata key +- an actor field for audited mutations before web Cancel is enabled + +### Step 6 · Errors + +Gateway helpers classify from gRPC codes and typed details, never from server messages: + +| Codes | Display classification | +|---|---| +| `InvalidArgument`, `NotFound`, `ResourceExhausted` | actionable user error | +| `Unavailable`, `DeadlineExceeded` | transient infrastructure error | +| `Internal`, `Unknown`, unmapped | infrastructure error | + +Unknown errors default to infrastructure errors, and raw gateway messages are never rendered. Gateway follow-up work attaches typed details; the current server only sends `err.Error()` and passes some unclassified errors through. + +Helpers and server actions return result unions for expected outcomes rather than throwing them. Unexpected infrastructure failures reach a client `error.tsx` boundary, which shows only Next's `error.digest`. On the server, `onRequestError` logs the same digest with the active trace ID, route, and request context. Stack traces and reporting destinations remain hidden and host-owned. + +### Step 7 · Host surface + +| File | Content | +|---|---| +| `instrumentation.ts` | Node-guarded OpenTelemetry registration and `onRequestError` | +| `app/layout.tsx` | `` + branding | +| `app/**/page.tsx` | authenticate, load through gateway helpers, and render library components | +| `app/**/action.ts` | authorize mutations and call gateway helpers | +| `proxy.ts` | optimistic anonymous-user redirect only | +| `server/gateway.ts` | `globalThis`-cached transports, clients, and `(queue) => client` routing | +| `server/auth.ts` | authoritative session and resource checks | +| `server/telemetry.ts` | `globalThis`-cached logger plus OpenTelemetry SDK registration | +| `server/config.ts` | validated host config and serializable display config | +| `next.config.ts` | standalone output, tracing root, and Server Action origins | +| `Dockerfile` | the host's image | + +These files contain no presentation logic, gateway request mapping, or status definitions. Those remain in the library. + +## What the library enforces + +| Concern | Enforced by | +|---|---| +| No environment reads in the library | Lint rule banning `process.env` under `web/submitqueue`; CI gate | +| No gateway client in browser bundles | `import 'server-only'` in the `./server` export | +| No unsafe external URLs | Every rendered external href passes an HTTP/HTTPS scheme allowlist | +| Status vocabulary drift | Shared fixture tests the string display table in Go and TypeScript | +| Host controls framework versions | `react` / `next` are `peerDependencies`, never dependencies | +| One protobuf runtime | API package, Connect, and Protobuf-ES versions are aligned in the workspace | +| No telemetry backend in the library | `pino` is an optional type peer and `@opentelemetry/api` is a no-op peer; SDKs and exporters are forbidden in library packages | + +Authorization is host-owned and therefore not enforceable by the library. Host route tests must verify that reads and mutations reject unauthorized sessions; hiding a component based on capabilities is never treated as enforcement. + +## Host telemetry + +`instrumentation.ts` is a separate bundle entry, so shared process objects are cached on `globalThis` rather than relying on module-instance singletons: + +```ts +export async function register() { + if (process.env.NEXT_RUNTIME !== 'nodejs') return + const { registerTelemetry } = await import('./server/telemetry') + await registerTelemetry() +} + +export const onRequestError: Instrumentation.onRequestError = async ( + error, + request, + context, +) => { + if (process.env.NEXT_RUNTIME !== 'nodejs') return + const { reportRequestError } = await import('./server/telemetry') + const digest = + typeof error === 'object' && error !== null && 'digest' in error + ? String(error.digest) + : undefined + reportRequestError({ + error, + digest, + route: context.routePath, + request, + }) +} +``` + +`server/telemetry.ts` owns pino and SDK construction through `globalThis` caches. The OpenTelemetry provider survives through the API global, and the library obtains its meter from that API. `reportRequestError` logs the supplied digest with the active trace ID. + +## Testing + +```ts +import pino from 'pino' + +const data = await loadRequest( + fakeGateway({ 'demo/1': summary({ status: 'landing' }) }), + { queue: 'demo', sqid: 'demo/1' }, + pino({ level: 'silent' }), +) + +if (!data.ok) throw new Error('test fixture must load') +render() +``` + +The fake implements the generated client shape. Proto fixtures include `bigint` timestamps, but presentation fixtures contain only serializable values. Playwright end-to-end tests use the domain-qualified compose context `e2e-submitqueue-web` ([testing guide](../howto/TESTING.md#container-naming)). + +## Deployment model + +The supported deployment is a long-running Node host with server-side gateway access. `next.config.ts` uses `output: "standalone"` and `outputFileTracingRoot` points at `web/`; the Docker build context is `web/`. Hosts behind a rewriting proxy set `experimental.serverActions.allowedOrigins`. + +## Build and CI + +The `web/` pnpm workspace adds these Makefile targets to the existing CI gates: + +```makefile +check-proto: ## Check committed Go and TypeScript protobuf files +web-build: ## Build all web packages +web-e2e-test: ## Run Playwright web end-to-end tests +web-lint: ## Lint and typecheck web packages +web-test: ## Run web unit tests +``` + +Targets remain alphabetically sorted with `## Description`; `make lint` and `make test` include their web equivalents. + +Phase one does not use Bazel for TypeScript or Playwright. Bazel 8's root `REPO.bazel` and the root Gazelle directive exclude all of `web/`; web tests run through Make/pnpm and feed the same required-checks gate. `web/go.mod` keeps the root Go module from traversing Node dependencies. This choice can be revisited if hermetic Bazel web tests become necessary. + +`web/package.json` pins pnpm through `packageManager`, and `.nvmrc` pins Node. The library publishes built ESM and declares `next >=16.3 <17`, compatible React 19, the generated API package, Connect v2, Protobuf-ES v2, and optional pino types as peers. + +## Rejected + +- **Container image instead of a library.** Hosts could not replace authentication, telemetry, or transport without a permanent source fork. +- **One catch-all route.** `app/[[...slug]]/page.tsx` would replace Next routing and lose per-segment layouts, metadata, streaming, Suspense boundaries, and caching. +- **Static export or embedding in a Go binary.** Dynamic request routes cannot be enumerated, server actions and authoritative route checks disappear, and the grpc-go gateway needs a browser-reachable proxy with its own resource authorization. Revisit only with a separate design. +- **Web extension interfaces.** The gateway is already the domain contract. Extra `Gateway`, `GatewayFactory`, `Authorizer`, and generic `Deps` interfaces duplicate that contract or turn host authentication into library policy. +- **A generic `web/platform` engine.** Next's filesystem already defines route topology, and only one web domain exists. Add shared platform code only after a second domain demonstrates the common behavior. +- **Library-owned auth, logger, or metrics backends.** Token sources, clients, timeouts, transports, and registries are process infrastructure. Shipping them also adds unused dependencies to every host. +- **Bespoke telemetry interfaces.** OpenTelemetry already provides a stable, no-op metrics API. A type-only `pino.Logger` is structurally compatible with other loggers without an adapter or runtime dependency. +- **Environment-based library defaults.** `NEXT_PUBLIC_GATEWAY_ADDR` or similar defaults create a hidden configuration surface. +- **GraphQL or REST BFF.** Server components already provide the BFF and the generated gateway client is the domain boundary; another wire contract adds no capability. +- **DI framework.** Host route modules are explicit composition roots and do not need a runtime graph. +- **Shared Go/TypeScript view model through WASM or code generation.** A cross-language status fixture catches drift; the remaining presentation code is cheaper than a cross-language runtime. +- **Non-Next SPA.** Server components keep gateway access and authorization on the server; an SPA moves both into the browser. + +## Migration path + +The first phase makes additive gateway changes without changing existing behavior: + +1. Add gateway contracts required by the UI (`ListQueues`, attached typed error details, typed `build_url`) and generate committed TypeScript packages under `web/api/`; add Go/TypeScript round-trip and status-fixture checks. +2. Add the read-only `web/submitqueue` package with gateway helpers, models, components, parameter parsers, href helpers, and fakes; add unit and screenshot tests. +3. Add the read-only `web/service/submitqueue` reference host and a `web-ui` service to `service/submitqueue/docker-compose.yml`, depending on `gateway-service`; add `e2e-submitqueue-web`. +4. Add an actor to `CancelRequest`, persist it in request history, and then add the host-authorized Cancel action. Land remains deferred until change-level authorization is designed. +5. Build another host from the published package. Promote shared code only when that host or another domain demonstrates a reusable need. + +## Open questions + +- **Live updates.** Phase one uses polling with server revalidation, matching `client/watch.go`. Server-sent events could follow; a gateway `Watch` RPC needs a separate RFC. +- **`List` filtering.** The [status/list RFC](submitqueue/status-list-api.md) deferred status filters. The first queue view must either filter a receipt-time window or extend the gateway. +- **Authorization capabilities.** Finalize resource-bound capability types for queue pages and request rows. Phase one exposes read capabilities; Cancel follows its actor contract, and Land follows change-level authorization. +- **Initial scope.** Decide whether the first app contains only SubmitQueue or also Stovepipe history. Each domain uses its published RPC contract; Runway currently exposes only `Ping`, so a Runway view requires a new API. A second domain may justify shared UI helpers. +- **OTLP compatibility.** Determine whether intended host metrics backends accept OTLP. Otherwise those hosts must provide a `MetricReader`, making their wiring less similar to the reference host. +- **npm publishing.** The proposed `@submitqueue/*` packages require an npm organization and release cadence. From a1afdc2c6ce145462fd53e3ac5f86aeb803aa771 Mon Sep 17 00:00:00 2001 From: Preetam Dwivedi Date: Mon, 28 Sep 2026 10:11:47 -0700 Subject: [PATCH 2/5] docs(web): refocus web library RFC on design boundaries ## Summary ### Why? The RFC had grown into an implementation guide, with migration steps, build targets, file-by-file host instructions, and code samples, which buried the design decisions reviewers need to evaluate. ### What? Recast the RFC around package, gateway, host-composition, wire, and error boundaries plus ownership invariants. Library components are synchronous and props-only, polling uses `router.refresh()`, multi-gateway queue collisions fail at startup, and server actions are covered by an authorization lint. Adds a layered UX testing strategy covering contracts, components, browser UX and accessibility, host authorization, and end-to-end flows. Co-authored-by: Cursor --- doc/rfc/web-library.md | 304 +++++++---------------------------------- 1 file changed, 49 insertions(+), 255 deletions(-) diff --git a/doc/rfc/web-library.md b/doc/rfc/web-library.md index 5d8e5a01c..5f51ad0ad 100644 --- a/doc/rfc/web-library.md +++ b/doc/rfc/web-library.md @@ -41,21 +41,9 @@ Next owns the request loop and bundler, so the library cannot own "no server" in - **Keep deployment policy in the host.** The library does not read environment variables, open listeners, import auth SDKs, choose telemetry exporters, hardcode external URLs, or exit the process. - **Keep server dependencies on the server.** Host objects are never serialized into client components. - **Compose with data and callbacks.** Host routes pass serializable data, capabilities, and authorized server actions to library components. -- **Keep routing private to the host.** Per-queue gateway selection is a plain host function, not a library interface. +- **Keep gateway routing private to the host.** Per-queue gateway selection is a plain host function, not a library interface. - **Let the host choose framework versions.** `react`, `react-dom`, and `next` are peer dependencies. -``` -┌───────────────────────────── HOST ──────────────────────────────┐ -│ app routes · proxy · auth · telemetry · config · transport │ -└──────────────────────────────┬──────────────────────────────────┘ - │ generated client, data, actions - ▼ -┌──────────────────────────── LIBRARY ────────────────────────────┐ -│ gateway helpers · param parsers · components · models · hrefs │ -│ NO env reads · NO listener · NO auth SDK · NO backend choices │ -└─────────────────────────────────────────────────────────────────┘ -``` - ## Go analogy and its limits The reusable web package most closely matches `submitqueue/client/`, not `pipeline.Construct`: @@ -68,11 +56,11 @@ The reusable web package most closely matches `submitqueue/client/`, not `pipeli The analogy stops at the wire boundary. Go controllers take domain entities and keep protobuf mapping in the host. The web library intentionally consumes the published gateway contract, so gateway-to-presentation mapping and gRPC-status-to-display-error mapping belong in the library. All deeper domain extensions remain behind the gateway. -## Proposal +## Design -### Step 1 · Package layout +### Package boundary -Start with one domain package and promote shared code only after another domain needs it: +Start with one domain package. Promote shared code only after another domain needs it: ``` web/ # pnpm workspace root @@ -87,79 +75,41 @@ web/ # pnpm workspace root │ └── submitqueue/gateway/ # @submitqueue/api-submitqueue-gateway ├── submitqueue/ # @submitqueue/web-submitqueue │ └── src/ -│ ├── gateway/ # ./server: server-only RPC helpers +│ ├── gateway/ # server-only RPC helpers │ ├── component/ # server and client UI components │ ├── model/ # presentation models and capabilities │ ├── params.ts # route/search parameter parsing │ ├── href.ts # external and internal link helpers -│ └── testing/ # ./testing: fixtures and fake generated clients -├── service/ -│ └── submitqueue/ # reference host and Next application root - -api/ # existing proto source + committed Go output -``` - -`web/` is a root-level artifact tree, like `api/`, `service/`, and `test/`. It contains the complete Node workspace, including the reference host, so dependency installation has one workspace boundary. pnpm may create package-local links, but all stay below `web/`. The Go domain and service trees remain Go-only. - -TypeScript stubs are generated into the pnpm workspace so their runtime dependencies resolve there. Protobuf-ES and Connect-ES v2 use pinned `@bufbuild/buf` and `@bufbuild/protoc-gen-es` development dependencies; no separate Connect generator is needed. After the existing Bazel Go generation, `make proto` runs `pnpm --dir web exec buf generate` for TypeScript. A new `check-proto` target regenerates both Go and TypeScript output and fails on a dirty tree. The API package, `@bufbuild/protobuf`, and `@connectrpc/connect` use one compatible v2 line across the workspace. - -The web library depends on `web/api/submitqueue/gateway`, not the Go code under `submitqueue/`. The cost is that one domain spans `submitqueue/`, `api/submitqueue/`, and `web/submitqueue/`; ownership and the Go/web hierarchies must remain aligned by convention. - -`web/service/submitqueue/` is the reference composition root used by Docker Compose. Deployers reuse the package and maintain their own host. - -### Step 2 · Gateway client and presentation helpers - -The generated gateway client is the web library's only domain dependency. The library adds concrete helpers for presentation mapping, error classification, and telemetry; it does not redefine the gateway as an interface. - -```ts -// web/submitqueue/src/gateway/request.ts -import type { Client } from '@connectrpc/connect' -import type { SubmitQueueGateway } from '@submitqueue/api-submitqueue-gateway' - -export async function loadRequest( - gateway: Client, - query: ByID, - logger?: Logger, -): Promise - -export async function cancelRequest( - gateway: Client, - command: CancelCommand, - logger?: Logger, -): Promise +│ └── testing/ # fixtures and fake generated clients +└── service/ + └── submitqueue/ # reference host and Next application root ``` -The library obtains its meter through `metrics.getMeter("@submitqueue/web-submitqueue", version)` and never accepts an SDK or exporter. A logger is optional and typed as `pino.Logger`; `pino` is an optional peer because the emitted declarations reference its type. +`web/` contains the complete Node workspace, including committed Protobuf-ES/Connect output and the reference host; proto sources and Go output stay in the root `api/` tree. `web/go.mod` and repository exclusions keep Node dependencies outside Go and Gazelle traversal. The web library depends on the generated API package, not Go domain code. -The host constructs Node clients with `@connectrpc/connect-node` `createGrpcTransport`, which speaks gRPC over HTTP/2 to the existing grpc-go server. The transport is cached on `globalThis` so route and instrumentation bundles plus development hot reload share one instance. It is configured with TLS and bearer/deadline interceptors. The library exports an API-only Connect tracing interceptor so gateway calls nest under the active Next span. Host startup validates every configured gateway. Per-queue routing is a plain function: +The package ships built ESM with default component exports plus `./server` and `./testing`. React, Next, Connect, Protobuf-ES, and optional pino types are peers so host and library share one runtime. `web/service/submitqueue/` demonstrates composition; deployers own their hosts. -```ts -type GatewayForQueue = (queue: string) => Client -``` +Phase one contains SubmitQueue only. Stovepipe can later add `web/stovepipe/` against its published RPCs; Runway needs a read API beyond `Ping` before it can expose UX. Shared UI is promoted only after that second domain exists. -Cross-queue helpers take this function. Library tests use a fake generated client. +### Gateway boundary -If a view needs domain data that the gateway does not expose, the gateway contract should grow. Phase one adds `ListQueues`, backed by the gateway's existing `queueconfig.Store.List`. A host spanning several gateways fans out over its configured gateway endpoints and associates each returned queue with that client. +The generated gateway client is the library's only domain dependency. Concrete helpers map gateway responses into presentation models; there is no web-specific gateway interface. -Links are also concrete helpers: +Gateway helpers take `Client`, typed queries, and an optional `pino.Logger`; expected outcomes use result unions. The library obtains its meter through `metrics.getMeter("@submitqueue/web-submitqueue", version)` and never accepts an SDK or exporter. -```ts -export function changeHref(uri: string, config: WebConfig): Href | null -export function buildHref(buildURL: string): Href | null -export function requestHref(id: ByID, paths: WebPaths): Href | null -``` +The host constructs Node gRPC clients, supplies credentials and deadlines, and routes queues with a plain `(queue) => generated client` function. Cross-queue helpers take that function. The library supplies presentation/error mapping, tracing instrumentation, and a fake generated client for tests. -`WebPaths` contains serializable templates such as `/q/{queue}/r/{sqid}`. A missing template means the host did not expose that route, so the component renders no link. The gateway contract adds `build_url` to `HistoryEvent`, where each build occurrence belongs, and may mirror the current build URL on `RequestSummary`. +If a view needs missing domain data, the gateway contract grows instead of adding a web-only backend. Phase one adds `ListQueues`, backed by `queueconfig.Store.List`; a multi-gateway host fans it out across configured endpoints and rejects its configuration at startup if two gateways report the same queue name. -#### Telemetry +Phase one refreshes views with a small client component that calls `router.refresh()` on an interval, re-running the server loaders, matching `submitqueue/client/watch.go`. It presents the bounded receipt-history `List` without status filtering. Streaming or filtered gateway APIs require separate contract changes. -`@opentelemetry/api` is stable, works in Node and browsers, and is a no-op until the host registers a provider. Library packages depend only on that API. SDKs, exporters, registries, transports, and collector addresses remain host dependencies. Connect interceptors create client spans so gateway calls nest under Next request traces. +Link helpers derive change and build URLs from gateway data and internal links from serializable `WebPaths` templates such as `/q/{queue}/r/{sqid}`. A missing template suppresses the link. `build_url` is added to `HistoryEvent`, where each build occurrence belongs, and may be mirrored on `RequestSummary` for the current build. -An incompatible OpenTelemetry API major silently becomes a no-op, so host startup asserts compatibility. Ephemeral/serverless hosts may flush through `after(...)`; the recommended long-running Node host does not flush per request. +The library depends only on `@opentelemetry/api` and obtains its own meter. SDKs, exporters, collector addresses, and logger construction remain host concerns. A Connect interceptor nests gateway spans under the active Next request trace. -### Step 3 · Host route composition +### Host composition -Next's filesystem is the route topology, so the library does not add a runtime engine or view registry. Each host route performs the composition work that Go service mains perform: +Next's filesystem is the route topology; there is no runtime engine or view registry. Each host route: 1. Resolve and authorize the host session. 2. Select and call the generated gateway client through library helpers. @@ -167,71 +117,19 @@ Next's filesystem is the route topology, so the library does not add a runtime e 4. Render a library component. 5. Provide host-owned server actions for enabled mutations. -```tsx -// web/service/submitqueue/app/request/[queue]/[sqid]/page.tsx -export default async function RequestRoute({ params }: RouteProps) { - const parsed = parseRequestParams(await params) // params is a Promise in Next 16 - if (!parsed.ok) { - return - } - const route = parsed.value - const session = await requireSession() - await requireAllowed(session, Action.ViewRequest, route) - - const result = await loadRequest(gatewayForQueue(route.queue), route, logger) - if (!result.ok) { - return - } - - return ( - - ) -} -``` - -Phase one is read-only. Cancel is enabled only after `CancelRequest` carries an additive actor field and the gateway records it in request history. A later host-owned action re-authenticates, authorizes the same queue/request resource, constructs identity fields from the trusted session rather than browser input, and returns a result union instead of throwing an expected error. Land remains out of scope until change-level authorization is defined. +Route params are untrusted and parsed before lookup. Phase one is read-only. Cancel follows only after `CancelRequest` records an actor; its action re-authenticates, authorizes the same queue/request, and derives identity from the trusted session rather than browser input. Land remains out of scope until change-level authorization is defined. Hiding a button is not authorization. The library receives resource-bound capabilities for presentation and callbacks for actions, not sessions or an authorization interface. List rows carry their own queue/request capabilities. -Routes remain hand-written host composition roots. A host may expose only a subset; missing `WebPaths` templates suppress links to unmounted routes. Separate route files preserve per-segment layouts, metadata, streaming, Suspense boundaries, and caching. - -The library ships built ESM that preserves `"use client"` directives, with package exports for the default component surface, `./server`, and `./testing`. The reference host targets `next >=16.3 <17`—above the Server Actions CSRF security floor—and compatible React 19 releases. Gateway loaders are `server-only`, never cached, and convert proto messages before data reaches client components. - -`proxy.ts` may perform an optimistic anonymous-user redirect, but every server component, route handler, and server action performs the real authorization check. Multi-instance hosts configure a shared `NEXT_SERVER_ACTIONS_ENCRYPTION_KEY` and `experimental.serverActions.allowedOrigins` when a proxy rewrites `Host`. - -### Step 4 · Browser boundary - -The host passes only serializable configuration, page data, and capabilities to client components. Sessions remain in the host: +Library components are synchronous and render only their props; they never fetch. Data loading lives in gateway helpers called by host routes, which keeps the boundary explicit and lets components be tested without a server. -```tsx -// web/service/submitqueue/app/layout.tsx (host-owned) -export default async function RootLayout({ children }: { children: React.ReactNode }) { - return ( - - - - {children} - - - - ) -} -``` +Routes remain hand-written composition roots, and hosts may expose subsets. `WebPaths` controls internal links so unmounted routes are not advertised. Gateway loaders are `server-only` and uncached. Only serializable models, resource-bound capabilities, and opaque server-action references cross to client components; sessions, proto messages, and gateway clients do not. -Phase one uses client `error.tsx` boundaries that show `error.digest`, plus `onRequestError`, which logs that digest with the active trace ID. Browser telemetry (`useReportWebVitals` or a dedicated endpoint) is added only when a concrete metric requires it; the library does not prescribe a second collector or browser credential path. +The reference host targets `next >=16.3 <17` and compatible React 19 releases. `proxy.ts` may optimize anonymous redirects, but routes and actions perform authoritative checks. -### Step 5 · Data and wire contracts +### Wire compatibility -Gateway calls run only in server components, route handlers, or server actions. The browser holds a session cookie, never gateway credentials. The gateway is a private dependency reachable only by trusted hosts; otherwise host authorization can be bypassed. +Gateway calls stay on the server, and the gateway remains reachable only by trusted hosts; otherwise host authorization can be bypassed. `make proto` generates and commits TypeScript stubs under `web/api/`, mirroring `protopb/`. Protobuf-ES maps `int64` timestamps to `bigint`; gateway helpers validate safe range and convert them to `number` milliseconds in presentation models. Proto messages never cross into client components. @@ -244,7 +142,7 @@ Before implementation, additive gateway changes provide: - typed `build_url` fields instead of an implicit metadata key - an actor field for audited mutations before web Cancel is enabled -### Step 6 · Errors +### Error boundary Gateway helpers classify from gRPC codes and typed details, never from server messages: @@ -254,144 +152,40 @@ Gateway helpers classify from gRPC codes and typed details, never from server me | `Unavailable`, `DeadlineExceeded` | transient infrastructure error | | `Internal`, `Unknown`, unmapped | infrastructure error | -Unknown errors default to infrastructure errors, and raw gateway messages are never rendered. Gateway follow-up work attaches typed details; the current server only sends `err.Error()` and passes some unclassified errors through. - -Helpers and server actions return result unions for expected outcomes rather than throwing them. Unexpected infrastructure failures reach a client `error.tsx` boundary, which shows only Next's `error.digest`. On the server, `onRequestError` logs the same digest with the active trace ID, route, and request context. Stack traces and reporting destinations remain hidden and host-owned. - -### Step 7 · Host surface - -| File | Content | -|---|---| -| `instrumentation.ts` | Node-guarded OpenTelemetry registration and `onRequestError` | -| `app/layout.tsx` | `` + branding | -| `app/**/page.tsx` | authenticate, load through gateway helpers, and render library components | -| `app/**/action.ts` | authorize mutations and call gateway helpers | -| `proxy.ts` | optimistic anonymous-user redirect only | -| `server/gateway.ts` | `globalThis`-cached transports, clients, and `(queue) => client` routing | -| `server/auth.ts` | authoritative session and resource checks | -| `server/telemetry.ts` | `globalThis`-cached logger plus OpenTelemetry SDK registration | -| `server/config.ts` | validated host config and serializable display config | -| `next.config.ts` | standalone output, tracing root, and Server Action origins | -| `Dockerfile` | the host's image | - -These files contain no presentation logic, gateway request mapping, or status definitions. Those remain in the library. - -## What the library enforces - -| Concern | Enforced by | -|---|---| -| No environment reads in the library | Lint rule banning `process.env` under `web/submitqueue`; CI gate | -| No gateway client in browser bundles | `import 'server-only'` in the `./server` export | -| No unsafe external URLs | Every rendered external href passes an HTTP/HTTPS scheme allowlist | -| Status vocabulary drift | Shared fixture tests the string display table in Go and TypeScript | -| Host controls framework versions | `react` / `next` are `peerDependencies`, never dependencies | -| One protobuf runtime | API package, Connect, and Protobuf-ES versions are aligned in the workspace | -| No telemetry backend in the library | `pino` is an optional type peer and `@opentelemetry/api` is a no-op peer; SDKs and exporters are forbidden in library packages | - -Authorization is host-owned and therefore not enforceable by the library. Host route tests must verify that reads and mutations reject unauthorized sessions; hiding a component based on capabilities is never treated as enforcement. - -## Host telemetry - -`instrumentation.ts` is a separate bundle entry, so shared process objects are cached on `globalThis` rather than relying on module-instance singletons: - -```ts -export async function register() { - if (process.env.NEXT_RUNTIME !== 'nodejs') return - const { registerTelemetry } = await import('./server/telemetry') - await registerTelemetry() -} - -export const onRequestError: Instrumentation.onRequestError = async ( - error, - request, - context, -) => { - if (process.env.NEXT_RUNTIME !== 'nodejs') return - const { reportRequestError } = await import('./server/telemetry') - const digest = - typeof error === 'object' && error !== null && 'digest' in error - ? String(error.digest) - : undefined - reportRequestError({ - error, - digest, - route: context.routePath, - request, - }) -} -``` - -`server/telemetry.ts` owns pino and SDK construction through `globalThis` caches. The OpenTelemetry provider survives through the API global, and the library obtains its meter from that API. `reportRequestError` logs the supplied digest with the active trace ID. - -## Testing - -```ts -import pino from 'pino' - -const data = await loadRequest( - fakeGateway({ 'demo/1': summary({ status: 'landing' }) }), - { queue: 'demo', sqid: 'demo/1' }, - pino({ level: 'silent' }), -) +Unknown errors default to infrastructure errors, and raw gateway messages are never rendered. Expected outcomes use result unions. Unexpected failures reach `error.tsx`, which shows only Next's digest; `onRequestError` logs that digest with trace and request context. -if (!data.ok) throw new Error('test fixture must load') -render() -``` - -The fake implements the generated client shape. Proto fixtures include `bigint` timestamps, but presentation fixtures contain only serializable values. Playwright end-to-end tests use the domain-qualified compose context `e2e-submitqueue-web` ([testing guide](../howto/TESTING.md#container-naming)). - -## Deployment model +### Ownership invariants -The supported deployment is a long-running Node host with server-side gateway access. `next.config.ts` uses `output: "standalone"` and `outputFileTracingRoot` points at `web/`; the Docker build context is `web/`. Hosts behind a rewriting proxy set `experimental.serverActions.allowedOrigins`. +- Host: routes, authorization, gateway construction/routing, telemetry SDKs, configuration, branding, process lifecycle, and deployment. +- Library: gateway mapping, presentation models, components, capabilities, status display, safe links, and display errors. +- `./server` imports `server-only`; library code never reads environment variables or chooses backends. +- External links pass an HTTP/HTTPS allowlist; status fixtures prevent Go/TypeScript drift. +- Committed Go and TypeScript proto outputs are regenerated in a clean-tree drift check. +- Authorization is enforced and tested in host routes/actions. Capabilities only control presentation. -## Build and CI +The supported deployment is a long-running Node host with private gateway access. `web/` stays outside Bazel/Gazelle while remaining inside required checks. -The `web/` pnpm workspace adds these Makefile targets to the existing CI gates: +## UX testing strategy -```makefile -check-proto: ## Check committed Go and TypeScript protobuf files -web-build: ## Build all web packages -web-e2e-test: ## Run Playwright web end-to-end tests -web-lint: ## Lint and typecheck web packages -web-test: ## Run web unit tests -``` - -Targets remain alphabetically sorted with `## Description`; `make lint` and `make test` include their web equivalents. +| Layer | Tool | What it verifies | +|---|---|---| +| Contract/model | Go tests + Vitest | Proto round trips and drift, status compatibility, `bigint` conversion, paging, href safety, error mapping, and client props containing no proto markers or `bigint` | +| Component behavior | Vitest + React Testing Library | Rendering states, interactions, resource capabilities, and keyboard/focus behavior | +| Browser UX | Playwright + axe-core | Responsive and visual regression, real-browser accessibility/contrast, non-color-only statuses, navigation, loading/empty/error states, and stable focus/scroll during polling | +| Host security | Route matrix generated from `app/` + action lint | Every discovered route has an authorization case and rejects missing/denied sessions without `proxy.ts`; a lint rule requires every `'use server'` export to call the authorization helper, and actions reject cross-resource arguments when called directly | +| Host integration | Playwright + Docker Compose | Real gateway list/history flows, polling, transport failures, and later mutation authorization | -Phase one does not use Bazel for TypeScript or Playwright. Bazel 8's root `REPO.bazel` and the root Gazelle directive exclude all of `web/`; web tests run through Make/pnpm and feed the same required-checks gate. `web/go.mod` keeps the root Go module from traversing Node dependencies. This choice can be revisited if hermetic Bazel web tests become necessary. +Browser UX tests use a separate build-time entry point that aliases the reference host's gateway and session providers to fakes; production builds contain neither fake, and a build check verifies the production artifact. Fixtures cover scripted status changes, unknown statuses, long and numerous URIs, long errors, full pages, omitted path templates, and per-row capabilities. Screenshots are deterministic, and the lowest and latest supported Next/React versions run the component and browser suites. -`web/package.json` pins pnpm through `packageManager`, and `.nvmrc` pins Node. The library publishes built ESM and declares `next >=16.3 <17`, compatible React 19, the generated API package, Connect v2, Protobuf-ES v2, and optional pino types as peers. +End-to-end tests use the real gateway in the domain-qualified `e2e-submitqueue-web` context ([testing guide](../howto/TESTING.md#container-naming)). Storybook is not required initially because the fixture mode already renders deterministic browser states. ## Rejected - **Container image instead of a library.** Hosts could not replace authentication, telemetry, or transport without a permanent source fork. - **One catch-all route.** `app/[[...slug]]/page.tsx` would replace Next routing and lose per-segment layouts, metadata, streaming, Suspense boundaries, and caching. - **Static export or embedding in a Go binary.** Dynamic request routes cannot be enumerated, server actions and authoritative route checks disappear, and the grpc-go gateway needs a browser-reachable proxy with its own resource authorization. Revisit only with a separate design. -- **Web extension interfaces.** The gateway is already the domain contract. Extra `Gateway`, `GatewayFactory`, `Authorizer`, and generic `Deps` interfaces duplicate that contract or turn host authentication into library policy. -- **A generic `web/platform` engine.** Next's filesystem already defines route topology, and only one web domain exists. Add shared platform code only after a second domain demonstrates the common behavior. -- **Library-owned auth, logger, or metrics backends.** Token sources, clients, timeouts, transports, and registries are process infrastructure. Shipping them also adds unused dependencies to every host. -- **Bespoke telemetry interfaces.** OpenTelemetry already provides a stable, no-op metrics API. A type-only `pino.Logger` is structurally compatible with other loggers without an adapter or runtime dependency. -- **Environment-based library defaults.** `NEXT_PUBLIC_GATEWAY_ADDR` or similar defaults create a hidden configuration surface. +- **Web extensions, DI, or a generic platform engine.** The gateway and Next filesystem already provide domain and route boundaries. Extra interfaces or runtime assembly duplicate them; shared code is promoted only after another domain demonstrates reuse. +- **Library-owned process infrastructure.** Auth SDKs, logger/metrics backends, environment defaults, token sources, clients, and registries belong to the host. OpenTelemetry and optional pino types already provide the needed contracts. - **GraphQL or REST BFF.** Server components already provide the BFF and the generated gateway client is the domain boundary; another wire contract adds no capability. -- **DI framework.** Host route modules are explicit composition roots and do not need a runtime graph. - **Shared Go/TypeScript view model through WASM or code generation.** A cross-language status fixture catches drift; the remaining presentation code is cheaper than a cross-language runtime. - **Non-Next SPA.** Server components keep gateway access and authorization on the server; an SPA moves both into the browser. - -## Migration path - -The first phase makes additive gateway changes without changing existing behavior: - -1. Add gateway contracts required by the UI (`ListQueues`, attached typed error details, typed `build_url`) and generate committed TypeScript packages under `web/api/`; add Go/TypeScript round-trip and status-fixture checks. -2. Add the read-only `web/submitqueue` package with gateway helpers, models, components, parameter parsers, href helpers, and fakes; add unit and screenshot tests. -3. Add the read-only `web/service/submitqueue` reference host and a `web-ui` service to `service/submitqueue/docker-compose.yml`, depending on `gateway-service`; add `e2e-submitqueue-web`. -4. Add an actor to `CancelRequest`, persist it in request history, and then add the host-authorized Cancel action. Land remains deferred until change-level authorization is designed. -5. Build another host from the published package. Promote shared code only when that host or another domain demonstrates a reusable need. - -## Open questions - -- **Live updates.** Phase one uses polling with server revalidation, matching `client/watch.go`. Server-sent events could follow; a gateway `Watch` RPC needs a separate RFC. -- **`List` filtering.** The [status/list RFC](submitqueue/status-list-api.md) deferred status filters. The first queue view must either filter a receipt-time window or extend the gateway. -- **Authorization capabilities.** Finalize resource-bound capability types for queue pages and request rows. Phase one exposes read capabilities; Cancel follows its actor contract, and Land follows change-level authorization. -- **Initial scope.** Decide whether the first app contains only SubmitQueue or also Stovepipe history. Each domain uses its published RPC contract; Runway currently exposes only `Ping`, so a Runway view requires a new API. A second domain may justify shared UI helpers. -- **OTLP compatibility.** Determine whether intended host metrics backends accept OTLP. Otherwise those hosts must provide a `MetricReader`, making their wiring less similar to the reference host. -- **npm publishing.** The proposed `@submitqueue/*` packages require an npm organization and release cadence. From d05545e22198a08dfb7d18486182c277c7ffbfaf Mon Sep 17 00:00:00 2001 From: Preetam Dwivedi Date: Mon, 28 Sep 2026 10:30:10 -0700 Subject: [PATCH 3/5] docs(web): tighten web library transport and link contracts ## Summary ### Why? Review found contracts that would mis-route requests, emit phishing links, or fail to build as published packages: sqids contain a slash, the gateway speaks native gRPC, speculation runs sibling builds, and helpers cannot install interceptors on an already-created client. ### What? Require percent-encoding of path segments, a scheme-and-authority allowlist, history-only build URLs, `createGrpcTransport` with TLS by default, an exported tracing interceptor, one `@submitqueue/api` package, RSC export invariants, a structural `Logger`, a Node-owned Compose end-to-end check, and a `pnpm pack` test against an external Next app. Co-authored-by: Cursor --- doc/rfc/web-library.md | 40 +++++++++++++++++++++------------------- 1 file changed, 21 insertions(+), 19 deletions(-) diff --git a/doc/rfc/web-library.md b/doc/rfc/web-library.md index 5f51ad0ad..368b05819 100644 --- a/doc/rfc/web-library.md +++ b/doc/rfc/web-library.md @@ -13,14 +13,14 @@ Hosts supply these deployment-specific inputs: | Concern | The host supplies | Wired with | |---|---|---| | Authentication and authorization | the caller session and allowed actions | host routes, server actions, and an optional optimistic `proxy.ts` redirect | -| Logging | a `pino.Logger`, or any structurally compatible object | `pino` plus `pino-pretty`, `pino-opentelemetry-transport`, or another host-selected transport | +| Logging | a logger matching the library's structural `Logger` type | `pino` plus `pino-pretty`, `pino-opentelemetry-transport`, or another host-selected transport | | Metrics | a registered global OpenTelemetry `MeterProvider`, or none | `@opentelemetry/sdk-metrics` plus a Prometheus or OTLP exporter | -| Gateway transport | a generated gateway client for each queue | Connect transport configured for the host's gateway address, credentials, and edge | +| Gateway transport | a generated gateway client for each queue | `@connectrpc/connect-node` `createGrpcTransport` against the grpc-go gateway | | Configuration | branding, external-host mappings, and internal path templates | a serializable `WebConfig` populated by the host | Keeping these inputs outside the library allows the same UX to run in multiple hosts. This also matches the gateway's existing security boundary: `submitqueue/client/conn.go` can send a bearer token, but this repository does not validate it. Authentication belongs to the proxy, sidecar, ingress, or host in front of the gateway. -Change and build links are concrete helpers over gateway data. Change URIs use `scheme://{host[:port]}/{path}` with a mandatory host and must be interpretable without deployment wiring ([change URIs](change-uri.md)). Build runners already write the provider's web URL into request metadata. Every rendered external link is validated against an HTTP/HTTPS allowlist; `git://` and deployments with different browser hosts use explicit `WebConfig` mappings. +Change and build links are concrete helpers over gateway data. Change URIs use `scheme://{host[:port]}/{path}` with a mandatory host and must be interpretable without deployment wiring ([change URIs](change-uri.md)). A rendered external link must match an explicit host mapping keyed by provider scheme and authority; an HTTP or HTTPS scheme alone is not enough, and an unmapped authority renders as text. Build URLs come from history occurrences, not from a single field on the request summary, because speculation can run several sibling builds for one request. ## Runtime boundary @@ -69,10 +69,10 @@ web/ # pnpm workspace root ├── package.json ├── pnpm-lock.yaml ├── pnpm-workspace.yaml -├── api/ # committed protobuf-es/Connect output -│ ├── base/change/ -│ ├── base/mergestrategy/ -│ └── submitqueue/gateway/ # @submitqueue/api-submitqueue-gateway +├── api/ # one package, @submitqueue/api +│ ├── base/change/ # generated with the gateway stubs +│ ├── base/mergestrategy/ # so cross-file imports stay intra-package +│ └── submitqueue/gateway/ ├── submitqueue/ # @submitqueue/web-submitqueue │ └── src/ │ ├── gateway/ # server-only RPC helpers @@ -87,7 +87,7 @@ web/ # pnpm workspace root `web/` contains the complete Node workspace, including committed Protobuf-ES/Connect output and the reference host; proto sources and Go output stay in the root `api/` tree. `web/go.mod` and repository exclusions keep Node dependencies outside Go and Gazelle traversal. The web library depends on the generated API package, not Go domain code. -The package ships built ESM with default component exports plus `./server` and `./testing`. React, Next, Connect, Protobuf-ES, and optional pino types are peers so host and library share one runtime. `web/service/submitqueue/` demonstrates composition; deployers own their hosts. +The package ships built ESM with an export map of the root, `./server`, and `./testing`. The root export is components and presentation types only: the build preserves `'use client'`, and nothing reachable from that export imports `server-only` or Node built-ins. `./server` is the only server entry, and `./testing` is test-only. React, Next, Connect, and Protobuf-ES are peers so host and library share one runtime. The logger parameter is a small structural type owned by the library, so pino is not a dependency or a peer. `web/service/submitqueue/` demonstrates composition; deployers own their hosts. Phase one contains SubmitQueue only. Stovepipe can later add `web/stovepipe/` against its published RPCs; Runway needs a read API beyond `Ping` before it can expose UX. Shared UI is promoted only after that second domain exists. @@ -95,17 +95,17 @@ Phase one contains SubmitQueue only. Stovepipe can later add `web/stovepipe/` ag The generated gateway client is the library's only domain dependency. Concrete helpers map gateway responses into presentation models; there is no web-specific gateway interface. -Gateway helpers take `Client`, typed queries, and an optional `pino.Logger`; expected outcomes use result unions. The library obtains its meter through `metrics.getMeter("@submitqueue/web-submitqueue", version)` and never accepts an SDK or exporter. +Gateway helpers take `Client`, typed queries, and an optional structural `Logger`; expected outcomes use result unions. The library obtains its meter through `metrics.getMeter("@submitqueue/web-submitqueue", version)` and never accepts an SDK or exporter. -The host constructs Node gRPC clients, supplies credentials and deadlines, and routes queues with a plain `(queue) => generated client` function. Cross-queue helpers take that function. The library supplies presentation/error mapping, tracing instrumentation, and a fake generated client for tests. +The gateway serves native gRPC, so the host builds clients with `@connectrpc/connect-node` `createGrpcTransport` over HTTP/2. TLS is the default; plaintext h2c is an explicit host opt-in for a local stack, and an integration test dials the real gateway. The host supplies credentials and deadlines, caches transports, and routes queues with a plain `(queue) => generated client` function. Cross-queue helpers take that function. Helpers cannot attach interceptors to a client they receive already built, so the library exports a tracing interceptor for the host to install while constructing the transport. Tests use a fake generated client. If a view needs missing domain data, the gateway contract grows instead of adding a web-only backend. Phase one adds `ListQueues`, backed by `queueconfig.Store.List`; a multi-gateway host fans it out across configured endpoints and rejects its configuration at startup if two gateways report the same queue name. Phase one refreshes views with a small client component that calls `router.refresh()` on an interval, re-running the server loaders, matching `submitqueue/client/watch.go`. It presents the bounded receipt-history `List` without status filtering. Streaming or filtered gateway APIs require separate contract changes. -Link helpers derive change and build URLs from gateway data and internal links from serializable `WebPaths` templates such as `/q/{queue}/r/{sqid}`. A missing template suppresses the link. `build_url` is added to `HistoryEvent`, where each build occurrence belongs, and may be mirrored on `RequestSummary` for the current build. +Link helpers derive change URLs from gateway data through the trusted scheme-and-authority mapping, and internal links from serializable `WebPaths` templates such as `/q/{queue}/r/{sqid}`. Each substituted value is percent-encoded as one path segment: an sqid is `/`, so raw substitution would split the route, and queue names may contain `/`, `%`, `?`, `#`, `.`, or `..`. A missing template suppresses the link. Build URLs stay on history occurrences; `RequestSummary` does not carry one, because several sibling builds can be in flight for the same request. -The library depends only on `@opentelemetry/api` and obtains its own meter. SDKs, exporters, collector addresses, and logger construction remain host concerns. A Connect interceptor nests gateway spans under the active Next request trace. +The library depends only on `@opentelemetry/api` and obtains its own meter. SDKs, exporters, collector addresses, and logger construction remain host concerns. The exported tracing interceptor nests gateway spans under the active Next request trace. ### Host composition @@ -139,7 +139,7 @@ Before implementation, additive gateway changes provide: - `ListQueues` - typed gRPC status details for known errors -- typed `build_url` fields instead of an implicit metadata key +- a typed build URL on history occurrences, not on `RequestSummary` - an actor field for audited mutations before web Cancel is enabled ### Error boundary @@ -159,25 +159,27 @@ Unknown errors default to infrastructure errors, and raw gateway messages are ne - Host: routes, authorization, gateway construction/routing, telemetry SDKs, configuration, branding, process lifecycle, and deployment. - Library: gateway mapping, presentation models, components, capabilities, status display, safe links, and display errors. - `./server` imports `server-only`; library code never reads environment variables or chooses backends. -- External links pass an HTTP/HTTPS allowlist; status fixtures prevent Go/TypeScript drift. +- External links require a trusted scheme-and-authority mapping; status fixtures prevent Go/TypeScript drift. +- Internal path substitutions encode each value as one segment, covering `/`, `%`, `?`, `#`, `.`, and `..`. - Committed Go and TypeScript proto outputs are regenerated in a clean-tree drift check. - Authorization is enforced and tested in host routes/actions. Capabilities only control presentation. -The supported deployment is a long-running Node host with private gateway access. `web/` stays outside Bazel/Gazelle while remaining inside required checks. +The supported deployment is a long-running Node host with private gateway access. `web/` stays outside Gazelle so the Go build graph does not index it. End-to-end coverage is a separate required check, not a Bazel test: a Node-owned job, pinned to the workspace Node version and lockfile, builds the host and runs Playwright against Compose using the domain-qualified `e2e-submitqueue-web` context ([testing guide](../howto/TESTING.md#container-naming)). ## UX testing strategy | Layer | Tool | What it verifies | |---|---|---| -| Contract/model | Go tests + Vitest | Proto round trips and drift, status compatibility, `bigint` conversion, paging, href safety, error mapping, and client props containing no proto markers or `bigint` | +| Contract/model | Go tests + Vitest | Proto round trips and drift, status compatibility, `bigint` conversion, paging, segment encoding, trusted-authority links, error mapping, and client props containing no proto markers or `bigint` | | Component behavior | Vitest + React Testing Library | Rendering states, interactions, resource capabilities, and keyboard/focus behavior | | Browser UX | Playwright + axe-core | Responsive and visual regression, real-browser accessibility/contrast, non-color-only statuses, navigation, loading/empty/error states, and stable focus/scroll during polling | | Host security | Route matrix generated from `app/` + action lint | Every discovered route has an authorization case and rejects missing/denied sessions without `proxy.ts`; a lint rule requires every `'use server'` export to call the authorization helper, and actions reject cross-resource arguments when called directly | -| Host integration | Playwright + Docker Compose | Real gateway list/history flows, polling, transport failures, and later mutation authorization | +| Host integration | Node-owned Playwright + Compose | Real grpc-go gateway list/history flows, TLS and explicit h2c, polling, transport failures, and later mutation authorization | +| Published package | `pnpm pack` into an external Next app | The packed library and API tarballs install outside the workspace, their export maps match the root, `./server`, and `./testing`, the root stays free of `server-only`, and both supported Next/React bounds build | Browser UX tests use a separate build-time entry point that aliases the reference host's gateway and session providers to fakes; production builds contain neither fake, and a build check verifies the production artifact. Fixtures cover scripted status changes, unknown statuses, long and numerous URIs, long errors, full pages, omitted path templates, and per-row capabilities. Screenshots are deterministic, and the lowest and latest supported Next/React versions run the component and browser suites. -End-to-end tests use the real gateway in the domain-qualified `e2e-submitqueue-web` context ([testing guide](../howto/TESTING.md#container-naming)). Storybook is not required initially because the fixture mode already renders deterministic browser states. +Storybook is not required initially because the fixture build already renders deterministic browser states. ## Rejected @@ -185,7 +187,7 @@ End-to-end tests use the real gateway in the domain-qualified `e2e-submitqueue-w - **One catch-all route.** `app/[[...slug]]/page.tsx` would replace Next routing and lose per-segment layouts, metadata, streaming, Suspense boundaries, and caching. - **Static export or embedding in a Go binary.** Dynamic request routes cannot be enumerated, server actions and authoritative route checks disappear, and the grpc-go gateway needs a browser-reachable proxy with its own resource authorization. Revisit only with a separate design. - **Web extensions, DI, or a generic platform engine.** The gateway and Next filesystem already provide domain and route boundaries. Extra interfaces or runtime assembly duplicate them; shared code is promoted only after another domain demonstrates reuse. -- **Library-owned process infrastructure.** Auth SDKs, logger/metrics backends, environment defaults, token sources, clients, and registries belong to the host. OpenTelemetry and optional pino types already provide the needed contracts. +- **Library-owned process infrastructure.** Auth SDKs, logger/metrics backends, environment defaults, token sources, clients, and registries belong to the host. OpenTelemetry supplies the metrics contract; logging uses a structural type the host's logger satisfies. - **GraphQL or REST BFF.** Server components already provide the BFF and the generated gateway client is the domain boundary; another wire contract adds no capability. - **Shared Go/TypeScript view model through WASM or code generation.** A cross-language status fixture catches drift; the remaining presentation code is cheaper than a cross-language runtime. - **Non-Next SPA.** Server components keep gateway access and authorization on the server; an SPA moves both into the browser. From 8617a077adb3a1c08228342448cf51def59ca1cc Mon Sep 17 00:00:00 2001 From: Preetam Dwivedi Date: Mon, 28 Sep 2026 10:39:19 -0700 Subject: [PATCH 4/5] docs(web): condense the web library RFC ## Summary ### Why? The RFC restated the same host and library boundaries and specified phase-one machinery the host already owns or that a read-only release does not need. ### What? State each boundary once. Keep phase one read-only, with queue names configured by the host. Record proxy-safe base64url paths, request-time rendering, stable list windows, and polling limits, and move build and test detail into acceptance criteria. Co-authored-by: Cursor --- doc/rfc/web-library.md | 206 ++++++++--------------------------------- 1 file changed, 38 insertions(+), 168 deletions(-) diff --git a/doc/rfc/web-library.md b/doc/rfc/web-library.md index 368b05819..23d1b7222 100644 --- a/doc/rfc/web-library.md +++ b/doc/rfc/web-library.md @@ -1,193 +1,63 @@ # Web as a Library -SubmitQueue should publish its web UX as a library of components, gateway helpers, and presentation models. A deployer-owned Next application supplies authentication, telemetry, configuration, routes, and gateway connectivity. +## Decision -## Problem +SubmitQueue publishes its browser UX as a library of components, gateway helpers, and presentation models. A deployer-owned Next application supplies routes, authentication, telemetry, configuration, and gateway connectivity. -There is no browser UX today. The gateway already exposes the required read APIs: `GetRequestSummaryByID`, `GetRequestSummaryByChangeURI`, `List`, `GetRequestHistoryByID`, and `GetRequestHistoryByChangeURI` ([status/list](submitqueue/status-list-api.md), [history](submitqueue/history-api.md)). It also exposes `Cancel`, which needs an actor/audit contract before the web host uses it. `submitqueue/client/` already renders the read data in a terminal. +The package resembles `submitqueue/client/`, while the host resembles service wiring. The library consumes the published gateway contract and maps it to presentation. Domain extensions stay behind the gateway. -A repository-owned Next application would couple the reusable UX to one deployment's environment variables, authentication, telemetry, and gateway routing. Unlike a Go deployer, who can import the library and replace `service/.../main.go`, a Next deployer would have to fork the application root (`app/`, `proxy.ts`, and `next.config.ts`). +Phase one is read-only: request summary, history, and queue receipt list ([status/list](submitqueue/status-list-api.md), [history](submitqueue/history-api.md)). Mutations require a separate authorization and audit design. -Hosts supply these deployment-specific inputs: +## Host/library boundary -| Concern | The host supplies | Wired with | -|---|---|---| -| Authentication and authorization | the caller session and allowed actions | host routes, server actions, and an optional optimistic `proxy.ts` redirect | -| Logging | a logger matching the library's structural `Logger` type | `pino` plus `pino-pretty`, `pino-opentelemetry-transport`, or another host-selected transport | -| Metrics | a registered global OpenTelemetry `MeterProvider`, or none | `@opentelemetry/sdk-metrics` plus a Prometheus or OTLP exporter | -| Gateway transport | a generated gateway client for each queue | `@connectrpc/connect-node` `createGrpcTransport` against the grpc-go gateway | -| Configuration | branding, external-host mappings, and internal path templates | a serializable `WebConfig` populated by the host | - -Keeping these inputs outside the library allows the same UX to run in multiple hosts. This also matches the gateway's existing security boundary: `submitqueue/client/conn.go` can send a bearer token, but this repository does not validate it. Authentication belongs to the proxy, sidecar, ingress, or host in front of the gateway. - -Change and build links are concrete helpers over gateway data. Change URIs use `scheme://{host[:port]}/{path}` with a mandatory host and must be interpretable without deployment wiring ([change URIs](change-uri.md)). A rendered external link must match an explicit host mapping keyed by provider scheme and authority; an HTTP or HTTPS scheme alone is not enough, and an unmapped authority renders as text. Build URLs come from history occurrences, not from a single field on the request summary, because speculation can run several sibling builds for one request. - -## Runtime boundary - -Unlike a Go service, a Next application runs in three environments: - -``` -build route discovery, bundling, and static generation; no host process -server Node server components, route handlers, server actions, and proxy -browser client components on another machine; serializable data and opaque - server-action references only -``` - -Next owns the request loop and bundler, so the library cannot own "no server" in the literal Go sense. Instead, it owns no process and no application root. The host owns the Next app root and resolves server dependencies once per process. Only serializable data, capabilities, and opaque server-action references cross into client components. - -## Design principles - -- **Use the gateway as the domain boundary.** Web code reads and mutates SubmitQueue only through the published gateway API. Missing domain data should extend that API. -- **Keep deployment policy in the host.** The library does not read environment variables, open listeners, import auth SDKs, choose telemetry exporters, hardcode external URLs, or exit the process. -- **Keep server dependencies on the server.** Host objects are never serialized into client components. -- **Compose with data and callbacks.** Host routes pass serializable data, capabilities, and authorized server actions to library components. -- **Keep gateway routing private to the host.** Per-queue gateway selection is a plain host function, not a library interface. -- **Let the host choose framework versions.** `react`, `react-dom`, and `next` are peer dependencies. +| Library | Host | +|---|---| +| Gateway-to-presentation mapping, components, status and error display | Next application root, routes, and which subset to expose | +| Internal and external link helpers | Queue names, per-queue gateway routing, credentials, and deadlines | +| OpenTelemetry API instrumentation and a structural `Logger` | Session authorization, telemetry backends, configuration, and process lifecycle | -## Go analogy and its limits +The host configures the queues it serves and selects each generated client with `(queue) => client`. It builds those clients with `@connectrpc/connect-node` `createGrpcTransport` over HTTP/2: TLS by default, plaintext h2c only as an explicit local option. It installs the library's tracing interceptor while constructing the transport. The gateway stays reachable only by trusted hosts. -The reusable web package most closely matches `submitqueue/client/`, not `pipeline.Construct`: +Components render serializable props and do not fetch. Sessions, gateway clients, and protobuf messages stay on the server. Server helpers call `connection()` before gateway I/O, host routes that call them are dynamically rendered, and neither side caches gateway results. -| Web | Go analogue | -|---|---| -| Gateway query helpers, paging, presentation models | `submitqueue/client/query.go`, `view.go`, and `watch.go` | -| Host route, auth checks, transport, and telemetry wiring | `service/.../main.go` | -| `(queue) => generated client` | host-private `Factory.For(Config{QueueName})` routing | +`WebPaths` selects internal links. Each substituted value is unpadded base64url, so a slash inside an sqid (`/`) remains one path segment after a proxy decodes `%2F`. External links require a host mapping keyed by change-URI scheme and authority; other values render as text. Build URLs stay on history occurrences, because one request can have concurrent sibling builds. -The analogy stops at the wire boundary. Go controllers take domain entities and keep protobuf mapping in the host. The web library intentionally consumes the published gateway contract, so gateway-to-presentation mapping and gRPC-status-to-display-error mapping belong in the library. All deeper domain extensions remain behind the gateway. +`List` requires a queue and a half-open receipt window. A first visit uses a trailing 24-hour window, which the host may override. Both bounds live in the route's search parameters and stay fixed across refresh and paging, because the page token is valid only for that queue and those bounds. Changing the window drops the token. -## Design +A client component calls `router.refresh()` to rerun the server loaders. One refresh runs at a time. The wait is the terminal client's poll interval plus jitter, grows after a transport failure, and pauses while the document is hidden or the browser is offline. A request view stops on `landed`, `error`, or `cancelled`. A queue list keeps polling for the life of its fixed window. -### Package boundary +## Package shape -Start with one domain package. Promote shared code only after another domain needs it: +The pnpm workspace lives under `web/`, with a nested `web/go.mod` so the Go build graph does not index it: ``` -web/ # pnpm workspace root -├── go.mod # exclude Node dependencies from root Go ./... -├── .nvmrc -├── package.json -├── pnpm-lock.yaml -├── pnpm-workspace.yaml -├── api/ # one package, @submitqueue/api -│ ├── base/change/ # generated with the gateway stubs -│ ├── base/mergestrategy/ # so cross-file imports stay intra-package -│ └── submitqueue/gateway/ -├── submitqueue/ # @submitqueue/web-submitqueue -│ └── src/ -│ ├── gateway/ # server-only RPC helpers -│ ├── component/ # server and client UI components -│ ├── model/ # presentation models and capabilities -│ ├── params.ts # route/search parameter parsing -│ ├── href.ts # external and internal link helpers -│ └── testing/ # fixtures and fake generated clients -└── service/ - └── submitqueue/ # reference host and Next application root +web/ +├── api/ # @submitqueue/api, one package for base and gateway stubs +├── submitqueue/ # @submitqueue/web-submitqueue +└── service/submitqueue/ # reference host ``` -`web/` contains the complete Node workspace, including committed Protobuf-ES/Connect output and the reference host; proto sources and Go output stay in the root `api/` tree. `web/go.mod` and repository exclusions keep Node dependencies outside Go and Gazelle traversal. The web library depends on the generated API package, not Go domain code. - -The package ships built ESM with an export map of the root, `./server`, and `./testing`. The root export is components and presentation types only: the build preserves `'use client'`, and nothing reachable from that export imports `server-only` or Node built-ins. `./server` is the only server entry, and `./testing` is test-only. React, Next, Connect, and Protobuf-ES are peers so host and library share one runtime. The logger parameter is a small structural type owned by the library, so pino is not a dependency or a peer. `web/service/submitqueue/` demonstrates composition; deployers own their hosts. - -Phase one contains SubmitQueue only. Stovepipe can later add `web/stovepipe/` against its published RPCs; Runway needs a read API beyond `Ping` before it can expose UX. Shared UI is promoted only after that second domain exists. - -### Gateway boundary - -The generated gateway client is the library's only domain dependency. Concrete helpers map gateway responses into presentation models; there is no web-specific gateway interface. - -Gateway helpers take `Client`, typed queries, and an optional structural `Logger`; expected outcomes use result unions. The library obtains its meter through `metrics.getMeter("@submitqueue/web-submitqueue", version)` and never accepts an SDK or exporter. - -The gateway serves native gRPC, so the host builds clients with `@connectrpc/connect-node` `createGrpcTransport` over HTTP/2. TLS is the default; plaintext h2c is an explicit host opt-in for a local stack, and an integration test dials the real gateway. The host supplies credentials and deadlines, caches transports, and routes queues with a plain `(queue) => generated client` function. Cross-queue helpers take that function. Helpers cannot attach interceptors to a client they receive already built, so the library exports a tracing interceptor for the host to install while constructing the transport. Tests use a fake generated client. - -If a view needs missing domain data, the gateway contract grows instead of adding a web-only backend. Phase one adds `ListQueues`, backed by `queueconfig.Store.List`; a multi-gateway host fans it out across configured endpoints and rejects its configuration at startup if two gateways report the same queue name. - -Phase one refreshes views with a small client component that calls `router.refresh()` on an interval, re-running the server loaders, matching `submitqueue/client/watch.go`. It presents the bounded receipt-history `List` without status filtering. Streaming or filtered gateway APIs require separate contract changes. - -Link helpers derive change URLs from gateway data through the trusted scheme-and-authority mapping, and internal links from serializable `WebPaths` templates such as `/q/{queue}/r/{sqid}`. Each substituted value is percent-encoded as one path segment: an sqid is `/`, so raw substitution would split the route, and queue names may contain `/`, `%`, `?`, `#`, `.`, or `..`. A missing template suppresses the link. Build URLs stay on history occurrences; `RequestSummary` does not carry one, because several sibling builds can be in flight for the same request. - -The library depends only on `@opentelemetry/api` and obtains its own meter. SDKs, exporters, collector addresses, and logger construction remain host concerns. The exported tracing interceptor nests gateway spans under the active Next request trace. - -### Host composition - -Next's filesystem is the route topology; there is no runtime engine or view registry. Each host route: - -1. Resolve and authorize the host session. -2. Select and call the generated gateway client through library helpers. -3. Derive serializable capabilities and data. -4. Render a library component. -5. Provide host-owned server actions for enabled mutations. - -Route params are untrusted and parsed before lookup. Phase one is read-only. Cancel follows only after `CancelRequest` records an actor; its action re-authenticates, authorizes the same queue/request, and derives identity from the trusted session rather than browser input. Land remains out of scope until change-level authorization is defined. - -Hiding a button is not authorization. The library receives resource-bound capabilities for presentation and callbacks for actions, not sessions or an authorization interface. List rows carry their own queue/request capabilities. - -Library components are synchronous and render only their props; they never fetch. Data loading lives in gateway helpers called by host routes, which keeps the boundary explicit and lets components be tested without a server. - -Routes remain hand-written composition roots, and hosts may expose subsets. `WebPaths` controls internal links so unmounted routes are not advertised. Gateway loaders are `server-only` and uncached. Only serializable models, resource-bound capabilities, and opaque server-action references cross to client components; sessions, proto messages, and gateway clients do not. - -The reference host targets `next >=16.3 <17` and compatible React 19 releases. `proxy.ts` may optimize anonymous redirects, but routes and actions perform authoritative checks. - -### Wire compatibility - -Gateway calls stay on the server, and the gateway remains reachable only by trusted hosts; otherwise host authorization can be bypassed. - -`make proto` generates and commits TypeScript stubs under `web/api/`, mirroring `protopb/`. Protobuf-ES maps `int64` timestamps to `bigint`; gateway helpers validate safe range and convert them to `number` milliseconds in presentation models. Proto messages never cross into client components. - -Gateway statuses are strings today, not an enum. The library owns a display table for color, ordering, and terminal state plus an explicit unknown-status fallback. A committed fixture under `api/submitqueue/gateway/testdata/` is checked by both Go and TypeScript tests so new `entity.RequestStatus*` values cannot silently miss web presentation; keeping it outside `web/` lets hermetic Go tests read it. - -Before implementation, additive gateway changes provide: - -- `ListQueues` -- typed gRPC status details for known errors -- a typed build URL on history occurrences, not on `RequestSummary` -- an actor field for audited mutations before web Cancel is enabled - -### Error boundary - -Gateway helpers classify from gRPC codes and typed details, never from server messages: - -| Codes | Display classification | -|---|---| -| `InvalidArgument`, `NotFound`, `ResourceExhausted` | actionable user error | -| `Unavailable`, `DeadlineExceeded` | transient infrastructure error | -| `Internal`, `Unknown`, unmapped | infrastructure error | - -Unknown errors default to infrastructure errors, and raw gateway messages are never rendered. Expected outcomes use result unions. Unexpected failures reach `error.tsx`, which shows only Next's digest; `onRequestError` logs that digest with trace and request context. - -### Ownership invariants - -- Host: routes, authorization, gateway construction/routing, telemetry SDKs, configuration, branding, process lifecycle, and deployment. -- Library: gateway mapping, presentation models, components, capabilities, status display, safe links, and display errors. -- `./server` imports `server-only`; library code never reads environment variables or chooses backends. -- External links require a trusted scheme-and-authority mapping; status fixtures prevent Go/TypeScript drift. -- Internal path substitutions encode each value as one segment, covering `/`, `%`, `?`, `#`, `.`, and `..`. -- Committed Go and TypeScript proto outputs are regenerated in a clean-tree drift check. -- Authorization is enforced and tested in host routes/actions. Capabilities only control presentation. - -The supported deployment is a long-running Node host with private gateway access. `web/` stays outside Gazelle so the Go build graph does not index it. End-to-end coverage is a separate required check, not a Bazel test: a Node-owned job, pinned to the workspace Node version and lockfile, builds the host and runs Playwright against Compose using the domain-qualified `e2e-submitqueue-web` context ([testing guide](../howto/TESTING.md#container-naming)). +Generated TypeScript is committed beside the Go stubs. Gateway stubs and the base protos they import share one package, so the imports stay valid after publish. The library ships ESM at the root, `./server`, and `./testing`. The root preserves `'use client'` and reaches no `server-only` or Node-only code. React, Next, Connect, and Protobuf-ES are peers. Shared UI waits until a second domain needs it. -## UX testing strategy +## Prerequisites -| Layer | Tool | What it verifies | -|---|---|---| -| Contract/model | Go tests + Vitest | Proto round trips and drift, status compatibility, `bigint` conversion, paging, segment encoding, trusted-authority links, error mapping, and client props containing no proto markers or `bigint` | -| Component behavior | Vitest + React Testing Library | Rendering states, interactions, resource capabilities, and keyboard/focus behavior | -| Browser UX | Playwright + axe-core | Responsive and visual regression, real-browser accessibility/contrast, non-color-only statuses, navigation, loading/empty/error states, and stable focus/scroll during polling | -| Host security | Route matrix generated from `app/` + action lint | Every discovered route has an authorization case and rejects missing/denied sessions without `proxy.ts`; a lint rule requires every `'use server'` export to call the authorization helper, and actions reject cross-resource arguments when called directly | -| Host integration | Node-owned Playwright + Compose | Real grpc-go gateway list/history flows, TLS and explicit h2c, polling, transport failures, and later mutation authorization | -| Published package | `pnpm pack` into an external Next app | The packed library and API tarballs install outside the workspace, their export maps match the root, `./server`, and `./testing`, the root stays free of `server-only`, and both supported Next/React bounds build | +- Typed gRPC details for known errors. Helpers classify `InvalidArgument`, `NotFound`, and `ResourceExhausted` as user errors, `Unavailable` and `DeadlineExceeded` as transient infrastructure errors, and every other code as an infrastructure error. Raw gateway messages are not rendered. +- A typed build URL on history occurrences. +- Status values stay strings. The library has a display table and an unknown fallback, checked against a fixture in `api/submitqueue/gateway/testdata/`. +- Protobuf `int64` timestamps become numeric milliseconds in presentation models after a safe-range check. -Browser UX tests use a separate build-time entry point that aliases the reference host's gateway and session providers to fakes; production builds contain neither fake, and a build check verifies the production artifact. Fixtures cover scripted status changes, unknown statuses, long and numerous URIs, long errors, full pages, omitted path templates, and per-row capabilities. Screenshots are deterministic, and the lowest and latest supported Next/React versions run the component and browser suites. +## Acceptance -Storybook is not required initially because the fixture build already renders deterministic browser states. +- Go and Vitest tests cover proto drift, the status fixture, timestamp conversion, stable list bounds, base64url paths, trusted links, and error classification. +- Component tests cover rendering and polling controls: single-flight, jitter and backoff, hidden and offline pause, and terminal stop. +- Playwright and axe-core cover browser accessibility, responsive and visual states, navigation, and stable focus and scroll. +- Every host route rejects a missing or denied session without relying on `proxy.ts`. +- A Node-owned Compose check, outside Bazel and in required checks, runs the reference host against the real grpc-go gateway as `e2e-submitqueue-web` ([testing guide](../howto/TESTING.md#container-naming)). It covers TLS and explicit h2c, and a slash-containing sqid fetched through the deployment proxy. +- `pnpm pack` tarballs install into an external Next app, match the root, `./server`, and `./testing` export map, keep `server-only` out of the root, and build at `next >=16.3 <17` with both supported React 19 bounds. +- Browser fakes are a build-time alias. The production artifact contains none. ## Rejected -- **Container image instead of a library.** Hosts could not replace authentication, telemetry, or transport without a permanent source fork. -- **One catch-all route.** `app/[[...slug]]/page.tsx` would replace Next routing and lose per-segment layouts, metadata, streaming, Suspense boundaries, and caching. -- **Static export or embedding in a Go binary.** Dynamic request routes cannot be enumerated, server actions and authoritative route checks disappear, and the grpc-go gateway needs a browser-reachable proxy with its own resource authorization. Revisit only with a separate design. -- **Web extensions, DI, or a generic platform engine.** The gateway and Next filesystem already provide domain and route boundaries. Extra interfaces or runtime assembly duplicate them; shared code is promoted only after another domain demonstrates reuse. -- **Library-owned process infrastructure.** Auth SDKs, logger/metrics backends, environment defaults, token sources, clients, and registries belong to the host. OpenTelemetry supplies the metrics contract; logging uses a structural type the host's logger satisfies. -- **GraphQL or REST BFF.** Server components already provide the BFF and the generated gateway client is the domain boundary; another wire contract adds no capability. -- **Shared Go/TypeScript view model through WASM or code generation.** A cross-language status fixture catches drift; the remaining presentation code is cheaper than a cross-language runtime. -- **Non-Next SPA.** Server components keep gateway access and authorization on the server; an SPA moves both into the browser. +- **A repository-owned application or container.** Another deployer would have to fork routes, authentication, telemetry, and transport. +- **Static export or embedding in a Go binary.** Request routes are dynamic, and the gateway stays private to a server that can authorize the caller. +- **A generic runtime, DI container, or web-extension layer.** Next's filesystem and the generated gateway client are the composition boundaries. From 5b67f948a1c8edef3f4b5c041e92a87fb8fd61f9 Mon Sep 17 00:00:00 2001 From: Preetam Dwivedi Date: Mon, 28 Sep 2026 11:31:02 -0700 Subject: [PATCH 5/5] feat(web): add read-only SubmitQueue request UX MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary ### Why? SubmitQueue had gateway APIs for request summaries and history but no simple browser experience for local demos or status inspection. ### What? Add an isolated pnpm workspace with generated TypeScript protobuf bindings, a packaged read-only React library, and a Basic-authenticated Next.js reference host for the hard-coded demo queue. The host lists the trailing 24 hours of requests, safely routes slash-containing SQIDs, shows lifecycle and build history, and refreshes without browser-to-gateway access. Add Compose and Make integration, package-consumer and component coverage, a real-stack Playwright and axe scenario, local documentation, and required web CI jobs. ## Test Plan - ✅ `make lint` - ✅ `make check-tidy` - ✅ `make check-gazelle` - ✅ `make build` - ✅ `make test` - ✅ `PATH=/Users/preetam/.nvm/versions/node/v24.19.0/bin:$PATH make web-check` (69 tests; generation, packaging, lint, typecheck, and Next production build passed) - ✅ Compose configuration, E2E shell syntax, and Playwright test discovery validated locally - ⚠️ `make web-e2e-test` could not run locally because the Docker daemon was unavailable; the required CI job runs the full real-stack Playwright and axe test --- .bazelignore | 1 + .dockerignore | 10 + .github/workflows/ci.yml | 41 + .gitignore | 8 + Makefile | 40 +- doc/howto/QUICKSTART.md | 5 + service/submitqueue/docker-compose.web.yml | 34 + web/.nvmrc | 1 + web/api/package.json | 32 + web/api/src/api.test.ts | 23 + .../gen/api/base/change/proto/change_pb.ts | 74 + .../mergestrategy/proto/mergestrategy_pb.ts | 95 + .../submitqueue/gateway/proto/gateway_pb.ts | 822 +++ web/api/tsconfig.build.json | 11 + web/api/tsconfig.json | 7 + web/buf.gen.yaml | 8 + web/e2e/submitqueue.spec.ts | 103 + web/go.mod | 3 + web/package.json | 33 + web/playwright.config.ts | 26 + web/pnpm-lock.yaml | 5968 +++++++++++++++++ web/pnpm-workspace.yaml | 8 + web/service/submitqueue/.gitignore | 3 + web/service/submitqueue/Dockerfile | 38 + web/service/submitqueue/eslint.config.mjs | 7 + web/service/submitqueue/next-env.d.ts | 7 + web/service/submitqueue/next.config.ts | 13 + web/service/submitqueue/package.json | 34 + .../src/app/(protected)/layout.tsx | 31 + .../submitqueue/src/app/(protected)/page.tsx | 18 + .../app/(protected)/requests/[sqid]/page.tsx | 87 + .../src/app/(protected)/requests/page.tsx | 79 + web/service/submitqueue/src/app/globals.css | 505 ++ .../submitqueue/src/app/healthz/route.test.ts | 13 + .../submitqueue/src/app/healthz/route.ts | 8 + web/service/submitqueue/src/app/layout.tsx | 20 + web/service/submitqueue/src/app/not-found.tsx | 16 + .../submitqueue/src/app/unauthorized.tsx | 11 + web/service/submitqueue/src/proxy.test.ts | 46 + web/service/submitqueue/src/proxy.ts | 21 + .../submitqueue/src/server/auth.test.ts | 54 + web/service/submitqueue/src/server/auth.ts | 86 + .../submitqueue/src/server/config.test.ts | 40 + web/service/submitqueue/src/server/config.ts | 45 + web/service/submitqueue/src/server/gateway.ts | 28 + .../submitqueue/src/server/request-auth.ts | 18 + .../submitqueue/src/server/window.test.ts | 41 + web/service/submitqueue/src/server/window.ts | 54 + .../submitqueue/src/test/server-only.ts | 1 + web/service/submitqueue/tsconfig.json | 36 + web/service/submitqueue/vitest.config.ts | 13 + web/submitqueue/.gitignore | 2 + web/submitqueue/package.json | 53 + web/submitqueue/src/auto-refresh.test.tsx | 87 + web/submitqueue/src/auto-refresh.tsx | 100 + web/submitqueue/src/components.test.tsx | 100 + web/submitqueue/src/components.tsx | 180 + web/submitqueue/src/index.ts | 32 + web/submitqueue/src/models.ts | 55 + web/submitqueue/src/paths.test.ts | 27 + web/submitqueue/src/paths.ts | 79 + web/submitqueue/src/server.test.ts | 129 + web/submitqueue/src/server.ts | 293 + web/submitqueue/src/status.test.ts | 24 + web/submitqueue/src/status.ts | 49 + web/submitqueue/src/test-setup.ts | 1 + web/submitqueue/src/testing.ts | 75 + web/submitqueue/tsconfig.build.json | 12 + web/submitqueue/tsconfig.json | 17 + web/submitqueue/vitest.config.ts | 9 + web/tool/check-packages.mjs | 121 + web/tool/generate-api.mjs | 82 + web/tool/run-e2e.sh | 54 + web/tsconfig.base.json | 18 + web/tsconfig.e2e.json | 7 + web/vitest.config.ts | 10 + 76 files changed, 10334 insertions(+), 8 deletions(-) create mode 100644 .dockerignore create mode 100644 service/submitqueue/docker-compose.web.yml create mode 100644 web/.nvmrc create mode 100644 web/api/package.json create mode 100644 web/api/src/api.test.ts create mode 100644 web/api/src/gen/api/base/change/proto/change_pb.ts create mode 100644 web/api/src/gen/api/base/mergestrategy/proto/mergestrategy_pb.ts create mode 100644 web/api/src/gen/api/submitqueue/gateway/proto/gateway_pb.ts create mode 100644 web/api/tsconfig.build.json create mode 100644 web/api/tsconfig.json create mode 100644 web/buf.gen.yaml create mode 100644 web/e2e/submitqueue.spec.ts create mode 100644 web/go.mod create mode 100644 web/package.json create mode 100644 web/playwright.config.ts create mode 100644 web/pnpm-lock.yaml create mode 100644 web/pnpm-workspace.yaml create mode 100644 web/service/submitqueue/.gitignore create mode 100644 web/service/submitqueue/Dockerfile create mode 100644 web/service/submitqueue/eslint.config.mjs create mode 100644 web/service/submitqueue/next-env.d.ts create mode 100644 web/service/submitqueue/next.config.ts create mode 100644 web/service/submitqueue/package.json create mode 100644 web/service/submitqueue/src/app/(protected)/layout.tsx create mode 100644 web/service/submitqueue/src/app/(protected)/page.tsx create mode 100644 web/service/submitqueue/src/app/(protected)/requests/[sqid]/page.tsx create mode 100644 web/service/submitqueue/src/app/(protected)/requests/page.tsx create mode 100644 web/service/submitqueue/src/app/globals.css create mode 100644 web/service/submitqueue/src/app/healthz/route.test.ts create mode 100644 web/service/submitqueue/src/app/healthz/route.ts create mode 100644 web/service/submitqueue/src/app/layout.tsx create mode 100644 web/service/submitqueue/src/app/not-found.tsx create mode 100644 web/service/submitqueue/src/app/unauthorized.tsx create mode 100644 web/service/submitqueue/src/proxy.test.ts create mode 100644 web/service/submitqueue/src/proxy.ts create mode 100644 web/service/submitqueue/src/server/auth.test.ts create mode 100644 web/service/submitqueue/src/server/auth.ts create mode 100644 web/service/submitqueue/src/server/config.test.ts create mode 100644 web/service/submitqueue/src/server/config.ts create mode 100644 web/service/submitqueue/src/server/gateway.ts create mode 100644 web/service/submitqueue/src/server/request-auth.ts create mode 100644 web/service/submitqueue/src/server/window.test.ts create mode 100644 web/service/submitqueue/src/server/window.ts create mode 100644 web/service/submitqueue/src/test/server-only.ts create mode 100644 web/service/submitqueue/tsconfig.json create mode 100644 web/service/submitqueue/vitest.config.ts create mode 100644 web/submitqueue/.gitignore create mode 100644 web/submitqueue/package.json create mode 100644 web/submitqueue/src/auto-refresh.test.tsx create mode 100644 web/submitqueue/src/auto-refresh.tsx create mode 100644 web/submitqueue/src/components.test.tsx create mode 100644 web/submitqueue/src/components.tsx create mode 100644 web/submitqueue/src/index.ts create mode 100644 web/submitqueue/src/models.ts create mode 100644 web/submitqueue/src/paths.test.ts create mode 100644 web/submitqueue/src/paths.ts create mode 100644 web/submitqueue/src/server.test.ts create mode 100644 web/submitqueue/src/server.ts create mode 100644 web/submitqueue/src/status.test.ts create mode 100644 web/submitqueue/src/status.ts create mode 100644 web/submitqueue/src/test-setup.ts create mode 100644 web/submitqueue/src/testing.ts create mode 100644 web/submitqueue/tsconfig.build.json create mode 100644 web/submitqueue/tsconfig.json create mode 100644 web/submitqueue/vitest.config.ts create mode 100644 web/tool/check-packages.mjs create mode 100644 web/tool/generate-api.mjs create mode 100755 web/tool/run-e2e.sh create mode 100644 web/tsconfig.base.json create mode 100644 web/tsconfig.e2e.json create mode 100644 web/vitest.config.ts diff --git a/.bazelignore b/.bazelignore index e46df37b6..c98fc8999 100644 --- a/.bazelignore +++ b/.bazelignore @@ -2,3 +2,4 @@ pkg bin .git .idea +web diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 000000000..28f1a1789 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,10 @@ +.git +bazel-* +bin +web/**/.next +web/**/coverage +web/**/dist +web/**/node_modules +web/**/playwright-report +web/**/test-results +web/**/*.tsbuildinfo diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d37772938..e3aab7397 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -104,6 +104,24 @@ jobs: if: ${{ failure() }} uses: ./.github/actions/upload-testlogs + web: + name: Web + if: ${{ github.event_name != 'pull_request' || github.event.pull_request.draft == false }} + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 + with: + persist-credentials: false + - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 + with: + node-version-file: web/.nvmrc + - name: Enable Corepack + run: corepack enable + - name: Install web dependencies + run: make web-install + - name: Run web checks + run: make web-check + # --------------------------------------------------------------------------- # INTEGRATION TESTS (e2e, gateway, orchestrator) # --------------------------------------------------------------------------- @@ -126,6 +144,27 @@ jobs: if: ${{ failure() }} uses: ./.github/actions/upload-testlogs + web-e2e: + name: Web E2E + if: ${{ github.event_name != 'pull_request' || github.event.pull_request.draft == false }} + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 + with: + persist-credentials: false + - uses: ./.github/actions/setup + - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 + with: + node-version-file: web/.nvmrc + - name: Enable Corepack + run: corepack enable + - name: Install web dependencies + run: make web-install + - name: Install Playwright Chromium + run: corepack pnpm@10.17.1 --dir web exec playwright install --with-deps chromium + - name: Run real-stack web E2E + run: make web-e2e-test + gateway-integration-test: name: Gateway Integration Test if: ${{ github.event_name != 'pull_request' || github.event.pull_request.draft == false }} @@ -310,6 +349,8 @@ jobs: - queue-integration-test - storage-integration-test - consumer-integration-test + - web + - web-e2e - workflow-security steps: - name: Fail if any required check did not succeed diff --git a/.gitignore b/.gitignore index 08d5b9cc1..00326615e 100644 --- a/.gitignore +++ b/.gitignore @@ -20,3 +20,11 @@ bin/ # Make completion cache .make_targets_cache +/web/node_modules/ +/web/**/node_modules/ +/web/**/dist/ +/web/**/.next/ +/web/**/coverage/ +/web/**/playwright-report/ +/web/**/test-results/ +/web/**/*.tsbuildinfo diff --git a/Makefile b/Makefile index b15be5161..16877c486 100644 --- a/Makefile +++ b/Makefile @@ -6,6 +6,7 @@ COMPOSE = docker-compose # SubmitQueue compose files COMPOSE_FILE = service/submitqueue/docker-compose.yml +WEB_COMPOSE_FILE = service/submitqueue/docker-compose.web.yml GATEWAY_COMPOSE_FILE = service/submitqueue/gateway/server/docker-compose.yml ORCHESTRATOR_COMPOSE_FILE = service/submitqueue/orchestrator/server/docker-compose.yml @@ -41,6 +42,10 @@ PROTO_PACKAGES = api/base/change api/base/hook api/base/mergestrategy api/base/m # Set REPO_ROOT for docker-compose export REPO_ROOT := $(shell pwd) +# Local-only credential for the reference web host. Override it for manual +# testing; production authentication is deliberately outside this demo. +export SUBMITQUEUE_WEB_TOKEN ?= submitqueue-local-demo-token-change-me + # Which provider the demo stack targets, and the only difference between a free # local run and a live one. Selects a configuration directory rather than a code # path, so adding a provider is mostly adding a directory — see @@ -148,7 +153,7 @@ define assert_clean fi endef -.PHONY: build build-all-linux build-runway-linux build-submitqueue-gateway-client build-submitqueue-gateway-linux build-submitqueue-gateway-server build-submitqueue-orchestrator-linux build-stovepipe-linux build-stovepipe-linux-debug check-gazelle check-mocks check-tidy clean clean-proto demo-requests deps e2e-test fmt gazelle integration-test integration-test-submitqueue-consumer integration-test-extensions integration-test-submitqueue-gateway integration-test-submitqueue-orchestrator license-fix lint lint-binary lint-fmt lint-license local-init-runway-queue-schema local-init-stovepipe-schemas local-runway-start local-runway-stop local-submitqueue-stop local-submitqueue-clean local-submitqueue-gateway-start local-submitqueue-gateway-stop local-init-submitqueue-schemas local-submitqueue-logs local-submitqueue-orchestrator-start local-submitqueue-orchestrator-stop local-submitqueue-ps local-submitqueue-restart local-submitqueue-start local-stop local-stovepipe-debug-start local-stovepipe-logs local-stovepipe-start local-stovepipe-stop mocks proto query-deps query-targets run-client-runway run-client-submitqueue-gateway run-client-submitqueue-orchestrator run-client-stovepipe run-queue-admin test test-no-cache test-race tidy tidy-bazel tidy-go help +.PHONY: build build-all-linux build-runway-linux build-submitqueue-gateway-client build-submitqueue-gateway-linux build-submitqueue-gateway-server build-submitqueue-orchestrator-linux build-stovepipe-linux build-stovepipe-linux-debug check-gazelle check-mocks check-tidy clean clean-proto demo-requests deps e2e-test fmt gazelle integration-test integration-test-submitqueue-consumer integration-test-extensions integration-test-submitqueue-gateway integration-test-submitqueue-orchestrator license-fix lint lint-binary lint-fmt lint-license local-init-runway-queue-schema local-init-stovepipe-schemas local-runway-start local-runway-stop local-submitqueue-stop local-submitqueue-clean local-submitqueue-gateway-start local-submitqueue-gateway-stop local-init-submitqueue-schemas local-submitqueue-logs local-submitqueue-orchestrator-start local-submitqueue-orchestrator-stop local-submitqueue-ps local-submitqueue-restart local-submitqueue-start local-stop local-stovepipe-debug-start local-stovepipe-logs local-stovepipe-start local-stovepipe-stop mocks proto query-deps query-targets run-client-runway run-client-submitqueue-gateway run-client-submitqueue-orchestrator run-client-stovepipe run-queue-admin test test-no-cache test-race tidy tidy-bazel tidy-go web-build web-check web-e2e-test web-install web-proto help build: ## Build all services and examples @@ -361,7 +366,7 @@ local-submitqueue-clean: ## Stop the stack and remove its volumes, images, and P @echo "Cleaning all services and data..." @# The overlay is named so that volumes it declares — Runway's checkouts — @# are removed too, rather than surviving as an orphan. - @$(COMPOSE) -f $(COMPOSE_FILE) -f $(PROVIDER_COMPOSE_FILE) -p $(SUBMITQUEUE_LOCAL_PROJECT) down -v --rmi local + @$(COMPOSE) -f $(COMPOSE_FILE) -f $(PROVIDER_COMPOSE_FILE) -f $(WEB_COMPOSE_FILE) -p $(SUBMITQUEUE_LOCAL_PROJECT) down -v --rmi local @rm -rf "$(SQ_GIT_SANDBOX_DIR)" @echo "All services, volumes, images, and $(SQ_GIT_SANDBOX_DIR) removed." @@ -442,7 +447,7 @@ local-runway-stop: ## Stop Runway service @echo "Runway services stopped." local-submitqueue-logs: ## View logs from all running services - @$(COMPOSE) -f $(COMPOSE_FILE) -p $(SUBMITQUEUE_LOCAL_PROJECT) logs -f + @$(COMPOSE) -f $(COMPOSE_FILE) -f $(PROVIDER_COMPOSE_FILE) -f $(WEB_COMPOSE_FILE) -p $(SUBMITQUEUE_LOCAL_PROJECT) logs -f local-submitqueue-orchestrator-start: build-submitqueue-orchestrator-linux ## Start Orchestrator service locally (Orchestrator + 2 MySQL databases) @echo "Starting Orchestrator with docker-compose..." @@ -466,11 +471,12 @@ local-submitqueue-orchestrator-stop: ## Stop Orchestrator service local-submitqueue-ps: ## Show running containers and their ports @echo "Running containers and ports:" @echo "" - @$(COMPOSE) -f $(COMPOSE_FILE) -p $(SUBMITQUEUE_LOCAL_PROJECT) ps + @$(COMPOSE) -f $(COMPOSE_FILE) -f $(PROVIDER_COMPOSE_FILE) -f $(WEB_COMPOSE_FILE) -p $(SUBMITQUEUE_LOCAL_PROJECT) ps @echo "" @echo "📡 Service Endpoints:" @echo " Gateway gRPC: localhost:$$(docker port $(SUBMITQUEUE_LOCAL_PROJECT)-gateway-service-1 8080 2>/dev/null | cut -d: -f2 || echo 'not running')" @echo " Orchestrator gRPC: localhost:$$(docker port $(SUBMITQUEUE_LOCAL_PROJECT)-orchestrator-service-1 8080 2>/dev/null | cut -d: -f2 || echo 'not running')" + @echo " Web UX: http://localhost:$$(docker port $(SUBMITQUEUE_LOCAL_PROJECT)-web-service-1 3000 2>/dev/null | cut -d: -f2 || echo 'not running')" @echo "" @echo "🗄️ Database Endpoints:" @echo " MySQL App: localhost:$$(docker port $(SUBMITQUEUE_LOCAL_PROJECT)-mysql-app-1 3306 2>/dev/null | cut -d: -f2 || echo 'not running')" @@ -488,7 +494,7 @@ local-submitqueue-ps: ## Show running containers and their ports local-submitqueue-restart: build-all-linux ## Restart all services (rebuild and restart) @echo "Restarting all services..." - @$(COMPOSE) -f $(COMPOSE_FILE) -p $(SUBMITQUEUE_LOCAL_PROJECT) restart + @$(COMPOSE) -f $(COMPOSE_FILE) -f $(PROVIDER_COMPOSE_FILE) -f $(WEB_COMPOSE_FILE) -p $(SUBMITQUEUE_LOCAL_PROJECT) restart @echo "Services restarted!" @make local-submitqueue-ps @@ -511,13 +517,16 @@ local-submitqueue-start: build-all-linux ## Start full stack (PROVIDER=fake|git| else \ export SQ_CONTAINER_USER=$$(id -u):$$(id -g); \ fi; \ - $(COMPOSE) -f $(COMPOSE_FILE) -f $(PROVIDER_COMPOSE_FILE) -p $(SUBMITQUEUE_LOCAL_PROJECT) up -d --build --wait + $(COMPOSE) -f $(COMPOSE_FILE) -f $(PROVIDER_COMPOSE_FILE) -f $(WEB_COMPOSE_FILE) -p $(SUBMITQUEUE_LOCAL_PROJECT) up -d --build --wait @echo "Applying database schemas..." @$(MAKE) -s local-init-submitqueue-schemas @echo "" @echo "✅ Stack is running against provider '$(PROVIDER)'." @echo "" @echo "Gateway gRPC port: $$(docker port $(SUBMITQUEUE_LOCAL_PROJECT)-gateway-service-1 8080 2>/dev/null | cut -d: -f2 || echo 'unknown')" + @echo "Web UX: http://localhost:$$(docker port $(SUBMITQUEUE_LOCAL_PROJECT)-web-service-1 3000 2>/dev/null | cut -d: -f2 || echo 'unknown')" + @echo "Web username: submitqueue" + @echo "Web token: $(SUBMITQUEUE_WEB_TOKEN)" @if [ "$(PROVIDER)" = "git" ]; then \ echo "Land target: $(SQ_GIT_SANDBOX_DIR)/sandbox.git"; \ fi @@ -527,7 +536,7 @@ local-submitqueue-start: build-all-linux ## Start full stack (PROVIDER=fake|git| local-submitqueue-stop: ## Stop the SubmitQueue stack (keeps PROVIDER=git's sandbox; the databases do not survive) @echo "Stopping SubmitQueue services..." - @$(COMPOSE) -f $(COMPOSE_FILE) -f $(PROVIDER_COMPOSE_FILE) -p $(SUBMITQUEUE_LOCAL_PROJECT) down + @$(COMPOSE) -f $(COMPOSE_FILE) -f $(PROVIDER_COMPOSE_FILE) -f $(WEB_COMPOSE_FILE) -p $(SUBMITQUEUE_LOCAL_PROJECT) down @# Both MySQL services mount anonymous volumes, so `down` detaches them and @# the next `up` creates fresh ones. Saying "data preserved" here would be @# read as "your requests are still there", which they are not. @@ -539,7 +548,7 @@ local-submitqueue-stop: ## Stop the SubmitQueue stack (keeps PROVIDER=git's sand local-stop: ## Stop every local stack — SubmitQueue, Stovepipe, and Runway (keep data) @echo "Stopping all services..." - @$(COMPOSE) -f $(COMPOSE_FILE) -p $(SUBMITQUEUE_LOCAL_PROJECT) down + @$(COMPOSE) -f $(COMPOSE_FILE) -f $(PROVIDER_COMPOSE_FILE) -f $(WEB_COMPOSE_FILE) -p $(SUBMITQUEUE_LOCAL_PROJECT) down @$(COMPOSE) -f $(STOVEPIPE_COMPOSE_FILE) -p $(STOVEPIPE_LOCAL_PROJECT) down @$(COMPOSE) -f $(RUNWAY_COMPOSE_FILE) -p $(RUNWAY_LOCAL_PROJECT) down @echo "Services stopped. Data volumes preserved." @@ -644,6 +653,21 @@ tidy-go: ## Run go mod tidy @echo "Running go mod tidy..." @$(BAZEL) run @rules_go//go -- mod tidy -e +web-build: web-install ## Build all web packages and the Next.js host + @corepack pnpm@10.17.1 --dir web build + +web-check: ## Run web generation, packaging, lint, type, unit, and build checks + @corepack pnpm@10.17.1 --dir web check + +web-e2e-test: build-submitqueue-gateway-linux build-submitqueue-orchestrator-linux build-runway-linux web-install ## Run the real-stack Playwright and axe web test + @web/tool/run-e2e.sh + +web-install: ## Install the pinned web workspace dependencies + @corepack pnpm@10.17.1 --dir web install --frozen-lockfile + +web-proto: web-install ## Regenerate the TypeScript protobuf API + @corepack pnpm@10.17.1 --dir web api:generate + help: ## Show this help message @echo "Available targets:" @echo "" diff --git a/doc/howto/QUICKSTART.md b/doc/howto/QUICKSTART.md index a094f8a0c..954fd5aba 100644 --- a/doc/howto/QUICKSTART.md +++ b/doc/howto/QUICKSTART.md @@ -30,10 +30,15 @@ Compose publishes each service on a **random** host port so several stacks can r ✅ Stack is running against provider 'fake'. Gateway gRPC port: 58537 +Web UX: http://localhost:58538 +Web username: submitqueue +Web token: submitqueue-local-demo-token-change-me ``` You do not have to note it down. Every command below finds the running stack's port for itself, which matters because Compose picks a fresh one on every start — a number copied from an earlier run is the most common reason a demo command cannot connect. Set `GATEWAY_ADDR=host:port` only to reach a gateway this Makefile did not start. +The Web UX is a read-only reference host for the same gateway. Open the printed URL and use the printed HTTP Basic credentials. It lists receipts from `demo-queue` in a fixed trailing 24-hour window; after `make demo-requests`, select any row to see its current status and retained lifecycle history. The host and its default credential are deliberately local-demo wiring, not a production authentication design. Override the token with `SUBMITQUEUE_WEB_TOKEN= make local-submitqueue-start` when desired. + ## Put traffic through it ```bash diff --git a/service/submitqueue/docker-compose.web.yml b/service/submitqueue/docker-compose.web.yml new file mode 100644 index 000000000..671517eca --- /dev/null +++ b/service/submitqueue/docker-compose.web.yml @@ -0,0 +1,34 @@ +# Compose overlay for the local, read-only SubmitQueue web demo. +# +# The browser talks only to this Next.js host. The host reaches the private +# grpc-go gateway over Compose's internal network using explicit plaintext +# HTTP/2, which is intentionally limited to the local demo. + +services: + web-service: + image: ${SQ_DOCKER_IMAGE_PREFIX:-submitqueue}-web-service:latest + build: + context: ${REPO_ROOT} + dockerfile: web/service/submitqueue/Dockerfile + ports: + - "3000" + environment: + - PORT=3000 + - HOSTNAME=0.0.0.0 + - SUBMITQUEUE_GATEWAY_URL=http://gateway-service:8080 + - SUBMITQUEUE_GATEWAY_ALLOW_PLAINTEXT=true + - SUBMITQUEUE_WEB_TOKEN=${SUBMITQUEUE_WEB_TOKEN:?set SUBMITQUEUE_WEB_TOKEN to at least 32 characters} + depends_on: + gateway-service: + condition: service_started + healthcheck: + test: + [ + "CMD", + "node", + "-e", + "fetch('http://127.0.0.1:3000/healthz').then(r=>{if(!r.ok)process.exit(1)}).catch(()=>process.exit(1))", + ] + interval: 2s + timeout: 2s + retries: 30 diff --git a/web/.nvmrc b/web/.nvmrc new file mode 100644 index 000000000..442c7587a --- /dev/null +++ b/web/.nvmrc @@ -0,0 +1 @@ +22.20.0 diff --git a/web/api/package.json b/web/api/package.json new file mode 100644 index 000000000..02e08eee1 --- /dev/null +++ b/web/api/package.json @@ -0,0 +1,32 @@ +{ + "name": "@submitqueue/api", + "version": "0.1.0", + "private": true, + "type": "module", + "files": [ + "dist" + ], + "exports": { + "./base/change": { + "types": "./dist/api/base/change/proto/change_pb.d.ts", + "import": "./dist/api/base/change/proto/change_pb.js" + }, + "./base/mergestrategy": { + "types": "./dist/api/base/mergestrategy/proto/mergestrategy_pb.d.ts", + "import": "./dist/api/base/mergestrategy/proto/mergestrategy_pb.js" + }, + "./submitqueue/gateway": { + "types": "./dist/api/submitqueue/gateway/proto/gateway_pb.d.ts", + "import": "./dist/api/submitqueue/gateway/proto/gateway_pb.js" + } + }, + "scripts": { + "build": "node -e \"require('node:fs').rmSync('dist',{recursive:true,force:true})\" && tsc --project tsconfig.build.json", + "lint": "tsc --project tsconfig.json", + "test": "vitest run", + "typecheck": "tsc --project tsconfig.json" + }, + "dependencies": { + "@bufbuild/protobuf": "2.15.0" + } +} diff --git a/web/api/src/api.test.ts b/web/api/src/api.test.ts new file mode 100644 index 000000000..74439b1d8 --- /dev/null +++ b/web/api/src/api.test.ts @@ -0,0 +1,23 @@ +import { create } from "@bufbuild/protobuf"; +import { describe, expect, it } from "vitest"; + +import { + ListRequestSchema, + SubmitQueueGateway, +} from "./gen/api/submitqueue/gateway/proto/gateway_pb.js"; + +describe("generated gateway API", () => { + it("constructs list requests without losing int64 values", () => { + const request = create(ListRequestSchema, { + queue: "demo-queue", + receivedAtOrAfterMs: 1_700_000_000_000n, + receivedBeforeMs: 1_700_086_400_000n, + pageSize: 50, + }); + + expect(request.queue).toBe("demo-queue"); + expect(request.receivedAtOrAfterMs).toBe(1_700_000_000_000n); + expect(request.receivedBeforeMs).toBe(1_700_086_400_000n); + expect(SubmitQueueGateway.methods.some((method) => method.localName === "list")).toBe(true); + }); +}); diff --git a/web/api/src/gen/api/base/change/proto/change_pb.ts b/web/api/src/gen/api/base/change/proto/change_pb.ts new file mode 100644 index 000000000..a2241fb04 --- /dev/null +++ b/web/api/src/gen/api/base/change/proto/change_pb.ts @@ -0,0 +1,74 @@ +// Copyright (c) 2025 Uber Technologies, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// @generated by protoc-gen-es v2.15.0 with parameter "target=ts,import_extension=js" +// @generated from file api/base/change/proto/change.proto (package uber.base.change, syntax proto3) +/* eslint-disable */ + +import type { GenFile, GenMessage } from "@bufbuild/protobuf/codegenv2"; +import { fileDesc, messageDesc } from "@bufbuild/protobuf/codegenv2"; +import type { Message } from "@bufbuild/protobuf"; + +/** + * Describes the file api/base/change/proto/change.proto. + */ +export const file_api_base_change_proto_change: GenFile = /*@__PURE__*/ + fileDesc("CiJhcGkvYmFzZS9jaGFuZ2UvcHJvdG8vY2hhbmdlLnByb3RvEhB1YmVyLmJhc2UuY2hhbmdlIhYKBkNoYW5nZRIMCgR1cmlzGAEgAygJQmYKIGNvbS51YmVyLnN1Ym1pdHF1ZXVlLmJhc2UuY2hhbmdlQgtDaGFuZ2VQcm90b1ABWjNnaXRodWIuY29tL3ViZXIvc3VibWl0cXVldWUvYXBpL2Jhc2UvY2hhbmdlL3Byb3RvcGJiBnByb3RvMw"); + +/** + * Change represents a code change identified by URIs from a code change provider + * (e.g. a GitHub Pull Request, a Phabricator Diff, or a git commit). The provider + * is extracted from the URI scheme. + * + * This is the shared wire contract for change identity, reused across SubmitQueue, + * Stovepipe, and other repo-local domains — the proto-level analog of the + * platform/base/change Go entity. Domains import it rather than redefining their own. + * + * @generated from message uber.base.change.Change + */ +export type Change = Message<"uber.base.change.Change"> & { + /** + * URIs identifying the change(s) (RFC 3986 compliant): scheme:///. + * The scheme identifies the change provider, the authority is the provider instance + * the change lives on, and the path contains provider-specific resource identifiers. + * + * Supported by default (other providers can be added): + * GitHub PR: "github://///pull//" + * Phabricator Diff: "phab:///D/" + * git commit: "git://///" + * ( is a fully-qualified, percent-encoded git ref) + * + * The commit SHA must be the full 40-character lowercase hex SHA; abbreviated + * SHAs are rejected because downstream staleness checks compare by strict equality. + * + * One URI is one unit of change -- a single pull request, revision, or + * commit -- and it is the granularity everything downstream operates at. A + * list is therefore an ordered set of distinct changes (a stack), not one + * change described several ways: they are applied in the order given, each + * on top of the last, and each yields its own result. An integration + * strategy chosen for a change applies to every URI in the list, the same + * way to each. + * + * @generated from field: repeated string uris = 1; + */ + uris: string[]; +}; + +/** + * Describes the message uber.base.change.Change. + * Use `create(ChangeSchema)` to create a new message. + */ +export const ChangeSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_api_base_change_proto_change, 0); + diff --git a/web/api/src/gen/api/base/mergestrategy/proto/mergestrategy_pb.ts b/web/api/src/gen/api/base/mergestrategy/proto/mergestrategy_pb.ts new file mode 100644 index 000000000..84c45fc23 --- /dev/null +++ b/web/api/src/gen/api/base/mergestrategy/proto/mergestrategy_pb.ts @@ -0,0 +1,95 @@ +// Copyright (c) 2025 Uber Technologies, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// @generated by protoc-gen-es v2.15.0 with parameter "target=ts,import_extension=js" +// @generated from file api/base/mergestrategy/proto/mergestrategy.proto (package uber.base.mergestrategy, syntax proto3) +/* eslint-disable */ + +import type { GenEnum, GenFile } from "@bufbuild/protobuf/codegenv2"; +import { enumDesc, fileDesc } from "@bufbuild/protobuf/codegenv2"; + +/** + * Describes the file api/base/mergestrategy/proto/mergestrategy.proto. + */ +export const file_api_base_mergestrategy_proto_mergestrategy: GenFile = /*@__PURE__*/ + fileDesc("CjBhcGkvYmFzZS9tZXJnZXN0cmF0ZWd5L3Byb3RvL21lcmdlc3RyYXRlZ3kucHJvdG8SF3ViZXIuYmFzZS5tZXJnZXN0cmF0ZWd5Kk4KCFN0cmF0ZWd5EgsKB0RFRkFVTFQQABIKCgZSRUJBU0UQARIRCg1TUVVBU0hfUkVCQVNFEAISCQoFTUVSR0UQAxILCgdQUk9NT1RFEARCewonY29tLnViZXIuc3VibWl0cXVldWUuYmFzZS5tZXJnZXN0cmF0ZWd5QhJNZXJnZVN0cmF0ZWd5UHJvdG9QAVo6Z2l0aHViLmNvbS91YmVyL3N1Ym1pdHF1ZXVlL2FwaS9iYXNlL21lcmdlc3RyYXRlZ3kvcHJvdG9wYmIGcHJvdG8z"); + +/** + * Strategy defines the possible source control integration methods — how a change + * is integrated into the target branch. + * + * This is the shared wire contract for merge strategy, reused across SubmitQueue + * and other repo-local domains — the proto-level analog of the + * platform/base/mergestrategy Go entity. Domains import it rather than redefining their own. + * + * A strategy is chosen once for a change and applies to every URI that change + * carries, the same way to each. Since one URI is one unit of change (see + * uber.base.change.Change), the values below are defined per URI: a change + * carrying three URIs is integrated as three separate applications of the same + * strategy, in order, not as one combined application. + * + * @generated from enum uber.base.mergestrategy.Strategy + */ +export enum Strategy { + /** + * Default strategy (let the server decide based on configuration). + * + * @generated from enum value: DEFAULT = 0; + */ + DEFAULT = 0, + + /** + * Rebase the commits the change introduces onto the target branch before landing. + * + * @generated from enum value: REBASE = 1; + */ + REBASE = 1, + + /** + * Same as REBASE, then squash into a single commit. The squash unit is the + * individual change: a change of ten commits becomes one commit, and a + * stack of three URIs becomes three commits rather than one, so the + * boundary between the stacked changes survives. + * + * @generated from enum value: SQUASH_REBASE = 2; + */ + SQUASH_REBASE = 2, + + /** + * Merge into the target branch by creating a separate merge commit, preserving commit history along with hashes. + * + * @generated from enum value: MERGE = 3; + */ + MERGE = 3, + + /** + * Integrate the exact revision as-is, with no content transform — advance the target branch to an already-existing + * commit rather than producing new revisions. The implementer maps it to its backend: git fast-forward, Mercurial + * bookmark advance, Subversion/Perforce copy. Used to promote an already-landed/verified commit onto another branch. + * + * The one strategy that constrains the list rather than repeating over it: + * advancing a ref to an exact revision admits a single URI, because a + * second one could not also be the revision the target ends at. + * + * @generated from enum value: PROMOTE = 4; + */ + PROMOTE = 4, +} + +/** + * Describes the enum uber.base.mergestrategy.Strategy. + */ +export const StrategySchema: GenEnum = /*@__PURE__*/ + enumDesc(file_api_base_mergestrategy_proto_mergestrategy, 0); + diff --git a/web/api/src/gen/api/submitqueue/gateway/proto/gateway_pb.ts b/web/api/src/gen/api/submitqueue/gateway/proto/gateway_pb.ts new file mode 100644 index 000000000..96351a112 --- /dev/null +++ b/web/api/src/gen/api/submitqueue/gateway/proto/gateway_pb.ts @@ -0,0 +1,822 @@ +// Copyright (c) 2025 Uber Technologies, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// @generated by protoc-gen-es v2.15.0 with parameter "target=ts,import_extension=js" +// @generated from file api/submitqueue/gateway/proto/gateway.proto (package uber.submitqueue.gateway, syntax proto3) +/* eslint-disable */ + +import type { GenFile, GenMessage, GenService } from "@bufbuild/protobuf/codegenv2"; +import { fileDesc, messageDesc, serviceDesc } from "@bufbuild/protobuf/codegenv2"; +import type { Change } from "../../../base/change/proto/change_pb.js"; +import { file_api_base_change_proto_change } from "../../../base/change/proto/change_pb.js"; +import type { Strategy } from "../../../base/mergestrategy/proto/mergestrategy_pb.js"; +import { file_api_base_mergestrategy_proto_mergestrategy } from "../../../base/mergestrategy/proto/mergestrategy_pb.js"; +import type { Message } from "@bufbuild/protobuf"; + +/** + * Describes the file api/submitqueue/gateway/proto/gateway.proto. + */ +export const file_api_submitqueue_gateway_proto_gateway: GenFile = /*@__PURE__*/ + fileDesc("CithcGkvc3VibWl0cXVldWUvZ2F0ZXdheS9wcm90by9nYXRld2F5LnByb3RvEhh1YmVyLnN1Ym1pdHF1ZXVlLmdhdGV3YXkiHgoLUGluZ1JlcXVlc3QSDwoHbWVzc2FnZRgBIAEoCSJaCgxQaW5nUmVzcG9uc2USDwoHbWVzc2FnZRgBIAEoCRIUCgxzZXJ2aWNlX25hbWUYAiABKAkSEQoJdGltZXN0YW1wGAMgASgDEhAKCGhvc3RuYW1lGAQgASgJInsKC0xhbmRSZXF1ZXN0Eg0KBXF1ZXVlGAEgASgJEigKBmNoYW5nZRgCIAEoCzIYLnViZXIuYmFzZS5jaGFuZ2UuQ2hhbmdlEjMKCHN0cmF0ZWd5GAQgASgOMiEudWJlci5iYXNlLm1lcmdlc3RyYXRlZ3kuU3RyYXRlZ3kiHAoMTGFuZFJlc3BvbnNlEgwKBHNxaWQYASABKAkiPAoNQ2FuY2VsUmVxdWVzdBIMCgRzcWlkGAEgASgJEg4KBnJlYXNvbhgCIAEoCRINCgVxdWV1ZRgDIAEoCSIQCg5DYW5jZWxSZXNwb25zZSL5AQoOUmVxdWVzdFN1bW1hcnkSDAoEc3FpZBgBIAEoCRINCgVxdWV1ZRgCIAEoCRITCgtjaGFuZ2VfdXJpcxgDIAMoCRIWCg5yZWNlaXZlZF9hdF9tcxgEIAEoAxIOCgZzdGF0dXMYBSABKAkSEgoKbGFzdF9lcnJvchgGIAEoCRJICghtZXRhZGF0YRgHIAMoCzI2LnViZXIuc3VibWl0cXVldWUuZ2F0ZXdheS5SZXF1ZXN0U3VtbWFyeS5NZXRhZGF0YUVudHJ5Gi8KDU1ldGFkYXRhRW50cnkSCwoDa2V5GAEgASgJEg0KBXZhbHVlGAIgASgJOgI4ASI7ChxHZXRSZXF1ZXN0U3VtbWFyeUJ5SURSZXF1ZXN0EgwKBHNxaWQYASABKAkSDQoFcXVldWUYAiABKAkiWgodR2V0UmVxdWVzdFN1bW1hcnlCeUlEUmVzcG9uc2USOQoHcmVxdWVzdBgBIAEoCzIoLnViZXIuc3VibWl0cXVldWUuZ2F0ZXdheS5SZXF1ZXN0U3VtbWFyeSJICiNHZXRSZXF1ZXN0U3VtbWFyeUJ5Q2hhbmdlVVJJUmVxdWVzdBISCgpjaGFuZ2VfdXJpGAEgASgJEg0KBXF1ZXVlGAIgASgJImIKJEdldFJlcXVlc3RTdW1tYXJ5QnlDaGFuZ2VVUklSZXNwb25zZRI6CghyZXF1ZXN0cxgBIAMoCzIoLnViZXIuc3VibWl0cXVldWUuZ2F0ZXdheS5SZXF1ZXN0U3VtbWFyeSKAAQoLTGlzdFJlcXVlc3QSDQoFcXVldWUYASABKAkSHwoXcmVjZWl2ZWRfYXRfb3JfYWZ0ZXJfbXMYAiABKAMSGgoScmVjZWl2ZWRfYmVmb3JlX21zGAMgASgDEhEKCXBhZ2Vfc2l6ZRgEIAEoBRISCgpwYWdlX3Rva2VuGAUgASgJImMKDExpc3RSZXNwb25zZRI6CghyZXF1ZXN0cxgBIAMoCzIoLnViZXIuc3VibWl0cXVldWUuZ2F0ZXdheS5SZXF1ZXN0U3VtbWFyeRIXCg9uZXh0X3BhZ2VfdG9rZW4YAiABKAkiOwocR2V0UmVxdWVzdEhpc3RvcnlCeUlEUmVxdWVzdBIMCgRzcWlkGAEgASgJEg0KBXF1ZXVlGAIgASgJIt4BCgxIaXN0b3J5RXZlbnQSFAoMdGltZXN0YW1wX21zGAEgASgDEg4KBnN0YXR1cxgCIAEoCRISCgpsYXN0X2Vycm9yGAMgASgJEkYKCG1ldGFkYXRhGAQgAygLMjQudWJlci5zdWJtaXRxdWV1ZS5nYXRld2F5Lkhpc3RvcnlFdmVudC5NZXRhZGF0YUVudHJ5EgwKBHR5cGUYBSABKAkSDQoFZXZlbnQYBiABKAkaLwoNTWV0YWRhdGFFbnRyeRILCgNrZXkYASABKAkSDQoFdmFsdWUYAiABKAk6AjgBIlcKHUdldFJlcXVlc3RIaXN0b3J5QnlJRFJlc3BvbnNlEjYKBmV2ZW50cxgBIAMoCzImLnViZXIuc3VibWl0cXVldWUuZ2F0ZXdheS5IaXN0b3J5RXZlbnQiSAojR2V0UmVxdWVzdEhpc3RvcnlCeUNoYW5nZVVSSVJlcXVlc3QSEgoKY2hhbmdlX3VyaRgBIAEoCRINCgVxdWV1ZRgCIAEoCSJWCg5SZXF1ZXN0SGlzdG9yeRIMCgRzcWlkGAEgASgJEjYKBmV2ZW50cxgCIAMoCzImLnViZXIuc3VibWl0cXVldWUuZ2F0ZXdheS5IaXN0b3J5RXZlbnQiYwokR2V0UmVxdWVzdEhpc3RvcnlCeUNoYW5nZVVSSVJlc3BvbnNlEjsKCWhpc3RvcmllcxgBIAMoCzIoLnViZXIuc3VibWl0cXVldWUuZ2F0ZXdheS5SZXF1ZXN0SGlzdG9yeSIYCgVFcnJvchIPCgdtZXNzYWdlGAEgASgJIlcKFlVucmVjb2duaXplZFF1ZXVlRXJyb3ISLgoFZXJyb3IYASABKAsyHy51YmVyLnN1Ym1pdHF1ZXVlLmdhdGV3YXkuRXJyb3ISDQoFcXVldWUYAiABKAkiaAoUUmVxdWVzdE5vdEZvdW5kRXJyb3ISLgoFZXJyb3IYASABKAsyHy51YmVyLnN1Ym1pdHF1ZXVlLmdhdGV3YXkuRXJyb3ISDAoEc3FpZBgCIAEoCRISCgpjaGFuZ2VfdXJpGAMgASgJMtwHChJTdWJtaXRRdWV1ZUdhdGV3YXkSVwoEUGluZxIlLnViZXIuc3VibWl0cXVldWUuZ2F0ZXdheS5QaW5nUmVxdWVzdBomLnViZXIuc3VibWl0cXVldWUuZ2F0ZXdheS5QaW5nUmVzcG9uc2UiABJXCgRMYW5kEiUudWJlci5zdWJtaXRxdWV1ZS5nYXRld2F5LkxhbmRSZXF1ZXN0GiYudWJlci5zdWJtaXRxdWV1ZS5nYXRld2F5LkxhbmRSZXNwb25zZSIAEl0KBkNhbmNlbBInLnViZXIuc3VibWl0cXVldWUuZ2F0ZXdheS5DYW5jZWxSZXF1ZXN0GigudWJlci5zdWJtaXRxdWV1ZS5nYXRld2F5LkNhbmNlbFJlc3BvbnNlIgASigEKFUdldFJlcXVlc3RTdW1tYXJ5QnlJRBI2LnViZXIuc3VibWl0cXVldWUuZ2F0ZXdheS5HZXRSZXF1ZXN0U3VtbWFyeUJ5SURSZXF1ZXN0GjcudWJlci5zdWJtaXRxdWV1ZS5nYXRld2F5LkdldFJlcXVlc3RTdW1tYXJ5QnlJRFJlc3BvbnNlIgASnwEKHEdldFJlcXVlc3RTdW1tYXJ5QnlDaGFuZ2VVUkkSPS51YmVyLnN1Ym1pdHF1ZXVlLmdhdGV3YXkuR2V0UmVxdWVzdFN1bW1hcnlCeUNoYW5nZVVSSVJlcXVlc3QaPi51YmVyLnN1Ym1pdHF1ZXVlLmdhdGV3YXkuR2V0UmVxdWVzdFN1bW1hcnlCeUNoYW5nZVVSSVJlc3BvbnNlIgASVwoETGlzdBIlLnViZXIuc3VibWl0cXVldWUuZ2F0ZXdheS5MaXN0UmVxdWVzdBomLnViZXIuc3VibWl0cXVldWUuZ2F0ZXdheS5MaXN0UmVzcG9uc2UiABKKAQoVR2V0UmVxdWVzdEhpc3RvcnlCeUlEEjYudWJlci5zdWJtaXRxdWV1ZS5nYXRld2F5LkdldFJlcXVlc3RIaXN0b3J5QnlJRFJlcXVlc3QaNy51YmVyLnN1Ym1pdHF1ZXVlLmdhdGV3YXkuR2V0UmVxdWVzdEhpc3RvcnlCeUlEUmVzcG9uc2UiABKfAQocR2V0UmVxdWVzdEhpc3RvcnlCeUNoYW5nZVVSSRI9LnViZXIuc3VibWl0cXVldWUuZ2F0ZXdheS5HZXRSZXF1ZXN0SGlzdG9yeUJ5Q2hhbmdlVVJJUmVxdWVzdBo+LnViZXIuc3VibWl0cXVldWUuZ2F0ZXdheS5HZXRSZXF1ZXN0SGlzdG9yeUJ5Q2hhbmdlVVJJUmVzcG9uc2UiAEJrChxjb20udWJlci5zdWJtaXRxdWV1ZS5nYXRld2F5QgxHYXRld2F5UHJvdG9QAVo7Z2l0aHViLmNvbS91YmVyL3N1Ym1pdHF1ZXVlL2FwaS9zdWJtaXRxdWV1ZS9nYXRld2F5L3Byb3RvcGJiBnByb3RvMw", [file_api_base_change_proto_change, file_api_base_mergestrategy_proto_mergestrategy]); + +/** + * PingRequest is the request for the Ping method + * + * @generated from message uber.submitqueue.gateway.PingRequest + */ +export type PingRequest = Message<"uber.submitqueue.gateway.PingRequest"> & { + /** + * Optional message to include in the ping + * + * @generated from field: string message = 1; + */ + message: string; +}; + +/** + * Describes the message uber.submitqueue.gateway.PingRequest. + * Use `create(PingRequestSchema)` to create a new message. + */ +export const PingRequestSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_api_submitqueue_gateway_proto_gateway, 0); + +/** + * PingResponse is the response for the Ping method + * + * @generated from message uber.submitqueue.gateway.PingResponse + */ +export type PingResponse = Message<"uber.submitqueue.gateway.PingResponse"> & { + /** + * The response message + * + * @generated from field: string message = 1; + */ + message: string; + + /** + * The service name that handled the request + * + * @generated from field: string service_name = 2; + */ + serviceName: string; + + /** + * Timestamp of when the ping was received + * + * @generated from field: int64 timestamp = 3; + */ + timestamp: bigint; + + /** + * Hostname of the server that handled the request + * + * @generated from field: string hostname = 4; + */ + hostname: string; +}; + +/** + * Describes the message uber.submitqueue.gateway.PingResponse. + * Use `create(PingResponseSchema)` to create a new message. + */ +export const PingResponseSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_api_submitqueue_gateway_proto_gateway, 1); + +/** + * LandRequest defines a request to land a set of code changes on the source control repository's target branch. + * + * SubmitQueue guarantees changes are landed in order with no other changes in between. + * SubmitQueue does not guarantee each change is individually valid, but produces a validity marker on such changes. + * The user is responsible for including all changes in a stack in order, starting from the earliest change. + * + * @generated from message uber.submitqueue.gateway.LandRequest + */ +export type LandRequest = Message<"uber.submitqueue.gateway.LandRequest"> & { + /** + * Name of the queue processing the land request. The queue should be defined in the configuration, otherwise an error is returned. + * + * @generated from field: string queue = 1; + */ + queue: string; + + /** + * Change (such as a pull request) to land into the target branch. Target branch is defined by the queue configuration. + * + * @generated from field: uber.base.change.Change change = 2; + */ + change?: Change | undefined; + + /** + * Source control integration strategy to use for this land operation. If not specified, the default queue strategy is used. + * It applies to every URI the change carries, the same way to each — a land + * request cannot pick a different strategy per URI. + * + * @generated from field: uber.base.mergestrategy.Strategy strategy = 4; + */ + strategy: Strategy; +}; + +/** + * Describes the message uber.submitqueue.gateway.LandRequest. + * Use `create(LandRequestSchema)` to create a new message. + */ +export const LandRequestSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_api_submitqueue_gateway_proto_gateway, 2); + +/** + * LandResponse defines the response to a land request. + * + * @generated from message uber.submitqueue.gateway.LandResponse + */ +export type LandResponse = Message<"uber.submitqueue.gateway.LandResponse"> & { + /** + * Globally unique identifier for the land request. Used to track the land request lifecycle. + * + * @generated from field: string sqid = 1; + */ + sqid: string; +}; + +/** + * Describes the message uber.submitqueue.gateway.LandResponse. + * Use `create(LandResponseSchema)` to create a new message. + */ +export const LandResponseSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_api_submitqueue_gateway_proto_gateway, 3); + +/** + * CancelRequest defines a request to cancel an in-flight land request. If the request is part of a batch, the entire batch is cancelled. + * + * @generated from message uber.submitqueue.gateway.CancelRequest + */ +export type CancelRequest = Message<"uber.submitqueue.gateway.CancelRequest"> & { + /** + * Globally unique identifier of the land request to cancel, returned by a prior Land call. + * + * @generated from field: string sqid = 1; + */ + sqid: string; + + /** + * Optional human-readable reason for the cancellation. Recorded for observability. + * + * @generated from field: string reason = 2; + */ + reason: string; + + /** + * Name of the queue processing the request. Required. A sqid is only resolvable within its own queue, + * so naming a queue the request does not belong to is reported as not found. + * + * @generated from field: string queue = 3; + */ + queue: string; +}; + +/** + * Describes the message uber.submitqueue.gateway.CancelRequest. + * Use `create(CancelRequestSchema)` to create a new message. + */ +export const CancelRequestSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_api_submitqueue_gateway_proto_gateway, 4); + +/** + * CancelResponse defines the response to a cancel request. Empty on success. + * + * A successful response indicates only that the cancellation intent was accepted and enqueued — it does NOT confirm + * that the request was actually cancelled. The terminal outcome may still be "landed" or "error" if a race was lost. + * Errors are returned as standard status codes (e.g. InvalidArgument for an empty sqid). + * + * @generated from message uber.submitqueue.gateway.CancelResponse + */ +export type CancelResponse = Message<"uber.submitqueue.gateway.CancelResponse"> & { +}; + +/** + * Describes the message uber.submitqueue.gateway.CancelResponse. + * Use `create(CancelResponseSchema)` to create a new message. + */ +export const CancelResponseSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_api_submitqueue_gateway_proto_gateway, 5); + +/** + * RequestSummary is the gateway-owned materialized current view of one received request. + * + * @generated from message uber.submitqueue.gateway.RequestSummary + */ +export type RequestSummary = Message<"uber.submitqueue.gateway.RequestSummary"> & { + /** + * Globally unique request identifier. This is request_id in internal storage. + * + * @generated from field: string sqid = 1; + */ + sqid: string; + + /** + * Queue supplied when the request was received. + * + * @generated from field: string queue = 2; + */ + queue: string; + + /** + * Change URIs supplied when the request was received, in caller order. + * + * @generated from field: repeated string change_uris = 3; + */ + changeUris: string[]; + + /** + * Receipt time in Unix milliseconds. + * + * @generated from field: int64 received_at_ms = 4; + */ + receivedAtMs: bigint; + + /** + * Current customer-friendly status of the request. + * + * @generated from field: string status = 5; + */ + status: string; + + /** + * Last error associated with the current status. Empty when absent. + * + * @generated from field: string last_error = 6; + */ + lastError: string; + + /** + * Display and debugging metadata associated with the current status. + * + * @generated from field: map metadata = 7; + */ + metadata: { [key: string]: string }; +}; + +/** + * Describes the message uber.submitqueue.gateway.RequestSummary. + * Use `create(RequestSummarySchema)` to create a new message. + */ +export const RequestSummarySchema: GenMessage = /*@__PURE__*/ + messageDesc(file_api_submitqueue_gateway_proto_gateway, 6); + +/** + * GetRequestSummaryByIDRequest selects one request by the sqid returned from Land. + * + * @generated from message uber.submitqueue.gateway.GetRequestSummaryByIDRequest + */ +export type GetRequestSummaryByIDRequest = Message<"uber.submitqueue.gateway.GetRequestSummaryByIDRequest"> & { + /** + * Globally unique identifier for the request. + * + * @generated from field: string sqid = 1; + */ + sqid: string; + + /** + * Name of the queue processing the request. Required. A sqid is only resolvable within its own queue, + * so naming a queue the request does not belong to is reported as not found. + * + * @generated from field: string queue = 2; + */ + queue: string; +}; + +/** + * Describes the message uber.submitqueue.gateway.GetRequestSummaryByIDRequest. + * Use `create(GetRequestSummaryByIDRequestSchema)` to create a new message. + */ +export const GetRequestSummaryByIDRequestSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_api_submitqueue_gateway_proto_gateway, 7); + +/** + * GetRequestSummaryByIDResponse contains the current materialized request view. + * + * @generated from message uber.submitqueue.gateway.GetRequestSummaryByIDResponse + */ +export type GetRequestSummaryByIDResponse = Message<"uber.submitqueue.gateway.GetRequestSummaryByIDResponse"> & { + /** + * Matching request. + * + * @generated from field: uber.submitqueue.gateway.RequestSummary request = 1; + */ + request?: RequestSummary | undefined; +}; + +/** + * Describes the message uber.submitqueue.gateway.GetRequestSummaryByIDResponse. + * Use `create(GetRequestSummaryByIDResponseSchema)` to create a new message. + */ +export const GetRequestSummaryByIDResponseSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_api_submitqueue_gateway_proto_gateway, 8); + +/** + * GetRequestSummaryByChangeURIRequest selects requests by an exact pinned change URI. + * + * @generated from message uber.submitqueue.gateway.GetRequestSummaryByChangeURIRequest + */ +export type GetRequestSummaryByChangeURIRequest = Message<"uber.submitqueue.gateway.GetRequestSummaryByChangeURIRequest"> & { + /** + * Exact change URI supplied in a Land request. + * + * @generated from field: string change_uri = 1; + */ + changeUri: string; + + /** + * Name of the queue to search. Required. Results are scoped to this queue: a change URI landed + * into several queues is looked up one queue at a time. + * + * @generated from field: string queue = 2; + */ + queue: string; +}; + +/** + * Describes the message uber.submitqueue.gateway.GetRequestSummaryByChangeURIRequest. + * Use `create(GetRequestSummaryByChangeURIRequestSchema)` to create a new message. + */ +export const GetRequestSummaryByChangeURIRequestSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_api_submitqueue_gateway_proto_gateway, 9); + +/** + * GetRequestSummaryByChangeURIResponse contains matching requests newest first. + * + * @generated from message uber.submitqueue.gateway.GetRequestSummaryByChangeURIResponse + */ +export type GetRequestSummaryByChangeURIResponse = Message<"uber.submitqueue.gateway.GetRequestSummaryByChangeURIResponse"> & { + /** + * Matching requests ordered by receipt time descending, then sqid descending. + * + * @generated from field: repeated uber.submitqueue.gateway.RequestSummary requests = 1; + */ + requests: RequestSummary[]; +}; + +/** + * Describes the message uber.submitqueue.gateway.GetRequestSummaryByChangeURIResponse. + * Use `create(GetRequestSummaryByChangeURIResponseSchema)` to create a new message. + */ +export const GetRequestSummaryByChangeURIResponseSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_api_submitqueue_gateway_proto_gateway, 10); + +/** + * ListRequest defines one bounded queue receipt-history query. + * + * @generated from message uber.submitqueue.gateway.ListRequest + */ +export type ListRequest = Message<"uber.submitqueue.gateway.ListRequest"> & { + /** + * Exact queue to query. + * + * @generated from field: string queue = 1; + */ + queue: string; + + /** + * Inclusive lower receipt-time bound in Unix milliseconds. + * + * @generated from field: int64 received_at_or_after_ms = 2; + */ + receivedAtOrAfterMs: bigint; + + /** + * Exclusive upper receipt-time bound in Unix milliseconds. + * + * @generated from field: int64 received_before_ms = 3; + */ + receivedBeforeMs: bigint; + + /** + * Maximum results to return. Zero selects the server default; values above the server maximum are invalid. + * + * @generated from field: int32 page_size = 4; + */ + pageSize: number; + + /** + * Opaque continuation token from a previous response. Empty selects the first page. + * + * @generated from field: string page_token = 5; + */ + pageToken: string; +}; + +/** + * Describes the message uber.submitqueue.gateway.ListRequest. + * Use `create(ListRequestSchema)` to create a new message. + */ +export const ListRequestSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_api_submitqueue_gateway_proto_gateway, 11); + +/** + * ListResponse defines one page of queue receipt history. + * + * @generated from message uber.submitqueue.gateway.ListResponse + */ +export type ListResponse = Message<"uber.submitqueue.gateway.ListResponse"> & { + /** + * Requests ordered by receipt time descending, then sqid descending. + * + * @generated from field: repeated uber.submitqueue.gateway.RequestSummary requests = 1; + */ + requests: RequestSummary[]; + + /** + * Opaque continuation token. Empty when this is the last page. + * + * @generated from field: string next_page_token = 2; + */ + nextPageToken: string; +}; + +/** + * Describes the message uber.submitqueue.gateway.ListResponse. + * Use `create(ListResponseSchema)` to create a new message. + */ +export const ListResponseSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_api_submitqueue_gateway_proto_gateway, 12); + +/** + * GetRequestHistoryByIDRequest selects one retained request history by sqid. + * + * @generated from message uber.submitqueue.gateway.GetRequestHistoryByIDRequest + */ +export type GetRequestHistoryByIDRequest = Message<"uber.submitqueue.gateway.GetRequestHistoryByIDRequest"> & { + /** + * Globally unique identifier for the request. + * + * @generated from field: string sqid = 1; + */ + sqid: string; + + /** + * Name of the queue processing the request. Required. A sqid is only resolvable within its own queue, + * so naming a queue the request does not belong to is reported as not found. + * + * @generated from field: string queue = 2; + */ + queue: string; +}; + +/** + * Describes the message uber.submitqueue.gateway.GetRequestHistoryByIDRequest. + * Use `create(GetRequestHistoryByIDRequestSchema)` to create a new message. + */ +export const GetRequestHistoryByIDRequestSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_api_submitqueue_gateway_proto_gateway, 13); + +/** + * HistoryEvent is one retained append-only request-log event. + * + * @generated from message uber.submitqueue.gateway.HistoryEvent + */ +export type HistoryEvent = Message<"uber.submitqueue.gateway.HistoryEvent"> & { + /** + * Time the request-log entry was created, in Unix milliseconds. + * + * @generated from field: int64 timestamp_ms = 1; + */ + timestampMs: bigint; + + /** + * Customer-friendly request status recorded by the event. Set only when type is "status". + * + * @generated from field: string status = 2; + */ + status: string; + + /** + * Error message associated with the event. Empty when absent. + * + * @generated from field: string last_error = 3; + */ + lastError: string; + + /** + * Display and debugging metadata associated with this event. Each lifecycle event carries its own values. + * + * @generated from field: map metadata = 4; + */ + metadata: { [key: string]: string }; + + /** + * What this entry records: "status" when the request reached a position in the pipeline, + * "event" when something happened while it sat at one. Exactly one of status and event is set. + * + * @generated from field: string type = 5; + */ + type: string; + + /** + * Occurrence recorded by the entry, e.g. a build starting or finishing. Set only when type is "event". + * A request records many of these — one per build — and they never change its current status. + * + * @generated from field: string event = 6; + */ + event: string; +}; + +/** + * Describes the message uber.submitqueue.gateway.HistoryEvent. + * Use `create(HistoryEventSchema)` to create a new message. + */ +export const HistoryEventSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_api_submitqueue_gateway_proto_gateway, 14); + +/** + * GetRequestHistoryByIDResponse contains all retained events for one request. + * + * @generated from message uber.submitqueue.gateway.GetRequestHistoryByIDResponse + */ +export type GetRequestHistoryByIDResponse = Message<"uber.submitqueue.gateway.GetRequestHistoryByIDResponse"> & { + /** + * Events ordered by timestamp ascending with a stable storage tie-breaker. + * + * @generated from field: repeated uber.submitqueue.gateway.HistoryEvent events = 1; + */ + events: HistoryEvent[]; +}; + +/** + * Describes the message uber.submitqueue.gateway.GetRequestHistoryByIDResponse. + * Use `create(GetRequestHistoryByIDResponseSchema)` to create a new message. + */ +export const GetRequestHistoryByIDResponseSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_api_submitqueue_gateway_proto_gateway, 15); + +/** + * GetRequestHistoryByChangeURIRequest selects retained histories by an exact pinned change URI. + * + * @generated from message uber.submitqueue.gateway.GetRequestHistoryByChangeURIRequest + */ +export type GetRequestHistoryByChangeURIRequest = Message<"uber.submitqueue.gateway.GetRequestHistoryByChangeURIRequest"> & { + /** + * Exact change URI supplied in a Land request. + * + * @generated from field: string change_uri = 1; + */ + changeUri: string; + + /** + * Name of the queue to search. Required. Results are scoped to this queue: a change URI landed + * into several queues is looked up one queue at a time. + * + * @generated from field: string queue = 2; + */ + queue: string; +}; + +/** + * Describes the message uber.submitqueue.gateway.GetRequestHistoryByChangeURIRequest. + * Use `create(GetRequestHistoryByChangeURIRequestSchema)` to create a new message. + */ +export const GetRequestHistoryByChangeURIRequestSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_api_submitqueue_gateway_proto_gateway, 16); + +/** + * RequestHistory groups retained events for one request. + * + * @generated from message uber.submitqueue.gateway.RequestHistory + */ +export type RequestHistory = Message<"uber.submitqueue.gateway.RequestHistory"> & { + /** + * Globally unique request identifier. + * + * @generated from field: string sqid = 1; + */ + sqid: string; + + /** + * Retained events ordered chronologically. + * + * @generated from field: repeated uber.submitqueue.gateway.HistoryEvent events = 2; + */ + events: HistoryEvent[]; +}; + +/** + * Describes the message uber.submitqueue.gateway.RequestHistory. + * Use `create(RequestHistorySchema)` to create a new message. + */ +export const RequestHistorySchema: GenMessage = /*@__PURE__*/ + messageDesc(file_api_submitqueue_gateway_proto_gateway, 17); + +/** + * GetRequestHistoryByChangeURIResponse contains retained histories ordered by numeric sqid counter ascending. + * + * @generated from message uber.submitqueue.gateway.GetRequestHistoryByChangeURIResponse + */ +export type GetRequestHistoryByChangeURIResponse = Message<"uber.submitqueue.gateway.GetRequestHistoryByChangeURIResponse"> & { + /** + * Matching request histories. + * + * @generated from field: repeated uber.submitqueue.gateway.RequestHistory histories = 1; + */ + histories: RequestHistory[]; +}; + +/** + * Describes the message uber.submitqueue.gateway.GetRequestHistoryByChangeURIResponse. + * Use `create(GetRequestHistoryByChangeURIResponseSchema)` to create a new message. + */ +export const GetRequestHistoryByChangeURIResponseSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_api_submitqueue_gateway_proto_gateway, 18); + +/** + * Generic error with metadata. Each custom error type should extend this message. + * + * @generated from message uber.submitqueue.gateway.Error + */ +export type Error = Message<"uber.submitqueue.gateway.Error"> & { + /** + * Free text error message describing the error. + * + * @generated from field: string message = 1; + */ + message: string; +}; + +/** + * Describes the message uber.submitqueue.gateway.Error. + * Use `create(ErrorSchema)` to create a new message. + */ +export const ErrorSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_api_submitqueue_gateway_proto_gateway, 19); + +/** + * UnrecognizedQueueError is returned when a queue name is not recognized. Typically this indicates a typo in the queue name or server misconfiguration. + * + * @generated from message uber.submitqueue.gateway.UnrecognizedQueueError + */ +export type UnrecognizedQueueError = Message<"uber.submitqueue.gateway.UnrecognizedQueueError"> & { + /** + * Free text error message describing the error. + * + * @generated from field: uber.submitqueue.gateway.Error error = 1; + */ + error?: Error | undefined; + + /** + * Name of the queue that was not recognized. + * + * @generated from field: string queue = 2; + */ + queue: string; +}; + +/** + * Describes the message uber.submitqueue.gateway.UnrecognizedQueueError. + * Use `create(UnrecognizedQueueErrorSchema)` to create a new message. + */ +export const UnrecognizedQueueErrorSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_api_submitqueue_gateway_proto_gateway, 20); + +/** + * RequestNotFoundError is returned when no request is found for the given sqid. Typically this indicates a typo in the sqid or that the request has not been accepted yet. + * + * @generated from message uber.submitqueue.gateway.RequestNotFoundError + */ +export type RequestNotFoundError = Message<"uber.submitqueue.gateway.RequestNotFoundError"> & { + /** + * Free text error message describing the error. + * + * @generated from field: uber.submitqueue.gateway.Error error = 1; + */ + error?: Error | undefined; + + /** + * The sqid that was not found. + * + * @generated from field: string sqid = 2; + */ + sqid: string; + + /** + * Exact change URI that was not found. Populated only for change-URI lookup. + * + * @generated from field: string change_uri = 3; + */ + changeUri: string; +}; + +/** + * Describes the message uber.submitqueue.gateway.RequestNotFoundError. + * Use `create(RequestNotFoundErrorSchema)` to create a new message. + */ +export const RequestNotFoundErrorSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_api_submitqueue_gateway_proto_gateway, 21); + +/** + * SubmitQueueGateway provides the gateway API + * + * @generated from service uber.submitqueue.gateway.SubmitQueueGateway + */ +export const SubmitQueueGateway: GenService<{ + /** + * Ping returns a response indicating the service is alive + * + * @generated from rpc uber.submitqueue.gateway.SubmitQueueGateway.Ping + */ + ping: { + methodKind: "unary"; + input: typeof PingRequestSchema; + output: typeof PingResponseSchema; + }, + /** + * Land lands a set of code changes into a target branch, performing the necessary validations across all other changes in the queue. + * The processing is asynchronous and returns a LandResponse immediately. The land request is processed in the background. + * + * @generated from rpc uber.submitqueue.gateway.SubmitQueueGateway.Land + */ + land: { + methodKind: "unary"; + input: typeof LandRequestSchema; + output: typeof LandResponseSchema; + }, + /** + * Cancel cancels an in-flight land request identified by its sqid. If the request has already been included in a batch, + * the entire batch is cancelled. Cancel is idempotent — calling it for an already-terminal request is a no-op. + * + * Cancellation is ASYNCHRONOUS: Cancel returns as soon as the cancellation intent has been accepted; the actual + * state transition is performed in the background by the orchestrator and may not have completed by the time the + * caller receives a response. + * + * Cancellation is NOT GUARANTEED: a request that has already landed, or that races to completion before the cancel + * signal propagates through the pipeline, may still land (or end in an error). Callers must NOT assume that a + * successful Cancel response means the request was cancelled — the actual terminal outcome (cancelled, landed, or + * error) must be checked through the request-summary or request-history APIs. + * + * @generated from rpc uber.submitqueue.gateway.SubmitQueueGateway.Cancel + */ + cancel: { + methodKind: "unary"; + input: typeof CancelRequestSchema; + output: typeof CancelResponseSchema; + }, + /** + * GetRequestSummaryByID returns the current materialized status of one request. + * + * @generated from rpc uber.submitqueue.gateway.SubmitQueueGateway.GetRequestSummaryByID + */ + getRequestSummaryByID: { + methodKind: "unary"; + input: typeof GetRequestSummaryByIDRequestSchema; + output: typeof GetRequestSummaryByIDResponseSchema; + }, + /** + * GetRequestSummaryByChangeURI returns current materialized statuses for an exact pinned change URI. + * + * @generated from rpc uber.submitqueue.gateway.SubmitQueueGateway.GetRequestSummaryByChangeURI + */ + getRequestSummaryByChangeURI: { + methodKind: "unary"; + input: typeof GetRequestSummaryByChangeURIRequestSchema; + output: typeof GetRequestSummaryByChangeURIResponseSchema; + }, + /** + * List returns requests received for one queue during a bounded receipt-time range. + * + * @generated from rpc uber.submitqueue.gateway.SubmitQueueGateway.List + */ + list: { + methodKind: "unary"; + input: typeof ListRequestSchema; + output: typeof ListResponseSchema; + }, + /** + * GetRequestHistoryByID returns every retained request-log event for one request. + * + * @generated from rpc uber.submitqueue.gateway.SubmitQueueGateway.GetRequestHistoryByID + */ + getRequestHistoryByID: { + methodKind: "unary"; + input: typeof GetRequestHistoryByIDRequestSchema; + output: typeof GetRequestHistoryByIDResponseSchema; + }, + /** + * GetRequestHistoryByChangeURI returns retained request histories for an exact pinned change URI. + * + * @generated from rpc uber.submitqueue.gateway.SubmitQueueGateway.GetRequestHistoryByChangeURI + */ + getRequestHistoryByChangeURI: { + methodKind: "unary"; + input: typeof GetRequestHistoryByChangeURIRequestSchema; + output: typeof GetRequestHistoryByChangeURIResponseSchema; + }, +}> = /*@__PURE__*/ + serviceDesc(file_api_submitqueue_gateway_proto_gateway, 0); + diff --git a/web/api/tsconfig.build.json b/web/api/tsconfig.build.json new file mode 100644 index 000000000..6ec5be4c9 --- /dev/null +++ b/web/api/tsconfig.build.json @@ -0,0 +1,11 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "declaration": true, + "noEmit": false, + "outDir": "dist", + "rootDir": "src/gen", + "sourceMap": true + }, + "exclude": ["src/**/*.test.ts"] +} diff --git a/web/api/tsconfig.json b/web/api/tsconfig.json new file mode 100644 index 000000000..d26062d30 --- /dev/null +++ b/web/api/tsconfig.json @@ -0,0 +1,7 @@ +{ + "extends": "../tsconfig.base.json", + "compilerOptions": { + "rootDir": "src" + }, + "include": ["src/**/*.ts"] +} diff --git a/web/buf.gen.yaml b/web/buf.gen.yaml new file mode 100644 index 000000000..bbfd7a12e --- /dev/null +++ b/web/buf.gen.yaml @@ -0,0 +1,8 @@ +version: v2 +clean: true +plugins: + - local: protoc-gen-es + out: api/src/gen + opt: + - target=ts + - import_extension=js diff --git a/web/e2e/submitqueue.spec.ts b/web/e2e/submitqueue.spec.ts new file mode 100644 index 000000000..fa7fca75d --- /dev/null +++ b/web/e2e/submitqueue.spec.ts @@ -0,0 +1,103 @@ +import AxeBuilder from "@axe-core/playwright"; +import { createClient } from "@connectrpc/connect"; +import { createGrpcTransport } from "@connectrpc/connect-node"; +import { expect, request as playwrightRequest, test } from "@playwright/test"; + +import { Strategy } from "../api/src/gen/api/base/mergestrategy/proto/mergestrategy_pb.js"; +import { SubmitQueueGateway } from "../api/src/gen/api/submitqueue/gateway/proto/gateway_pb.js"; + +const queue = "demo-queue"; +const gatewayURL = process.env.SUBMITQUEUE_E2E_GATEWAY_URL as string; +const webURL = process.env.SUBMITQUEUE_E2E_WEB_URL as string; + +const gateway = createClient( + SubmitQueueGateway, + createGrpcTransport({ baseUrl: gatewayURL }), +); + +async function submitRequest(uri: string): Promise { + const response = await gateway.land({ + queue, + change: { uris: [uri] }, + strategy: Strategy.SQUASH_REBASE, + }); + expect(response.sqid).not.toBe(""); + await expect + .poll( + async () => { + const summary = await gateway.getRequestSummaryByID({ + queue, + sqid: response.sqid, + }); + return summary.request?.status; + }, + { timeout: 0 }, + ) + .toBe("landed"); + return response.sqid; +} + +function expectOrderedHistory(values: readonly string[], expected: readonly string[]): void { + let position = 0; + for (const value of values) { + if (value.includes(expected[position] ?? "\u0000")) { + position += 1; + } + } + expect(position).toBe(expected.length); +} + +test("challenges anonymous requests", async () => { + const anonymous = await playwrightRequest.newContext({ baseURL: webURL }); + try { + const response = await anonymous.get("/requests"); + expect(response.status()).toBe(401); + expect(response.headers()["www-authenticate"]).toContain("Basic"); + } finally { + await anonymous.dispose(); + } +}); + +test("shows newest requests and an accessible encoded detail history", async ({ page }) => { + const firstSqid = await submitRequest( + "git://git.example.com/demo/refs%2Fheads%2Fweb-first/1111111111111111111111111111111111111111?sq-files=web/first.txt", + ); + const secondSqid = await submitRequest( + "git://git.example.com/demo/refs%2Fheads%2Fweb-second/2222222222222222222222222222222222222222?sq-files=web/second.txt", + ); + + await page.goto("/requests"); + const requestLinks = page.locator(".sq-request-list__item a"); + await expect(requestLinks).toHaveCount(2, { timeout: 0 }); + await expect(requestLinks.nth(0)).toHaveText(secondSqid); + await expect(requestLinks.nth(1)).toHaveText(firstSqid); + + const listAccessibility = await new AxeBuilder({ page }).analyze(); + expect(listAccessibility.violations).toEqual([]); + + await requestLinks.nth(0).click(); + await expect(page.getByRole("heading", { name: "Request status" })).toBeVisible(); + await expect(page.getByText(secondSqid, { exact: true })).toBeVisible(); + await expect(page.locator(".sq-request-detail .sq-status").first()).toHaveText("Landed"); + + const encodedSegment = new URL(page.url()).pathname.split("/").at(-1); + expect(encodedSegment).toMatch(/^[A-Za-z0-9_-]+$/u); + expect(encodedSegment).not.toContain("/"); + + const history = await page.locator(".sq-history__item").allTextContents(); + expectOrderedHistory(history, [ + "Accepted", + "Started", + "Validating", + "Batched", + "Speculating", + "Building", + "Built", + "Speculated", + "Landing", + "Landed", + ]); + + const detailAccessibility = await new AxeBuilder({ page }).analyze(); + expect(detailAccessibility.violations).toEqual([]); +}); diff --git a/web/go.mod b/web/go.mod new file mode 100644 index 000000000..47cc235d9 --- /dev/null +++ b/web/go.mod @@ -0,0 +1,3 @@ +module github.com/uber/submitqueue/web + +go 1.25.0 diff --git a/web/package.json b/web/package.json new file mode 100644 index 000000000..350c327cc --- /dev/null +++ b/web/package.json @@ -0,0 +1,33 @@ +{ + "name": "submitqueue-web-workspace", + "private": true, + "version": "0.0.0", + "packageManager": "pnpm@10.17.1", + "engines": { + "node": ">=22.20.0 <23", + "pnpm": ">=10.17.1 <11" + }, + "scripts": { + "api:generate": "node ./tool/generate-api.mjs", + "api:check": "node ./tool/generate-api.mjs --check", + "build": "pnpm --recursive --if-present run build", + "check": "pnpm api:check && pnpm build && pnpm package:check && pnpm lint && pnpm typecheck && pnpm test", + "e2e": "playwright test", + "lint": "pnpm --recursive --if-present run lint", + "package:check": "node ./tool/check-packages.mjs", + "test": "pnpm --recursive --if-present run test", + "typecheck": "tsc --project tsconfig.e2e.json && pnpm --recursive --if-present run typecheck" + }, + "devDependencies": { + "@axe-core/playwright": "4.10.2", + "@bufbuild/buf": "1.73.0", + "@bufbuild/protoc-gen-es": "2.15.0", + "@connectrpc/connect": "2.1.1", + "@connectrpc/connect-node": "2.1.1", + "@playwright/test": "1.55.1", + "@submitqueue/api": "workspace:*", + "@types/node": "22.18.6", + "typescript": "5.9.3", + "vitest": "4.0.7" + } +} diff --git a/web/playwright.config.ts b/web/playwright.config.ts new file mode 100644 index 000000000..67c105ce6 --- /dev/null +++ b/web/playwright.config.ts @@ -0,0 +1,26 @@ +import { defineConfig } from "@playwright/test"; + +const baseURL = process.env.SUBMITQUEUE_E2E_WEB_URL; +const gatewayURL = process.env.SUBMITQUEUE_E2E_GATEWAY_URL; +const token = process.env.SUBMITQUEUE_WEB_TOKEN; + +if (!baseURL || !gatewayURL || !token) { + throw new Error( + "SUBMITQUEUE_E2E_WEB_URL, SUBMITQUEUE_E2E_GATEWAY_URL, and SUBMITQUEUE_WEB_TOKEN are required", + ); +} + +export default defineConfig({ + testDir: "./e2e", + fullyParallel: false, + workers: 1, + reporter: "line", + use: { + baseURL, + httpCredentials: { + username: "submitqueue", + password: token, + }, + trace: "retain-on-failure", + }, +}); diff --git a/web/pnpm-lock.yaml b/web/pnpm-lock.yaml new file mode 100644 index 000000000..b124d96a3 --- /dev/null +++ b/web/pnpm-lock.yaml @@ -0,0 +1,5968 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + devDependencies: + '@axe-core/playwright': + specifier: 4.10.2 + version: 4.10.2(playwright-core@1.55.1) + '@bufbuild/buf': + specifier: 1.73.0 + version: 1.73.0 + '@bufbuild/protoc-gen-es': + specifier: 2.15.0 + version: 2.15.0(@bufbuild/protobuf@2.15.0) + '@connectrpc/connect': + specifier: 2.1.1 + version: 2.1.1(@bufbuild/protobuf@2.15.0) + '@connectrpc/connect-node': + specifier: 2.1.1 + version: 2.1.1(@bufbuild/protobuf@2.15.0)(@connectrpc/connect@2.1.1(@bufbuild/protobuf@2.15.0)) + '@playwright/test': + specifier: 1.55.1 + version: 1.55.1 + '@submitqueue/api': + specifier: workspace:* + version: link:api + '@types/node': + specifier: 22.18.6 + version: 22.18.6 + typescript: + specifier: 5.9.3 + version: 5.9.3 + vitest: + specifier: 4.0.7 + version: 4.0.7(@types/node@22.18.6)(jsdom@27.4.0) + + api: + dependencies: + '@bufbuild/protobuf': + specifier: 2.15.0 + version: 2.15.0 + + service/submitqueue: + dependencies: + '@bufbuild/protobuf': + specifier: 2.15.0 + version: 2.15.0 + '@connectrpc/connect': + specifier: ^2.1.1 + version: 2.2.0(@bufbuild/protobuf@2.15.0) + '@connectrpc/connect-node': + specifier: ^2.1.1 + version: 2.2.0(@bufbuild/protobuf@2.15.0)(@connectrpc/connect@2.2.0(@bufbuild/protobuf@2.15.0)) + '@submitqueue/api': + specifier: workspace:* + version: link:../../api + '@submitqueue/web-submitqueue': + specifier: workspace:* + version: link:../../submitqueue + next: + specifier: '>=16.3.0 <17' + version: 16.3.6(@babel/core@7.29.7)(@playwright/test@1.55.1)(@types/node@22.18.6)(react-dom@19.3.0(react@19.3.0))(react@19.3.0) + react: + specifier: ^19.2.0 + version: 19.3.0 + react-dom: + specifier: ^19.2.0 + version: 19.3.0(react@19.3.0) + server-only: + specifier: ^0.0.1 + version: 0.0.1 + devDependencies: + '@types/node': + specifier: 22.18.6 + version: 22.18.6 + '@types/react': + specifier: ^19.1.16 + version: 19.3.0 + '@types/react-dom': + specifier: ^19.1.9 + version: 19.3.0(@types/react@19.3.0) + eslint: + specifier: ^9.36.0 + version: 9.39.5 + eslint-config-next: + specifier: '>=16.3.0 <17' + version: 16.3.6(@typescript-eslint/parser@8.71.0(eslint@9.39.5)(typescript@5.9.3))(eslint@9.39.5)(typescript@5.9.3) + typescript: + specifier: 5.9.3 + version: 5.9.3 + vitest: + specifier: 4.0.7 + version: 4.0.7(@types/node@22.18.6)(jsdom@27.4.0) + + submitqueue: + dependencies: + '@bufbuild/protobuf': + specifier: '>=2 <3' + version: 2.15.0 + '@submitqueue/api': + specifier: workspace:* + version: link:../api + devDependencies: + '@connectrpc/connect': + specifier: ^2.1.1 + version: 2.2.0(@bufbuild/protobuf@2.15.0) + '@testing-library/jest-dom': + specifier: ^6.6.3 + version: 6.10.0(@testing-library/dom@10.4.2) + '@testing-library/react': + specifier: ^16.3.0 + version: 16.3.3(@testing-library/dom@10.4.2)(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0) + '@types/react': + specifier: ^19.1.0 + version: 19.3.0 + '@types/react-dom': + specifier: ^19.1.0 + version: 19.3.0(@types/react@19.3.0) + jsdom: + specifier: ^26.1.0 + version: 26.1.0 + next: + specifier: ^16.3.0 + version: 16.3.6(@babel/core@7.29.7)(@playwright/test@1.55.1)(@types/node@22.18.6)(react-dom@19.3.0(react@19.3.0))(react@19.3.0) + react: + specifier: ^19.1.0 + version: 19.3.0 + react-dom: + specifier: ^19.1.0 + version: 19.3.0(react@19.3.0) + typescript: + specifier: ^5.8.3 + version: 5.9.3 + vitest: + specifier: ^3.1.2 + version: 3.2.7(@types/node@22.18.6)(jsdom@26.1.0) + +packages: + + '@acemir/cssom@0.9.31': + resolution: {integrity: sha512-ZnR3GSaH+/vJ0YlHau21FjfLYjMpYVIzTD8M8vIEQvIGxeOXyXdzCI140rrCY862p/C/BbzWsjc1dgnM9mkoTA==} + + '@adobe/css-tools@4.5.0': + resolution: {integrity: sha512-6OzddxPio9UiWTCemp4N8cYLV2ZN1ncRnV1cVGtve7dhPOtRkleRyx32GQCYSwDYgaHU3USMm84tNsvKzRCa1Q==} + + '@asamuzakjp/css-color@3.2.0': + resolution: {integrity: sha512-K1A6z8tS3XsmCMM86xoWdn7Fkdn9m6RSVtocUrJYIwZnFVkng/PvkEoWtOWmP+Scc6saYWHWZYbndEEXxl24jw==} + + '@asamuzakjp/css-color@4.1.2': + resolution: {integrity: sha512-NfBUvBaYgKIuq6E/RBLY1m0IohzNHAYyaJGuTK79Z23uNwmz2jl1mPsC5ZxCCxylinKhT1Amn5oNTlx1wN8cQg==} + + '@asamuzakjp/dom-selector@6.8.1': + resolution: {integrity: sha512-MvRz1nCqW0fsy8Qz4dnLIvhOlMzqDVBabZx6lH+YywFDdjXhMY37SmpV1XFX3JzG5GWHn63j6HX6QPr3lZXHvQ==} + + '@asamuzakjp/nwsapi@2.3.9': + resolution: {integrity: sha512-n8GuYSrI9bF7FFZ/SjhwevlHc8xaVlb/7HmHelnc/PZXBD2ZR49NnN9sMMuDdEGPeeRQ5d0hqlSlEpgCX3Wl0Q==} + + '@axe-core/playwright@4.10.2': + resolution: {integrity: sha512-6/b5BJjG6hDaRNtgzLIfKr5DfwyiLHO4+ByTLB0cJgWSM8Ll7KqtdblIS6bEkwSF642/Ex91vNqIl3GLXGlceg==} + peerDependencies: + playwright-core: '>= 1.0.0' + + '@babel/code-frame@7.29.7': + resolution: {integrity: sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==} + engines: {node: '>=6.9.0'} + + '@babel/compat-data@7.29.7': + resolution: {integrity: sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==} + engines: {node: '>=6.9.0'} + + '@babel/core@7.29.7': + resolution: {integrity: sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==} + engines: {node: '>=6.9.0'} + + '@babel/generator@7.29.8': + resolution: {integrity: sha512-gZbepsdh3WDtgZKWL+vTPh71LSBrm/Y4/QDZBVCcYfmeTEEuoOYwlSy+G1StfJg+/Zy550u/3TATbm7qDbbMtg==} + engines: {node: '>=6.9.0'} + + '@babel/helper-compilation-targets@7.29.7': + resolution: {integrity: sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==} + engines: {node: '>=6.9.0'} + + '@babel/helper-globals@7.29.7': + resolution: {integrity: sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-imports@7.29.7': + resolution: {integrity: sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-transforms@7.29.7': + resolution: {integrity: sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/helper-string-parser@7.29.7': + resolution: {integrity: sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-identifier@7.29.7': + resolution: {integrity: sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-option@7.29.7': + resolution: {integrity: sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==} + engines: {node: '>=6.9.0'} + + '@babel/helpers@7.29.7': + resolution: {integrity: sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==} + engines: {node: '>=6.9.0'} + + '@babel/parser@7.29.9': + resolution: {integrity: sha512-CjXrNHTnvqBVqHgdBysY3vk2T8tpJHb5/RMeHJBTyVa9xgugCB0CJTx/3oO8RV2QRQP391RWpB7D6hLjm8V9uA==} + engines: {node: '>=6.0.0'} + hasBin: true + + '@babel/runtime@7.29.7': + resolution: {integrity: sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==} + engines: {node: '>=6.9.0'} + + '@babel/template@7.29.7': + resolution: {integrity: sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==} + engines: {node: '>=6.9.0'} + + '@babel/traverse@7.29.8': + resolution: {integrity: sha512-I5z7H3bf/41ktsNVLtpN0wAa336HkqIHQ5BuPLEhTkt1jVSyZpeNKIzTgEWmlxjdg81R0IgUCcaE+Ok3NvrfZg==} + engines: {node: '>=6.9.0'} + + '@babel/types@7.29.8': + resolution: {integrity: sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg==} + engines: {node: '>=6.9.0'} + + '@bufbuild/buf-darwin-arm64@1.73.0': + resolution: {integrity: sha512-cDd3y376ecClhyytNLlY3AtsZqsqXvEkQjMvrahrv/xUWWhEIX3efD8y0uufXDe8Yuk3sEmj7J1Xlx73Y46SkA==} + engines: {node: '>=12'} + cpu: [arm64] + os: [darwin] + + '@bufbuild/buf-darwin-x64@1.73.0': + resolution: {integrity: sha512-VTBTCwaqT7otsMGXlk3MRu6x5hp+YH9PaLKcyWAa8wEt0NnMzbXdkE8GiLgI8Qkl7S3+pC9YSTCE/I+O87Fvrw==} + engines: {node: '>=12'} + cpu: [x64] + os: [darwin] + + '@bufbuild/buf-linux-aarch64@1.73.0': + resolution: {integrity: sha512-HjoQ3wbLiUv5N5XLM3EAR2FY51Yn6t5MEXLPnS/Hx0KDkK/5eyIBZZ6toHBLjczAr1RTA1LagF38+lglZI+kvA==} + engines: {node: '>=12'} + cpu: [arm64] + os: [linux] + + '@bufbuild/buf-linux-armv7@1.73.0': + resolution: {integrity: sha512-NsZuVg8hXfGcR96Is14ArKxZg6pdJb6kb63bx2g7q8Cjo+gRqZMBFKGvF/fnQMxTrRQAm6hzeF4a2vV4W3lszg==} + engines: {node: '>=12'} + cpu: [arm] + os: [linux] + + '@bufbuild/buf-linux-x64@1.73.0': + resolution: {integrity: sha512-dMe6lBQrlHTU7DzqzaR7Udky7R78EDzzxtYZQdjkZun9jKdjGR4fLbPWEPyY4rLGVyk6UUnrtkf5qoIK2SC6qw==} + engines: {node: '>=12'} + cpu: [x64] + os: [linux] + + '@bufbuild/buf-win32-arm64@1.73.0': + resolution: {integrity: sha512-UmubCflc8zJ48bVW0WeGeVTkNuumJP//wCWKUmeTXT1iNlEqkpaXy6y7o8YJKHGWLfX0/1UV3oay9ti0EyojMw==} + engines: {node: '>=12'} + cpu: [arm64] + os: [win32] + + '@bufbuild/buf-win32-x64@1.73.0': + resolution: {integrity: sha512-/Buo10rXzqNLnFDh3BpzGdcrx6OalcU8o8gUyObCFZA5wKcUvBVNDhhpMmoB6YeiOPpPn47jHgW4bSSlypaaOQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [win32] + + '@bufbuild/buf@1.73.0': + resolution: {integrity: sha512-W3whb4P1y3Hs9bqwCu9++W1v1ofHZ3dBUCe6JSZq0Ff5OK5UKKZuMN4iunPbgdyXY4+4ujEYf2ESrN/6MbcFSQ==} + engines: {node: '>=12'} + hasBin: true + + '@bufbuild/protobuf@2.15.0': + resolution: {integrity: sha512-DAheWUkVr/SJTWCc+lg9dhY0eN4SaWlf4+bG1KzHeXbnqt0AfB/NX0Z+VunGlM1ki1B4zVvye27MpKh/svySUA==} + + '@bufbuild/protoc-gen-es@2.15.0': + resolution: {integrity: sha512-FRhNTAZEXW6arWx2JxgtpuazhmewYctXugu5SfXGpXpLLCInDT1005fSKDzEM/70Es2oIn07PM4RvpwvDvR/fg==} + engines: {node: '>=22'} + hasBin: true + peerDependencies: + '@bufbuild/protobuf': 2.15.0 + peerDependenciesMeta: + '@bufbuild/protobuf': + optional: true + + '@bufbuild/protoplugin@2.15.0': + resolution: {integrity: sha512-vZhRoqB/jTHATqItRvJQf1zbxdrGRRhiusGty4+lKcfEFODG0PMXky2Ezv4WHHS6oSxq+5qShITuE7K1/82SuQ==} + + '@connectrpc/connect-node@2.1.1': + resolution: {integrity: sha512-s3TfsI1XF+n+1z6MBS9rTnFsxxR4Rw5wmdEnkQINli81ESGxcsfaEet8duzq8LVuuCupmhUsgpRo0Nv9pZkufg==} + engines: {node: '>=20'} + peerDependencies: + '@bufbuild/protobuf': ^2.7.0 + '@connectrpc/connect': 2.1.1 + + '@connectrpc/connect-node@2.2.0': + resolution: {integrity: sha512-E6uUO6e11WdynAuBjuN+0WpHjqxEM9BNDdfDIJHcn49Esg/WxSIbn2y1KFt9XL96IJi01qdBbvubcdpBz1X3Zw==} + engines: {node: '>=22'} + peerDependencies: + '@bufbuild/protobuf': ^2.7.0 + '@connectrpc/connect': 2.2.0 + + '@connectrpc/connect@2.1.1': + resolution: {integrity: sha512-JzhkaTvM73m2K1URT6tv53k2RwngSmCXLZJgK580qNQOXRzZRR/BCMfZw3h+90JpnG6XksP5bYT+cz0rpUzUWQ==} + peerDependencies: + '@bufbuild/protobuf': ^2.7.0 + + '@connectrpc/connect@2.2.0': + resolution: {integrity: sha512-M63OQ1ZFcPUm85CuewJ9G9CMV8LjzGR9935xOnXNQGSazAbwMb3x2W7vXBYM1zqrP7XFD8uxp/dUAhirclgwnQ==} + peerDependencies: + '@bufbuild/protobuf': ^2.7.0 + + '@csstools/color-helpers@5.1.0': + resolution: {integrity: sha512-S11EXWJyy0Mz5SYvRmY8nJYTFFd1LCNV+7cXyAgQtOOuzb4EsgfqDufL+9esx72/eLhsRdGZwaldu/h+E4t4BA==} + engines: {node: '>=18'} + + '@csstools/color-helpers@6.1.2': + resolution: {integrity: sha512-grhRy3OKmniaAEKXMjua5z/EODX0MSqBGjunw8+j/3HQjOnahs2AGhvEOIYVUWcU6ScApbhLhVrQTX8XqrMrow==} + engines: {node: '>=20.19.0'} + + '@csstools/css-calc@2.1.4': + resolution: {integrity: sha512-3N8oaj+0juUw/1H3YwmDDJXCgTB1gKU6Hc/bB502u9zR0q2vd786XJH9QfrKIEgFlZmhZiq6epXl4rHqhzsIgQ==} + engines: {node: '>=18'} + peerDependencies: + '@csstools/css-parser-algorithms': ^3.0.5 + '@csstools/css-tokenizer': ^3.0.4 + + '@csstools/css-calc@3.4.1': + resolution: {integrity: sha512-EtC7SoN1j6J4E4DCwg5QgbO5TGxgxIA1RXqe+W+qUM+BUcezx9wT+/tiQ/WO2yCX4i5X+Cuf9ciJ22aP4UEwWw==} + engines: {node: '>=20.19.0'} + peerDependencies: + '@csstools/css-parser-algorithms': ^4.0.1 + '@csstools/css-tokenizer': ^4.0.2 + + '@csstools/css-color-parser@3.1.0': + resolution: {integrity: sha512-nbtKwh3a6xNVIp/VRuXV64yTKnb1IjTAEEh3irzS+HkKjAOYLTGNb9pmVNntZ8iVBHcWDA2Dof0QtPgFI1BaTA==} + engines: {node: '>=18'} + peerDependencies: + '@csstools/css-parser-algorithms': ^3.0.5 + '@csstools/css-tokenizer': ^3.0.4 + + '@csstools/css-color-parser@4.2.4': + resolution: {integrity: sha512-DyefytAZ735mX4Dq/WcDAXFtXhaEFvme0ZS9tVEBAc2whxUthXr0R0L2rmEPm59SrMBkGrFzQviBXMs/UtnABQ==} + engines: {node: '>=20.19.0'} + peerDependencies: + '@csstools/css-parser-algorithms': ^4.0.1 + '@csstools/css-tokenizer': ^4.0.2 + + '@csstools/css-parser-algorithms@3.0.5': + resolution: {integrity: sha512-DaDeUkXZKjdGhgYaHNJTV9pV7Y9B3b644jCLs9Upc3VeNGg6LWARAT6O+Q+/COo+2gg/bM5rhpMAtf70WqfBdQ==} + engines: {node: '>=18'} + peerDependencies: + '@csstools/css-tokenizer': ^3.0.4 + + '@csstools/css-parser-algorithms@4.0.1': + resolution: {integrity: sha512-ShL8BqPfbKJrJiKFH0xBbN0i7Nrh9HXYRuF+pzyj93R/BL2YAsUxJeqANErzqM+0JGl7vjHp+3OIgd/DL69YIA==} + engines: {node: '>=20.19.0'} + peerDependencies: + '@csstools/css-tokenizer': ^4.0.2 + + '@csstools/css-syntax-patches-for-csstree@1.1.14': + resolution: {integrity: sha512-HpbVXyrofRXpHpgkNIjU/3EWR4WJvOkO3emNK/L6X/mTJU7bGUI3AkkpoTNXznQLp0KRjLHELTGeKI5dIkI9JQ==} + peerDependencies: + css-tree: ^3.2.1 + peerDependenciesMeta: + css-tree: + optional: true + + '@csstools/css-tokenizer@3.0.4': + resolution: {integrity: sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==} + engines: {node: '>=18'} + + '@csstools/css-tokenizer@4.0.2': + resolution: {integrity: sha512-OoKoR0f76dCY666JlcbhmVTs2drYj1GUXZTYTcbUgJjh9Nv41aFfZ21bPQTERm5+L5cBDo466NltB2lplS5GBw==} + engines: {node: '>=20.19.0'} + + '@emnapi/core@1.10.0': + resolution: {integrity: sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==} + + '@emnapi/runtime@1.10.0': + resolution: {integrity: sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==} + + '@emnapi/runtime@1.11.3': + resolution: {integrity: sha512-Xz4Tpyki7XyrpbUK1jR1AhdAdaXyhhY4lZ3neLodmhpuWfy2PAQN5B46sAiU4liOXGLkHypn/qU+jvfWSCYYLA==} + + '@emnapi/wasi-threads@1.2.1': + resolution: {integrity: sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==} + + '@esbuild/aix-ppc64@0.28.2': + resolution: {integrity: sha512-XExcO+dvLKvVtNTibSTBej1NCAbaGhWn9Ww1ZPx80qsahhPFe/8jgWP0IchNe0F3HwkU7n8ejhH8bjonqht8mQ==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + + '@esbuild/android-arm64@0.28.2': + resolution: {integrity: sha512-5YfKeeI8qWfBZIX+u2xZC3Zlb3Os/gLS2sbEKM+I4ZOcsWmHS2WLysCcQZDAFRslDUU5Oiq44gf6PYN1vGwG5A==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm@0.28.2': + resolution: {integrity: sha512-kXXoiPVVGQcnIYGOeaovwOURpniDBpSq4A03qkQ+BMQqtGG6HYap3xne9C1O1yo4TR3qxlCX5IqqmX6fFo2Lqg==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + + '@esbuild/android-x64@0.28.2': + resolution: {integrity: sha512-O387ite7SzUyCcy3JQX4P4bLtEA7bLLkx+esve5JHnyYfNTxcVpXZo9jhdB0lTKN44gztELTdU7nS8Nr16Fs1Q==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + + '@esbuild/darwin-arm64@0.28.2': + resolution: {integrity: sha512-n4KqkOQrraxHJcgjM1RvwbigfQKIKJVpM7xp+KsxiyUSrRdIXnt73VhrPAx0fV44hgfmIVKjxMN9J1t5jySVkw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-x64@0.28.2': + resolution: {integrity: sha512-uq6suIWYP37qzGddBKPw5QEQPi6HiLGsO7UmkpfyaYNQ3D+rN6w6WfwH+nuqcGXWvawGwxOEroO4YGnFh95azw==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + + '@esbuild/freebsd-arm64@0.28.2': + resolution: {integrity: sha512-n+I0BTSRIoy+d6RPKnEVwql5UwBJolytvY4mAOIEJorKlqgPII8ix6slVVrfZ5Tnj7glIZvloylbB/EJPMWEXw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.28.2': + resolution: {integrity: sha512-78XJTJkvPs0kz2w61301PJjXl4g7q3JqiYMZ/M/yVI73EHBrCRTgkhu9oqG7vPqq+a/yadEW8aD+agKlk5xrmg==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + + '@esbuild/linux-arm64@0.28.2': + resolution: {integrity: sha512-pW4AC0P3it8c7do9MVM4p51FzHzdM/TZrerurgRcHJ2WTa1VQ1CIq18xncfpBJw4ojkiZZrKW2yIBWBP92j6Ug==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm@0.28.2': + resolution: {integrity: sha512-XlDnu2q5yoqems+xay6wSAcg9DDD7K9RLKZEBOMZm3ckNpJBvOX20tSfby8KfrrhINDyv9V2YVZKY/SpoGJI8w==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-ia32@0.28.2': + resolution: {integrity: sha512-CYbnj78HsIeA+DhgUKgFCfvNsTHFhMMrinUrMZpDXJXKN8T3XViTZ/+wtHeVxEWY8ewSzTFN+nRmSwO2tZaLUQ==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-loong64@0.28.2': + resolution: {integrity: sha512-buwkd8nsph4R+ajRvw0qM5Hja/TXQow3ptzWO2EbG/cqcIkHloRrdlBtQlshyYGTNFvfkfJ5tpPLVkY4DtsPfQ==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-mips64el@0.28.2': + resolution: {integrity: sha512-ZVykbDyk7519VwiNb9Lcj9m8XM6v5V9uKPvrEMkkEedVewf+0itkhahp4HDpgERXhwLRpWFypsGbG/J8s0QjJA==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-ppc64@0.28.2': + resolution: {integrity: sha512-CAXl+Dtd9UUuJd8pKKdwh6MLm3MUMiqMPmhZ3tTSXPqfyQ3vDl6R5hZdZ/kYojK4ofXtdfSv1tFq8XzWx3heNQ==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-riscv64@0.28.2': + resolution: {integrity: sha512-GeXCej4IQtU1B+QlDV8W/RRvbzI3O/Stss+/bCXv4lZls5WGRtu2a+3JkA3i4qIUlMXpcHebWpF8AkJhATowuA==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-s390x@0.28.2': + resolution: {integrity: sha512-3H1weTYZPxt/WOhByszQZybS9w5lKzUn1FDMsgEChbHWQwHYQQRfBxgCcZvPhjHfKyJjIievvMmEUawJrdY9Dg==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-x64@0.28.2': + resolution: {integrity: sha512-4xTZr1FUmSoQW4XIWmit3tzQrUTZM+N3P0XV8xROKYF50XfI7xeO90+1bZvNwxIufQ9hDQVRJH5YhgPVF8A/HQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + + '@esbuild/netbsd-arm64@0.28.2': + resolution: {integrity: sha512-sSATRjPeDBg3pdgHoQfoYBob11Kk1FGa9lui5RIHZCoCkJa9QKlvl3/vKz2usCmYYjs7ymJR/2Nnsqe+Hjt5nw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.28.2': + resolution: {integrity: sha512-lqnzCV+mM0gIADaKihiCg6ifgfU2L3h5E33rNQBN1Y4MaVGnzryzmvvf7UHxprpQdE8hpqLolJ9Rl+SkIRDpyw==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-arm64@0.28.2': + resolution: {integrity: sha512-AL2qJILH7lNjrDmCQDvdxMfAUIv8KMNZOvrwAQ8i8//ntL9FflhOyMJ8OZSMBb8/AWXe3/5v5S20y3zCoZWKoQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.28.2': + resolution: {integrity: sha512-QtiuPytchRyC4rwUKhexJdQKvDuZ6hWloi3igqPQNUJCS1/v9EiO3UTOXR6A3FoMo4fnAKbWJdqaIwhOzh8qEw==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openharmony-arm64@0.28.2': + resolution: {integrity: sha512-WkhYDmpTjLvGlScA1rwjRUmhl4k8oXR3cIbtqWmELgU/dFeHHlEllxDvdWcNJV9rbzCexB5vz8gtNewWLgCT7Q==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + + '@esbuild/sunos-x64@0.28.2': + resolution: {integrity: sha512-GPMSkTOtMnv2U2F8gxe4Io6qmVs+YKyp832Etqqxr0hFngmXQ3rzwytelm3GIn7T4VviRUlf3sOgBOiTdvaf7g==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + + '@esbuild/win32-arm64@0.28.2': + resolution: {integrity: sha512-PIhhEkE9uPBleRBrQEJpUn7MBnibZzbGzYWPmY3x+YoVg/95zbjB4CxPPOQ8l5tYYM4mMaCthF8/1DIfBQQyWQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-ia32@0.28.2': + resolution: {integrity: sha512-YmJbfTlvU7Sdn9BB+4PRES4oB6pxgS37MAONj+hBr/cpXS1aBPKXxNnDbu+QCWPj0o9dgyxeq79g6c5P8KeuYA==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-x64@0.28.2': + resolution: {integrity: sha512-5ebpxr3nWMzrL/rnUI755Jkuee0bHL/Gq0WTF9lvcpv73wAp5eu8MfBUgWK9bhWvZjj7yX8etf/8tI8Ney695g==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + + '@eslint-community/eslint-utils@4.10.1': + resolution: {integrity: sha512-cuadcxVFE8sDK6iWJbs8Sn0av2Nrh2QSGQhVlBW9AaAHqHwjWsZHT8LJ4hFGPh7ASBV2deFdM7H/DPjulmh8rg==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 + + '@eslint-community/eslint-utils@4.9.1': + resolution: {integrity: sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 + + '@eslint-community/regexpp@4.12.2': + resolution: {integrity: sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==} + engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} + + '@eslint/config-array@0.21.2': + resolution: {integrity: sha512-nJl2KGTlrf9GjLimgIru+V/mzgSK0ABCDQRvxw5BjURL7WfH5uoWmizbH7QB6MmnMBd8cIC9uceWnezL1VZWWw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/config-helpers@0.4.2': + resolution: {integrity: sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/core@0.17.0': + resolution: {integrity: sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/eslintrc@3.3.7': + resolution: {integrity: sha512-F42g89Qd5oAWtp0k0nnSrjziAKza7w8SVT4mStc18LZMaRb4J1HQAHLCalEtDCxrTuksx7NU9qsmeLwpOfPqWw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/js@9.39.5': + resolution: {integrity: sha512-QywQuszQh77pIXCsq998c8hbhSTI/azTty1Z6N53dmAudKHhy573j3yvRLsX2BSp8YpLtoCEG8E9DJe+8zUh4A==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/object-schema@2.1.7': + resolution: {integrity: sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/plugin-kit@0.4.1': + resolution: {integrity: sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@exodus/bytes@1.16.0': + resolution: {integrity: sha512-IcpW84uEn3N7ETtNZMlxKhfl6Pec8rUNGOTBtWbK1FKhJxIFAptZyVrvVRVBimAJxJCgc3PxepxkdWWG4DVzfA==} + engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} + peerDependencies: + '@noble/hashes': ^1.8.0 || ^2.0.0 + peerDependenciesMeta: + '@noble/hashes': + optional: true + + '@humanfs/core@0.19.2': + resolution: {integrity: sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==} + engines: {node: '>=18.18.0'} + + '@humanfs/node@0.16.8': + resolution: {integrity: sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==} + engines: {node: '>=18.18.0'} + + '@humanfs/types@0.15.0': + resolution: {integrity: sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==} + engines: {node: '>=18.18.0'} + + '@humanwhocodes/module-importer@1.0.1': + resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==} + engines: {node: '>=12.22'} + + '@humanwhocodes/retry@0.4.3': + resolution: {integrity: sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==} + engines: {node: '>=18.18'} + + '@img/colour@1.1.0': + resolution: {integrity: sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==} + engines: {node: '>=18'} + + '@img/sharp-darwin-arm64@0.35.5': + resolution: {integrity: sha512-QRUlFQ0WxvdWyqqG/WtI3iupfD5rBzmCHXSdPsY91sAtVtTo7Q4cb6zOccZ3gqEqkr0f1As1ehLqmEpDsRf+lg==} + engines: {node: '>=20.9.0'} + cpu: [arm64] + os: [darwin] + + '@img/sharp-darwin-x64@0.35.5': + resolution: {integrity: sha512-+BR255RhDlpygUpOc/Jdt1nT6DQ3XG/ERo5wbcdOf5Q320dKtPCKPLR1LJs9VGXRaMa8l1uUa0tkCNOXiAxZUw==} + engines: {node: '>=20.9.0'} + cpu: [x64] + os: [darwin] + + '@img/sharp-freebsd-wasm32@0.35.5': + resolution: {integrity: sha512-Y/z91nEZ4uIBX5X3nfTovjU9lHNKFYbL2lpHCLVNmXQK03VIZvXBBt0KxbPGp2SdGSF+2mQU4e+hQaWOt86iAw==} + engines: {node: '>=20.9.0'} + os: [freebsd] + + '@img/sharp-libvips-darwin-arm64@1.3.4': + resolution: {integrity: sha512-5R89nBYiRdUlSWJxPhO+GVtaXzXSxKnRu/xqMn3KTA3L9EB9Oy/P+Nn2f2vlhPuUdy/Zusb2DarbyTpGCfEDuw==} + cpu: [arm64] + os: [darwin] + + '@img/sharp-libvips-darwin-x64@1.3.4': + resolution: {integrity: sha512-iR2OKH80yi0U+dUplyh3/xdpFvps6YkCwsXenIJxqxR1v9o+xtKTGbS9H7cps+2Vxjc8B1j96p75NmTGjIhtpQ==} + cpu: [x64] + os: [darwin] + + '@img/sharp-libvips-linux-arm64@1.3.4': + resolution: {integrity: sha512-Y3dgX/6lE2QhQb+Gxy0WZxfg9MEm/JBjamZpS2IklP7xIQoKN4hzAm7KcMVGtaVDt3neE9OKBC7vAfonA/Lr1A==} + cpu: [arm64] + os: [linux] + + '@img/sharp-libvips-linux-arm@1.3.4': + resolution: {integrity: sha512-LmRtTsOHuvM2+wlO2Db37dx5MiZhB0FvSunciw48YjdOkZz9KAiRbm8ujeMOA1INqmei5NapFxYEK1D1ZSidmw==} + cpu: [arm] + os: [linux] + + '@img/sharp-libvips-linux-ppc64@1.3.4': + resolution: {integrity: sha512-Le6boB8Tai0Nis+gIxIpKx68UDVVIqdR8Tin5Yf1z2LJJQLDJvCDRqRu+jC2qCoD+eIomonmOwB4smBRxfVpYQ==} + cpu: [ppc64] + os: [linux] + + '@img/sharp-libvips-linux-riscv64@1.3.4': + resolution: {integrity: sha512-aHkkIEHPRdQEegJN20MLmGtxYD9R2wQr3Cwpddnu5+YKMt6Uzax7S9h5gpZTo8wyrGuZSlfQ63OevL5mTyOC7Q==} + cpu: [riscv64] + os: [linux] + + '@img/sharp-libvips-linux-s390x@1.3.4': + resolution: {integrity: sha512-ra/mB6MikESDUO7Yg+Mi95bFBb9GsObURuhnOv3OqknjGe9sZrG8tCe9q0xSIGrtLgvgw0gKnFWcK4blSgQOuQ==} + cpu: [s390x] + os: [linux] + + '@img/sharp-libvips-linux-x64@1.3.4': + resolution: {integrity: sha512-GJ//SSXbnwSDes02umB3nDJLFcQzw8a18V8fyhqr6tV515tOEMdImjjxj1AoafMRz56F3PHgftnj1QEKSU1zkw==} + cpu: [x64] + os: [linux] + + '@img/sharp-libvips-linuxmusl-arm64@1.3.4': + resolution: {integrity: sha512-hvulFwtjUcagsis6BBxHwGFwWoNZjgYmULGVrZcyfNbjA8hKILbRxGg15/7w5HDyXHXUos/j6baAWqnCyQ2DWA==} + cpu: [arm64] + os: [linux] + + '@img/sharp-libvips-linuxmusl-x64@1.3.4': + resolution: {integrity: sha512-6zXKeE/p39I1AmA3cJG35eyBGNqNddLnUXjhwBnsGjFPWqf5VKkDBEqaEkPDoTEtkxwi2vv8Tcr2mDyP4So7Fg==} + cpu: [x64] + os: [linux] + + '@img/sharp-linux-arm64@0.35.5': + resolution: {integrity: sha512-LYVx5JTsOM2CBzmxreh+nl64/3H6Xb09iSLknqH47z2T2DFFxDeFLP5y4dJwe6H7uGQlHPyEEtIqyo3DYsRwdQ==} + engines: {node: '>=20.9.0'} + cpu: [arm64] + os: [linux] + + '@img/sharp-linux-arm@0.35.5': + resolution: {integrity: sha512-LEaXK2WdXVK5ykcw0buWyPMsmLLL2vpHLD6yrNSW+JGEL3BZPA4tpKN6iaMc4AxTTAoaX/sU1rOL51lcIz48ZQ==} + engines: {node: '>=20.9.0'} + cpu: [arm] + os: [linux] + + '@img/sharp-linux-ppc64@0.35.5': + resolution: {integrity: sha512-QVxAAq8evVRI9ia2vqgwrmWucn5Dfv+JdWzj75pD8omHLPSP7f8p20O8jxzjCcuCEQEOtYOZUmX1hkiZ0kdevA==} + engines: {node: '>=20.9.0'} + cpu: [ppc64] + os: [linux] + + '@img/sharp-linux-riscv64@0.35.5': + resolution: {integrity: sha512-LtdreXguaavKODPIfzJ4kffx7UNt1omwtK0rch4EBbbSTXPnxWmYSayXdLJw0fJzQ97kHt1gL/yh4tvU+nCyRQ==} + engines: {node: '>=20.9.0'} + cpu: [riscv64] + os: [linux] + + '@img/sharp-linux-s390x@0.35.5': + resolution: {integrity: sha512-UZasTOFiYzotTsGOCu42BfUzP6Tu6Do/947iRm1RsLKvlllxwGcn4RN27LibGWceix4Y+Pmw3jsnTcCQIgWjqA==} + engines: {node: '>=20.9.0'} + cpu: [s390x] + os: [linux] + + '@img/sharp-linux-x64@0.35.5': + resolution: {integrity: sha512-SxFtLTeJInhAA9Q836kux2vZNeOBQEx658qvbboZScr0wIARym3IcGmW7KpVD5sbVg0Ojy+udFQdayYIZyoNog==} + engines: {node: '>=20.9.0'} + cpu: [x64] + os: [linux] + + '@img/sharp-linuxmusl-arm64@0.35.5': + resolution: {integrity: sha512-9HbMclmI1zlNkFRs3z9/eBtDjfD0sGlrX1z6b1qwmiFY5ElDLh4BC0LPBdVp7z1DXFiKlIcznf+ZlsuZzLxQqg==} + engines: {node: '>=20.9.0'} + cpu: [arm64] + os: [linux] + + '@img/sharp-linuxmusl-x64@0.35.5': + resolution: {integrity: sha512-4KOphqB035HrVdqLZfCgMzzERrQkkzOwRhl4OAkRO1YCldbaFjySXMaK534Mo0V+LndnlJk+sbUyLeU0ULyD1A==} + engines: {node: '>=20.9.0'} + cpu: [x64] + os: [linux] + + '@img/sharp-wasm32@0.35.5': + resolution: {integrity: sha512-Ptsga1su4tQx+LLF1ECS9U6nz5kmrXKo6XVbtR48Ke3ZRxxgaWBu7IDtEe1quo8hiupwm6WFqxVlXaSf7IINGQ==} + engines: {node: '>=20.9.0'} + + '@img/sharp-webcontainers-wasm32@0.35.5': + resolution: {integrity: sha512-hfhF/FmoQyTUkA0bIKFOtw536BQSeBMe6BF6QyWlrPxT754+TFLaZ7sKKTfvvM0yJgKgaYTwnFCIZ/GuDw5SUA==} + engines: {node: '>=20.9.0'} + cpu: [wasm32] + + '@img/sharp-win32-arm64@0.35.5': + resolution: {integrity: sha512-X4t7g+7ZA5DKblCBEXGjUqqemj4vczING/5viFwAL8h4N3qYeyjwdCvRLHi4EdOUI+2Z7UFlp1VM+p/AuEtm6Q==} + engines: {node: '>=20.9.0'} + cpu: [arm64] + os: [win32] + + '@img/sharp-win32-ia32@0.35.5': + resolution: {integrity: sha512-5Zm82LoBc43nhwNybZlG7Y1KO//Zhsn306fQl29ZOuStHLGTo3BWL83q3cznX0poxSAMuYL1On/BHBxkBeKr6A==} + engines: {node: ^20.9.0} + cpu: [ia32] + os: [win32] + + '@img/sharp-win32-x64@0.35.5': + resolution: {integrity: sha512-x76eH0vEiHlcMQu8Y8IenntaACtddpT6W0wmXtWrnKcnKI7ME5DdgqhAD6SEWOEl1v2zDvkZDhFA9KnURwpfqg==} + engines: {node: '>=20.9.0'} + cpu: [x64] + os: [win32] + + '@jridgewell/gen-mapping@0.3.13': + resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} + + '@jridgewell/remapping@2.3.5': + resolution: {integrity: sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==} + + '@jridgewell/resolve-uri@3.1.2': + resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} + engines: {node: '>=6.0.0'} + + '@jridgewell/sourcemap-codec@1.6.0': + resolution: {integrity: sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw==} + + '@jridgewell/trace-mapping@0.3.31': + resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} + + '@napi-rs/lzma-linux-x64-gnu@1.5.1': + resolution: {integrity: sha512-oTXEIha4SsuXdTA4Iyskj0kpdx2yVXdhd75c2v3xGrHFfVMsbhTPZU/nMPL4sWKo4pBHm3aucLaqGlF696dTyQ==} + engines: {node: ^22.20 || ^24.12 || >=25} + cpu: [x64] + os: [linux] + + '@napi-rs/wasm-runtime@1.2.4': + resolution: {integrity: sha512-AJxoUD2/15ESHbvpcyjU274nsAPLuOtPHCk0vKJM5pj//Fg/B1FXNWjPnXTT9PymCYYiHo4zPj0ZomXBKhoy7g==} + engines: {node: ^20.19.0 || ^22.13.0 || >=23.5.0} + peerDependencies: + '@emnapi/core': ^1.7.1 || ^2.0.0-alpha.4 + '@emnapi/runtime': ^1.7.1 || ^2.0.0-alpha.4 + + '@next/env@16.3.6': + resolution: {integrity: sha512-x9Vblze1EbtltQYnNH38xCPWU3TVfBd1eXqA3+w9+BTpedkkdNpAaltXlGQ/nsc1+E0mVTNrtcbX3GoO09zeLQ==} + + '@next/eslint-plugin-next@16.3.6': + resolution: {integrity: sha512-jowwDX+7DOlDIjJLgTMxudw+k37QnWu1JkZLkSi9MaJBfDYcfhAPMKBhXL0idYzFN/AGg//axnOR4cLkHX/Rng==} + + '@next/swc-darwin-arm64@16.3.6': + resolution: {integrity: sha512-E/7GEqaUkt8mk/T8v9lAnrhzR06kdq1ZBkC12F8tAMkdIadwNp3H1KqHynDHrpcTlGCUdq/qu6vUL2aYVyYBdw==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [darwin] + + '@next/swc-darwin-x64@16.3.6': + resolution: {integrity: sha512-yBE893/nDWTlaiBD1p+qgt7NUen4U5R6FXyH0s67Npq1S3E0cVSef1WIXC2xBRgQvwAvJq6DnS6Y6PrY0cy4Ew==} + engines: {node: '>= 10'} + cpu: [x64] + os: [darwin] + + '@next/swc-linux-arm64-gnu@16.3.6': + resolution: {integrity: sha512-KJDpjBqBPYlvkivmyrp+Qys6k/7ksbqGQvRVc6ZEGfR+cjQxx+nUkJaWmNZJsmoOrqYNbaXByF8wa0lBwDhB3Q==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + + '@next/swc-linux-arm64-musl@16.3.6': + resolution: {integrity: sha512-mqNg2K+hvWskSRb/QM+Ix412DvBsuSF0XV+frTSw5vmoucNnIlynFwKYew8D01bfATErMOM7Bujrf0BA5DRKFA==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + + '@next/swc-linux-x64-gnu@16.3.6': + resolution: {integrity: sha512-nFncBNGAYouRHjRVaITs9beZRfhX4ssVwpnvPIAbkZVH6LtGoAVlH4bJ8Cnf9SOo9bsXgPFer/GdHtEE3JNOkw==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + + '@next/swc-linux-x64-musl@16.3.6': + resolution: {integrity: sha512-5Mf3cHDGR/Iz0ng2Bj3zUR3p5QS9YK3Hn2QiAfavFmyF48zwThAjpFoiTKNIcOHLYS4zEk+gzyJ/9deQ2ZB8yQ==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + + '@next/swc-win32-arm64-msvc@16.3.6': + resolution: {integrity: sha512-0jkJy0C2kbrJWTk4YLa3xk80pVBpx8FCHJym7CnUfDAXe/FWv5qT7SQJbR0KuemyxaEDlEx5WT4VQJoTW+/9Qw==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [win32] + + '@next/swc-win32-x64-msvc@16.3.6': + resolution: {integrity: sha512-/YXjI1e5OXcZ7YpxRwgP/1jAV/SBKTzeVKqN2mk7mLpcICsyn3Gl5+dIfDTJp70M0ccMhyMMRso4v6mPDCGepg==} + engines: {node: '>= 10'} + cpu: [x64] + os: [win32] + + '@nodelib/fs.scandir@2.1.5': + resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} + engines: {node: '>= 8'} + + '@nodelib/fs.stat@2.0.5': + resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==} + engines: {node: '>= 8'} + + '@nodelib/fs.walk@1.2.8': + resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} + engines: {node: '>= 8'} + + '@nolyfill/is-core-module@1.0.39': + resolution: {integrity: sha512-nn5ozdjYQpUCZlWGuxcJY/KpxkWQs4DcbMCmKojjyrYDEAGy4Ce19NN4v5MduafTwJlbKc99UA8YhSVqq9yPZA==} + engines: {node: '>=12.4.0'} + + '@playwright/test@1.55.1': + resolution: {integrity: sha512-IVAh/nOJaw6W9g+RJVlIQJ6gSiER+ae6mKQ5CX1bERzQgbC1VSeBlwdvczT7pxb0GWiyrxH4TGKbMfDb4Sq/ig==} + engines: {node: '>=18'} + hasBin: true + + '@rollup/rollup-android-arm-eabi@4.63.5': + resolution: {integrity: sha512-J25QJU+B78T4FhhBsNpLJyVWOi31mwtpcMwywHmOKH65Q9IWGA81gPj+dnwlhU8wktVriYE+tFAaQgrnJRzAZg==} + cpu: [arm] + os: [android] + + '@rollup/rollup-android-arm64@4.63.5': + resolution: {integrity: sha512-LDopB3zuZM5Ux9TT2luNEBJW/tYbGU2g1d+VpKk6I+gSKDb+/7sYE6M225gRQt4RbMX6MSwMsVR/phdjVUgRLg==} + cpu: [arm64] + os: [android] + + '@rollup/rollup-darwin-arm64@4.63.5': + resolution: {integrity: sha512-wlJEERGfeuHeBavCL2qVnNacOK43NDoZM4sjkeRPymd04OAE9T1zBqDJgmZ+CIsPTYKwdzpUC8vmOw84dwY4Tg==} + cpu: [arm64] + os: [darwin] + + '@rollup/rollup-darwin-x64@4.63.5': + resolution: {integrity: sha512-4nJJGg5jbo2wwPP4JP+LfEBA3bvP8rU9CLuhp7jWvq9sxEyhjQFTFdrqi+/dHEin/pd8jpT0vcehIpnZtmEdcQ==} + cpu: [x64] + os: [darwin] + + '@rollup/rollup-freebsd-arm64@4.63.5': + resolution: {integrity: sha512-DrZbyCDF1hneuO6jRbvZ2D7+PIBM6yIwYnJpg2vIk58T+wuFpiaGZrfUr59lDWw45bg+IrpTGLPiNi/Fk4w3Cg==} + cpu: [arm64] + os: [freebsd] + + '@rollup/rollup-freebsd-x64@4.63.5': + resolution: {integrity: sha512-gqfUVMJMB3mehqywxp6hTBFfgtMQykZY19+cfiaYP0toIJLb/1DZRJHVkQQGP13W4TAwfZDWeg1qBcheTRioXQ==} + cpu: [x64] + os: [freebsd] + + '@rollup/rollup-linux-arm-gnueabihf@4.63.5': + resolution: {integrity: sha512-CFmhpvAwzSaWMlN3VN7UtmoTihlZNzoP0juQib5TQRnYUyDV8dXeWOp29sobWAT6gXl/hQgAClLlEiYozQG3OQ==} + cpu: [arm] + os: [linux] + + '@rollup/rollup-linux-arm-musleabihf@4.63.5': + resolution: {integrity: sha512-Uc9H8eXCOayV6JLTH5bXKMId6qbhNHa818/BgYjm4jrlq3vZquC9cqyvHBw17xy5Mnj5f+I3gFK5JcEf3hSqrw==} + cpu: [arm] + os: [linux] + + '@rollup/rollup-linux-arm64-gnu@4.63.5': + resolution: {integrity: sha512-VcPr/szv/1BFw112Kt//fxulXt/JPqzzidU84iW68L2DdjnOO8QFUv2zTSYBEPHD6movBD4z+bbr5y60GYM7Jw==} + cpu: [arm64] + os: [linux] + + '@rollup/rollup-linux-arm64-musl@4.63.5': + resolution: {integrity: sha512-BnxtJ5/91BrIHYIkGrmjz/lbMhqEHt1dPFqIxIFR+jPn0xVc/oUSCtIT089zfp5ufwGDlYz2UC+Fe1SRBpYFbQ==} + cpu: [arm64] + os: [linux] + + '@rollup/rollup-linux-loong64-gnu@4.63.5': + resolution: {integrity: sha512-LrYcHZwF+fAMNKHYTOQ5osWM4AZF7YF6D+XtsjDyEvljtt11twc+zHVXBLNEjxVSUnKYsOhvVz4Z213eW02COQ==} + cpu: [loong64] + os: [linux] + + '@rollup/rollup-linux-loong64-musl@4.63.5': + resolution: {integrity: sha512-nj7QKQePAAUpCpJHtg0pR0W/b92A9NO17JS3BAQmHDn/yhmkir2p8llrKY9TOhleKIaSzy1JhxS3T9FVld6coA==} + cpu: [loong64] + os: [linux] + + '@rollup/rollup-linux-ppc64-gnu@4.63.5': + resolution: {integrity: sha512-5ylkX6dWMeBKge9nTU+Rxfb+ZfaCIJ9lRqIFaK0eAMcWp7OJbYnLveLgXmm0VrvuLKb8qIK+mHyH0qu88RM+iA==} + cpu: [ppc64] + os: [linux] + + '@rollup/rollup-linux-ppc64-musl@4.63.5': + resolution: {integrity: sha512-oHK4ZHYFDKjZviK34I+NwgfbGxgI7ztrNxj2hPTSSNFgeq1a/lEd7dHV2fdGAuTH4Iym3RHJg+vAbWaWG4B7Zg==} + cpu: [ppc64] + os: [linux] + + '@rollup/rollup-linux-riscv64-gnu@4.63.5': + resolution: {integrity: sha512-UcetmHZ6XOXuUByiKZyQmb55ZPr0LABr3Ec/HB9wKZn6CEAFWZkE+hsJErJ9hbPBC7nI0dKuELx7CoV6IM7TMg==} + cpu: [riscv64] + os: [linux] + + '@rollup/rollup-linux-riscv64-musl@4.63.5': + resolution: {integrity: sha512-C5CmDPQBtvjVo8cgQsBs+w6WB0JLkiixhgi6hVLV11hERWdn/p0XcPU2OUcZzac9BPOFq7SbaHFa8r3SWEysCQ==} + cpu: [riscv64] + os: [linux] + + '@rollup/rollup-linux-s390x-gnu@4.63.5': + resolution: {integrity: sha512-lHVQHJFKsuuxLMi3MQO9XVL8Tje3JR82CzB+QDKC5NWBcsIWuwsn9uIM5e3lBhI+fF1/s63qnyYqsg65+8rV/w==} + cpu: [s390x] + os: [linux] + + '@rollup/rollup-linux-x64-gnu@4.63.5': + resolution: {integrity: sha512-3W9bTFcQNJn71cSJVM9RKIiZOy8DO/XLDii8Uv/Pm6WKqDRj7JV3ZfuXIEfyuy5LXpIzAbB/1M4Ukp9GKNa7nA==} + cpu: [x64] + os: [linux] + + '@rollup/rollup-linux-x64-musl@4.63.5': + resolution: {integrity: sha512-VDC7rRJlee/scpki96GZ27Omf6yU87s1YXwVTpjE5841faVlDYYT565rgfmoR1U0sqL7z5ivQSDjcsF6VRXyBA==} + cpu: [x64] + os: [linux] + + '@rollup/rollup-openbsd-x64@4.63.5': + resolution: {integrity: sha512-z86Ok2p4pTdv5xqCKZsTooO7yBEiaJR/HzU3Wx8RmWsPoLppnMKROhJusQob8B3IE1ghC343kUW9rC2r+Wf3ig==} + cpu: [x64] + os: [openbsd] + + '@rollup/rollup-openharmony-arm64@4.63.5': + resolution: {integrity: sha512-IzQmj+xXwQFGhMAMKMQVXkMwMZN3TqkJgAE0nSsqvVwWWciP4AIPMmWRqOQ2GfX7TUDZr+xqGFcBS36CRPGw0g==} + cpu: [arm64] + os: [openharmony] + + '@rollup/rollup-win32-arm64-msvc@4.63.5': + resolution: {integrity: sha512-F6qpTaPc9bwBH85kjy0/BLmLSW1uv7AoOXCoRIkg2arlgCYlWYcAbiMkvZuAcaWk9TpCRG//okznLAqLGshkMw==} + cpu: [arm64] + os: [win32] + + '@rollup/rollup-win32-ia32-msvc@4.63.5': + resolution: {integrity: sha512-igoDsTFhhwECBeGbUuLeIk7t8Y1apa+cs6mDWpx2EZ0ch7oEQgzHbFUXN9euoHekCAQzXdXApAGkV6jznS7tWw==} + cpu: [ia32] + os: [win32] + + '@rollup/rollup-win32-x64-gnu@4.63.5': + resolution: {integrity: sha512-U3teMeMbXFmaM5D+OTJpsOXd+wV/qftIeYF9kBKL4v73641qyJmoXFtA28DQLsnmlyayEsTe72xpLHrArq6vHw==} + cpu: [x64] + os: [win32] + + '@rollup/rollup-win32-x64-msvc@4.63.5': + resolution: {integrity: sha512-ypfC34F3RKXvCXBglGqGMsUSMKlgwd1HX9AOAlx9RoZZ6GaI42YHVeKpzg3JG+wpBUJYTG+NNZhqbDWL8tBZkw==} + cpu: [x64] + os: [win32] + + '@rtsao/scc@1.1.0': + resolution: {integrity: sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==} + + '@standard-schema/spec@1.1.0': + resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} + + '@swc/helpers@0.5.23': + resolution: {integrity: sha512-5lSsMOTXURePglDfvuAQUqkGek9Hg2kksOYay2m0+XR++b2NWYL/4sWyuvVBIs8oKnJaxkdi9whaL/sqN13afw==} + + '@testing-library/dom@10.4.2': + resolution: {integrity: sha512-yzr2S9HyAIdhz2/6qHgbs665Q7PKVcDF05vsOlHPxG1mo36gKVesdYVeDLnXgfjJ03CrKRk08knc6+E/9m8v2Q==} + engines: {node: '>=18'} + + '@testing-library/jest-dom@6.10.0': + resolution: {integrity: sha512-HQwu0KaB2zyT0iLzBL+8CLyZDL3KlZlZJ+2iyc9uCUnlJVskJU/UlPuVCyIPhtukjPQdT2QNoR5nCP5FqTmmDQ==} + engines: {node: '>=22', npm: '>=6', yarn: '>=1'} + deprecated: Incorrect minor release with breaking changes (Node >=22 and required @testing-library/dom peer). Use 6.9.1 for the 6.x line, or upgrade to 7.0.0. + peerDependencies: + '@testing-library/dom': '>=10 <11' + + '@testing-library/react@16.3.3': + resolution: {integrity: sha512-Uo193NgQbPMz6lrrhtRQQFcMC6Re/ELLFbbuVL30WDlZxlpZf9/lMHTAVxPRLw1q1iu9OJmR1c2BLiENRstdBg==} + engines: {node: '>=18'} + peerDependencies: + '@testing-library/dom': ^10.0.0 + '@types/react': ^18.0.0 || ^19.0.0 + '@types/react-dom': ^18.0.0 || ^19.0.0 + react: ^18.0.0 || ^19.0.0 + react-dom: ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@tybys/wasm-util@0.10.4': + resolution: {integrity: sha512-W3c4gRigFS0T/Ma4qIYF3GDAc5AQdHb1yL5znJT1Zv1YaD9Kitx656wBjvr19qbiosmZT8lWDM5BEMynUqX65A==} + + '@types/aria-query@5.0.4': + resolution: {integrity: sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==} + + '@types/chai@5.2.3': + resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==} + + '@types/deep-eql@4.0.2': + resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==} + + '@types/estree@1.0.9': + resolution: {integrity: sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==} + + '@types/json-schema@7.0.15': + resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} + + '@types/json5@0.0.29': + resolution: {integrity: sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==} + + '@types/node@22.18.6': + resolution: {integrity: sha512-r8uszLPpeIWbNKtvWRt/DbVi5zbqZyj1PTmhRMqBMvDnaz1QpmSKujUtJLrqGZeoM8v72MfYggDceY4K1itzWQ==} + + '@types/react-dom@19.3.0': + resolution: {integrity: sha512-ZI7bU42mZXXKHn/qNLEw2IrbiINU7X5+vfgdixBHkCNpYWXjKgfQ/P+uyGb5CjOLB9UcnTeg3rylQtV2hym44Q==} + peerDependencies: + '@types/react': ^19.3.0 + + '@types/react@19.3.0': + resolution: {integrity: sha512-N0rFCuH9YoxG9/m61l9MfpJKfmLOVU0em7ipIz6TRgSSkvReLB9vL85GB+yr8Bs5leqpvg96JSwF4ZS1s4viQg==} + + '@typescript-eslint/eslint-plugin@8.71.0': + resolution: {integrity: sha512-pqcS9c1HxZTHt7End4nXqd0s5lJrrFzrgCkKFJrsbUnaL6M3+6oBFZaslg6Gjsl3argl2DDRFROnXARaZ2e4Nw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + '@typescript-eslint/parser': ^8.71.0 + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/parser@8.71.0': + resolution: {integrity: sha512-CG4nPk1f2zc8yw4pALqHsFYH2hdo+h1T9daSp21+Hnxi9LOE3GT9hAfTKJCBXVNM2GmYs1eMEP615wPoeOgk3A==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/project-service@8.71.0': + resolution: {integrity: sha512-aABjw5rjBacYONVPaPiWOCjJu0vEF4a25iQuodlmQYL1trtLZ0X/y+2Vzl3BKI1odM4LnwLE1oUDXYp1wzx1TQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/scope-manager@8.71.0': + resolution: {integrity: sha512-gWF0BhUcnjZxSpLE8ngS/59n2SB0J3YqRxvX1+2aoRJk9hNtHSLOV+TcarFiOr5ipXm3yc1QrI4c9YZc8zyCxw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@typescript-eslint/tsconfig-utils@8.71.0': + resolution: {integrity: sha512-Z1UlWHADEK2Mlb9NpWfDeSjqoZ5EyrOv4R3eQpbkzqn/EwaIdOpXXupEA1+0ZIOSJSZZDBHG0BrQyN8zUG6Pwg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/type-utils@8.71.0': + resolution: {integrity: sha512-i8uO1qbdxeKgRnS5sCRt6On3/nfo2d2DwQe3Yvjx543zLy7r8ySqRuPPiIIXAhS03U0v5NfAFx+rUgxFzKKwNw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/types@8.71.0': + resolution: {integrity: sha512-cJ4OoxPGWvFnBTnSZyaU+qJzGTqPTGJY+gDchj6cRyLRdmIdt4rcsE4twj+zPfrNiWuVi38wijHzShL++Z9atQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@typescript-eslint/typescript-estree@8.71.0': + resolution: {integrity: sha512-PEEF4G5sLLWAS5BpPrUvms4ySZkiBQQZM4z+3ReI46axK5Vqr/vXBQatJQIZZOYdGyPUAKTtsrWzpqKuU+3DEw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/utils@8.71.0': + resolution: {integrity: sha512-pKR/tEMVrXZG23UFKUn5BQf3zfmfk7KQceI2cGzywZ5nxM5Eu3hEJU1utjWzydtzBbcJAQhHN8iPCxobHpPcZQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/visitor-keys@8.71.0': + resolution: {integrity: sha512-8eQ9R218XORK+KLosnf4bu/QsUXvUyVwTbArg7/0NMB1Pu87OJKvj4nhFblkYE8gQV73mW1dx1ptlPCkwRGa7A==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@typescript/vfs@1.6.5': + resolution: {integrity: sha512-gobLHQLmvQGJInFk31NvgMd1uYzRXsd8qgnOssyLD055R2QPnw1ubRbBQ4+T+5qC7IQVuYXdvNDE2G7tMDVHgw==} + peerDependencies: + typescript: '*' + + '@unrs/resolver-binding-android-arm-eabi@1.12.2': + resolution: {integrity: sha512-g5T90pqg1bo/7mytQx6F4iBNC0Wsh9cu+z9veDbFjc7HjpesJFWD7QMS0NGStXM075+7dJPPVvBbpZlnrdpi/w==} + cpu: [arm] + os: [android] + + '@unrs/resolver-binding-android-arm64@1.12.2': + resolution: {integrity: sha512-YGCRZv/9GLhwmz6mYDeTsm/92BAyR28l6c2ReweVW5pWgfsitWLY8upvfRlGdoyD8HjeTHSYJWyZGD4KJA/nFQ==} + cpu: [arm64] + os: [android] + + '@unrs/resolver-binding-darwin-arm64@1.12.2': + resolution: {integrity: sha512-u9DiNT1auQMO20A9SyTuG3wUgQWB9Z7KjAg0uFuCDR1FsAY8A0CG2S6JpHS1xwm/w1G08bjXZDcyOCjv1WAm2w==} + cpu: [arm64] + os: [darwin] + + '@unrs/resolver-binding-darwin-x64@1.12.2': + resolution: {integrity: sha512-f7rPLi/T1HVKZu/u6t87lroib16n8vrSzcyxI7lg4BGO9UF26KhQL44sd9eOUgrTYhvRXtWOIZT5PejdPyJfUA==} + cpu: [x64] + os: [darwin] + + '@unrs/resolver-binding-freebsd-x64@1.12.2': + resolution: {integrity: sha512-BpcOjWCJub6nRZUS2zA20pmLvjtqAtGejETaIyRLiZiQf++cbrjltLA5NN/xaXfqeOBOSlMFbemIl5/S5tljmg==} + cpu: [x64] + os: [freebsd] + + '@unrs/resolver-binding-linux-arm-gnueabihf@1.12.2': + resolution: {integrity: sha512-vZTDvdSISZjJx66OzJqtsOhzifbqRjbmI1Mnu49fQDwog5GtDI4QidRiEAYbZCRj9C8YZEW+3ZjqsyS9GR4k2A==} + cpu: [arm] + os: [linux] + + '@unrs/resolver-binding-linux-arm-musleabihf@1.12.2': + resolution: {integrity: sha512-BiPI+IrIlwcW4nLLMM21+B1dFPzd55yAVgVGrdgDjNef+ch03GdxrcyaIz8X9SsQirh/kCQ7mviyWlMxdh2D7g==} + cpu: [arm] + os: [linux] + + '@unrs/resolver-binding-linux-arm64-gnu@1.12.2': + resolution: {integrity: sha512-zJc0H99FEPoFfSrNpa91HYfxzfAJCr502oxNK1cfdC9hlaFI43RT+JFCann9JUgZmLzzntChHyn13Sgn9ljHNg==} + cpu: [arm64] + os: [linux] + + '@unrs/resolver-binding-linux-arm64-musl@1.12.2': + resolution: {integrity: sha512-KQ3Lki6l+Pz1k/eBipN41ES+YUK30beLGb9YqcB1O542cyLCNE6GaxrfcY3T6EezmGGk84wb5XyO9loTM9tkcA==} + cpu: [arm64] + os: [linux] + + '@unrs/resolver-binding-linux-loong64-gnu@1.12.2': + resolution: {integrity: sha512-3SJGEh1DborhG6pyxvhPzCT4bbSIVihsvgJc13P1bHG7KLdNDaF9T3gsTwFc7Jw/5Y5/iWOjkEx7Zy0NvCGX3Q==} + cpu: [loong64] + os: [linux] + + '@unrs/resolver-binding-linux-loong64-musl@1.12.2': + resolution: {integrity: sha512-jiuG/Obbel7uw1PwHNFfrkiKhLAF6mnyZ6aWlOAVN9WqKm8v0OFGnciJIHu8+CMvXLQ8AD51LPzAoUfT21D5Ew==} + cpu: [loong64] + os: [linux] + + '@unrs/resolver-binding-linux-ppc64-gnu@1.12.2': + resolution: {integrity: sha512-q7xRvVpmcfeL+LlZg8Pbbo6QaTZwDU5BaGZbwfhkEsXJn3Was8xYfE0RBH266xZt0rM6B7i8xAYIvjthuUIWHg==} + cpu: [ppc64] + os: [linux] + + '@unrs/resolver-binding-linux-riscv64-gnu@1.12.2': + resolution: {integrity: sha512-0CVdx6lcnT3Q9inOH8tsMIOJ6ImndllMjqJHg8RLVdB7Vq4SfkEXl9mCSsVNuNA4MCYycRicCUxPCabVHJRr6A==} + cpu: [riscv64] + os: [linux] + + '@unrs/resolver-binding-linux-riscv64-musl@1.12.2': + resolution: {integrity: sha512-iOwlRo9vnp6R6ohHQS11n0NnfdXx/omhkocmIfaPRpQhKZ+3BDMkkdRVh53qjkFkpPddf+FETA28NwGN7l5l+w==} + cpu: [riscv64] + os: [linux] + + '@unrs/resolver-binding-linux-s390x-gnu@1.12.2': + resolution: {integrity: sha512-HYJtLfXq94q8iZNFT1lknx258wlkkWhZeUXJRqzKBBUJ00CvZ+N33zgbCqimLjsyw5Va6uUxhVa12mI+kaveEw==} + cpu: [s390x] + os: [linux] + + '@unrs/resolver-binding-linux-x64-gnu@1.12.2': + resolution: {integrity: sha512-mPsUhunKKDih5O96Y6enDQyHc1SqBPlY1E/SfMWDM3EdJ95Z9CArPeCVwCCqbP45ljvivdEk8Fxn+SIb1rDAJQ==} + cpu: [x64] + os: [linux] + + '@unrs/resolver-binding-linux-x64-musl@1.12.2': + resolution: {integrity: sha512-azrt6+5ydLd8Vt210AAFis/lZevSfPw93EJRIJG+xPu4WCJ8K0kppCTpMyLPcKT7H15M4Jnt2tMp5bOvCkRC6A==} + cpu: [x64] + os: [linux] + + '@unrs/resolver-binding-openharmony-arm64@1.12.2': + resolution: {integrity: sha512-YZ9hP4O0X9PQb8eO980qmLNGH4zT3I9+SZTdt0Pr0YyuGQhYKoOZkV02VzrzyOZJ5xIJ3UFIenKkUkGg8GjgWQ==} + cpu: [arm64] + os: [openharmony] + + '@unrs/resolver-binding-wasm32-wasi@1.12.2': + resolution: {integrity: sha512-tYFDIkMxSflfEc/h92ZWNsZlHSwgimbNHSO3PL2JWQHfCuC2q316jMyYU9TIWZsFK2bQwyK5VAdYgn8ygPj69A==} + engines: {node: '>=14.0.0'} + cpu: [wasm32] + + '@unrs/resolver-binding-win32-arm64-msvc@1.12.2': + resolution: {integrity: sha512-qzNyg3xL0VPQmCaUh+N5jSitce6k+uCBfMDesWRnlULOZaqUkaJ0ybdT+UqlAWJoQjuqfIU/0Ptx9bteN4D82g==} + cpu: [arm64] + os: [win32] + + '@unrs/resolver-binding-win32-ia32-msvc@1.12.2': + resolution: {integrity: sha512-WD9sY00OfpHVGfsnHZoA8jVT+esS/Bg8z8jzxp5BnDCjjwsuKsPQrzswwpFy4J1AUJbXPRfkpcX0mXrzeXW79g==} + cpu: [ia32] + os: [win32] + + '@unrs/resolver-binding-win32-x64-msvc@1.12.2': + resolution: {integrity: sha512-nAB74NfSNKknqQ1RrYj6uz8FcXEomu/MATJZxh/x+BArzN2U3JbOYC0APYzUIGhVY3m5hRxA8VPNdPBoG8txlA==} + cpu: [x64] + os: [win32] + + '@vitest/expect@3.2.7': + resolution: {integrity: sha512-E8eBXaKibuvH2pSZErOjdVb5vF4PbKYcrnluBTYxEk1l/VhhwZg1kZQsdtjq+CsF5CFydf2Rdkz7jDHKSisi3w==} + + '@vitest/expect@4.0.7': + resolution: {integrity: sha512-jGRG6HghnJDjljdjYIoVzX17S6uCVCBRFnsgdLGJ6CaxfPh8kzUKe/2n533y4O/aeZ/sIr7q7GbuEbeGDsWv4Q==} + + '@vitest/mocker@3.2.7': + resolution: {integrity: sha512-Trr0hYO9CM3Wj6ksWHRhK9IZpIY6wTMO5u/MqXurMxT57sWBaOPEtP3Oq60ihZuh5JsiagKfz95OcxdEP6dBrA==} + peerDependencies: + msw: ^2.4.9 + vite: ^5.0.0 || ^6.0.0 || ^7.0.0-0 + peerDependenciesMeta: + msw: + optional: true + vite: + optional: true + + '@vitest/mocker@4.0.7': + resolution: {integrity: sha512-OsDwLS7WnpuNslOV6bJkXVYVV/6RSc4eeVxV7h9wxQPNxnjRvTTrIikfwCbMyl8XJmW6oOccBj2Q07YwZtQcCw==} + peerDependencies: + msw: ^2.4.9 + vite: ^6.0.0 || ^7.0.0-0 + peerDependenciesMeta: + msw: + optional: true + vite: + optional: true + + '@vitest/pretty-format@3.2.7': + resolution: {integrity: sha512-KUHlwqVu0sRlhCdyPdQ/wBoTfRahjUky1MubOmYw9fWfIZy1gNoHpuaaQBPAaMaVYdQYHJLurzj8ECCj5OwTqA==} + + '@vitest/pretty-format@4.0.7': + resolution: {integrity: sha512-YY//yxqTmk29+/pK+Wi1UB4DUH3lSVgIm+M10rAJ74pOSMgT7rydMSc+vFuq9LjZLhFvVEXir8EcqMke3SVM6Q==} + + '@vitest/runner@3.2.7': + resolution: {integrity: sha512-sB9y4ovltoQP+WaUPwmSxO9WIg9Ig694Di5PalVPsYHklAdE027mehpWF2SQSVq+k6sFgaivbTjTJwZLSHbedA==} + + '@vitest/runner@4.0.7': + resolution: {integrity: sha512-orU1lsu4PxLEcDWfjVCNGIedOSF/YtZ+XMrd1PZb90E68khWCNzD8y1dtxtgd0hyBIQk8XggteKN/38VQLvzuw==} + + '@vitest/snapshot@3.2.7': + resolution: {integrity: sha512-7C+MwShwtBSI5Buwoyg3s/iY1eHL9PKAf+O1wVh/TdnjXUtkoL/9YQtre90i4MtNXM6edP1wJ2zOBpfCyhIS7g==} + + '@vitest/snapshot@4.0.7': + resolution: {integrity: sha512-xJL+Nkw0OjaUXXQf13B8iKK5pI9QVtN9uOtzNHYuG/o/B7fIEg0DQ+xOe0/RcqwDEI15rud1k7y5xznBKGUXAA==} + + '@vitest/spy@3.2.7': + resolution: {integrity: sha512-Q2eQGI6d2L/hBtZ0qNuKcAGid68XK6cv1xsoaIma6PaJhHPoqcEJhYpXZ/5myCMqkNgtP6UKuBhbc0nHKnrkuQ==} + + '@vitest/spy@4.0.7': + resolution: {integrity: sha512-FW4X8hzIEn4z+HublB4hBF/FhCVaXfIHm8sUfvlznrcy1MQG7VooBgZPMtVCGZtHi0yl3KESaXTqsKh16d8cFg==} + + '@vitest/utils@3.2.7': + resolution: {integrity: sha512-x6BDOd7dyo3PFLY3I9/HJ25X/6OurhGXk2/B9gOZNPF7XDVjeBK4k01lQE5uvDpbuheErh91qYuE1E2OEjK3Rw==} + + '@vitest/utils@4.0.7': + resolution: {integrity: sha512-HNrg9CM/Z4ZWB6RuExhuC6FPmLipiShKVMnT9JlQvfhwR47JatWLChA6mtZqVHqypE6p/z6ofcjbyWpM7YLxPQ==} + + acorn-jsx@5.3.2: + resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} + peerDependencies: + acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 + + acorn@8.18.0: + resolution: {integrity: sha512-lGq+9yr1/GuAWaVYIHRjvvySG5/4VfKIvC8EWxStPdcDh/Ka7FG3twP6v4d5BkravUilhIAsG4Qj83t02LWUPQ==} + engines: {node: '>=0.4.0'} + hasBin: true + + agent-base@7.1.4: + resolution: {integrity: sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==} + engines: {node: '>= 14'} + + ajv@6.15.0: + resolution: {integrity: sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==} + + ansi-regex@5.0.1: + resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} + engines: {node: '>=8'} + + ansi-styles@4.3.0: + resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} + engines: {node: '>=8'} + + ansi-styles@5.2.0: + resolution: {integrity: sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==} + engines: {node: '>=10'} + + argparse@2.0.1: + resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + + aria-query@5.3.0: + resolution: {integrity: sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==} + + aria-query@5.3.2: + resolution: {integrity: sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==} + engines: {node: '>= 0.4'} + + array-buffer-byte-length@1.0.2: + resolution: {integrity: sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==} + engines: {node: '>= 0.4'} + + array-includes@3.2.0: + resolution: {integrity: sha512-VXY5eFRarnXcYxwBjJzPmEhH55+rmP79/+ueDhi0F+TuqfHCItagIHqxeUZrmgrOPa31QTh9H85DjX3FfJ0FTg==} + engines: {node: '>= 0.4'} + + array.prototype.findlast@1.2.5: + resolution: {integrity: sha512-CVvd6FHg1Z3POpBLxO6E6zr+rSKEQ9L6rZHAaY7lLfhKsWYUBBOuMs0e9o24oopj6H+geRCX0YJ+TJLBK2eHyQ==} + engines: {node: '>= 0.4'} + + array.prototype.findlastindex@1.2.6: + resolution: {integrity: sha512-F/TKATkzseUExPlfvmwQKGITM3DGTK+vkAsCZoDc5daVygbJBnjEUCbgkAvVFsgfXfX4YIqZ/27G3k3tdXrTxQ==} + engines: {node: '>= 0.4'} + + array.prototype.flat@1.3.3: + resolution: {integrity: sha512-rwG/ja1neyLqCuGZ5YYrznA62D4mZXg0i1cIskIUKSiqF3Cje9/wXAls9B9s1Wa2fomMsIv8czB8jZcPmxCXFg==} + engines: {node: '>= 0.4'} + + array.prototype.flatmap@1.3.3: + resolution: {integrity: sha512-Y7Wt51eKJSyi80hFrJCePGGNo5ktJCslFuboqJsbf57CCPcm5zztluPlc4/aD8sWsKvlwatezpV4U1efk8kpjg==} + engines: {node: '>= 0.4'} + + array.prototype.tosorted@1.1.4: + resolution: {integrity: sha512-p6Fx8B7b7ZhL/gmUsAy0D15WhvDccw3mnGNbZpi3pmeJdxtWsj2jEaI4Y6oo3XiHfzuSgPwKc04MYt6KgvC/wA==} + engines: {node: '>= 0.4'} + + arraybuffer.prototype.slice@1.0.4: + resolution: {integrity: sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==} + engines: {node: '>= 0.4'} + + assertion-error@2.0.1: + resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} + engines: {node: '>=12'} + + ast-types-flow@0.0.8: + resolution: {integrity: sha512-OH/2E5Fg20h2aPrbe+QL8JZQFko0YZaF+j4mnQ7BGhfavO7OpSLa8a0y9sBwomHdSbkhTS8TQNayBfnW5DwbvQ==} + + async-function@1.0.0: + resolution: {integrity: sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==} + engines: {node: '>= 0.4'} + + available-typed-arrays@1.0.7: + resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==} + engines: {node: '>= 0.4'} + + axe-core@4.10.3: + resolution: {integrity: sha512-Xm7bpRXnDSX2YE2YFfBk2FnF0ep6tmG7xPh8iHee8MIcrgq762Nkce856dYtJYLkuIoYZvGfTs/PbZhideTcEg==} + engines: {node: '>=4'} + + axe-core@4.13.0: + resolution: {integrity: sha512-UzGt8zg7Ny8djbYMhxl2zuEevVa7r2gJjYY5Lwr1xM7+XU2nd6CkIWFTVcCIbAP63vSz71NaVyyuSk9lHKcy0A==} + engines: {node: '>=4'} + + axobject-query@4.1.0: + resolution: {integrity: sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==} + engines: {node: '>= 0.4'} + + balanced-match@1.0.2: + resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + + balanced-match@4.0.4: + resolution: {integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==} + engines: {node: 18 || 20 || >=22} + + baseline-browser-mapping@2.11.26: + resolution: {integrity: sha512-GLQdD3y6UF8iVuMJl5fHgE4jdn/ua7n+toKfLgNlg3BqQtOZjpy68T8Tup8/wGWZCDlm7KMg7tPb4MPn7oN0TQ==} + engines: {node: '>=6.0.0'} + hasBin: true + + bidi-js@1.1.0: + resolution: {integrity: sha512-fX1Onk0tdVPC7obPWB5EbJ1z7NVhLq4m2xZLq2YXBkxzMXIGRpNMU88n0EPgWseKl12J7zXs7qrDxPK4sRs2fg==} + + brace-expansion@1.1.21: + resolution: {integrity: sha512-9zeA+KLZNNzglF2TPKRQEDyx6Yby7daAkuy8MiPzpXPsYDWi/DRM8jmwUDxokQjYqBpv5DgPiwD4h4ZZSy1Ujw==} + + brace-expansion@5.0.12: + resolution: {integrity: sha512-YovQ3rzhaLMIrDjNDMkNS01tea93qhEhG5xy8f6+R0l+dw3Ki+5sCoIoI942iuLZTHWogWktgwVDhU09iNEimQ==} + engines: {node: 20 || >=22} + + braces@3.0.3: + resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} + engines: {node: '>=8'} + + browserslist@4.29.2: + resolution: {integrity: sha512-/u9r8k8ue4ZhHCw9ovDtltpWdXeXzjhdxCGj6vm1RGLPXb8lu33EMdoCHF5Sx8oYBS+Q/FYYNmAqlHncxR3RmA==} + engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} + hasBin: true + + cac@6.7.14: + resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} + engines: {node: '>=8'} + + call-bind-apply-helpers@1.0.2: + resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} + engines: {node: '>= 0.4'} + + call-bind@1.0.9: + resolution: {integrity: sha512-a/hy+pNsFUTR+Iz8TCJvXudKVLAnz/DyeSUo10I5yvFDQJBFU2s9uqQpoSrJlroHUKoKqzg+epxyP9lqFdzfBQ==} + engines: {node: '>= 0.4'} + + call-bound@1.0.4: + resolution: {integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==} + engines: {node: '>= 0.4'} + + callsites@3.1.0: + resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} + engines: {node: '>=6'} + + caniuse-lite@1.0.30001813: + resolution: {integrity: sha512-zfjJo4rM0+fUomGDBW/xcDjhIwz/210DGvip2MAMDZ8KHcRPnOHmEgHPZP0UHlZoxr8fYKVJOqcORhYQcG4FKQ==} + + chai@5.3.3: + resolution: {integrity: sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==} + engines: {node: '>=18'} + + chai@6.2.2: + resolution: {integrity: sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==} + engines: {node: '>=18'} + + chalk@4.1.2: + resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} + engines: {node: '>=10'} + + check-error@2.1.3: + resolution: {integrity: sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==} + engines: {node: '>= 16'} + + client-only@0.0.1: + resolution: {integrity: sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==} + + color-convert@2.0.1: + resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} + engines: {node: '>=7.0.0'} + + color-name@1.1.4: + resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + + concat-map@0.0.1: + resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} + + convert-source-map@2.0.0: + resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + + cross-spawn@7.0.6: + resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} + engines: {node: '>= 8'} + + css-tree@3.2.1: + resolution: {integrity: sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA==} + engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0} + + css.escape@1.5.1: + resolution: {integrity: sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==} + + cssstyle@4.6.0: + resolution: {integrity: sha512-2z+rWdzbbSZv6/rhtvzvqeZQHrBaqgogqt85sqFNbabZOuFbCVFb8kPeEtZjiKkbrm395irpNKiYeFeLiQnFPg==} + engines: {node: '>=18'} + + cssstyle@5.3.7: + resolution: {integrity: sha512-7D2EPVltRrsTkhpQmksIu+LxeWAIEk6wRDMJ1qljlv+CKHJM+cJLlfhWIzNA44eAsHXSNe3+vO6DW1yCYx8SuQ==} + engines: {node: '>=20'} + + csstype@3.2.3: + resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} + + damerau-levenshtein@1.0.8: + resolution: {integrity: sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==} + + data-urls@5.0.0: + resolution: {integrity: sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==} + engines: {node: '>=18'} + + data-urls@6.0.1: + resolution: {integrity: sha512-euIQENZg6x8mj3fO6o9+fOW8MimUI4PpD/fZBhJfeioZVy9TUpM4UY7KjQNVZFlqwJ0UdzRDzkycB997HEq1BQ==} + engines: {node: '>=20'} + + data-view-buffer@1.0.2: + resolution: {integrity: sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==} + engines: {node: '>= 0.4'} + + data-view-byte-length@1.0.2: + resolution: {integrity: sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ==} + engines: {node: '>= 0.4'} + + data-view-byte-offset@1.0.1: + resolution: {integrity: sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==} + engines: {node: '>= 0.4'} + + debug@3.2.7: + resolution: {integrity: sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + debug@4.4.3: + resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + decimal.js@10.6.0: + resolution: {integrity: sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==} + + deep-eql@5.0.2: + resolution: {integrity: sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==} + engines: {node: '>=6'} + + deep-is@0.1.4: + resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} + + define-data-property@1.1.4: + resolution: {integrity: sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==} + engines: {node: '>= 0.4'} + + define-properties@1.2.1: + resolution: {integrity: sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==} + engines: {node: '>= 0.4'} + + dequal@2.0.3: + resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} + engines: {node: '>=6'} + + detect-libc@2.1.2: + resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} + engines: {node: '>=8'} + + doctrine@2.1.0: + resolution: {integrity: sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==} + engines: {node: '>=0.10.0'} + + dom-accessibility-api@0.5.16: + resolution: {integrity: sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==} + + dom-accessibility-api@0.6.3: + resolution: {integrity: sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==} + + dunder-proto@1.0.1: + resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} + engines: {node: '>= 0.4'} + + electron-to-chromium@1.5.439: + resolution: {integrity: sha512-qu6QIPXhsb+CRcAiTMNjR4A1y/7tCYKkKjr5CZXRVih6qkDf79peZ2BpEU3qoDBNCRrcy3Mra3X9nG5oruuA7Q==} + + emoji-regex@9.2.2: + resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} + + entities@6.0.1: + resolution: {integrity: sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==} + engines: {node: '>=0.12'} + + entities@8.1.0: + resolution: {integrity: sha512-kxL7msIffSuh9aaFAMD7rxAIuTRMAHMeBtgHW2yUdWw732ZNh4MehkF2gdjvtdmikkaIP9bFDDJOPlsvm7avrA==} + engines: {node: '>=20.19.0'} + + es-abstract-get@1.0.0: + resolution: {integrity: sha512-6PMWXpdhshVvFp+FoWYs1EvG1Nj0tvk0dZM+XcK0xMEM1czRVcP6ohqPWHy6qPagSpC8j4+p89WXlT+xXJs/fg==} + engines: {node: '>= 0.4'} + + es-abstract@1.24.2: + resolution: {integrity: sha512-2FpH9Q5i2RRwyEP1AylXe6nYLR5OhaJTZwmlcP0dL/+JCbgg7yyEo/sEK6HeGZRf3dFpWwThaRHVApXSkW3xeg==} + engines: {node: '>= 0.4'} + + es-define-property@1.0.1: + resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} + engines: {node: '>= 0.4'} + + es-errors@1.3.0: + resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} + engines: {node: '>= 0.4'} + + es-iterator-helpers@1.4.0: + resolution: {integrity: sha512-c/A0P0oxkACDc+cKWw8evLXK83oBKgn0qPOqCYT4x9uolpCIJAcYvJC9QYKNDRPsTeGyCrQ326jrvgZWdCdK5Q==} + engines: {node: '>= 0.4'} + + es-module-lexer@1.7.0: + resolution: {integrity: sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==} + + es-object-atoms@1.1.2: + resolution: {integrity: sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==} + engines: {node: '>= 0.4'} + + es-set-tostringtag@2.1.0: + resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==} + engines: {node: '>= 0.4'} + + es-shim-unscopables@1.1.0: + resolution: {integrity: sha512-d9T8ucsEhh8Bi1woXCf+TIKDIROLG5WCkxg8geBCbvk22kzwC5G2OnXVMO6FUsvQlgUUXQ2itephWDLqDzbeCw==} + engines: {node: '>= 0.4'} + + es-to-primitive@1.3.4: + resolution: {integrity: sha512-yPDz7wqpg1/mmHLmS3tcfTfbw5f1eryXvyghYBffGdERwe+mV7ZcWzTR8LR17Kvqt3qfPurjlonmnq3MKXIOXw==} + engines: {node: '>= 0.4'} + + esbuild@0.28.2: + resolution: {integrity: sha512-HKVLS8dvII+xoKW9kmqxbRKrnWEXfJJr/FZhhJmiqIB0e053QNYFqOBouTMO/k5sID4MvCiUCvv8b9M4h32wIA==} + engines: {node: '>=18'} + hasBin: true + + escalade@3.2.0: + resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} + engines: {node: '>=6'} + + escape-string-regexp@4.0.0: + resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} + engines: {node: '>=10'} + + eslint-config-next@16.3.6: + resolution: {integrity: sha512-1Upt3U7BDwU+ilpe2byZjAfts9oNq4d4fv/zXEvs8/4yS+cwOQW/WCxUNy8gCDquX67SzeehDvKblVC6ZBMocQ==} + peerDependencies: + eslint: '>=9.0.0' + typescript: '>=3.3.1' + peerDependenciesMeta: + typescript: + optional: true + + eslint-import-resolver-node@0.3.10: + resolution: {integrity: sha512-tRrKqFyCaKict5hOd244sL6EQFNycnMQnBe+j8uqGNXYzsImGbGUU4ibtoaBmv5FLwJwcFJNeg1GeVjQfbMrDQ==} + + eslint-import-resolver-typescript@3.10.1: + resolution: {integrity: sha512-A1rHYb06zjMGAxdLSkN2fXPBwuSaQ0iO5M/hdyS0Ajj1VBaRp0sPD3dn1FhME3c/JluGFbwSxyCfqdSbtQLAHQ==} + engines: {node: ^14.18.0 || >=16.0.0} + peerDependencies: + eslint: '*' + eslint-plugin-import: '*' + eslint-plugin-import-x: '*' + peerDependenciesMeta: + eslint-plugin-import: + optional: true + eslint-plugin-import-x: + optional: true + + eslint-module-utils@2.14.0: + resolution: {integrity: sha512-W2WCRZ9Dqntd+2u8jJcVMV2PKulc6RdLgUUoh/yQr3uB6lo/ZOeGx11sv60/8S4QFFKNslAlWhr9u0Ef7ZW6Ig==} + engines: {node: '>=4'} + peerDependencies: + '@typescript-eslint/parser': '*' + eslint: '*' + eslint-import-resolver-node: '*' + eslint-import-resolver-typescript: '*' + eslint-import-resolver-webpack: '*' + peerDependenciesMeta: + '@typescript-eslint/parser': + optional: true + eslint: + optional: true + eslint-import-resolver-node: + optional: true + eslint-import-resolver-typescript: + optional: true + eslint-import-resolver-webpack: + optional: true + + eslint-plugin-import@2.32.0: + resolution: {integrity: sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==} + engines: {node: '>=4'} + peerDependencies: + '@typescript-eslint/parser': '*' + eslint: ^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8 || ^9 + peerDependenciesMeta: + '@typescript-eslint/parser': + optional: true + + eslint-plugin-jsx-a11y@6.10.2: + resolution: {integrity: sha512-scB3nz4WmG75pV8+3eRUQOHZlNSUhFNq37xnpgRkCCELU3XMvXAxLk1eqWWyE22Ki4Q01Fnsw9BA3cJHDPgn2Q==} + engines: {node: '>=4.0'} + peerDependencies: + eslint: ^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9 + + eslint-plugin-react-hooks@7.1.1: + resolution: {integrity: sha512-f2I7Gw6JbvCexzIInuSbZpfdQ44D7iqdWX01FKLvrPgqxoE7oMj8clOfto8U6vYiz4yd5oKu39rRSVOe1zRu0g==} + engines: {node: '>=18'} + peerDependencies: + eslint: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0 || ^10.0.0 + + eslint-plugin-react@7.37.5: + resolution: {integrity: sha512-Qteup0SqU15kdocexFNAJMvCJEfa2xUKNV4CC1xsVMrIIqEy3SQ/rqyxCWNzfrd3/ldy6HMlD2e0JDVpDg2qIA==} + engines: {node: '>=4'} + peerDependencies: + eslint: ^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9.7 + + eslint-scope@8.4.0: + resolution: {integrity: sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + eslint-visitor-keys@3.4.3: + resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + eslint-visitor-keys@4.2.1: + resolution: {integrity: sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + eslint-visitor-keys@5.0.1: + resolution: {integrity: sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + + eslint@9.39.5: + resolution: {integrity: sha512-DgZS62aPLXKlnxILS/AYCoRvHaZeXceIzlXPkkGGzJWSow1aEk0lbTlxUSlyjC8jcaKxAdOnTDz+o1JFSBsyjw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + deprecated: This version is no longer supported. Please see https://eslint.org/version-support for other options. + hasBin: true + peerDependencies: + jiti: '*' + peerDependenciesMeta: + jiti: + optional: true + + espree@10.4.0: + resolution: {integrity: sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + esquery@1.7.0: + resolution: {integrity: sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==} + engines: {node: '>=0.10'} + + esrecurse@4.3.0: + resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==} + engines: {node: '>=4.0'} + + estraverse@5.3.0: + resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} + engines: {node: '>=4.0'} + + estree-walker@3.0.3: + resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} + + esutils@2.0.3: + resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} + engines: {node: '>=0.10.0'} + + expect-type@1.4.0: + resolution: {integrity: sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==} + engines: {node: '>=12.0.0'} + + fast-deep-equal@3.1.3: + resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + + fast-glob@3.3.1: + resolution: {integrity: sha512-kNFPyjhh5cKjrUltxs+wFx+ZkbRaxxmZ+X0ZU31SOsxCEtP9VPgtq2teZw1DebupL5GmDaNQ6yKMMVcM41iqDg==} + engines: {node: '>=8.6.0'} + + fast-json-stable-stringify@2.1.0: + resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} + + fast-levenshtein@2.0.6: + resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} + + fastq@1.20.3: + resolution: {integrity: sha512-XKv5nnLs6nLF71NgiKJLIZFLkPyIEuOselLG7ujZnGrRfQK8HpvY+WqKhAJUAdLomwVHErVS4LfxFlPq0/FTAw==} + + fdir@6.5.0: + resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} + engines: {node: '>=12.0.0'} + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + + file-entry-cache@8.0.0: + resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==} + engines: {node: '>=16.0.0'} + + fill-range@7.1.1: + resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} + engines: {node: '>=8'} + + find-up@5.0.0: + resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} + engines: {node: '>=10'} + + flat-cache@4.0.1: + resolution: {integrity: sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==} + engines: {node: '>=16'} + + flatted@3.4.4: + resolution: {integrity: sha512-5+ybhBZANEJxaH3X5evAFatUxLfEHSr7n6kYJ+1Qd0mUqr4eu9gIf6GDbWHf8RJijHrjjO8G+la14SlL2SeS1Q==} + + for-each@0.3.5: + resolution: {integrity: sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==} + engines: {node: '>= 0.4'} + + fsevents@2.3.2: + resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + function-bind@1.1.2: + resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + + function.prototype.name@1.2.0: + resolution: {integrity: sha512-jObKIik1P2QjPHP5nz5BaOtUlfgS0fWo8IUByNXkM+o+02sJOi94em77GwJKQSJ3gfPHdgzLNrHc1uokV4P/ew==} + engines: {node: '>= 0.4'} + + functions-have-names@1.2.3: + resolution: {integrity: sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==} + + generator-function@2.0.1: + resolution: {integrity: sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g==} + engines: {node: '>= 0.4'} + + gensync@1.0.0-beta.2: + resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} + engines: {node: '>=6.9.0'} + + get-intrinsic@1.3.0: + resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} + engines: {node: '>= 0.4'} + + get-proto@1.0.1: + resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} + engines: {node: '>= 0.4'} + + get-symbol-description@1.1.0: + resolution: {integrity: sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==} + engines: {node: '>= 0.4'} + + get-tsconfig@4.14.3: + resolution: {integrity: sha512-++QEw4DIY7WGoukz+/+A/8dGYPT9l9yIadnmSgZ8Rjr3YVSVDipQSO9CdnJo9ePqFqUUqh+wk9uIaoiAwsiPkA==} + + glob-parent@5.1.2: + resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} + engines: {node: '>= 6'} + + glob-parent@6.0.2: + resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} + engines: {node: '>=10.13.0'} + + globals@14.0.0: + resolution: {integrity: sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==} + engines: {node: '>=18'} + + globals@16.4.0: + resolution: {integrity: sha512-ob/2LcVVaVGCYN+r14cnwnoDPUufjiYgSqRhiFD0Q1iI4Odora5RE8Iv1D24hAz5oMophRGkGz+yuvQmmUMnMw==} + engines: {node: '>=18'} + + globalthis@1.0.4: + resolution: {integrity: sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==} + engines: {node: '>= 0.4'} + + gopd@1.2.0: + resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} + engines: {node: '>= 0.4'} + + has-bigints@1.1.0: + resolution: {integrity: sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==} + engines: {node: '>= 0.4'} + + has-flag@4.0.0: + resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} + engines: {node: '>=8'} + + has-property-descriptors@1.0.2: + resolution: {integrity: sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==} + + has-proto@1.2.0: + resolution: {integrity: sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ==} + engines: {node: '>= 0.4'} + + has-symbols@1.1.0: + resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} + engines: {node: '>= 0.4'} + + has-tostringtag@1.0.2: + resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==} + engines: {node: '>= 0.4'} + + hasown@2.0.4: + resolution: {integrity: sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==} + engines: {node: '>= 0.4'} + + hermes-estree@0.25.1: + resolution: {integrity: sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw==} + + hermes-parser@0.25.1: + resolution: {integrity: sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==} + + html-encoding-sniffer@4.0.0: + resolution: {integrity: sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==} + engines: {node: '>=18'} + + html-encoding-sniffer@6.0.0: + resolution: {integrity: sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg==} + engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} + + http-proxy-agent@7.0.2: + resolution: {integrity: sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==} + engines: {node: '>= 14'} + + https-proxy-agent@7.0.6: + resolution: {integrity: sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==} + engines: {node: '>= 14'} + + iconv-lite@0.6.3: + resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} + engines: {node: '>=0.10.0'} + + ignore@5.3.2: + resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} + engines: {node: '>= 4'} + + ignore@7.0.10: + resolution: {integrity: sha512-HpbUakT7xp5miBUywCHf36ZEuAJNklBJDDsGpUIjMzOSmM8ELSfA9Sa/QDPeNeqeoN31u+UTCkL4klCOVvRm4Q==} + engines: {node: '>= 4'} + + import-fresh@3.3.1: + resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==} + engines: {node: '>=6'} + + imurmurhash@0.1.4: + resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} + engines: {node: '>=0.8.19'} + + indent-string@4.0.0: + resolution: {integrity: sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==} + engines: {node: '>=8'} + + internal-slot@1.1.0: + resolution: {integrity: sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==} + engines: {node: '>= 0.4'} + + is-array-buffer@3.0.5: + resolution: {integrity: sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==} + engines: {node: '>= 0.4'} + + is-async-function@2.1.1: + resolution: {integrity: sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ==} + engines: {node: '>= 0.4'} + + is-bigint@1.1.0: + resolution: {integrity: sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==} + engines: {node: '>= 0.4'} + + is-boolean-object@1.2.2: + resolution: {integrity: sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==} + engines: {node: '>= 0.4'} + + is-bun-module@2.0.0: + resolution: {integrity: sha512-gNCGbnnnnFAUGKeZ9PdbyeGYJqewpmc2aKHUEMO5nQPWU9lOmv7jcmQIv+qHD8fXW6W7qfuCwX4rY9LNRjXrkQ==} + + is-callable@1.2.7: + resolution: {integrity: sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==} + engines: {node: '>= 0.4'} + + is-core-module@2.17.0: + resolution: {integrity: sha512-J/vG0zBCbIKOQFfufSwyXdMrsohyJIUNkrnmo6WZGzoM7tr/lsbfW5b2BvisL6zsyMzK9UxV9L6c7AoFbyXHOA==} + engines: {node: '>= 0.4'} + + is-data-view@1.0.2: + resolution: {integrity: sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw==} + engines: {node: '>= 0.4'} + + is-date-object@1.1.0: + resolution: {integrity: sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==} + engines: {node: '>= 0.4'} + + is-document.all@1.0.0: + resolution: {integrity: sha512-+XSoyS05OdBbhFuELhgTCpFNHkpBOJqtsZfUFFpe5QTw+9Sjbh8zitxhQkYAo6wV7e1Vb8cAPvpCk9jGam/82g==} + engines: {node: '>= 0.4'} + + is-extglob@2.1.1: + resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} + engines: {node: '>=0.10.0'} + + is-finalizationregistry@1.1.1: + resolution: {integrity: sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg==} + engines: {node: '>= 0.4'} + + is-generator-function@1.1.2: + resolution: {integrity: sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA==} + engines: {node: '>= 0.4'} + + is-glob@4.0.3: + resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} + engines: {node: '>=0.10.0'} + + is-map@2.0.3: + resolution: {integrity: sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==} + engines: {node: '>= 0.4'} + + is-negative-zero@2.0.3: + resolution: {integrity: sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==} + engines: {node: '>= 0.4'} + + is-number-object@1.1.1: + resolution: {integrity: sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==} + engines: {node: '>= 0.4'} + + is-number@7.0.0: + resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} + engines: {node: '>=0.12.0'} + + is-potential-custom-element-name@1.0.1: + resolution: {integrity: sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==} + + is-regex@1.2.1: + resolution: {integrity: sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==} + engines: {node: '>= 0.4'} + + is-set@2.0.3: + resolution: {integrity: sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==} + engines: {node: '>= 0.4'} + + is-shared-array-buffer@1.0.4: + resolution: {integrity: sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==} + engines: {node: '>= 0.4'} + + is-string@1.1.1: + resolution: {integrity: sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==} + engines: {node: '>= 0.4'} + + is-symbol@1.1.1: + resolution: {integrity: sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==} + engines: {node: '>= 0.4'} + + is-typed-array@1.1.15: + resolution: {integrity: sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==} + engines: {node: '>= 0.4'} + + is-weakmap@2.0.2: + resolution: {integrity: sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==} + engines: {node: '>= 0.4'} + + is-weakref@1.1.1: + resolution: {integrity: sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew==} + engines: {node: '>= 0.4'} + + is-weakset@2.0.4: + resolution: {integrity: sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==} + engines: {node: '>= 0.4'} + + isarray@2.0.5: + resolution: {integrity: sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==} + + isexe@2.0.0: + resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + + iterator.prototype@1.1.5: + resolution: {integrity: sha512-H0dkQoCa3b2VEeKQBOxFph+JAbcrQdE7KC0UkqwpLmv2EC4P41QXP+rqo9wYodACiG5/WM5s9oDApTU8utwj9g==} + engines: {node: '>= 0.4'} + + js-tokens@4.0.0: + resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + + js-tokens@9.0.1: + resolution: {integrity: sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==} + + js-yaml@4.3.2: + resolution: {integrity: sha512-SFNOvSJ+Dgf/9An904Yx+CgSlIPCkIpao4qo51lpee25TIRejdH3rhR4EZMGoNx3/TP3O+wzWuiTFl4sqbltzA==} + hasBin: true + + jsdom@26.1.0: + resolution: {integrity: sha512-Cvc9WUhxSMEo4McES3P7oK3QaXldCfNWp7pl2NNeiIFlCoLr3kfq9kb1fxftiwk1FLV7CvpvDfonxtzUDeSOPg==} + engines: {node: '>=18'} + peerDependencies: + canvas: ^3.0.0 + peerDependenciesMeta: + canvas: + optional: true + + jsdom@27.4.0: + resolution: {integrity: sha512-mjzqwWRD9Y1J1KUi7W97Gja1bwOOM5Ug0EZ6UDK3xS7j7mndrkwozHtSblfomlzyB4NepioNt+B2sOSzczVgtQ==} + engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} + peerDependencies: + canvas: ^3.0.0 + peerDependenciesMeta: + canvas: + optional: true + + jsesc@3.1.0: + resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==} + engines: {node: '>=6'} + hasBin: true + + json-buffer@3.0.1: + resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} + + json-schema-traverse@0.4.1: + resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} + + json-stable-stringify-without-jsonify@1.0.1: + resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} + + json5@1.0.2: + resolution: {integrity: sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==} + hasBin: true + + json5@2.2.3: + resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} + engines: {node: '>=6'} + hasBin: true + + jsx-ast-utils@3.3.5: + resolution: {integrity: sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==} + engines: {node: '>=4.0'} + + keyv@4.5.4: + resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} + + language-subtag-registry@0.3.23: + resolution: {integrity: sha512-0K65Lea881pHotoGEa5gDlMxt3pctLi2RplBb7Ezh4rRdLEOtgi7n4EwK9lamnUCkKBqaeKRVebTq6BAxSkpXQ==} + + language-tags@1.0.9: + resolution: {integrity: sha512-MbjN408fEndfiQXbFQ1vnd+1NoLDsnQW41410oQBXiyXDMYH5z505juWa4KUE1LqxRC7DgOgZDbKLxHIwm27hA==} + engines: {node: '>=0.10'} + + levn@0.4.1: + resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} + engines: {node: '>= 0.8.0'} + + locate-path@6.0.0: + resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} + engines: {node: '>=10'} + + lodash.merge@4.6.2: + resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} + + loose-envify@1.4.0: + resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} + hasBin: true + + loupe@3.2.1: + resolution: {integrity: sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==} + + lru-cache@10.4.3: + resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} + + lru-cache@11.5.3: + resolution: {integrity: sha512-U4N8FgzmWxc8k1VH8Kr6lQg18U7Fjvby6wXHVRX/ZZ7IwWbRMgrRbP0Wrb5q5NVinryp4SQampHKdvtecItxUg==} + engines: {node: 20 || >=22} + + lru-cache@5.1.1: + resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} + + lz-string@1.5.0: + resolution: {integrity: sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==} + hasBin: true + + magic-string@0.30.21: + resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + + math-intrinsics@1.1.0: + resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} + engines: {node: '>= 0.4'} + + mdn-data@2.27.1: + resolution: {integrity: sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==} + + merge2@1.4.1: + resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} + engines: {node: '>= 8'} + + micromatch@4.0.8: + resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} + engines: {node: '>=8.6'} + + min-indent@1.0.1: + resolution: {integrity: sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==} + engines: {node: '>=4'} + + minimatch@10.2.6: + resolution: {integrity: sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==} + engines: {node: 18 || 20 || >=22} + + minimatch@3.1.5: + resolution: {integrity: sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==} + + minimist@1.2.8: + resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} + + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + + nanoid@3.3.19: + resolution: {integrity: sha512-Y2tUNy4ouw6tq5oDSKeQYGOyhkUBhNOcGV/02KC+6kd9eDGqdZd++mjMiIDilrBYvjEnCYvVtsuHCuP+okSfug==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + + napi-postinstall@0.3.4: + resolution: {integrity: sha512-PHI5f1O0EP5xJ9gQmFGMS6IZcrVvTjpXjz7Na41gTE7eE2hK11lg04CECCYEEjdc17EV4DO+fkGEtt7TpTaTiQ==} + engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0} + hasBin: true + + natural-compare@1.4.0: + resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} + + next@16.3.6: + resolution: {integrity: sha512-L+otWM/aQbYTx98aZhgEoMb4bZAXx1YVW4UMA/vuCyCoWG5HJyZUili8QAkqzrcC+5///tsz3s0M+SlyB5bLMw==} + engines: {node: '>=20.9.0'} + hasBin: true + peerDependencies: + '@opentelemetry/api': ^1.1.0 + '@playwright/test': ^1.51.1 + babel-plugin-react-compiler: '*' + react: ^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0 + react-dom: ^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0 + sass: ^1.3.0 + peerDependenciesMeta: + '@opentelemetry/api': + optional: true + '@playwright/test': + optional: true + babel-plugin-react-compiler: + optional: true + sass: + optional: true + + node-exports-info@1.6.2: + resolution: {integrity: sha512-kXs9Go0cah0qHVV2v389IXQLdLCeE1xfFtjOAF+iobu0OIoG1pje8At2vMHyaPMiPMnG/LWP50twML21eMcAag==} + engines: {node: '>= 0.4'} + + node-releases@2.0.57: + resolution: {integrity: sha512-kQK9LGGFiHtrWiNhZtA7Qbw17AQz+dmsEKODRIVTXA9+e5MS/2gZEBhYJt13GrAz5/IOZKddH/0Z3TP/Zgo+yw==} + engines: {node: '>=18'} + + nwsapi@2.2.28: + resolution: {integrity: sha512-IlVB7OS7qrOsVYlpnFIkETjMwT9jwvmocJmmM+GZU/PAB3uGi9Ezd7vcWhWBUnSc0ya4ppmQITOyP1ez9gg8cg==} + + object-assign@4.1.1: + resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} + engines: {node: '>=0.10.0'} + + object-inspect@1.13.4: + resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==} + engines: {node: '>= 0.4'} + + object-keys@1.1.1: + resolution: {integrity: sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==} + engines: {node: '>= 0.4'} + + object.assign@4.1.7: + resolution: {integrity: sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==} + engines: {node: '>= 0.4'} + + object.entries@1.1.9: + resolution: {integrity: sha512-8u/hfXFRBD1O0hPUjioLhoWFHRmt6tKA4/vZPyckBr18l1KE9uHrFaFaUi8MDRTpi4uak2goyPTSNJLXX2k2Hw==} + engines: {node: '>= 0.4'} + + object.fromentries@2.0.8: + resolution: {integrity: sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ==} + engines: {node: '>= 0.4'} + + object.groupby@1.0.3: + resolution: {integrity: sha512-+Lhy3TQTuzXI5hevh8sBGqbmurHbbIjAi0Z4S63nthVLmLxfbj4T54a4CfZrXIrt9iP4mVAPYMo/v99taj3wjQ==} + engines: {node: '>= 0.4'} + + object.values@1.2.1: + resolution: {integrity: sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==} + engines: {node: '>= 0.4'} + + optionator@0.9.4: + resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} + engines: {node: '>= 0.8.0'} + + own-keys@1.0.2: + resolution: {integrity: sha512-19YVAg7T+WTrxggPukVq7DjTv6+PJ867TmhCvBsYwmbFCsZd344rq2Ld1p0wo8f8Qrrhgp82c6FJRqdXWtSEhg==} + engines: {node: '>= 0.4'} + + p-limit@3.1.0: + resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} + engines: {node: '>=10'} + + p-locate@5.0.0: + resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} + engines: {node: '>=10'} + + parent-module@1.0.1: + resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} + engines: {node: '>=6'} + + parse5@7.3.0: + resolution: {integrity: sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==} + + parse5@8.0.1: + resolution: {integrity: sha512-z1e/HMG90obSGeidlli3hj7cbocou0/wa5HacvI3ASx34PecNjNQeaHNo5WIZpWofN9kgkqV1q5YvXe3F0FoPw==} + + path-exists@4.0.0: + resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} + engines: {node: '>=8'} + + path-key@3.1.1: + resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} + engines: {node: '>=8'} + + path-parse@1.0.7: + resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} + + pathe@2.0.3: + resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} + + pathval@2.0.1: + resolution: {integrity: sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==} + engines: {node: '>= 14.16'} + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + picomatch@2.3.2: + resolution: {integrity: sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==} + engines: {node: '>=8.6'} + + picomatch@4.0.7: + resolution: {integrity: sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==} + engines: {node: '>=12'} + + playwright-core@1.55.1: + resolution: {integrity: sha512-Z6Mh9mkwX+zxSlHqdr5AOcJnfp+xUWLCt9uKV18fhzA8eyxUd8NUWzAjxUh55RZKSYwDGX0cfaySdhZJGMoJ+w==} + engines: {node: '>=18'} + hasBin: true + + playwright@1.55.1: + resolution: {integrity: sha512-cJW4Xd/G3v5ovXtJJ52MAOclqeac9S/aGGgRzLabuF8TnIb6xHvMzKIa6JmrRzUkeXJgfL1MhukP0NK6l39h3A==} + engines: {node: '>=18'} + hasBin: true + + possible-typed-array-names@1.1.0: + resolution: {integrity: sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==} + engines: {node: '>= 0.4'} + + postcss@8.5.23: + resolution: {integrity: sha512-g50586zr4bZmwFiTlflMu8E0bDTb5I5gertgwAKmsdUlTQIhZtunzUlD1WSzwcVWPoAVpsrA6vlfCD7oXvRwgg==} + engines: {node: ^10 || ^12 || >=14} + + postcss@8.5.28: + resolution: {integrity: sha512-RRuzqDtt5Y9h3quz5hWhK+TPnsmVs6WwSU6LkJMeY4HstUEDuYTG8UJSdawMRzmzAtV+KEoG8N3Qg2qLy5vM/A==} + engines: {node: ^10 || ^12 || >=14} + + prelude-ls@1.2.1: + resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} + engines: {node: '>= 0.8.0'} + + pretty-format@27.5.1: + resolution: {integrity: sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==} + engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} + + prop-types@15.8.1: + resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==} + + punycode@2.3.1: + resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} + engines: {node: '>=6'} + + queue-microtask@1.2.3: + resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} + + react-dom@19.3.0: + resolution: {integrity: sha512-JDk8dgif51OjFoDE70+OT9ICyYr+69HlmihNwp1+Nsfbna3t5sIiCa9ZJktDmQ4/1b/rn26hIAR2uYXDMr5r0Q==} + peerDependencies: + react: ^19.3.0 + + react-is@16.13.1: + resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==} + + react-is@17.0.2: + resolution: {integrity: sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==} + + react@19.3.0: + resolution: {integrity: sha512-E8LUcbtBWt20bbl2YoHfx4ZDBdxVTfOKtCZn9cDSJ4l6/nuoApcpIBcj47t2wZoVX8g2ZHuMHbiShgCR1T5Sog==} + engines: {node: '>=0.10.0'} + + redent@3.0.0: + resolution: {integrity: sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==} + engines: {node: '>=8'} + + reflect.getprototypeof@1.0.10: + resolution: {integrity: sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==} + engines: {node: '>= 0.4'} + + regexp.prototype.flags@1.5.4: + resolution: {integrity: sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==} + engines: {node: '>= 0.4'} + + require-from-string@2.0.2: + resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} + engines: {node: '>=0.10.0'} + + resolve-from@4.0.0: + resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} + engines: {node: '>=4'} + + resolve-pkg-maps@1.0.0: + resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} + + resolve@2.0.0-next.7: + resolution: {integrity: sha512-tqt+NBWwyaMgw3zDsnygx4CByWjQEJHOPMdslYhppaQSJUtL/D4JO9CcBBlhPoI8lz9oJIDXkwXfhF4aWqP8xQ==} + engines: {node: '>= 0.4'} + hasBin: true + + reusify@1.1.0: + resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} + engines: {iojs: '>=1.0.0', node: '>=0.10.0'} + + rollup@4.63.5: + resolution: {integrity: sha512-KRWwmNLlPw5M7HcdYfm15oBv9n9LPtjzpzCIxS/phwqvPyxHSoKX6Y2YU3pxSPfy0CLquVgsx/j/hBi6OvH1Nw==} + engines: {node: '>=18.0.0', npm: '>=8.0.0'} + hasBin: true + + rrweb-cssom@0.8.0: + resolution: {integrity: sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw==} + + run-parallel@1.2.0: + resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} + + safe-array-concat@1.1.4: + resolution: {integrity: sha512-wtZlHyOje6OZTGqAoaDKxFkgRtkF9CnHAVnCHKfuj200wAgL+bSJhdsCD2l0Qx/2ekEXjPWcyKkfGb5CPboslg==} + engines: {node: '>=0.4'} + + safe-push-apply@1.0.0: + resolution: {integrity: sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA==} + engines: {node: '>= 0.4'} + + safe-regex-test@1.1.0: + resolution: {integrity: sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==} + engines: {node: '>= 0.4'} + + safer-buffer@2.1.2: + resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + + saxes@6.0.0: + resolution: {integrity: sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==} + engines: {node: '>=v12.22.7'} + + scheduler@0.28.0: + resolution: {integrity: sha512-juorfCmIkIw8tT+p5BXSm6PJjQF/ycEYmKyzURCIt/RaZIhL+PulbQ9Yu2z1HdOJDdqDTlxA1+xKBmHXJsczAw==} + + semver@6.3.1: + resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} + hasBin: true + + semver@7.8.5: + resolution: {integrity: sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==} + engines: {node: '>=10'} + hasBin: true + + server-only@0.0.1: + resolution: {integrity: sha512-qepMx2JxAa5jjfzxG79yPPq+8BuFToHd1hm7kI+Z4zAq1ftQiP7HcxMhDDItrbtwVeLg/cY2JnKnrcFkmiswNA==} + + set-function-length@1.2.2: + resolution: {integrity: sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==} + engines: {node: '>= 0.4'} + + set-function-name@2.0.2: + resolution: {integrity: sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==} + engines: {node: '>= 0.4'} + + set-proto@1.0.0: + resolution: {integrity: sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==} + engines: {node: '>= 0.4'} + + sharp@0.35.5: + resolution: {integrity: sha512-Ywn4OnzGukp7CDMrp08RQ50YKmuwG47brZgIVPTvBaaAfQlRlygrRqSrxdCiL9M+LlzLBiJ68IR1QqvzHyjC7g==} + engines: {node: '>=20.9.0'} + peerDependencies: + '@types/node': '*' + peerDependenciesMeta: + '@types/node': + optional: true + + shebang-command@2.0.0: + resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} + engines: {node: '>=8'} + + shebang-regex@3.0.0: + resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} + engines: {node: '>=8'} + + side-channel-list@1.0.1: + resolution: {integrity: sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==} + engines: {node: '>= 0.4'} + + side-channel-map@1.0.1: + resolution: {integrity: sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==} + engines: {node: '>= 0.4'} + + side-channel-weakmap@1.0.2: + resolution: {integrity: sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==} + engines: {node: '>= 0.4'} + + side-channel@1.1.1: + resolution: {integrity: sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==} + engines: {node: '>= 0.4'} + + siginfo@2.0.0: + resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} + + source-map-js@1.2.1: + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + engines: {node: '>=0.10.0'} + + stable-hash@0.0.5: + resolution: {integrity: sha512-+L3ccpzibovGXFK+Ap/f8LOS0ahMrHTf3xu7mMLSpEGU0EO9ucaysSylKo9eRDFNhWve/y275iPmIZ4z39a9iA==} + + stackback@0.0.2: + resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} + + std-env@3.10.0: + resolution: {integrity: sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==} + + stop-iteration-iterator@1.1.0: + resolution: {integrity: sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==} + engines: {node: '>= 0.4'} + + string.prototype.includes@2.0.1: + resolution: {integrity: sha512-o7+c9bW6zpAdJHTtujeePODAhkuicdAryFsfVKwA+wGw89wJ4GTY484WTucM9hLtDEOpOvI+aHnzqnC5lHp4Rg==} + engines: {node: '>= 0.4'} + + string.prototype.matchall@4.1.0: + resolution: {integrity: sha512-tHNHTxInrYLCga9O9YGxWA3G9/nnzQw8UGAyqGx3Ar1pSTTzIuM4woFSq4SowkXCjJIwq5sIiQvEfRI9tCH1qQ==} + engines: {node: '>= 0.4'} + + string.prototype.repeat@1.0.0: + resolution: {integrity: sha512-0u/TldDbKD8bFCQ/4f5+mNRrXwZ8hg2w7ZR8wa16e8z9XpePWl3eGEcUD0OXpEH/VJH/2G3gjUtR3ZOiBe2S/w==} + + string.prototype.trim@1.2.11: + resolution: {integrity: sha512-PwvK7BU+CMTJGYQCTZb5RWXIML92lftJLhQz1tBzgKiqGxJaMlBAa48POXaNAC2s4y8jr3EFqrkF9+44neS46w==} + engines: {node: '>= 0.4'} + + string.prototype.trimend@1.0.10: + resolution: {integrity: sha512-2+3aDAOmPTmuFwjDnmJG2ctEkQKVki7vOSqaxkv42Mowj1V6PnvuwFCRrR5lChUux1TBskPjfkeTOhqczDMxTw==} + engines: {node: '>= 0.4'} + + string.prototype.trimstart@1.0.8: + resolution: {integrity: sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==} + engines: {node: '>= 0.4'} + + strip-bom@3.0.0: + resolution: {integrity: sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==} + engines: {node: '>=4'} + + strip-indent@3.0.0: + resolution: {integrity: sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==} + engines: {node: '>=8'} + + strip-json-comments@3.1.1: + resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} + engines: {node: '>=8'} + + strip-literal@3.1.0: + resolution: {integrity: sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg==} + + styled-jsx@5.1.6: + resolution: {integrity: sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA==} + engines: {node: '>= 12.0.0'} + peerDependencies: + '@babel/core': '*' + babel-plugin-macros: '*' + react: '>= 16.8.0 || 17.x.x || ^18.0.0-0 || ^19.0.0-0' + peerDependenciesMeta: + '@babel/core': + optional: true + babel-plugin-macros: + optional: true + + supports-color@7.2.0: + resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} + engines: {node: '>=8'} + + supports-preserve-symlinks-flag@1.0.0: + resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} + engines: {node: '>= 0.4'} + + symbol-tree@3.2.4: + resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==} + + tinybench@2.9.0: + resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} + + tinyexec@0.3.2: + resolution: {integrity: sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==} + + tinyglobby@0.2.17: + resolution: {integrity: sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==} + engines: {node: '>=12.0.0'} + + tinypool@1.1.1: + resolution: {integrity: sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==} + engines: {node: ^18.0.0 || >=20.0.0} + + tinyrainbow@2.0.0: + resolution: {integrity: sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==} + engines: {node: '>=14.0.0'} + + tinyrainbow@3.1.1: + resolution: {integrity: sha512-yau8yJdTt989Mm0Bd/236QnzEiPf2xLLTqUZRUJOo/3CB078LSwzei343DgtJVmfJKJE3TMINY1u42SQsP6mXw==} + engines: {node: '>=14.0.0'} + + tinyspy@4.0.6: + resolution: {integrity: sha512-u8KszXvGfU68hVcZpRHKG28T0krMuv2G5nDhiHaMLen/gIuFEgIJhaJuO69qjnXg5paSrbPMFfx3brNuN8eVSg==} + engines: {node: '>=14.0.0'} + + tldts-core@6.1.86: + resolution: {integrity: sha512-Je6p7pkk+KMzMv2XXKmAE3McmolOQFdxkKw0R8EYNr7sELW46JqnNeTX8ybPiQgvg1ymCoF8LXs5fzFaZvJPTA==} + + tldts-core@7.4.16: + resolution: {integrity: sha512-MDolfaSJtlSK5Y0A1xl3277ekubZwobpBjugknDizI9O5Rm60a1m8k4ICK+MRsCDzPygT81mp3BBf5RKDlFRfA==} + + tldts@6.1.86: + resolution: {integrity: sha512-WMi/OQ2axVTf/ykqCQgXiIct+mSQDFdH2fkwhPwgEwvJ1kSzZRiinb0zF2Xb8u4+OqPChmyI6MEu4EezNJz+FQ==} + hasBin: true + + tldts@7.4.16: + resolution: {integrity: sha512-QwBER5KMR86IIjpIiO7H/Z3IMJPsZ1A6RKPAqzTTgOyUQUSt9FdnKcqhTaJmkY6HVrgouZHZR0ncK5QxvmnQeg==} + hasBin: true + + to-regex-range@5.0.1: + resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} + engines: {node: '>=8.0'} + + tough-cookie@5.1.2: + resolution: {integrity: sha512-FVDYdxtnj0G6Qm/DhNPSb8Ju59ULcup3tuJxkFb5K8Bv2pUXILbf0xZWU8PX8Ov19OXljbUyveOFwRMwkXzO+A==} + engines: {node: '>=16'} + + tough-cookie@6.0.2: + resolution: {integrity: sha512-exgYmnmL/sJpR3upZfXG5PoatXQii55xAiXGXzY+sROLZ/Y+SLcp9PgJNI9Vz37HpQ74WvDcLT8eqm+kV3FzrA==} + engines: {node: '>=16'} + + tr46@5.1.1: + resolution: {integrity: sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==} + engines: {node: '>=18'} + + tr46@6.0.0: + resolution: {integrity: sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw==} + engines: {node: '>=20'} + + ts-api-utils@2.5.0: + resolution: {integrity: sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==} + engines: {node: '>=18.12'} + peerDependencies: + typescript: '>=4.8.4' + + tsconfig-paths@3.15.0: + resolution: {integrity: sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg==} + + tslib@2.8.1: + resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + + type-check@0.4.0: + resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} + engines: {node: '>= 0.8.0'} + + typed-array-buffer@1.0.3: + resolution: {integrity: sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==} + engines: {node: '>= 0.4'} + + typed-array-byte-length@1.0.3: + resolution: {integrity: sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg==} + engines: {node: '>= 0.4'} + + typed-array-byte-offset@1.0.5: + resolution: {integrity: sha512-0FHJvLPqZ7KJzp17O13jfsAjsqazgrxBu2zEK95PmUz8lv2+GjRuxUInCr2Rk9Dms3ihN21zJ929ZO43yJ95QQ==} + engines: {node: '>= 0.4'} + + typed-array-length@1.0.8: + resolution: {integrity: sha512-phPGCwqr2+Qo0fwniCE8e4pKnGu/yFb5nD5Y8bf0EEeiI5GklnACYA9GFy/DrAeRrKHXvHn+1SUsOWgJp6RO+g==} + engines: {node: '>= 0.4'} + + typescript-eslint@8.71.0: + resolution: {integrity: sha512-fBdHYiqQ14RW6mOMXD14Svn82ZsCYAoQSzGRzyEjR59S5A2Krh/l7fGTOQ7iCr8gGy/mHVXtEF7s5fgjEdV0Pw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.1.0' + + typescript@5.4.5: + resolution: {integrity: sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ==} + engines: {node: '>=14.17'} + hasBin: true + + typescript@5.9.3: + resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} + engines: {node: '>=14.17'} + hasBin: true + + unbox-primitive@1.1.0: + resolution: {integrity: sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==} + engines: {node: '>= 0.4'} + + undici-types@6.21.0: + resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} + + unrs-resolver@1.12.2: + resolution: {integrity: sha512-dmlRxBJJayXjqTwC+JtF1HhJmgf3ftQ3YejFcZrf4+KKtJv0qDsK1pjqaaVjG7wJ5NJ6UVP1OqRMQ71Z4C3rxQ==} + + update-browserslist-db@1.3.3: + resolution: {integrity: sha512-pJ2sYawQS0R/WI928Gj5GlPhTGzbMelq0+4INtSYNDV9ErKJcX6xjGWkoG/VnB3dpUm00zALaqkrUD77pO5TDQ==} + hasBin: true + peerDependencies: + browserslist: '>= 4.21.0' + + uri-js@4.4.1: + resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} + + vite-node@3.2.4: + resolution: {integrity: sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==} + engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} + hasBin: true + + vite@7.3.6: + resolution: {integrity: sha512-4XP60spRGjSZFf1qYH+dJIkK2znL3zQfl9KkOV9MkkRR/3Dls0dxaBsQPTloEc5BLXWPL9vsOxopxyKoMmDueg==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + peerDependencies: + '@types/node': ^20.19.0 || >=22.12.0 + jiti: '>=1.21.0' + less: ^4.0.0 + lightningcss: ^1.21.0 + sass: ^1.70.0 + sass-embedded: ^1.70.0 + stylus: '>=0.54.8' + sugarss: ^5.0.0 + terser: ^5.16.0 + tsx: ^4.8.1 + yaml: ^2.4.2 + peerDependenciesMeta: + '@types/node': + optional: true + jiti: + optional: true + less: + optional: true + lightningcss: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + tsx: + optional: true + yaml: + optional: true + + vitest@3.2.7: + resolution: {integrity: sha512-KrxIJ62Fd89gfysR4WotlgZABiz2dqFPgqGzX7s+CwsqLFomRH7777ZcrOD6+WVAh7khPQP41A+BKbpcJFrdEg==} + engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} + hasBin: true + peerDependencies: + '@edge-runtime/vm': '*' + '@types/debug': ^4.1.12 + '@types/node': ^18.0.0 || ^20.0.0 || >=22.0.0 + '@vitest/browser': 3.2.7 + '@vitest/ui': 3.2.7 + happy-dom: '*' + jsdom: '*' + peerDependenciesMeta: + '@edge-runtime/vm': + optional: true + '@types/debug': + optional: true + '@types/node': + optional: true + '@vitest/browser': + optional: true + '@vitest/ui': + optional: true + happy-dom: + optional: true + jsdom: + optional: true + + vitest@4.0.7: + resolution: {integrity: sha512-xQroKAadK503CrmbzCISvQUjeuvEZzv6U0wlnlVFOi5i3gnzfH4onyQ29f3lzpe0FresAiTAd3aqK0Bi/jLI8w==} + engines: {node: ^20.0.0 || ^22.0.0 || >=24.0.0} + hasBin: true + peerDependencies: + '@edge-runtime/vm': '*' + '@types/debug': ^4.1.12 + '@types/node': ^20.0.0 || ^22.0.0 || >=24.0.0 + '@vitest/browser-playwright': 4.0.7 + '@vitest/browser-preview': 4.0.7 + '@vitest/browser-webdriverio': 4.0.7 + '@vitest/ui': 4.0.7 + happy-dom: '*' + jsdom: '*' + peerDependenciesMeta: + '@edge-runtime/vm': + optional: true + '@types/debug': + optional: true + '@types/node': + optional: true + '@vitest/browser-playwright': + optional: true + '@vitest/browser-preview': + optional: true + '@vitest/browser-webdriverio': + optional: true + '@vitest/ui': + optional: true + happy-dom: + optional: true + jsdom: + optional: true + + w3c-xmlserializer@5.0.0: + resolution: {integrity: sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==} + engines: {node: '>=18'} + + webidl-conversions@7.0.0: + resolution: {integrity: sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==} + engines: {node: '>=12'} + + webidl-conversions@8.0.1: + resolution: {integrity: sha512-BMhLD/Sw+GbJC21C/UgyaZX41nPt8bUTg+jWyDeg7e7YN4xOM05YPSIXceACnXVtqyEw/LMClUQMtMZ+PGGpqQ==} + engines: {node: '>=20'} + + whatwg-encoding@3.1.1: + resolution: {integrity: sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==} + engines: {node: '>=18'} + deprecated: Use @exodus/bytes instead for a more spec-conformant and faster implementation + + whatwg-mimetype@4.0.0: + resolution: {integrity: sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==} + engines: {node: '>=18'} + + whatwg-mimetype@5.0.0: + resolution: {integrity: sha512-sXcNcHOC51uPGF0P/D4NVtrkjSU2fNsm9iog4ZvZJsL3rjoDAzXZhkm2MWt1y+PUdggKAYVoMAIYcs78wJ51Cw==} + engines: {node: '>=20'} + + whatwg-url@14.2.0: + resolution: {integrity: sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==} + engines: {node: '>=18'} + + whatwg-url@15.1.0: + resolution: {integrity: sha512-2ytDk0kiEj/yu90JOAp44PVPUkO9+jVhyf+SybKlRHSDlvOOZhdPIrr7xTH64l4WixO2cP+wQIcgujkGBPPz6g==} + engines: {node: '>=20'} + + which-boxed-primitive@1.1.1: + resolution: {integrity: sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==} + engines: {node: '>= 0.4'} + + which-builtin-type@1.2.1: + resolution: {integrity: sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q==} + engines: {node: '>= 0.4'} + + which-collection@1.0.2: + resolution: {integrity: sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==} + engines: {node: '>= 0.4'} + + which-typed-array@1.1.24: + resolution: {integrity: sha512-wk4Mf4pR5mRP7eYuuTBCIQ9d0ud2Fv2jRLQpfgnRjbOxAFHmjKFValgTpitVKzJJS8ajnYQV2Du1SZ8j6b/EUQ==} + engines: {node: '>= 0.4'} + + which@2.0.2: + resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} + engines: {node: '>= 8'} + hasBin: true + + why-is-node-running@2.3.0: + resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==} + engines: {node: '>=8'} + hasBin: true + + word-wrap@1.2.5: + resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} + engines: {node: '>=0.10.0'} + + ws@8.22.0: + resolution: {integrity: sha512-Ydggc987+RO0AnWtZ/7Wq9FtNvcrL1b/RO0ud9mWjUPgDrsAAwQSF51sm2hm1XofbU/4jkpGEsLFsZZxU+1DOg==} + engines: {node: '>=10.0.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: '>=5.0.2' + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + + xml-name-validator@5.0.0: + resolution: {integrity: sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==} + engines: {node: '>=18'} + + xmlchars@2.2.0: + resolution: {integrity: sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==} + + yallist@3.1.1: + resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} + + yocto-queue@0.1.0: + resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} + engines: {node: '>=10'} + + zod-validation-error@4.0.2: + resolution: {integrity: sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ==} + engines: {node: '>=18.0.0'} + peerDependencies: + zod: ^3.25.0 || ^4.0.0 + + zod@4.6.5: + resolution: {integrity: sha512-v5l/aFXZQeai4awLbOpSoHecE9UiMrnfx75tEXLjNonXVARxQ5mOeipTjROUchszUNCqnE+hqAMujRsRHsut2Q==} + +snapshots: + + '@acemir/cssom@0.9.31': + optional: true + + '@adobe/css-tools@4.5.0': {} + + '@asamuzakjp/css-color@3.2.0': + dependencies: + '@csstools/css-calc': 2.1.4(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) + '@csstools/css-color-parser': 3.1.0(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) + '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) + '@csstools/css-tokenizer': 3.0.4 + lru-cache: 10.4.3 + + '@asamuzakjp/css-color@4.1.2': + dependencies: + '@csstools/css-calc': 3.4.1(@csstools/css-parser-algorithms@4.0.1(@csstools/css-tokenizer@4.0.2))(@csstools/css-tokenizer@4.0.2) + '@csstools/css-color-parser': 4.2.4(@csstools/css-parser-algorithms@4.0.1(@csstools/css-tokenizer@4.0.2))(@csstools/css-tokenizer@4.0.2) + '@csstools/css-parser-algorithms': 4.0.1(@csstools/css-tokenizer@4.0.2) + '@csstools/css-tokenizer': 4.0.2 + lru-cache: 11.5.3 + optional: true + + '@asamuzakjp/dom-selector@6.8.1': + dependencies: + '@asamuzakjp/nwsapi': 2.3.9 + bidi-js: 1.1.0 + css-tree: 3.2.1 + is-potential-custom-element-name: 1.0.1 + lru-cache: 11.5.3 + optional: true + + '@asamuzakjp/nwsapi@2.3.9': + optional: true + + '@axe-core/playwright@4.10.2(playwright-core@1.55.1)': + dependencies: + axe-core: 4.10.3 + playwright-core: 1.55.1 + + '@babel/code-frame@7.29.7': + dependencies: + '@babel/helper-validator-identifier': 7.29.7 + js-tokens: 4.0.0 + picocolors: 1.1.1 + + '@babel/compat-data@7.29.7': {} + + '@babel/core@7.29.7': + dependencies: + '@babel/code-frame': 7.29.7 + '@babel/generator': 7.29.8 + '@babel/helper-compilation-targets': 7.29.7 + '@babel/helper-module-transforms': 7.29.7(@babel/core@7.29.7) + '@babel/helpers': 7.29.7 + '@babel/parser': 7.29.9 + '@babel/template': 7.29.7 + '@babel/traverse': 7.29.8 + '@babel/types': 7.29.8 + '@jridgewell/remapping': 2.3.5 + convert-source-map: 2.0.0 + debug: 4.4.3 + gensync: 1.0.0-beta.2 + json5: 2.2.3 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + + '@babel/generator@7.29.8': + dependencies: + '@babel/parser': 7.29.9 + '@babel/types': 7.29.8 + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + jsesc: 3.1.0 + + '@babel/helper-compilation-targets@7.29.7': + dependencies: + '@babel/compat-data': 7.29.7 + '@babel/helper-validator-option': 7.29.7 + browserslist: 4.29.2 + lru-cache: 5.1.1 + semver: 6.3.1 + + '@babel/helper-globals@7.29.7': {} + + '@babel/helper-module-imports@7.29.7': + dependencies: + '@babel/traverse': 7.29.8 + '@babel/types': 7.29.8 + transitivePeerDependencies: + - supports-color + + '@babel/helper-module-transforms@7.29.7(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-module-imports': 7.29.7 + '@babel/helper-validator-identifier': 7.29.7 + '@babel/traverse': 7.29.8 + transitivePeerDependencies: + - supports-color + + '@babel/helper-string-parser@7.29.7': {} + + '@babel/helper-validator-identifier@7.29.7': {} + + '@babel/helper-validator-option@7.29.7': {} + + '@babel/helpers@7.29.7': + dependencies: + '@babel/template': 7.29.7 + '@babel/types': 7.29.8 + + '@babel/parser@7.29.9': + dependencies: + '@babel/types': 7.29.8 + + '@babel/runtime@7.29.7': {} + + '@babel/template@7.29.7': + dependencies: + '@babel/code-frame': 7.29.7 + '@babel/parser': 7.29.9 + '@babel/types': 7.29.8 + + '@babel/traverse@7.29.8': + dependencies: + '@babel/code-frame': 7.29.7 + '@babel/generator': 7.29.8 + '@babel/helper-globals': 7.29.7 + '@babel/parser': 7.29.9 + '@babel/template': 7.29.7 + '@babel/types': 7.29.8 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + + '@babel/types@7.29.8': + dependencies: + '@babel/helper-string-parser': 7.29.7 + '@babel/helper-validator-identifier': 7.29.7 + + '@bufbuild/buf-darwin-arm64@1.73.0': + optional: true + + '@bufbuild/buf-darwin-x64@1.73.0': + optional: true + + '@bufbuild/buf-linux-aarch64@1.73.0': + optional: true + + '@bufbuild/buf-linux-armv7@1.73.0': + optional: true + + '@bufbuild/buf-linux-x64@1.73.0': + optional: true + + '@bufbuild/buf-win32-arm64@1.73.0': + optional: true + + '@bufbuild/buf-win32-x64@1.73.0': + optional: true + + '@bufbuild/buf@1.73.0': + optionalDependencies: + '@bufbuild/buf-darwin-arm64': 1.73.0 + '@bufbuild/buf-darwin-x64': 1.73.0 + '@bufbuild/buf-linux-aarch64': 1.73.0 + '@bufbuild/buf-linux-armv7': 1.73.0 + '@bufbuild/buf-linux-x64': 1.73.0 + '@bufbuild/buf-win32-arm64': 1.73.0 + '@bufbuild/buf-win32-x64': 1.73.0 + + '@bufbuild/protobuf@2.15.0': {} + + '@bufbuild/protoc-gen-es@2.15.0(@bufbuild/protobuf@2.15.0)': + dependencies: + '@bufbuild/protoplugin': 2.15.0 + optionalDependencies: + '@bufbuild/protobuf': 2.15.0 + transitivePeerDependencies: + - supports-color + + '@bufbuild/protoplugin@2.15.0': + dependencies: + '@bufbuild/protobuf': 2.15.0 + '@typescript/vfs': 1.6.5(typescript@5.4.5) + typescript: 5.4.5 + transitivePeerDependencies: + - supports-color + + '@connectrpc/connect-node@2.1.1(@bufbuild/protobuf@2.15.0)(@connectrpc/connect@2.1.1(@bufbuild/protobuf@2.15.0))': + dependencies: + '@bufbuild/protobuf': 2.15.0 + '@connectrpc/connect': 2.1.1(@bufbuild/protobuf@2.15.0) + + '@connectrpc/connect-node@2.2.0(@bufbuild/protobuf@2.15.0)(@connectrpc/connect@2.2.0(@bufbuild/protobuf@2.15.0))': + dependencies: + '@bufbuild/protobuf': 2.15.0 + '@connectrpc/connect': 2.2.0(@bufbuild/protobuf@2.15.0) + + '@connectrpc/connect@2.1.1(@bufbuild/protobuf@2.15.0)': + dependencies: + '@bufbuild/protobuf': 2.15.0 + + '@connectrpc/connect@2.2.0(@bufbuild/protobuf@2.15.0)': + dependencies: + '@bufbuild/protobuf': 2.15.0 + + '@csstools/color-helpers@5.1.0': {} + + '@csstools/color-helpers@6.1.2': + optional: true + + '@csstools/css-calc@2.1.4(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4)': + dependencies: + '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) + '@csstools/css-tokenizer': 3.0.4 + + '@csstools/css-calc@3.4.1(@csstools/css-parser-algorithms@4.0.1(@csstools/css-tokenizer@4.0.2))(@csstools/css-tokenizer@4.0.2)': + dependencies: + '@csstools/css-parser-algorithms': 4.0.1(@csstools/css-tokenizer@4.0.2) + '@csstools/css-tokenizer': 4.0.2 + optional: true + + '@csstools/css-color-parser@3.1.0(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4)': + dependencies: + '@csstools/color-helpers': 5.1.0 + '@csstools/css-calc': 2.1.4(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) + '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) + '@csstools/css-tokenizer': 3.0.4 + + '@csstools/css-color-parser@4.2.4(@csstools/css-parser-algorithms@4.0.1(@csstools/css-tokenizer@4.0.2))(@csstools/css-tokenizer@4.0.2)': + dependencies: + '@csstools/color-helpers': 6.1.2 + '@csstools/css-calc': 3.4.1(@csstools/css-parser-algorithms@4.0.1(@csstools/css-tokenizer@4.0.2))(@csstools/css-tokenizer@4.0.2) + '@csstools/css-parser-algorithms': 4.0.1(@csstools/css-tokenizer@4.0.2) + '@csstools/css-tokenizer': 4.0.2 + optional: true + + '@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4)': + dependencies: + '@csstools/css-tokenizer': 3.0.4 + + '@csstools/css-parser-algorithms@4.0.1(@csstools/css-tokenizer@4.0.2)': + dependencies: + '@csstools/css-tokenizer': 4.0.2 + optional: true + + '@csstools/css-syntax-patches-for-csstree@1.1.14(css-tree@3.2.1)': + optionalDependencies: + css-tree: 3.2.1 + optional: true + + '@csstools/css-tokenizer@3.0.4': {} + + '@csstools/css-tokenizer@4.0.2': + optional: true + + '@emnapi/core@1.10.0': + dependencies: + '@emnapi/wasi-threads': 1.2.1 + tslib: 2.8.1 + optional: true + + '@emnapi/runtime@1.10.0': + dependencies: + tslib: 2.8.1 + optional: true + + '@emnapi/runtime@1.11.3': + dependencies: + tslib: 2.8.1 + optional: true + + '@emnapi/wasi-threads@1.2.1': + dependencies: + tslib: 2.8.1 + optional: true + + '@esbuild/aix-ppc64@0.28.2': + optional: true + + '@esbuild/android-arm64@0.28.2': + optional: true + + '@esbuild/android-arm@0.28.2': + optional: true + + '@esbuild/android-x64@0.28.2': + optional: true + + '@esbuild/darwin-arm64@0.28.2': + optional: true + + '@esbuild/darwin-x64@0.28.2': + optional: true + + '@esbuild/freebsd-arm64@0.28.2': + optional: true + + '@esbuild/freebsd-x64@0.28.2': + optional: true + + '@esbuild/linux-arm64@0.28.2': + optional: true + + '@esbuild/linux-arm@0.28.2': + optional: true + + '@esbuild/linux-ia32@0.28.2': + optional: true + + '@esbuild/linux-loong64@0.28.2': + optional: true + + '@esbuild/linux-mips64el@0.28.2': + optional: true + + '@esbuild/linux-ppc64@0.28.2': + optional: true + + '@esbuild/linux-riscv64@0.28.2': + optional: true + + '@esbuild/linux-s390x@0.28.2': + optional: true + + '@esbuild/linux-x64@0.28.2': + optional: true + + '@esbuild/netbsd-arm64@0.28.2': + optional: true + + '@esbuild/netbsd-x64@0.28.2': + optional: true + + '@esbuild/openbsd-arm64@0.28.2': + optional: true + + '@esbuild/openbsd-x64@0.28.2': + optional: true + + '@esbuild/openharmony-arm64@0.28.2': + optional: true + + '@esbuild/sunos-x64@0.28.2': + optional: true + + '@esbuild/win32-arm64@0.28.2': + optional: true + + '@esbuild/win32-ia32@0.28.2': + optional: true + + '@esbuild/win32-x64@0.28.2': + optional: true + + '@eslint-community/eslint-utils@4.10.1(eslint@9.39.5)': + dependencies: + eslint: 9.39.5 + eslint-visitor-keys: 3.4.3 + + '@eslint-community/eslint-utils@4.9.1(eslint@9.39.5)': + dependencies: + eslint: 9.39.5 + eslint-visitor-keys: 3.4.3 + + '@eslint-community/regexpp@4.12.2': {} + + '@eslint/config-array@0.21.2': + dependencies: + '@eslint/object-schema': 2.1.7 + debug: 4.4.3 + minimatch: 3.1.5 + transitivePeerDependencies: + - supports-color + + '@eslint/config-helpers@0.4.2': + dependencies: + '@eslint/core': 0.17.0 + + '@eslint/core@0.17.0': + dependencies: + '@types/json-schema': 7.0.15 + + '@eslint/eslintrc@3.3.7': + dependencies: + ajv: 6.15.0 + debug: 4.4.3 + espree: 10.4.0 + globals: 14.0.0 + ignore: 5.3.2 + import-fresh: 3.3.1 + js-yaml: 4.3.2 + minimatch: 3.1.5 + strip-json-comments: 3.1.1 + transitivePeerDependencies: + - supports-color + + '@eslint/js@9.39.5': {} + + '@eslint/object-schema@2.1.7': {} + + '@eslint/plugin-kit@0.4.1': + dependencies: + '@eslint/core': 0.17.0 + levn: 0.4.1 + + '@exodus/bytes@1.16.0': + optional: true + + '@humanfs/core@0.19.2': + dependencies: + '@humanfs/types': 0.15.0 + + '@humanfs/node@0.16.8': + dependencies: + '@humanfs/core': 0.19.2 + '@humanfs/types': 0.15.0 + '@humanwhocodes/retry': 0.4.3 + + '@humanfs/types@0.15.0': {} + + '@humanwhocodes/module-importer@1.0.1': {} + + '@humanwhocodes/retry@0.4.3': {} + + '@img/colour@1.1.0': + optional: true + + '@img/sharp-darwin-arm64@0.35.5': + optionalDependencies: + '@img/sharp-libvips-darwin-arm64': 1.3.4 + optional: true + + '@img/sharp-darwin-x64@0.35.5': + optionalDependencies: + '@img/sharp-libvips-darwin-x64': 1.3.4 + optional: true + + '@img/sharp-freebsd-wasm32@0.35.5': + dependencies: + '@img/sharp-wasm32': 0.35.5 + optional: true + + '@img/sharp-libvips-darwin-arm64@1.3.4': + optional: true + + '@img/sharp-libvips-darwin-x64@1.3.4': + optional: true + + '@img/sharp-libvips-linux-arm64@1.3.4': + optional: true + + '@img/sharp-libvips-linux-arm@1.3.4': + optional: true + + '@img/sharp-libvips-linux-ppc64@1.3.4': + optional: true + + '@img/sharp-libvips-linux-riscv64@1.3.4': + optional: true + + '@img/sharp-libvips-linux-s390x@1.3.4': + optional: true + + '@img/sharp-libvips-linux-x64@1.3.4': + optional: true + + '@img/sharp-libvips-linuxmusl-arm64@1.3.4': + optional: true + + '@img/sharp-libvips-linuxmusl-x64@1.3.4': + optional: true + + '@img/sharp-linux-arm64@0.35.5': + optionalDependencies: + '@img/sharp-libvips-linux-arm64': 1.3.4 + optional: true + + '@img/sharp-linux-arm@0.35.5': + optionalDependencies: + '@img/sharp-libvips-linux-arm': 1.3.4 + optional: true + + '@img/sharp-linux-ppc64@0.35.5': + optionalDependencies: + '@img/sharp-libvips-linux-ppc64': 1.3.4 + optional: true + + '@img/sharp-linux-riscv64@0.35.5': + optionalDependencies: + '@img/sharp-libvips-linux-riscv64': 1.3.4 + optional: true + + '@img/sharp-linux-s390x@0.35.5': + optionalDependencies: + '@img/sharp-libvips-linux-s390x': 1.3.4 + optional: true + + '@img/sharp-linux-x64@0.35.5': + optionalDependencies: + '@img/sharp-libvips-linux-x64': 1.3.4 + optional: true + + '@img/sharp-linuxmusl-arm64@0.35.5': + optionalDependencies: + '@img/sharp-libvips-linuxmusl-arm64': 1.3.4 + optional: true + + '@img/sharp-linuxmusl-x64@0.35.5': + optionalDependencies: + '@img/sharp-libvips-linuxmusl-x64': 1.3.4 + optional: true + + '@img/sharp-wasm32@0.35.5': + dependencies: + '@emnapi/runtime': 1.11.3 + optional: true + + '@img/sharp-webcontainers-wasm32@0.35.5': + dependencies: + '@img/sharp-wasm32': 0.35.5 + optional: true + + '@img/sharp-win32-arm64@0.35.5': + optional: true + + '@img/sharp-win32-ia32@0.35.5': + optional: true + + '@img/sharp-win32-x64@0.35.5': + optional: true + + '@jridgewell/gen-mapping@0.3.13': + dependencies: + '@jridgewell/sourcemap-codec': 1.6.0 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/remapping@2.3.5': + dependencies: + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/resolve-uri@3.1.2': {} + + '@jridgewell/sourcemap-codec@1.6.0': {} + + '@jridgewell/trace-mapping@0.3.31': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.6.0 + + '@napi-rs/lzma-linux-x64-gnu@1.5.1': + optional: true + + '@napi-rs/wasm-runtime@1.2.4(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)': + dependencies: + '@emnapi/core': 1.10.0 + '@emnapi/runtime': 1.10.0 + '@tybys/wasm-util': 0.10.4 + optional: true + + '@next/env@16.3.6': {} + + '@next/eslint-plugin-next@16.3.6(eslint@9.39.5)': + dependencies: + '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.5) + fast-glob: 3.3.1 + transitivePeerDependencies: + - eslint + + '@next/swc-darwin-arm64@16.3.6': + optional: true + + '@next/swc-darwin-x64@16.3.6': + optional: true + + '@next/swc-linux-arm64-gnu@16.3.6': + optional: true + + '@next/swc-linux-arm64-musl@16.3.6': + optional: true + + '@next/swc-linux-x64-gnu@16.3.6': + optional: true + + '@next/swc-linux-x64-musl@16.3.6': + optional: true + + '@next/swc-win32-arm64-msvc@16.3.6': + optional: true + + '@next/swc-win32-x64-msvc@16.3.6': + optional: true + + '@nodelib/fs.scandir@2.1.5': + dependencies: + '@nodelib/fs.stat': 2.0.5 + run-parallel: 1.2.0 + + '@nodelib/fs.stat@2.0.5': {} + + '@nodelib/fs.walk@1.2.8': + dependencies: + '@nodelib/fs.scandir': 2.1.5 + fastq: 1.20.3 + + '@nolyfill/is-core-module@1.0.39': {} + + '@playwright/test@1.55.1': + dependencies: + playwright: 1.55.1 + + '@rollup/rollup-android-arm-eabi@4.63.5': + optional: true + + '@rollup/rollup-android-arm64@4.63.5': + optional: true + + '@rollup/rollup-darwin-arm64@4.63.5': + optional: true + + '@rollup/rollup-darwin-x64@4.63.5': + optional: true + + '@rollup/rollup-freebsd-arm64@4.63.5': + optional: true + + '@rollup/rollup-freebsd-x64@4.63.5': + optional: true + + '@rollup/rollup-linux-arm-gnueabihf@4.63.5': + optional: true + + '@rollup/rollup-linux-arm-musleabihf@4.63.5': + optional: true + + '@rollup/rollup-linux-arm64-gnu@4.63.5': + optional: true + + '@rollup/rollup-linux-arm64-musl@4.63.5': + optional: true + + '@rollup/rollup-linux-loong64-gnu@4.63.5': + optional: true + + '@rollup/rollup-linux-loong64-musl@4.63.5': + optional: true + + '@rollup/rollup-linux-ppc64-gnu@4.63.5': + optional: true + + '@rollup/rollup-linux-ppc64-musl@4.63.5': + optional: true + + '@rollup/rollup-linux-riscv64-gnu@4.63.5': + optional: true + + '@rollup/rollup-linux-riscv64-musl@4.63.5': + optional: true + + '@rollup/rollup-linux-s390x-gnu@4.63.5': + optional: true + + '@rollup/rollup-linux-x64-gnu@4.63.5': + optional: true + + '@rollup/rollup-linux-x64-musl@4.63.5': + optional: true + + '@rollup/rollup-openbsd-x64@4.63.5': + optional: true + + '@rollup/rollup-openharmony-arm64@4.63.5': + optional: true + + '@rollup/rollup-win32-arm64-msvc@4.63.5': + optional: true + + '@rollup/rollup-win32-ia32-msvc@4.63.5': + optional: true + + '@rollup/rollup-win32-x64-gnu@4.63.5': + optional: true + + '@rollup/rollup-win32-x64-msvc@4.63.5': + optional: true + + '@rtsao/scc@1.1.0': {} + + '@standard-schema/spec@1.1.0': {} + + '@swc/helpers@0.5.23': + dependencies: + tslib: 2.8.1 + + '@testing-library/dom@10.4.2': + dependencies: + '@babel/code-frame': 7.29.7 + '@babel/runtime': 7.29.7 + '@types/aria-query': 5.0.4 + aria-query: 5.3.0 + dom-accessibility-api: 0.5.16 + lz-string: 1.5.0 + picocolors: 1.1.1 + pretty-format: 27.5.1 + + '@testing-library/jest-dom@6.10.0(@testing-library/dom@10.4.2)': + dependencies: + '@adobe/css-tools': 4.5.0 + '@testing-library/dom': 10.4.2 + aria-query: 5.3.2 + css.escape: 1.5.1 + dom-accessibility-api: 0.6.3 + picocolors: 1.1.1 + redent: 3.0.0 + + '@testing-library/react@16.3.3(@testing-library/dom@10.4.2)(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)': + dependencies: + '@babel/runtime': 7.29.7 + '@testing-library/dom': 10.4.2 + react: 19.3.0 + react-dom: 19.3.0(react@19.3.0) + optionalDependencies: + '@types/react': 19.3.0 + '@types/react-dom': 19.3.0(@types/react@19.3.0) + + '@tybys/wasm-util@0.10.4': + dependencies: + tslib: 2.8.1 + optional: true + + '@types/aria-query@5.0.4': {} + + '@types/chai@5.2.3': + dependencies: + '@types/deep-eql': 4.0.2 + assertion-error: 2.0.1 + + '@types/deep-eql@4.0.2': {} + + '@types/estree@1.0.9': {} + + '@types/json-schema@7.0.15': {} + + '@types/json5@0.0.29': {} + + '@types/node@22.18.6': + dependencies: + undici-types: 6.21.0 + + '@types/react-dom@19.3.0(@types/react@19.3.0)': + dependencies: + '@types/react': 19.3.0 + + '@types/react@19.3.0': + dependencies: + csstype: 3.2.3 + + '@typescript-eslint/eslint-plugin@8.71.0(@typescript-eslint/parser@8.71.0(eslint@9.39.5)(typescript@5.9.3))(eslint@9.39.5)(typescript@5.9.3)': + dependencies: + '@eslint-community/regexpp': 4.12.2 + '@typescript-eslint/parser': 8.71.0(eslint@9.39.5)(typescript@5.9.3) + '@typescript-eslint/scope-manager': 8.71.0 + '@typescript-eslint/type-utils': 8.71.0(eslint@9.39.5)(typescript@5.9.3) + '@typescript-eslint/utils': 8.71.0(eslint@9.39.5)(typescript@5.9.3) + '@typescript-eslint/visitor-keys': 8.71.0 + eslint: 9.39.5 + ignore: 7.0.10 + natural-compare: 1.4.0 + ts-api-utils: 2.5.0(typescript@5.9.3) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/parser@8.71.0(eslint@9.39.5)(typescript@5.9.3)': + dependencies: + '@typescript-eslint/scope-manager': 8.71.0 + '@typescript-eslint/types': 8.71.0 + '@typescript-eslint/typescript-estree': 8.71.0(typescript@5.9.3) + '@typescript-eslint/visitor-keys': 8.71.0 + debug: 4.4.3 + eslint: 9.39.5 + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/project-service@8.71.0(typescript@5.9.3)': + dependencies: + '@typescript-eslint/tsconfig-utils': 8.71.0(typescript@5.9.3) + '@typescript-eslint/types': 8.71.0 + debug: 4.4.3 + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/scope-manager@8.71.0': + dependencies: + '@typescript-eslint/types': 8.71.0 + '@typescript-eslint/visitor-keys': 8.71.0 + + '@typescript-eslint/tsconfig-utils@8.71.0(typescript@5.9.3)': + dependencies: + typescript: 5.9.3 + + '@typescript-eslint/type-utils@8.71.0(eslint@9.39.5)(typescript@5.9.3)': + dependencies: + '@typescript-eslint/types': 8.71.0 + '@typescript-eslint/typescript-estree': 8.71.0(typescript@5.9.3) + '@typescript-eslint/utils': 8.71.0(eslint@9.39.5)(typescript@5.9.3) + debug: 4.4.3 + eslint: 9.39.5 + ts-api-utils: 2.5.0(typescript@5.9.3) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/types@8.71.0': {} + + '@typescript-eslint/typescript-estree@8.71.0(typescript@5.9.3)': + dependencies: + '@typescript-eslint/project-service': 8.71.0(typescript@5.9.3) + '@typescript-eslint/tsconfig-utils': 8.71.0(typescript@5.9.3) + '@typescript-eslint/types': 8.71.0 + '@typescript-eslint/visitor-keys': 8.71.0 + debug: 4.4.3 + minimatch: 10.2.6 + semver: 7.8.5 + tinyglobby: 0.2.17 + ts-api-utils: 2.5.0(typescript@5.9.3) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/utils@8.71.0(eslint@9.39.5)(typescript@5.9.3)': + dependencies: + '@eslint-community/eslint-utils': 4.10.1(eslint@9.39.5) + '@typescript-eslint/scope-manager': 8.71.0 + '@typescript-eslint/types': 8.71.0 + '@typescript-eslint/typescript-estree': 8.71.0(typescript@5.9.3) + eslint: 9.39.5 + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/visitor-keys@8.71.0': + dependencies: + '@typescript-eslint/types': 8.71.0 + eslint-visitor-keys: 5.0.1 + + '@typescript/vfs@1.6.5(typescript@5.4.5)': + dependencies: + debug: 4.4.3 + typescript: 5.4.5 + transitivePeerDependencies: + - supports-color + + '@unrs/resolver-binding-android-arm-eabi@1.12.2': + optional: true + + '@unrs/resolver-binding-android-arm64@1.12.2': + optional: true + + '@unrs/resolver-binding-darwin-arm64@1.12.2': + optional: true + + '@unrs/resolver-binding-darwin-x64@1.12.2': + optional: true + + '@unrs/resolver-binding-freebsd-x64@1.12.2': + optional: true + + '@unrs/resolver-binding-linux-arm-gnueabihf@1.12.2': + optional: true + + '@unrs/resolver-binding-linux-arm-musleabihf@1.12.2': + optional: true + + '@unrs/resolver-binding-linux-arm64-gnu@1.12.2': + optional: true + + '@unrs/resolver-binding-linux-arm64-musl@1.12.2': + optional: true + + '@unrs/resolver-binding-linux-loong64-gnu@1.12.2': + optional: true + + '@unrs/resolver-binding-linux-loong64-musl@1.12.2': + optional: true + + '@unrs/resolver-binding-linux-ppc64-gnu@1.12.2': + optional: true + + '@unrs/resolver-binding-linux-riscv64-gnu@1.12.2': + optional: true + + '@unrs/resolver-binding-linux-riscv64-musl@1.12.2': + optional: true + + '@unrs/resolver-binding-linux-s390x-gnu@1.12.2': + optional: true + + '@unrs/resolver-binding-linux-x64-gnu@1.12.2': + optional: true + + '@unrs/resolver-binding-linux-x64-musl@1.12.2': + optional: true + + '@unrs/resolver-binding-openharmony-arm64@1.12.2': + optional: true + + '@unrs/resolver-binding-wasm32-wasi@1.12.2': + dependencies: + '@emnapi/core': 1.10.0 + '@emnapi/runtime': 1.10.0 + '@napi-rs/wasm-runtime': 1.2.4(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0) + optional: true + + '@unrs/resolver-binding-win32-arm64-msvc@1.12.2': + optional: true + + '@unrs/resolver-binding-win32-ia32-msvc@1.12.2': + optional: true + + '@unrs/resolver-binding-win32-x64-msvc@1.12.2': + optional: true + + '@vitest/expect@3.2.7': + dependencies: + '@types/chai': 5.2.3 + '@vitest/spy': 3.2.7 + '@vitest/utils': 3.2.7 + chai: 5.3.3 + tinyrainbow: 2.0.0 + + '@vitest/expect@4.0.7': + dependencies: + '@standard-schema/spec': 1.1.0 + '@types/chai': 5.2.3 + '@vitest/spy': 4.0.7 + '@vitest/utils': 4.0.7 + chai: 6.2.2 + tinyrainbow: 3.1.1 + + '@vitest/mocker@3.2.7(vite@7.3.6(@types/node@22.18.6))': + dependencies: + '@vitest/spy': 3.2.7 + estree-walker: 3.0.3 + magic-string: 0.30.21 + optionalDependencies: + vite: 7.3.6(@types/node@22.18.6) + + '@vitest/mocker@4.0.7(vite@7.3.6(@types/node@22.18.6))': + dependencies: + '@vitest/spy': 4.0.7 + estree-walker: 3.0.3 + magic-string: 0.30.21 + optionalDependencies: + vite: 7.3.6(@types/node@22.18.6) + + '@vitest/pretty-format@3.2.7': + dependencies: + tinyrainbow: 2.0.0 + + '@vitest/pretty-format@4.0.7': + dependencies: + tinyrainbow: 3.1.1 + + '@vitest/runner@3.2.7': + dependencies: + '@vitest/utils': 3.2.7 + pathe: 2.0.3 + strip-literal: 3.1.0 + + '@vitest/runner@4.0.7': + dependencies: + '@vitest/utils': 4.0.7 + pathe: 2.0.3 + + '@vitest/snapshot@3.2.7': + dependencies: + '@vitest/pretty-format': 3.2.7 + magic-string: 0.30.21 + pathe: 2.0.3 + + '@vitest/snapshot@4.0.7': + dependencies: + '@vitest/pretty-format': 4.0.7 + magic-string: 0.30.21 + pathe: 2.0.3 + + '@vitest/spy@3.2.7': + dependencies: + tinyspy: 4.0.6 + + '@vitest/spy@4.0.7': {} + + '@vitest/utils@3.2.7': + dependencies: + '@vitest/pretty-format': 3.2.7 + loupe: 3.2.1 + tinyrainbow: 2.0.0 + + '@vitest/utils@4.0.7': + dependencies: + '@vitest/pretty-format': 4.0.7 + tinyrainbow: 3.1.1 + + acorn-jsx@5.3.2(acorn@8.18.0): + dependencies: + acorn: 8.18.0 + + acorn@8.18.0: {} + + agent-base@7.1.4: {} + + ajv@6.15.0: + dependencies: + fast-deep-equal: 3.1.3 + fast-json-stable-stringify: 2.1.0 + json-schema-traverse: 0.4.1 + uri-js: 4.4.1 + + ansi-regex@5.0.1: {} + + ansi-styles@4.3.0: + dependencies: + color-convert: 2.0.1 + + ansi-styles@5.2.0: {} + + argparse@2.0.1: {} + + aria-query@5.3.0: + dependencies: + dequal: 2.0.3 + + aria-query@5.3.2: {} + + array-buffer-byte-length@1.0.2: + dependencies: + call-bound: 1.0.4 + is-array-buffer: 3.0.5 + + array-includes@3.2.0: + dependencies: + call-bind: 1.0.9 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-abstract: 1.24.2 + es-object-atoms: 1.1.2 + es-shim-unscopables: 1.1.0 + is-string: 1.1.1 + math-intrinsics: 1.1.0 + + array.prototype.findlast@1.2.5: + dependencies: + call-bind: 1.0.9 + define-properties: 1.2.1 + es-abstract: 1.24.2 + es-errors: 1.3.0 + es-object-atoms: 1.1.2 + es-shim-unscopables: 1.1.0 + + array.prototype.findlastindex@1.2.6: + dependencies: + call-bind: 1.0.9 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-abstract: 1.24.2 + es-errors: 1.3.0 + es-object-atoms: 1.1.2 + es-shim-unscopables: 1.1.0 + + array.prototype.flat@1.3.3: + dependencies: + call-bind: 1.0.9 + define-properties: 1.2.1 + es-abstract: 1.24.2 + es-shim-unscopables: 1.1.0 + + array.prototype.flatmap@1.3.3: + dependencies: + call-bind: 1.0.9 + define-properties: 1.2.1 + es-abstract: 1.24.2 + es-shim-unscopables: 1.1.0 + + array.prototype.tosorted@1.1.4: + dependencies: + call-bind: 1.0.9 + define-properties: 1.2.1 + es-abstract: 1.24.2 + es-errors: 1.3.0 + es-shim-unscopables: 1.1.0 + + arraybuffer.prototype.slice@1.0.4: + dependencies: + array-buffer-byte-length: 1.0.2 + call-bind: 1.0.9 + define-properties: 1.2.1 + es-abstract: 1.24.2 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + is-array-buffer: 3.0.5 + + assertion-error@2.0.1: {} + + ast-types-flow@0.0.8: {} + + async-function@1.0.0: {} + + available-typed-arrays@1.0.7: + dependencies: + possible-typed-array-names: 1.1.0 + + axe-core@4.10.3: {} + + axe-core@4.13.0: {} + + axobject-query@4.1.0: {} + + balanced-match@1.0.2: {} + + balanced-match@4.0.4: {} + + baseline-browser-mapping@2.11.26: {} + + bidi-js@1.1.0: + dependencies: + require-from-string: 2.0.2 + optional: true + + brace-expansion@1.1.21: + dependencies: + balanced-match: 1.0.2 + concat-map: 0.0.1 + + brace-expansion@5.0.12: + dependencies: + balanced-match: 4.0.4 + + braces@3.0.3: + dependencies: + fill-range: 7.1.1 + + browserslist@4.29.2: + dependencies: + baseline-browser-mapping: 2.11.26 + caniuse-lite: 1.0.30001813 + electron-to-chromium: 1.5.439 + node-releases: 2.0.57 + update-browserslist-db: 1.3.3(browserslist@4.29.2) + + cac@6.7.14: {} + + call-bind-apply-helpers@1.0.2: + dependencies: + es-errors: 1.3.0 + function-bind: 1.1.2 + + call-bind@1.0.9: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-define-property: 1.0.1 + get-intrinsic: 1.3.0 + set-function-length: 1.2.2 + + call-bound@1.0.4: + dependencies: + call-bind-apply-helpers: 1.0.2 + get-intrinsic: 1.3.0 + + callsites@3.1.0: {} + + caniuse-lite@1.0.30001813: {} + + chai@5.3.3: + dependencies: + assertion-error: 2.0.1 + check-error: 2.1.3 + deep-eql: 5.0.2 + loupe: 3.2.1 + pathval: 2.0.1 + + chai@6.2.2: {} + + chalk@4.1.2: + dependencies: + ansi-styles: 4.3.0 + supports-color: 7.2.0 + + check-error@2.1.3: {} + + client-only@0.0.1: {} + + color-convert@2.0.1: + dependencies: + color-name: 1.1.4 + + color-name@1.1.4: {} + + concat-map@0.0.1: {} + + convert-source-map@2.0.0: {} + + cross-spawn@7.0.6: + dependencies: + path-key: 3.1.1 + shebang-command: 2.0.0 + which: 2.0.2 + + css-tree@3.2.1: + dependencies: + mdn-data: 2.27.1 + source-map-js: 1.2.1 + optional: true + + css.escape@1.5.1: {} + + cssstyle@4.6.0: + dependencies: + '@asamuzakjp/css-color': 3.2.0 + rrweb-cssom: 0.8.0 + + cssstyle@5.3.7: + dependencies: + '@asamuzakjp/css-color': 4.1.2 + '@csstools/css-syntax-patches-for-csstree': 1.1.14(css-tree@3.2.1) + css-tree: 3.2.1 + lru-cache: 11.5.3 + optional: true + + csstype@3.2.3: {} + + damerau-levenshtein@1.0.8: {} + + data-urls@5.0.0: + dependencies: + whatwg-mimetype: 4.0.0 + whatwg-url: 14.2.0 + + data-urls@6.0.1: + dependencies: + whatwg-mimetype: 5.0.0 + whatwg-url: 15.1.0 + optional: true + + data-view-buffer@1.0.2: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + is-data-view: 1.0.2 + + data-view-byte-length@1.0.2: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + is-data-view: 1.0.2 + + data-view-byte-offset@1.0.1: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + is-data-view: 1.0.2 + + debug@3.2.7: + dependencies: + ms: 2.1.3 + + debug@4.4.3: + dependencies: + ms: 2.1.3 + + decimal.js@10.6.0: {} + + deep-eql@5.0.2: {} + + deep-is@0.1.4: {} + + define-data-property@1.1.4: + dependencies: + es-define-property: 1.0.1 + es-errors: 1.3.0 + gopd: 1.2.0 + + define-properties@1.2.1: + dependencies: + define-data-property: 1.1.4 + has-property-descriptors: 1.0.2 + object-keys: 1.1.1 + + dequal@2.0.3: {} + + detect-libc@2.1.2: + optional: true + + doctrine@2.1.0: + dependencies: + esutils: 2.0.3 + + dom-accessibility-api@0.5.16: {} + + dom-accessibility-api@0.6.3: {} + + dunder-proto@1.0.1: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-errors: 1.3.0 + gopd: 1.2.0 + + electron-to-chromium@1.5.439: {} + + emoji-regex@9.2.2: {} + + entities@6.0.1: {} + + entities@8.1.0: + optional: true + + es-abstract-get@1.0.0: + dependencies: + es-errors: 1.3.0 + es-object-atoms: 1.1.2 + is-callable: 1.2.7 + object-inspect: 1.13.4 + + es-abstract@1.24.2: + dependencies: + array-buffer-byte-length: 1.0.2 + arraybuffer.prototype.slice: 1.0.4 + available-typed-arrays: 1.0.7 + call-bind: 1.0.9 + call-bound: 1.0.4 + data-view-buffer: 1.0.2 + data-view-byte-length: 1.0.2 + data-view-byte-offset: 1.0.1 + es-define-property: 1.0.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.2 + es-set-tostringtag: 2.1.0 + es-to-primitive: 1.3.4 + function.prototype.name: 1.2.0 + get-intrinsic: 1.3.0 + get-proto: 1.0.1 + get-symbol-description: 1.1.0 + globalthis: 1.0.4 + gopd: 1.2.0 + has-property-descriptors: 1.0.2 + has-proto: 1.2.0 + has-symbols: 1.1.0 + hasown: 2.0.4 + internal-slot: 1.1.0 + is-array-buffer: 3.0.5 + is-callable: 1.2.7 + is-data-view: 1.0.2 + is-negative-zero: 2.0.3 + is-regex: 1.2.1 + is-set: 2.0.3 + is-shared-array-buffer: 1.0.4 + is-string: 1.1.1 + is-typed-array: 1.1.15 + is-weakref: 1.1.1 + math-intrinsics: 1.1.0 + object-inspect: 1.13.4 + object-keys: 1.1.1 + object.assign: 4.1.7 + own-keys: 1.0.2 + regexp.prototype.flags: 1.5.4 + safe-array-concat: 1.1.4 + safe-push-apply: 1.0.0 + safe-regex-test: 1.1.0 + set-proto: 1.0.0 + stop-iteration-iterator: 1.1.0 + string.prototype.trim: 1.2.11 + string.prototype.trimend: 1.0.10 + string.prototype.trimstart: 1.0.8 + typed-array-buffer: 1.0.3 + typed-array-byte-length: 1.0.3 + typed-array-byte-offset: 1.0.5 + typed-array-length: 1.0.8 + unbox-primitive: 1.1.0 + which-typed-array: 1.1.24 + + es-define-property@1.0.1: {} + + es-errors@1.3.0: {} + + es-iterator-helpers@1.4.0: + dependencies: + call-bind: 1.0.9 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-abstract: 1.24.2 + es-errors: 1.3.0 + es-set-tostringtag: 2.1.0 + function-bind: 1.1.2 + get-intrinsic: 1.3.0 + globalthis: 1.0.4 + gopd: 1.2.0 + has-property-descriptors: 1.0.2 + has-proto: 1.2.0 + has-symbols: 1.1.0 + internal-slot: 1.1.0 + iterator.prototype: 1.1.5 + math-intrinsics: 1.1.0 + + es-module-lexer@1.7.0: {} + + es-object-atoms@1.1.2: + dependencies: + es-errors: 1.3.0 + + es-set-tostringtag@2.1.0: + dependencies: + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + has-tostringtag: 1.0.2 + hasown: 2.0.4 + + es-shim-unscopables@1.1.0: + dependencies: + hasown: 2.0.4 + + es-to-primitive@1.3.4: + dependencies: + es-abstract-get: 1.0.0 + es-define-property: 1.0.1 + es-errors: 1.3.0 + is-callable: 1.2.7 + is-date-object: 1.1.0 + is-symbol: 1.1.1 + + esbuild@0.28.2: + optionalDependencies: + '@esbuild/aix-ppc64': 0.28.2 + '@esbuild/android-arm': 0.28.2 + '@esbuild/android-arm64': 0.28.2 + '@esbuild/android-x64': 0.28.2 + '@esbuild/darwin-arm64': 0.28.2 + '@esbuild/darwin-x64': 0.28.2 + '@esbuild/freebsd-arm64': 0.28.2 + '@esbuild/freebsd-x64': 0.28.2 + '@esbuild/linux-arm': 0.28.2 + '@esbuild/linux-arm64': 0.28.2 + '@esbuild/linux-ia32': 0.28.2 + '@esbuild/linux-loong64': 0.28.2 + '@esbuild/linux-mips64el': 0.28.2 + '@esbuild/linux-ppc64': 0.28.2 + '@esbuild/linux-riscv64': 0.28.2 + '@esbuild/linux-s390x': 0.28.2 + '@esbuild/linux-x64': 0.28.2 + '@esbuild/netbsd-arm64': 0.28.2 + '@esbuild/netbsd-x64': 0.28.2 + '@esbuild/openbsd-arm64': 0.28.2 + '@esbuild/openbsd-x64': 0.28.2 + '@esbuild/openharmony-arm64': 0.28.2 + '@esbuild/sunos-x64': 0.28.2 + '@esbuild/win32-arm64': 0.28.2 + '@esbuild/win32-ia32': 0.28.2 + '@esbuild/win32-x64': 0.28.2 + + escalade@3.2.0: {} + + escape-string-regexp@4.0.0: {} + + eslint-config-next@16.3.6(@typescript-eslint/parser@8.71.0(eslint@9.39.5)(typescript@5.9.3))(eslint@9.39.5)(typescript@5.9.3): + dependencies: + '@next/eslint-plugin-next': 16.3.6(eslint@9.39.5) + eslint: 9.39.5 + eslint-import-resolver-node: 0.3.10 + eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.71.0(eslint@9.39.5)(typescript@5.9.3))(eslint@9.39.5))(eslint@9.39.5) + eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.71.0(eslint@9.39.5)(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.71.0(eslint@9.39.5)(typescript@5.9.3))(eslint@9.39.5))(eslint@9.39.5))(eslint@9.39.5) + eslint-plugin-jsx-a11y: 6.10.2(eslint@9.39.5) + eslint-plugin-react: 7.37.5(eslint@9.39.5) + eslint-plugin-react-hooks: 7.1.1(eslint@9.39.5) + globals: 16.4.0 + typescript-eslint: 8.71.0(eslint@9.39.5)(typescript@5.9.3) + optionalDependencies: + typescript: 5.9.3 + transitivePeerDependencies: + - '@typescript-eslint/parser' + - eslint-import-resolver-webpack + - eslint-plugin-import-x + - supports-color + + eslint-import-resolver-node@0.3.10: + dependencies: + debug: 3.2.7 + is-core-module: 2.17.0 + resolve: 2.0.0-next.7 + transitivePeerDependencies: + - supports-color + + eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.71.0(eslint@9.39.5)(typescript@5.9.3))(eslint@9.39.5))(eslint@9.39.5): + dependencies: + '@nolyfill/is-core-module': 1.0.39 + debug: 4.4.3 + eslint: 9.39.5 + get-tsconfig: 4.14.3 + is-bun-module: 2.0.0 + stable-hash: 0.0.5 + tinyglobby: 0.2.17 + unrs-resolver: 1.12.2 + optionalDependencies: + eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.71.0(eslint@9.39.5)(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.71.0(eslint@9.39.5)(typescript@5.9.3))(eslint@9.39.5))(eslint@9.39.5))(eslint@9.39.5) + transitivePeerDependencies: + - supports-color + + eslint-module-utils@2.14.0(@typescript-eslint/parser@8.71.0(eslint@9.39.5)(typescript@5.9.3))(eslint-import-resolver-node@0.3.10)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.71.0(eslint@9.39.5)(typescript@5.9.3))(eslint@9.39.5))(eslint@9.39.5))(eslint@9.39.5): + dependencies: + debug: 3.2.7 + optionalDependencies: + '@typescript-eslint/parser': 8.71.0(eslint@9.39.5)(typescript@5.9.3) + eslint: 9.39.5 + eslint-import-resolver-node: 0.3.10 + eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.71.0(eslint@9.39.5)(typescript@5.9.3))(eslint@9.39.5))(eslint@9.39.5) + transitivePeerDependencies: + - supports-color + + eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.71.0(eslint@9.39.5)(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.71.0(eslint@9.39.5)(typescript@5.9.3))(eslint@9.39.5))(eslint@9.39.5))(eslint@9.39.5): + dependencies: + '@rtsao/scc': 1.1.0 + array-includes: 3.2.0 + array.prototype.findlastindex: 1.2.6 + array.prototype.flat: 1.3.3 + array.prototype.flatmap: 1.3.3 + debug: 3.2.7 + doctrine: 2.1.0 + eslint: 9.39.5 + eslint-import-resolver-node: 0.3.10 + eslint-module-utils: 2.14.0(@typescript-eslint/parser@8.71.0(eslint@9.39.5)(typescript@5.9.3))(eslint-import-resolver-node@0.3.10)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.71.0(eslint@9.39.5)(typescript@5.9.3))(eslint@9.39.5))(eslint@9.39.5))(eslint@9.39.5) + hasown: 2.0.4 + is-core-module: 2.17.0 + is-glob: 4.0.3 + minimatch: 3.1.5 + object.fromentries: 2.0.8 + object.groupby: 1.0.3 + object.values: 1.2.1 + semver: 6.3.1 + string.prototype.trimend: 1.0.10 + tsconfig-paths: 3.15.0 + optionalDependencies: + '@typescript-eslint/parser': 8.71.0(eslint@9.39.5)(typescript@5.9.3) + transitivePeerDependencies: + - eslint-import-resolver-typescript + - eslint-import-resolver-webpack + - supports-color + + eslint-plugin-jsx-a11y@6.10.2(eslint@9.39.5): + dependencies: + aria-query: 5.3.2 + array-includes: 3.2.0 + array.prototype.flatmap: 1.3.3 + ast-types-flow: 0.0.8 + axe-core: 4.13.0 + axobject-query: 4.1.0 + damerau-levenshtein: 1.0.8 + emoji-regex: 9.2.2 + eslint: 9.39.5 + hasown: 2.0.4 + jsx-ast-utils: 3.3.5 + language-tags: 1.0.9 + minimatch: 3.1.5 + object.fromentries: 2.0.8 + safe-regex-test: 1.1.0 + string.prototype.includes: 2.0.1 + + eslint-plugin-react-hooks@7.1.1(eslint@9.39.5): + dependencies: + '@babel/core': 7.29.7 + '@babel/parser': 7.29.9 + eslint: 9.39.5 + hermes-parser: 0.25.1 + zod: 4.6.5 + zod-validation-error: 4.0.2(zod@4.6.5) + transitivePeerDependencies: + - supports-color + + eslint-plugin-react@7.37.5(eslint@9.39.5): + dependencies: + array-includes: 3.2.0 + array.prototype.findlast: 1.2.5 + array.prototype.flatmap: 1.3.3 + array.prototype.tosorted: 1.1.4 + doctrine: 2.1.0 + es-iterator-helpers: 1.4.0 + eslint: 9.39.5 + estraverse: 5.3.0 + hasown: 2.0.4 + jsx-ast-utils: 3.3.5 + minimatch: 3.1.5 + object.entries: 1.1.9 + object.fromentries: 2.0.8 + object.values: 1.2.1 + prop-types: 15.8.1 + resolve: 2.0.0-next.7 + semver: 6.3.1 + string.prototype.matchall: 4.1.0 + string.prototype.repeat: 1.0.0 + + eslint-scope@8.4.0: + dependencies: + esrecurse: 4.3.0 + estraverse: 5.3.0 + + eslint-visitor-keys@3.4.3: {} + + eslint-visitor-keys@4.2.1: {} + + eslint-visitor-keys@5.0.1: {} + + eslint@9.39.5: + dependencies: + '@eslint-community/eslint-utils': 4.10.1(eslint@9.39.5) + '@eslint-community/regexpp': 4.12.2 + '@eslint/config-array': 0.21.2 + '@eslint/config-helpers': 0.4.2 + '@eslint/core': 0.17.0 + '@eslint/eslintrc': 3.3.7 + '@eslint/js': 9.39.5 + '@eslint/plugin-kit': 0.4.1 + '@humanfs/node': 0.16.8 + '@humanwhocodes/module-importer': 1.0.1 + '@humanwhocodes/retry': 0.4.3 + '@types/estree': 1.0.9 + ajv: 6.15.0 + chalk: 4.1.2 + cross-spawn: 7.0.6 + debug: 4.4.3 + escape-string-regexp: 4.0.0 + eslint-scope: 8.4.0 + eslint-visitor-keys: 4.2.1 + espree: 10.4.0 + esquery: 1.7.0 + esutils: 2.0.3 + fast-deep-equal: 3.1.3 + file-entry-cache: 8.0.0 + find-up: 5.0.0 + glob-parent: 6.0.2 + ignore: 5.3.2 + imurmurhash: 0.1.4 + is-glob: 4.0.3 + json-stable-stringify-without-jsonify: 1.0.1 + lodash.merge: 4.6.2 + minimatch: 3.1.5 + natural-compare: 1.4.0 + optionator: 0.9.4 + transitivePeerDependencies: + - supports-color + + espree@10.4.0: + dependencies: + acorn: 8.18.0 + acorn-jsx: 5.3.2(acorn@8.18.0) + eslint-visitor-keys: 4.2.1 + + esquery@1.7.0: + dependencies: + estraverse: 5.3.0 + + esrecurse@4.3.0: + dependencies: + estraverse: 5.3.0 + + estraverse@5.3.0: {} + + estree-walker@3.0.3: + dependencies: + '@types/estree': 1.0.9 + + esutils@2.0.3: {} + + expect-type@1.4.0: {} + + fast-deep-equal@3.1.3: {} + + fast-glob@3.3.1: + dependencies: + '@nodelib/fs.stat': 2.0.5 + '@nodelib/fs.walk': 1.2.8 + glob-parent: 5.1.2 + merge2: 1.4.1 + micromatch: 4.0.8 + + fast-json-stable-stringify@2.1.0: {} + + fast-levenshtein@2.0.6: {} + + fastq@1.20.3: + dependencies: + reusify: 1.1.0 + + fdir@6.5.0(picomatch@4.0.7): + optionalDependencies: + picomatch: 4.0.7 + + file-entry-cache@8.0.0: + dependencies: + flat-cache: 4.0.1 + + fill-range@7.1.1: + dependencies: + to-regex-range: 5.0.1 + + find-up@5.0.0: + dependencies: + locate-path: 6.0.0 + path-exists: 4.0.0 + + flat-cache@4.0.1: + dependencies: + flatted: 3.4.4 + keyv: 4.5.4 + + flatted@3.4.4: {} + + for-each@0.3.5: + dependencies: + is-callable: 1.2.7 + + fsevents@2.3.2: + optional: true + + fsevents@2.3.3: + optional: true + + function-bind@1.1.2: {} + + function.prototype.name@1.2.0: + dependencies: + call-bind: 1.0.9 + call-bound: 1.0.4 + es-define-property: 1.0.1 + es-errors: 1.3.0 + functions-have-names: 1.2.3 + has-property-descriptors: 1.0.2 + hasown: 2.0.4 + is-callable: 1.2.7 + is-document.all: 1.0.0 + + functions-have-names@1.2.3: {} + + generator-function@2.0.1: {} + + gensync@1.0.0-beta.2: {} + + get-intrinsic@1.3.0: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-define-property: 1.0.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.2 + function-bind: 1.1.2 + get-proto: 1.0.1 + gopd: 1.2.0 + has-symbols: 1.1.0 + hasown: 2.0.4 + math-intrinsics: 1.1.0 + + get-proto@1.0.1: + dependencies: + dunder-proto: 1.0.1 + es-object-atoms: 1.1.2 + + get-symbol-description@1.1.0: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + + get-tsconfig@4.14.3: + dependencies: + resolve-pkg-maps: 1.0.0 + + glob-parent@5.1.2: + dependencies: + is-glob: 4.0.3 + + glob-parent@6.0.2: + dependencies: + is-glob: 4.0.3 + + globals@14.0.0: {} + + globals@16.4.0: {} + + globalthis@1.0.4: + dependencies: + define-properties: 1.2.1 + gopd: 1.2.0 + + gopd@1.2.0: {} + + has-bigints@1.1.0: {} + + has-flag@4.0.0: {} + + has-property-descriptors@1.0.2: + dependencies: + es-define-property: 1.0.1 + + has-proto@1.2.0: + dependencies: + dunder-proto: 1.0.1 + + has-symbols@1.1.0: {} + + has-tostringtag@1.0.2: + dependencies: + has-symbols: 1.1.0 + + hasown@2.0.4: + dependencies: + function-bind: 1.1.2 + + hermes-estree@0.25.1: {} + + hermes-parser@0.25.1: + dependencies: + hermes-estree: 0.25.1 + + html-encoding-sniffer@4.0.0: + dependencies: + whatwg-encoding: 3.1.1 + + html-encoding-sniffer@6.0.0: + dependencies: + '@exodus/bytes': 1.16.0 + transitivePeerDependencies: + - '@noble/hashes' + optional: true + + http-proxy-agent@7.0.2: + dependencies: + agent-base: 7.1.4 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + + https-proxy-agent@7.0.6: + dependencies: + agent-base: 7.1.4 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + + iconv-lite@0.6.3: + dependencies: + safer-buffer: 2.1.2 + + ignore@5.3.2: {} + + ignore@7.0.10: {} + + import-fresh@3.3.1: + dependencies: + parent-module: 1.0.1 + resolve-from: 4.0.0 + + imurmurhash@0.1.4: {} + + indent-string@4.0.0: {} + + internal-slot@1.1.0: + dependencies: + es-errors: 1.3.0 + hasown: 2.0.4 + side-channel: 1.1.1 + + is-array-buffer@3.0.5: + dependencies: + call-bind: 1.0.9 + call-bound: 1.0.4 + get-intrinsic: 1.3.0 + + is-async-function@2.1.1: + dependencies: + async-function: 1.0.0 + call-bound: 1.0.4 + get-proto: 1.0.1 + has-tostringtag: 1.0.2 + safe-regex-test: 1.1.0 + + is-bigint@1.1.0: + dependencies: + has-bigints: 1.1.0 + + is-boolean-object@1.2.2: + dependencies: + call-bound: 1.0.4 + has-tostringtag: 1.0.2 + + is-bun-module@2.0.0: + dependencies: + semver: 7.8.5 + + is-callable@1.2.7: {} + + is-core-module@2.17.0: + dependencies: + hasown: 2.0.4 + + is-data-view@1.0.2: + dependencies: + call-bound: 1.0.4 + get-intrinsic: 1.3.0 + is-typed-array: 1.1.15 + + is-date-object@1.1.0: + dependencies: + call-bound: 1.0.4 + has-tostringtag: 1.0.2 + + is-document.all@1.0.0: + dependencies: + call-bound: 1.0.4 + + is-extglob@2.1.1: {} + + is-finalizationregistry@1.1.1: + dependencies: + call-bound: 1.0.4 + + is-generator-function@1.1.2: + dependencies: + call-bound: 1.0.4 + generator-function: 2.0.1 + get-proto: 1.0.1 + has-tostringtag: 1.0.2 + safe-regex-test: 1.1.0 + + is-glob@4.0.3: + dependencies: + is-extglob: 2.1.1 + + is-map@2.0.3: {} + + is-negative-zero@2.0.3: {} + + is-number-object@1.1.1: + dependencies: + call-bound: 1.0.4 + has-tostringtag: 1.0.2 + + is-number@7.0.0: {} + + is-potential-custom-element-name@1.0.1: {} + + is-regex@1.2.1: + dependencies: + call-bound: 1.0.4 + gopd: 1.2.0 + has-tostringtag: 1.0.2 + hasown: 2.0.4 + + is-set@2.0.3: {} + + is-shared-array-buffer@1.0.4: + dependencies: + call-bound: 1.0.4 + + is-string@1.1.1: + dependencies: + call-bound: 1.0.4 + has-tostringtag: 1.0.2 + + is-symbol@1.1.1: + dependencies: + call-bound: 1.0.4 + has-symbols: 1.1.0 + safe-regex-test: 1.1.0 + + is-typed-array@1.1.15: + dependencies: + which-typed-array: 1.1.24 + + is-weakmap@2.0.2: {} + + is-weakref@1.1.1: + dependencies: + call-bound: 1.0.4 + + is-weakset@2.0.4: + dependencies: + call-bound: 1.0.4 + get-intrinsic: 1.3.0 + + isarray@2.0.5: {} + + isexe@2.0.0: {} + + iterator.prototype@1.1.5: + dependencies: + define-data-property: 1.1.4 + es-object-atoms: 1.1.2 + get-intrinsic: 1.3.0 + get-proto: 1.0.1 + has-symbols: 1.1.0 + set-function-name: 2.0.2 + + js-tokens@4.0.0: {} + + js-tokens@9.0.1: {} + + js-yaml@4.3.2: + dependencies: + argparse: 2.0.1 + + jsdom@26.1.0: + dependencies: + cssstyle: 4.6.0 + data-urls: 5.0.0 + decimal.js: 10.6.0 + html-encoding-sniffer: 4.0.0 + http-proxy-agent: 7.0.2 + https-proxy-agent: 7.0.6 + is-potential-custom-element-name: 1.0.1 + nwsapi: 2.2.28 + parse5: 7.3.0 + rrweb-cssom: 0.8.0 + saxes: 6.0.0 + symbol-tree: 3.2.4 + tough-cookie: 5.1.2 + w3c-xmlserializer: 5.0.0 + webidl-conversions: 7.0.0 + whatwg-encoding: 3.1.1 + whatwg-mimetype: 4.0.0 + whatwg-url: 14.2.0 + ws: 8.22.0 + xml-name-validator: 5.0.0 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + + jsdom@27.4.0: + dependencies: + '@acemir/cssom': 0.9.31 + '@asamuzakjp/dom-selector': 6.8.1 + '@exodus/bytes': 1.16.0 + cssstyle: 5.3.7 + data-urls: 6.0.1 + decimal.js: 10.6.0 + html-encoding-sniffer: 6.0.0 + http-proxy-agent: 7.0.2 + https-proxy-agent: 7.0.6 + is-potential-custom-element-name: 1.0.1 + parse5: 8.0.1 + saxes: 6.0.0 + symbol-tree: 3.2.4 + tough-cookie: 6.0.2 + w3c-xmlserializer: 5.0.0 + webidl-conversions: 8.0.1 + whatwg-mimetype: 4.0.0 + whatwg-url: 15.1.0 + ws: 8.22.0 + xml-name-validator: 5.0.0 + transitivePeerDependencies: + - '@noble/hashes' + - bufferutil + - supports-color + - utf-8-validate + optional: true + + jsesc@3.1.0: {} + + json-buffer@3.0.1: {} + + json-schema-traverse@0.4.1: {} + + json-stable-stringify-without-jsonify@1.0.1: {} + + json5@1.0.2: + dependencies: + minimist: 1.2.8 + + json5@2.2.3: {} + + jsx-ast-utils@3.3.5: + dependencies: + array-includes: 3.2.0 + array.prototype.flat: 1.3.3 + object.assign: 4.1.7 + object.values: 1.2.1 + + keyv@4.5.4: + dependencies: + json-buffer: 3.0.1 + + language-subtag-registry@0.3.23: {} + + language-tags@1.0.9: + dependencies: + language-subtag-registry: 0.3.23 + + levn@0.4.1: + dependencies: + prelude-ls: 1.2.1 + type-check: 0.4.0 + + locate-path@6.0.0: + dependencies: + p-locate: 5.0.0 + + lodash.merge@4.6.2: {} + + loose-envify@1.4.0: + dependencies: + js-tokens: 4.0.0 + + loupe@3.2.1: {} + + lru-cache@10.4.3: {} + + lru-cache@11.5.3: + optional: true + + lru-cache@5.1.1: + dependencies: + yallist: 3.1.1 + + lz-string@1.5.0: {} + + magic-string@0.30.21: + dependencies: + '@jridgewell/sourcemap-codec': 1.6.0 + + math-intrinsics@1.1.0: {} + + mdn-data@2.27.1: + optional: true + + merge2@1.4.1: {} + + micromatch@4.0.8: + dependencies: + braces: 3.0.3 + picomatch: 2.3.2 + + min-indent@1.0.1: {} + + minimatch@10.2.6: + dependencies: + brace-expansion: 5.0.12 + + minimatch@3.1.5: + dependencies: + brace-expansion: 1.1.21 + + minimist@1.2.8: {} + + ms@2.1.3: {} + + nanoid@3.3.19: {} + + napi-postinstall@0.3.4: {} + + natural-compare@1.4.0: {} + + next@16.3.6(@babel/core@7.29.7)(@playwright/test@1.55.1)(@types/node@22.18.6)(react-dom@19.3.0(react@19.3.0))(react@19.3.0): + dependencies: + '@next/env': 16.3.6 + '@swc/helpers': 0.5.23 + baseline-browser-mapping: 2.11.26 + caniuse-lite: 1.0.30001813 + postcss: 8.5.23 + react: 19.3.0 + react-dom: 19.3.0(react@19.3.0) + styled-jsx: 5.1.6(@babel/core@7.29.7)(react@19.3.0) + optionalDependencies: + '@next/swc-darwin-arm64': 16.3.6 + '@next/swc-darwin-x64': 16.3.6 + '@next/swc-linux-arm64-gnu': 16.3.6 + '@next/swc-linux-arm64-musl': 16.3.6 + '@next/swc-linux-x64-gnu': 16.3.6 + '@next/swc-linux-x64-musl': 16.3.6 + '@next/swc-win32-arm64-msvc': 16.3.6 + '@next/swc-win32-x64-msvc': 16.3.6 + '@playwright/test': 1.55.1 + sharp: 0.35.5(@types/node@22.18.6) + transitivePeerDependencies: + - '@babel/core' + - '@types/node' + - babel-plugin-macros + + node-exports-info@1.6.2: + dependencies: + array.prototype.flatmap: 1.3.3 + es-errors: 1.3.0 + object.entries: 1.1.9 + semver: 6.3.1 + + node-releases@2.0.57: {} + + nwsapi@2.2.28: {} + + object-assign@4.1.1: {} + + object-inspect@1.13.4: {} + + object-keys@1.1.1: {} + + object.assign@4.1.7: + dependencies: + call-bind: 1.0.9 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-object-atoms: 1.1.2 + has-symbols: 1.1.0 + object-keys: 1.1.1 + + object.entries@1.1.9: + dependencies: + call-bind: 1.0.9 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-object-atoms: 1.1.2 + + object.fromentries@2.0.8: + dependencies: + call-bind: 1.0.9 + define-properties: 1.2.1 + es-abstract: 1.24.2 + es-object-atoms: 1.1.2 + + object.groupby@1.0.3: + dependencies: + call-bind: 1.0.9 + define-properties: 1.2.1 + es-abstract: 1.24.2 + + object.values@1.2.1: + dependencies: + call-bind: 1.0.9 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-object-atoms: 1.1.2 + + optionator@0.9.4: + dependencies: + deep-is: 0.1.4 + fast-levenshtein: 2.0.6 + levn: 0.4.1 + prelude-ls: 1.2.1 + type-check: 0.4.0 + word-wrap: 1.2.5 + + own-keys@1.0.2: + dependencies: + call-bound: 1.0.4 + get-intrinsic: 1.3.0 + object-keys: 1.1.1 + safe-push-apply: 1.0.0 + + p-limit@3.1.0: + dependencies: + yocto-queue: 0.1.0 + + p-locate@5.0.0: + dependencies: + p-limit: 3.1.0 + + parent-module@1.0.1: + dependencies: + callsites: 3.1.0 + + parse5@7.3.0: + dependencies: + entities: 6.0.1 + + parse5@8.0.1: + dependencies: + entities: 8.1.0 + optional: true + + path-exists@4.0.0: {} + + path-key@3.1.1: {} + + path-parse@1.0.7: {} + + pathe@2.0.3: {} + + pathval@2.0.1: {} + + picocolors@1.1.1: {} + + picomatch@2.3.2: {} + + picomatch@4.0.7: {} + + playwright-core@1.55.1: {} + + playwright@1.55.1: + dependencies: + playwright-core: 1.55.1 + optionalDependencies: + fsevents: 2.3.2 + + possible-typed-array-names@1.1.0: {} + + postcss@8.5.23: + dependencies: + nanoid: 3.3.19 + picocolors: 1.1.1 + source-map-js: 1.2.1 + + postcss@8.5.28: + dependencies: + nanoid: 3.3.19 + picocolors: 1.1.1 + source-map-js: 1.2.1 + + prelude-ls@1.2.1: {} + + pretty-format@27.5.1: + dependencies: + ansi-regex: 5.0.1 + ansi-styles: 5.2.0 + react-is: 17.0.2 + + prop-types@15.8.1: + dependencies: + loose-envify: 1.4.0 + object-assign: 4.1.1 + react-is: 16.13.1 + + punycode@2.3.1: {} + + queue-microtask@1.2.3: {} + + react-dom@19.3.0(react@19.3.0): + dependencies: + react: 19.3.0 + scheduler: 0.28.0 + + react-is@16.13.1: {} + + react-is@17.0.2: {} + + react@19.3.0: {} + + redent@3.0.0: + dependencies: + indent-string: 4.0.0 + strip-indent: 3.0.0 + + reflect.getprototypeof@1.0.10: + dependencies: + call-bind: 1.0.9 + define-properties: 1.2.1 + es-abstract: 1.24.2 + es-errors: 1.3.0 + es-object-atoms: 1.1.2 + get-intrinsic: 1.3.0 + get-proto: 1.0.1 + which-builtin-type: 1.2.1 + + regexp.prototype.flags@1.5.4: + dependencies: + call-bind: 1.0.9 + define-properties: 1.2.1 + es-errors: 1.3.0 + get-proto: 1.0.1 + gopd: 1.2.0 + set-function-name: 2.0.2 + + require-from-string@2.0.2: + optional: true + + resolve-from@4.0.0: {} + + resolve-pkg-maps@1.0.0: {} + + resolve@2.0.0-next.7: + dependencies: + es-errors: 1.3.0 + is-core-module: 2.17.0 + node-exports-info: 1.6.2 + object-keys: 1.1.1 + path-parse: 1.0.7 + supports-preserve-symlinks-flag: 1.0.0 + + reusify@1.1.0: {} + + rollup@4.63.5: + dependencies: + '@types/estree': 1.0.9 + optionalDependencies: + '@napi-rs/lzma-linux-x64-gnu': 1.5.1 + '@rollup/rollup-android-arm-eabi': 4.63.5 + '@rollup/rollup-android-arm64': 4.63.5 + '@rollup/rollup-darwin-arm64': 4.63.5 + '@rollup/rollup-darwin-x64': 4.63.5 + '@rollup/rollup-freebsd-arm64': 4.63.5 + '@rollup/rollup-freebsd-x64': 4.63.5 + '@rollup/rollup-linux-arm-gnueabihf': 4.63.5 + '@rollup/rollup-linux-arm-musleabihf': 4.63.5 + '@rollup/rollup-linux-arm64-gnu': 4.63.5 + '@rollup/rollup-linux-arm64-musl': 4.63.5 + '@rollup/rollup-linux-loong64-gnu': 4.63.5 + '@rollup/rollup-linux-loong64-musl': 4.63.5 + '@rollup/rollup-linux-ppc64-gnu': 4.63.5 + '@rollup/rollup-linux-ppc64-musl': 4.63.5 + '@rollup/rollup-linux-riscv64-gnu': 4.63.5 + '@rollup/rollup-linux-riscv64-musl': 4.63.5 + '@rollup/rollup-linux-s390x-gnu': 4.63.5 + '@rollup/rollup-linux-x64-gnu': 4.63.5 + '@rollup/rollup-linux-x64-musl': 4.63.5 + '@rollup/rollup-openbsd-x64': 4.63.5 + '@rollup/rollup-openharmony-arm64': 4.63.5 + '@rollup/rollup-win32-arm64-msvc': 4.63.5 + '@rollup/rollup-win32-ia32-msvc': 4.63.5 + '@rollup/rollup-win32-x64-gnu': 4.63.5 + '@rollup/rollup-win32-x64-msvc': 4.63.5 + fsevents: 2.3.3 + + rrweb-cssom@0.8.0: {} + + run-parallel@1.2.0: + dependencies: + queue-microtask: 1.2.3 + + safe-array-concat@1.1.4: + dependencies: + call-bind: 1.0.9 + call-bound: 1.0.4 + get-intrinsic: 1.3.0 + has-symbols: 1.1.0 + isarray: 2.0.5 + + safe-push-apply@1.0.0: + dependencies: + es-errors: 1.3.0 + isarray: 2.0.5 + + safe-regex-test@1.1.0: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + is-regex: 1.2.1 + + safer-buffer@2.1.2: {} + + saxes@6.0.0: + dependencies: + xmlchars: 2.2.0 + + scheduler@0.28.0: {} + + semver@6.3.1: {} + + semver@7.8.5: {} + + server-only@0.0.1: {} + + set-function-length@1.2.2: + dependencies: + define-data-property: 1.1.4 + es-errors: 1.3.0 + function-bind: 1.1.2 + get-intrinsic: 1.3.0 + gopd: 1.2.0 + has-property-descriptors: 1.0.2 + + set-function-name@2.0.2: + dependencies: + define-data-property: 1.1.4 + es-errors: 1.3.0 + functions-have-names: 1.2.3 + has-property-descriptors: 1.0.2 + + set-proto@1.0.0: + dependencies: + dunder-proto: 1.0.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.2 + + sharp@0.35.5(@types/node@22.18.6): + dependencies: + '@img/colour': 1.1.0 + detect-libc: 2.1.2 + semver: 7.8.5 + optionalDependencies: + '@img/sharp-darwin-arm64': 0.35.5 + '@img/sharp-darwin-x64': 0.35.5 + '@img/sharp-freebsd-wasm32': 0.35.5 + '@img/sharp-libvips-darwin-arm64': 1.3.4 + '@img/sharp-libvips-darwin-x64': 1.3.4 + '@img/sharp-libvips-linux-arm': 1.3.4 + '@img/sharp-libvips-linux-arm64': 1.3.4 + '@img/sharp-libvips-linux-ppc64': 1.3.4 + '@img/sharp-libvips-linux-riscv64': 1.3.4 + '@img/sharp-libvips-linux-s390x': 1.3.4 + '@img/sharp-libvips-linux-x64': 1.3.4 + '@img/sharp-libvips-linuxmusl-arm64': 1.3.4 + '@img/sharp-libvips-linuxmusl-x64': 1.3.4 + '@img/sharp-linux-arm': 0.35.5 + '@img/sharp-linux-arm64': 0.35.5 + '@img/sharp-linux-ppc64': 0.35.5 + '@img/sharp-linux-riscv64': 0.35.5 + '@img/sharp-linux-s390x': 0.35.5 + '@img/sharp-linux-x64': 0.35.5 + '@img/sharp-linuxmusl-arm64': 0.35.5 + '@img/sharp-linuxmusl-x64': 0.35.5 + '@img/sharp-webcontainers-wasm32': 0.35.5 + '@img/sharp-win32-arm64': 0.35.5 + '@img/sharp-win32-ia32': 0.35.5 + '@img/sharp-win32-x64': 0.35.5 + '@types/node': 22.18.6 + optional: true + + shebang-command@2.0.0: + dependencies: + shebang-regex: 3.0.0 + + shebang-regex@3.0.0: {} + + side-channel-list@1.0.1: + dependencies: + es-errors: 1.3.0 + object-inspect: 1.13.4 + + side-channel-map@1.0.1: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + object-inspect: 1.13.4 + + side-channel-weakmap@1.0.2: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + object-inspect: 1.13.4 + side-channel-map: 1.0.1 + + side-channel@1.1.1: + dependencies: + es-errors: 1.3.0 + object-inspect: 1.13.4 + side-channel-list: 1.0.1 + side-channel-map: 1.0.1 + side-channel-weakmap: 1.0.2 + + siginfo@2.0.0: {} + + source-map-js@1.2.1: {} + + stable-hash@0.0.5: {} + + stackback@0.0.2: {} + + std-env@3.10.0: {} + + stop-iteration-iterator@1.1.0: + dependencies: + es-errors: 1.3.0 + internal-slot: 1.1.0 + + string.prototype.includes@2.0.1: + dependencies: + call-bind: 1.0.9 + define-properties: 1.2.1 + es-abstract: 1.24.2 + + string.prototype.matchall@4.1.0: + dependencies: + call-bind: 1.0.9 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-abstract: 1.24.2 + es-errors: 1.3.0 + es-object-atoms: 1.1.2 + get-intrinsic: 1.3.0 + gopd: 1.2.0 + has-symbols: 1.1.0 + internal-slot: 1.1.0 + regexp.prototype.flags: 1.5.4 + set-function-name: 2.0.2 + side-channel: 1.1.1 + + string.prototype.repeat@1.0.0: + dependencies: + define-properties: 1.2.1 + es-abstract: 1.24.2 + + string.prototype.trim@1.2.11: + dependencies: + call-bind: 1.0.9 + call-bound: 1.0.4 + define-data-property: 1.1.4 + define-properties: 1.2.1 + es-abstract: 1.24.2 + es-object-atoms: 1.1.2 + has-property-descriptors: 1.0.2 + safe-regex-test: 1.1.0 + + string.prototype.trimend@1.0.10: + dependencies: + call-bind: 1.0.9 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-object-atoms: 1.1.2 + + string.prototype.trimstart@1.0.8: + dependencies: + call-bind: 1.0.9 + define-properties: 1.2.1 + es-object-atoms: 1.1.2 + + strip-bom@3.0.0: {} + + strip-indent@3.0.0: + dependencies: + min-indent: 1.0.1 + + strip-json-comments@3.1.1: {} + + strip-literal@3.1.0: + dependencies: + js-tokens: 9.0.1 + + styled-jsx@5.1.6(@babel/core@7.29.7)(react@19.3.0): + dependencies: + client-only: 0.0.1 + react: 19.3.0 + optionalDependencies: + '@babel/core': 7.29.7 + + supports-color@7.2.0: + dependencies: + has-flag: 4.0.0 + + supports-preserve-symlinks-flag@1.0.0: {} + + symbol-tree@3.2.4: {} + + tinybench@2.9.0: {} + + tinyexec@0.3.2: {} + + tinyglobby@0.2.17: + dependencies: + fdir: 6.5.0(picomatch@4.0.7) + picomatch: 4.0.7 + + tinypool@1.1.1: {} + + tinyrainbow@2.0.0: {} + + tinyrainbow@3.1.1: {} + + tinyspy@4.0.6: {} + + tldts-core@6.1.86: {} + + tldts-core@7.4.16: + optional: true + + tldts@6.1.86: + dependencies: + tldts-core: 6.1.86 + + tldts@7.4.16: + dependencies: + tldts-core: 7.4.16 + optional: true + + to-regex-range@5.0.1: + dependencies: + is-number: 7.0.0 + + tough-cookie@5.1.2: + dependencies: + tldts: 6.1.86 + + tough-cookie@6.0.2: + dependencies: + tldts: 7.4.16 + optional: true + + tr46@5.1.1: + dependencies: + punycode: 2.3.1 + + tr46@6.0.0: + dependencies: + punycode: 2.3.1 + optional: true + + ts-api-utils@2.5.0(typescript@5.9.3): + dependencies: + typescript: 5.9.3 + + tsconfig-paths@3.15.0: + dependencies: + '@types/json5': 0.0.29 + json5: 1.0.2 + minimist: 1.2.8 + strip-bom: 3.0.0 + + tslib@2.8.1: {} + + type-check@0.4.0: + dependencies: + prelude-ls: 1.2.1 + + typed-array-buffer@1.0.3: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + is-typed-array: 1.1.15 + + typed-array-byte-length@1.0.3: + dependencies: + call-bind: 1.0.9 + for-each: 0.3.5 + gopd: 1.2.0 + has-proto: 1.2.0 + is-typed-array: 1.1.15 + + typed-array-byte-offset@1.0.5: + dependencies: + available-typed-arrays: 1.0.7 + call-bind: 1.0.9 + for-each: 0.3.5 + gopd: 1.2.0 + is-typed-array: 1.1.15 + reflect.getprototypeof: 1.0.10 + + typed-array-length@1.0.8: + dependencies: + call-bind: 1.0.9 + for-each: 0.3.5 + gopd: 1.2.0 + is-typed-array: 1.1.15 + possible-typed-array-names: 1.1.0 + reflect.getprototypeof: 1.0.10 + + typescript-eslint@8.71.0(eslint@9.39.5)(typescript@5.9.3): + dependencies: + '@typescript-eslint/eslint-plugin': 8.71.0(@typescript-eslint/parser@8.71.0(eslint@9.39.5)(typescript@5.9.3))(eslint@9.39.5)(typescript@5.9.3) + '@typescript-eslint/parser': 8.71.0(eslint@9.39.5)(typescript@5.9.3) + '@typescript-eslint/typescript-estree': 8.71.0(typescript@5.9.3) + '@typescript-eslint/utils': 8.71.0(eslint@9.39.5)(typescript@5.9.3) + eslint: 9.39.5 + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + typescript@5.4.5: {} + + typescript@5.9.3: {} + + unbox-primitive@1.1.0: + dependencies: + call-bound: 1.0.4 + has-bigints: 1.1.0 + has-symbols: 1.1.0 + which-boxed-primitive: 1.1.1 + + undici-types@6.21.0: {} + + unrs-resolver@1.12.2: + dependencies: + napi-postinstall: 0.3.4 + optionalDependencies: + '@unrs/resolver-binding-android-arm-eabi': 1.12.2 + '@unrs/resolver-binding-android-arm64': 1.12.2 + '@unrs/resolver-binding-darwin-arm64': 1.12.2 + '@unrs/resolver-binding-darwin-x64': 1.12.2 + '@unrs/resolver-binding-freebsd-x64': 1.12.2 + '@unrs/resolver-binding-linux-arm-gnueabihf': 1.12.2 + '@unrs/resolver-binding-linux-arm-musleabihf': 1.12.2 + '@unrs/resolver-binding-linux-arm64-gnu': 1.12.2 + '@unrs/resolver-binding-linux-arm64-musl': 1.12.2 + '@unrs/resolver-binding-linux-loong64-gnu': 1.12.2 + '@unrs/resolver-binding-linux-loong64-musl': 1.12.2 + '@unrs/resolver-binding-linux-ppc64-gnu': 1.12.2 + '@unrs/resolver-binding-linux-riscv64-gnu': 1.12.2 + '@unrs/resolver-binding-linux-riscv64-musl': 1.12.2 + '@unrs/resolver-binding-linux-s390x-gnu': 1.12.2 + '@unrs/resolver-binding-linux-x64-gnu': 1.12.2 + '@unrs/resolver-binding-linux-x64-musl': 1.12.2 + '@unrs/resolver-binding-openharmony-arm64': 1.12.2 + '@unrs/resolver-binding-wasm32-wasi': 1.12.2 + '@unrs/resolver-binding-win32-arm64-msvc': 1.12.2 + '@unrs/resolver-binding-win32-ia32-msvc': 1.12.2 + '@unrs/resolver-binding-win32-x64-msvc': 1.12.2 + + update-browserslist-db@1.3.3(browserslist@4.29.2): + dependencies: + browserslist: 4.29.2 + escalade: 3.2.0 + picocolors: 1.1.1 + + uri-js@4.4.1: + dependencies: + punycode: 2.3.1 + + vite-node@3.2.4(@types/node@22.18.6): + dependencies: + cac: 6.7.14 + debug: 4.4.3 + es-module-lexer: 1.7.0 + pathe: 2.0.3 + vite: 7.3.6(@types/node@22.18.6) + transitivePeerDependencies: + - '@types/node' + - jiti + - less + - lightningcss + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + - tsx + - yaml + + vite@7.3.6(@types/node@22.18.6): + dependencies: + esbuild: 0.28.2 + fdir: 6.5.0(picomatch@4.0.7) + picomatch: 4.0.7 + postcss: 8.5.28 + rollup: 4.63.5 + tinyglobby: 0.2.17 + optionalDependencies: + '@types/node': 22.18.6 + fsevents: 2.3.3 + + vitest@3.2.7(@types/node@22.18.6)(jsdom@26.1.0): + dependencies: + '@types/chai': 5.2.3 + '@vitest/expect': 3.2.7 + '@vitest/mocker': 3.2.7(vite@7.3.6(@types/node@22.18.6)) + '@vitest/pretty-format': 3.2.7 + '@vitest/runner': 3.2.7 + '@vitest/snapshot': 3.2.7 + '@vitest/spy': 3.2.7 + '@vitest/utils': 3.2.7 + chai: 5.3.3 + debug: 4.4.3 + expect-type: 1.4.0 + magic-string: 0.30.21 + pathe: 2.0.3 + picomatch: 4.0.7 + std-env: 3.10.0 + tinybench: 2.9.0 + tinyexec: 0.3.2 + tinyglobby: 0.2.17 + tinypool: 1.1.1 + tinyrainbow: 2.0.0 + vite: 7.3.6(@types/node@22.18.6) + vite-node: 3.2.4(@types/node@22.18.6) + why-is-node-running: 2.3.0 + optionalDependencies: + '@types/node': 22.18.6 + jsdom: 26.1.0 + transitivePeerDependencies: + - jiti + - less + - lightningcss + - msw + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + - tsx + - yaml + + vitest@4.0.7(@types/node@22.18.6)(jsdom@27.4.0): + dependencies: + '@vitest/expect': 4.0.7 + '@vitest/mocker': 4.0.7(vite@7.3.6(@types/node@22.18.6)) + '@vitest/pretty-format': 4.0.7 + '@vitest/runner': 4.0.7 + '@vitest/snapshot': 4.0.7 + '@vitest/spy': 4.0.7 + '@vitest/utils': 4.0.7 + debug: 4.4.3 + es-module-lexer: 1.7.0 + expect-type: 1.4.0 + magic-string: 0.30.21 + pathe: 2.0.3 + picomatch: 4.0.7 + std-env: 3.10.0 + tinybench: 2.9.0 + tinyexec: 0.3.2 + tinyglobby: 0.2.17 + tinyrainbow: 3.1.1 + vite: 7.3.6(@types/node@22.18.6) + why-is-node-running: 2.3.0 + optionalDependencies: + '@types/node': 22.18.6 + jsdom: 27.4.0 + transitivePeerDependencies: + - jiti + - less + - lightningcss + - msw + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + - tsx + - yaml + + w3c-xmlserializer@5.0.0: + dependencies: + xml-name-validator: 5.0.0 + + webidl-conversions@7.0.0: {} + + webidl-conversions@8.0.1: + optional: true + + whatwg-encoding@3.1.1: + dependencies: + iconv-lite: 0.6.3 + + whatwg-mimetype@4.0.0: {} + + whatwg-mimetype@5.0.0: + optional: true + + whatwg-url@14.2.0: + dependencies: + tr46: 5.1.1 + webidl-conversions: 7.0.0 + + whatwg-url@15.1.0: + dependencies: + tr46: 6.0.0 + webidl-conversions: 8.0.1 + optional: true + + which-boxed-primitive@1.1.1: + dependencies: + is-bigint: 1.1.0 + is-boolean-object: 1.2.2 + is-number-object: 1.1.1 + is-string: 1.1.1 + is-symbol: 1.1.1 + + which-builtin-type@1.2.1: + dependencies: + call-bound: 1.0.4 + function.prototype.name: 1.2.0 + has-tostringtag: 1.0.2 + is-async-function: 2.1.1 + is-date-object: 1.1.0 + is-finalizationregistry: 1.1.1 + is-generator-function: 1.1.2 + is-regex: 1.2.1 + is-weakref: 1.1.1 + isarray: 2.0.5 + which-boxed-primitive: 1.1.1 + which-collection: 1.0.2 + which-typed-array: 1.1.24 + + which-collection@1.0.2: + dependencies: + is-map: 2.0.3 + is-set: 2.0.3 + is-weakmap: 2.0.2 + is-weakset: 2.0.4 + + which-typed-array@1.1.24: + dependencies: + available-typed-arrays: 1.0.7 + call-bind: 1.0.9 + call-bound: 1.0.4 + for-each: 0.3.5 + get-proto: 1.0.1 + gopd: 1.2.0 + has-tostringtag: 1.0.2 + + which@2.0.2: + dependencies: + isexe: 2.0.0 + + why-is-node-running@2.3.0: + dependencies: + siginfo: 2.0.0 + stackback: 0.0.2 + + word-wrap@1.2.5: {} + + ws@8.22.0: {} + + xml-name-validator@5.0.0: {} + + xmlchars@2.2.0: {} + + yallist@3.1.1: {} + + yocto-queue@0.1.0: {} + + zod-validation-error@4.0.2(zod@4.6.5): + dependencies: + zod: 4.6.5 + + zod@4.6.5: {} diff --git a/web/pnpm-workspace.yaml b/web/pnpm-workspace.yaml new file mode 100644 index 000000000..3d8f3cbcc --- /dev/null +++ b/web/pnpm-workspace.yaml @@ -0,0 +1,8 @@ +packages: + - api + - submitqueue + - service/* + +onlyBuiltDependencies: + - '@bufbuild/buf' + - esbuild diff --git a/web/service/submitqueue/.gitignore b/web/service/submitqueue/.gitignore new file mode 100644 index 000000000..5c560c0c2 --- /dev/null +++ b/web/service/submitqueue/.gitignore @@ -0,0 +1,3 @@ +.next/ +node_modules/ +tsconfig.tsbuildinfo diff --git a/web/service/submitqueue/Dockerfile b/web/service/submitqueue/Dockerfile new file mode 100644 index 000000000..dc0fc68cb --- /dev/null +++ b/web/service/submitqueue/Dockerfile @@ -0,0 +1,38 @@ +FROM node:22.20.0-bookworm-slim AS dependencies + +ENV PNPM_HOME=/pnpm +ENV PATH=$PNPM_HOME:$PATH +RUN corepack enable && corepack prepare pnpm@10.17.1 --activate + +WORKDIR /workspace + +COPY web/package.json web/pnpm-lock.yaml web/pnpm-workspace.yaml web/tsconfig.base.json ./ +COPY web/api/package.json ./api/package.json +COPY web/submitqueue/package.json ./submitqueue/package.json +COPY web/service/submitqueue/package.json ./service/submitqueue/package.json +RUN pnpm install --frozen-lockfile + +FROM dependencies AS build + +COPY web/api ./api +COPY web/submitqueue ./submitqueue +COPY web/service/submitqueue ./service/submitqueue +RUN pnpm --filter @submitqueue/api build \ + && pnpm --filter @submitqueue/web-submitqueue build \ + && pnpm --filter @submitqueue/service-submitqueue-web build + +FROM node:22.20.0-bookworm-slim AS runtime + +ENV HOSTNAME=0.0.0.0 +ENV NODE_ENV=production +ENV PORT=3000 + +WORKDIR /app + +COPY --chown=node:node --from=build /workspace/service/submitqueue/.next/standalone ./ +COPY --chown=node:node --from=build /workspace/service/submitqueue/.next/static ./service/submitqueue/.next/static + +EXPOSE 3000 + +USER node +CMD ["node", "service/submitqueue/server.js"] diff --git a/web/service/submitqueue/eslint.config.mjs b/web/service/submitqueue/eslint.config.mjs new file mode 100644 index 000000000..db21edbe5 --- /dev/null +++ b/web/service/submitqueue/eslint.config.mjs @@ -0,0 +1,7 @@ +import { defineConfig, globalIgnores } from "eslint/config"; +import nextVitals from "eslint-config-next/core-web-vitals"; + +export default defineConfig([ + ...nextVitals, + globalIgnores([".next/**", "node_modules/**"]), +]); diff --git a/web/service/submitqueue/next-env.d.ts b/web/service/submitqueue/next-env.d.ts new file mode 100644 index 000000000..ce4e94a6b --- /dev/null +++ b/web/service/submitqueue/next-env.d.ts @@ -0,0 +1,7 @@ +/// +/// +import "./.next/types/routes.d.ts"; +import "./.next/types/root-params.d.ts"; + +// NOTE: This file should not be edited +// see https://nextjs.org/docs/app/api-reference/config/typescript for more information. diff --git a/web/service/submitqueue/next.config.ts b/web/service/submitqueue/next.config.ts new file mode 100644 index 000000000..44eba9eb6 --- /dev/null +++ b/web/service/submitqueue/next.config.ts @@ -0,0 +1,13 @@ +import type { NextConfig } from "next"; +import path from "node:path"; + +const nextConfig: NextConfig = { + experimental: { + authInterrupts: true, + }, + output: "standalone", + outputFileTracingRoot: path.join(import.meta.dirname, "../.."), + transpilePackages: ["@submitqueue/api", "@submitqueue/web-submitqueue"], +}; + +export default nextConfig; diff --git a/web/service/submitqueue/package.json b/web/service/submitqueue/package.json new file mode 100644 index 000000000..545fbb8d7 --- /dev/null +++ b/web/service/submitqueue/package.json @@ -0,0 +1,34 @@ +{ + "name": "@submitqueue/service-submitqueue-web", + "version": "0.0.0", + "private": true, + "type": "module", + "scripts": { + "build": "next build", + "dev": "next dev --hostname 0.0.0.0 --port 3000", + "lint": "eslint .", + "start": "next start --hostname 0.0.0.0 --port 3000", + "test": "vitest run", + "typecheck": "tsc --noEmit" + }, + "dependencies": { + "@bufbuild/protobuf": "2.15.0", + "@connectrpc/connect": "^2.1.1", + "@connectrpc/connect-node": "^2.1.1", + "@submitqueue/api": "workspace:*", + "@submitqueue/web-submitqueue": "workspace:*", + "next": ">=16.3.0 <17", + "react": "^19.2.0", + "react-dom": "^19.2.0", + "server-only": "^0.0.1" + }, + "devDependencies": { + "@types/node": "22.18.6", + "@types/react": "^19.1.16", + "@types/react-dom": "^19.1.9", + "eslint": "^9.36.0", + "eslint-config-next": ">=16.3.0 <17", + "typescript": "5.9.3", + "vitest": "4.0.7" + } +} diff --git a/web/service/submitqueue/src/app/(protected)/layout.tsx b/web/service/submitqueue/src/app/(protected)/layout.tsx new file mode 100644 index 000000000..5ed016675 --- /dev/null +++ b/web/service/submitqueue/src/app/(protected)/layout.tsx @@ -0,0 +1,31 @@ +import type { ReactNode } from "react"; +import Link from "next/link"; + +import { requireAuthorization } from "../../server/request-auth"; + +export const dynamic = "force-dynamic"; +export const revalidate = 0; + +export default async function ProtectedLayout({ + children, +}: Readonly<{ children: ReactNode }>) { + await requireAuthorization(); + + return ( +
+
+ + + SubmitQueue + + demo-queue +
+ {children} +
+ Read-only demo · updates automatically +
+
+ ); +} diff --git a/web/service/submitqueue/src/app/(protected)/page.tsx b/web/service/submitqueue/src/app/(protected)/page.tsx new file mode 100644 index 000000000..4be8ef69f --- /dev/null +++ b/web/service/submitqueue/src/app/(protected)/page.tsx @@ -0,0 +1,18 @@ +import { connection } from "next/server"; +import { redirect } from "next/navigation"; + +import { requireAuthorization } from "../../server/request-auth"; +import { + defaultRequestWindow, + requestWindowSearch, +} from "../../server/window"; + +export const dynamic = "force-dynamic"; +export const revalidate = 0; + +export default async function HomePage() { + await connection(); + await requireAuthorization(); + const search = requestWindowSearch(defaultRequestWindow()); + redirect(`/requests?${search.toString()}`); +} diff --git a/web/service/submitqueue/src/app/(protected)/requests/[sqid]/page.tsx b/web/service/submitqueue/src/app/(protected)/requests/[sqid]/page.tsx new file mode 100644 index 000000000..12c1e29f0 --- /dev/null +++ b/web/service/submitqueue/src/app/(protected)/requests/[sqid]/page.tsx @@ -0,0 +1,87 @@ +import { + AutoRefresh, + ErrorState, + RequestDetail, +} from "@submitqueue/web-submitqueue"; +import { + isTerminalStatus, + loadRequestDetail, + WebPaths, +} from "@submitqueue/web-submitqueue/server"; +import type { Metadata } from "next"; +import Link from "next/link"; +import { notFound } from "next/navigation"; +import { connection } from "next/server"; + +import { DEMO_QUEUE } from "../../../../server/config"; +import { resolveDemoGateway } from "../../../../server/gateway"; +import { requireAuthorization } from "../../../../server/request-auth"; + +export const dynamic = "force-dynamic"; +export const revalidate = 0; + +export const metadata: Metadata = { + title: "Request status", +}; + +const paths = new WebPaths(); + +export default async function RequestPage({ + params, +}: Readonly<{ + params: Promise<{ sqid: string }>; +}>) { + await connection(); + await requireAuthorization(); + + const sqid = paths.decodeRequest((await params).sqid); + if (!sqid) { + notFound(); + } + + const result = await loadRequestDetail(resolveDemoGateway, { + queue: DEMO_QUEUE, + sqid, + }); + + if (!result.ok) { + if (result.error.kind === "not-found") { + notFound(); + } + return ( +
+
+
+

Request status

+

Unable to load request

+
+
+
+ + +
+
+ ); + } + + return ( +
+
+
+

+ Requests / Detail +

+

Request status

+

{result.data.request.sqid}

+
+ +
+ +
+ ); +} diff --git a/web/service/submitqueue/src/app/(protected)/requests/page.tsx b/web/service/submitqueue/src/app/(protected)/requests/page.tsx new file mode 100644 index 000000000..3f6896436 --- /dev/null +++ b/web/service/submitqueue/src/app/(protected)/requests/page.tsx @@ -0,0 +1,79 @@ +import { + AutoRefresh, + ErrorState, + RequestList, +} from "@submitqueue/web-submitqueue"; +import { loadRequestList } from "@submitqueue/web-submitqueue/server"; +import type { Metadata } from "next"; +import { redirect } from "next/navigation"; +import { connection } from "next/server"; + +import { DEMO_QUEUE } from "../../../server/config"; +import { resolveDemoGateway } from "../../../server/gateway"; +import { requireAuthorization } from "../../../server/request-auth"; +import { + defaultRequestWindow, + parseRequestWindow, + REQUEST_PAGE_SIZE, + requestWindowSearch, + type RequestSearchParams, +} from "../../../server/window"; + +export const dynamic = "force-dynamic"; +export const revalidate = 0; + +export const metadata: Metadata = { + title: "Requests", +}; + +export default async function RequestsPage({ + searchParams, +}: Readonly<{ + searchParams: Promise; +}>) { + await connection(); + await requireAuthorization(); + + const requestWindow = parseRequestWindow(await searchParams); + if (!requestWindow) { + const canonical = requestWindowSearch(defaultRequestWindow()); + redirect(`/requests?${canonical.toString()}`); + } + + const result = await loadRequestList(resolveDemoGateway, { + queue: DEMO_QUEUE, + receivedAtOrAfterMs: requestWindow.fromMs, + receivedBeforeMs: requestWindow.toMs, + pageSize: REQUEST_PAGE_SIZE, + pageToken: requestWindow.pageToken, + }); + + return ( +
+
+
+

Queue activity

+

Requests

+

+ Requests received in the fixed 24-hour demo window, newest first. +

+
+ +
+ + {result.ok ? ( +
+ +
+ ) : ( +
+ + +
+ )} +
+ ); +} diff --git a/web/service/submitqueue/src/app/globals.css b/web/service/submitqueue/src/app/globals.css new file mode 100644 index 000000000..0b31ece92 --- /dev/null +++ b/web/service/submitqueue/src/app/globals.css @@ -0,0 +1,505 @@ +:root { + color-scheme: light; + --bg: #f5f7fb; + --surface: #ffffff; + --surface-muted: #eef2f7; + --text: #172033; + --muted: #667085; + --border: #d8dee9; + --accent: #2457d6; + --accent-dark: #183f9f; + --success: #087a4b; + --warning: #a65f00; + --danger: #c9362b; + --shadow: 0 16px 48px rgb(21 35 64 / 8%); + font-family: + Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", + sans-serif; +} + +* { + box-sizing: border-box; +} + +html { + min-width: 320px; + background: var(--bg); +} + +body { + min-height: 100vh; + margin: 0; + color: var(--text); + background: + radial-gradient(circle at 10% 0%, rgb(36 87 214 / 9%), transparent 28rem), + var(--bg); +} + +a { + color: var(--accent); + text-decoration-thickness: 0.08em; + text-underline-offset: 0.16em; +} + +a:hover { + color: var(--accent-dark); +} + +a:focus-visible, +button:focus-visible { + outline: 3px solid rgb(36 87 214 / 35%); + outline-offset: 3px; +} + +.site-frame { + min-height: 100vh; + display: grid; + grid-template-rows: auto 1fr auto; +} + +.site-header, +.site-footer, +.shell { + width: min(1120px, calc(100% - 40px)); + margin-inline: auto; +} + +.site-header { + min-height: 76px; + display: flex; + align-items: center; + justify-content: space-between; + gap: 20px; + border-bottom: 1px solid rgb(216 222 233 / 80%); +} + +.brand { + display: inline-flex; + align-items: center; + gap: 11px; + color: var(--text); + font-size: 1rem; + font-weight: 750; + letter-spacing: -0.01em; + text-decoration: none; +} + +.brand-mark { + width: 34px; + height: 34px; + display: grid; + place-items: center; + border-radius: 9px; + color: white; + background: var(--accent); + font-size: 0.75rem; + letter-spacing: 0.04em; +} + +.environment-pill, +.status-pill { + display: inline-flex; + align-items: center; + width: fit-content; + min-height: 28px; + padding: 4px 10px; + border: 1px solid var(--border); + border-radius: 999px; + color: var(--muted); + background: rgb(255 255 255 / 70%); + font-size: 0.75rem; + font-weight: 650; +} + +.shell { + padding-block: 50px 72px; +} + +.centered-state { + min-height: 100vh; + display: grid; + place-items: center; +} + +.page-heading { + display: flex; + align-items: end; + justify-content: space-between; + gap: 24px; + margin-bottom: 28px; +} + +.page-heading h1, +.state-card h1 { + margin: 0; + font-size: clamp(2rem, 5vw, 3.25rem); + line-height: 1.05; + letter-spacing: -0.045em; +} + +.page-heading p:not(.eyebrow), +.state-card p:not(.eyebrow) { + max-width: 64ch; + margin: 10px 0 0; + color: var(--muted); + line-height: 1.6; +} + +.eyebrow { + margin: 0 0 9px; + color: var(--accent); + font-size: 0.75rem; + font-weight: 800; + letter-spacing: 0.09em; + text-transform: uppercase; +} + +.panel, +.state-card { + border: 1px solid var(--border); + border-radius: 18px; + background: rgb(255 255 255 / 92%); + box-shadow: var(--shadow); +} + +.panel { + overflow: hidden; +} + +.state-card { + max-width: 600px; + padding: clamp(28px, 6vw, 52px); +} + +.button-link { + display: inline-flex; + margin-top: 24px; + padding: 10px 15px; + border-radius: 9px; + color: white; + background: var(--accent); + font-weight: 700; + text-decoration: none; +} + +.button-link:hover { + color: white; + background: var(--accent-dark); +} + +.pagination { + display: flex; + justify-content: flex-end; + margin-top: 24px; +} + +.request-identifier { + font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace; + overflow-wrap: anywhere; +} + +.sq-refresh { + display: flex; + min-height: 38px; + align-items: center; + gap: 10px; + color: var(--muted); + font-size: 0.75rem; +} + +.sq-refresh button { + min-height: 38px; + padding: 8px 13px; + border: 1px solid var(--border); + border-radius: 9px; + color: var(--text); + background: var(--surface); + font: inherit; + font-weight: 700; + cursor: pointer; +} + +.sq-refresh button:hover:not(:disabled) { + border-color: var(--accent); + color: var(--accent); +} + +.sq-refresh button:disabled { + cursor: default; + opacity: 0.6; +} + +.sq-request-list, +.sq-history, +.sq-changes { + padding: 0; + margin: 0; + list-style: none; +} + +.sq-request-list__item + .sq-request-list__item { + border-top: 1px solid var(--border); +} + +.sq-request-list__item article { + padding: 23px 26px; +} + +.sq-request-list__item header { + display: flex; + align-items: center; + justify-content: space-between; + gap: 16px; +} + +.sq-request-list__item header a { + color: var(--text); + font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace; + font-size: 0.92rem; + font-weight: 750; + overflow-wrap: anywhere; +} + +.sq-request-list__item p { + margin: 12px 0 0; + color: var(--muted); + font-size: 0.82rem; +} + +.sq-changes { + display: grid; + gap: 7px; + margin-top: 15px; +} + +.sq-changes code { + display: block; + width: fit-content; + max-width: 100%; + padding: 5px 8px; + border-radius: 6px; + color: #344054; + background: var(--surface-muted); + font-size: 0.78rem; + overflow-wrap: anywhere; + white-space: normal; +} + +.sq-status, +.sq-event { + display: inline-flex; + align-items: center; + width: fit-content; + min-height: 27px; + padding: 4px 10px; + border-radius: 999px; + color: var(--accent-dark); + background: #e8efff; + font-size: 0.72rem; + font-weight: 750; +} + +.sq-status[data-tone="success"] { + color: var(--success); + background: #e5f6ee; +} + +.sq-status[data-tone="warning"] { + color: var(--warning); + background: #fff2da; +} + +.sq-status[data-tone="danger"] { + color: var(--danger); + background: #ffebe9; +} + +.sq-empty { + padding: 52px 26px; + color: var(--muted); + text-align: center; +} + +.sq-error h2 { + margin: 0; + font-size: 1.25rem; +} + +.sq-error p { + margin: 8px 0 0; + color: var(--muted); + line-height: 1.55; +} + +.sq-request-detail { + display: grid; + gap: 18px; +} + +.sq-request-detail > header, +.sq-request-detail > section, +.sq-request-detail > .sq-metadata { + padding: 26px; + border: 1px solid var(--border); + border-radius: 16px; + background: rgb(255 255 255 / 92%); + box-shadow: var(--shadow); +} + +.sq-request-detail > header > p:first-child { + margin-top: 0; + color: var(--muted); + font-size: 0.78rem; + font-weight: 750; + letter-spacing: 0.04em; + text-transform: uppercase; +} + +.sq-request-detail > header h1 { + margin: 9px 0 18px; + font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace; + font-size: clamp(1.15rem, 4vw, 1.75rem); + overflow-wrap: anywhere; +} + +.sq-request-detail > header > p:last-child { + margin: 16px 0 0; + color: var(--muted); + font-size: 0.82rem; +} + +.sq-request-detail section h2 { + margin: 0 0 18px; + font-size: 1rem; +} + +.sq-metadata { + display: grid; + gap: 1px; + margin: 18px 0 0; +} + +.sq-metadata__entry { + display: grid; + grid-template-columns: minmax(120px, 0.35fr) 1fr; + gap: 16px; + padding: 9px 0; + border-bottom: 1px solid var(--border); + font-size: 0.8rem; +} + +.sq-metadata__entry:last-child { + border-bottom: 0; +} + +.sq-metadata dt { + color: var(--muted); + font-weight: 700; +} + +.sq-metadata dd { + margin: 0; + overflow-wrap: anywhere; +} + +.sq-history { + display: grid; + gap: 0; +} + +.sq-history__item { + position: relative; + display: grid; + grid-template-columns: minmax(170px, 0.35fr) 1fr; + gap: 12px 22px; + padding: 17px 0 17px 21px; + border-left: 2px solid var(--border); +} + +.sq-history__item::before { + position: absolute; + top: 22px; + left: -6px; + width: 10px; + height: 10px; + border: 2px solid var(--surface); + border-radius: 50%; + background: var(--accent); + content: ""; +} + +.sq-history__item time { + color: var(--muted); + font-size: 0.78rem; +} + +.sq-history__item p, +.sq-history__item .sq-metadata { + grid-column: 2; +} + +.site-footer { + padding-block: 22px 30px; + border-top: 1px solid rgb(216 222 233 / 80%); + color: var(--muted); + font-size: 0.78rem; +} + +@media (max-width: 680px) { + .site-header, + .site-footer, + .shell { + width: min(100% - 24px, 1120px); + } + + .site-header { + min-height: 64px; + } + + .shell { + padding-block: 34px 52px; + } + + .page-heading { + align-items: start; + flex-direction: column; + } + + .environment-pill { + max-width: 132px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + + .sq-request-list__item article, + .sq-request-detail > header, + .sq-request-detail > section, + .sq-request-detail > .sq-metadata { + padding: 19px; + } + + .sq-request-list__item header { + align-items: flex-start; + flex-direction: column; + } + + .sq-metadata__entry, + .sq-history__item { + grid-template-columns: 1fr; + } + + .sq-history__item p, + .sq-history__item .sq-metadata { + grid-column: 1; + } +} + +@media (prefers-reduced-motion: reduce) { + *, + *::before, + *::after { + scroll-behavior: auto !important; + transition-duration: 0.01ms !important; + } +} diff --git a/web/service/submitqueue/src/app/healthz/route.test.ts b/web/service/submitqueue/src/app/healthz/route.test.ts new file mode 100644 index 000000000..fb9071cf7 --- /dev/null +++ b/web/service/submitqueue/src/app/healthz/route.test.ts @@ -0,0 +1,13 @@ +import { describe, expect, it } from "vitest"; + +import { GET } from "./route"; + +describe("GET /healthz", () => { + it("is a public no-store liveness response", async () => { + const response = GET(); + + expect(response.status).toBe(200); + expect(response.headers.get("cache-control")).toBe("no-store"); + await expect(response.json()).resolves.toEqual({ status: "ok" }); + }); +}); diff --git a/web/service/submitqueue/src/app/healthz/route.ts b/web/service/submitqueue/src/app/healthz/route.ts new file mode 100644 index 000000000..3acb406b0 --- /dev/null +++ b/web/service/submitqueue/src/app/healthz/route.ts @@ -0,0 +1,8 @@ +export const dynamic = "force-dynamic"; + +export function GET(): Response { + return Response.json( + { status: "ok" }, + { headers: { "Cache-Control": "no-store" } }, + ); +} diff --git a/web/service/submitqueue/src/app/layout.tsx b/web/service/submitqueue/src/app/layout.tsx new file mode 100644 index 000000000..239735840 --- /dev/null +++ b/web/service/submitqueue/src/app/layout.tsx @@ -0,0 +1,20 @@ +import type { Metadata } from "next"; +import type { ReactNode } from "react"; + +import "./globals.css"; + +export const metadata: Metadata = { + title: { + default: "SubmitQueue", + template: "%s · SubmitQueue", + }, + description: "Read-only request status for the SubmitQueue demo queue.", +}; + +export default function RootLayout({ children }: { children: ReactNode }) { + return ( + + {children} + + ); +} diff --git a/web/service/submitqueue/src/app/not-found.tsx b/web/service/submitqueue/src/app/not-found.tsx new file mode 100644 index 000000000..5d24db36d --- /dev/null +++ b/web/service/submitqueue/src/app/not-found.tsx @@ -0,0 +1,16 @@ +import Link from "next/link"; + +export default function NotFound() { + return ( +
+
+

Request lookup

+

Request not found

+

The request may not exist in the demo queue or may have expired.

+ + Return to requests + +
+
+ ); +} diff --git a/web/service/submitqueue/src/app/unauthorized.tsx b/web/service/submitqueue/src/app/unauthorized.tsx new file mode 100644 index 000000000..cfd1a9293 --- /dev/null +++ b/web/service/submitqueue/src/app/unauthorized.tsx @@ -0,0 +1,11 @@ +export default function Unauthorized() { + return ( +
+
+

SubmitQueue

+

Authentication required

+

Use the demo credentials configured for this host.

+
+
+ ); +} diff --git a/web/service/submitqueue/src/proxy.test.ts b/web/service/submitqueue/src/proxy.test.ts new file mode 100644 index 000000000..cd82036f7 --- /dev/null +++ b/web/service/submitqueue/src/proxy.test.ts @@ -0,0 +1,46 @@ +import { afterEach, describe, expect, it, vi } from "vitest"; + +import { BASIC_AUTH_USERNAME } from "./server/auth"; +import { proxy } from "./proxy"; + +const token = "demo-token-with-at-least-thirty-two-characters"; + +function request(authorization?: string) { + return { + headers: new Headers( + authorization ? { authorization } : undefined, + ), + } as Parameters[0]; +} + +function basic(username: string, password: string): string { + return `Basic ${Buffer.from(`${username}:${password}`).toString("base64")}`; +} + +describe("proxy", () => { + afterEach(() => { + vi.unstubAllEnvs(); + }); + + it("challenges an anonymous browser", () => { + vi.stubEnv("SUBMITQUEUE_WEB_TOKEN", token); + const response = proxy(request()); + + expect(response?.status).toBe(401); + expect(response?.headers.get("www-authenticate")).toMatch(/^Basic /u); + expect(response?.headers.get("cache-control")).toBe("no-store"); + }); + + it("allows valid demo credentials", () => { + vi.stubEnv("SUBMITQUEUE_WEB_TOKEN", token); + expect(proxy(request(basic(BASIC_AUTH_USERNAME, token)))).toBeUndefined(); + }); + + it("challenges an incorrect password", () => { + vi.stubEnv("SUBMITQUEUE_WEB_TOKEN", token); + expect( + proxy(request(basic(BASIC_AUTH_USERNAME, "incorrect-password-value-000000"))) + ?.status, + ).toBe(401); + }); +}); diff --git a/web/service/submitqueue/src/proxy.ts b/web/service/submitqueue/src/proxy.ts new file mode 100644 index 000000000..8b6dcaffe --- /dev/null +++ b/web/service/submitqueue/src/proxy.ts @@ -0,0 +1,21 @@ +import type { NextRequest } from "next/server"; + +import { + authenticationChallenge, + isAuthorized, + loadAuthConfiguration, +} from "./server/auth"; + +export function proxy(request: NextRequest): Response | undefined { + const configuration = loadAuthConfiguration(); + if (!isAuthorized(request.headers.get("authorization"), configuration)) { + return authenticationChallenge(); + } + return undefined; +} + +export const config = { + matcher: [ + "/((?!healthz$|_next/static|_next/image|favicon.ico|robots.txt).*)", + ], +}; diff --git a/web/service/submitqueue/src/server/auth.test.ts b/web/service/submitqueue/src/server/auth.test.ts new file mode 100644 index 000000000..e7b66da21 --- /dev/null +++ b/web/service/submitqueue/src/server/auth.test.ts @@ -0,0 +1,54 @@ +import { describe, expect, it } from "vitest"; + +import { + BASIC_AUTH_USERNAME, + isAuthorized, + loadAuthConfiguration, + MINIMUM_TOKEN_LENGTH, +} from "./auth"; + +const token = "a".repeat(MINIMUM_TOKEN_LENGTH); + +function basic(username: string, password: string): string { + return `Basic ${Buffer.from(`${username}:${password}`).toString("base64")}`; +} + +describe("loadAuthConfiguration", () => { + it("loads a sufficiently long token", () => { + expect(loadAuthConfiguration({ SUBMITQUEUE_WEB_TOKEN: token })).toEqual({ + token, + }); + }); + + it.each([undefined, "short"])("rejects a missing or short token", (value) => { + expect(() => + loadAuthConfiguration({ SUBMITQUEUE_WEB_TOKEN: value }), + ).toThrow(/at least 32 characters/u); + }); +}); + +describe("isAuthorized", () => { + it("accepts the configured credentials", () => { + expect( + isAuthorized(basic(BASIC_AUTH_USERNAME, token), { token }), + ).toBe(true); + }); + + it.each([ + ["missing header", null], + ["wrong scheme", `Bearer ${token}`], + ["malformed base64", "Basic !!!"], + ["missing separator", `Basic ${Buffer.from("submitqueue").toString("base64")}`], + ["wrong user", basic("operator", token)], + ["wrong password", basic(BASIC_AUTH_USERNAME, "b".repeat(32))], + ])("rejects %s", (_name, authorization) => { + expect(isAuthorized(authorization, { token })).toBe(false); + }); + + it("allows colons in the password", () => { + const password = `${token}:suffix`; + expect( + isAuthorized(basic(BASIC_AUTH_USERNAME, password), { token: password }), + ).toBe(true); + }); +}); diff --git a/web/service/submitqueue/src/server/auth.ts b/web/service/submitqueue/src/server/auth.ts new file mode 100644 index 000000000..eccf88ba6 --- /dev/null +++ b/web/service/submitqueue/src/server/auth.ts @@ -0,0 +1,86 @@ +import "server-only"; + +import { createHash, timingSafeEqual } from "node:crypto"; + +export const BASIC_AUTH_USERNAME = "submitqueue"; +export const BASIC_AUTH_REALM = "SubmitQueue demo"; +export const MINIMUM_TOKEN_LENGTH = 32; + +export type AuthConfiguration = Readonly<{ + token: string; +}>; + +export function loadAuthConfiguration( + environment: Readonly> = process.env, +): AuthConfiguration { + const token = environment.SUBMITQUEUE_WEB_TOKEN ?? ""; + if (token.length < MINIMUM_TOKEN_LENGTH) { + throw new Error( + `SUBMITQUEUE_WEB_TOKEN must contain at least ${MINIMUM_TOKEN_LENGTH} characters`, + ); + } + return { token }; +} + +function decodeBasicCredentials( + authorization: string | null, +): Readonly<{ username: string; password: string }> | undefined { + if (authorization === null) { + return undefined; + } + + const [scheme, encoded, ...extra] = authorization.trim().split(/\s+/u); + if (scheme?.toLowerCase() !== "basic" || !encoded || extra.length !== 0) { + return undefined; + } + if ( + !/^[A-Za-z0-9+/]+={0,2}$/u.test(encoded) || + encoded.length % 4 === 1 + ) { + return undefined; + } + + const decoded = Buffer.from(encoded, "base64").toString("utf8"); + + const separator = decoded.indexOf(":"); + if (separator < 0) { + return undefined; + } + return { + username: decoded.slice(0, separator), + password: decoded.slice(separator + 1), + }; +} + +function digest(value: string): Buffer { + return createHash("sha256").update(value, "utf8").digest(); +} + +function securelyEqual(actual: string, expected: string): boolean { + return timingSafeEqual(digest(actual), digest(expected)); +} + +export function isAuthorized( + authorization: string | null, + configuration: AuthConfiguration, +): boolean { + const credentials = decodeBasicCredentials(authorization); + if (!credentials) { + return false; + } + return ( + securelyEqual(credentials.username, BASIC_AUTH_USERNAME) && + securelyEqual(credentials.password, configuration.token) + ); +} + +export function authenticationChallenge(): Response { + return new Response("Authentication required", { + status: 401, + headers: { + "Cache-Control": "no-store", + "Content-Type": "text/plain; charset=utf-8", + "WWW-Authenticate": `Basic realm="${BASIC_AUTH_REALM}", charset="UTF-8"`, + }, + }); +} diff --git a/web/service/submitqueue/src/server/config.test.ts b/web/service/submitqueue/src/server/config.test.ts new file mode 100644 index 000000000..7f384068c --- /dev/null +++ b/web/service/submitqueue/src/server/config.test.ts @@ -0,0 +1,40 @@ +import { describe, expect, it } from "vitest"; + +import { loadGatewayConfiguration } from "./config"; + +describe("loadGatewayConfiguration", () => { + it("uses TLS by default", () => { + expect( + loadGatewayConfiguration({ + SUBMITQUEUE_GATEWAY_URL: "https://gateway.example:8081", + }), + ).toEqual({ + baseUrl: "https://gateway.example:8081", + allowPlaintext: false, + }); + }); + + it("allows h2c only when explicitly enabled", () => { + expect( + loadGatewayConfiguration({ + SUBMITQUEUE_GATEWAY_ALLOW_PLAINTEXT: "true", + SUBMITQUEUE_GATEWAY_URL: "http://gateway-service:8081", + }), + ).toEqual({ + baseUrl: "http://gateway-service:8081", + allowPlaintext: true, + }); + }); + + it.each([ + ["missing URL", {}], + ["relative URL", { SUBMITQUEUE_GATEWAY_URL: "gateway:8081" }], + ["implicit plaintext", { SUBMITQUEUE_GATEWAY_URL: "http://gateway:8081" }], + ["unsupported scheme", { SUBMITQUEUE_GATEWAY_URL: "ftp://gateway:8081" }], + ["path", { SUBMITQUEUE_GATEWAY_URL: "https://gateway:8081/grpc" }], + ["query", { SUBMITQUEUE_GATEWAY_URL: "https://gateway:8081?x=1" }], + ["credentials", { SUBMITQUEUE_GATEWAY_URL: "https://user:pass@gateway:8081" }], + ])("rejects %s", (_name, environment) => { + expect(() => loadGatewayConfiguration(environment)).toThrow(); + }); +}); diff --git a/web/service/submitqueue/src/server/config.ts b/web/service/submitqueue/src/server/config.ts new file mode 100644 index 000000000..7050231d4 --- /dev/null +++ b/web/service/submitqueue/src/server/config.ts @@ -0,0 +1,45 @@ +import "server-only"; + +export const DEMO_QUEUE = "demo-queue"; +export const GATEWAY_DEADLINE_MS = 5_000; + +export type GatewayConfiguration = Readonly<{ + baseUrl: string; + allowPlaintext: boolean; +}>; + +export function loadGatewayConfiguration( + environment: Readonly> = process.env, +): GatewayConfiguration { + const rawBaseUrl = environment.SUBMITQUEUE_GATEWAY_URL; + if (!rawBaseUrl) { + throw new Error("SUBMITQUEUE_GATEWAY_URL is required"); + } + + if (!URL.canParse(rawBaseUrl)) { + throw new Error("SUBMITQUEUE_GATEWAY_URL must be a valid absolute URL"); + } + const gatewayUrl = new URL(rawBaseUrl); + + if (gatewayUrl.pathname !== "/" || gatewayUrl.search || gatewayUrl.hash) { + throw new Error("SUBMITQUEUE_GATEWAY_URL must not contain a path, query, or fragment"); + } + if (gatewayUrl.username || gatewayUrl.password) { + throw new Error("SUBMITQUEUE_GATEWAY_URL must not contain credentials"); + } + + const allowPlaintext = environment.SUBMITQUEUE_GATEWAY_ALLOW_PLAINTEXT === "true"; + if (gatewayUrl.protocol === "http:" && !allowPlaintext) { + throw new Error( + "Plaintext gateway connections require SUBMITQUEUE_GATEWAY_ALLOW_PLAINTEXT=true", + ); + } + if (gatewayUrl.protocol !== "https:" && gatewayUrl.protocol !== "http:") { + throw new Error("SUBMITQUEUE_GATEWAY_URL must use https, or explicitly enabled http"); + } + + return { + baseUrl: gatewayUrl.origin, + allowPlaintext, + }; +} diff --git a/web/service/submitqueue/src/server/gateway.ts b/web/service/submitqueue/src/server/gateway.ts new file mode 100644 index 000000000..cece8a254 --- /dev/null +++ b/web/service/submitqueue/src/server/gateway.ts @@ -0,0 +1,28 @@ +import "server-only"; + +import { createClient } from "@connectrpc/connect"; +import { createGrpcTransport } from "@connectrpc/connect-node"; +import { SubmitQueueGateway } from "@submitqueue/api/submitqueue/gateway"; +import type { + GatewayReader, + GatewayResolver, +} from "@submitqueue/web-submitqueue/server"; +import { gatewayReader } from "@submitqueue/web-submitqueue/server"; + +import { GATEWAY_DEADLINE_MS, loadGatewayConfiguration } from "./config"; + +let reader: GatewayReader | undefined; + +function createGatewayReader(): GatewayReader { + const configuration = loadGatewayConfiguration(); + const transport = createGrpcTransport({ + baseUrl: configuration.baseUrl, + defaultTimeoutMs: GATEWAY_DEADLINE_MS, + }); + return gatewayReader(createClient(SubmitQueueGateway, transport)); +} + +export const resolveDemoGateway: GatewayResolver = () => { + reader ??= createGatewayReader(); + return reader; +}; diff --git a/web/service/submitqueue/src/server/request-auth.ts b/web/service/submitqueue/src/server/request-auth.ts new file mode 100644 index 000000000..86dd45a29 --- /dev/null +++ b/web/service/submitqueue/src/server/request-auth.ts @@ -0,0 +1,18 @@ +import "server-only"; + +import { headers } from "next/headers"; +import { unauthorized } from "next/navigation"; + +import { isAuthorized, loadAuthConfiguration } from "./auth"; + +export async function requireAuthorization(): Promise { + const requestHeaders = await headers(); + if ( + !isAuthorized( + requestHeaders.get("authorization"), + loadAuthConfiguration(), + ) + ) { + unauthorized(); + } +} diff --git a/web/service/submitqueue/src/server/window.test.ts b/web/service/submitqueue/src/server/window.test.ts new file mode 100644 index 000000000..70beb589f --- /dev/null +++ b/web/service/submitqueue/src/server/window.test.ts @@ -0,0 +1,41 @@ +import { describe, expect, it } from "vitest"; + +import { + defaultRequestWindow, + parseRequestWindow, + REQUEST_WINDOW_MS, + requestWindowSearch, +} from "./window"; + +describe("request window", () => { + it("creates a fixed trailing 24-hour interval", () => { + expect(defaultRequestWindow(REQUEST_WINDOW_MS + 100)).toEqual({ + fromMs: 100, + toMs: REQUEST_WINDOW_MS + 100, + }); + }); + + it("parses stable bounds and an opaque page token", () => { + expect( + parseRequestWindow({ from: "100", to: "200", page: "token/+=" }), + ).toEqual({ fromMs: 100, toMs: 200, pageToken: "token/+=" }); + }); + + it.each([ + ["missing bounds", {}], + ["duplicate bounds", { from: ["1", "2"], to: "3" }], + ["negative bound", { from: "-1", to: "3" }], + ["fractional bound", { from: "1.5", to: "3" }], + ["unsafe bound", { from: "1", to: "9007199254740992" }], + ["empty interval", { from: "3", to: "3" }], + ["reversed interval", { from: "4", to: "3" }], + ])("rejects %s", (_name, search) => { + expect(parseRequestWindow(search)).toBeUndefined(); + }); + + it("serializes bounds and preserves the page token", () => { + expect( + requestWindowSearch({ fromMs: 100, toMs: 200, pageToken: "a/b+c" }).toString(), + ).toBe("from=100&to=200&page=a%2Fb%2Bc"); + }); +}); diff --git a/web/service/submitqueue/src/server/window.ts b/web/service/submitqueue/src/server/window.ts new file mode 100644 index 000000000..804ffdaf1 --- /dev/null +++ b/web/service/submitqueue/src/server/window.ts @@ -0,0 +1,54 @@ +export const REQUEST_WINDOW_MS = 24 * 60 * 60 * 1_000; +export const REQUEST_PAGE_SIZE = 50; + +export type RequestWindow = Readonly<{ + fromMs: number; + toMs: number; + pageToken?: string; +}>; + +export type RequestSearchParams = Readonly<{ + from?: string | string[]; + to?: string | string[]; + page?: string | string[]; +}>; + +function one(value: string | string[] | undefined): string | undefined { + return Array.isArray(value) ? undefined : value; +} + +function parseMillisecond(value: string | undefined): number | undefined { + if (!value || !/^\d+$/u.test(value)) { + return undefined; + } + const parsed = Number(value); + return Number.isSafeInteger(parsed) && parsed >= 0 ? parsed : undefined; +} + +export function parseRequestWindow( + searchParams: RequestSearchParams, +): RequestWindow | undefined { + const fromMs = parseMillisecond(one(searchParams.from)); + const toMs = parseMillisecond(one(searchParams.to)); + if (fromMs === undefined || toMs === undefined || fromMs >= toMs) { + return undefined; + } + + const pageToken = one(searchParams.page); + return pageToken ? { fromMs, toMs, pageToken } : { fromMs, toMs }; +} + +export function defaultRequestWindow(nowMs = Date.now()): RequestWindow { + return { fromMs: nowMs - REQUEST_WINDOW_MS, toMs: nowMs }; +} + +export function requestWindowSearch(window: RequestWindow): URLSearchParams { + const search = new URLSearchParams({ + from: String(window.fromMs), + to: String(window.toMs), + }); + if (window.pageToken) { + search.set("page", window.pageToken); + } + return search; +} diff --git a/web/service/submitqueue/src/test/server-only.ts b/web/service/submitqueue/src/test/server-only.ts new file mode 100644 index 000000000..cb0ff5c3b --- /dev/null +++ b/web/service/submitqueue/src/test/server-only.ts @@ -0,0 +1 @@ +export {}; diff --git a/web/service/submitqueue/tsconfig.json b/web/service/submitqueue/tsconfig.json new file mode 100644 index 000000000..2523b07a6 --- /dev/null +++ b/web/service/submitqueue/tsconfig.json @@ -0,0 +1,36 @@ +{ + "compilerOptions": { + "allowJs": false, + "esModuleInterop": true, + "incremental": true, + "isolatedModules": true, + "jsx": "react-jsx", + "lib": [ + "dom", + "dom.iterable", + "esnext" + ], + "module": "esnext", + "moduleResolution": "bundler", + "noEmit": true, + "plugins": [ + { + "name": "next" + } + ], + "resolveJsonModule": true, + "skipLibCheck": true, + "strict": true, + "target": "ES2022" + }, + "include": [ + "next-env.d.ts", + "**/*.ts", + "**/*.tsx", + ".next/types/**/*.ts", + ".next/dev/types/**/*.ts" + ], + "exclude": [ + "node_modules" + ] +} diff --git a/web/service/submitqueue/vitest.config.ts b/web/service/submitqueue/vitest.config.ts new file mode 100644 index 000000000..4da82d11b --- /dev/null +++ b/web/service/submitqueue/vitest.config.ts @@ -0,0 +1,13 @@ +import path from "node:path"; +import { defineConfig } from "vitest/config"; + +export default defineConfig({ + resolve: { + alias: { + "server-only": path.resolve(import.meta.dirname, "src/test/server-only.ts"), + }, + }, + test: { + environment: "node", + }, +}); diff --git a/web/submitqueue/.gitignore b/web/submitqueue/.gitignore new file mode 100644 index 000000000..1eae0cf67 --- /dev/null +++ b/web/submitqueue/.gitignore @@ -0,0 +1,2 @@ +dist/ +node_modules/ diff --git a/web/submitqueue/package.json b/web/submitqueue/package.json new file mode 100644 index 000000000..fa0f4fdf9 --- /dev/null +++ b/web/submitqueue/package.json @@ -0,0 +1,53 @@ +{ + "name": "@submitqueue/web-submitqueue", + "version": "0.0.0", + "private": true, + "type": "module", + "sideEffects": false, + "files": [ + "dist" + ], + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.js" + }, + "./server": { + "types": "./dist/server.d.ts", + "import": "./dist/server.js" + }, + "./testing": { + "types": "./dist/testing.d.ts", + "import": "./dist/testing.js" + } + }, + "scripts": { + "build": "node -e \"require('node:fs').rmSync('dist',{recursive:true,force:true})\" && tsc -p tsconfig.build.json", + "lint": "tsc --noEmit", + "test": "vitest run", + "typecheck": "tsc --noEmit" + }, + "dependencies": { + "@submitqueue/api": "workspace:*" + }, + "peerDependencies": { + "@connectrpc/connect": ">=2 <3", + "@bufbuild/protobuf": ">=2 <3", + "next": ">=16.3 <17", + "react": ">=19 <20", + "react-dom": ">=19 <20" + }, + "devDependencies": { + "@connectrpc/connect": "^2.1.1", + "@testing-library/jest-dom": "^6.6.3", + "@testing-library/react": "^16.3.0", + "@types/react": "^19.1.0", + "@types/react-dom": "^19.1.0", + "jsdom": "^26.1.0", + "next": "^16.3.0", + "react": "^19.1.0", + "react-dom": "^19.1.0", + "typescript": "^5.8.3", + "vitest": "^3.1.2" + } +} diff --git a/web/submitqueue/src/auto-refresh.test.tsx b/web/submitqueue/src/auto-refresh.test.tsx new file mode 100644 index 000000000..ee759d35b --- /dev/null +++ b/web/submitqueue/src/auto-refresh.test.tsx @@ -0,0 +1,87 @@ +import { act, fireEvent, render, screen } from "@testing-library/react"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; + +const routerRefresh = vi.fn(); +vi.mock("next/navigation", () => ({ + useRouter: () => ({ refresh: routerRefresh }), +})); + +import { AutoRefresh } from "./auto-refresh"; + +describe("AutoRefresh", () => { + beforeEach(() => { + vi.useFakeTimers(); + routerRefresh.mockReset(); + Object.defineProperty(navigator, "onLine", { configurable: true, value: true }); + Object.defineProperty(document, "visibilityState", { configurable: true, value: "visible" }); + }); + + afterEach(() => vi.useRealTimers()); + + it("uses the interval plus jitter and continues polling", async () => { + const refresh = vi.fn(async () => undefined); + render( 0.5} refresh={refresh} />); + + await act(async () => vi.advanceTimersByTime(2_249)); + expect(refresh).not.toHaveBeenCalled(); + await act(async () => vi.advanceTimersByTime(1)); + expect(refresh).toHaveBeenCalledTimes(1); + await act(async () => vi.advanceTimersByTime(2_250)); + expect(refresh).toHaveBeenCalledTimes(2); + }); + + it("keeps manual and timer refreshes single-flight", async () => { + let finish: (() => void) | undefined; + const refresh = vi.fn(() => new Promise((resolve) => (finish = resolve))); + render(); + + await act(async () => vi.advanceTimersByTime(100)); + fireEvent.click(screen.getByRole("button", { name: "Refreshing…" })); + expect(refresh).toHaveBeenCalledTimes(1); + await act(async () => finish?.()); + }); + + it("backs off transient failures up to the configured cap", async () => { + const refresh = vi.fn(async () => undefined); + render( + 0} + refresh={refresh} + transientFailureCount={4} + />, + ); + + await act(async () => vi.advanceTimersByTime(29_999)); + expect(refresh).not.toHaveBeenCalled(); + await act(async () => vi.advanceTimersByTime(1)); + expect(refresh).toHaveBeenCalledTimes(1); + }); + + it("pauses while offline and resumes when online", async () => { + Object.defineProperty(navigator, "onLine", { configurable: true, value: false }); + const refresh = vi.fn(async () => undefined); + render(); + + expect(screen.getByText("Automatic refresh paused")).toBeTruthy(); + await act(async () => vi.advanceTimersByTime(1_000)); + expect(refresh).not.toHaveBeenCalled(); + Object.defineProperty(navigator, "onLine", { configurable: true, value: true }); + fireEvent(window, new Event("online")); + await act(async () => vi.advanceTimersByTime(100)); + expect(refresh).toHaveBeenCalledTimes(1); + }); + + it("stops automatic polling for terminal detail views", async () => { + const refresh = vi.fn(async () => undefined); + render(); + + await act(async () => vi.advanceTimersByTime(1_000)); + expect(refresh).not.toHaveBeenCalled(); + expect(screen.getByText("Automatic refresh stopped")).toBeTruthy(); + await act(async () => fireEvent.click(screen.getByRole("button", { name: "Refresh" }))); + expect(refresh).toHaveBeenCalledTimes(1); + }); +}); diff --git a/web/submitqueue/src/auto-refresh.tsx b/web/submitqueue/src/auto-refresh.tsx new file mode 100644 index 000000000..86b3dae1a --- /dev/null +++ b/web/submitqueue/src/auto-refresh.tsx @@ -0,0 +1,100 @@ +"use client"; + +import { useCallback, useEffect, useRef, useState } from "react"; +import { useRouter } from "next/navigation"; + +export interface AutoRefreshProps { + terminal?: boolean; + intervalMs?: number; + jitterMs?: number; + maxBackoffMs?: number; + transientFailureCount?: number; + refresh?: () => void | Promise; + random?: () => number; +} + +function pageCanRefresh(): boolean { + return document.visibilityState !== "hidden" && navigator.onLine; +} + +export function AutoRefresh({ + terminal = false, + intervalMs = 2_000, + jitterMs = 500, + maxBackoffMs = 30_000, + transientFailureCount = 0, + refresh, + random = Math.random, +}: AutoRefreshProps) { + const router = useRouter(); + const [refreshing, setRefreshing] = useState(false); + const [paused, setPaused] = useState(false); + const [cycle, setCycle] = useState(0); + const inFlight = useRef(false); + const localFailures = useRef(0); + const timer = useRef | null>(null); + const routerRefresh = useCallback(() => router.refresh(), [router]); + const refreshPage = refresh ?? routerRefresh; + + const runRefresh = useCallback(async () => { + if (inFlight.current || !pageCanRefresh()) { + setPaused(!pageCanRefresh()); + return; + } + inFlight.current = true; + setRefreshing(true); + try { + await refreshPage(); + localFailures.current = 0; + } catch { + localFailures.current += 1; + } finally { + inFlight.current = false; + setRefreshing(false); + setCycle((value) => value + 1); + } + }, [refreshPage]); + + useEffect(() => { + const updateAvailability = () => setPaused(!pageCanRefresh()); + document.addEventListener("visibilitychange", updateAvailability); + window.addEventListener("online", updateAvailability); + window.addEventListener("offline", updateAvailability); + updateAvailability(); + return () => { + document.removeEventListener("visibilitychange", updateAvailability); + window.removeEventListener("online", updateAvailability); + window.removeEventListener("offline", updateAvailability); + }; + }, []); + + useEffect(() => { + if (terminal || paused) { + return; + } + const failures = Math.max(transientFailureCount, localFailures.current); + const multiplier = 2 ** Math.min(failures, 20); + const backoff = Math.min(intervalMs * multiplier, maxBackoffMs); + const delay = backoff + Math.floor(random() * (jitterMs + 1)); + timer.current = setTimeout(() => { + void runRefresh(); + }, delay); + return () => { + if (timer.current !== null) { + clearTimeout(timer.current); + timer.current = null; + } + }; + }, [cycle, intervalMs, jitterMs, maxBackoffMs, paused, random, runRefresh, terminal, transientFailureCount]); + + return ( +
+ + + {terminal ? "Automatic refresh stopped" : paused ? "Automatic refresh paused" : ""} + +
+ ); +} diff --git a/web/submitqueue/src/components.test.tsx b/web/submitqueue/src/components.test.tsx new file mode 100644 index 000000000..9f40fbde7 --- /dev/null +++ b/web/submitqueue/src/components.test.tsx @@ -0,0 +1,100 @@ +import { render, screen, within } from "@testing-library/react"; +import { describe, expect, it } from "vitest"; +import { ErrorState, RequestDetail, RequestList, RequestStatus } from "./components"; +import type { RequestDetailModel, RequestListModel } from "./models"; + +const request = { + sqid: "demo-queue/1", + queue: "demo-queue", + changeUris: ["github://github.com/uber/submitqueue/pull/1/abc"], + receivedAtMs: 1_700_000_000_000, + status: "speculating", + lastError: null, + metadata: { batch: "batch-1" }, +}; + +describe("request components", () => { + it("renders a linked queue request with an encoded sqid", () => { + const model: RequestListModel = { + queue: "demo-queue", + receivedAtOrAfterMs: 1, + receivedBeforeMs: 2, + requests: [request], + nextPageToken: "opaque-token", + }; + render(); + + const link = screen.getByRole("link", { name: "demo-queue/1" }); + expect(link.getAttribute("href")).toBe("/requests/ZGVtby1xdWV1ZS8x"); + expect(screen.getByText("Speculating").getAttribute("data-tone")).toBe("progress"); + expect(screen.getByRole("link", { name: "Next page" }).getAttribute("href")).toBe( + "/requests?from=1&to=2&page=opaque-token", + ); + }); + + it("renders an accessible empty state", () => { + render( + , + ); + expect(screen.getByText("No requests were received in this window.")).toBeTruthy(); + }); + + it("renders summary and ordered status/build history together", () => { + const model: RequestDetailModel = { + request: { ...request, status: "landed" }, + history: [ + { + timestampMs: 1_700_000_000_000, + type: "status", + status: "started", + event: null, + lastError: null, + metadata: {}, + }, + { + timestampMs: 1_700_000_001_000, + type: "event", + status: null, + event: "building", + lastError: null, + metadata: { build_url: "https://build.example/1" }, + }, + ], + historyError: null, + }; + render(); + + expect(screen.getByRole("heading", { level: 1 }).textContent).toContain("demo-queue/1"); + const history = screen.getByRole("list", { name: "Request history" }); + expect(within(history).getAllByRole("listitem")).toHaveLength(2); + expect(within(history).getByText("Building")).toBeTruthy(); + expect(within(history).getByText("https://build.example/1")).toBeTruthy(); + }); + + it("shows unknown statuses safely", () => { + render(); + expect(screen.getByText("New Pipeline Step").getAttribute("data-tone")).toBe("neutral"); + }); + + it("uses an alert for a full-page safe error", () => { + render( + , + ); + expect(screen.getByRole("alert").textContent).toContain("Something went wrong"); + }); +}); diff --git a/web/submitqueue/src/components.tsx b/web/submitqueue/src/components.tsx new file mode 100644 index 000000000..7bd3e1ee1 --- /dev/null +++ b/web/submitqueue/src/components.tsx @@ -0,0 +1,180 @@ +"use client"; + +import type { ReactNode } from "react"; +import type { + HistoryEventModel, + RequestDetailModel, + RequestListModel, + RequestSummaryModel, + WebError, +} from "./models.js"; +import { WebPaths } from "./paths.js"; +import { statusDisplay } from "./status.js"; + +function Timestamp({ value }: { value: number }) { + const date = new Date(value); + if (Number.isNaN(date.getTime())) { + return ; + } + return ; +} + +function Metadata({ values }: { values: Record }) { + const entries = Object.entries(values).sort(([left], [right]) => left.localeCompare(right)); + if (entries.length === 0) { + return null; + } + return ( +
+ {entries.map(([key, value]) => ( +
+
{key}
+
{value}
+
+ ))} +
+ ); +} + +export function RequestStatus({ status }: { status: string }) { + const display = statusDisplay(status); + return ( + + {display.label} + + ); +} + +function RequestChanges({ request }: { request: RequestSummaryModel }) { + return ( +
    + {request.changeUris.map((uri) => ( +
  • + {uri} +
  • + ))} +
+ ); +} + +export interface RequestListProps { + model: RequestListModel; + requestsBasePath?: string; + empty?: ReactNode; +} + +export function RequestList({ model, requestsBasePath, empty }: RequestListProps) { + if (model.requests.length === 0) { + return
{empty ?? "No requests were received in this window."}
; + } + const paths = new WebPaths( + requestsBasePath === undefined ? {} : { requestsPath: requestsBasePath }, + ); + return ( + <> +
    + {model.requests.map((request) => ( +
  1. +
    +
    + {request.sqid} + +
    + +

    + Received +

    + {request.lastError ?

    {request.lastError}

    : null} +
    +
  2. + ))} +
+ {model.nextPageToken ? ( + + ) : null} + + ); +} + +function HistoryValue({ item }: { item: HistoryEventModel }) { + if (item.type === "status" && item.status !== null) { + return ; + } + if (item.type === "event" && item.event !== null) { + return {statusDisplay(item.event).label}; + } + return Unknown event; +} + +export function RequestHistory({ events }: { events: HistoryEventModel[] }) { + if (events.length === 0) { + return

No history has been retained for this request.

; + } + return ( +
    + {events.map((item, index) => ( +
  1. + + + {item.lastError ?

    {item.lastError}

    : null} + +
  2. + ))} +
+ ); +} + +export function ErrorState({ error, compact = false }: { error: WebError; compact?: boolean }) { + return ( +
+

{error.title}

+

{error.message}

+
+ ); +} + +export interface RequestDetailProps { + model: RequestDetailModel; +} + +export function RequestDetail({ model }: RequestDetailProps) { + const { request } = model; + return ( +
+
+

{request.queue}

+

{request.sqid}

+ +

+ Received +

+
+
+

Changes

+ +
+ {request.lastError ? ( +
+

Last error

+

{request.lastError}

+
+ ) : null} + +
+

History

+ {model.historyError ? : null} + +
+
+ ); +} diff --git a/web/submitqueue/src/index.ts b/web/submitqueue/src/index.ts new file mode 100644 index 000000000..a36a9e4ca --- /dev/null +++ b/web/submitqueue/src/index.ts @@ -0,0 +1,32 @@ +"use client"; + +export { AutoRefresh, type AutoRefreshProps } from "./auto-refresh.js"; +export { + ErrorState, + RequestDetail, + type RequestDetailProps, + RequestHistory, + RequestList, + type RequestListProps, + RequestStatus, +} from "./components.js"; +export type { + HistoryEventModel, + HistoryEventType, + LoadResult, + RequestDetailModel, + RequestListModel, + RequestSummaryModel, + StatusDisplay, + StatusTone, + WebError, + WebErrorKind, +} from "./models.js"; +export { + decodeSqidSegment, + encodeSqidSegment, + type RequestsPathOptions, + WebPaths, + type WebPathsOptions, +} from "./paths.js"; +export { isTerminalStatus, knownRequestStatuses, statusDisplay } from "./status.js"; diff --git a/web/submitqueue/src/models.ts b/web/submitqueue/src/models.ts new file mode 100644 index 000000000..556912ae4 --- /dev/null +++ b/web/submitqueue/src/models.ts @@ -0,0 +1,55 @@ +export type StatusTone = "neutral" | "progress" | "success" | "danger" | "warning"; + +export interface StatusDisplay { + label: string; + tone: StatusTone; + terminal: boolean; +} + +export interface RequestSummaryModel { + sqid: string; + queue: string; + changeUris: string[]; + receivedAtMs: number; + status: string; + lastError: string | null; + metadata: Record; +} + +export type HistoryEventType = "status" | "event" | "unknown"; + +export interface HistoryEventModel { + timestampMs: number; + type: HistoryEventType; + status: string | null; + event: string | null; + lastError: string | null; + metadata: Record; +} + +export interface RequestListModel { + queue: string; + receivedAtOrAfterMs: number; + receivedBeforeMs: number; + requests: RequestSummaryModel[]; + nextPageToken: string | null; +} + +export interface RequestDetailModel { + request: RequestSummaryModel; + history: HistoryEventModel[]; + historyError: WebError | null; +} + +export type WebErrorKind = "user" | "not-found" | "transient" | "internal"; + +export interface WebError { + kind: WebErrorKind; + title: string; + message: string; + retryable: boolean; +} + +export type LoadResult = + | { ok: true; data: T } + | { ok: false; error: WebError }; diff --git a/web/submitqueue/src/paths.test.ts b/web/submitqueue/src/paths.test.ts new file mode 100644 index 000000000..1ffbbbd02 --- /dev/null +++ b/web/submitqueue/src/paths.test.ts @@ -0,0 +1,27 @@ +import { describe, expect, it } from "vitest"; +import { decodeSqidSegment, encodeSqidSegment, WebPaths } from "./paths"; + +describe("WebPaths", () => { + it("round-trips a slash-containing sqid in one path segment", () => { + const paths = new WebPaths(); + const href = paths.request("demo-queue/42"); + const segment = href.split("/").at(-1); + + expect(segment).toBeDefined(); + expect(segment).not.toContain("/"); + expect(paths.decodeRequest(segment!)).toBe("demo-queue/42"); + expect(encodeSqidSegment("demo-queue/42")).not.toMatch(/=|\//u); + }); + + it("rejects malformed segments without throwing", () => { + expect(decodeSqidSegment("not+base64")).toBeUndefined(); + expect(decodeSqidSegment("_w")).toBeUndefined(); + }); + + it("preserves fixed list bounds while paging", () => { + const paths = new WebPaths({ requestsPath: "/submitqueue/requests/" }); + const href = paths.requests({ fromMs: 10, toMs: 20, pageToken: "opaque+/=" }); + + expect(href).toBe("/submitqueue/requests?from=10&to=20&page=opaque%2B%2F%3D"); + }); +}); diff --git a/web/submitqueue/src/paths.ts b/web/submitqueue/src/paths.ts new file mode 100644 index 000000000..fc760fbb3 --- /dev/null +++ b/web/submitqueue/src/paths.ts @@ -0,0 +1,79 @@ +export interface RequestsPathOptions { + fromMs?: number; + toMs?: number; + pageToken?: string | null; +} + +export interface WebPathsOptions { + requestsPath?: string; +} + +function encodeBase64Url(value: string): string { + const bytes = new TextEncoder().encode(value); + let binary = ""; + for (const byte of bytes) { + binary += String.fromCharCode(byte); + } + return globalThis + .btoa(binary) + .replaceAll("+", "-") + .replaceAll("/", "_") + .replace(/=+$/u, ""); +} + +function decodeBase64Url(value: string): string | undefined { + if (!/^[A-Za-z0-9_-]+$/u.test(value)) { + return undefined; + } + const standard = value.replaceAll("-", "+").replaceAll("_", "/"); + const padding = "=".repeat((4 - (standard.length % 4)) % 4); + try { + const binary = globalThis.atob(`${standard}${padding}`); + const bytes = Uint8Array.from(binary, (character) => character.charCodeAt(0)); + return new TextDecoder("utf-8", { fatal: true }).decode(bytes); + } catch { + return undefined; + } +} + +export class WebPaths { + readonly requestsPath: string; + + constructor(options: WebPathsOptions = {}) { + const requestedPath = options.requestsPath ?? "/requests"; + this.requestsPath = requestedPath.endsWith("/") + ? requestedPath.slice(0, -1) + : requestedPath; + } + + requests(options: RequestsPathOptions = {}): string { + const parameters = new URLSearchParams(); + if (options.fromMs !== undefined) { + parameters.set("from", String(options.fromMs)); + } + if (options.toMs !== undefined) { + parameters.set("to", String(options.toMs)); + } + if (options.pageToken) { + parameters.set("page", options.pageToken); + } + const query = parameters.toString(); + return query === "" ? this.requestsPath : `${this.requestsPath}?${query}`; + } + + request(sqid: string): string { + return `${this.requestsPath}/${encodeBase64Url(sqid)}`; + } + + decodeRequest(segment: string): string | undefined { + return decodeBase64Url(segment); + } +} + +export function encodeSqidSegment(sqid: string): string { + return encodeBase64Url(sqid); +} + +export function decodeSqidSegment(segment: string): string | undefined { + return decodeBase64Url(segment); +} diff --git a/web/submitqueue/src/server.test.ts b/web/submitqueue/src/server.test.ts new file mode 100644 index 000000000..29cf75a0f --- /dev/null +++ b/web/submitqueue/src/server.test.ts @@ -0,0 +1,129 @@ +import { Code, ConnectError } from "@connectrpc/connect"; +import { beforeEach, describe, expect, it, vi } from "vitest"; +import { createFakeGatewayReader, gatewayHistoryFixture, gatewayRequestFixture } from "./testing"; + +vi.mock("next/server", () => ({ connection: vi.fn(async () => undefined) })); + +import { + classifyGatewayError, + loadRequestDetail, + loadRequestList, + safeInt64ToNumber, +} from "./server"; + +describe("server presentation mapping", () => { + beforeEach(() => vi.clearAllMocks()); + + it("converts int64 only within JavaScript's safe range", () => { + expect(safeInt64ToNumber(1_700_000_000_000n)).toBe(1_700_000_000_000); + expect(safeInt64ToNumber(BigInt(Number.MAX_SAFE_INTEGER) + 1n)).toBeNull(); + expect(safeInt64ToNumber("not-an-integer")).toBeNull(); + }); + + it("maps a request page while preserving its bounds", async () => { + const reader = createFakeGatewayReader({ nextPageToken: "next" }); + const result = await loadRequestList(() => reader, { + queue: "demo-queue", + receivedAtOrAfterMs: 100, + receivedBeforeMs: 200, + pageToken: "current", + }); + + expect(result).toMatchObject({ + ok: true, + data: { + queue: "demo-queue", + receivedAtOrAfterMs: 100, + receivedBeforeMs: 200, + nextPageToken: "next", + requests: [{ sqid: "demo-queue/1", receivedAtMs: 1_700_000_000_000 }], + }, + }); + }); + + it("returns an explicit invalid-input result before resolving a client", async () => { + const resolver = vi.fn(() => createFakeGatewayReader()); + const result = await loadRequestList(resolver, { + queue: "demo-queue", + receivedAtOrAfterMs: 200, + receivedBeforeMs: 100, + }); + + expect(result).toMatchObject({ ok: false, error: { kind: "user" } }); + expect(resolver).not.toHaveBeenCalled(); + }); + + it("keeps the summary available when history has a transient failure", async () => { + const reader = createFakeGatewayReader({ + summary: gatewayRequestFixture({ status: "landed" }), + historyError: new ConnectError("private upstream text", Code.Unavailable), + }); + const result = await loadRequestDetail(() => reader, { + queue: "demo-queue", + sqid: "demo-queue/1", + }); + + expect(result).toMatchObject({ + ok: true, + data: { + request: { status: "landed" }, + history: [], + historyError: { kind: "transient", retryable: true }, + }, + }); + expect(JSON.stringify(result)).not.toContain("private upstream text"); + }); + + it("maps chronological status and occurrence history", async () => { + const reader = createFakeGatewayReader({ + history: [ + gatewayHistoryFixture({ timestampMs: 1n, status: "started" }), + gatewayHistoryFixture({ + timestampMs: 2n, + type: "event", + status: "", + event: "building", + metadata: { build_url: "https://build.example/1" }, + }), + ], + }); + const result = await loadRequestDetail(() => reader, { + queue: "demo-queue", + sqid: "demo-queue/1", + }); + + expect(result).toMatchObject({ + ok: true, + data: { + history: [ + { timestampMs: 1, type: "status", status: "started" }, + { timestampMs: 2, type: "event", event: "building" }, + ], + historyError: null, + }, + }); + }); +}); + +describe("gateway error classification", () => { + it.each([ + [Code.InvalidArgument, "user", false], + [Code.NotFound, "not-found", false], + [Code.ResourceExhausted, "user", false], + [Code.Unavailable, "transient", true], + [Code.DeadlineExceeded, "transient", true], + [Code.Internal, "internal", false], + ] as const)("classifies code %s", (code, kind, retryable) => { + const classified = classifyGatewayError(new ConnectError("raw gateway detail", code)); + + expect(classified).toMatchObject({ kind, retryable }); + expect(classified.message).not.toContain("raw gateway detail"); + }); + + it("does not expose arbitrary errors", () => { + expect(classifyGatewayError(new Error("database password"))).toMatchObject({ + kind: "internal", + retryable: false, + }); + }); +}); diff --git a/web/submitqueue/src/server.ts b/web/submitqueue/src/server.ts new file mode 100644 index 000000000..22f859c6a --- /dev/null +++ b/web/submitqueue/src/server.ts @@ -0,0 +1,293 @@ +import { Code, ConnectError, type Client } from "@connectrpc/connect"; +import { SubmitQueueGateway } from "@submitqueue/api/submitqueue/gateway"; +import { connection } from "next/server"; +import type { + HistoryEventModel, + LoadResult, + RequestDetailModel, + RequestListModel, + RequestSummaryModel, + WebError, +} from "./models.js"; + +export { decodeSqidSegment, encodeSqidSegment, WebPaths } from "./paths.js"; +export { isTerminalStatus, statusDisplay } from "./status.js"; + +type Int64Value = bigint | number | string; +const GATEWAY_TIMEOUT_MS = 5_000; + +export interface GatewayCallOptions { + timeoutMs?: number; +} + +export interface GatewayRequestSummary { + sqid: string; + queue: string; + changeUris: readonly string[]; + receivedAtMs: Int64Value; + status: string; + lastError: string; + metadata: Readonly>; +} + +export interface GatewayHistoryEvent { + timestampMs: Int64Value; + status: string; + lastError: string; + metadata: Readonly>; + type: string; + event: string; +} + +export interface GatewayReader { + list( + input: { + queue: string; + receivedAtOrAfterMs: bigint; + receivedBeforeMs: bigint; + pageSize: number; + pageToken: string; + }, + options?: GatewayCallOptions, + ): Promise<{ + requests: readonly GatewayRequestSummary[]; + nextPageToken: string; + }>; + getRequestSummaryByID( + input: { sqid: string; queue: string }, + options?: GatewayCallOptions, + ): Promise<{ + request?: GatewayRequestSummary | undefined; + }>; + getRequestHistoryByID( + input: { sqid: string; queue: string }, + options?: GatewayCallOptions, + ): Promise<{ + events: readonly GatewayHistoryEvent[]; + }>; +} + +export type GatewayResolver = (queue: string) => GatewayReader; + +export type SubmitQueueGatewayClient = Client; + +export function gatewayReader(client: SubmitQueueGatewayClient): GatewayReader { + return client; +} + +export interface LoadRequestListInput { + queue: string; + receivedAtOrAfterMs: number; + receivedBeforeMs: number; + pageSize?: number; + pageToken?: string; +} + +export interface LoadRequestDetailInput { + queue: string; + sqid: string; +} + +const INVALID_INPUT: WebError = { + kind: "user", + title: "Invalid request", + message: "The request parameters are invalid.", + retryable: false, +}; + +const INTERNAL_ERROR: WebError = { + kind: "internal", + title: "Something went wrong", + message: "SubmitQueue could not load this information.", + retryable: false, +}; + +export function safeInt64ToNumber(value: Int64Value): number | null { + let converted: bigint; + if (typeof value === "bigint") { + converted = value; + } else if (typeof value === "number") { + return Number.isSafeInteger(value) ? value : null; + } else { + try { + converted = BigInt(value); + } catch { + return null; + } + } + if (converted > BigInt(Number.MAX_SAFE_INTEGER) || converted < BigInt(Number.MIN_SAFE_INTEGER)) { + return null; + } + return Number(converted); +} + +export function classifyGatewayError(error: unknown): WebError { + if (!(error instanceof ConnectError)) { + return INTERNAL_ERROR; + } + switch (error.code) { + case Code.InvalidArgument: + return INVALID_INPUT; + case Code.NotFound: + return { + kind: "not-found", + title: "Request not found", + message: "The requested SubmitQueue request was not found.", + retryable: false, + }; + case Code.ResourceExhausted: + return { + kind: "user", + title: "Request limit reached", + message: "The requested result set is too large. Narrow the time window and try again.", + retryable: false, + }; + case Code.Unavailable: + case Code.DeadlineExceeded: + return { + kind: "transient", + title: "SubmitQueue is unavailable", + message: "SubmitQueue could not be reached. Try again shortly.", + retryable: true, + }; + default: + return INTERNAL_ERROR; + } +} + +function mapSummary(value: GatewayRequestSummary): RequestSummaryModel | null { + const receivedAtMs = safeInt64ToNumber(value.receivedAtMs); + if (receivedAtMs === null) { + return null; + } + return { + sqid: value.sqid, + queue: value.queue, + changeUris: [...value.changeUris], + receivedAtMs, + status: value.status, + lastError: value.lastError === "" ? null : value.lastError, + metadata: { ...value.metadata }, + }; +} + +function mapHistoryEvent(value: GatewayHistoryEvent): HistoryEventModel | null { + const timestampMs = safeInt64ToNumber(value.timestampMs); + if (timestampMs === null) { + return null; + } + const type = value.type === "status" || value.type === "event" ? value.type : "unknown"; + return { + timestampMs, + type, + status: value.status === "" ? null : value.status, + event: value.event === "" ? null : value.event, + lastError: value.lastError === "" ? null : value.lastError, + metadata: { ...value.metadata }, + }; +} + +function validListInput(input: LoadRequestListInput): boolean { + const pageSize = input.pageSize ?? 50; + return ( + input.queue !== "" && + Number.isSafeInteger(input.receivedAtOrAfterMs) && + Number.isSafeInteger(input.receivedBeforeMs) && + input.receivedAtOrAfterMs < input.receivedBeforeMs && + Number.isInteger(pageSize) && + pageSize > 0 + ); +} + +export async function loadRequestList( + resolveGateway: GatewayResolver, + input: LoadRequestListInput, +): Promise> { + if (!validListInput(input)) { + return { ok: false, error: INVALID_INPUT }; + } + try { + await connection(); + const response = await resolveGateway(input.queue).list( + { + queue: input.queue, + receivedAtOrAfterMs: BigInt(input.receivedAtOrAfterMs), + receivedBeforeMs: BigInt(input.receivedBeforeMs), + pageSize: input.pageSize ?? 50, + pageToken: input.pageToken ?? "", + }, + { timeoutMs: GATEWAY_TIMEOUT_MS }, + ); + const requests: RequestSummaryModel[] = []; + for (const value of response.requests) { + const request = mapSummary(value); + if (request === null) { + return { ok: false, error: INTERNAL_ERROR }; + } + requests.push(request); + } + return { + ok: true, + data: { + queue: input.queue, + receivedAtOrAfterMs: input.receivedAtOrAfterMs, + receivedBeforeMs: input.receivedBeforeMs, + requests, + nextPageToken: response.nextPageToken === "" ? null : response.nextPageToken, + }, + }; + } catch (error) { + return { ok: false, error: classifyGatewayError(error) }; + } +} + +export async function loadRequestDetail( + resolveGateway: GatewayResolver, + input: LoadRequestDetailInput, +): Promise> { + if (input.queue === "" || input.sqid === "") { + return { ok: false, error: INVALID_INPUT }; + } + try { + await connection(); + const gateway = resolveGateway(input.queue); + const [summaryResult, historyResult] = await Promise.allSettled([ + gateway.getRequestSummaryByID(input, { timeoutMs: GATEWAY_TIMEOUT_MS }), + gateway.getRequestHistoryByID(input, { timeoutMs: GATEWAY_TIMEOUT_MS }), + ]); + if (summaryResult.status === "rejected") { + return { ok: false, error: classifyGatewayError(summaryResult.reason) }; + } + if (summaryResult.value.request === undefined) { + return { ok: false, error: INTERNAL_ERROR }; + } + const request = mapSummary(summaryResult.value.request); + if (request === null) { + return { ok: false, error: INTERNAL_ERROR }; + } + if (historyResult.status === "rejected") { + return { + ok: true, + data: { + request, + history: [], + historyError: classifyGatewayError(historyResult.reason), + }, + }; + } + const history: HistoryEventModel[] = []; + for (const value of historyResult.value.events) { + const event = mapHistoryEvent(value); + if (event === null) { + return { + ok: true, + data: { request, history: [], historyError: INTERNAL_ERROR }, + }; + } + history.push(event); + } + return { ok: true, data: { request, history, historyError: null } }; + } catch (error) { + return { ok: false, error: classifyGatewayError(error) }; + } +} diff --git a/web/submitqueue/src/status.test.ts b/web/submitqueue/src/status.test.ts new file mode 100644 index 000000000..8c75e2560 --- /dev/null +++ b/web/submitqueue/src/status.test.ts @@ -0,0 +1,24 @@ +import { describe, expect, it } from "vitest"; +import { isTerminalStatus, knownRequestStatuses, statusDisplay } from "./status"; + +describe("request status display", () => { + it.each(["landed", "error", "cancelled"])("marks %s terminal", (status) => { + expect(isTerminalStatus(status)).toBe(true); + }); + + it.each(["accepted", "validating", "speculating", "landing", "cancelling"])( + "keeps %s active", + (status) => { + expect(isTerminalStatus(status)).toBe(false); + }, + ); + + it("renders newly-added statuses with a readable fallback", () => { + expect(statusDisplay("waiting_for_owner")).toEqual({ + label: "Waiting For Owner", + tone: "neutral", + terminal: false, + }); + expect(knownRequestStatuses).toContain("landed"); + }); +}); diff --git a/web/submitqueue/src/status.ts b/web/submitqueue/src/status.ts new file mode 100644 index 000000000..4de73d99b --- /dev/null +++ b/web/submitqueue/src/status.ts @@ -0,0 +1,49 @@ +import type { StatusDisplay } from "./models.js"; + +const STATUS_DISPLAY: Readonly> = { + accepting: { label: "Accepting", tone: "progress", terminal: false }, + accepted: { label: "Accepted", tone: "progress", terminal: false }, + started: { label: "Started", tone: "progress", terminal: false }, + validating: { label: "Validating", tone: "progress", terminal: false }, + validated: { label: "Validated", tone: "progress", terminal: false }, + batching: { label: "Batching", tone: "progress", terminal: false }, + batched: { label: "Batched", tone: "progress", terminal: false }, + speculating: { label: "Speculating", tone: "progress", terminal: false }, + speculated: { label: "Speculated", tone: "progress", terminal: false }, + landing: { label: "Landing", tone: "progress", terminal: false }, + landed: { label: "Landed", tone: "success", terminal: true }, + cancelling: { label: "Cancelling", tone: "warning", terminal: false }, + cancelled: { label: "Cancelled", tone: "neutral", terminal: true }, + error: { label: "Error", tone: "danger", terminal: true }, +}; + +const UNKNOWN_STATUS: StatusDisplay = { + label: "Unknown", + tone: "neutral", + terminal: false, +}; + +export const knownRequestStatuses = Object.freeze(Object.keys(STATUS_DISPLAY)); + +export function statusDisplay(status: string): StatusDisplay { + const known = STATUS_DISPLAY[status]; + if (known !== undefined) { + return known; + } + const normalized = status.trim(); + if (normalized === "") { + return UNKNOWN_STATUS; + } + return { + ...UNKNOWN_STATUS, + label: normalized + .split(/[-_\s]+/u) + .filter(Boolean) + .map((part) => `${part.charAt(0).toUpperCase()}${part.slice(1)}`) + .join(" "), + }; +} + +export function isTerminalStatus(status: string): boolean { + return statusDisplay(status).terminal; +} diff --git a/web/submitqueue/src/test-setup.ts b/web/submitqueue/src/test-setup.ts new file mode 100644 index 000000000..f149f27ae --- /dev/null +++ b/web/submitqueue/src/test-setup.ts @@ -0,0 +1 @@ +import "@testing-library/jest-dom/vitest"; diff --git a/web/submitqueue/src/testing.ts b/web/submitqueue/src/testing.ts new file mode 100644 index 000000000..c9e5f5bd8 --- /dev/null +++ b/web/submitqueue/src/testing.ts @@ -0,0 +1,75 @@ +import type { + GatewayHistoryEvent, + GatewayReader, + GatewayRequestSummary, +} from "./server.js"; + +const DEFAULT_TIMESTAMP = 1_700_000_000_000n; + +export function gatewayRequestFixture( + overrides: Partial = {}, +): GatewayRequestSummary { + return { + sqid: "demo-queue/1", + queue: "demo-queue", + changeUris: ["github://github.com/uber/submitqueue/pull/1/0123456789012345678901234567890123456789"], + receivedAtMs: DEFAULT_TIMESTAMP, + status: "speculating", + lastError: "", + metadata: {}, + ...overrides, + }; +} + +export function gatewayHistoryFixture( + overrides: Partial = {}, +): GatewayHistoryEvent { + return { + timestampMs: DEFAULT_TIMESTAMP, + status: "started", + lastError: "", + metadata: {}, + type: "status", + event: "", + ...overrides, + }; +} + +export interface FakeGatewayReaderOptions { + requests?: readonly GatewayRequestSummary[]; + nextPageToken?: string; + summary?: GatewayRequestSummary; + history?: readonly GatewayHistoryEvent[]; + listError?: unknown; + summaryError?: unknown; + historyError?: unknown; +} + +export function createFakeGatewayReader(options: FakeGatewayReaderOptions = {}): GatewayReader { + const summary = options.summary ?? options.requests?.[0] ?? gatewayRequestFixture(); + return { + async list() { + if (options.listError !== undefined) { + throw options.listError; + } + return { + requests: options.requests ?? [summary], + nextPageToken: options.nextPageToken ?? "", + }; + }, + async getRequestSummaryByID() { + if (options.summaryError !== undefined) { + throw options.summaryError; + } + return { request: summary }; + }, + async getRequestHistoryByID() { + if (options.historyError !== undefined) { + throw options.historyError; + } + return { events: options.history ?? [gatewayHistoryFixture()] }; + }, + }; +} + +export type { GatewayHistoryEvent, GatewayReader, GatewayRequestSummary } from "./server.js"; diff --git a/web/submitqueue/tsconfig.build.json b/web/submitqueue/tsconfig.build.json new file mode 100644 index 000000000..ba8a166de --- /dev/null +++ b/web/submitqueue/tsconfig.build.json @@ -0,0 +1,12 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "declaration": true, + "declarationMap": true, + "outDir": "dist", + "rootDir": "src", + "types": [] + }, + "include": ["src/**/*.ts", "src/**/*.tsx"], + "exclude": ["src/**/*.test.ts", "src/**/*.test.tsx", "src/test-setup.ts"] +} diff --git a/web/submitqueue/tsconfig.json b/web/submitqueue/tsconfig.json new file mode 100644 index 000000000..9272498a9 --- /dev/null +++ b/web/submitqueue/tsconfig.json @@ -0,0 +1,17 @@ +{ + "compilerOptions": { + "target": "ES2022", + "lib": ["DOM", "DOM.Iterable", "ES2022"], + "module": "ESNext", + "moduleResolution": "Bundler", + "jsx": "react-jsx", + "strict": true, + "noUncheckedIndexedAccess": true, + "exactOptionalPropertyTypes": true, + "esModuleInterop": true, + "skipLibCheck": true, + "resolveJsonModule": true, + "types": ["vitest/globals", "@testing-library/jest-dom"] + }, + "include": ["src/**/*.ts", "src/**/*.tsx", "vitest.config.ts"] +} diff --git a/web/submitqueue/vitest.config.ts b/web/submitqueue/vitest.config.ts new file mode 100644 index 000000000..c0ea710e0 --- /dev/null +++ b/web/submitqueue/vitest.config.ts @@ -0,0 +1,9 @@ +import { defineConfig } from "vitest/config"; + +export default defineConfig({ + test: { + environment: "jsdom", + globals: true, + setupFiles: ["./src/test-setup.ts"], + }, +}); diff --git a/web/tool/check-packages.mjs b/web/tool/check-packages.mjs new file mode 100644 index 000000000..75bd197d8 --- /dev/null +++ b/web/tool/check-packages.mjs @@ -0,0 +1,121 @@ +import { spawnSync } from "node:child_process"; +import { mkdtempSync, readdirSync, rmSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; + +const temporaryRoot = mkdtempSync(join(tmpdir(), "submitqueue-web-packages-")); + +function run(command, args, options = {}) { + const result = spawnSync(command, args, { + cwd: options.cwd, + encoding: "utf8", + env: options.env ?? process.env, + stdio: options.capture ? "pipe" : "inherit", + }); + if (result.status !== 0) { + if (options.capture) { + process.stderr.write(result.stdout ?? ""); + process.stderr.write(result.stderr ?? ""); + } + throw new Error(`${command} ${args.join(" ")} failed`); + } +} + +try { + run("corepack", [ + "pnpm@10.17.1", + "--filter", + "@submitqueue/api", + "pack", + "--pack-destination", + temporaryRoot, + ]); + run("corepack", [ + "pnpm@10.17.1", + "--filter", + "@submitqueue/web-submitqueue", + "pack", + "--pack-destination", + temporaryRoot, + ]); + + const archives = readdirSync(temporaryRoot).filter((name) => name.endsWith(".tgz")); + const apiArchive = archives.find((name) => name.includes("submitqueue-api-")); + const libraryArchive = archives.find((name) => name.includes("web-submitqueue-")); + if (!apiArchive || !libraryArchive) { + throw new Error(`expected API and library archives, found: ${archives.join(", ")}`); + } + + writeFileSync( + join(temporaryRoot, "package.json"), + `${JSON.stringify( + { + private: true, + type: "module", + dependencies: { + "@bufbuild/protobuf": "2.15.0", + "@connectrpc/connect": "2.1.1", + "@submitqueue/api": `file:${join(temporaryRoot, apiArchive)}`, + "@submitqueue/web-submitqueue": `file:${join(temporaryRoot, libraryArchive)}`, + "@types/react": "19.1.16", + "@types/react-dom": "19.1.9", + next: "16.3.6", + react: "19.2.0", + "react-dom": "19.2.0", + typescript: "5.9.3", + }, + pnpm: { + overrides: { + "@submitqueue/api": `file:${join(temporaryRoot, apiArchive)}`, + }, + }, + }, + null, + 2, + )}\n`, + ); + writeFileSync( + join(temporaryRoot, "consumer.ts"), + [ + 'import { RequestList } from "@submitqueue/web-submitqueue";', + 'import { loadRequestList } from "@submitqueue/web-submitqueue/server";', + 'import { createFakeGatewayReader } from "@submitqueue/web-submitqueue/testing";', + "void RequestList;", + "void loadRequestList;", + "void createFakeGatewayReader;", + "", + ].join("\n"), + ); + writeFileSync( + join(temporaryRoot, "tsconfig.json"), + `${JSON.stringify( + { + compilerOptions: { + module: "ESNext", + moduleResolution: "Bundler", + noEmit: true, + skipLibCheck: true, + strict: true, + target: "ES2023", + }, + include: ["consumer.ts"], + }, + null, + 2, + )}\n`, + ); + + run( + "corepack", + ["pnpm@10.17.1", "install", "--ignore-scripts", "--no-frozen-lockfile"], + { + cwd: temporaryRoot, + capture: true, + env: { ...process.env, npm_config_registry: "https://registry.npmjs.org" }, + }, + ); + run("corepack", ["pnpm@10.17.1", "exec", "tsc"], { cwd: temporaryRoot }); + console.log("package consumer imports passed"); +} finally { + rmSync(temporaryRoot, { recursive: true, force: true }); +} diff --git a/web/tool/generate-api.mjs b/web/tool/generate-api.mjs new file mode 100644 index 000000000..63051f11f --- /dev/null +++ b/web/tool/generate-api.mjs @@ -0,0 +1,82 @@ +import { spawnSync } from "node:child_process"; +import { existsSync, mkdtempSync, readdirSync, readFileSync, rmSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { dirname, join, relative, resolve } from "node:path"; +import { fileURLToPath } from "node:url"; + +const webRoot = resolve(dirname(fileURLToPath(import.meta.url)), ".."); +const repositoryRoot = resolve(webRoot, ".."); +const generatedRoot = join(webRoot, "api", "src", "gen"); +const check = process.argv.includes("--check"); +const temporaryRoot = mkdtempSync(join(tmpdir(), "submitqueue-web-api-")); +const outputRoot = check ? join(temporaryRoot, "api", "src", "gen") : generatedRoot; +const buf = join(webRoot, "node_modules", ".bin", process.platform === "win32" ? "buf.cmd" : "buf"); +const plugin = join(webRoot, "node_modules", ".bin", process.platform === "win32" ? "protoc-gen-es.cmd" : "protoc-gen-es"); +const protoFiles = [ + "api/base/change/proto/change.proto", + "api/base/mergestrategy/proto/mergestrategy.proto", + "api/submitqueue/gateway/proto/gateway.proto", +]; + +function listFiles(root, current = root) { + if (!existsSync(current)) { + return []; + } + return readdirSync(current, { withFileTypes: true }).flatMap((entry) => { + const path = join(current, entry.name); + return entry.isDirectory() ? listFiles(root, path) : [relative(root, path)]; + }).sort(); +} + +function generate() { + if (!existsSync(buf) || !existsSync(plugin)) { + throw new Error("Buf and protoc-gen-es are not installed; run pnpm install from web/"); + } + const template = check ? join(temporaryRoot, "buf.gen.yaml") : join(webRoot, "buf.gen.yaml"); + if (check) { + writeFileSync(template, `version: v2\nclean: true\nplugins:\n - local: protoc-gen-es\n out: ${JSON.stringify(outputRoot)}\n opt:\n - target=ts\n - import_extension=js\n`); + } + const path = [join(webRoot, "node_modules", ".bin"), process.env.PATH].filter(Boolean).join(process.platform === "win32" ? ";" : ":"); + const result = spawnSync(buf, [ + "generate", + repositoryRoot, + `--template=${template}`, + ...protoFiles.flatMap((protoFile) => ["--path", join("..", protoFile)]), + ], { + cwd: webRoot, + encoding: "utf8", + env: { ...process.env, PATH: path }, + }); + if (result.status !== 0) { + process.stderr.write(result.stderr); + process.exit(result.status ?? 1); + } +} + +function verifyDrift() { + const expectedFiles = listFiles(outputRoot); + const actualFiles = listFiles(generatedRoot); + const mismatches = []; + if (expectedFiles.join("\n") !== actualFiles.join("\n")) { + mismatches.push("generated file list differs"); + } + for (const path of expectedFiles) { + const actualPath = join(generatedRoot, path); + if (!existsSync(actualPath) || readFileSync(join(outputRoot, path), "utf8") !== readFileSync(actualPath, "utf8")) { + mismatches.push(path); + } + } + if (mismatches.length > 0) { + process.stderr.write(`Generated API is stale (${mismatches.join(", ")}). Run pnpm api:generate.\n`); + process.exitCode = 1; + } +} + +try { + generate(); + if (check) { + verifyDrift(); + } +} finally { + rmSync(temporaryRoot, { recursive: true, force: true }); +} diff --git a/web/tool/run-e2e.sh b/web/tool/run-e2e.sh new file mode 100755 index 000000000..acf763e8c --- /dev/null +++ b/web/tool/run-e2e.sh @@ -0,0 +1,54 @@ +#!/usr/bin/env bash + +set -euo pipefail + +repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" +project="e2e-submitqueue-web" +token="submitqueue-e2e-local-token-000000000000" +gate_dir="$(mktemp -d -t submitqueue-web-e2e.XXXXXX)" +compose=( + docker-compose + -f "$repo_root/service/submitqueue/docker-compose.yml" + -f "$repo_root/service/submitqueue/docker-compose.fake.yml" + -f "$repo_root/service/submitqueue/docker-compose.web.yml" + -p "$project" +) + +cleanup() { + local status=$? + "${compose[@]}" down -v --remove-orphans || true + rm -rf "$gate_dir" + return "$status" +} +trap cleanup EXIT + +export REPO_ROOT="$repo_root" +export SQ_PROVIDER_CONFIG_DIR="$repo_root/service/submitqueue/demo/provider/fake" +export SQ_CONSUMER_GATE_DIR="$gate_dir" +export SUBMITQUEUE_WEB_TOKEN="$token" +export SQ_MYSQL_INITDB_SKIP_TZINFO=1 + +if docker info --format '{{json .SecurityOptions}}' | grep -q 'name=rootless'; then + export SQ_CONTAINER_USER=0:0 +else + export SQ_CONTAINER_USER="$(id -u):$(id -g)" +fi + +"${compose[@]}" up -d --build --wait + +for schema in "$repo_root"/submitqueue/extension/storage/mysql/schema/*.sql; do + docker exec -i "$project-mysql-app-1" mysql -uroot -proot submitqueue < "$schema" +done +for schema in "$repo_root"/platform/extension/counter/mysql/schema/*.sql; do + docker exec -i "$project-mysql-app-1" mysql -uroot -proot submitqueue < "$schema" +done +for schema in "$repo_root"/platform/extension/messagequeue/mysql/schema/*.sql; do + docker exec -i "$project-mysql-queue-1" mysql -uroot -proot submitqueue < "$schema" +done + +gateway_port="$(docker port "$project-gateway-service-1" 8080/tcp | head -1 | sed 's/.*://')" +web_port="$(docker port "$project-web-service-1" 3000/tcp | head -1 | sed 's/.*://')" +export SUBMITQUEUE_E2E_GATEWAY_URL="http://127.0.0.1:$gateway_port" +export SUBMITQUEUE_E2E_WEB_URL="http://127.0.0.1:$web_port" + +corepack pnpm@10.17.1 --dir "$repo_root/web" e2e diff --git a/web/tsconfig.base.json b/web/tsconfig.base.json new file mode 100644 index 000000000..1fa82e87f --- /dev/null +++ b/web/tsconfig.base.json @@ -0,0 +1,18 @@ +{ + "compilerOptions": { + "allowJs": false, + "declaration": true, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "isolatedModules": true, + "lib": ["ES2023", "DOM", "DOM.Iterable"], + "module": "ESNext", + "moduleResolution": "Bundler", + "noEmit": true, + "noUncheckedIndexedAccess": true, + "resolveJsonModule": true, + "skipLibCheck": true, + "strict": true, + "target": "ES2023" + } +} diff --git a/web/tsconfig.e2e.json b/web/tsconfig.e2e.json new file mode 100644 index 000000000..76c390d3e --- /dev/null +++ b/web/tsconfig.e2e.json @@ -0,0 +1,7 @@ +{ + "extends": "./tsconfig.base.json", + "compilerOptions": { + "types": ["node"] + }, + "include": ["e2e/**/*.ts", "playwright.config.ts"] +} diff --git a/web/vitest.config.ts b/web/vitest.config.ts new file mode 100644 index 000000000..9b6cee87b --- /dev/null +++ b/web/vitest.config.ts @@ -0,0 +1,10 @@ +import { defineConfig } from "vitest/config"; + +export default defineConfig({ + test: { + clearMocks: true, + coverage: { + provider: "v8", + }, + }, +});