diff --git a/CLAUDE.md b/CLAUDE.md index 2be1a2f..0a97e66 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -4,7 +4,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co ## What this is -`devcloud` is a local cloud service emulator: a single Rust binary that runs compatible development endpoints for Mail (SMTP), S3, GCS, DynamoDB, BigQuery, SQS, Pub/Sub, Redshift, Redis, Application Auto Scaling, Lambda, Cloud Run, and a React dashboard. It targets deterministic local tests and manual inspection — not production parity. New provider behavior should be added deliberately, backed by tests in the relevant Rust crate and usually an acceptance gate under `scripts/*-autoloop/`. +`devcloud` is a local cloud service emulator: a single Rust binary that runs compatible development endpoints for Mail (SMTP), S3, GCS, DynamoDB, BigQuery, SQS, Pub/Sub, Redshift, Redis, Application Auto Scaling, SSM Parameter Store, STS, Lambda, Cloud Run, and a React dashboard. It targets deterministic local tests and manual inspection — not production parity. New provider behavior should be added deliberately, backed by tests in the relevant Rust crate and usually an acceptance gate under `scripts/*-autoloop/`. ## Commands @@ -48,7 +48,7 @@ Each provider lives under `services//` and exposes crate-local config/serve `services/dashboard` is the HTTP entry point users hit at `:18025`. It serves: - The React SPA from `assets/react` (embedded). `assets.rs` mounts `/dashboard/` and falls back to `index.html` for client-side routes. - A set of `/api/*` JSON endpoints that forward to each service's introspection/control or provider-protocol surface. -- **Route convention:** every service page lives under `/dashboard/` (`mail`, `s3`, `gcs`, `dynamodb`, `bigquery`, `sqs`, `pubsub`, `redshift`, `redis`, `applicationautoscaling`, `lambda`, `cloudrun`). The compatibility short paths `/mail`, `/s3`, `/gcs`, `/dynamodb`, `/bigquery`, `/redis` return 301 redirects to their `/dashboard/` counterpart — never add new functionality to the compatibility redirects. +- **Route convention:** every service page lives under `/dashboard/` (`mail`, `s3`, `gcs`, `dynamodb`, `bigquery`, `sqs`, `pubsub`, `redshift`, `redis`, `applicationautoscaling`, `ssm`, `sts`, `lambda`, `cloudrun`). The compatibility short paths `/mail`, `/s3`, `/gcs`, `/dynamodb`, `/bigquery`, `/redis` return 301 redirects to their `/dashboard/` counterpart — never add new functionality to the compatibility redirects. - **Safety rule:** dashboard mutations MUST go through the provider-protocol path (`/api//*` forwarding into the in-process service) — never directly through storage. Never log credentials, Authorization headers, signatures, message bodies, or object payloads. See `AGENTS.md` and the per-service notes in `README.md`. ### Always-on auxiliary services @@ -59,7 +59,7 @@ Each provider lives under `services//` and exposes crate-local config/serve Every service supports a `relaxed` mode (default, used by all local tooling) and a stricter mode that validates configured credentials. Relaxed mode is what tests and autoloops assume; if you add credential checks, gate them on the mode string so the existing scripts still pass. ### Configuration & storage -Config lives at `.devcloud/config.yaml` (custom YAML-ish parser in `orchestrator/src/config.rs`). Runtime data is rooted at `Storage.Path` (default `.devcloud/data`) with per-service subdirectories (`mail`, `s3`, `dynamodb`, `bigquery`, `sqs`, `pubsub`, `redshift`, `gcs/upload_sessions`, `applicationautoscaling`, `lambda`, `cloudrun`, `kv`). `kv` is reserved for a future key-value store — `init_workspace` creates it but no service currently reads or writes it. `.devcloud/` is gitignored and must not be committed. +Config lives at `.devcloud/config.yaml` (custom YAML-ish parser in `orchestrator/src/config.rs`). Runtime data is rooted at `Storage.Path` (default `.devcloud/data`) with per-service subdirectories (`mail`, `s3`, `dynamodb`, `bigquery`, `sqs`, `pubsub`, `redshift`, `gcs/upload_sessions`, `applicationautoscaling`, `ssm`, `lambda`, `cloudrun`, `kv`). `kv` is reserved for a future key-value store — `init_workspace` creates it but no service currently reads or writes it. `.devcloud/` is gitignored and must not be committed. ## Conventions diff --git a/Cargo.lock b/Cargo.lock index b5ecf0b..7a89e7d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -169,6 +169,15 @@ dependencies = [ "libc", ] +[[package]] +name = "crc32fast" +version = "1.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f97d3416d5337d15c4b0d78c8c91df922ca143f57ec20e2d1c188333047ef04" +dependencies = [ + "cfg-if", +] + [[package]] name = "crypto-common" version = "0.1.7" @@ -281,7 +290,7 @@ dependencies = [ "hex", "hmac", "libc", - "miniz_oxide", + "miniz_oxide 0.8.9", "serde", "serde_json", "sha2", @@ -331,6 +340,8 @@ dependencies = [ "devcloud-redshift", "devcloud-s3", "devcloud-sqs", + "devcloud-ssm", + "devcloud-sts", "serde", "serde_json", "tokio", @@ -374,6 +385,7 @@ name = "devcloud-redshift" version = "0.1.0" dependencies = [ "devcloud-s3", + "flate2", "hex", "hmac", "md-5", @@ -409,6 +421,30 @@ dependencies = [ "tokio", ] +[[package]] +name = "devcloud-ssm" +version = "0.1.0" +dependencies = [ + "hex", + "hmac", + "serde", + "serde_json", + "sha2", + "tokio", +] + +[[package]] +name = "devcloud-sts" +version = "0.1.0" +dependencies = [ + "hex", + "hmac", + "serde", + "serde_json", + "sha2", + "tokio", +] + [[package]] name = "digest" version = "0.10.7" @@ -453,7 +489,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -468,6 +504,16 @@ version = "0.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1d674e81391d1e1ab681a28d99df07927c6d4aa5b027d7da16ba32d1d21ecd99" +[[package]] +name = "flate2" +version = "1.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e634e2e0ebac1ee034020da1ca582e17ffe4e0f5e985823721e168928136dcb" +dependencies = [ + "crc32fast", + "miniz_oxide 0.9.1", +] + [[package]] name = "fnv" version = "1.0.7" @@ -721,7 +767,7 @@ dependencies = [ "hyper", "libc", "pin-project-lite", - "socket2 0.5.10", + "socket2 0.6.4", "tokio", "tower-service", "tracing", @@ -850,6 +896,16 @@ dependencies = [ "adler2", ] +[[package]] +name = "miniz_oxide" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b63fbc4a50860e98e7b2aa7804ded1db5cbc3aff9193adaff57a6931bf7c4b4c" +dependencies = [ + "adler2", + "simd-adler32", +] + [[package]] name = "mio" version = "1.2.1" @@ -1085,7 +1141,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -1181,6 +1237,12 @@ dependencies = [ "libc", ] +[[package]] +name = "simd-adler32" +version = "0.3.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a219298ac11a56ea9a6d2120044824d6f01aeb034955e7af7bc16858527deea" + [[package]] name = "simdutf8" version = "0.1.5" @@ -1252,7 +1314,7 @@ dependencies = [ "getrandom 0.4.2", "once_cell", "rustix", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 2c701a3..d632ceb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -22,4 +22,6 @@ members = [ "services/redshift", "services/s3", "services/sqs", + "services/ssm", + "services/sts", ] diff --git a/README.md b/README.md index c5151f4..2f7a4dc 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ Local cloud service emulator for development and E2E inspection. -`devcloud` runs a local dashboard plus compatible development endpoints for Mail, S3, GCS, DynamoDB, BigQuery, SQS, Google Cloud Pub/Sub, Redshift, Redis, Application Auto Scaling, AWS Lambda, and Cloud Run. It is designed for deterministic local tests and manual inspection, not for production workloads or full cloud-provider parity. +`devcloud` runs a local dashboard plus compatible development endpoints for Mail, S3, GCS, DynamoDB, BigQuery, SQS, Google Cloud Pub/Sub, Redshift, Redis, Application Auto Scaling, SSM Parameter Store, STS, AWS Lambda, and Cloud Run. It is designed for deterministic local tests and manual inspection, not for production workloads or full cloud-provider parity. ## Quick Start @@ -36,6 +36,8 @@ Default local endpoints: | Redshift API | `http://127.0.0.1:19099` | `http://127.0.0.1:18025/dashboard/redshift` | | Redis | `redis://127.0.0.1:16379` | `http://127.0.0.1:18025/dashboard/redis` | | Application Auto Scaling | `http://127.0.0.1:18030` | `http://127.0.0.1:18025/dashboard/applicationautoscaling` | +| SSM Parameter Store | `http://127.0.0.1:18031` | `http://127.0.0.1:18025/dashboard/ssm` | +| STS | `http://127.0.0.1:18032` | `http://127.0.0.1:18025/dashboard/sts` | | Lambda | `http://127.0.0.1:19010` | `http://127.0.0.1:18025/dashboard/lambda` | | Cloud Run | `http://127.0.0.1:18095` (services: `http://...run.localhost:18095`) | `http://127.0.0.1:18025/dashboard/cloudrun` | @@ -98,6 +100,8 @@ server: redisPort: 16379 redisHttpPort: 16380 appAutoScalingPort: 18030 + ssmPort: 18031 + stsPort: 18032 lambdaPort: 19010 cloudRunPort: 18095 @@ -145,6 +149,16 @@ auth: accessKeyId: dev secretAccessKey: dev accountId: "000000000000" + ssm: + mode: relaxed + accessKeyId: dev + secretAccessKey: dev + accountId: "000000000000" + sts: + mode: relaxed + accessKeyId: dev + secretAccessKey: dev + accountId: "000000000000" lambda: mode: relaxed accessKeyId: dev @@ -252,6 +266,12 @@ services: appAutoScaling: enabled: true region: us-east-1 + ssm: + enabled: true + region: us-east-1 + sts: + enabled: true + region: us-east-1 lambda: enabled: true region: us-east-1 @@ -427,10 +447,10 @@ Pub/Sub dashboard actions are available under `/dashboard/pubsub`: | PostgreSQL wire endpoint | Yes | Listens on `127.0.0.1:15439` by default for local Redshift-style SQL clients. | | PostgreSQL simple query protocol | Yes | Covers `psql` smoke workflows. | | PostgreSQL extended query protocol | Partial | Supports Parse, Bind, Describe, Execute, Sync, Close, text bind parameters, portal resume, and safe unsupported errors for binary formats. | -| PostgreSQL execution backend | Yes | Default backend is managed local PostgreSQL; explicit memory fallback remains available for development continuity. | +| PostgreSQL execution backend | Yes | Default backend is managed local PostgreSQL; explicit memory fallback remains available for development continuity. Each SQL client connection gets its own PostgreSQL session, so temp tables, `SET`s, and transactions stay with that client and end when it disconnects. The SQL endpoint does not offer TLS (`sslmode=disable`/`prefer` clients only). | | Redshift SQL translation | Partial | Handles local subsets for DDL/DML, Redshift table attributes, CTAS/views/materialized-view metadata, function rewrites, COPY, and UNLOAD. | -| COPY from local S3 | Yes | Local S3 side effect imports into the PostgreSQL-backed table path. | -| UNLOAD to local S3 | Yes | Query results can be exported through local S3 side effects. | +| COPY from local S3 | Partial | `COPY FROM 's3://bucket/prefix'` loads every object under the prefix from the local S3 store into the PostgreSQL backend, on the client's own session (temp tables and open transactions work): default text format with `DELIMITER`, `CSV` / `FORMAT AS CSV`, `JSON 'auto'` (or `'auto ignorecase'`), `GZIP`, `IGNOREHEADER`, `NULL AS`, `EMPTYASNULL`, `BLANKSASNULL`; an empty field is NULL except in character columns, as on Redshift. `IAM_ROLE` / `CREDENTIALS` / `REGION` are accepted and ignored. Column lists, `MANIFEST`, JSONPaths, other compressions, and columnar formats are refused. The memory backend keeps its own CSV/JSON COPY. | +| UNLOAD to local S3 | Partial | Memory backend only; the PostgreSQL backend does not run UNLOAD yet. | | Redshift Data API | Yes | Execute/describe/get-result/list workflows are covered by local gates. | | Redshift management API | Partial | Cluster, parameter group, tags, credentials, and snapshot metadata workflows are local metadata. | | Redshift Serverless API | Partial | Namespace and workgroup metadata are local compatibility responses. | @@ -463,7 +483,8 @@ Pub/Sub dashboard actions are available under `/dashboard/pubsub`: | Deployment packages | Partial | Zip (`ZipFile` base64, or `S3Bucket`/`S3Key` from the local S3 store when S3 is enabled; stored + DEFLATE entries, no zip64) and container images (below). The layer APIs are not supported. As on AWS, a zip package over 250 MB unzipped is rejected (50 MB for a direct `ZipFile` upload). Unix symlinks in the zip (`zip -y`, e.g. `node_modules/.bin`) are kept as symlinks (pointing anywhere, as on AWS); an entry stored beneath a link rejects the package, since extraction never writes through a link. | | Container images | Partial | `PackageType: Image` with `Code.ImageUri` and `ImageConfig` (`EntryPoint`, `Command`, `WorkingDirectory`) on create/update/get; `GetFunction` returns `RepositoryType: ECR`. Invoking needs `services.lambda.docker: true` (`DEVCLOUD_LAMBDA_DOCKER=true` for `devcloud-lambda`) and the `docker` CLI: each environment is a `docker run` of the image (any local tag or pullable reference; the first run may pull it, within a 120 s start budget) with the Runtime Interface Emulator port published on loopback — or, with `services.lambda.dockerNetwork` (`DEVCLOUD_LAMBDA_DOCKER_NETWORK`) set because devcloud itself runs in a container sharing the Docker socket, joined to that network and reached by container name — so the image must start the RIE — every AWS base image (`public.ecr.aws/lambda/*`) does through its entrypoint; a custom `EntryPoint` must too. Environments stay warm like zip functions. Containers are labelled `devcloud.lambda.owner=` (the id is kept in the Lambda storage directory as `instance-id`); on startup devcloud removes containers carrying its own id that a killed previous run left behind, and never touches other instances' containers. `MemorySize` becomes the container's `--memory` limit; `Architectures` is not mapped to `--platform` (the image's own platform runs). Variables reach the container through an owner-only `--env-file`, so a value with a line break cannot be passed. The RIE answers 200 for function errors too, so a response that is exactly a Lambda error document (`errorType`, `errorMessage`, and only error fields) is reported as `X-Amz-Function-Error: Unhandled`. `Init Duration` is the RIE's runtime init time, and a cold start's log opens with `INIT_START Image: `. REPORT's `Max Memory Used` is the container's cgroup memory peak since it started (page cache included; read with `docker exec cat`, so it is omitted for images without `cat`), with the same warning above `MemorySize`. The standalone `devcloud-lambda` Docker image does not include Docker (its `docker` build target does): invoking an image function without it fails with an error saying so (and `DEVCLOUD_LAMBDA_DOCKER=true` without a `docker` CLI on `PATH` is warned about at startup). | | Invoke | Partial | `RequestResponse`, `Event` (async, 202), and `DryRun`; `X-Amz-Function-Error: Unhandled` on handler errors; `X-Amz-Log-Type: Tail` returns the last 4 KB of the START/END/REPORT-framed log; per-function `Timeout` is enforced. **Execution environments are reused (warm starts)**: an environment is one `python3` / `node` process that loads the handler module once and then serves one invocation at a time, so module-level state and one-time initialization carry over, as on AWS. A cold start adds an `INIT_START` line, the init-phase output, and `Init Duration` in REPORT; init gets its own budget of one `Timeout`. Concurrent invocations get separate environments. An environment is stopped after `services.lambda.idleTimeoutSeconds` (default 300) without invocations, when the function's configuration or code changes or the function is deleted, after a timeout, crash, or init failure, and on shutdown; `idleTimeoutSeconds: 0` makes every invocation a cold start. Unlike AWS, an idle environment is not frozen: threads a Python handler left running keep running (Node handlers are paused between invocations). `MemorySize` is reported to the handler and in REPORT but does not limit the process's memory. On Linux, REPORT adds `Max Memory Used` (the interpreter process's peak resident memory during the invocation, init included on a cold start), and the log warns when it exceeds `MemorySize`. With `services.lambda.logInvocations: true` (`DEVCLOUD_LAMBDA_LOG_INVOCATIONS`, on by default for `devcloud-lambda`) every invocation's framed log is also printed to stdout, each line tagged `[]`. | -| Runtimes | Partial | `python3.x` and `nodejs*` handlers run as local `python3` / `node` processes (CommonJS, ESM `.mjs`, async and callback handlers). A Python runtime uses the matching `python3.` on devcloud's `PATH` when there is one (e.g. `python3.12`), otherwise `python3`; Node uses `node`. When the interpreter's version differs from the runtime (`python3.12` on Python 3.11, `nodejs20.x` on Node 22), the invocation log gets a `[WARNING] devcloud: runtime ... is running on ...` line before the handler's output (on a cold start), and devcloud's stderr repeats it once per function and runtime. The AWS-bundled SDKs (`boto3` for Python, AWS SDK v3 for Node) are not provided; ship them in the package or under the `/opt` directory. Other runtimes (`java*`, `dotnet*`, `ruby*`, `provided*`) can be deployed but invoking them returns `InvalidRuntimeException`. | +| Runtimes | Partial | `python3.x` and `nodejs*` handlers run as local `python3` / `node` processes (CommonJS, ESM `.mjs`, async and callback handlers). A Python runtime uses the matching `python3.` on devcloud's `PATH` when there is one (e.g. `python3.12`), otherwise `python3`; Node uses `node`. When the interpreter's version differs from the runtime (`python3.12` on Python 3.11, `nodejs20.x` on Node 22), the invocation log gets a `[WARNING] devcloud: runtime ... is running on ...` line before the handler's output (on a cold start), and devcloud's stderr repeats it once per function and runtime. The AWS-bundled SDKs (`boto3` for Python, AWS SDK v3 for Node) are not provided; ship them in the package or under the `/opt` directory. Other runtimes (`java*`, `dotnet*`, `ruby*`, `go1.x`) can be deployed but invoking them returns `InvalidRuntimeException`. | +| Custom runtimes (`provided*`) | Partial | The package's `bootstrap` (in the code root, else in the `/opt` directory) runs as a local process against a per-environment Lambda Runtime API on loopback (`AWS_LAMBDA_RUNTIME_API`): `invocation/next`, `invocation/{id}/response` (including chunked bodies and the error trailers of streamed responses), `invocation/{id}/error`, and `init/error`. Environments are reused like interpreter runtimes; output is framed per invocation. The bootstrap must be built for the host devcloud runs on (a Go runner for macOS needs `GOOS=darwin`); a missing or foreign-OS bootstrap fails the invocation with `Runtime.InvalidEntrypoint`. Extensions may register (so `aws-lambda-go`'s `WithEnableSIGTERM` starts cleanly) but receive no events, and stopped environments are killed without SIGTERM. | | Handler environment | Partial | Cleared environment: `PATH`, Lambda's reserved variables (`AWS_LAMBDA_FUNCTION_NAME`, `AWS_REGION`, `LAMBDA_TASK_ROOT`, ...), and the function's `Environment.Variables`. Host credentials are never passed through. To stand in for the execution role, set `services.lambda.functionAccessKeyId` / `functionSecretAccessKey` (and optionally `functionSessionToken`) and every handler receives them as `AWS_ACCESS_KEY_ID` / `AWS_SECRET_ACCESS_KEY` / `AWS_SESSION_TOKEN`. | | `/opt` (layer contents) | Partial | As on AWS, the default `PYTHONPATH` is `/opt/python/lib/python/site-packages:/opt/python` and the default `NODE_PATH` is `/opt/nodejs/node/node_modules:/opt/nodejs/node_modules`, so dependencies placed (or mounted) there are importable without extra settings. `services.lambda.optDir` points these paths at another directory. A function that sets its own `PYTHONPATH` / `NODE_PATH` replaces the default, as on AWS. | | Tags / account settings | Yes | `/2017-03-31/tags/{arn}` list/tag/untag and `GetAccountSettings`. | @@ -472,6 +493,24 @@ Pub/Sub dashboard actions are available under `/dashboard/pubsub`: | Auth | Partial | `relaxed`, `signed-relaxed`, and `strict` SigV4 (service `lambda`), like Application Auto Scaling. `strict` requires the credential scope's date to match `X-Amz-Date` and accepts a signature over the path either normalized as most SDKs sign it (`//x` and dot segments collapsed) or exactly as sent; there is no clock-skew check, so requests signed with fixed timestamps keep working. | | Browser (CSRF) guard | Yes | Non-GET requests carrying a non-loopback `Origin` (or `Sec-Fetch-Site: cross-site`) are rejected with 403, so a web page cannot create or invoke functions; SDKs/CLIs (no `Origin`) and loopback front-ends are unaffected. The dashboard's test invoke applies the same rule. Requests whose `Host` is a public DNS name are refused too (DNS rebinding; see the Dashboard API notes). | +### SSM Parameter Store-Compatible API + +| Feature | Status | Notes | +| --- | --- | --- | +| Parameters | Partial | `PutParameter` (`String`, `StringList`, `SecureString`; `Overwrite`; `Standard`/`Advanced` value limits), `GetParameter` (with a `:` selector for the current version only), `DeleteParameter`, and `DescribeParameters` (`MaxResults`/`NextToken`; filters are rejected). AWS JSON 1.1 under `X-Amz-Target: AmazonSSM.`. Persisted to `.devcloud/data/ssm/state.json`. | +| `SecureString` | Partial | No KMS: values are stored in plaintext in `state.json`. `GetParameter` returns the value only with `WithDecryption: true`; without it the value is an opaque placeholder, never the plaintext. | +| Not supported | No | `GetParameters`, `GetParametersByPath`, labels, version history, tags, policies. | +| Auth | Partial | `relaxed` / `signed-relaxed` / `strict` SigV4 (service `ssm`). `strict` only accepts `auth.ssm` credentials, so STS temporary credentials are rejected there. | +| Dashboard | Partial | `/dashboard/ssm` lists names, types, and versions through `DescribeParameters`; values never reach the dashboard. | + +### STS-Compatible API + +| Feature | Status | Notes | +| --- | --- | --- | +| `AssumeRole` | Partial | AWS Query protocol (form `POST /` or `GET /?Action=`), XML responses. Validates `RoleArn`, `RoleSessionName`, and `DurationSeconds` (900–43200), and issues new `ASIA…` temporary credentials per call. An inline session `Policy` must be JSON of at most 2048 characters; it is accepted but not evaluated. Stateless. | +| `GetCallerIdentity` | Partial | Always reports the configured account's root identity, not the assumed role. | +| Auth | Partial | `relaxed` / `signed-relaxed` / `strict` SigV4 (service `sts`). | + ### Cloud Run-Compatible API | Feature | Status | Notes | @@ -585,6 +624,8 @@ Run before claiming a service MVP is complete or when investigating a service-le | Redshift SQL translator | `scripts/redshift-translator-e2e.sh` | `psql`, `aws`, and `postgres` server binary on `PATH` | | Redis | `scripts/redis-e2e.sh` | `redis-cli` | | Application Auto Scaling | `scripts/applicationautoscaling-e2e.sh` | none | +| SSM Parameter Store | `scripts/ssm-e2e.sh` | none | +| STS | `scripts/sts-e2e.sh` | none | | Lambda | `scripts/lambda-e2e.sh` | `python3` (handler runtime) | | Cloud Run | `scripts/cloudrun-e2e.sh` | `python3` (sample service) | @@ -609,6 +650,8 @@ Useful env vars: | `services/pubsub` | Google Cloud Pub/Sub-compatible REST and gRPC service. | | `services/redshift` | Redshift SQL, Data API, and management API service. | | `services/applicationautoscaling` | Application Auto Scaling-compatible JSON API service. | +| `services/ssm` | SSM Parameter Store-compatible JSON API service. | +| `services/sts` | STS-compatible Query API service (`AssumeRole`, `GetCallerIdentity`). | | `services/lambda` | AWS Lambda-compatible REST API with local python/node handler execution. | | `services/cloudrun` | Cloud Run Admin API v2 plus a reverse proxy to locally run service processes. | | `services/dashboard` | Local Web UI, embedded React assets, and dashboard APIs. | diff --git a/orchestrator/Cargo.toml b/orchestrator/Cargo.toml index c7924cc..b0e3eb9 100644 --- a/orchestrator/Cargo.toml +++ b/orchestrator/Cargo.toml @@ -21,6 +21,8 @@ devcloud-dynamodb = { path = "../services/dynamodb" } devcloud-bigquery = { path = "../services/bigquery" } devcloud-sqs = { path = "../services/sqs" } devcloud-applicationautoscaling = { path = "../services/applicationautoscaling" } +devcloud-ssm = { path = "../services/ssm" } +devcloud-sts = { path = "../services/sts" } devcloud-lambda = { path = "../services/lambda" } devcloud-cloudrun = { path = "../services/cloudrun" } devcloud-pubsub = { path = "../services/pubsub" } diff --git a/orchestrator/src/config.rs b/orchestrator/src/config.rs index acb4bb2..eacd34a 100644 --- a/orchestrator/src/config.rs +++ b/orchestrator/src/config.rs @@ -53,6 +53,8 @@ pub struct ServerConfig { pub pubsub_grpc_port: i32, pub pubsub_rest_port: i32, pub app_auto_scaling_port: i32, + pub ssm_port: i32, + pub sts_port: i32, pub lambda_port: i32, pub cloud_run_port: i32, } @@ -69,6 +71,8 @@ pub struct AuthConfig { pub sqs: SqsAuthConfig, pub pubsub: PubSubAuthConfig, pub app_auto_scaling: AppAutoScalingAuthConfig, + pub ssm: AwsAuthConfig, + pub sts: AwsAuthConfig, pub lambda: LambdaAuthConfig, pub cloud_run: CloudRunAuthConfig, } @@ -147,6 +151,15 @@ pub struct AppAutoScalingAuthConfig { pub account_id: String, } +/// SigV4 auth settings for the AWS services added after the legacy port (SSM, STS). +#[derive(Debug, Clone, PartialEq, Eq, Default)] +pub struct AwsAuthConfig { + pub mode: String, + pub access_key_id: String, + pub secret_access_key: String, + pub account_id: String, +} + #[derive(Debug, Clone, PartialEq, Eq, Default)] pub struct LambdaAuthConfig { pub mode: String, @@ -178,6 +191,8 @@ pub struct ServicesConfig { pub sqs: SqsServiceConfig, pub pubsub: PubSubServiceConfig, pub app_auto_scaling: AppAutoScalingServiceConfig, + pub ssm: AwsServiceConfig, + pub sts: AwsServiceConfig, pub lambda: LambdaServiceConfig, pub cloud_run: CloudRunServiceConfig, } @@ -321,6 +336,13 @@ pub struct AppAutoScalingServiceConfig { pub region: String, } +/// Enabled flag + region for SSM and STS. +#[derive(Debug, Clone, PartialEq, Eq, Default)] +pub struct AwsServiceConfig { + pub enabled: bool, + pub region: String, +} + #[derive(Debug, Clone, PartialEq, Eq, Default)] pub struct LambdaServiceConfig { pub enabled: bool, @@ -399,6 +421,8 @@ pub fn default_config() -> Config { pubsub_grpc_port: 18085, pubsub_rest_port: 18086, app_auto_scaling_port: 18030, + ssm_port: 18031, + sts_port: 18032, lambda_port: 19010, cloud_run_port: 18095, }, @@ -457,6 +481,18 @@ pub fn default_config() -> Config { secret_access_key: "dev".to_string(), account_id: "000000000000".to_string(), }, + ssm: AwsAuthConfig { + mode: "relaxed".to_string(), + access_key_id: "dev".to_string(), + secret_access_key: "dev".to_string(), + account_id: "000000000000".to_string(), + }, + sts: AwsAuthConfig { + mode: "relaxed".to_string(), + access_key_id: "dev".to_string(), + secret_access_key: "dev".to_string(), + account_id: "000000000000".to_string(), + }, lambda: LambdaAuthConfig { mode: "relaxed".to_string(), access_key_id: "dev".to_string(), @@ -586,6 +622,14 @@ pub fn default_config() -> Config { enabled: true, region: "us-east-1".to_string(), }, + ssm: AwsServiceConfig { + enabled: true, + region: "us-east-1".to_string(), + }, + sts: AwsServiceConfig { + enabled: true, + region: "us-east-1".to_string(), + }, lambda: LambdaServiceConfig { enabled: true, region: "us-east-1".to_string(), @@ -775,6 +819,8 @@ pub fn apply_config_value(cfg: &mut Config, path: &[String], value: &str) -> io: "server.appAutoScalingPort" => { cfg.server.app_auto_scaling_port = parse_int("server.appAutoScalingPort", value)? } + "server.ssmPort" => cfg.server.ssm_port = parse_int("server.ssmPort", value)?, + "server.stsPort" => cfg.server.sts_port = parse_int("server.stsPort", value)?, "server.lambdaPort" => cfg.server.lambda_port = parse_int("server.lambdaPort", value)?, "server.cloudRunPort" => { cfg.server.cloud_run_port = parse_int("server.cloudRunPort", value)? @@ -821,6 +867,14 @@ pub fn apply_config_value(cfg: &mut Config, path: &[String], value: &str) -> io: "auth.appAutoScaling.accountId" => { cfg.auth.app_auto_scaling.account_id = trim_quotes(value) } + "auth.ssm.mode" => cfg.auth.ssm.mode = value.to_string(), + "auth.ssm.accessKeyId" => cfg.auth.ssm.access_key_id = value.to_string(), + "auth.ssm.secretAccessKey" => cfg.auth.ssm.secret_access_key = value.to_string(), + "auth.ssm.accountId" => cfg.auth.ssm.account_id = trim_quotes(value), + "auth.sts.mode" => cfg.auth.sts.mode = value.to_string(), + "auth.sts.accessKeyId" => cfg.auth.sts.access_key_id = value.to_string(), + "auth.sts.secretAccessKey" => cfg.auth.sts.secret_access_key = value.to_string(), + "auth.sts.accountId" => cfg.auth.sts.account_id = trim_quotes(value), "auth.lambda.mode" => cfg.auth.lambda.mode = value.to_string(), "auth.lambda.accessKeyId" => cfg.auth.lambda.access_key_id = value.to_string(), "auth.lambda.secretAccessKey" => cfg.auth.lambda.secret_access_key = value.to_string(), @@ -1181,6 +1235,14 @@ pub fn apply_config_value(cfg: &mut Config, path: &[String], value: &str) -> io: "services.appAutoScaling.region" => { cfg.services.app_auto_scaling.region = value.to_string() } + "services.ssm.enabled" => { + cfg.services.ssm.enabled = parse_bool("services.ssm.enabled", value)? + } + "services.ssm.region" => cfg.services.ssm.region = value.to_string(), + "services.sts.enabled" => { + cfg.services.sts.enabled = parse_bool("services.sts.enabled", value)? + } + "services.sts.region" => cfg.services.sts.region = value.to_string(), "services.lambda.enabled" => { cfg.services.lambda.enabled = parse_bool("services.lambda.enabled", value)? } @@ -1398,6 +1460,8 @@ pub fn default_config_yaml(cfg: &Config) -> String { " pubsubGrpcPort: {pubsub_grpc_port}\n", " pubsubRestPort: {pubsub_rest_port}\n", " appAutoScalingPort: {app_auto_scaling_port}\n", + " ssmPort: {ssm_port}\n", + " stsPort: {sts_port}\n", " lambdaPort: {lambda_port}\n", " cloudRunPort: {cloud_run_port}\n", "\n", @@ -1445,6 +1509,16 @@ pub fn default_config_yaml(cfg: &Config) -> String { " accessKeyId: {aas_access_key_id}\n", " secretAccessKey: {aas_secret_access_key}\n", " accountId: \"{aas_account_id}\"\n", + " ssm:\n", + " mode: {ssm_auth_mode}\n", + " accessKeyId: {ssm_access_key_id}\n", + " secretAccessKey: {ssm_secret_access_key}\n", + " accountId: \"{ssm_account_id}\"\n", + " sts:\n", + " mode: {sts_auth_mode}\n", + " accessKeyId: {sts_access_key_id}\n", + " secretAccessKey: {sts_secret_access_key}\n", + " accountId: \"{sts_account_id}\"\n", " lambda:\n", " mode: {lambda_auth_mode}\n", " accessKeyId: {lambda_access_key_id}\n", @@ -1556,6 +1630,12 @@ pub fn default_config_yaml(cfg: &Config) -> String { " appAutoScaling:\n", " enabled: {aas_enabled}\n", " region: {aas_region}\n", + " ssm:\n", + " enabled: {ssm_enabled}\n", + " region: {ssm_region}\n", + " sts:\n", + " enabled: {sts_enabled}\n", + " region: {sts_region}\n", " lambda:\n", " enabled: {lambda_enabled}\n", " region: {lambda_region}\n", @@ -1583,6 +1663,8 @@ pub fn default_config_yaml(cfg: &Config) -> String { pubsub_grpc_port = s.pubsub_grpc_port, pubsub_rest_port = s.pubsub_rest_port, app_auto_scaling_port = s.app_auto_scaling_port, + ssm_port = s.ssm_port, + sts_port = s.sts_port, lambda_port = s.lambda_port, cloud_run_port = s.cloud_run_port, smtp_mode = a.smtp.mode, @@ -1618,6 +1700,14 @@ pub fn default_config_yaml(cfg: &Config) -> String { aas_access_key_id = a.app_auto_scaling.access_key_id, aas_secret_access_key = a.app_auto_scaling.secret_access_key, aas_account_id = a.app_auto_scaling.account_id, + ssm_auth_mode = a.ssm.mode, + ssm_access_key_id = a.ssm.access_key_id, + ssm_secret_access_key = a.ssm.secret_access_key, + ssm_account_id = a.ssm.account_id, + sts_auth_mode = a.sts.mode, + sts_access_key_id = a.sts.access_key_id, + sts_secret_access_key = a.sts.secret_access_key, + sts_account_id = a.sts.account_id, lambda_auth_mode = a.lambda.mode, lambda_access_key_id = a.lambda.access_key_id, lambda_secret_access_key = a.lambda.secret_access_key, @@ -1709,6 +1799,10 @@ pub fn default_config_yaml(cfg: &Config) -> String { pubsub_enable_push = fmt_bool(sv.pubsub.enable_push), aas_enabled = fmt_bool(sv.app_auto_scaling.enabled), aas_region = sv.app_auto_scaling.region, + ssm_enabled = fmt_bool(sv.ssm.enabled), + ssm_region = sv.ssm.region, + sts_enabled = fmt_bool(sv.sts.enabled), + sts_region = sv.sts.region, lambda_enabled = fmt_bool(sv.lambda.enabled), lambda_region = sv.lambda.region, lambda_optional = lambda_optional_lines(&sv.lambda), @@ -1832,6 +1926,7 @@ pub fn init_workspace(cfg: &Config) -> io::Result<()> { &path_join(&cfg.storage.path, "applicationautoscaling"), "create applicationautoscaling storage", )?; + mkdir_all(&path_join(&cfg.storage.path, "ssm"), "create ssm storage")?; mkdir_all( &path_join(&cfg.storage.path, "lambda"), "create lambda storage", @@ -1910,6 +2005,8 @@ const SERVICE_TOGGLES: &[&str] = &[ "sqs", "pubsub", "appautoscaling", + "ssm", + "sts", "lambda", "cloudrun", ]; @@ -1934,6 +2031,8 @@ fn set_service_enabled(cfg: &mut Config, name: &str, v: bool) { "sqs" => cfg.services.sqs.enabled = v, "pubsub" => cfg.services.pubsub.enabled = v, "appautoscaling" => cfg.services.app_auto_scaling.enabled = v, + "ssm" => cfg.services.ssm.enabled = v, + "sts" => cfg.services.sts.enabled = v, "lambda" => cfg.services.lambda.enabled = v, "cloudrun" => cfg.services.cloud_run.enabled = v, _ => {} @@ -1961,6 +2060,9 @@ fn service_aliases() -> HashMap<&'static str, &'static str> { ("app-autoscaling", "appautoscaling"), ("app_autoscaling", "appautoscaling"), ("applicationautoscaling", "appautoscaling"), + ("ssm", "ssm"), + ("parameterstore", "ssm"), + ("sts", "sts"), ("lambda", "lambda"), ("cloudrun", "cloudrun"), ("cloud-run", "cloudrun"), @@ -2045,6 +2147,8 @@ mod tests { "server.pubsubGrpcPort", "server.pubsubRestPort", "server.appAutoScalingPort", + "server.ssmPort", + "server.stsPort", "server.lambdaPort", "server.cloudRunPort", "services.dynamodb.maxTables", @@ -2199,6 +2303,8 @@ mod tests { "redshift", "s3", "sqs", + "ssm", + "sts", ] ); } diff --git a/orchestrator/src/services/dashboard.rs b/orchestrator/src/services/dashboard.rs index d738eae..7bced8c 100644 --- a/orchestrator/src/services/dashboard.rs +++ b/orchestrator/src/services/dashboard.rs @@ -99,6 +99,15 @@ fn build_config(cfg: &AppConfig) -> Config { app_auto_scaling_storage_path: join(s, "applicationautoscaling"), app_auto_scaling_region: cfg.services.app_auto_scaling.region.clone(), + ssm_base: enabled_ep(cfg.services.ssm.enabled, cfg.server.ssm_port), + ssm_endpoint: http_ep(cfg.server.ssm_port), + ssm_storage_path: join(s, "ssm"), + ssm_region: cfg.services.ssm.region.clone(), + + sts_base: enabled_ep(cfg.services.sts.enabled, cfg.server.sts_port), + sts_endpoint: http_ep(cfg.server.sts_port), + sts_region: cfg.services.sts.region.clone(), + lambda_base: enabled_ep(cfg.services.lambda.enabled, cfg.server.lambda_port), lambda_endpoint: http_ep(cfg.server.lambda_port), lambda_storage_path: join(s, "lambda"), @@ -161,6 +170,8 @@ mod tests { "applicationautoscaling", "/api/applicationautoscaling/scalable-targets", ), + ("ssm", "ssm", "/api/ssm/parameters"), + ("sts", "sts", "/api/sts/caller-identity"), ("lambda", "lambda", "/api/lambda/functions"), ("cloudrun", "cloudrun", "/api/cloudrun/services"), ]; diff --git a/orchestrator/src/services/mod.rs b/orchestrator/src/services/mod.rs index ee6e139..dddbe3f 100644 --- a/orchestrator/src/services/mod.rs +++ b/orchestrator/src/services/mod.rs @@ -20,4 +20,6 @@ pub mod redis; pub mod redshift; pub mod s3; pub mod sqs; +pub mod ssm; +pub mod sts; pub mod util; diff --git a/orchestrator/src/services/ssm.rs b/orchestrator/src/services/ssm.rs new file mode 100644 index 0000000..7d9e257 --- /dev/null +++ b/orchestrator/src/services/ssm.rs @@ -0,0 +1,40 @@ +//! SSM Parameter Store, run in-process as a supervisor task. + +use std::future::Future; +use std::path::Path; +use std::sync::Arc; + +use devcloud_ssm::{Config, Server}; + +use crate::config::Config as OrchestratorConfig; + +/// Runs the SSM HTTP server until it errors or `shutdown` resolves. +/// +/// Storage layout: state under `/ssm`. +pub async fn run( + cfg: &OrchestratorConfig, + shutdown: impl Future, +) -> Result<(), String> { + let addr = format!("127.0.0.1:{}", cfg.server.ssm_port); + let storage = Path::new(&cfg.storage.path).join("ssm"); + + let server = Arc::new(Server::new(Config { + addr: addr.clone(), + region: cfg.services.ssm.region.clone(), + account_id: cfg.auth.ssm.account_id.clone(), + auth_mode: cfg.auth.ssm.mode.clone(), + access_key_id: cfg.auth.ssm.access_key_id.clone(), + secret_access_key: cfg.auth.ssm.secret_access_key.clone(), + storage_path: storage.to_string_lossy().into_owned(), + })); + if let Some(err) = server.load_err() { + return Err(format!("ssm: failed to load state: {err}")); + } + + let listener = tokio::net::TcpListener::bind(&addr) + .await + .map_err(|e| format!("ssm: bind {addr}: {e}"))?; + devcloud_ssm::http::serve(listener, server, shutdown) + .await + .map_err(|e| format!("ssm: server error: {e}")) +} diff --git a/orchestrator/src/services/sts.rs b/orchestrator/src/services/sts.rs new file mode 100644 index 0000000..380c00c --- /dev/null +++ b/orchestrator/src/services/sts.rs @@ -0,0 +1,31 @@ +//! STS, run in-process as a supervisor task. Stateless: no storage directory. + +use std::future::Future; +use std::sync::Arc; + +use devcloud_sts::{Config, Server}; + +use crate::config::Config as OrchestratorConfig; + +/// Runs the STS HTTP server until it errors or `shutdown` resolves. +pub async fn run( + cfg: &OrchestratorConfig, + shutdown: impl Future, +) -> Result<(), String> { + let addr = format!("127.0.0.1:{}", cfg.server.sts_port); + let server = Arc::new(Server::new(Config { + addr: addr.clone(), + region: cfg.services.sts.region.clone(), + account_id: cfg.auth.sts.account_id.clone(), + auth_mode: cfg.auth.sts.mode.clone(), + access_key_id: cfg.auth.sts.access_key_id.clone(), + secret_access_key: cfg.auth.sts.secret_access_key.clone(), + })); + + let listener = tokio::net::TcpListener::bind(&addr) + .await + .map_err(|e| format!("sts: bind {addr}: {e}"))?; + devcloud_sts::http::serve(listener, server, shutdown) + .await + .map_err(|e| format!("sts: server error: {e}")) +} diff --git a/orchestrator/src/supervisor.rs b/orchestrator/src/supervisor.rs index 8c0e96c..da7b92b 100644 --- a/orchestrator/src/supervisor.rs +++ b/orchestrator/src/supervisor.rs @@ -111,6 +111,16 @@ pub async fn run(cfg: Config) -> Result<(), String> { let sd = shutdown_future(rx.clone()); set.spawn(async move { services::applicationautoscaling::run(&c, sd).await }); } + if cfg.services.ssm.enabled { + let c = cfg.clone(); + let sd = shutdown_future(rx.clone()); + set.spawn(async move { services::ssm::run(&c, sd).await }); + } + if cfg.services.sts.enabled { + let c = cfg.clone(); + let sd = shutdown_future(rx.clone()); + set.spawn(async move { services::sts::run(&c, sd).await }); + } if cfg.services.lambda.enabled { let c = cfg.clone(); let sd = shutdown_future(rx.clone()); diff --git a/orchestrator/tests/dashboard_conformance.rs b/orchestrator/tests/dashboard_conformance.rs index 130db4f..fd1604b 100644 --- a/orchestrator/tests/dashboard_conformance.rs +++ b/orchestrator/tests/dashboard_conformance.rs @@ -53,8 +53,8 @@ //! and the shipped product, so it is masked symmetrically. Every other service's //! status is asserted unmasked. //! -//! Deliberate post-legacy additions: the `applicationautoscaling`, `lambda`, and -//! `cloudrun` entries in the two registry goldens. The legacy dashboard never had +//! Deliberate post-legacy additions: the `applicationautoscaling`, `ssm`, `sts`, +//! `lambda`, and `cloudrun` entries in the two registry goldens. The legacy dashboard never had //! those services; the entries were appended when the Rust dashboard gained their //! pages, so the goldens track intended current behavior, not a byte-frozen //! legacy capture, for those rows. @@ -733,6 +733,8 @@ fn spawn_devcloud_up(workspace: &Path) -> Child { \u{20}\u{20}pubsubGrpcPort: 28085\n\ \u{20}\u{20}pubsubRestPort: 28086\n\ \u{20}\u{20}appAutoScalingPort: 28030\n\ +\u{20}\u{20}ssmPort: 28031\n\ +\u{20}\u{20}stsPort: 28032\n\ \u{20}\u{20}lambdaPort: 29010\n\ \u{20}\u{20}cloudRunPort: 28095\n", ); diff --git a/scripts/applicationautoscaling-autoloop/verify.sh b/scripts/applicationautoscaling-autoloop/verify.sh index e2bde7f..f9ab29e 100644 --- a/scripts/applicationautoscaling-autoloop/verify.sh +++ b/scripts/applicationautoscaling-autoloop/verify.sh @@ -193,6 +193,10 @@ services: enabled: false cloudRun: enabled: false + ssm: + enabled: false + sts: + enabled: false EOF run_check "devcloud binary builds" devcloud_build "${TMP_DIR}/devcloud" diff --git a/scripts/applicationautoscaling-e2e.sh b/scripts/applicationautoscaling-e2e.sh index 21977b9..5e48c4c 100755 --- a/scripts/applicationautoscaling-e2e.sh +++ b/scripts/applicationautoscaling-e2e.sh @@ -220,6 +220,10 @@ services: enabled: false cloudRun: enabled: false + ssm: + enabled: false + sts: + enabled: false EOF } diff --git a/scripts/bigquery-autoloop/verify.sh b/scripts/bigquery-autoloop/verify.sh index f65654f..6f55a07 100755 --- a/scripts/bigquery-autoloop/verify.sh +++ b/scripts/bigquery-autoloop/verify.sh @@ -198,6 +198,10 @@ services: enabled: false cloudRun: enabled: false + ssm: + enabled: false + sts: + enabled: false EOF local failures_before_build="${FAIL}" diff --git a/scripts/bigquery-e2e.sh b/scripts/bigquery-e2e.sh index 6761cf0..60a6d4b 100755 --- a/scripts/bigquery-e2e.sh +++ b/scripts/bigquery-e2e.sh @@ -286,6 +286,10 @@ services: enabled: false cloudRun: enabled: false + ssm: + enabled: false + sts: + enabled: false EOF } diff --git a/scripts/cloudrun-autoloop/verify.sh b/scripts/cloudrun-autoloop/verify.sh index 96cf654..7e0e1eb 100644 --- a/scripts/cloudrun-autoloop/verify.sh +++ b/scripts/cloudrun-autoloop/verify.sh @@ -176,6 +176,10 @@ services: enabled: true project: ${PROJECT} region: ${REGION} + ssm: + enabled: false + sts: + enabled: false EOF run_check "devcloud binary builds" devcloud_build "${TMP_DIR}/devcloud" diff --git a/scripts/cloudrun-e2e.sh b/scripts/cloudrun-e2e.sh index 4528982..3b838d6 100755 --- a/scripts/cloudrun-e2e.sh +++ b/scripts/cloudrun-e2e.sh @@ -215,6 +215,10 @@ services: enabled: true project: ${PROJECT} region: ${REGION} + ssm: + enabled: false + sts: + enabled: false EOF } diff --git a/scripts/dashboard-design-renewal-autoloop/verify.sh b/scripts/dashboard-design-renewal-autoloop/verify.sh index 6a4a92f..9a19a45 100755 --- a/scripts/dashboard-design-renewal-autoloop/verify.sh +++ b/scripts/dashboard-design-renewal-autoloop/verify.sh @@ -162,6 +162,10 @@ services: enabled: false cloudRun: enabled: false + ssm: + enabled: false + sts: + enabled: false EOF devcloud_build "${TMP_DIR}/devcloud" diff --git a/scripts/dynamodb-autoloop/verify.sh b/scripts/dynamodb-autoloop/verify.sh index 0ca09b2..1bb3f1e 100755 --- a/scripts/dynamodb-autoloop/verify.sh +++ b/scripts/dynamodb-autoloop/verify.sh @@ -170,6 +170,10 @@ services: enabled: false cloudRun: enabled: false + ssm: + enabled: false + sts: + enabled: false EOF run_check "devcloud binary builds" devcloud_build "${TMP_DIR}/devcloud" diff --git a/scripts/dynamodb-e2e.sh b/scripts/dynamodb-e2e.sh index 9a94833..be8a819 100755 --- a/scripts/dynamodb-e2e.sh +++ b/scripts/dynamodb-e2e.sh @@ -259,6 +259,10 @@ services: enabled: false cloudRun: enabled: false + ssm: + enabled: false + sts: + enabled: false EOF } diff --git a/scripts/gcs-autoloop/verify.sh b/scripts/gcs-autoloop/verify.sh index 2cd2b85..3d2ee92 100755 --- a/scripts/gcs-autoloop/verify.sh +++ b/scripts/gcs-autoloop/verify.sh @@ -140,6 +140,10 @@ services: enabled: false cloudRun: enabled: false + ssm: + enabled: false + sts: + enabled: false EOF local failures_before_build="${FAIL}" diff --git a/scripts/gcs-e2e.sh b/scripts/gcs-e2e.sh index 53a9964..6aec283 100755 --- a/scripts/gcs-e2e.sh +++ b/scripts/gcs-e2e.sh @@ -205,6 +205,10 @@ services: enabled: false cloudRun: enabled: false + ssm: + enabled: false + sts: + enabled: false EOF } diff --git a/scripts/lambda-autoloop/verify.sh b/scripts/lambda-autoloop/verify.sh index 2cde76b..4d32239 100644 --- a/scripts/lambda-autoloop/verify.sh +++ b/scripts/lambda-autoloop/verify.sh @@ -169,6 +169,10 @@ services: enabled: false cloudRun: enabled: false + ssm: + enabled: false + sts: + enabled: false lambda: enabled: true region: ${REGION} diff --git a/scripts/lambda-e2e.sh b/scripts/lambda-e2e.sh index 8219857..4e332fc 100755 --- a/scripts/lambda-e2e.sh +++ b/scripts/lambda-e2e.sh @@ -210,6 +210,10 @@ services: enabled: false cloudRun: enabled: false + ssm: + enabled: false + sts: + enabled: false lambda: enabled: true region: ${REGION} diff --git a/scripts/mail-autoloop/verify.sh b/scripts/mail-autoloop/verify.sh index cc80752..46910c5 100755 --- a/scripts/mail-autoloop/verify.sh +++ b/scripts/mail-autoloop/verify.sh @@ -91,6 +91,10 @@ services: enabled: false cloudRun: enabled: false + ssm: + enabled: false + sts: + enabled: false EOF } diff --git a/scripts/mail-e2e.sh b/scripts/mail-e2e.sh index 098cee8..4add572 100755 --- a/scripts/mail-e2e.sh +++ b/scripts/mail-e2e.sh @@ -115,6 +115,10 @@ services: enabled: false cloudRun: enabled: false + ssm: + enabled: false + sts: + enabled: false EOF } diff --git a/scripts/pubsub-autoloop/verify.sh b/scripts/pubsub-autoloop/verify.sh index 4f05532..2edfac6 100755 --- a/scripts/pubsub-autoloop/verify.sh +++ b/scripts/pubsub-autoloop/verify.sh @@ -218,6 +218,10 @@ services: enabled: false cloudRun: enabled: false + ssm: + enabled: false + sts: + enabled: false EOF local failures_before_build="${FAIL}" diff --git a/scripts/pubsub-e2e.sh b/scripts/pubsub-e2e.sh index f1425c6..09649c5 100755 --- a/scripts/pubsub-e2e.sh +++ b/scripts/pubsub-e2e.sh @@ -245,6 +245,10 @@ services: enabled: false cloudRun: enabled: false + ssm: + enabled: false + sts: + enabled: false EOF log "building devcloud" diff --git a/scripts/redis-e2e.sh b/scripts/redis-e2e.sh index 348535b..d2289dd 100755 --- a/scripts/redis-e2e.sh +++ b/scripts/redis-e2e.sh @@ -146,6 +146,10 @@ services: enabled: false cloudRun: enabled: false + ssm: + enabled: false + sts: + enabled: false EOF } diff --git a/scripts/redshift-autoloop/verify.sh b/scripts/redshift-autoloop/verify.sh index 5f6e4fe..d564522 100755 --- a/scripts/redshift-autoloop/verify.sh +++ b/scripts/redshift-autoloop/verify.sh @@ -256,6 +256,10 @@ services: enabled: false cloudRun: enabled: false + ssm: + enabled: false + sts: + enabled: false EOF local failures_before_build="${FAIL}" diff --git a/scripts/redshift-e2e.sh b/scripts/redshift-e2e.sh index 55e82ed..7b594f1 100755 --- a/scripts/redshift-e2e.sh +++ b/scripts/redshift-e2e.sh @@ -320,6 +320,10 @@ services: enabled: false cloudRun: enabled: false + ssm: + enabled: false + sts: + enabled: false EOF log "building devcloud" @@ -347,10 +351,103 @@ insert into e2e.events values (1, 'created'); select id, payload from e2e.events where id = 1; SQL -log "running Data API smoke" export AWS_ACCESS_KEY_ID="${AWS_ACCESS_KEY_ID:-dev}" export AWS_SECRET_ACCESS_KEY="${AWS_SECRET_ACCESS_KEY:-dev}" export AWS_DEFAULT_REGION="${AWS_DEFAULT_REGION:-us-east-1}" + +if [[ "${REDSHIFT_BACKEND_KIND}" == "postgres" ]]; then + # The statement sequence of a data load that stages through session temp + # tables: COPY from local S3 per format (plain and GZIP), then an upsert in + # one transaction. Run twice on separate connections — the second must not + # see the first connection's temp tables. + log "running COPY data load journey" + S3_ENDPOINT="http://${HOST}:${S3_PORT}" + printf '1,alice,10.5\n2,"bob, jr",20\n' >"${TMP_DIR}/d.csv" + printf '1\talice\t10.5\n2\tbob jr\t20\n' >"${TMP_DIR}/d.tsv" + printf '{"id":1,"name":"alice","amount":10.5}\n{"id":2,"name":"bob jr","amount":20}\n' >"${TMP_DIR}/d.json" + aws s3 mb s3://copy-e2e --endpoint-url "${S3_ENDPOINT}" >/dev/null + for format in csv tsv json; do + gzip -kf "${TMP_DIR}/d.${format}" + aws s3 cp "${TMP_DIR}/d.${format}" "s3://copy-e2e/plain/d.${format}" --endpoint-url "${S3_ENDPOINT}" >/dev/null + aws s3 cp "${TMP_DIR}/d.${format}.gz" "s3://copy-e2e/gz/d.${format}.gz" --endpoint-url "${S3_ENDPOINT}" >/dev/null + done + PGPASSWORD="${DB_PASSWORD}" psql "host=${HOST} port=${REDSHIFT_SQL_PORT} dbname=${DATABASE} user=${DB_USER} sslmode=disable" \ + -v ON_ERROR_STOP=1 -q <<'SQL' +create schema if not exists load; +drop table if exists load.target; +create table load.target (id integer, name varchar(64), amount numeric(10,2), csa_dwh_file_path varchar(1024), csa_dwh_created_at timestamp); +insert into load.target values (1, 'old', 0, 'x', '2020-01-01 00:00:00'); +SQL + copy_load() { + local key="$1" clause="$2" + PGPASSWORD="${DB_PASSWORD}" psql "host=${HOST} port=${REDSHIFT_SQL_PORT} dbname=${DATABASE} user=${DB_USER} sslmode=disable" \ + -v ON_ERROR_STOP=1 -q <&2 + exit 1 + fi + } + copy_load plain/d.csv "FORMAT AS CSV" ", jr" + copy_load gz/d.csv.gz "FORMAT AS CSV GZIP" ", jr" + copy_load plain/d.tsv "DELIMITER '\\t'" " jr" + copy_load gz/d.tsv.gz "DELIMITER '\\t' GZIP" " jr" + copy_load plain/d.json "FORMAT AS JSON 'auto'" " jr" + copy_load gz/d.json.gz "FORMAT AS JSON 'auto' GZIP" " jr" + + # A COPY is all or nothing: 600 good rows (more than one INSERT batch) in + # the first object and a type error in the second leave no rows behind, + # in autocommit and inside a transaction. + log "running COPY atomicity check" + python3 -c 'print("\n".join(f"{i},n{i}" for i in range(600)))' >"${TMP_DIR}/good.csv" + printf '600,ok\nnot-a-number,bad\n' >"${TMP_DIR}/bad.csv" + aws s3 cp "${TMP_DIR}/good.csv" s3://copy-e2e/atomic/1.csv --endpoint-url "${S3_ENDPOINT}" >/dev/null + aws s3 cp "${TMP_DIR}/bad.csv" s3://copy-e2e/atomic/2.csv --endpoint-url "${S3_ENDPOINT}" >/dev/null + PGPASSWORD="${DB_PASSWORD}" psql "host=${HOST} port=${REDSHIFT_SQL_PORT} dbname=${DATABASE} user=${DB_USER} sslmode=disable" \ + -v ON_ERROR_STOP=1 -q -c "drop table if exists load.atomic" -c "create table load.atomic (id integer, name varchar(16))" + for wrap in autocommit transaction; do + if [[ "${wrap}" == "transaction" ]]; then begin="BEGIN;"; end="ROLLBACK;"; else begin=""; end=""; fi + if PGPASSWORD="${DB_PASSWORD}" psql "host=${HOST} port=${REDSHIFT_SQL_PORT} dbname=${DATABASE} user=${DB_USER} sslmode=disable" \ + -v ON_ERROR_STOP=1 -q >/dev/null 2>&1 <&2 + exit 1 + fi + got="$(PGPASSWORD="${DB_PASSWORD}" psql "host=${HOST} port=${REDSHIFT_SQL_PORT} dbname=${DATABASE} user=${DB_USER} sslmode=disable" -Atc "select count(*) from load.atomic")" + if [[ "${got}" != "0" ]]; then + echo "[FAIL] failed COPY left ${got} rows (${wrap})" >&2 + exit 1 + fi + done + aws s3 rm s3://copy-e2e/atomic/2.csv --endpoint-url "${S3_ENDPOINT}" >/dev/null + got="$(PGPASSWORD="${DB_PASSWORD}" psql "host=${HOST} port=${REDSHIFT_SQL_PORT} dbname=${DATABASE} user=${DB_USER} sslmode=disable" \ + -qAtc "COPY load.atomic FROM 's3://copy-e2e/atomic/' IAM_ROLE 'r' CSV" -c "select count(*) from load.atomic")" + if [[ "${got}" != "600" ]]; then + echo "[FAIL] COPY after removing the bad object loaded: ${got}" >&2 + exit 1 + fi +fi + +log "running Data API smoke" STATEMENT_ID="$(aws redshift-data execute-statement \ --endpoint-url "${API_ENDPOINT}" \ --region "${AWS_DEFAULT_REGION}" \ diff --git a/scripts/redshift-translator-e2e.sh b/scripts/redshift-translator-e2e.sh index b079559..0f43143 100755 --- a/scripts/redshift-translator-e2e.sh +++ b/scripts/redshift-translator-e2e.sh @@ -166,6 +166,10 @@ services: enabled: false cloudRun: enabled: false + ssm: + enabled: false + sts: + enabled: false EOF log "building devcloud" diff --git a/scripts/s3-autoloop/verify.sh b/scripts/s3-autoloop/verify.sh index 9f8cee1..4c27797 100755 --- a/scripts/s3-autoloop/verify.sh +++ b/scripts/s3-autoloop/verify.sh @@ -104,6 +104,10 @@ services: enabled: false cloudRun: enabled: false + ssm: + enabled: false + sts: + enabled: false EOF run_check "devcloud binary builds" devcloud_build "${TMP_DIR}/devcloud" diff --git a/scripts/s3-e2e.sh b/scripts/s3-e2e.sh index 895723c..6d0b3ac 100755 --- a/scripts/s3-e2e.sh +++ b/scripts/s3-e2e.sh @@ -151,6 +151,10 @@ services: enabled: false cloudRun: enabled: false + ssm: + enabled: false + sts: + enabled: false EOF } diff --git a/scripts/sqs-autoloop/verify.sh b/scripts/sqs-autoloop/verify.sh index 99017ed..9e996a0 100755 --- a/scripts/sqs-autoloop/verify.sh +++ b/scripts/sqs-autoloop/verify.sh @@ -178,6 +178,10 @@ services: enabled: false cloudRun: enabled: false + ssm: + enabled: false + sts: + enabled: false EOF run_check "devcloud binary builds" devcloud_build "${TMP_DIR}/devcloud" diff --git a/scripts/sqs-e2e.sh b/scripts/sqs-e2e.sh index 2cfb33c..8d2865b 100755 --- a/scripts/sqs-e2e.sh +++ b/scripts/sqs-e2e.sh @@ -303,6 +303,10 @@ services: enabled: false cloudRun: enabled: false + ssm: + enabled: false + sts: + enabled: false EOF } diff --git a/scripts/ssm-e2e.sh b/scripts/ssm-e2e.sh new file mode 100755 index 0000000..49f87d4 --- /dev/null +++ b/scripts/ssm-e2e.sh @@ -0,0 +1,279 @@ +#!/usr/bin/env bash +set -euo pipefail + +ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +source "${ROOT_DIR}/scripts/lib/devcloud-engine.sh" +cd "${ROOT_DIR}" + +SVC_PORT="${E2E_SSM_PORT:-}" +DASHBOARD_PORT="${E2E_DASHBOARD_PORT:-}" +EVENT_RELAY_PORT="${E2E_EVENT_RELAY_PORT:-}" +SVC_ENDPOINT="" +DASHBOARD_ENDPOINT="" +REGION="${E2E_SSM_REGION:-us-east-1}" +ACCOUNT_ID="${E2E_SSM_ACCOUNT_ID:-000000000000}" +KEEP_WORKDIR="${E2E_KEEP_WORKDIR:-false}" +INTERACTIVE="${E2E_INTERACTIVE:-false}" +DELETE_DATA="${E2E_DELETE_DATA:-true}" + +TMP_DIR="" +DEV_PID="" +WORKSPACE="" + +usage() { + cat <<'EOF' +Usage: + scripts/ssm-e2e.sh + +Environment: + E2E_SSM_PORT=18031 Override the SSM endpoint port. Defaults to an available port. + E2E_DASHBOARD_PORT=18025 Override the dashboard port. Defaults to an available port. + E2E_EVENT_RELAY_PORT=18027 Override the dashboard event relay port. Defaults to an available port. + E2E_SSM_REGION=us-east-1 Override the configured region. + E2E_SSM_ACCOUNT_ID=000000000000 Override the configured account id. + E2E_DELETE_DATA=false Skip cleanup steps so created data stays behind for inspection. + E2E_KEEP_WORKDIR=true Keep the temporary workspace for debugging. + E2E_INTERACTIVE=true Keep devcloud running after assertions. +EOF +} + +if [[ "${1:-}" == "-h" || "${1:-}" == "--help" ]]; then + usage + exit 0 +fi + +if [[ "${INTERACTIVE}" == "true" && -z "${E2E_DELETE_DATA+x}" ]]; then + DELETE_DATA="false" +fi +if [[ "${INTERACTIVE}" == "true" && -z "${E2E_KEEP_WORKDIR+x}" ]]; then + KEEP_WORKDIR="true" +fi +if [[ "${DELETE_DATA}" == "false" && -z "${E2E_KEEP_WORKDIR+x}" ]]; then + KEEP_WORKDIR="true" +fi + +log() { + printf '[ssm-e2e] %s\n' "$1" +} + +cleanup() { + if [[ -n "${DEV_PID}" ]]; then + kill "${DEV_PID}" >/dev/null 2>&1 || true + wait "${DEV_PID}" >/dev/null 2>&1 || true + fi + if [[ "${KEEP_WORKDIR}" != "true" && -n "${TMP_DIR}" && -d "${TMP_DIR}" ]]; then + rm -rf "${TMP_DIR}" + elif [[ -n "${TMP_DIR}" ]]; then + log "kept workdir: ${TMP_DIR}" + fi +} +trap cleanup EXIT + +require_command() { + if ! command -v "$1" >/dev/null 2>&1; then + echo "[ssm-e2e] missing command: $1" >&2 + exit 1 + fi +} + +find_free_port() { + python3 - <<'PY' +import socket + +with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock: + sock.bind(("127.0.0.1", 0)) + print(sock.getsockname()[1]) +PY +} + +assign_ports() { + [[ -n "${SVC_PORT}" ]] || SVC_PORT="$(find_free_port)" + [[ -n "${DASHBOARD_PORT}" ]] || DASHBOARD_PORT="$(find_free_port)" + [[ -n "${EVENT_RELAY_PORT}" ]] || EVENT_RELAY_PORT="$(find_free_port)" + SVC_ENDPOINT="http://127.0.0.1:${SVC_PORT}" + DASHBOARD_ENDPOINT="http://127.0.0.1:${DASHBOARD_PORT}" +} + +json_assert() { + python3 -c 'import json,sys; data=json.load(sys.stdin); assert eval(sys.argv[1], {"json": json}, {"data": data}), data' "$1" +} + +wait_until() { + local what="$1" + shift + local deadline=$((SECONDS + 20)) + until "$@" >/dev/null 2>&1; do + if [[ -n "${DEV_PID}" ]] && ! kill -0 "${DEV_PID}" 2>/dev/null; then + echo "[ssm-e2e] devcloud exited while waiting for ${what}" >&2 + sed 's/^/[ssm-e2e] devcloud: /' "${TMP_DIR}/devcloud-up.log" >&2 || true + return 1 + fi + if (( SECONDS >= deadline )); then + echo "[ssm-e2e] timed out waiting for ${what}" >&2 + return 1 + fi + sleep 0.2 + done +} + +write_config() { + local workspace="$1" + mkdir -p "${workspace}/.devcloud" + cat > "${workspace}/.devcloud/config.yaml" <>"${TMP_DIR}/devcloud-up.log" 2>&1 & + DEV_PID="$!" +} + +start_devcloud() { + TMP_DIR="$(mktemp -d)" + WORKSPACE="${TMP_DIR}/workspace" + mkdir -p "${WORKSPACE}" + write_config "${WORKSPACE}" + + log "building devcloud" + devcloud_build "${TMP_DIR}/devcloud" + + log "starting devcloud" + launch_devcloud +} + +hold_if_interactive() { + if [[ "${INTERACTIVE}" == "true" ]]; then + printf '\n[ssm-e2e] interactive mode\n SSM endpoint: %s\n Dashboard: %s/dashboard/ssm\n Workspace: %s\n\nPress Ctrl-C to stop devcloud.\n' \ + "${SVC_ENDPOINT}" "${DASHBOARD_ENDPOINT}" "${WORKSPACE}" + while kill -0 "${DEV_PID}" 2>/dev/null; do + sleep 3600 + done + fi +} + +ssm() { + curl -sS \ + -X POST \ + -H 'Content-Type: application/x-amz-json-1.1' \ + -H "X-Amz-Target: AmazonSSM.$1" \ + --data "$2" \ + "${SVC_ENDPOINT}/" +} + +ssm_ok() { + curl -fsS \ + -X POST \ + -H 'Content-Type: application/x-amz-json-1.1' \ + -H "X-Amz-Target: AmazonSSM.$1" \ + --data "$2" \ + "${SVC_ENDPOINT}/" +} + +main() { + require_command curl + require_command python3 + assign_ports + start_devcloud + + wait_until "ssm" ssm_ok DescribeParameters '{}' + wait_until "dashboard" curl -fsS "${DASHBOARD_ENDPOINT}/" + + log "put / get String and SecureString" + ssm_ok PutParameter '{"Name":"/e2e/dwh/user","Value":"loader","Type":"String"}' | + json_assert 'data == {"Version": 1, "Tier": "Standard"}' + ssm_ok PutParameter '{"Name":"/e2e/dwh/password","Value":"e2e-s3cret","Type":"SecureString"}' | + json_assert 'data["Version"] == 1' + ssm_ok GetParameter '{"Name":"/e2e/dwh/user"}' | + json_assert 'data["Parameter"]["Value"] == "loader" and data["Parameter"]["ARN"] == "arn:aws:ssm:'"${REGION}"':'"${ACCOUNT_ID}"':parameter/e2e/dwh/user"' + ssm_ok GetParameter '{"Name":"/e2e/dwh/password","WithDecryption":true}' | + json_assert 'data["Parameter"]["Value"] == "e2e-s3cret" and data["Parameter"]["Type"] == "SecureString"' + ssm_ok GetParameter '{"Name":"/e2e/dwh/password"}' | + json_assert '"e2e-s3cret" not in data["Parameter"]["Value"]' + + log "overwrite rules" + ssm PutParameter '{"Name":"/e2e/dwh/user","Value":"other","Type":"String"}' | + json_assert 'data["__type"] == "ParameterAlreadyExists"' + ssm_ok PutParameter '{"Name":"/e2e/dwh/user","Value":"other","Overwrite":true}' | + json_assert 'data["Version"] == 2' + ssm GetParameter '{"Name":"/e2e/missing"}' | + json_assert 'data["__type"] == "ParameterNotFound"' + + log "dashboard lists metadata without values" + curl -fsS "${DASHBOARD_ENDPOINT}/api/ssm/parameters" | + json_assert 'sorted(p["Name"] for p in data["parameters"]) == ["/e2e/dwh/password", "/e2e/dwh/user"] and "e2e-s3cret" not in json.dumps(data) and "Value" not in json.dumps(data)' + curl -fsS "${DASHBOARD_ENDPOINT}/api/ssm/status" | + json_assert 'data["status"] == "running" and data["parameterCount"] == 2' + + log "restart keeps parameters" + kill "${DEV_PID}" + wait "${DEV_PID}" 2>/dev/null || true + launch_devcloud + wait_until "ssm after restart" ssm_ok DescribeParameters '{}' + ssm_ok GetParameter '{"Name":"/e2e/dwh/password","WithDecryption":true}' | + json_assert 'data["Parameter"]["Value"] == "e2e-s3cret"' + + if [[ "${DELETE_DATA}" == "true" ]]; then + log "delete" + ssm_ok DeleteParameter '{"Name":"/e2e/dwh/user"}' | json_assert 'data == {}' + ssm_ok DeleteParameter '{"Name":"/e2e/dwh/password"}' | json_assert 'data == {}' + ssm_ok DescribeParameters '{}' | json_assert 'data["Parameters"] == []' + fi + + hold_if_interactive + log "SSM E2E passed" +} + +main "$@" diff --git a/scripts/sts-e2e.sh b/scripts/sts-e2e.sh new file mode 100755 index 0000000..21f9463 --- /dev/null +++ b/scripts/sts-e2e.sh @@ -0,0 +1,269 @@ +#!/usr/bin/env bash +set -euo pipefail + +ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +source "${ROOT_DIR}/scripts/lib/devcloud-engine.sh" +cd "${ROOT_DIR}" + +SVC_PORT="${E2E_STS_PORT:-}" +DASHBOARD_PORT="${E2E_DASHBOARD_PORT:-}" +EVENT_RELAY_PORT="${E2E_EVENT_RELAY_PORT:-}" +SVC_ENDPOINT="" +DASHBOARD_ENDPOINT="" +REGION="${E2E_STS_REGION:-us-east-1}" +ACCOUNT_ID="${E2E_STS_ACCOUNT_ID:-000000000000}" +KEEP_WORKDIR="${E2E_KEEP_WORKDIR:-false}" +INTERACTIVE="${E2E_INTERACTIVE:-false}" +DELETE_DATA="${E2E_DELETE_DATA:-true}" + +TMP_DIR="" +DEV_PID="" +WORKSPACE="" + +usage() { + cat <<'EOF' +Usage: + scripts/sts-e2e.sh + +Environment: + E2E_STS_PORT=18032 Override the STS endpoint port. Defaults to an available port. + E2E_DASHBOARD_PORT=18025 Override the dashboard port. Defaults to an available port. + E2E_EVENT_RELAY_PORT=18027 Override the dashboard event relay port. Defaults to an available port. + E2E_STS_REGION=us-east-1 Override the configured region. + E2E_STS_ACCOUNT_ID=000000000000 Override the configured account id. + E2E_DELETE_DATA=false Skip cleanup steps so created data stays behind for inspection. + E2E_KEEP_WORKDIR=true Keep the temporary workspace for debugging. + E2E_INTERACTIVE=true Keep devcloud running after assertions. +EOF +} + +if [[ "${1:-}" == "-h" || "${1:-}" == "--help" ]]; then + usage + exit 0 +fi + +if [[ "${INTERACTIVE}" == "true" && -z "${E2E_DELETE_DATA+x}" ]]; then + DELETE_DATA="false" +fi +if [[ "${INTERACTIVE}" == "true" && -z "${E2E_KEEP_WORKDIR+x}" ]]; then + KEEP_WORKDIR="true" +fi +if [[ "${DELETE_DATA}" == "false" && -z "${E2E_KEEP_WORKDIR+x}" ]]; then + KEEP_WORKDIR="true" +fi + +log() { + printf '[sts-e2e] %s\n' "$1" +} + +cleanup() { + if [[ -n "${DEV_PID}" ]]; then + kill "${DEV_PID}" >/dev/null 2>&1 || true + wait "${DEV_PID}" >/dev/null 2>&1 || true + fi + if [[ "${KEEP_WORKDIR}" != "true" && -n "${TMP_DIR}" && -d "${TMP_DIR}" ]]; then + rm -rf "${TMP_DIR}" + elif [[ -n "${TMP_DIR}" ]]; then + log "kept workdir: ${TMP_DIR}" + fi +} +trap cleanup EXIT + +require_command() { + if ! command -v "$1" >/dev/null 2>&1; then + echo "[sts-e2e] missing command: $1" >&2 + exit 1 + fi +} + +find_free_port() { + python3 - <<'PY' +import socket + +with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock: + sock.bind(("127.0.0.1", 0)) + print(sock.getsockname()[1]) +PY +} + +assign_ports() { + [[ -n "${SVC_PORT}" ]] || SVC_PORT="$(find_free_port)" + [[ -n "${DASHBOARD_PORT}" ]] || DASHBOARD_PORT="$(find_free_port)" + [[ -n "${EVENT_RELAY_PORT}" ]] || EVENT_RELAY_PORT="$(find_free_port)" + SVC_ENDPOINT="http://127.0.0.1:${SVC_PORT}" + DASHBOARD_ENDPOINT="http://127.0.0.1:${DASHBOARD_PORT}" +} + +json_assert() { + python3 -c 'import json,sys; data=json.load(sys.stdin); assert eval(sys.argv[1], {"json": json}, {"data": data}), data' "$1" +} + +wait_until() { + local what="$1" + shift + local deadline=$((SECONDS + 20)) + until "$@" >/dev/null 2>&1; do + if [[ -n "${DEV_PID}" ]] && ! kill -0 "${DEV_PID}" 2>/dev/null; then + echo "[sts-e2e] devcloud exited while waiting for ${what}" >&2 + sed 's/^/[sts-e2e] devcloud: /' "${TMP_DIR}/devcloud-up.log" >&2 || true + return 1 + fi + if (( SECONDS >= deadline )); then + echo "[sts-e2e] timed out waiting for ${what}" >&2 + return 1 + fi + sleep 0.2 + done +} + +write_config() { + local workspace="$1" + mkdir -p "${workspace}/.devcloud" + cat > "${workspace}/.devcloud/config.yaml" <>"${TMP_DIR}/devcloud-up.log" 2>&1 & + DEV_PID="$!" +} + +start_devcloud() { + TMP_DIR="$(mktemp -d)" + WORKSPACE="${TMP_DIR}/workspace" + mkdir -p "${WORKSPACE}" + write_config "${WORKSPACE}" + + log "building devcloud" + devcloud_build "${TMP_DIR}/devcloud" + + log "starting devcloud" + launch_devcloud +} + +hold_if_interactive() { + if [[ "${INTERACTIVE}" == "true" ]]; then + printf '\n[sts-e2e] interactive mode\n STS endpoint: %s\n Dashboard: %s/dashboard/sts\n Workspace: %s\n\nPress Ctrl-C to stop devcloud.\n' \ + "${SVC_ENDPOINT}" "${DASHBOARD_ENDPOINT}" "${WORKSPACE}" + while kill -0 "${DEV_PID}" 2>/dev/null; do + sleep 3600 + done + fi +} + +sts() { + curl -sS \ + -X POST \ + -H 'Content-Type: application/x-www-form-urlencoded' \ + --data "$1" \ + "${SVC_ENDPOINT}/" +} + +sts_ok() { + curl -fsS \ + -X POST \ + -H 'Content-Type: application/x-www-form-urlencoded' \ + --data "$1" \ + "${SVC_ENDPOINT}/" +} + +xml_assert() { + python3 -c ' +import sys, xml.etree.ElementTree as ET +root = ET.fromstring(sys.stdin.read()) +ns = {"s": "https://sts.amazonaws.com/doc/2011-06-15/"} +def text(path): + node = root.find(path, ns) + return None if node is None else node.text +assert eval(sys.argv[1], {}, {"root": root, "text": text}), ET.tostring(root) +' "$1" +} + +main() { + require_command curl + require_command python3 + assign_ports + start_devcloud + + wait_until "sts" sts_ok 'Action=GetCallerIdentity&Version=2011-06-15' + wait_until "dashboard" curl -fsS "${DASHBOARD_ENDPOINT}/" + + log "GetCallerIdentity" + sts_ok 'Action=GetCallerIdentity&Version=2011-06-15' | + xml_assert 'text("s:GetCallerIdentityResult/s:Account") == "'"${ACCOUNT_ID}"'"' + + log "AssumeRole with an inline session policy" + local policy + policy="$(python3 -c 'import urllib.parse; print(urllib.parse.quote("{\"Version\":\"2012-10-17\",\"Statement\":[{\"Effect\":\"Allow\",\"Action\":\"s3:GetObject\",\"Resource\":\"*\"}]}", safe=""))')" + sts_ok "Action=AssumeRole&Version=2011-06-15&RoleArn=arn%3Aaws%3Aiam%3A%3A${ACCOUNT_ID}%3Arole%2Fe2e-runner&RoleSessionName=site-a&DurationSeconds=900&Policy=${policy}" | + xml_assert 'text("s:AssumeRoleResult/s:Credentials/s:AccessKeyId").startswith("ASIA") and text("s:AssumeRoleResult/s:Credentials/s:SessionToken") and text("s:AssumeRoleResult/s:AssumedRoleUser/s:Arn") == "arn:aws:sts::'"${ACCOUNT_ID}"':assumed-role/e2e-runner/site-a"' + + log "AssumeRole validation" + sts 'Action=AssumeRole&Version=2011-06-15&RoleArn=arn%3Aaws%3Aiam%3A%3A1%3Arole%2Fr&RoleSessionName=s1&Policy=not-json' | + xml_assert 'text("s:Error/s:Code") == "MalformedPolicyDocument"' + sts 'Action=AssumeRole&Version=2011-06-15&RoleSessionName=s1' | + xml_assert 'text("s:Error/s:Code") == "ValidationError"' + + log "dashboard" + curl -fsS "${DASHBOARD_ENDPOINT}/api/sts/status" | json_assert 'data["status"] == "running"' + curl -fsS "${DASHBOARD_ENDPOINT}/api/sts/caller-identity" | + json_assert 'data["account"] == "'"${ACCOUNT_ID}"'"' + + hold_if_interactive + log "STS E2E passed" +} + +main "$@" diff --git a/services/dashboard/assets/react/assets/index-C7w4pPfk.js b/services/dashboard/assets/react/assets/index-BO3HAKPI.js similarity index 50% rename from services/dashboard/assets/react/assets/index-C7w4pPfk.js rename to services/dashboard/assets/react/assets/index-BO3HAKPI.js index 045184c..2652a0d 100644 --- a/services/dashboard/assets/react/assets/index-C7w4pPfk.js +++ b/services/dashboard/assets/react/assets/index-BO3HAKPI.js @@ -1,4 +1,4 @@ -var Zf=Object.defineProperty;var ep=(r,l,i)=>l in r?Zf(r,l,{enumerable:!0,configurable:!0,writable:!0,value:i}):r[l]=i;var fs=(r,l,i)=>ep(r,typeof l!="symbol"?l+"":l,i);(function(){const l=document.createElement("link").relList;if(l&&l.supports&&l.supports("modulepreload"))return;for(const d of document.querySelectorAll('link[rel="modulepreload"]'))u(d);new MutationObserver(d=>{for(const p of d)if(p.type==="childList")for(const g of p.addedNodes)g.tagName==="LINK"&&g.rel==="modulepreload"&&u(g)}).observe(document,{childList:!0,subtree:!0});function i(d){const p={};return d.integrity&&(p.integrity=d.integrity),d.referrerPolicy&&(p.referrerPolicy=d.referrerPolicy),d.crossOrigin==="use-credentials"?p.credentials="include":d.crossOrigin==="anonymous"?p.credentials="omit":p.credentials="same-origin",p}function u(d){if(d.ep)return;d.ep=!0;const p=i(d);fetch(d.href,p)}})();function zu(r){return r&&r.__esModule&&Object.prototype.hasOwnProperty.call(r,"default")?r.default:r}var Ci={exports:{}},Rr={},Ri={exports:{}},Te={};/** +var Zf=Object.defineProperty;var ep=(r,i,l)=>i in r?Zf(r,i,{enumerable:!0,configurable:!0,writable:!0,value:l}):r[i]=l;var fs=(r,i,l)=>ep(r,typeof i!="symbol"?i+"":i,l);(function(){const i=document.createElement("link").relList;if(i&&i.supports&&i.supports("modulepreload"))return;for(const d of document.querySelectorAll('link[rel="modulepreload"]'))u(d);new MutationObserver(d=>{for(const p of d)if(p.type==="childList")for(const x of p.addedNodes)x.tagName==="LINK"&&x.rel==="modulepreload"&&u(x)}).observe(document,{childList:!0,subtree:!0});function l(d){const p={};return d.integrity&&(p.integrity=d.integrity),d.referrerPolicy&&(p.referrerPolicy=d.referrerPolicy),d.crossOrigin==="use-credentials"?p.credentials="include":d.crossOrigin==="anonymous"?p.credentials="omit":p.credentials="same-origin",p}function u(d){if(d.ep)return;d.ep=!0;const p=l(d);fetch(d.href,p)}})();function zu(r){return r&&r.__esModule&&Object.prototype.hasOwnProperty.call(r,"default")?r.default:r}var Cl={exports:{}},Rr={},Rl={exports:{}},Te={};/** * @license React * react.production.min.js * @@ -6,7 +6,7 @@ var Zf=Object.defineProperty;var ep=(r,l,i)=>l in r?Zf(r,l,{enumerable:!0,config * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. - */var xu;function tp(){if(xu)return Te;xu=1;var r=Symbol.for("react.element"),l=Symbol.for("react.portal"),i=Symbol.for("react.fragment"),u=Symbol.for("react.strict_mode"),d=Symbol.for("react.profiler"),p=Symbol.for("react.provider"),g=Symbol.for("react.context"),y=Symbol.for("react.forward_ref"),S=Symbol.for("react.suspense"),v=Symbol.for("react.memo"),N=Symbol.for("react.lazy"),w=Symbol.iterator;function L(j){return j===null||typeof j!="object"?null:(j=w&&j[w]||j["@@iterator"],typeof j=="function"?j:null)}var C={isMounted:function(){return!1},enqueueForceUpdate:function(){},enqueueReplaceState:function(){},enqueueSetState:function(){}},A=Object.assign,I={};function E(j,T,ne){this.props=j,this.context=T,this.refs=I,this.updater=ne||C}E.prototype.isReactComponent={},E.prototype.setState=function(j,T){if(typeof j!="object"&&typeof j!="function"&&j!=null)throw Error("setState(...): takes an object of state variables to update or a function which returns an object of state variables.");this.updater.enqueueSetState(this,j,T,"setState")},E.prototype.forceUpdate=function(j){this.updater.enqueueForceUpdate(this,j,"forceUpdate")};function J(){}J.prototype=E.prototype;function ue(j,T,ne){this.props=j,this.context=T,this.refs=I,this.updater=ne||C}var q=ue.prototype=new J;q.constructor=ue,A(q,E.prototype),q.isPureReactComponent=!0;var P=Array.isArray,oe=Object.prototype.hasOwnProperty,pe={current:null},Z={key:!0,ref:!0,__self:!0,__source:!0};function Re(j,T,ne){var ce,ge={},we=null,De=null;if(T!=null)for(ce in T.ref!==void 0&&(De=T.ref),T.key!==void 0&&(we=""+T.key),T)oe.call(T,ce)&&!Z.hasOwnProperty(ce)&&(ge[ce]=T[ce]);var se=arguments.length-2;if(se===1)ge.children=ne;else if(1l in r?Zf(r,l,{enumerable:!0,config * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. - */var ju;function np(){if(ju)return Rr;ju=1;var r=Bi(),l=Symbol.for("react.element"),i=Symbol.for("react.fragment"),u=Object.prototype.hasOwnProperty,d=r.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.ReactCurrentOwner,p={key:!0,ref:!0,__self:!0,__source:!0};function g(y,S,v){var N,w={},L=null,C=null;v!==void 0&&(L=""+v),S.key!==void 0&&(L=""+S.key),S.ref!==void 0&&(C=S.ref);for(N in S)u.call(S,N)&&!p.hasOwnProperty(N)&&(w[N]=S[N]);if(y&&y.defaultProps)for(N in S=y.defaultProps,S)w[N]===void 0&&(w[N]=S[N]);return{$$typeof:l,type:y,key:L,ref:C,props:w,_owner:d.current}}return Rr.Fragment=i,Rr.jsx=g,Rr.jsxs=g,Rr}var vu;function sp(){return vu||(vu=1,Ci.exports=np()),Ci.exports}var n=sp(),m=Bi();const rp=zu(m);var _a={},Di={exports:{}},_t={},Ii={exports:{}},Ei={};/** + */var ju;function np(){if(ju)return Rr;ju=1;var r=Bl(),i=Symbol.for("react.element"),l=Symbol.for("react.fragment"),u=Object.prototype.hasOwnProperty,d=r.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.ReactCurrentOwner,p={key:!0,ref:!0,__self:!0,__source:!0};function x(g,S,v){var N,w={},L=null,C=null;v!==void 0&&(L=""+v),S.key!==void 0&&(L=""+S.key),S.ref!==void 0&&(C=S.ref);for(N in S)u.call(S,N)&&!p.hasOwnProperty(N)&&(w[N]=S[N]);if(g&&g.defaultProps)for(N in S=g.defaultProps,S)w[N]===void 0&&(w[N]=S[N]);return{$$typeof:i,type:g,key:L,ref:C,props:w,_owner:d.current}}return Rr.Fragment=l,Rr.jsx=x,Rr.jsxs=x,Rr}var vu;function sp(){return vu||(vu=1,Cl.exports=np()),Cl.exports}var n=sp(),m=Bl();const rp=zu(m);var _a={},Dl={exports:{}},_t={},Il={exports:{}},El={};/** * @license React * scheduler.production.min.js * @@ -22,7 +22,7 @@ var Zf=Object.defineProperty;var ep=(r,l,i)=>l in r?Zf(r,l,{enumerable:!0,config * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. - */var bu;function ap(){return bu||(bu=1,(function(r){function l($,U){var _=$.length;$.push(U);e:for(;0<_;){var j=_-1>>>1,T=$[j];if(0>>1;jd(ge,_))wed(De,ge)?($[j]=De,$[we]=_,j=we):($[j]=ge,$[ce]=_,j=ce);else if(wed(De,_))$[j]=De,$[we]=_,j=we;else break e}}return U}function d($,U){var _=$.sortIndex-U.sortIndex;return _!==0?_:$.id-U.id}if(typeof performance=="object"&&typeof performance.now=="function"){var p=performance;r.unstable_now=function(){return p.now()}}else{var g=Date,y=g.now();r.unstable_now=function(){return g.now()-y}}var S=[],v=[],N=1,w=null,L=3,C=!1,A=!1,I=!1,E=typeof setTimeout=="function"?setTimeout:null,J=typeof clearTimeout=="function"?clearTimeout:null,ue=typeof setImmediate<"u"?setImmediate:null;typeof navigator<"u"&&navigator.scheduling!==void 0&&navigator.scheduling.isInputPending!==void 0&&navigator.scheduling.isInputPending.bind(navigator.scheduling);function q($){for(var U=i(v);U!==null;){if(U.callback===null)u(v);else if(U.startTime<=$)u(v),U.sortIndex=U.expirationTime,l(S,U);else break;U=i(v)}}function P($){if(I=!1,q($),!A)if(i(S)!==null)A=!0,$e(oe);else{var U=i(v);U!==null&&Ne(P,U.startTime-$)}}function oe($,U){A=!1,I&&(I=!1,J(Re),Re=-1),C=!0;var _=L;try{for(q(U),w=i(S);w!==null&&(!(w.expirationTime>U)||$&&!Se());){var j=w.callback;if(typeof j=="function"){w.callback=null,L=w.priorityLevel;var T=j(w.expirationTime<=U);U=r.unstable_now(),typeof T=="function"?w.callback=T:w===i(S)&&u(S),q(U)}else u(S);w=i(S)}if(w!==null)var ne=!0;else{var ce=i(v);ce!==null&&Ne(P,ce.startTime-U),ne=!1}return ne}finally{w=null,L=_,C=!1}}var pe=!1,Z=null,Re=-1,ee=5,fe=-1;function Se(){return!(r.unstable_now()-fe$||125<$?console.error("forceFrameRate takes a positive int between 0 and 125, forcing frame rates higher than 125 fps is not supported"):ee=0<$?Math.floor(1e3/$):5},r.unstable_getCurrentPriorityLevel=function(){return L},r.unstable_getFirstCallbackNode=function(){return i(S)},r.unstable_next=function($){switch(L){case 1:case 2:case 3:var U=3;break;default:U=L}var _=L;L=U;try{return $()}finally{L=_}},r.unstable_pauseExecution=function(){},r.unstable_requestPaint=function(){},r.unstable_runWithPriority=function($,U){switch($){case 1:case 2:case 3:case 4:case 5:break;default:$=3}var _=L;L=$;try{return U()}finally{L=_}},r.unstable_scheduleCallback=function($,U,_){var j=r.unstable_now();switch(typeof _=="object"&&_!==null?(_=_.delay,_=typeof _=="number"&&0<_?j+_:j):_=j,$){case 1:var T=-1;break;case 2:T=250;break;case 5:T=1073741823;break;case 4:T=1e4;break;default:T=5e3}return T=_+T,$={id:N++,callback:U,priorityLevel:$,startTime:_,expirationTime:T,sortIndex:-1},_>j?($.sortIndex=_,l(v,$),i(S)===null&&$===i(v)&&(I?(J(Re),Re=-1):I=!0,Ne(P,_-j))):($.sortIndex=T,l(S,$),A||C||(A=!0,$e(oe))),$},r.unstable_shouldYield=Se,r.unstable_wrapCallback=function($){var U=L;return function(){var _=L;L=U;try{return $.apply(this,arguments)}finally{L=_}}}})(Ei)),Ei}var Su;function lp(){return Su||(Su=1,Ii.exports=ap()),Ii.exports}/** + */var bu;function ap(){return bu||(bu=1,(function(r){function i(P,U){var B=P.length;P.push(U);e:for(;0>>1,T=P[j];if(0>>1;jd(xe,B))wed(De,xe)?(P[j]=De,P[we]=B,j=we):(P[j]=xe,P[ce]=B,j=ce);else if(wed(De,B))P[j]=De,P[we]=B,j=we;else break e}}return U}function d(P,U){var B=P.sortIndex-U.sortIndex;return B!==0?B:P.id-U.id}if(typeof performance=="object"&&typeof performance.now=="function"){var p=performance;r.unstable_now=function(){return p.now()}}else{var x=Date,g=x.now();r.unstable_now=function(){return x.now()-g}}var S=[],v=[],N=1,w=null,L=3,C=!1,_=!1,I=!1,E=typeof setTimeout=="function"?setTimeout:null,J=typeof clearTimeout=="function"?clearTimeout:null,de=typeof setImmediate<"u"?setImmediate:null;typeof navigator<"u"&&navigator.scheduling!==void 0&&navigator.scheduling.isInputPending!==void 0&&navigator.scheduling.isInputPending.bind(navigator.scheduling);function V(P){for(var U=l(v);U!==null;){if(U.callback===null)u(v);else if(U.startTime<=P)u(v),U.sortIndex=U.expirationTime,i(S,U);else break;U=l(v)}}function $(P){if(I=!1,V(P),!_)if(l(S)!==null)_=!0,Pe(oe);else{var U=l(v);U!==null&&Ne($,U.startTime-P)}}function oe(P,U){_=!1,I&&(I=!1,J(Re),Re=-1),C=!0;var B=L;try{for(V(U),w=l(S);w!==null&&(!(w.expirationTime>U)||P&&!Se());){var j=w.callback;if(typeof j=="function"){w.callback=null,L=w.priorityLevel;var T=j(w.expirationTime<=U);U=r.unstable_now(),typeof T=="function"?w.callback=T:w===l(S)&&u(S),V(U)}else u(S);w=l(S)}if(w!==null)var se=!0;else{var ce=l(v);ce!==null&&Ne($,ce.startTime-U),se=!1}return se}finally{w=null,L=B,C=!1}}var me=!1,Z=null,Re=-1,ee=5,pe=-1;function Se(){return!(r.unstable_now()-peP||125j?(P.sortIndex=B,i(v,P),l(S)===null&&P===l(v)&&(I?(J(Re),Re=-1):I=!0,Ne($,B-j))):(P.sortIndex=T,i(S,P),_||C||(_=!0,Pe(oe))),P},r.unstable_shouldYield=Se,r.unstable_wrapCallback=function(P){var U=L;return function(){var B=L;L=U;try{return P.apply(this,arguments)}finally{L=B}}}})(El)),El}var Su;function ip(){return Su||(Su=1,Il.exports=ap()),Il.exports}/** * @license React * react-dom.production.min.js * @@ -30,16 +30,16 @@ var Zf=Object.defineProperty;var ep=(r,l,i)=>l in r?Zf(r,l,{enumerable:!0,config * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. - */var Nu;function ip(){if(Nu)return _t;Nu=1;var r=Bi(),l=lp();function i(e){for(var t="https://reactjs.org/docs/error-decoder.html?invariant="+e,s=1;s"u"||typeof window.document>"u"||typeof window.document.createElement>"u"),S=Object.prototype.hasOwnProperty,v=/^[:A-Z_a-z\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u02FF\u0370-\u037D\u037F-\u1FFF\u200C-\u200D\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD][:A-Z_a-z\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u02FF\u0370-\u037D\u037F-\u1FFF\u200C-\u200D\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD\-.0-9\u00B7\u0300-\u036F\u203F-\u2040]*$/,N={},w={};function L(e){return S.call(w,e)?!0:S.call(N,e)?!1:v.test(e)?w[e]=!0:(N[e]=!0,!1)}function C(e,t,s,a){if(s!==null&&s.type===0)return!1;switch(typeof t){case"function":case"symbol":return!0;case"boolean":return a?!1:s!==null?!s.acceptsBooleans:(e=e.toLowerCase().slice(0,5),e!=="data-"&&e!=="aria-");default:return!1}}function A(e,t,s,a){if(t===null||typeof t>"u"||C(e,t,s,a))return!0;if(a)return!1;if(s!==null)switch(s.type){case 3:return!t;case 4:return t===!1;case 5:return isNaN(t);case 6:return isNaN(t)||1>t}return!1}function I(e,t,s,a,o,c,f){this.acceptsBooleans=t===2||t===3||t===4,this.attributeName=a,this.attributeNamespace=o,this.mustUseProperty=s,this.propertyName=e,this.type=t,this.sanitizeURL=c,this.removeEmptyString=f}var E={};"children dangerouslySetInnerHTML defaultValue defaultChecked innerHTML suppressContentEditableWarning suppressHydrationWarning style".split(" ").forEach(function(e){E[e]=new I(e,0,!1,e,null,!1,!1)}),[["acceptCharset","accept-charset"],["className","class"],["htmlFor","for"],["httpEquiv","http-equiv"]].forEach(function(e){var t=e[0];E[t]=new I(t,1,!1,e[1],null,!1,!1)}),["contentEditable","draggable","spellCheck","value"].forEach(function(e){E[e]=new I(e,2,!1,e.toLowerCase(),null,!1,!1)}),["autoReverse","externalResourcesRequired","focusable","preserveAlpha"].forEach(function(e){E[e]=new I(e,2,!1,e,null,!1,!1)}),"allowFullScreen async autoFocus autoPlay controls default defer disabled disablePictureInPicture disableRemotePlayback formNoValidate hidden loop noModule noValidate open playsInline readOnly required reversed scoped seamless itemScope".split(" ").forEach(function(e){E[e]=new I(e,3,!1,e.toLowerCase(),null,!1,!1)}),["checked","multiple","muted","selected"].forEach(function(e){E[e]=new I(e,3,!0,e,null,!1,!1)}),["capture","download"].forEach(function(e){E[e]=new I(e,4,!1,e,null,!1,!1)}),["cols","rows","size","span"].forEach(function(e){E[e]=new I(e,6,!1,e,null,!1,!1)}),["rowSpan","start"].forEach(function(e){E[e]=new I(e,5,!1,e.toLowerCase(),null,!1,!1)});var J=/[\-:]([a-z])/g;function ue(e){return e[1].toUpperCase()}"accent-height alignment-baseline arabic-form baseline-shift cap-height clip-path clip-rule color-interpolation color-interpolation-filters color-profile color-rendering dominant-baseline enable-background fill-opacity fill-rule flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-name glyph-orientation-horizontal glyph-orientation-vertical horiz-adv-x horiz-origin-x image-rendering letter-spacing lighting-color marker-end marker-mid marker-start overline-position overline-thickness paint-order panose-1 pointer-events rendering-intent shape-rendering stop-color stop-opacity strikethrough-position strikethrough-thickness stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width text-anchor text-decoration text-rendering underline-position underline-thickness unicode-bidi unicode-range units-per-em v-alphabetic v-hanging v-ideographic v-mathematical vector-effect vert-adv-y vert-origin-x vert-origin-y word-spacing writing-mode xmlns:xlink x-height".split(" ").forEach(function(e){var t=e.replace(J,ue);E[t]=new I(t,1,!1,e,null,!1,!1)}),"xlink:actuate xlink:arcrole xlink:role xlink:show xlink:title xlink:type".split(" ").forEach(function(e){var t=e.replace(J,ue);E[t]=new I(t,1,!1,e,"http://www.w3.org/1999/xlink",!1,!1)}),["xml:base","xml:lang","xml:space"].forEach(function(e){var t=e.replace(J,ue);E[t]=new I(t,1,!1,e,"http://www.w3.org/XML/1998/namespace",!1,!1)}),["tabIndex","crossOrigin"].forEach(function(e){E[e]=new I(e,1,!1,e.toLowerCase(),null,!1,!1)}),E.xlinkHref=new I("xlinkHref",1,!1,"xlink:href","http://www.w3.org/1999/xlink",!0,!1),["src","href","action","formAction"].forEach(function(e){E[e]=new I(e,1,!1,e.toLowerCase(),null,!0,!0)});function q(e,t,s,a){var o=E.hasOwnProperty(t)?E[t]:null;(o!==null?o.type!==0:a||!(2"u"||typeof window.document>"u"||typeof window.document.createElement>"u"),S=Object.prototype.hasOwnProperty,v=/^[:A-Z_a-z\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u02FF\u0370-\u037D\u037F-\u1FFF\u200C-\u200D\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD][:A-Z_a-z\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u02FF\u0370-\u037D\u037F-\u1FFF\u200C-\u200D\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD\-.0-9\u00B7\u0300-\u036F\u203F-\u2040]*$/,N={},w={};function L(e){return S.call(w,e)?!0:S.call(N,e)?!1:v.test(e)?w[e]=!0:(N[e]=!0,!1)}function C(e,t,s,a){if(s!==null&&s.type===0)return!1;switch(typeof t){case"function":case"symbol":return!0;case"boolean":return a?!1:s!==null?!s.acceptsBooleans:(e=e.toLowerCase().slice(0,5),e!=="data-"&&e!=="aria-");default:return!1}}function _(e,t,s,a){if(t===null||typeof t>"u"||C(e,t,s,a))return!0;if(a)return!1;if(s!==null)switch(s.type){case 3:return!t;case 4:return t===!1;case 5:return isNaN(t);case 6:return isNaN(t)||1>t}return!1}function I(e,t,s,a,o,c,f){this.acceptsBooleans=t===2||t===3||t===4,this.attributeName=a,this.attributeNamespace=o,this.mustUseProperty=s,this.propertyName=e,this.type=t,this.sanitizeURL=c,this.removeEmptyString=f}var E={};"children dangerouslySetInnerHTML defaultValue defaultChecked innerHTML suppressContentEditableWarning suppressHydrationWarning style".split(" ").forEach(function(e){E[e]=new I(e,0,!1,e,null,!1,!1)}),[["acceptCharset","accept-charset"],["className","class"],["htmlFor","for"],["httpEquiv","http-equiv"]].forEach(function(e){var t=e[0];E[t]=new I(t,1,!1,e[1],null,!1,!1)}),["contentEditable","draggable","spellCheck","value"].forEach(function(e){E[e]=new I(e,2,!1,e.toLowerCase(),null,!1,!1)}),["autoReverse","externalResourcesRequired","focusable","preserveAlpha"].forEach(function(e){E[e]=new I(e,2,!1,e,null,!1,!1)}),"allowFullScreen async autoFocus autoPlay controls default defer disabled disablePictureInPicture disableRemotePlayback formNoValidate hidden loop noModule noValidate open playsInline readOnly required reversed scoped seamless itemScope".split(" ").forEach(function(e){E[e]=new I(e,3,!1,e.toLowerCase(),null,!1,!1)}),["checked","multiple","muted","selected"].forEach(function(e){E[e]=new I(e,3,!0,e,null,!1,!1)}),["capture","download"].forEach(function(e){E[e]=new I(e,4,!1,e,null,!1,!1)}),["cols","rows","size","span"].forEach(function(e){E[e]=new I(e,6,!1,e,null,!1,!1)}),["rowSpan","start"].forEach(function(e){E[e]=new I(e,5,!1,e.toLowerCase(),null,!1,!1)});var J=/[\-:]([a-z])/g;function de(e){return e[1].toUpperCase()}"accent-height alignment-baseline arabic-form baseline-shift cap-height clip-path clip-rule color-interpolation color-interpolation-filters color-profile color-rendering dominant-baseline enable-background fill-opacity fill-rule flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-name glyph-orientation-horizontal glyph-orientation-vertical horiz-adv-x horiz-origin-x image-rendering letter-spacing lighting-color marker-end marker-mid marker-start overline-position overline-thickness paint-order panose-1 pointer-events rendering-intent shape-rendering stop-color stop-opacity strikethrough-position strikethrough-thickness stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width text-anchor text-decoration text-rendering underline-position underline-thickness unicode-bidi unicode-range units-per-em v-alphabetic v-hanging v-ideographic v-mathematical vector-effect vert-adv-y vert-origin-x vert-origin-y word-spacing writing-mode xmlns:xlink x-height".split(" ").forEach(function(e){var t=e.replace(J,de);E[t]=new I(t,1,!1,e,null,!1,!1)}),"xlink:actuate xlink:arcrole xlink:role xlink:show xlink:title xlink:type".split(" ").forEach(function(e){var t=e.replace(J,de);E[t]=new I(t,1,!1,e,"http://www.w3.org/1999/xlink",!1,!1)}),["xml:base","xml:lang","xml:space"].forEach(function(e){var t=e.replace(J,de);E[t]=new I(t,1,!1,e,"http://www.w3.org/XML/1998/namespace",!1,!1)}),["tabIndex","crossOrigin"].forEach(function(e){E[e]=new I(e,1,!1,e.toLowerCase(),null,!1,!1)}),E.xlinkHref=new I("xlinkHref",1,!1,"xlink:href","http://www.w3.org/1999/xlink",!0,!1),["src","href","action","formAction"].forEach(function(e){E[e]=new I(e,1,!1,e.toLowerCase(),null,!0,!0)});function V(e,t,s,a){var o=E.hasOwnProperty(t)?E[t]:null;(o!==null?o.type!==0:a||!(2h||o[f]!==c[h]){var x=` -`+o[f].replace(" at new "," at ");return e.displayName&&x.includes("")&&(x=x.replace("",e.displayName)),x}while(1<=f&&0<=h);break}}}finally{ne=!1,Error.prepareStackTrace=s}return(e=e?e.displayName||e.name:"")?T(e):""}function ge(e){switch(e.tag){case 5:return T(e.type);case 16:return T("Lazy");case 13:return T("Suspense");case 19:return T("SuspenseList");case 0:case 2:case 15:return e=ce(e.type,!1),e;case 11:return e=ce(e.type.render,!1),e;case 1:return e=ce(e.type,!0),e;default:return""}}function we(e){if(e==null)return null;if(typeof e=="function")return e.displayName||e.name||null;if(typeof e=="string")return e;switch(e){case Z:return"Fragment";case pe:return"Portal";case ee:return"Profiler";case Re:return"StrictMode";case z:return"Suspense";case H:return"SuspenseList"}if(typeof e=="object")switch(e.$$typeof){case Se:return(e.displayName||"Context")+".Consumer";case fe:return(e._context.displayName||"Context")+".Provider";case me:var t=e.render;return e=e.displayName,e||(e=t.displayName||t.name||"",e=e!==""?"ForwardRef("+e+")":"ForwardRef"),e;case be:return t=e.displayName||null,t!==null?t:we(e.type)||"Memo";case $e:t=e._payload,e=e._init;try{return we(e(t))}catch{}}return null}function De(e){var t=e.type;switch(e.tag){case 24:return"Cache";case 9:return(t.displayName||"Context")+".Consumer";case 10:return(t._context.displayName||"Context")+".Provider";case 18:return"DehydratedFragment";case 11:return e=t.render,e=e.displayName||e.name||"",t.displayName||(e!==""?"ForwardRef("+e+")":"ForwardRef");case 7:return"Fragment";case 5:return t;case 4:return"Portal";case 3:return"Root";case 6:return"Text";case 16:return we(t);case 8:return t===Re?"StrictMode":"Mode";case 22:return"Offscreen";case 12:return"Profiler";case 21:return"Scope";case 13:return"Suspense";case 19:return"SuspenseList";case 25:return"TracingMarker";case 1:case 0:case 17:case 2:case 14:case 15:if(typeof t=="function")return t.displayName||t.name||null;if(typeof t=="string")return t}return null}function se(e){switch(typeof e){case"boolean":case"number":case"string":case"undefined":return e;case"object":return e;default:return""}}function Ie(e){var t=e.type;return(e=e.nodeName)&&e.toLowerCase()==="input"&&(t==="checkbox"||t==="radio")}function Qe(e){var t=Ie(e)?"checked":"value",s=Object.getOwnPropertyDescriptor(e.constructor.prototype,t),a=""+e[t];if(!e.hasOwnProperty(t)&&typeof s<"u"&&typeof s.get=="function"&&typeof s.set=="function"){var o=s.get,c=s.set;return Object.defineProperty(e,t,{configurable:!0,get:function(){return o.call(this)},set:function(f){a=""+f,c.call(this,f)}}),Object.defineProperty(e,t,{enumerable:s.enumerable}),{getValue:function(){return a},setValue:function(f){a=""+f},stopTracking:function(){e._valueTracker=null,delete e[t]}}}}function tt(e){e._valueTracker||(e._valueTracker=Qe(e))}function Dt(e){if(!e)return!1;var t=e._valueTracker;if(!t)return!0;var s=t.getValue(),a="";return e&&(a=Ie(e)?e.checked?"true":"false":e.value),e=a,e!==s?(t.setValue(e),!0):!1}function ut(e){if(e=e||(typeof document<"u"?document:void 0),typeof e>"u")return null;try{return e.activeElement||e.body}catch{return e.body}}function de(e,t){var s=t.checked;return _({},t,{defaultChecked:void 0,defaultValue:void 0,value:void 0,checked:s??e._wrapperState.initialChecked})}function ke(e,t){var s=t.defaultValue==null?"":t.defaultValue,a=t.checked!=null?t.checked:t.defaultChecked;s=se(t.value!=null?t.value:s),e._wrapperState={initialChecked:a,initialValue:s,controlled:t.type==="checkbox"||t.type==="radio"?t.checked!=null:t.value!=null}}function Y(e,t){t=t.checked,t!=null&&q(e,"checked",t,!1)}function je(e,t){Y(e,t);var s=se(t.value),a=t.type;if(s!=null)a==="number"?(s===0&&e.value===""||e.value!=s)&&(e.value=""+s):e.value!==""+s&&(e.value=""+s);else if(a==="submit"||a==="reset"){e.removeAttribute("value");return}t.hasOwnProperty("value")?at(e,t.type,s):t.hasOwnProperty("defaultValue")&&at(e,t.type,se(t.defaultValue)),t.checked==null&&t.defaultChecked!=null&&(e.defaultChecked=!!t.defaultChecked)}function rt(e,t,s){if(t.hasOwnProperty("value")||t.hasOwnProperty("defaultValue")){var a=t.type;if(!(a!=="submit"&&a!=="reset"||t.value!==void 0&&t.value!==null))return;t=""+e._wrapperState.initialValue,s||t===e.value||(e.value=t),e.defaultValue=t}s=e.name,s!==""&&(e.name=""),e.defaultChecked=!!e._wrapperState.initialChecked,s!==""&&(e.name=s)}function at(e,t,s){(t!=="number"||ut(e.ownerDocument)!==e)&&(s==null?e.defaultValue=""+e._wrapperState.initialValue:e.defaultValue!==""+s&&(e.defaultValue=""+s))}var yt=Array.isArray;function It(e,t,s,a){if(e=e.options,t){t={};for(var o=0;o"+t.valueOf().toString()+"",t=kt.firstChild;e.firstChild;)e.removeChild(e.firstChild);for(;t.firstChild;)e.appendChild(t.firstChild)}});function jt(e,t){if(t){var s=e.firstChild;if(s&&s===e.lastChild&&s.nodeType===3){s.nodeValue=t;return}}e.textContent=t}var Ae={animationIterationCount:!0,aspectRatio:!0,borderImageOutset:!0,borderImageSlice:!0,borderImageWidth:!0,boxFlex:!0,boxFlexGroup:!0,boxOrdinalGroup:!0,columnCount:!0,columns:!0,flex:!0,flexGrow:!0,flexPositive:!0,flexShrink:!0,flexNegative:!0,flexOrder:!0,gridArea:!0,gridRow:!0,gridRowEnd:!0,gridRowSpan:!0,gridRowStart:!0,gridColumn:!0,gridColumnEnd:!0,gridColumnSpan:!0,gridColumnStart:!0,fontWeight:!0,lineClamp:!0,lineHeight:!0,opacity:!0,order:!0,orphans:!0,tabSize:!0,widows:!0,zIndex:!0,zoom:!0,fillOpacity:!0,floodOpacity:!0,stopOpacity:!0,strokeDasharray:!0,strokeDashoffset:!0,strokeMiterlimit:!0,strokeOpacity:!0,strokeWidth:!0},hn=["Webkit","ms","Moz","O"];Object.keys(Ae).forEach(function(e){hn.forEach(function(t){t=t+e.charAt(0).toUpperCase()+e.substring(1),Ae[t]=Ae[e]})});function it(e,t,s){return t==null||typeof t=="boolean"||t===""?"":s||typeof t!="number"||t===0||Ae.hasOwnProperty(e)&&Ae[e]?(""+t).trim():t+"px"}function Xn(e,t){e=e.style;for(var s in t)if(t.hasOwnProperty(s)){var a=s.indexOf("--")===0,o=it(s,t[s],a);s==="float"&&(s="cssFloat"),a?e.setProperty(s,o):e[s]=o}}var Tt=_({menuitem:!0},{area:!0,base:!0,br:!0,col:!0,embed:!0,hr:!0,img:!0,input:!0,keygen:!0,link:!0,meta:!0,param:!0,source:!0,track:!0,wbr:!0});function an(e,t){if(t){if(Tt[e]&&(t.children!=null||t.dangerouslySetInnerHTML!=null))throw Error(i(137,e));if(t.dangerouslySetInnerHTML!=null){if(t.children!=null)throw Error(i(60));if(typeof t.dangerouslySetInnerHTML!="object"||!("__html"in t.dangerouslySetInnerHTML))throw Error(i(61))}if(t.style!=null&&typeof t.style!="object")throw Error(i(62))}}function dt(e,t){if(e.indexOf("-")===-1)return typeof t.is=="string";switch(e){case"annotation-xml":case"color-profile":case"font-face":case"font-face-src":case"font-face-uri":case"font-face-format":case"font-face-name":case"missing-glyph":return!1;default:return!0}}var $t=null;function ln(e){return e=e.target||e.srcElement||window,e.correspondingUseElement&&(e=e.correspondingUseElement),e.nodeType===3?e.parentNode:e}var In=null,ht=null,on=null;function gn(e){if(e=fr(e)){if(typeof In!="function")throw Error(i(280));var t=e.stateNode;t&&(t=Zr(t),In(e.stateNode,e.type,t))}}function hs(e){ht?on?on.push(e):on=[e]:ht=e}function gs(){if(ht){var e=ht,t=on;if(on=ht=null,gn(e),t)for(e=0;e>>=0,e===0?32:31-(md(e)/hd|0)|0}var Mr=64,Or=4194304;function Hs(e){switch(e&-e){case 1:return 1;case 2:return 2;case 4:return 4;case 8:return 8;case 16:return 16;case 32:return 32;case 64:case 128:case 256:case 512:case 1024:case 2048:case 4096:case 8192:case 16384:case 32768:case 65536:case 131072:case 262144:case 524288:case 1048576:case 2097152:return e&4194240;case 4194304:case 8388608:case 16777216:case 33554432:case 67108864:return e&130023424;case 134217728:return 134217728;case 268435456:return 268435456;case 536870912:return 536870912;case 1073741824:return 1073741824;default:return e}}function Ar(e,t){var s=e.pendingLanes;if(s===0)return 0;var a=0,o=e.suspendedLanes,c=e.pingedLanes,f=s&268435455;if(f!==0){var h=f&~o;h!==0?a=Hs(h):(c&=f,c!==0&&(a=Hs(c)))}else f=s&~o,f!==0?a=Hs(f):c!==0&&(a=Hs(c));if(a===0)return 0;if(t!==0&&t!==a&&(t&o)===0&&(o=a&-a,c=t&-t,o>=c||o===16&&(c&4194240)!==0))return t;if((a&4)!==0&&(a|=s&16),t=e.entangledLanes,t!==0)for(e=e.entanglements,t&=a;0s;s++)t.push(e);return t}function Ws(e,t,s){e.pendingLanes|=t,t!==536870912&&(e.suspendedLanes=0,e.pingedLanes=0),e=e.eventTimes,t=31-Xt(t),e[t]=s}function jd(e,t){var s=e.pendingLanes&~t;e.pendingLanes=t,e.suspendedLanes=0,e.pingedLanes=0,e.expiredLanes&=t,e.mutableReadLanes&=t,e.entangledLanes&=t,t=e.entanglements;var a=e.eventTimes;for(e=e.expirationTimes;0=sr),co=" ",uo=!1;function fo(e,t){switch(e){case"keyup":return Jd.indexOf(t.keyCode)!==-1;case"keydown":return t.keyCode!==229;case"keypress":case"mousedown":case"focusout":return!0;default:return!1}}function po(e){return e=e.detail,typeof e=="object"&&"data"in e?e.data:null}var Ss=!1;function Wd(e,t){switch(e){case"compositionend":return po(t);case"keypress":return t.which!==32?null:(uo=!0,co);case"textInput":return e=t.data,e===co&&uo?null:e;default:return null}}function Gd(e,t){if(Ss)return e==="compositionend"||!ll&&fo(e,t)?(e=so(),Qr=el=On=null,Ss=!1,e):null;switch(e){case"paste":return null;case"keypress":if(!(t.ctrlKey||t.altKey||t.metaKey)||t.ctrlKey&&t.altKey){if(t.char&&1=t)return{node:s,offset:t-e};e=a}e:{for(;s;){if(s.nextSibling){s=s.nextSibling;break e}s=s.parentNode}s=void 0}s=vo(s)}}function So(e,t){return e&&t?e===t?!0:e&&e.nodeType===3?!1:t&&t.nodeType===3?So(e,t.parentNode):"contains"in e?e.contains(t):e.compareDocumentPosition?!!(e.compareDocumentPosition(t)&16):!1:!1}function No(){for(var e=window,t=ut();t instanceof e.HTMLIFrameElement;){try{var s=typeof t.contentWindow.location.href=="string"}catch{s=!1}if(s)e=t.contentWindow;else break;t=ut(e.document)}return t}function cl(e){var t=e&&e.nodeName&&e.nodeName.toLowerCase();return t&&(t==="input"&&(e.type==="text"||e.type==="search"||e.type==="tel"||e.type==="url"||e.type==="password")||t==="textarea"||e.contentEditable==="true")}function af(e){var t=No(),s=e.focusedElem,a=e.selectionRange;if(t!==s&&s&&s.ownerDocument&&So(s.ownerDocument.documentElement,s)){if(a!==null&&cl(s)){if(t=a.start,e=a.end,e===void 0&&(e=t),"selectionStart"in s)s.selectionStart=t,s.selectionEnd=Math.min(e,s.value.length);else if(e=(t=s.ownerDocument||document)&&t.defaultView||window,e.getSelection){e=e.getSelection();var o=s.textContent.length,c=Math.min(a.start,o);a=a.end===void 0?c:Math.min(a.end,o),!e.extend&&c>a&&(o=a,a=c,c=o),o=bo(s,c);var f=bo(s,a);o&&f&&(e.rangeCount!==1||e.anchorNode!==o.node||e.anchorOffset!==o.offset||e.focusNode!==f.node||e.focusOffset!==f.offset)&&(t=t.createRange(),t.setStart(o.node,o.offset),e.removeAllRanges(),c>a?(e.addRange(t),e.extend(f.node,f.offset)):(t.setEnd(f.node,f.offset),e.addRange(t)))}}for(t=[],e=s;e=e.parentNode;)e.nodeType===1&&t.push({element:e,left:e.scrollLeft,top:e.scrollTop});for(typeof s.focus=="function"&&s.focus(),s=0;s=document.documentMode,Ns=null,ul=null,ir=null,dl=!1;function wo(e,t,s){var a=s.window===s?s.document:s.nodeType===9?s:s.ownerDocument;dl||Ns==null||Ns!==ut(a)||(a=Ns,"selectionStart"in a&&cl(a)?a={start:a.selectionStart,end:a.selectionEnd}:(a=(a.ownerDocument&&a.ownerDocument.defaultView||window).getSelection(),a={anchorNode:a.anchorNode,anchorOffset:a.anchorOffset,focusNode:a.focusNode,focusOffset:a.focusOffset}),ir&&lr(ir,a)||(ir=a,a=Gr(ul,"onSelect"),0Ds||(e.current=Nl[Ds],Nl[Ds]=null,Ds--)}function Ve(e,t){Ds++,Nl[Ds]=e.current,e.current=t}var Fn={},vt=Bn(Fn),Lt=Bn(!1),ts=Fn;function Is(e,t){var s=e.type.contextTypes;if(!s)return Fn;var a=e.stateNode;if(a&&a.__reactInternalMemoizedUnmaskedChildContext===t)return a.__reactInternalMemoizedMaskedChildContext;var o={},c;for(c in s)o[c]=t[c];return a&&(e=e.stateNode,e.__reactInternalMemoizedUnmaskedChildContext=t,e.__reactInternalMemoizedMaskedChildContext=o),o}function Pt(e){return e=e.childContextTypes,e!=null}function ea(){Je(Lt),Je(vt)}function Bo(e,t,s){if(vt.current!==Fn)throw Error(i(168));Ve(vt,t),Ve(Lt,s)}function Fo(e,t,s){var a=e.stateNode;if(t=t.childContextTypes,typeof a.getChildContext!="function")return s;a=a.getChildContext();for(var o in a)if(!(o in t))throw Error(i(108,De(e)||"Unknown",o));return _({},s,a)}function ta(e){return e=(e=e.stateNode)&&e.__reactInternalMemoizedMergedChildContext||Fn,ts=vt.current,Ve(vt,e),Ve(Lt,Lt.current),!0}function zo(e,t,s){var a=e.stateNode;if(!a)throw Error(i(169));s?(e=Fo(e,t,ts),a.__reactInternalMemoizedMergedChildContext=e,Je(Lt),Je(vt),Ve(vt,e)):Je(Lt),Ve(Lt,s)}var jn=null,na=!1,wl=!1;function Qo(e){jn===null?jn=[e]:jn.push(e)}function yf(e){na=!0,Qo(e)}function zn(){if(!wl&&jn!==null){wl=!0;var e=0,t=ze;try{var s=jn;for(ze=1;e>=f,o-=f,vn=1<<32-Xt(t)+o|s<ve?(mt=ye,ye=null):mt=ye.sibling;var Oe=M(k,ye,R[ve],Q);if(Oe===null){ye===null&&(ye=mt);break}e&&ye&&Oe.alternate===null&&t(k,ye),b=c(Oe,b,ve),xe===null?ie=Oe:xe.sibling=Oe,xe=Oe,ye=mt}if(ve===R.length)return s(k,ye),We&&ss(k,ve),ie;if(ye===null){for(;veve?(mt=ye,ye=null):mt=ye.sibling;var Gn=M(k,ye,Oe.value,Q);if(Gn===null){ye===null&&(ye=mt);break}e&&ye&&Gn.alternate===null&&t(k,ye),b=c(Gn,b,ve),xe===null?ie=Gn:xe.sibling=Gn,xe=Gn,ye=mt}if(Oe.done)return s(k,ye),We&&ss(k,ve),ie;if(ye===null){for(;!Oe.done;ve++,Oe=R.next())Oe=B(k,Oe.value,Q),Oe!==null&&(b=c(Oe,b,ve),xe===null?ie=Oe:xe.sibling=Oe,xe=Oe);return We&&ss(k,ve),ie}for(ye=a(k,ye);!Oe.done;ve++,Oe=R.next())Oe=G(ye,k,ve,Oe.value,Q),Oe!==null&&(e&&Oe.alternate!==null&&ye.delete(Oe.key===null?ve:Oe.key),b=c(Oe,b,ve),xe===null?ie=Oe:xe.sibling=Oe,xe=Oe);return e&&ye.forEach(function(Xf){return t(k,Xf)}),We&&ss(k,ve),ie}function st(k,b,R,Q){if(typeof R=="object"&&R!==null&&R.type===Z&&R.key===null&&(R=R.props.children),typeof R=="object"&&R!==null){switch(R.$$typeof){case oe:e:{for(var ie=R.key,xe=b;xe!==null;){if(xe.key===ie){if(ie=R.type,ie===Z){if(xe.tag===7){s(k,xe.sibling),b=o(xe,R.props.children),b.return=k,k=b;break e}}else if(xe.elementType===ie||typeof ie=="object"&&ie!==null&&ie.$$typeof===$e&&Ho(ie)===xe.type){s(k,xe.sibling),b=o(xe,R.props),b.ref=pr(k,xe,R),b.return=k,k=b;break e}s(k,xe);break}else t(k,xe);xe=xe.sibling}R.type===Z?(b=ds(R.props.children,k.mode,Q,R.key),b.return=k,k=b):(Q=Ea(R.type,R.key,R.props,null,k.mode,Q),Q.ref=pr(k,b,R),Q.return=k,k=Q)}return f(k);case pe:e:{for(xe=R.key;b!==null;){if(b.key===xe)if(b.tag===4&&b.stateNode.containerInfo===R.containerInfo&&b.stateNode.implementation===R.implementation){s(k,b.sibling),b=o(b,R.children||[]),b.return=k,k=b;break e}else{s(k,b);break}else t(k,b);b=b.sibling}b=bi(R,k.mode,Q),b.return=k,k=b}return f(k);case $e:return xe=R._init,st(k,b,xe(R._payload),Q)}if(yt(R))return te(k,b,R,Q);if(U(R))return re(k,b,R,Q);la(k,R)}return typeof R=="string"&&R!==""||typeof R=="number"?(R=""+R,b!==null&&b.tag===6?(s(k,b.sibling),b=o(b,R),b.return=k,k=b):(s(k,b),b=vi(R,k.mode,Q),b.return=k,k=b),f(k)):s(k,b)}return st}var Ls=Wo(!0),Go=Wo(!1),ia=Bn(null),oa=null,Ps=null,El=null;function Tl(){El=Ps=oa=null}function $l(e){var t=ia.current;Je(ia),e._currentValue=t}function Ll(e,t,s){for(;e!==null;){var a=e.alternate;if((e.childLanes&t)!==t?(e.childLanes|=t,a!==null&&(a.childLanes|=t)):a!==null&&(a.childLanes&t)!==t&&(a.childLanes|=t),e===s)break;e=e.return}}function Ms(e,t){oa=e,El=Ps=null,e=e.dependencies,e!==null&&e.firstContext!==null&&((e.lanes&t)!==0&&(Mt=!0),e.firstContext=null)}function Ht(e){var t=e._currentValue;if(El!==e)if(e={context:e,memoizedValue:t,next:null},Ps===null){if(oa===null)throw Error(i(308));Ps=e,oa.dependencies={lanes:0,firstContext:e}}else Ps=Ps.next=e;return t}var rs=null;function Pl(e){rs===null?rs=[e]:rs.push(e)}function Yo(e,t,s,a){var o=t.interleaved;return o===null?(s.next=s,Pl(t)):(s.next=o.next,o.next=s),t.interleaved=s,Sn(e,a)}function Sn(e,t){e.lanes|=t;var s=e.alternate;for(s!==null&&(s.lanes|=t),s=e,e=e.return;e!==null;)e.childLanes|=t,s=e.alternate,s!==null&&(s.childLanes|=t),s=e,e=e.return;return s.tag===3?s.stateNode:null}var Qn=!1;function Ml(e){e.updateQueue={baseState:e.memoizedState,firstBaseUpdate:null,lastBaseUpdate:null,shared:{pending:null,interleaved:null,lanes:0},effects:null}}function Xo(e,t){e=e.updateQueue,t.updateQueue===e&&(t.updateQueue={baseState:e.baseState,firstBaseUpdate:e.firstBaseUpdate,lastBaseUpdate:e.lastBaseUpdate,shared:e.shared,effects:e.effects})}function Nn(e,t){return{eventTime:e,lane:t,tag:0,payload:null,callback:null,next:null}}function Un(e,t,s){var a=e.updateQueue;if(a===null)return null;if(a=a.shared,(Me&2)!==0){var o=a.pending;return o===null?t.next=t:(t.next=o.next,o.next=t),a.pending=t,Sn(e,s)}return o=a.interleaved,o===null?(t.next=t,Pl(a)):(t.next=o.next,o.next=t),a.interleaved=t,Sn(e,s)}function ca(e,t,s){if(t=t.updateQueue,t!==null&&(t=t.shared,(s&4194240)!==0)){var a=t.lanes;a&=e.pendingLanes,s|=a,t.lanes=s,Wa(e,s)}}function Zo(e,t){var s=e.updateQueue,a=e.alternate;if(a!==null&&(a=a.updateQueue,s===a)){var o=null,c=null;if(s=s.firstBaseUpdate,s!==null){do{var f={eventTime:s.eventTime,lane:s.lane,tag:s.tag,payload:s.payload,callback:s.callback,next:null};c===null?o=c=f:c=c.next=f,s=s.next}while(s!==null);c===null?o=c=t:c=c.next=t}else o=c=t;s={baseState:a.baseState,firstBaseUpdate:o,lastBaseUpdate:c,shared:a.shared,effects:a.effects},e.updateQueue=s;return}e=s.lastBaseUpdate,e===null?s.firstBaseUpdate=t:e.next=t,s.lastBaseUpdate=t}function ua(e,t,s,a){var o=e.updateQueue;Qn=!1;var c=o.firstBaseUpdate,f=o.lastBaseUpdate,h=o.shared.pending;if(h!==null){o.shared.pending=null;var x=h,D=x.next;x.next=null,f===null?c=D:f.next=D,f=x;var O=e.alternate;O!==null&&(O=O.updateQueue,h=O.lastBaseUpdate,h!==f&&(h===null?O.firstBaseUpdate=D:h.next=D,O.lastBaseUpdate=x))}if(c!==null){var B=o.baseState;f=0,O=D=x=null,h=c;do{var M=h.lane,G=h.eventTime;if((a&M)===M){O!==null&&(O=O.next={eventTime:G,lane:0,tag:h.tag,payload:h.payload,callback:h.callback,next:null});e:{var te=e,re=h;switch(M=t,G=s,re.tag){case 1:if(te=re.payload,typeof te=="function"){B=te.call(G,B,M);break e}B=te;break e;case 3:te.flags=te.flags&-65537|128;case 0:if(te=re.payload,M=typeof te=="function"?te.call(G,B,M):te,M==null)break e;B=_({},B,M);break e;case 2:Qn=!0}}h.callback!==null&&h.lane!==0&&(e.flags|=64,M=o.effects,M===null?o.effects=[h]:M.push(h))}else G={eventTime:G,lane:M,tag:h.tag,payload:h.payload,callback:h.callback,next:null},O===null?(D=O=G,x=B):O=O.next=G,f|=M;if(h=h.next,h===null){if(h=o.shared.pending,h===null)break;M=h,h=M.next,M.next=null,o.lastBaseUpdate=M,o.shared.pending=null}}while(!0);if(O===null&&(x=B),o.baseState=x,o.firstBaseUpdate=D,o.lastBaseUpdate=O,t=o.shared.interleaved,t!==null){o=t;do f|=o.lane,o=o.next;while(o!==t)}else c===null&&(o.shared.lanes=0);is|=f,e.lanes=f,e.memoizedState=B}}function ec(e,t,s){if(e=t.effects,t.effects=null,e!==null)for(t=0;ts?s:4,e(!0);var a=Fl.transition;Fl.transition={};try{e(!1),t()}finally{ze=s,Fl.transition=a}}function jc(){return Wt().memoizedState}function Sf(e,t,s){var a=Jn(e);if(s={lane:a,action:s,hasEagerState:!1,eagerState:null,next:null},vc(e))bc(t,s);else if(s=Yo(e,t,s,a),s!==null){var o=Rt();rn(s,e,a,o),Sc(s,t,a)}}function Nf(e,t,s){var a=Jn(e),o={lane:a,action:s,hasEagerState:!1,eagerState:null,next:null};if(vc(e))bc(t,o);else{var c=e.alternate;if(e.lanes===0&&(c===null||c.lanes===0)&&(c=t.lastRenderedReducer,c!==null))try{var f=t.lastRenderedState,h=c(f,s);if(o.hasEagerState=!0,o.eagerState=h,Zt(h,f)){var x=t.interleaved;x===null?(o.next=o,Pl(t)):(o.next=x.next,x.next=o),t.interleaved=o;return}}catch{}finally{}s=Yo(e,t,o,a),s!==null&&(o=Rt(),rn(s,e,a,o),Sc(s,t,a))}}function vc(e){var t=e.alternate;return e===Xe||t!==null&&t===Xe}function bc(e,t){xr=pa=!0;var s=e.pending;s===null?t.next=t:(t.next=s.next,s.next=t),e.pending=t}function Sc(e,t,s){if((s&4194240)!==0){var a=t.lanes;a&=e.pendingLanes,s|=a,t.lanes=s,Wa(e,s)}}var ga={readContext:Ht,useCallback:bt,useContext:bt,useEffect:bt,useImperativeHandle:bt,useInsertionEffect:bt,useLayoutEffect:bt,useMemo:bt,useReducer:bt,useRef:bt,useState:bt,useDebugValue:bt,useDeferredValue:bt,useTransition:bt,useMutableSource:bt,useSyncExternalStore:bt,useId:bt,unstable_isNewReconciler:!1},wf={readContext:Ht,useCallback:function(e,t){return fn().memoizedState=[e,t===void 0?null:t],e},useContext:Ht,useEffect:dc,useImperativeHandle:function(e,t,s){return s=s!=null?s.concat([e]):null,ma(4194308,4,mc.bind(null,t,e),s)},useLayoutEffect:function(e,t){return ma(4194308,4,e,t)},useInsertionEffect:function(e,t){return ma(4,2,e,t)},useMemo:function(e,t){var s=fn();return t=t===void 0?null:t,e=e(),s.memoizedState=[e,t],e},useReducer:function(e,t,s){var a=fn();return t=s!==void 0?s(t):t,a.memoizedState=a.baseState=t,e={pending:null,interleaved:null,lanes:0,dispatch:null,lastRenderedReducer:e,lastRenderedState:t},a.queue=e,e=e.dispatch=Sf.bind(null,Xe,e),[a.memoizedState,e]},useRef:function(e){var t=fn();return e={current:e},t.memoizedState=e},useState:cc,useDebugValue:Jl,useDeferredValue:function(e){return fn().memoizedState=e},useTransition:function(){var e=cc(!1),t=e[0];return e=bf.bind(null,e[1]),fn().memoizedState=e,[t,e]},useMutableSource:function(){},useSyncExternalStore:function(e,t,s){var a=Xe,o=fn();if(We){if(s===void 0)throw Error(i(407));s=s()}else{if(s=t(),pt===null)throw Error(i(349));(ls&30)!==0||rc(a,t,s)}o.memoizedState=s;var c={value:s,getSnapshot:t};return o.queue=c,dc(lc.bind(null,a,c,e),[e]),a.flags|=2048,vr(9,ac.bind(null,a,c,s,t),void 0,null),s},useId:function(){var e=fn(),t=pt.identifierPrefix;if(We){var s=bn,a=vn;s=(a&~(1<<32-Xt(a)-1)).toString(32)+s,t=":"+t+"R"+s,s=yr++,0h||o[f]!==c[h]){var y=` +`+o[f].replace(" at new "," at ");return e.displayName&&y.includes("")&&(y=y.replace("",e.displayName)),y}while(1<=f&&0<=h);break}}}finally{se=!1,Error.prepareStackTrace=s}return(e=e?e.displayName||e.name:"")?T(e):""}function xe(e){switch(e.tag){case 5:return T(e.type);case 16:return T("Lazy");case 13:return T("Suspense");case 19:return T("SuspenseList");case 0:case 2:case 15:return e=ce(e.type,!1),e;case 11:return e=ce(e.type.render,!1),e;case 1:return e=ce(e.type,!0),e;default:return""}}function we(e){if(e==null)return null;if(typeof e=="function")return e.displayName||e.name||null;if(typeof e=="string")return e;switch(e){case Z:return"Fragment";case me:return"Portal";case ee:return"Profiler";case Re:return"StrictMode";case z:return"Suspense";case H:return"SuspenseList"}if(typeof e=="object")switch(e.$$typeof){case Se:return(e.displayName||"Context")+".Consumer";case pe:return(e._context.displayName||"Context")+".Provider";case he:var t=e.render;return e=e.displayName,e||(e=t.displayName||t.name||"",e=e!==""?"ForwardRef("+e+")":"ForwardRef"),e;case be:return t=e.displayName||null,t!==null?t:we(e.type)||"Memo";case Pe:t=e._payload,e=e._init;try{return we(e(t))}catch{}}return null}function De(e){var t=e.type;switch(e.tag){case 24:return"Cache";case 9:return(t.displayName||"Context")+".Consumer";case 10:return(t._context.displayName||"Context")+".Provider";case 18:return"DehydratedFragment";case 11:return e=t.render,e=e.displayName||e.name||"",t.displayName||(e!==""?"ForwardRef("+e+")":"ForwardRef");case 7:return"Fragment";case 5:return t;case 4:return"Portal";case 3:return"Root";case 6:return"Text";case 16:return we(t);case 8:return t===Re?"StrictMode":"Mode";case 22:return"Offscreen";case 12:return"Profiler";case 21:return"Scope";case 13:return"Suspense";case 19:return"SuspenseList";case 25:return"TracingMarker";case 1:case 0:case 17:case 2:case 14:case 15:if(typeof t=="function")return t.displayName||t.name||null;if(typeof t=="string")return t}return null}function re(e){switch(typeof e){case"boolean":case"number":case"string":case"undefined":return e;case"object":return e;default:return""}}function Ie(e){var t=e.type;return(e=e.nodeName)&&e.toLowerCase()==="input"&&(t==="checkbox"||t==="radio")}function Qe(e){var t=Ie(e)?"checked":"value",s=Object.getOwnPropertyDescriptor(e.constructor.prototype,t),a=""+e[t];if(!e.hasOwnProperty(t)&&typeof s<"u"&&typeof s.get=="function"&&typeof s.set=="function"){var o=s.get,c=s.set;return Object.defineProperty(e,t,{configurable:!0,get:function(){return o.call(this)},set:function(f){a=""+f,c.call(this,f)}}),Object.defineProperty(e,t,{enumerable:s.enumerable}),{getValue:function(){return a},setValue:function(f){a=""+f},stopTracking:function(){e._valueTracker=null,delete e[t]}}}}function tt(e){e._valueTracker||(e._valueTracker=Qe(e))}function Dt(e){if(!e)return!1;var t=e._valueTracker;if(!t)return!0;var s=t.getValue(),a="";return e&&(a=Ie(e)?e.checked?"true":"false":e.value),e=a,e!==s?(t.setValue(e),!0):!1}function ut(e){if(e=e||(typeof document<"u"?document:void 0),typeof e>"u")return null;try{return e.activeElement||e.body}catch{return e.body}}function fe(e,t){var s=t.checked;return B({},t,{defaultChecked:void 0,defaultValue:void 0,value:void 0,checked:s??e._wrapperState.initialChecked})}function ke(e,t){var s=t.defaultValue==null?"":t.defaultValue,a=t.checked!=null?t.checked:t.defaultChecked;s=re(t.value!=null?t.value:s),e._wrapperState={initialChecked:a,initialValue:s,controlled:t.type==="checkbox"||t.type==="radio"?t.checked!=null:t.value!=null}}function Y(e,t){t=t.checked,t!=null&&V(e,"checked",t,!1)}function je(e,t){Y(e,t);var s=re(t.value),a=t.type;if(s!=null)a==="number"?(s===0&&e.value===""||e.value!=s)&&(e.value=""+s):e.value!==""+s&&(e.value=""+s);else if(a==="submit"||a==="reset"){e.removeAttribute("value");return}t.hasOwnProperty("value")?at(e,t.type,s):t.hasOwnProperty("defaultValue")&&at(e,t.type,re(t.defaultValue)),t.checked==null&&t.defaultChecked!=null&&(e.defaultChecked=!!t.defaultChecked)}function rt(e,t,s){if(t.hasOwnProperty("value")||t.hasOwnProperty("defaultValue")){var a=t.type;if(!(a!=="submit"&&a!=="reset"||t.value!==void 0&&t.value!==null))return;t=""+e._wrapperState.initialValue,s||t===e.value||(e.value=t),e.defaultValue=t}s=e.name,s!==""&&(e.name=""),e.defaultChecked=!!e._wrapperState.initialChecked,s!==""&&(e.name=s)}function at(e,t,s){(t!=="number"||ut(e.ownerDocument)!==e)&&(s==null?e.defaultValue=""+e._wrapperState.initialValue:e.defaultValue!==""+s&&(e.defaultValue=""+s))}var jt=Array.isArray;function It(e,t,s,a){if(e=e.options,t){t={};for(var o=0;o"+t.valueOf().toString()+"",t=kt.firstChild;e.firstChild;)e.removeChild(e.firstChild);for(;t.firstChild;)e.appendChild(t.firstChild)}});function vt(e,t){if(t){var s=e.firstChild;if(s&&s===e.lastChild&&s.nodeType===3){s.nodeValue=t;return}}e.textContent=t}var Ae={animationIterationCount:!0,aspectRatio:!0,borderImageOutset:!0,borderImageSlice:!0,borderImageWidth:!0,boxFlex:!0,boxFlexGroup:!0,boxOrdinalGroup:!0,columnCount:!0,columns:!0,flex:!0,flexGrow:!0,flexPositive:!0,flexShrink:!0,flexNegative:!0,flexOrder:!0,gridArea:!0,gridRow:!0,gridRowEnd:!0,gridRowSpan:!0,gridRowStart:!0,gridColumn:!0,gridColumnEnd:!0,gridColumnSpan:!0,gridColumnStart:!0,fontWeight:!0,lineClamp:!0,lineHeight:!0,opacity:!0,order:!0,orphans:!0,tabSize:!0,widows:!0,zIndex:!0,zoom:!0,fillOpacity:!0,floodOpacity:!0,stopOpacity:!0,strokeDasharray:!0,strokeDashoffset:!0,strokeMiterlimit:!0,strokeOpacity:!0,strokeWidth:!0},hn=["Webkit","ms","Moz","O"];Object.keys(Ae).forEach(function(e){hn.forEach(function(t){t=t+e.charAt(0).toUpperCase()+e.substring(1),Ae[t]=Ae[e]})});function lt(e,t,s){return t==null||typeof t=="boolean"||t===""?"":s||typeof t!="number"||t===0||Ae.hasOwnProperty(e)&&Ae[e]?(""+t).trim():t+"px"}function Xn(e,t){e=e.style;for(var s in t)if(t.hasOwnProperty(s)){var a=s.indexOf("--")===0,o=lt(s,t[s],a);s==="float"&&(s="cssFloat"),a?e.setProperty(s,o):e[s]=o}}var Tt=B({menuitem:!0},{area:!0,base:!0,br:!0,col:!0,embed:!0,hr:!0,img:!0,input:!0,keygen:!0,link:!0,meta:!0,param:!0,source:!0,track:!0,wbr:!0});function an(e,t){if(t){if(Tt[e]&&(t.children!=null||t.dangerouslySetInnerHTML!=null))throw Error(l(137,e));if(t.dangerouslySetInnerHTML!=null){if(t.children!=null)throw Error(l(60));if(typeof t.dangerouslySetInnerHTML!="object"||!("__html"in t.dangerouslySetInnerHTML))throw Error(l(61))}if(t.style!=null&&typeof t.style!="object")throw Error(l(62))}}function dt(e,t){if(e.indexOf("-")===-1)return typeof t.is=="string";switch(e){case"annotation-xml":case"color-profile":case"font-face":case"font-face-src":case"font-face-uri":case"font-face-format":case"font-face-name":case"missing-glyph":return!1;default:return!0}}var Pt=null;function ln(e){return e=e.target||e.srcElement||window,e.correspondingUseElement&&(e=e.correspondingUseElement),e.nodeType===3?e.parentNode:e}var In=null,xt=null,on=null;function xn(e){if(e=fr(e)){if(typeof In!="function")throw Error(l(280));var t=e.stateNode;t&&(t=Zr(t),In(e.stateNode,e.type,t))}}function hs(e){xt?on?on.push(e):on=[e]:xt=e}function xs(){if(xt){var e=xt,t=on;if(on=xt=null,xn(e),t)for(e=0;e>>=0,e===0?32:31-(md(e)/hd|0)|0}var Mr=64,Or=4194304;function Hs(e){switch(e&-e){case 1:return 1;case 2:return 2;case 4:return 4;case 8:return 8;case 16:return 16;case 32:return 32;case 64:case 128:case 256:case 512:case 1024:case 2048:case 4096:case 8192:case 16384:case 32768:case 65536:case 131072:case 262144:case 524288:case 1048576:case 2097152:return e&4194240;case 4194304:case 8388608:case 16777216:case 33554432:case 67108864:return e&130023424;case 134217728:return 134217728;case 268435456:return 268435456;case 536870912:return 536870912;case 1073741824:return 1073741824;default:return e}}function Ar(e,t){var s=e.pendingLanes;if(s===0)return 0;var a=0,o=e.suspendedLanes,c=e.pingedLanes,f=s&268435455;if(f!==0){var h=f&~o;h!==0?a=Hs(h):(c&=f,c!==0&&(a=Hs(c)))}else f=s&~o,f!==0?a=Hs(f):c!==0&&(a=Hs(c));if(a===0)return 0;if(t!==0&&t!==a&&(t&o)===0&&(o=a&-a,c=t&-t,o>=c||o===16&&(c&4194240)!==0))return t;if((a&4)!==0&&(a|=s&16),t=e.entangledLanes,t!==0)for(e=e.entanglements,t&=a;0s;s++)t.push(e);return t}function Ws(e,t,s){e.pendingLanes|=t,t!==536870912&&(e.suspendedLanes=0,e.pingedLanes=0),e=e.eventTimes,t=31-Xt(t),e[t]=s}function jd(e,t){var s=e.pendingLanes&~t;e.pendingLanes=t,e.suspendedLanes=0,e.pingedLanes=0,e.expiredLanes&=t,e.mutableReadLanes&=t,e.entangledLanes&=t,t=e.entanglements;var a=e.eventTimes;for(e=e.expirationTimes;0=sr),co=" ",uo=!1;function fo(e,t){switch(e){case"keyup":return Jd.indexOf(t.keyCode)!==-1;case"keydown":return t.keyCode!==229;case"keypress":case"mousedown":case"focusout":return!0;default:return!1}}function po(e){return e=e.detail,typeof e=="object"&&"data"in e?e.data:null}var Ss=!1;function Wd(e,t){switch(e){case"compositionend":return po(t);case"keypress":return t.which!==32?null:(uo=!0,co);case"textInput":return e=t.data,e===co&&uo?null:e;default:return null}}function Gd(e,t){if(Ss)return e==="compositionend"||!ii&&fo(e,t)?(e=so(),Qr=ei=On=null,Ss=!1,e):null;switch(e){case"paste":return null;case"keypress":if(!(t.ctrlKey||t.altKey||t.metaKey)||t.ctrlKey&&t.altKey){if(t.char&&1=t)return{node:s,offset:t-e};e=a}e:{for(;s;){if(s.nextSibling){s=s.nextSibling;break e}s=s.parentNode}s=void 0}s=vo(s)}}function So(e,t){return e&&t?e===t?!0:e&&e.nodeType===3?!1:t&&t.nodeType===3?So(e,t.parentNode):"contains"in e?e.contains(t):e.compareDocumentPosition?!!(e.compareDocumentPosition(t)&16):!1:!1}function No(){for(var e=window,t=ut();t instanceof e.HTMLIFrameElement;){try{var s=typeof t.contentWindow.location.href=="string"}catch{s=!1}if(s)e=t.contentWindow;else break;t=ut(e.document)}return t}function ci(e){var t=e&&e.nodeName&&e.nodeName.toLowerCase();return t&&(t==="input"&&(e.type==="text"||e.type==="search"||e.type==="tel"||e.type==="url"||e.type==="password")||t==="textarea"||e.contentEditable==="true")}function af(e){var t=No(),s=e.focusedElem,a=e.selectionRange;if(t!==s&&s&&s.ownerDocument&&So(s.ownerDocument.documentElement,s)){if(a!==null&&ci(s)){if(t=a.start,e=a.end,e===void 0&&(e=t),"selectionStart"in s)s.selectionStart=t,s.selectionEnd=Math.min(e,s.value.length);else if(e=(t=s.ownerDocument||document)&&t.defaultView||window,e.getSelection){e=e.getSelection();var o=s.textContent.length,c=Math.min(a.start,o);a=a.end===void 0?c:Math.min(a.end,o),!e.extend&&c>a&&(o=a,a=c,c=o),o=bo(s,c);var f=bo(s,a);o&&f&&(e.rangeCount!==1||e.anchorNode!==o.node||e.anchorOffset!==o.offset||e.focusNode!==f.node||e.focusOffset!==f.offset)&&(t=t.createRange(),t.setStart(o.node,o.offset),e.removeAllRanges(),c>a?(e.addRange(t),e.extend(f.node,f.offset)):(t.setEnd(f.node,f.offset),e.addRange(t)))}}for(t=[],e=s;e=e.parentNode;)e.nodeType===1&&t.push({element:e,left:e.scrollLeft,top:e.scrollTop});for(typeof s.focus=="function"&&s.focus(),s=0;s=document.documentMode,Ns=null,ui=null,lr=null,di=!1;function wo(e,t,s){var a=s.window===s?s.document:s.nodeType===9?s:s.ownerDocument;di||Ns==null||Ns!==ut(a)||(a=Ns,"selectionStart"in a&&ci(a)?a={start:a.selectionStart,end:a.selectionEnd}:(a=(a.ownerDocument&&a.ownerDocument.defaultView||window).getSelection(),a={anchorNode:a.anchorNode,anchorOffset:a.anchorOffset,focusNode:a.focusNode,focusOffset:a.focusOffset}),lr&&ir(lr,a)||(lr=a,a=Gr(ui,"onSelect"),0Ds||(e.current=Ni[Ds],Ni[Ds]=null,Ds--)}function Ve(e,t){Ds++,Ni[Ds]=e.current,e.current=t}var Fn={},bt=Bn(Fn),Lt=Bn(!1),ts=Fn;function Is(e,t){var s=e.type.contextTypes;if(!s)return Fn;var a=e.stateNode;if(a&&a.__reactInternalMemoizedUnmaskedChildContext===t)return a.__reactInternalMemoizedMaskedChildContext;var o={},c;for(c in s)o[c]=t[c];return a&&(e=e.stateNode,e.__reactInternalMemoizedUnmaskedChildContext=t,e.__reactInternalMemoizedMaskedChildContext=o),o}function $t(e){return e=e.childContextTypes,e!=null}function ea(){Je(Lt),Je(bt)}function Bo(e,t,s){if(bt.current!==Fn)throw Error(l(168));Ve(bt,t),Ve(Lt,s)}function Fo(e,t,s){var a=e.stateNode;if(t=t.childContextTypes,typeof a.getChildContext!="function")return s;a=a.getChildContext();for(var o in a)if(!(o in t))throw Error(l(108,De(e)||"Unknown",o));return B({},s,a)}function ta(e){return e=(e=e.stateNode)&&e.__reactInternalMemoizedMergedChildContext||Fn,ts=bt.current,Ve(bt,e),Ve(Lt,Lt.current),!0}function zo(e,t,s){var a=e.stateNode;if(!a)throw Error(l(169));s?(e=Fo(e,t,ts),a.__reactInternalMemoizedMergedChildContext=e,Je(Lt),Je(bt),Ve(bt,e)):Je(Lt),Ve(Lt,s)}var jn=null,na=!1,wi=!1;function Qo(e){jn===null?jn=[e]:jn.push(e)}function yf(e){na=!0,Qo(e)}function zn(){if(!wi&&jn!==null){wi=!0;var e=0,t=ze;try{var s=jn;for(ze=1;e>=f,o-=f,vn=1<<32-Xt(t)+o|s<ve?(mt=ye,ye=null):mt=ye.sibling;var Oe=M(k,ye,R[ve],Q);if(Oe===null){ye===null&&(ye=mt);break}e&&ye&&Oe.alternate===null&&t(k,ye),b=c(Oe,b,ve),ge===null?le=Oe:ge.sibling=Oe,ge=Oe,ye=mt}if(ve===R.length)return s(k,ye),We&&ss(k,ve),le;if(ye===null){for(;veve?(mt=ye,ye=null):mt=ye.sibling;var Gn=M(k,ye,Oe.value,Q);if(Gn===null){ye===null&&(ye=mt);break}e&&ye&&Gn.alternate===null&&t(k,ye),b=c(Gn,b,ve),ge===null?le=Gn:ge.sibling=Gn,ge=Gn,ye=mt}if(Oe.done)return s(k,ye),We&&ss(k,ve),le;if(ye===null){for(;!Oe.done;ve++,Oe=R.next())Oe=F(k,Oe.value,Q),Oe!==null&&(b=c(Oe,b,ve),ge===null?le=Oe:ge.sibling=Oe,ge=Oe);return We&&ss(k,ve),le}for(ye=a(k,ye);!Oe.done;ve++,Oe=R.next())Oe=G(ye,k,ve,Oe.value,Q),Oe!==null&&(e&&Oe.alternate!==null&&ye.delete(Oe.key===null?ve:Oe.key),b=c(Oe,b,ve),ge===null?le=Oe:ge.sibling=Oe,ge=Oe);return e&&ye.forEach(function(Xf){return t(k,Xf)}),We&&ss(k,ve),le}function st(k,b,R,Q){if(typeof R=="object"&&R!==null&&R.type===Z&&R.key===null&&(R=R.props.children),typeof R=="object"&&R!==null){switch(R.$$typeof){case oe:e:{for(var le=R.key,ge=b;ge!==null;){if(ge.key===le){if(le=R.type,le===Z){if(ge.tag===7){s(k,ge.sibling),b=o(ge,R.props.children),b.return=k,k=b;break e}}else if(ge.elementType===le||typeof le=="object"&&le!==null&&le.$$typeof===Pe&&Ho(le)===ge.type){s(k,ge.sibling),b=o(ge,R.props),b.ref=pr(k,ge,R),b.return=k,k=b;break e}s(k,ge);break}else t(k,ge);ge=ge.sibling}R.type===Z?(b=ds(R.props.children,k.mode,Q,R.key),b.return=k,k=b):(Q=Ea(R.type,R.key,R.props,null,k.mode,Q),Q.ref=pr(k,b,R),Q.return=k,k=Q)}return f(k);case me:e:{for(ge=R.key;b!==null;){if(b.key===ge)if(b.tag===4&&b.stateNode.containerInfo===R.containerInfo&&b.stateNode.implementation===R.implementation){s(k,b.sibling),b=o(b,R.children||[]),b.return=k,k=b;break e}else{s(k,b);break}else t(k,b);b=b.sibling}b=bl(R,k.mode,Q),b.return=k,k=b}return f(k);case Pe:return ge=R._init,st(k,b,ge(R._payload),Q)}if(jt(R))return te(k,b,R,Q);if(U(R))return ae(k,b,R,Q);ia(k,R)}return typeof R=="string"&&R!==""||typeof R=="number"?(R=""+R,b!==null&&b.tag===6?(s(k,b.sibling),b=o(b,R),b.return=k,k=b):(s(k,b),b=vl(R,k.mode,Q),b.return=k,k=b),f(k)):s(k,b)}return st}var Ls=Wo(!0),Go=Wo(!1),la=Bn(null),oa=null,$s=null,Ei=null;function Ti(){Ei=$s=oa=null}function Pi(e){var t=la.current;Je(la),e._currentValue=t}function Li(e,t,s){for(;e!==null;){var a=e.alternate;if((e.childLanes&t)!==t?(e.childLanes|=t,a!==null&&(a.childLanes|=t)):a!==null&&(a.childLanes&t)!==t&&(a.childLanes|=t),e===s)break;e=e.return}}function Ms(e,t){oa=e,Ei=$s=null,e=e.dependencies,e!==null&&e.firstContext!==null&&((e.lanes&t)!==0&&(Mt=!0),e.firstContext=null)}function Ht(e){var t=e._currentValue;if(Ei!==e)if(e={context:e,memoizedValue:t,next:null},$s===null){if(oa===null)throw Error(l(308));$s=e,oa.dependencies={lanes:0,firstContext:e}}else $s=$s.next=e;return t}var rs=null;function $i(e){rs===null?rs=[e]:rs.push(e)}function Yo(e,t,s,a){var o=t.interleaved;return o===null?(s.next=s,$i(t)):(s.next=o.next,o.next=s),t.interleaved=s,Sn(e,a)}function Sn(e,t){e.lanes|=t;var s=e.alternate;for(s!==null&&(s.lanes|=t),s=e,e=e.return;e!==null;)e.childLanes|=t,s=e.alternate,s!==null&&(s.childLanes|=t),s=e,e=e.return;return s.tag===3?s.stateNode:null}var Qn=!1;function Mi(e){e.updateQueue={baseState:e.memoizedState,firstBaseUpdate:null,lastBaseUpdate:null,shared:{pending:null,interleaved:null,lanes:0},effects:null}}function Xo(e,t){e=e.updateQueue,t.updateQueue===e&&(t.updateQueue={baseState:e.baseState,firstBaseUpdate:e.firstBaseUpdate,lastBaseUpdate:e.lastBaseUpdate,shared:e.shared,effects:e.effects})}function Nn(e,t){return{eventTime:e,lane:t,tag:0,payload:null,callback:null,next:null}}function Un(e,t,s){var a=e.updateQueue;if(a===null)return null;if(a=a.shared,(Me&2)!==0){var o=a.pending;return o===null?t.next=t:(t.next=o.next,o.next=t),a.pending=t,Sn(e,s)}return o=a.interleaved,o===null?(t.next=t,$i(a)):(t.next=o.next,o.next=t),a.interleaved=t,Sn(e,s)}function ca(e,t,s){if(t=t.updateQueue,t!==null&&(t=t.shared,(s&4194240)!==0)){var a=t.lanes;a&=e.pendingLanes,s|=a,t.lanes=s,Wa(e,s)}}function Zo(e,t){var s=e.updateQueue,a=e.alternate;if(a!==null&&(a=a.updateQueue,s===a)){var o=null,c=null;if(s=s.firstBaseUpdate,s!==null){do{var f={eventTime:s.eventTime,lane:s.lane,tag:s.tag,payload:s.payload,callback:s.callback,next:null};c===null?o=c=f:c=c.next=f,s=s.next}while(s!==null);c===null?o=c=t:c=c.next=t}else o=c=t;s={baseState:a.baseState,firstBaseUpdate:o,lastBaseUpdate:c,shared:a.shared,effects:a.effects},e.updateQueue=s;return}e=s.lastBaseUpdate,e===null?s.firstBaseUpdate=t:e.next=t,s.lastBaseUpdate=t}function ua(e,t,s,a){var o=e.updateQueue;Qn=!1;var c=o.firstBaseUpdate,f=o.lastBaseUpdate,h=o.shared.pending;if(h!==null){o.shared.pending=null;var y=h,D=y.next;y.next=null,f===null?c=D:f.next=D,f=y;var O=e.alternate;O!==null&&(O=O.updateQueue,h=O.lastBaseUpdate,h!==f&&(h===null?O.firstBaseUpdate=D:h.next=D,O.lastBaseUpdate=y))}if(c!==null){var F=o.baseState;f=0,O=D=y=null,h=c;do{var M=h.lane,G=h.eventTime;if((a&M)===M){O!==null&&(O=O.next={eventTime:G,lane:0,tag:h.tag,payload:h.payload,callback:h.callback,next:null});e:{var te=e,ae=h;switch(M=t,G=s,ae.tag){case 1:if(te=ae.payload,typeof te=="function"){F=te.call(G,F,M);break e}F=te;break e;case 3:te.flags=te.flags&-65537|128;case 0:if(te=ae.payload,M=typeof te=="function"?te.call(G,F,M):te,M==null)break e;F=B({},F,M);break e;case 2:Qn=!0}}h.callback!==null&&h.lane!==0&&(e.flags|=64,M=o.effects,M===null?o.effects=[h]:M.push(h))}else G={eventTime:G,lane:M,tag:h.tag,payload:h.payload,callback:h.callback,next:null},O===null?(D=O=G,y=F):O=O.next=G,f|=M;if(h=h.next,h===null){if(h=o.shared.pending,h===null)break;M=h,h=M.next,M.next=null,o.lastBaseUpdate=M,o.shared.pending=null}}while(!0);if(O===null&&(y=F),o.baseState=y,o.firstBaseUpdate=D,o.lastBaseUpdate=O,t=o.shared.interleaved,t!==null){o=t;do f|=o.lane,o=o.next;while(o!==t)}else c===null&&(o.shared.lanes=0);ls|=f,e.lanes=f,e.memoizedState=F}}function ec(e,t,s){if(e=t.effects,t.effects=null,e!==null)for(t=0;ts?s:4,e(!0);var a=Fi.transition;Fi.transition={};try{e(!1),t()}finally{ze=s,Fi.transition=a}}function jc(){return Wt().memoizedState}function Sf(e,t,s){var a=Jn(e);if(s={lane:a,action:s,hasEagerState:!1,eagerState:null,next:null},vc(e))bc(t,s);else if(s=Yo(e,t,s,a),s!==null){var o=Rt();rn(s,e,a,o),Sc(s,t,a)}}function Nf(e,t,s){var a=Jn(e),o={lane:a,action:s,hasEagerState:!1,eagerState:null,next:null};if(vc(e))bc(t,o);else{var c=e.alternate;if(e.lanes===0&&(c===null||c.lanes===0)&&(c=t.lastRenderedReducer,c!==null))try{var f=t.lastRenderedState,h=c(f,s);if(o.hasEagerState=!0,o.eagerState=h,Zt(h,f)){var y=t.interleaved;y===null?(o.next=o,$i(t)):(o.next=y.next,y.next=o),t.interleaved=o;return}}catch{}finally{}s=Yo(e,t,o,a),s!==null&&(o=Rt(),rn(s,e,a,o),Sc(s,t,a))}}function vc(e){var t=e.alternate;return e===Xe||t!==null&&t===Xe}function bc(e,t){gr=pa=!0;var s=e.pending;s===null?t.next=t:(t.next=s.next,s.next=t),e.pending=t}function Sc(e,t,s){if((s&4194240)!==0){var a=t.lanes;a&=e.pendingLanes,s|=a,t.lanes=s,Wa(e,s)}}var xa={readContext:Ht,useCallback:St,useContext:St,useEffect:St,useImperativeHandle:St,useInsertionEffect:St,useLayoutEffect:St,useMemo:St,useReducer:St,useRef:St,useState:St,useDebugValue:St,useDeferredValue:St,useTransition:St,useMutableSource:St,useSyncExternalStore:St,useId:St,unstable_isNewReconciler:!1},wf={readContext:Ht,useCallback:function(e,t){return fn().memoizedState=[e,t===void 0?null:t],e},useContext:Ht,useEffect:dc,useImperativeHandle:function(e,t,s){return s=s!=null?s.concat([e]):null,ma(4194308,4,mc.bind(null,t,e),s)},useLayoutEffect:function(e,t){return ma(4194308,4,e,t)},useInsertionEffect:function(e,t){return ma(4,2,e,t)},useMemo:function(e,t){var s=fn();return t=t===void 0?null:t,e=e(),s.memoizedState=[e,t],e},useReducer:function(e,t,s){var a=fn();return t=s!==void 0?s(t):t,a.memoizedState=a.baseState=t,e={pending:null,interleaved:null,lanes:0,dispatch:null,lastRenderedReducer:e,lastRenderedState:t},a.queue=e,e=e.dispatch=Sf.bind(null,Xe,e),[a.memoizedState,e]},useRef:function(e){var t=fn();return e={current:e},t.memoizedState=e},useState:cc,useDebugValue:Ji,useDeferredValue:function(e){return fn().memoizedState=e},useTransition:function(){var e=cc(!1),t=e[0];return e=bf.bind(null,e[1]),fn().memoizedState=e,[t,e]},useMutableSource:function(){},useSyncExternalStore:function(e,t,s){var a=Xe,o=fn();if(We){if(s===void 0)throw Error(l(407));s=s()}else{if(s=t(),pt===null)throw Error(l(349));(is&30)!==0||rc(a,t,s)}o.memoizedState=s;var c={value:s,getSnapshot:t};return o.queue=c,dc(ic.bind(null,a,c,e),[e]),a.flags|=2048,vr(9,ac.bind(null,a,c,s,t),void 0,null),s},useId:function(){var e=fn(),t=pt.identifierPrefix;if(We){var s=bn,a=vn;s=(a&~(1<<32-Xt(a)-1)).toString(32)+s,t=":"+t+"R"+s,s=yr++,0<\/script>",e=e.removeChild(e.firstChild)):typeof a.is=="string"?e=f.createElement(s,{is:a.is}):(e=f.createElement(s),s==="select"&&(f=e,a.multiple?f.multiple=!0:a.size&&(f.size=a.size))):e=f.createElementNS(e,s),e[un]=t,e[dr]=a,Qc(e,t,!1,!1),t.stateNode=e;e:{switch(f=dt(s,a),s){case"dialog":Ke("cancel",e),Ke("close",e),o=a;break;case"iframe":case"object":case"embed":Ke("load",e),o=a;break;case"video":case"audio":for(o=0;oFs&&(t.flags|=128,a=!0,br(c,!1),t.lanes=4194304)}else{if(!a)if(e=da(f),e!==null){if(t.flags|=128,a=!0,s=e.updateQueue,s!==null&&(t.updateQueue=s,t.flags|=4),br(c,!0),c.tail===null&&c.tailMode==="hidden"&&!f.alternate&&!We)return St(t),null}else 2*nt()-c.renderingStartTime>Fs&&s!==1073741824&&(t.flags|=128,a=!0,br(c,!1),t.lanes=4194304);c.isBackwards?(f.sibling=t.child,t.child=f):(s=c.last,s!==null?s.sibling=f:t.child=f,c.last=f)}return c.tail!==null?(t=c.tail,c.rendering=t,c.tail=t.sibling,c.renderingStartTime=nt(),t.sibling=null,s=Ye.current,Ve(Ye,a?s&1|2:s&1),t):(St(t),null);case 22:case 23:return xi(),a=t.memoizedState!==null,e!==null&&e.memoizedState!==null!==a&&(t.flags|=8192),a&&(t.mode&1)!==0?(qt&1073741824)!==0&&(St(t),t.subtreeFlags&6&&(t.flags|=8192)):St(t),null;case 24:return null;case 25:return null}throw Error(i(156,t.tag))}function $f(e,t){switch(Cl(t),t.tag){case 1:return Pt(t.type)&&ea(),e=t.flags,e&65536?(t.flags=e&-65537|128,t):null;case 3:return Os(),Je(Lt),Je(vt),Bl(),e=t.flags,(e&65536)!==0&&(e&128)===0?(t.flags=e&-65537|128,t):null;case 5:return Al(t),null;case 13:if(Je(Ye),e=t.memoizedState,e!==null&&e.dehydrated!==null){if(t.alternate===null)throw Error(i(340));$s()}return e=t.flags,e&65536?(t.flags=e&-65537|128,t):null;case 19:return Je(Ye),null;case 4:return Os(),null;case 10:return $l(t.type._context),null;case 22:case 23:return xi(),null;case 24:return null;default:return null}}var va=!1,Nt=!1,Lf=typeof WeakSet=="function"?WeakSet:Set,X=null;function _s(e,t){var s=e.ref;if(s!==null)if(typeof s=="function")try{s(null)}catch(a){et(e,t,a)}else s.current=null}function ai(e,t,s){try{s()}catch(a){et(e,t,a)}}var Vc=!1;function Pf(e,t){if(xl=Fr,e=No(),cl(e)){if("selectionStart"in e)var s={start:e.selectionStart,end:e.selectionEnd};else e:{s=(s=e.ownerDocument)&&s.defaultView||window;var a=s.getSelection&&s.getSelection();if(a&&a.rangeCount!==0){s=a.anchorNode;var o=a.anchorOffset,c=a.focusNode;a=a.focusOffset;try{s.nodeType,c.nodeType}catch{s=null;break e}var f=0,h=-1,x=-1,D=0,O=0,B=e,M=null;t:for(;;){for(var G;B!==s||o!==0&&B.nodeType!==3||(h=f+o),B!==c||a!==0&&B.nodeType!==3||(x=f+a),B.nodeType===3&&(f+=B.nodeValue.length),(G=B.firstChild)!==null;)M=B,B=G;for(;;){if(B===e)break t;if(M===s&&++D===o&&(h=f),M===c&&++O===a&&(x=f),(G=B.nextSibling)!==null)break;B=M,M=B.parentNode}B=G}s=h===-1||x===-1?null:{start:h,end:x}}else s=null}s=s||{start:0,end:0}}else s=null;for(yl={focusedElem:e,selectionRange:s},Fr=!1,X=t;X!==null;)if(t=X,e=t.child,(t.subtreeFlags&1028)!==0&&e!==null)e.return=t,X=e;else for(;X!==null;){t=X;try{var te=t.alternate;if((t.flags&1024)!==0)switch(t.tag){case 0:case 11:case 15:break;case 1:if(te!==null){var re=te.memoizedProps,st=te.memoizedState,k=t.stateNode,b=k.getSnapshotBeforeUpdate(t.elementType===t.type?re:tn(t.type,re),st);k.__reactInternalSnapshotBeforeUpdate=b}break;case 3:var R=t.stateNode.containerInfo;R.nodeType===1?R.textContent="":R.nodeType===9&&R.documentElement&&R.removeChild(R.documentElement);break;case 5:case 6:case 4:case 17:break;default:throw Error(i(163))}}catch(Q){et(t,t.return,Q)}if(e=t.sibling,e!==null){e.return=t.return,X=e;break}X=t.return}return te=Vc,Vc=!1,te}function Sr(e,t,s){var a=t.updateQueue;if(a=a!==null?a.lastEffect:null,a!==null){var o=a=a.next;do{if((o.tag&e)===e){var c=o.destroy;o.destroy=void 0,c!==void 0&&ai(t,s,c)}o=o.next}while(o!==a)}}function ba(e,t){if(t=t.updateQueue,t=t!==null?t.lastEffect:null,t!==null){var s=t=t.next;do{if((s.tag&e)===e){var a=s.create;s.destroy=a()}s=s.next}while(s!==t)}}function li(e){var t=e.ref;if(t!==null){var s=e.stateNode;switch(e.tag){case 5:e=s;break;default:e=s}typeof t=="function"?t(e):t.current=e}}function Kc(e){var t=e.alternate;t!==null&&(e.alternate=null,Kc(t)),e.child=null,e.deletions=null,e.sibling=null,e.tag===5&&(t=e.stateNode,t!==null&&(delete t[un],delete t[dr],delete t[Sl],delete t[gf],delete t[xf])),e.stateNode=null,e.return=null,e.dependencies=null,e.memoizedProps=null,e.memoizedState=null,e.pendingProps=null,e.stateNode=null,e.updateQueue=null}function Jc(e){return e.tag===5||e.tag===3||e.tag===4}function Hc(e){e:for(;;){for(;e.sibling===null;){if(e.return===null||Jc(e.return))return null;e=e.return}for(e.sibling.return=e.return,e=e.sibling;e.tag!==5&&e.tag!==6&&e.tag!==18;){if(e.flags&2||e.child===null||e.tag===4)continue e;e.child.return=e,e=e.child}if(!(e.flags&2))return e.stateNode}}function ii(e,t,s){var a=e.tag;if(a===5||a===6)e=e.stateNode,t?s.nodeType===8?s.parentNode.insertBefore(e,t):s.insertBefore(e,t):(s.nodeType===8?(t=s.parentNode,t.insertBefore(e,s)):(t=s,t.appendChild(e)),s=s._reactRootContainer,s!=null||t.onclick!==null||(t.onclick=Xr));else if(a!==4&&(e=e.child,e!==null))for(ii(e,t,s),e=e.sibling;e!==null;)ii(e,t,s),e=e.sibling}function oi(e,t,s){var a=e.tag;if(a===5||a===6)e=e.stateNode,t?s.insertBefore(e,t):s.appendChild(e);else if(a!==4&&(e=e.child,e!==null))for(oi(e,t,s),e=e.sibling;e!==null;)oi(e,t,s),e=e.sibling}var gt=null,nn=!1;function qn(e,t,s){for(s=s.child;s!==null;)Wc(e,t,s),s=s.sibling}function Wc(e,t,s){if(cn&&typeof cn.onCommitFiberUnmount=="function")try{cn.onCommitFiberUnmount(Pr,s)}catch{}switch(s.tag){case 5:Nt||_s(s,t);case 6:var a=gt,o=nn;gt=null,qn(e,t,s),gt=a,nn=o,gt!==null&&(nn?(e=gt,s=s.stateNode,e.nodeType===8?e.parentNode.removeChild(s):e.removeChild(s)):gt.removeChild(s.stateNode));break;case 18:gt!==null&&(nn?(e=gt,s=s.stateNode,e.nodeType===8?bl(e.parentNode,s):e.nodeType===1&&bl(e,s),er(e)):bl(gt,s.stateNode));break;case 4:a=gt,o=nn,gt=s.stateNode.containerInfo,nn=!0,qn(e,t,s),gt=a,nn=o;break;case 0:case 11:case 14:case 15:if(!Nt&&(a=s.updateQueue,a!==null&&(a=a.lastEffect,a!==null))){o=a=a.next;do{var c=o,f=c.destroy;c=c.tag,f!==void 0&&((c&2)!==0||(c&4)!==0)&&ai(s,t,f),o=o.next}while(o!==a)}qn(e,t,s);break;case 1:if(!Nt&&(_s(s,t),a=s.stateNode,typeof a.componentWillUnmount=="function"))try{a.props=s.memoizedProps,a.state=s.memoizedState,a.componentWillUnmount()}catch(h){et(s,t,h)}qn(e,t,s);break;case 21:qn(e,t,s);break;case 22:s.mode&1?(Nt=(a=Nt)||s.memoizedState!==null,qn(e,t,s),Nt=a):qn(e,t,s);break;default:qn(e,t,s)}}function Gc(e){var t=e.updateQueue;if(t!==null){e.updateQueue=null;var s=e.stateNode;s===null&&(s=e.stateNode=new Lf),t.forEach(function(a){var o=Uf.bind(null,e,a);s.has(a)||(s.add(a),a.then(o,o))})}}function sn(e,t){var s=t.deletions;if(s!==null)for(var a=0;ao&&(o=f),a&=~c}if(a=o,a=nt()-a,a=(120>a?120:480>a?480:1080>a?1080:1920>a?1920:3e3>a?3e3:4320>a?4320:1960*Of(a/1960))-a,10e?16:e,Kn===null)var a=!1;else{if(e=Kn,Kn=null,Ca=0,(Me&6)!==0)throw Error(i(331));var o=Me;for(Me|=4,X=e.current;X!==null;){var c=X,f=c.child;if((X.flags&16)!==0){var h=c.deletions;if(h!==null){for(var x=0;xnt()-di?cs(e,0):ui|=s),At(e,t)}function cu(e,t){t===0&&((e.mode&1)===0?t=1:(t=Or,Or<<=1,(Or&130023424)===0&&(Or=4194304)));var s=Rt();e=Sn(e,t),e!==null&&(Ws(e,t,s),At(e,s))}function Qf(e){var t=e.memoizedState,s=0;t!==null&&(s=t.retryLane),cu(e,s)}function Uf(e,t){var s=0;switch(e.tag){case 13:var a=e.stateNode,o=e.memoizedState;o!==null&&(s=o.retryLane);break;case 19:a=e.stateNode;break;default:throw Error(i(314))}a!==null&&a.delete(t),cu(e,s)}var uu;uu=function(e,t,s){if(e!==null)if(e.memoizedProps!==t.pendingProps||Lt.current)Mt=!0;else{if((e.lanes&s)===0&&(t.flags&128)===0)return Mt=!1,Ef(e,t,s);Mt=(e.flags&131072)!==0}else Mt=!1,We&&(t.flags&1048576)!==0&&Uo(t,ra,t.index);switch(t.lanes=0,t.tag){case 2:var a=t.type;ja(e,t),e=t.pendingProps;var o=Is(t,vt.current);Ms(t,s),o=Ql(null,t,a,e,o,s);var c=Ul();return t.flags|=1,typeof o=="object"&&o!==null&&typeof o.render=="function"&&o.$$typeof===void 0?(t.tag=1,t.memoizedState=null,t.updateQueue=null,Pt(a)?(c=!0,ta(t)):c=!1,t.memoizedState=o.state!==null&&o.state!==void 0?o.state:null,Ml(t),o.updater=xa,t.stateNode=o,o._reactInternals=t,Wl(t,a,e,s),t=Zl(null,t,a,!0,c,s)):(t.tag=0,We&&c&&kl(t),Ct(null,t,o,s),t=t.child),t;case 16:a=t.elementType;e:{switch(ja(e,t),e=t.pendingProps,o=a._init,a=o(a._payload),t.type=a,o=t.tag=Vf(a),e=tn(a,e),o){case 0:t=Xl(null,t,a,e,s);break e;case 1:t=Oc(null,t,a,e,s);break e;case 11:t=Tc(null,t,a,e,s);break e;case 14:t=$c(null,t,a,tn(a.type,e),s);break e}throw Error(i(306,a,""))}return t;case 0:return a=t.type,o=t.pendingProps,o=t.elementType===a?o:tn(a,o),Xl(e,t,a,o,s);case 1:return a=t.type,o=t.pendingProps,o=t.elementType===a?o:tn(a,o),Oc(e,t,a,o,s);case 3:e:{if(Ac(t),e===null)throw Error(i(387));a=t.pendingProps,c=t.memoizedState,o=c.element,Xo(e,t),ua(t,a,null,s);var f=t.memoizedState;if(a=f.element,c.isDehydrated)if(c={element:a,isDehydrated:!1,cache:f.cache,pendingSuspenseBoundaries:f.pendingSuspenseBoundaries,transitions:f.transitions},t.updateQueue.baseState=c,t.memoizedState=c,t.flags&256){o=As(Error(i(423)),t),t=_c(e,t,a,s,o);break e}else if(a!==o){o=As(Error(i(424)),t),t=_c(e,t,a,s,o);break e}else for(Ut=_n(t.stateNode.containerInfo.firstChild),Qt=t,We=!0,en=null,s=Go(t,null,a,s),t.child=s;s;)s.flags=s.flags&-3|4096,s=s.sibling;else{if($s(),a===o){t=wn(e,t,s);break e}Ct(e,t,a,s)}t=t.child}return t;case 5:return tc(t),e===null&&Dl(t),a=t.type,o=t.pendingProps,c=e!==null?e.memoizedProps:null,f=o.children,jl(a,o)?f=null:c!==null&&jl(a,c)&&(t.flags|=32),Mc(e,t),Ct(e,t,f,s),t.child;case 6:return e===null&&Dl(t),null;case 13:return Bc(e,t,s);case 4:return Ol(t,t.stateNode.containerInfo),a=t.pendingProps,e===null?t.child=Ls(t,null,a,s):Ct(e,t,a,s),t.child;case 11:return a=t.type,o=t.pendingProps,o=t.elementType===a?o:tn(a,o),Tc(e,t,a,o,s);case 7:return Ct(e,t,t.pendingProps,s),t.child;case 8:return Ct(e,t,t.pendingProps.children,s),t.child;case 12:return Ct(e,t,t.pendingProps.children,s),t.child;case 10:e:{if(a=t.type._context,o=t.pendingProps,c=t.memoizedProps,f=o.value,Ve(ia,a._currentValue),a._currentValue=f,c!==null)if(Zt(c.value,f)){if(c.children===o.children&&!Lt.current){t=wn(e,t,s);break e}}else for(c=t.child,c!==null&&(c.return=t);c!==null;){var h=c.dependencies;if(h!==null){f=c.child;for(var x=h.firstContext;x!==null;){if(x.context===a){if(c.tag===1){x=Nn(-1,s&-s),x.tag=2;var D=c.updateQueue;if(D!==null){D=D.shared;var O=D.pending;O===null?x.next=x:(x.next=O.next,O.next=x),D.pending=x}}c.lanes|=s,x=c.alternate,x!==null&&(x.lanes|=s),Ll(c.return,s,t),h.lanes|=s;break}x=x.next}}else if(c.tag===10)f=c.type===t.type?null:c.child;else if(c.tag===18){if(f=c.return,f===null)throw Error(i(341));f.lanes|=s,h=f.alternate,h!==null&&(h.lanes|=s),Ll(f,s,t),f=c.sibling}else f=c.child;if(f!==null)f.return=c;else for(f=c;f!==null;){if(f===t){f=null;break}if(c=f.sibling,c!==null){c.return=f.return,f=c;break}f=f.return}c=f}Ct(e,t,o.children,s),t=t.child}return t;case 9:return o=t.type,a=t.pendingProps.children,Ms(t,s),o=Ht(o),a=a(o),t.flags|=1,Ct(e,t,a,s),t.child;case 14:return a=t.type,o=tn(a,t.pendingProps),o=tn(a.type,o),$c(e,t,a,o,s);case 15:return Lc(e,t,t.type,t.pendingProps,s);case 17:return a=t.type,o=t.pendingProps,o=t.elementType===a?o:tn(a,o),ja(e,t),t.tag=1,Pt(a)?(e=!0,ta(t)):e=!1,Ms(t,s),wc(t,a,o),Wl(t,a,o,s),Zl(null,t,a,!0,e,s);case 19:return zc(e,t,s);case 22:return Pc(e,t,s)}throw Error(i(156,t.tag))};function du(e,t){return Js(e,t)}function qf(e,t,s,a){this.tag=e,this.key=s,this.sibling=this.child=this.return=this.stateNode=this.type=this.elementType=null,this.index=0,this.ref=null,this.pendingProps=t,this.dependencies=this.memoizedState=this.updateQueue=this.memoizedProps=null,this.mode=a,this.subtreeFlags=this.flags=0,this.deletions=null,this.childLanes=this.lanes=0,this.alternate=null}function Yt(e,t,s,a){return new qf(e,t,s,a)}function ji(e){return e=e.prototype,!(!e||!e.isReactComponent)}function Vf(e){if(typeof e=="function")return ji(e)?1:0;if(e!=null){if(e=e.$$typeof,e===me)return 11;if(e===be)return 14}return 2}function Wn(e,t){var s=e.alternate;return s===null?(s=Yt(e.tag,t,e.key,e.mode),s.elementType=e.elementType,s.type=e.type,s.stateNode=e.stateNode,s.alternate=e,e.alternate=s):(s.pendingProps=t,s.type=e.type,s.flags=0,s.subtreeFlags=0,s.deletions=null),s.flags=e.flags&14680064,s.childLanes=e.childLanes,s.lanes=e.lanes,s.child=e.child,s.memoizedProps=e.memoizedProps,s.memoizedState=e.memoizedState,s.updateQueue=e.updateQueue,t=e.dependencies,s.dependencies=t===null?null:{lanes:t.lanes,firstContext:t.firstContext},s.sibling=e.sibling,s.index=e.index,s.ref=e.ref,s}function Ea(e,t,s,a,o,c){var f=2;if(a=e,typeof e=="function")ji(e)&&(f=1);else if(typeof e=="string")f=5;else e:switch(e){case Z:return ds(s.children,o,c,t);case Re:f=8,o|=8;break;case ee:return e=Yt(12,s,t,o|2),e.elementType=ee,e.lanes=c,e;case z:return e=Yt(13,s,t,o),e.elementType=z,e.lanes=c,e;case H:return e=Yt(19,s,t,o),e.elementType=H,e.lanes=c,e;case Ne:return Ta(s,o,c,t);default:if(typeof e=="object"&&e!==null)switch(e.$$typeof){case fe:f=10;break e;case Se:f=9;break e;case me:f=11;break e;case be:f=14;break e;case $e:f=16,a=null;break e}throw Error(i(130,e==null?e:typeof e,""))}return t=Yt(f,s,t,o),t.elementType=e,t.type=a,t.lanes=c,t}function ds(e,t,s,a){return e=Yt(7,e,a,t),e.lanes=s,e}function Ta(e,t,s,a){return e=Yt(22,e,a,t),e.elementType=Ne,e.lanes=s,e.stateNode={isHidden:!1},e}function vi(e,t,s){return e=Yt(6,e,null,t),e.lanes=s,e}function bi(e,t,s){return t=Yt(4,e.children!==null?e.children:[],e.key,t),t.lanes=s,t.stateNode={containerInfo:e.containerInfo,pendingChildren:null,implementation:e.implementation},t}function Kf(e,t,s,a,o){this.tag=t,this.containerInfo=e,this.finishedWork=this.pingCache=this.current=this.pendingChildren=null,this.timeoutHandle=-1,this.callbackNode=this.pendingContext=this.context=null,this.callbackPriority=0,this.eventTimes=Ha(0),this.expirationTimes=Ha(-1),this.entangledLanes=this.finishedLanes=this.mutableReadLanes=this.expiredLanes=this.pingedLanes=this.suspendedLanes=this.pendingLanes=0,this.entanglements=Ha(0),this.identifierPrefix=a,this.onRecoverableError=o,this.mutableSourceEagerHydrationData=null}function Si(e,t,s,a,o,c,f,h,x){return e=new Kf(e,t,s,h,x),t===1?(t=1,c===!0&&(t|=8)):t=0,c=Yt(3,null,null,t),e.current=c,c.stateNode=e,c.memoizedState={element:a,isDehydrated:s,cache:null,transitions:null,pendingSuspenseBoundaries:null},Ml(c),e}function Jf(e,t,s){var a=3"u"||typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE!="function"))try{__REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE(r)}catch(l){console.error(l)}}return r(),Di.exports=ip(),Di.exports}var ku;function cp(){if(ku)return _a;ku=1;var r=op();return _a.createRoot=r.createRoot,_a.hydrateRoot=r.hydrateRoot,_a}var up=cp();const dp=zu(up);class Ba extends Error{constructor(l,i,u){super(l),this.code=i,this.status=u,this.name="DashboardRequestError"}}const Mi=new Set;let Tr;function fp(){return Tr}function pp(r){return Mi.add(r),Tr&&r(Tr),()=>{Mi.delete(r)}}async function V(r,l={}){return Fi(r,i=>i.json(),l)}async function mp(r,l={}){return Fi(r,i=>i.text(),l)}async function mn(r,l={}){return Fi(r,async()=>{},l)}async function Fi(r,l,i={}){const u=i.timeoutMs??5e3,d=i.method??"GET",p=new AbortController,g=window.setTimeout(()=>p.abort(),u),y=new Headers(i.headers);y.has("Accept")||y.set("Accept","application/json"),i.body!==void 0&&!y.has("Content-Type")&&y.set("Content-Type","application/json");const S=i.rawBody??(i.body===void 0?void 0:JSON.stringify(i.body));try{Dr({path:r,status:"pending"});const v=await fetch(r,{headers:y,body:S,method:d,signal:p.signal});if(!v.ok){const w=await hp(v);throw Dr({path:r,status:"error",statusCode:v.status,message:w}),new Ba(w,v.status===503?"disabled":"http",v.status)}const N=await l(v);return Dr({path:r,status:"success",statusCode:v.status}),N}catch(v){if(v instanceof Ba)throw v;if(v instanceof DOMException&&v.name==="AbortError")throw Dr({path:r,status:"error",message:"Dashboard request timed out"}),new Ba("Dashboard request timed out","timeout");const N=v instanceof Error?v.message:"Dashboard request failed";throw Dr({path:r,status:"error",message:N}),new Ba(N,"network")}finally{window.clearTimeout(g)}}function Dr(r){Tr={...r,at:new Date().toISOString()},Mi.forEach(l=>l(Tr))}async function hp(r){if((r.headers.get("Content-Type")??"").includes("application/json")){const u=await r.json().catch(()=>{}),d=(u==null?void 0:u.error)??(u==null?void 0:u.message);if(typeof d=="string"&&d.trim()!=="")return d}const i=await r.text().catch(()=>"");return i.trim()!==""?i.trim():`Dashboard request failed with status ${r.status}`}async function gp(r=5e3){return V("/api/dashboard/services",{timeoutMs:r})}let Yn=Qu();function Qu(){return{status:"pending",promise:gp().then(l=>{Yn={status:"success",value:l}}).catch(l=>{Yn={status:"error",error:l}})}}function xp(){if(Yn.status==="pending")throw Yn.promise;if(Yn.status==="error")throw Yn.error;return Yn.value}function yp(){Yn=Qu()}function ae({children:r,className:l,type:i="button",...u}){return n.jsx("button",{className:l?`button ${l}`:"button",type:i,...u,children:r})}function F({title:r,description:l,actionLabel:i,onAction:u}){return n.jsxs("div",{className:"empty-state",children:[n.jsx("strong",{children:r}),n.jsx("p",{children:l}),i&&u?n.jsx(ae,{onClick:u,children:i}):null]})}function he({title:r,children:l}){return n.jsxs("section",{className:"panel",children:[r?n.jsx("header",{className:"panel-head",children:n.jsx("h2",{children:r})}):null,n.jsx("div",{className:"panel-body",children:l})]})}const Cn="/dashboard";function Er(r){return r===Cn||r===`${Cn}/`?"/":r.startsWith(`${Cn}/`)?r.slice(Cn.length)||"/":r}function za(r,l=window.location.pathname){return r===Cn||r.startsWith(`${Cn}/`)||!l.startsWith(Cn)?r:r==="/"?`${Cn}/`:`${Cn}${r}`}function jp({services:r}){const l=r.filter(u=>u.status==="running").length,i=r.filter(u=>u.status==="disabled").length;return r.length===0?n.jsx("section",{className:"service-index-page","aria-label":"Services",children:n.jsx(F,{title:"No dashboard services",description:"Configured services will appear here when they are available."})}):n.jsxs("section",{className:"service-index-page","aria-label":"Services",children:[n.jsxs("header",{className:"service-index-summary",children:[n.jsxs("div",{children:[n.jsx("h1",{children:"Services"}),n.jsx("p",{children:"Local dashboard registry from /api/dashboard/services."})]}),n.jsxs("dl",{className:"health-summary","aria-label":"Service health summary",children:[n.jsxs("div",{children:[n.jsx("dt",{children:"Running"}),n.jsx("dd",{children:l})]}),n.jsxs("div",{children:[n.jsx("dt",{children:"Disabled"}),n.jsx("dd",{children:i})]}),n.jsxs("div",{children:[n.jsx("dt",{children:"Total"}),n.jsx("dd",{children:r.length})]})]})]}),n.jsx("div",{className:"service-index",children:r.map(u=>n.jsx("a",{className:"service-card",href:za(u.path),children:n.jsxs(he,{children:[n.jsxs("div",{className:"service-card-head",children:[n.jsx("h2",{children:u.name}),n.jsx("span",{className:`service-status ${u.status}`,children:u.status})]}),n.jsx("p",{children:u.description}),u.endpoint?n.jsxs("dl",{className:"endpoint-summary",children:[n.jsx("dt",{children:"Endpoint"}),n.jsx("dd",{children:n.jsx("code",{children:u.endpoint})})]}):null]})},u.id))})]})}const Uu=m.createContext(null);function vp({children:r}){const[l,i]=m.useState(null),u=m.useCallback(p=>new Promise(g=>{i({options:p,resolve:g})}),[]),d=m.useCallback(p=>{i(g=>(g==null||g.resolve(p),null))},[]);return n.jsxs(Uu.Provider,{value:u,children:[r,l?n.jsx(bp,{options:l.options,onCancel:()=>d(!1),onConfirm:()=>d(!0)}):null]})}function $r(){const r=m.useContext(Uu);if(!r)throw new Error("useConfirm must be called inside ");return r}function bp({options:r,onCancel:l,onConfirm:i}){const u=m.useId(),d=m.useId(),p=m.useId(),g=r.confirmLabel??"Confirm",y=r.cancelLabel??"Cancel",S=r.tone??"default",v=r.requirePhrase??"",[N,w]=m.useState(""),L=m.useRef(null),C=v===""||N===v;m.useEffect(()=>{w("")},[r]),m.useEffect(()=>{v!==""&&L.current&&L.current.focus()},[v]),m.useEffect(()=>{function I(E){if(E.key==="Escape")E.preventDefault(),l();else if(E.key==="Enter"&&C){const J=E.target;if((J==null?void 0:J.tagName)==="TEXTAREA")return;E.preventDefault(),i()}}return document.addEventListener("keydown",I),()=>document.removeEventListener("keydown",I)},[l,i,C]);function A(I){I.target===I.currentTarget&&l()}return n.jsx("div",{className:"confirm-backdrop",onMouseDown:A,role:"presentation",children:n.jsxs("section",{"aria-describedby":r.description||r.detail?d:void 0,"aria-labelledby":u,"aria-modal":"true",className:`confirm-dialog tone-${S}`,role:"alertdialog",children:[n.jsx("h2",{className:"confirm-title",id:u,children:r.title}),r.description||r.detail?n.jsxs("div",{className:"confirm-body",id:d,children:[r.description?n.jsx("p",{className:"confirm-description",children:r.description}):null,r.detail?n.jsx("p",{className:"confirm-detail",children:r.detail}):null]}):null,v!==""?n.jsxs("div",{className:"confirm-phrase",children:[n.jsx("label",{htmlFor:p,children:r.requirePhraseLabel??n.jsxs(n.Fragment,{children:["Type ",n.jsx("code",{children:v})," to confirm"]})}),n.jsx("input",{autoComplete:"off",autoCorrect:"off",className:"confirm-phrase-input",id:p,onChange:I=>w(I.target.value),ref:L,spellCheck:!1,type:"text",value:N})]}):null,n.jsxs("div",{className:"confirm-actions",children:[n.jsx(ae,{onClick:l,type:"button",children:y}),n.jsx(ae,{autoFocus:v==="",className:S==="danger"?"danger primary":"primary",disabled:!C,onClick:i,type:"button",children:g})]})]})})}function Rn(r){return{title:r.title,description:r.description,detail:n.jsxs(n.Fragment,{children:["Target: ",n.jsx("code",{children:r.target})]}),confirmLabel:r.confirmLabel??"Delete",tone:"danger",requirePhrase:r.target}}const Cu=[1e3,2e3,4e3,8e3,16e3,3e4];function Sp(){return`${window.location.protocol==="https:"?"wss:":"ws:"}//${window.location.host}/api/events`}class Np{constructor(){fs(this,"ws",null);fs(this,"listeners",new Map);fs(this,"nextID",0);fs(this,"retryIndex",0);fs(this,"retryTimeout",null)}subscribe(l,i,u){const d=this.nextID;this.nextID+=1,this.listeners.set(d,{topics:new Set(l),cb:i,onResync:u}),this.ensureOpen();let p=!1;return()=>{p||(p=!0,this.listeners.delete(d),this.listeners.size===0&&this.close())}}ensureOpen(){this.ws||this.retryTimeout!==null||this.connect()}connect(){if(typeof WebSocket>"u")return;let l;try{l=new WebSocket(Sp())}catch{this.scheduleReconnect();return}this.ws=l,l.onopen=()=>{this.ws===l&&(this.retryIndex=0)},l.onmessage=u=>{if(!(this.ws!==l||typeof u.data!="string"))try{const d=JSON.parse(u.data);if(!Oi(d))return;d.type==="event"&&wp(d.event)?this.dispatch(d.event):d.type==="hello"?this.resync():d.type==="resync"&&this.resync()}catch{}};const i=()=>{if(this.ws===l){this.ws=null,this.detach(l);try{l.close()}catch{}this.listeners.size!==0&&this.scheduleReconnect()}};l.onerror=i,l.onclose=i}scheduleReconnect(){if(this.retryTimeout!==null)return;const l=Cu[Math.min(this.retryIndex,Cu.length-1)];this.retryIndex+=1,this.retryTimeout=window.setTimeout(()=>{this.retryTimeout=null,this.listeners.size>0&&!this.ws&&this.connect()},l)}dispatch(l){for(const i of this.listeners.values())if(!(i.topics.size>0&&!i.topics.has(l.service)))try{i.cb(l)}catch{}}resync(){var l;for(const i of this.listeners.values())try{(l=i.onResync)==null||l.call(i)}catch{}}detach(l){l.onopen=null,l.onmessage=null,l.onerror=null,l.onclose=null}close(){if(this.retryTimeout!==null&&(window.clearTimeout(this.retryTimeout),this.retryTimeout=null),this.ws){this.detach(this.ws);try{this.ws.close(1e3,"no listeners")}catch{}}this.ws=null,this.retryIndex=0}}function Oi(r){return typeof r=="object"&&r!==null&&!Array.isArray(r)}function wp(r){return Oi(r)&&typeof r.type=="string"&&typeof r.service=="string"&&typeof r.timestamp=="string"&&(r.payload===void 0||Oi(r.payload))}const kp=new Np;function Cp(r,l,i){return kp.subscribe(r,l,i)}function Vt({topics:r,onEvent:l,onResync:i,enabled:u=!0}){const d=m.useRef(l),p=m.useRef(i);m.useEffect(()=>{d.current=l,p.current=i},[l,i]);const g=r.slice().sort().join(",");m.useEffect(()=>{if(!u)return;const y=g?g.split(","):[];return Cp(y,v=>d.current(v),()=>{var v;return(v=p.current)==null?void 0:v.call(p)})},[g,u])}function wt(){const r=m.useRef(0),l=m.useRef(!0);return m.useEffect(()=>(l.current=!0,()=>{l.current=!1,r.current+=1}),[]),m.useCallback(()=>{const i=++r.current;return()=>l.current&&r.current===i},[])}async function Rp(){return V("/api/messages")}async function Dp(r){return V(`/api/messages/${encodeURIComponent(r)}`)}async function Ip(r){return mp(`/api/messages/${encodeURIComponent(r)}/raw`)}async function Ep(){return mn("/api/messages",{method:"DELETE"})}function ms(r){return!r||r.indexOf("=?")===-1?r:r.replace(/=\?([^?]+)\?([BbQq])\?([^?]*)\?=(\s+(?==\?))?/g,(l,i,u,d)=>{try{const p=u.toUpperCase()==="B"?Tp(d):$p(d);return new TextDecoder(Lp(i)).decode(p)}catch{return l}})}function Dn(r){return r?ms(r).trim():""}function Tp(r){const l=atob(r.replace(/\s+/g,"")),i=new Uint8Array(l.length);for(let u=0;u{const I=v();if(S){u({status:"success",messages:[]});return}u({status:"loading"}),Rp().then(({messages:E})=>{I()&&(u({status:"success",messages:E}),p(J=>{var ue;return J??((ue=E[0])==null?void 0:ue.id)}))}).catch(E=>{I()&&u({status:"error",message:E.message})})},[v,S]);Vt({topics:["mail"],onEvent:N,onResync:N,enabled:!S});async function w(){S||!await l(Rn({title:"Clear inbox",description:"Delete all stored Mail messages from this local devcloud inbox.",target:"CLEAR INBOX",confirmLabel:"Clear inbox"}))||(u({status:"loading"}),Ep().then(()=>{p(void 0),u({status:"success",messages:[]})}).catch(E=>{u({status:"error",message:E.message})}))}m.useEffect(()=>{N()},[N]);const L=i.status==="success"?i.messages:[],C=m.useMemo(()=>zp(L,g),[L,g]),A=C.find(I=>I.id===d)??C[0]??(g.trim()===""?L[0]:void 0);return S?n.jsx(he,{title:"Mail",children:n.jsx(F,{title:"Mail is disabled",description:"Enable services.mail.enabled in .devcloud/config.yaml to inspect received messages."})}):n.jsxs("div",{className:"mail-shell",children:[n.jsxs(he,{title:"Inbox",children:[n.jsxs("div",{className:"mail-inbox-toolbar",children:[n.jsxs("label",{className:"mail-inbox-filter",children:[n.jsx("span",{children:"Filter"}),n.jsx("input",{"aria-label":"Filter messages",onChange:I=>y(I.target.value),placeholder:"sender, recipient, subject",type:"search",value:g})]}),n.jsxs("div",{className:"mail-inbox-actions",children:[n.jsx(ae,{onClick:N,children:"Refresh"}),n.jsx(ae,{className:"danger",disabled:L.length===0,onClick:w,children:"Clear all"})]})]}),i.status==="loading"?n.jsx(F,{title:"Loading messages",description:"Reading the local Mail inbox."}):null,i.status==="error"?n.jsx(F,{title:"Mail messages unavailable",description:i.message,actionLabel:"Retry",onAction:N}):null,i.status==="success"?n.jsx(Mp,{messages:C,selectedID:A==null?void 0:A.id,totalMessages:L.length,onSelectMessage:p}):null]}),n.jsx(he,{title:"Message",children:n.jsx(Op,{message:A})})]})}function Mp({messages:r,selectedID:l,totalMessages:i,onSelectMessage:u}){return i===0?n.jsx(F,{title:"Inbox is empty",description:"Send mail to localhost:11025 (relaxed SMTP AUTH) and refresh."}):r.length===0?n.jsx(F,{title:"No matches",description:"Try a different filter or clear the search."}):n.jsx("ul",{className:"mail-inbox-list","aria-label":"Messages",children:r.map(d=>{const p=d.id===l,g=ms(d.subject)||"(No subject)",y=Dn(d.from)||"(unknown sender)",S=qu(d);return n.jsx("li",{children:n.jsxs("button",{"aria-current":p?"true":void 0,className:p?"mail-inbox-row active":"mail-inbox-row",onClick:()=>u(d.id),type:"button",children:[n.jsxs("span",{className:"mail-inbox-row-top",children:[n.jsx("span",{className:"mail-inbox-sender",children:y}),n.jsx("time",{className:"mail-inbox-time",dateTime:d.receivedAt,title:Ai(d.receivedAt),children:Vu(d.receivedAt)})]}),n.jsx("span",{className:"mail-inbox-subject",children:g}),S?n.jsx("span",{className:"mail-inbox-snippet",children:S}):null]})},d.id)})})}function Op({message:r}){const[l,i]=m.useState("preview"),[u,d]=m.useState(!1);if(m.useEffect(()=>{i("preview"),d(!1)},[r==null?void 0:r.id]),!r)return n.jsx(F,{title:"Select a message",description:"Messages accepted via SMTP appear here. Pick a row on the left to inspect."});const p=ms(r.subject)||"(No subject)",g=Dn(r.from),y=r.to.map(Dn).filter(Boolean),S=Ti(r.headers,"Cc"),v=Ti(r.headers,"Bcc"),N=Ti(r.headers,"Reply-To"),w=r.attachments??[],L=r.textBody||r.htmlBody||r.parseError||"",C=L.trim().length>0,A=[{id:"preview",label:"Preview"},{id:"attachments",label:"Attachments",count:w.length},{id:"raw",label:"Raw"}];return n.jsxs("article",{className:"mail-inspector",children:[n.jsxs("header",{className:"mail-inspector-header",children:[n.jsx("h1",{className:"mail-subject",children:p}),n.jsxs("dl",{className:"mail-recipients",children:[n.jsx(Ir,{label:"From",values:g?[g]:["(unknown sender)"]}),n.jsx(Ir,{label:"To",values:y.length>0?y:["(no recipients)"]}),S.length>0?n.jsx(Ir,{label:"Cc",values:S}):null,v.length>0?n.jsx(Ir,{label:"Bcc",values:v}):null,N.length>0?n.jsx(Ir,{label:"Reply-To",values:N}):null,n.jsxs("div",{className:"mail-recipients-row",children:[n.jsx("dt",{children:"Date"}),n.jsxs("dd",{children:[n.jsx("time",{dateTime:r.receivedAt,title:Ai(r.receivedAt),children:Ai(r.receivedAt)}),n.jsxs("span",{className:"mail-recipients-aux",children:["· ",Vu(r.receivedAt)]})]})]})]})]}),n.jsx("nav",{className:"mail-tabs",role:"tablist","aria-label":"Message view",children:A.map(I=>n.jsxs("button",{role:"tab","aria-selected":l===I.id,className:l===I.id?"mail-tab active":"mail-tab",onClick:()=>i(I.id),type:"button",children:[I.label,I.count!==void 0&&I.count>0?n.jsx("span",{className:"mail-tab-count",children:I.count}):null]},I.id))}),l==="preview"?n.jsx(Ap,{body:L,hasBody:C,parseError:r.parseError}):null,l==="attachments"?n.jsx(_p,{attachments:w}):null,l==="raw"?n.jsx(Bp,{messageID:r.id}):null,n.jsxs("details",{className:"mail-headers",open:u,onToggle:I=>d(I.target.open),children:[n.jsxs("summary",{children:[n.jsx("span",{children:"Headers"}),n.jsx("span",{className:"mail-headers-count",children:Object.keys(r.headers??{}).length})]}),n.jsx(Fp,{headers:r.headers??{}})]})]})}function Ir({label:r,values:l}){return n.jsxs("div",{className:"mail-recipients-row",children:[n.jsx("dt",{children:r}),n.jsx("dd",{children:l.join(", ")})]})}function Ap({body:r,hasBody:l,parseError:i}){return!l&&!i?n.jsx(F,{title:"No body",description:"The message contains no decoded text content."}):i&&!l?n.jsxs("div",{className:"mail-preview-error",children:[n.jsx("span",{className:"mail-eyebrow",children:"Parse error"}),n.jsx("p",{children:i})]}):n.jsx("div",{className:"mail-preview",lang:"auto",children:Up(r)})}function _p({attachments:r}){return r.length===0?n.jsx(F,{title:"No attachments",description:"This message has no parsed attachments."}):n.jsx("ul",{className:"mail-attachment-list","aria-label":"Attachments",children:r.map(l=>n.jsxs("li",{className:"mail-attachment-item",children:[n.jsx("span",{className:"mail-attachment-icon","aria-hidden":"true",children:qp(l.contentType)}),n.jsxs("div",{className:"mail-attachment-body",children:[n.jsx("span",{className:"mail-attachment-name",children:l.fileName||l.id}),n.jsxs("span",{className:"mail-attachment-meta",children:[l.contentType||"application/octet-stream"," · ",Vp(l.size)]})]})]},l.id))})}function Bp({messageID:r}){const[l,i]=m.useState({status:"loading"});if(m.useEffect(()=>{let p=!1;return i({status:"loading"}),Ip(r).then(g=>{p||i({status:"success",raw:g})}).catch(g=>{p||i({status:"error",message:g.message})}),()=>{p=!0}},[r]),l.status==="loading")return n.jsx(F,{title:"Loading raw source",description:"Reading the stored RFC 822 message."});if(l.status==="error")return n.jsx(F,{title:"Raw source unavailable",description:l.message});const u=l.raw;function d(){navigator.clipboard&&navigator.clipboard.writeText(u).catch(()=>{})}return n.jsxs("div",{className:"mail-raw",children:[n.jsxs("div",{className:"mail-raw-toolbar",children:[n.jsxs("span",{className:"mail-raw-meta",children:[u.length.toLocaleString()," bytes"]}),n.jsx("button",{className:"mail-raw-copy",onClick:d,type:"button",children:"Copy"})]}),n.jsx("pre",{className:"mail-raw-pre",children:u||"(empty)"})]})}function Fp({headers:r}){const l=Object.entries(r).sort(([i],[u])=>i.localeCompare(u));return l.length===0?n.jsx("p",{className:"mail-headers-empty",children:"No parsed headers."}):n.jsx("table",{className:"mail-headers-table",children:n.jsx("tbody",{children:l.map(([i,u])=>n.jsxs("tr",{children:[n.jsx("th",{scope:"row",children:i}),n.jsx("td",{children:u.map(ms).join(", ")})]},i))})})}function zp(r,l){const i=l.trim().toLowerCase();return i===""?r:r.filter(u=>[ms(u.subject),Dn(u.from),...u.to.map(Dn),qu(u)].join(" ").toLowerCase().includes(i))}function qu(r){return(r.textBody||r.htmlBody||r.parseError||"").replace(/\s+/g," ").trim()}function Ti(r,l){if(!r)return[];const i=l.toLowerCase();for(const[u,d]of Object.entries(r))if(u.toLowerCase()===i)return d.flatMap(p=>Qp(p)).map(Dn).filter(Boolean);return[]}function Qp(r){return r.split(/,(?![^<]*>)/).map(l=>l.trim()).filter(l=>l.length>0)}function Up(r){const l=[],i=/(https?:\/\/[^\s<>"']+)|((?:[a-zA-Z0-9._%+-]+)@(?:[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}))/g;let u=0,d=0,p=i.exec(r);for(;p!==null;){const y=r.slice(u,p.index);y&&(d+=1,l.push(n.jsx("span",{children:y},`t-${d}`))),d+=1,p[1]?l.push(n.jsx("a",{href:p[1],rel:"noopener noreferrer",target:"_blank",children:p[1]},`l-${d}`)):l.push(n.jsx("a",{href:`mailto:${p[2]}`,children:p[2]},`m-${d}`)),u=i.lastIndex,p=i.exec(r)}const g=r.slice(u);return g&&(d+=1,l.push(n.jsx("span",{children:g},`t-${d}`))),l}function qp(r){if(!r)return"📎";const l=r.toLowerCase();return l.startsWith("image/")?"🖼":l==="application/pdf"?"📄":l.startsWith("audio/")?"🎵":l.startsWith("video/")?"🎬":l.includes("zip")||l.includes("tar")||l.includes("gzip")?"🗜":l.startsWith("text/")?"📝":"📎"}function Ai(r){const l=new Date(r);return Number.isNaN(l.getTime())?r||"unknown":l.toLocaleString()}function Vu(r){const l=new Date(r);if(Number.isNaN(l.getTime()))return r||"unknown";const i=Date.now()-l.getTime();if(i<0)return"in the future";const u=Math.floor(i/1e3);if(u<60)return"just now";const d=Math.floor(u/60);if(d<60)return`${d}m ago`;const p=Math.floor(d/60);if(p<24)return`${p}h ago`;const g=Math.floor(p/24);return g<7?`${g}d ago`:l.toLocaleDateString()}function Vp(r){if(!Number.isFinite(r)||r<0)return"unknown";if(r<1024)return`${r} B`;const l=["KB","MB","GB","TB"];let i=r/1024,u=0;for(;i>=1024&&u=10?0:1)} ${l[u]}`}function Kp({buckets:r,activeBucket:l,disabled:i,onDeleteBucket:u,onSelectBucket:d}){return r.length===0?n.jsx(F,{title:"No buckets",description:"Buckets created through the S3 API will appear here."}):n.jsx("ul",{className:"s3-bucket-list","aria-label":"S3 buckets",children:r.map(p=>{const g=p.name===l,y=Yp(p.creationDate),S=Wp(p.objectCount),v=`${p.name} -${S} · created ${Gp(p.creationDate)}`;return n.jsxs("li",{className:g?"s3-bucket-row active":"s3-bucket-row",title:v,children:[n.jsxs("button",{className:"s3-bucket-select",onClick:()=>d(p.name),title:v,type:"button",children:[n.jsx("span",{className:"s3-bucket-icon","aria-hidden":!0,children:n.jsx(Jp,{})}),n.jsxs("span",{className:"s3-bucket-info",children:[n.jsx("span",{className:"s3-bucket-name",title:p.name,children:p.name}),n.jsxs("span",{className:"s3-bucket-meta",children:[S," · ",y]})]})]}),n.jsx("button",{"aria-label":`Delete bucket ${p.name}`,className:"s3-icon-button danger s3-bucket-delete",disabled:i,onClick:()=>u(p.name),type:"button",children:n.jsx(Hp,{})})]},p.name)})})}function Jp(){return n.jsxs("svg",{width:"16",height:"16",viewBox:"0 0 16 16",fill:"none",stroke:"currentColor",strokeWidth:"1.6",strokeLinecap:"round",strokeLinejoin:"round",children:[n.jsx("path",{d:"M2.5 4.5h11l-1 8.5a1 1 0 0 1-1 .9H4.5a1 1 0 0 1-1-.9z"}),n.jsx("path",{d:"M2.5 4.5L4 2.5h8l1.5 2"}),n.jsx("path",{d:"M6 7v4"}),n.jsx("path",{d:"M10 7v4"})]})}function Hp(){return n.jsxs("svg",{"aria-hidden":!0,width:"14",height:"14",viewBox:"0 0 16 16",fill:"none",stroke:"currentColor",strokeWidth:"1.6",strokeLinecap:"round",strokeLinejoin:"round",children:[n.jsx("path",{d:"M2.5 4h11"}),n.jsx("path",{d:"M6 4V2.5h4V4"}),n.jsx("path",{d:"M4 4l.7 9.2a1 1 0 0 0 1 .9h4.6a1 1 0 0 0 1-.9L12 4"}),n.jsx("path",{d:"M6.5 7v5"}),n.jsx("path",{d:"M9.5 7v5"})]})}function Wp(r){return!Number.isFinite(r)||r<0?"— objects":r===1?"1 object":`${r} objects`}function Gp(r){const l=new Date(r);return Number.isNaN(l.getTime())?r||"unknown":l.toLocaleString()}function Yp(r){const l=new Date(r);if(Number.isNaN(l.getTime()))return"created —";const i=Date.now()-l.getTime(),u=60*1e3,d=60*u,p=24*d,g=7*p,y=30*p,S=365*p;return i{if(d(),!r){w({status:"idle"});return}let C=!1;return w({status:"loading"}),tm(r,S.trim()).then(({objects:A})=>{C||w({status:"success",objects:A})}).catch(A=>{C||w({status:"error",message:A.message})}),()=>{C=!0}},[r,d,S,u]),!r)return n.jsx(F,{title:"Select a bucket",description:"Choose a bucket to browse its objects."});const L=N.status==="success"?N.objects:[];return n.jsxs("div",{className:"s3-browser",children:[n.jsxs("div",{className:"s3-browser-toolbar",children:[n.jsx(cm,{prefix:S,onChange:v}),n.jsx(um,{disabled:i,onUploadObject:g})]}),n.jsx(om,{bucketName:r,disabled:i}),N.status==="loading"?n.jsx(F,{title:"Loading objects",description:`Reading objects in ${r}.`}):null,N.status==="error"?n.jsx(F,{title:"Objects unavailable",description:N.message}):null,N.status==="success"&&L.length===0?n.jsx(F,{title:"No objects",description:S.trim()===""?`Objects uploaded to ${r} will appear here.`:`No objects in ${r} match this prefix.`}):null,N.status==="success"&&L.length>0?n.jsx("div",{className:"s3-object-table-wrap",children:n.jsxs("table",{className:"s3-object-table",children:[n.jsx("thead",{children:n.jsxs("tr",{children:[n.jsx("th",{scope:"col",children:"Key"}),n.jsx("th",{scope:"col",children:"Size"}),n.jsx("th",{scope:"col",children:"Modified"}),n.jsx("th",{scope:"col",children:"Type"}),n.jsx("th",{"aria-label":"Row actions",className:"s3-object-actions-head",scope:"col"})]})}),n.jsx("tbody",{children:L.map(C=>{const A=C.key===l;return n.jsxs("tr",{className:A?"s3-object-row active":"s3-object-row",children:[n.jsx("td",{children:n.jsx("button",{className:"s3-object-key",onClick:()=>y(C),title:C.key,type:"button",children:C.key})}),n.jsx("td",{className:"s3-object-numeric",children:fm(C.size)}),n.jsx("td",{className:"s3-object-numeric",children:Ju(C.lastModified)}),n.jsx("td",{className:"s3-object-type",children:C.contentType||"application/octet-stream"}),n.jsxs("td",{className:"s3-object-actions",children:[n.jsx("a",{"aria-label":`Download ${C.key}`,className:"s3-icon-button neutral",href:pm(C.downloadUrl),title:"Download",children:n.jsx(gm,{})}),n.jsx("button",{"aria-label":`Delete ${C.key}`,className:"s3-icon-button danger",disabled:i,onClick:()=>p(C),title:"Delete",type:"button",children:n.jsx(xm,{})})]})]},C.key)})})]})}):null]})}function om({bucketName:r,disabled:l}){const[i,u]=m.useState({status:"loading"}),[d,p]=m.useState(0),[g,y]=m.useState(""),S=$r();m.useEffect(()=>{let N=!1;return u({status:"loading"}),sm(r).then(({uploads:w})=>{N||u({status:"success",uploads:w})}).catch(w=>{N||u({status:"error",message:w.message})}),()=>{N=!0}},[r,d]);async function v(N){if(!(l||g!==""||!await S(Rn({title:"Abort multipart upload",description:"Pending parts will be discarded and the upload will be unrecoverable.",target:N.uploadId,confirmLabel:"Abort upload"})))){y(N.uploadId);try{await rm(r,N.uploadId),p(L=>L+1)}finally{y("")}}}return i.status!=="success"||i.uploads.length===0?null:n.jsxs("details",{className:"s3-multipart-details",children:[n.jsxs("summary",{children:[n.jsx("span",{children:"Incomplete multipart uploads"}),n.jsx("span",{className:"s3-multipart-count",children:i.uploads.length})]}),n.jsx("ul",{className:"s3-multipart-list",children:i.uploads.map(N=>n.jsxs("li",{className:"s3-multipart-item",children:[n.jsxs("div",{className:"s3-multipart-meta",children:[n.jsx("strong",{children:N.key}),n.jsx("code",{children:N.uploadId}),n.jsx("span",{children:Ju(N.initiated)})]}),n.jsx(ae,{"aria-label":`Abort multipart upload ${N.uploadId} with confirmation`,className:"danger",disabled:l||g!=="",onClick:()=>void v(N),children:g===N.uploadId?"Aborting":"Abort"})]},N.uploadId))})]})}function cm({prefix:r,onChange:l}){return n.jsxs("label",{className:"s3-search",children:[n.jsx(mm,{}),n.jsx("input",{"aria-label":"Filter objects by prefix",onChange:i=>l(i.target.value),placeholder:"Filter by prefix (e.g. docs/)",type:"search",value:r})]})}function um({disabled:r,onUploadObject:l}){const i=m.useRef(null),[u,d]=m.useState(""),[p,g]=m.useState(""),[y,S]=m.useState(""),[v,N]=m.useState(!1),w=m.useRef(null);async function L(C){var I,E;C.preventDefault();const A=(E=(I=i.current)==null?void 0:I.files)==null?void 0:E[0];if(!(r||v||u.trim()===""||!A)){N(!0);try{await l({key:u.trim(),file:A,contentType:p.trim()||A.type||"application/octet-stream",metadata:dm(y)}),d(""),g(""),S(""),i.current&&(i.current.value=""),w.current&&(w.current.open=!1)}finally{N(!1)}}}return n.jsxs("details",{className:"s3-upload-details",ref:w,children:[n.jsxs("summary",{children:[n.jsx(hm,{}),n.jsx("span",{children:"Upload object"})]}),n.jsxs("form",{className:"s3-upload-form",onSubmit:L,children:[n.jsxs("label",{className:"s3-field",children:[n.jsx("span",{children:"Key"}),n.jsx("input",{"aria-label":"S3 upload object key",className:"s3-text-input",disabled:r||v,onChange:C=>d(C.target.value),placeholder:"docs/readme.txt",value:u})]}),n.jsxs("label",{className:"s3-field",children:[n.jsx("span",{children:"Content-Type"}),n.jsx("input",{"aria-label":"S3 upload content type",className:"s3-text-input",disabled:r||v,onChange:C=>g(C.target.value),placeholder:"text/plain",value:p})]}),n.jsxs("label",{className:"s3-field s3-field-full",children:[n.jsx("span",{children:"File"}),n.jsx("input",{"aria-label":"S3 upload file",className:"s3-file-input",disabled:r||v,ref:i,type:"file"})]}),n.jsxs("label",{className:"s3-field s3-field-full",children:[n.jsx("span",{children:"Metadata (x-amz-meta)"}),n.jsx("textarea",{"aria-label":"S3 upload metadata",className:"s3-text-input",disabled:r||v,onChange:C=>S(C.target.value),placeholder:`source=dashboard -purpose=local-test`,rows:2,value:y})]}),n.jsx("div",{className:"s3-upload-actions",children:n.jsx(ae,{disabled:r||v||u.trim()==="",type:"submit",children:v?"Uploading…":"Upload"})})]})]})}function dm(r){return r.split(/\r?\n/).reduce((l,i)=>{const u=i.trim();if(u==="")return l;const[d,...p]=u.split("="),g=d.trim(),y=p.join("=").trim();return g!==""&&(l[g]=y),l},{})}function fm(r){if(!Number.isFinite(r)||r<0)return"unknown";if(r<1024)return`${r} B`;const l=["KB","MB","GB","TB"];let i=r/1024,u=0;for(;i>=1024&&u=10?0:1)} ${l[u]}`}function Ju(r){const l=new Date(r);return Number.isNaN(l.getTime())?r||"unknown":l.toLocaleString()}function pm(r){return r.startsWith("/api/s3/")?r:"#"}function mm(){return n.jsxs("svg",{"aria-hidden":!0,width:"14",height:"14",viewBox:"0 0 16 16",fill:"none",stroke:"currentColor",strokeWidth:"1.6",strokeLinecap:"round",strokeLinejoin:"round",children:[n.jsx("circle",{cx:"7",cy:"7",r:"4.5"}),n.jsx("path",{d:"M13.5 13.5L10.5 10.5"})]})}function hm(){return n.jsxs("svg",{"aria-hidden":!0,width:"14",height:"14",viewBox:"0 0 16 16",fill:"none",stroke:"currentColor",strokeWidth:"1.8",strokeLinecap:"round",children:[n.jsx("path",{d:"M8 3v10"}),n.jsx("path",{d:"M3 8h10"})]})}function gm(){return n.jsxs("svg",{"aria-hidden":!0,width:"14",height:"14",viewBox:"0 0 16 16",fill:"none",stroke:"currentColor",strokeWidth:"1.6",strokeLinecap:"round",strokeLinejoin:"round",children:[n.jsx("path",{d:"M8 2.5v8"}),n.jsx("path",{d:"M4.5 7L8 10.5 11.5 7"}),n.jsx("path",{d:"M3 13h10"})]})}function xm(){return n.jsxs("svg",{"aria-hidden":!0,width:"14",height:"14",viewBox:"0 0 16 16",fill:"none",stroke:"currentColor",strokeWidth:"1.6",strokeLinecap:"round",strokeLinejoin:"round",children:[n.jsx("path",{d:"M2.5 4h11"}),n.jsx("path",{d:"M6 4V2.5h4V4"}),n.jsx("path",{d:"M4 4l.7 9.2a1 1 0 0 0 1 .9h4.6a1 1 0 0 0 1-.9L12 4"}),n.jsx("path",{d:"M6.5 7v5"}),n.jsx("path",{d:"M9.5 7v5"})]})}function ym({bucketName:r,disabled:l,object:i,onCopyObject:u}){if(!i)return n.jsxs("div",{className:"s3-inspector-empty",children:[n.jsx(Nm,{}),n.jsx("p",{className:"s3-inspector-empty-title",children:"No object selected"}),n.jsx("p",{className:"s3-inspector-empty-body",children:r?`Pick an object in ${r} to see metadata, copy, and download actions.`:"Select a bucket first, then click an object to inspect it here."})]});const d=Object.entries(i.metadata??{});return n.jsxs("div",{className:"s3-inspector",children:[n.jsxs("div",{className:"s3-inspector-section",children:[n.jsx("span",{className:"s3-field-label",children:"Object"}),n.jsx("code",{className:"s3-inspector-key",title:i.key,children:i.key}),n.jsx("code",{className:"s3-inspector-uri",title:i.s3Uri,children:i.s3Uri})]}),n.jsxs("dl",{className:"s3-inspector-list",children:[n.jsxs("div",{children:[n.jsx("dt",{children:"Size"}),n.jsx("dd",{children:vm(i.size)})]}),n.jsxs("div",{children:[n.jsx("dt",{children:"Content type"}),n.jsx("dd",{children:i.contentType||"application/octet-stream"})]}),n.jsxs("div",{children:[n.jsx("dt",{children:"Last modified"}),n.jsx("dd",{children:bm(i.lastModified)})]}),n.jsxs("div",{children:[n.jsx("dt",{children:"ETag"}),n.jsx("dd",{children:n.jsx("code",{children:i.etag||"unknown"})})]})]}),n.jsxs("div",{className:"s3-inspector-section",children:[n.jsx("span",{className:"s3-field-label",children:"Metadata"}),d.length===0?n.jsx("p",{className:"s3-inspector-muted",children:"No user metadata."}):n.jsx("dl",{className:"s3-inspector-metadata",children:d.map(([p,g])=>n.jsxs("div",{children:[n.jsx("dt",{children:p}),n.jsx("dd",{children:g})]},p))})]}),n.jsxs("a",{className:"s3-inspector-download",href:Sm(i.downloadUrl),children:[n.jsx(wm,{}),n.jsx("span",{children:"Download object"})]}),n.jsx(jm,{disabled:l,object:i,onCopyObject:u})]})}function jm({disabled:r,object:l,onCopyObject:i}){const[u,d]=m.useState(`${l.key}.copy`),[p,g]=m.useState(!1);m.useEffect(()=>{d(`${l.key}.copy`)},[l.key]);async function y(S){S.preventDefault();const v=u.trim();if(!(r||p||v==="")){g(!0);try{await i(v)}finally{g(!1)}}}return n.jsxs("form",{className:"s3-inspector-copy",onSubmit:y,children:[n.jsxs("label",{className:"s3-field",children:[n.jsx("span",{children:"Copy to key"}),n.jsx("input",{"aria-label":"S3 copy destination key",className:"s3-text-input",disabled:r||p,onChange:S=>d(S.target.value),value:u})]}),n.jsx(ae,{disabled:r||p||u.trim()==="",type:"submit",children:p?"Copying…":"Copy"})]})}function vm(r){if(!Number.isFinite(r)||r<0)return"unknown";if(r<1024)return`${r} B`;const l=["KB","MB","GB","TB"];let i=r/1024,u=0;for(;i>=1024&&u=10?0:1)} ${l[u]}`}function bm(r){const l=new Date(r);return Number.isNaN(l.getTime())?r||"unknown":l.toLocaleString()}function Sm(r){return r.startsWith("/api/s3/")?r:"#"}function Nm(){return n.jsxs("svg",{"aria-hidden":!0,width:"36",height:"36",viewBox:"0 0 36 36",fill:"none",stroke:"currentColor",strokeWidth:"1.6",strokeLinecap:"round",strokeLinejoin:"round",children:[n.jsx("path",{d:"M7 11l11-6 11 6v14L18 31 7 25z"}),n.jsx("path",{d:"M7 11l11 6 11-6"}),n.jsx("path",{d:"M18 17v14"})]})}function wm(){return n.jsxs("svg",{"aria-hidden":!0,width:"14",height:"14",viewBox:"0 0 16 16",fill:"none",stroke:"currentColor",strokeWidth:"1.6",strokeLinecap:"round",strokeLinejoin:"round",children:[n.jsx("path",{d:"M8 2.5v8"}),n.jsx("path",{d:"M4.5 7L8 10.5 11.5 7"}),n.jsx("path",{d:"M3 13h10"})]})}function km({service:r}){var me;const[l,i]=m.useState({status:"loading"}),[u,d]=m.useState(),[p,g]=m.useState(),[y,S]=m.useState(""),[v,N]=m.useState(""),[w,L]=m.useState(0),C=(r==null?void 0:r.status)==="disabled",A=$r(),I=wt(),E=m.useCallback(()=>{const z=I();if(C){i({status:"success",buckets:[]}),d(void 0),g(void 0);return}i({status:"loading"}),Xp().then(({buckets:H})=>{z()&&(i({status:"success",buckets:H}),d(be=>{var $e;return be&&H.some(Ne=>Ne.name===be)?be:($e=H[0])==null?void 0:$e.name}))}).catch(H=>{z()&&i({status:"error",message:H.message})})},[I,C]);m.useEffect(()=>{E()},[E]);const J=m.useCallback(z=>{E(),z.type.startsWith("s3.object.")&&L(H=>H+1)},[E]);Vt({topics:["s3"],onEvent:J,onResync:()=>{E(),L(z=>z+1)},enabled:!C});const ue=l.status==="success"?l.buckets:[],q=(me=ue.find(z=>z.name===u))==null?void 0:me.name,P=p&&q?p:void 0,oe=m.useCallback(()=>{g(void 0)},[]);if(C)return n.jsx(he,{title:"S3",children:n.jsx(F,{title:"S3 is disabled",description:"Enable the S3 service in devcloud config to browse buckets and objects."})});function pe(z){d(z),g(void 0)}function Z(){const z=y.trim();C||z===""||Zp(z).then(H=>{N(`Created bucket ${H.name}`),S(""),d(H.name),E()}).catch(H=>N(H.message))}async function Re(z){C||!await A(Rn({title:"Delete bucket",description:"All objects and metadata in this bucket will be removed. This cannot be undone.",target:z}))||em(z).then(()=>{N(`Deleted bucket ${z}`),d(be=>be===z?void 0:be),g(void 0),E()}).catch(be=>N(be.message))}async function ee(z){!q||C||!await A(Rn({title:"Delete object",description:"This object will be permanently removed from the bucket.",target:z.key}))||nm(q,z.key).then(()=>{N(`Deleted object ${z.s3Uri}`),g(void 0),L(be=>be+1),E()}).catch(be=>N(be.message))}async function fe(z){if(!(!q||C))try{const H=await am({bucketName:q,objectKey:z.key,body:z.file,contentType:z.contentType,metadata:z.metadata});N(`Uploaded object ${H.s3Uri}`),g(H),L(be=>be+1),E()}catch(H){throw N(H instanceof Error?H.message:"S3 upload failed"),H}}async function Se(z){if(!(!q||!P||C))try{const H=await lm({sourceBucketName:q,sourceObjectKey:P.key,destinationBucketName:q,destinationObjectKey:z});N(`Copied object ${P.s3Uri} to ${H.s3Uri}`),g(H),L(be=>be+1),E()}catch(H){throw N(H instanceof Error?H.message:"S3 copy failed"),H}}return n.jsxs("div",{className:"s3-workspace",children:[n.jsxs(he,{title:"Buckets",children:[n.jsxs("div",{className:"s3-bucket-toolbar",children:[n.jsx("span",{className:"s3-bucket-count",children:ue.length===1?"1 bucket":`${ue.length} buckets`}),n.jsx("button",{"aria-label":"Refresh buckets",className:"s3-icon-button neutral",onClick:E,type:"button",children:n.jsx(Rm,{})})]}),n.jsx(Cm,{bucketName:y,disabled:C,onChange:S,onCreate:Z}),l.status==="loading"?n.jsx(F,{title:"Loading buckets",description:"Reading the local S3 bucket registry."}):null,l.status==="error"?n.jsx(F,{title:"S3 buckets unavailable",description:l.message,actionLabel:"Retry",onAction:E}):null,l.status==="success"?n.jsx(Kp,{buckets:ue,activeBucket:q,disabled:C,onDeleteBucket:Re,onSelectBucket:pe}):null]}),n.jsx(he,{title:q?`Objects · ${q}`:"Objects",children:n.jsx(im,{bucketName:q,activeObjectKey:P==null?void 0:P.key,disabled:C,refreshNonce:w,onClearObject:oe,onDeleteObject:ee,onUploadObject:fe,onSelectObject:g})}),n.jsx(he,{title:"Inspector",children:n.jsx(ym,{bucketName:q,disabled:C,object:P,onCopyObject:Se})}),v?n.jsx("p",{className:"s3-status-banner",children:v}):null]})}function Cm({bucketName:r,disabled:l,onChange:i,onCreate:u}){return n.jsxs("form",{className:"s3-bucket-create",onSubmit:d=>{d.preventDefault(),u()},children:[n.jsx("input",{"aria-label":"New bucket name",className:"s3-text-input",disabled:l,onChange:d=>i(d.target.value),placeholder:"New bucket name",value:r}),n.jsx(ae,{disabled:l||r.trim()==="",type:"submit",children:"Create"})]})}function Rm(){return n.jsxs("svg",{"aria-hidden":!0,width:"16",height:"16",viewBox:"0 0 16 16",fill:"none",stroke:"currentColor",strokeWidth:"1.6",strokeLinecap:"round",strokeLinejoin:"round",children:[n.jsx("path",{d:"M13.5 8a5.5 5.5 0 1 1-1.61-3.89"}),n.jsx("path",{d:"M13.5 3v3h-3"})]})}async function Dm(){return V("/api/dynamodb/status")}async function Im(){return V("/api/dynamodb/tables")}async function Em(r){return V("/api/dynamodb/tables",{method:"POST",body:{input:r}})}async function Tm(r,l=100){return V(`/api/dynamodb/tables/${encodeURIComponent(r)}/items?limit=${encodeURIComponent(String(l))}`)}async function $m(r,l){return V(`/api/dynamodb/tables/${encodeURIComponent(r)}/items`,{method:"POST",body:{input:l}})}async function Lm(r,l){return V(`/api/dynamodb/tables/${encodeURIComponent(r)}/items/update`,{method:"POST",body:{input:l}})}async function Pm(r,l,i){return V(`/api/dynamodb/tables/${encodeURIComponent(r)}/items/delete`,{method:"POST",body:{input:l,confirmation:i}})}async function Mm(r,l){return V(`/api/dynamodb/tables/${encodeURIComponent(r)}/query`,{method:"POST",body:{input:l}})}async function Om(r,l){return V(`/api/dynamodb/tables/${encodeURIComponent(r)}/scan`,{method:"POST",body:{input:l}})}async function Am(r){return V(`/api/dynamodb/tables/${encodeURIComponent(r)}`)}async function _m(r){return V(`/api/dynamodb/tables/${encodeURIComponent(r)}/indexes`)}async function Bm(r){return V(`/api/dynamodb/tables/${encodeURIComponent(r)}/ttl`)}async function Fm(r,l){return V(`/api/dynamodb/tables/${encodeURIComponent(r)}/ttl`,{method:"POST",body:{input:l}})}async function zm(r,l,i){return V(`/api/dynamodb/tables/${encodeURIComponent(r)}/delete`,{method:"POST",body:{input:l,confirmation:i}})}async function Qm(r){return V(`/api/dynamodb/tables/${encodeURIComponent(r)}/streams`)}const _i="devcloud.dynamodb.recentOperations.v1",zi=10,Um=`{ +`+c.stack}return{value:e,source:t,stack:o,digest:null}}function Gi(e,t,s){return{value:e,source:null,stack:s??null,digest:t??null}}function Yi(e,t){try{console.error(t.value)}catch(s){setTimeout(function(){throw s})}}var Rf=typeof WeakMap=="function"?WeakMap:Map;function Cc(e,t,s){s=Nn(-1,s),s.tag=3,s.payload={element:null};var a=t.value;return s.callback=function(){wa||(wa=!0,fl=a),Yi(e,t)},s}function Rc(e,t,s){s=Nn(-1,s),s.tag=3;var a=e.type.getDerivedStateFromError;if(typeof a=="function"){var o=t.value;s.payload=function(){return a(o)},s.callback=function(){Yi(e,t)}}var c=e.stateNode;return c!==null&&typeof c.componentDidCatch=="function"&&(s.callback=function(){Yi(e,t),typeof a!="function"&&(Vn===null?Vn=new Set([this]):Vn.add(this));var f=t.stack;this.componentDidCatch(t.value,{componentStack:f!==null?f:""})}),s}function Dc(e,t,s){var a=e.pingCache;if(a===null){a=e.pingCache=new Rf;var o=new Set;a.set(t,o)}else o=a.get(t),o===void 0&&(o=new Set,a.set(t,o));o.has(s)||(o.add(s),e=zf.bind(null,e,t,s),t.then(e,e))}function Ic(e){do{var t;if((t=e.tag===13)&&(t=e.memoizedState,t=t!==null?t.dehydrated!==null:!0),t)return e;e=e.return}while(e!==null);return null}function Ec(e,t,s,a,o){return(e.mode&1)===0?(e===t?e.flags|=65536:(e.flags|=128,s.flags|=131072,s.flags&=-52805,s.tag===1&&(s.alternate===null?s.tag=17:(t=Nn(-1,1),t.tag=2,Un(s,t,1))),s.lanes|=1),e):(e.flags|=65536,e.lanes=o,e)}var Df=$.ReactCurrentOwner,Mt=!1;function Ct(e,t,s,a){t.child=e===null?Go(t,null,s,a):Ls(t,e.child,s,a)}function Tc(e,t,s,a,o){s=s.render;var c=t.ref;return Ms(t,o),a=Qi(e,t,s,a,c,o),s=Ui(),e!==null&&!Mt?(t.updateQueue=e.updateQueue,t.flags&=-2053,e.lanes&=~o,wn(e,t,o)):(We&&s&&ki(t),t.flags|=1,Ct(e,t,a,o),t.child)}function Pc(e,t,s,a,o){if(e===null){var c=s.type;return typeof c=="function"&&!jl(c)&&c.defaultProps===void 0&&s.compare===null&&s.defaultProps===void 0?(t.tag=15,t.type=c,Lc(e,t,c,a,o)):(e=Ea(s.type,null,a,t,t.mode,o),e.ref=t.ref,e.return=t,t.child=e)}if(c=e.child,(e.lanes&o)===0){var f=c.memoizedProps;if(s=s.compare,s=s!==null?s:ir,s(f,a)&&e.ref===t.ref)return wn(e,t,o)}return t.flags|=1,e=Wn(c,a),e.ref=t.ref,e.return=t,t.child=e}function Lc(e,t,s,a,o){if(e!==null){var c=e.memoizedProps;if(ir(c,a)&&e.ref===t.ref)if(Mt=!1,t.pendingProps=a=c,(e.lanes&o)!==0)(e.flags&131072)!==0&&(Mt=!0);else return t.lanes=e.lanes,wn(e,t,o)}return Xi(e,t,s,a,o)}function $c(e,t,s){var a=t.pendingProps,o=a.children,c=e!==null?e.memoizedState:null;if(a.mode==="hidden")if((t.mode&1)===0)t.memoizedState={baseLanes:0,cachePool:null,transitions:null},Ve(Bs,qt),qt|=s;else{if((s&1073741824)===0)return e=c!==null?c.baseLanes|s:s,t.lanes=t.childLanes=1073741824,t.memoizedState={baseLanes:e,cachePool:null,transitions:null},t.updateQueue=null,Ve(Bs,qt),qt|=e,null;t.memoizedState={baseLanes:0,cachePool:null,transitions:null},a=c!==null?c.baseLanes:s,Ve(Bs,qt),qt|=a}else c!==null?(a=c.baseLanes|s,t.memoizedState=null):a=s,Ve(Bs,qt),qt|=a;return Ct(e,t,o,s),t.child}function Mc(e,t){var s=t.ref;(e===null&&s!==null||e!==null&&e.ref!==s)&&(t.flags|=512,t.flags|=2097152)}function Xi(e,t,s,a,o){var c=$t(s)?ts:bt.current;return c=Is(t,c),Ms(t,o),s=Qi(e,t,s,a,c,o),a=Ui(),e!==null&&!Mt?(t.updateQueue=e.updateQueue,t.flags&=-2053,e.lanes&=~o,wn(e,t,o)):(We&&a&&ki(t),t.flags|=1,Ct(e,t,s,o),t.child)}function Oc(e,t,s,a,o){if($t(s)){var c=!0;ta(t)}else c=!1;if(Ms(t,o),t.stateNode===null)ja(e,t),wc(t,s,a),Wi(t,s,a,o),a=!0;else if(e===null){var f=t.stateNode,h=t.memoizedProps;f.props=h;var y=f.context,D=s.contextType;typeof D=="object"&&D!==null?D=Ht(D):(D=$t(s)?ts:bt.current,D=Is(t,D));var O=s.getDerivedStateFromProps,F=typeof O=="function"||typeof f.getSnapshotBeforeUpdate=="function";F||typeof f.UNSAFE_componentWillReceiveProps!="function"&&typeof f.componentWillReceiveProps!="function"||(h!==a||y!==D)&&kc(t,f,a,D),Qn=!1;var M=t.memoizedState;f.state=M,ua(t,a,f,o),y=t.memoizedState,h!==a||M!==y||Lt.current||Qn?(typeof O=="function"&&(Hi(t,s,O,a),y=t.memoizedState),(h=Qn||Nc(t,s,h,a,M,y,D))?(F||typeof f.UNSAFE_componentWillMount!="function"&&typeof f.componentWillMount!="function"||(typeof f.componentWillMount=="function"&&f.componentWillMount(),typeof f.UNSAFE_componentWillMount=="function"&&f.UNSAFE_componentWillMount()),typeof f.componentDidMount=="function"&&(t.flags|=4194308)):(typeof f.componentDidMount=="function"&&(t.flags|=4194308),t.memoizedProps=a,t.memoizedState=y),f.props=a,f.state=y,f.context=D,a=h):(typeof f.componentDidMount=="function"&&(t.flags|=4194308),a=!1)}else{f=t.stateNode,Xo(e,t),h=t.memoizedProps,D=t.type===t.elementType?h:tn(t.type,h),f.props=D,F=t.pendingProps,M=f.context,y=s.contextType,typeof y=="object"&&y!==null?y=Ht(y):(y=$t(s)?ts:bt.current,y=Is(t,y));var G=s.getDerivedStateFromProps;(O=typeof G=="function"||typeof f.getSnapshotBeforeUpdate=="function")||typeof f.UNSAFE_componentWillReceiveProps!="function"&&typeof f.componentWillReceiveProps!="function"||(h!==F||M!==y)&&kc(t,f,a,y),Qn=!1,M=t.memoizedState,f.state=M,ua(t,a,f,o);var te=t.memoizedState;h!==F||M!==te||Lt.current||Qn?(typeof G=="function"&&(Hi(t,s,G,a),te=t.memoizedState),(D=Qn||Nc(t,s,D,a,M,te,y)||!1)?(O||typeof f.UNSAFE_componentWillUpdate!="function"&&typeof f.componentWillUpdate!="function"||(typeof f.componentWillUpdate=="function"&&f.componentWillUpdate(a,te,y),typeof f.UNSAFE_componentWillUpdate=="function"&&f.UNSAFE_componentWillUpdate(a,te,y)),typeof f.componentDidUpdate=="function"&&(t.flags|=4),typeof f.getSnapshotBeforeUpdate=="function"&&(t.flags|=1024)):(typeof f.componentDidUpdate!="function"||h===e.memoizedProps&&M===e.memoizedState||(t.flags|=4),typeof f.getSnapshotBeforeUpdate!="function"||h===e.memoizedProps&&M===e.memoizedState||(t.flags|=1024),t.memoizedProps=a,t.memoizedState=te),f.props=a,f.state=te,f.context=y,a=D):(typeof f.componentDidUpdate!="function"||h===e.memoizedProps&&M===e.memoizedState||(t.flags|=4),typeof f.getSnapshotBeforeUpdate!="function"||h===e.memoizedProps&&M===e.memoizedState||(t.flags|=1024),a=!1)}return Zi(e,t,s,a,c,o)}function Zi(e,t,s,a,o,c){Mc(e,t);var f=(t.flags&128)!==0;if(!a&&!f)return o&&zo(t,s,!1),wn(e,t,c);a=t.stateNode,Df.current=t;var h=f&&typeof s.getDerivedStateFromError!="function"?null:a.render();return t.flags|=1,e!==null&&f?(t.child=Ls(t,e.child,null,c),t.child=Ls(t,null,h,c)):Ct(e,t,h,c),t.memoizedState=a.state,o&&zo(t,s,!0),t.child}function Ac(e){var t=e.stateNode;t.pendingContext?Bo(e,t.pendingContext,t.pendingContext!==t.context):t.context&&Bo(e,t.context,!1),Oi(e,t.containerInfo)}function _c(e,t,s,a,o){return Ps(),Ii(o),t.flags|=256,Ct(e,t,s,a),t.child}var el={dehydrated:null,treeContext:null,retryLane:0};function tl(e){return{baseLanes:e,cachePool:null,transitions:null}}function Bc(e,t,s){var a=t.pendingProps,o=Ye.current,c=!1,f=(t.flags&128)!==0,h;if((h=f)||(h=e!==null&&e.memoizedState===null?!1:(o&2)!==0),h?(c=!0,t.flags&=-129):(e===null||e.memoizedState!==null)&&(o|=1),Ve(Ye,o&1),e===null)return Di(t),e=t.memoizedState,e!==null&&(e=e.dehydrated,e!==null)?((t.mode&1)===0?t.lanes=1:e.data==="$!"?t.lanes=8:t.lanes=1073741824,null):(f=a.children,e=a.fallback,c?(a=t.mode,c=t.child,f={mode:"hidden",children:f},(a&1)===0&&c!==null?(c.childLanes=0,c.pendingProps=f):c=Ta(f,a,0,null),e=ds(e,a,s,null),c.return=t,e.return=t,c.sibling=e,t.child=c,t.child.memoizedState=tl(s),t.memoizedState=el,e):nl(t,f));if(o=e.memoizedState,o!==null&&(h=o.dehydrated,h!==null))return If(e,t,f,a,h,o,s);if(c){c=a.fallback,f=t.mode,o=e.child,h=o.sibling;var y={mode:"hidden",children:a.children};return(f&1)===0&&t.child!==o?(a=t.child,a.childLanes=0,a.pendingProps=y,t.deletions=null):(a=Wn(o,y),a.subtreeFlags=o.subtreeFlags&14680064),h!==null?c=Wn(h,c):(c=ds(c,f,s,null),c.flags|=2),c.return=t,a.return=t,a.sibling=c,t.child=a,a=c,c=t.child,f=e.child.memoizedState,f=f===null?tl(s):{baseLanes:f.baseLanes|s,cachePool:null,transitions:f.transitions},c.memoizedState=f,c.childLanes=e.childLanes&~s,t.memoizedState=el,a}return c=e.child,e=c.sibling,a=Wn(c,{mode:"visible",children:a.children}),(t.mode&1)===0&&(a.lanes=s),a.return=t,a.sibling=null,e!==null&&(s=t.deletions,s===null?(t.deletions=[e],t.flags|=16):s.push(e)),t.child=a,t.memoizedState=null,a}function nl(e,t){return t=Ta({mode:"visible",children:t},e.mode,0,null),t.return=e,e.child=t}function ya(e,t,s,a){return a!==null&&Ii(a),Ls(t,e.child,null,s),e=nl(t,t.pendingProps.children),e.flags|=2,t.memoizedState=null,e}function If(e,t,s,a,o,c,f){if(s)return t.flags&256?(t.flags&=-257,a=Gi(Error(l(422))),ya(e,t,f,a)):t.memoizedState!==null?(t.child=e.child,t.flags|=128,null):(c=a.fallback,o=t.mode,a=Ta({mode:"visible",children:a.children},o,0,null),c=ds(c,o,f,null),c.flags|=2,a.return=t,c.return=t,a.sibling=c,t.child=a,(t.mode&1)!==0&&Ls(t,e.child,null,f),t.child.memoizedState=tl(f),t.memoizedState=el,c);if((t.mode&1)===0)return ya(e,t,f,null);if(o.data==="$!"){if(a=o.nextSibling&&o.nextSibling.dataset,a)var h=a.dgst;return a=h,c=Error(l(419)),a=Gi(c,a,void 0),ya(e,t,f,a)}if(h=(f&e.childLanes)!==0,Mt||h){if(a=pt,a!==null){switch(f&-f){case 4:o=2;break;case 16:o=8;break;case 64:case 128:case 256:case 512:case 1024:case 2048:case 4096:case 8192:case 16384:case 32768:case 65536:case 131072:case 262144:case 524288:case 1048576:case 2097152:case 4194304:case 8388608:case 16777216:case 33554432:case 67108864:o=32;break;case 536870912:o=268435456;break;default:o=0}o=(o&(a.suspendedLanes|f))!==0?0:o,o!==0&&o!==c.retryLane&&(c.retryLane=o,Sn(e,o),rn(a,e,o,-1))}return yl(),a=Gi(Error(l(421))),ya(e,t,f,a)}return o.data==="$?"?(t.flags|=128,t.child=e.child,t=Qf.bind(null,e),o._reactRetry=t,null):(e=c.treeContext,Ut=_n(o.nextSibling),Qt=t,We=!0,en=null,e!==null&&(Kt[Jt++]=vn,Kt[Jt++]=bn,Kt[Jt++]=ns,vn=e.id,bn=e.overflow,ns=t),t=nl(t,a.children),t.flags|=4096,t)}function Fc(e,t,s){e.lanes|=t;var a=e.alternate;a!==null&&(a.lanes|=t),Li(e.return,t,s)}function sl(e,t,s,a,o){var c=e.memoizedState;c===null?e.memoizedState={isBackwards:t,rendering:null,renderingStartTime:0,last:a,tail:s,tailMode:o}:(c.isBackwards=t,c.rendering=null,c.renderingStartTime=0,c.last=a,c.tail=s,c.tailMode=o)}function zc(e,t,s){var a=t.pendingProps,o=a.revealOrder,c=a.tail;if(Ct(e,t,a.children,s),a=Ye.current,(a&2)!==0)a=a&1|2,t.flags|=128;else{if(e!==null&&(e.flags&128)!==0)e:for(e=t.child;e!==null;){if(e.tag===13)e.memoizedState!==null&&Fc(e,s,t);else if(e.tag===19)Fc(e,s,t);else if(e.child!==null){e.child.return=e,e=e.child;continue}if(e===t)break e;for(;e.sibling===null;){if(e.return===null||e.return===t)break e;e=e.return}e.sibling.return=e.return,e=e.sibling}a&=1}if(Ve(Ye,a),(t.mode&1)===0)t.memoizedState=null;else switch(o){case"forwards":for(s=t.child,o=null;s!==null;)e=s.alternate,e!==null&&da(e)===null&&(o=s),s=s.sibling;s=o,s===null?(o=t.child,t.child=null):(o=s.sibling,s.sibling=null),sl(t,!1,o,s,c);break;case"backwards":for(s=null,o=t.child,t.child=null;o!==null;){if(e=o.alternate,e!==null&&da(e)===null){t.child=o;break}e=o.sibling,o.sibling=s,s=o,o=e}sl(t,!0,s,null,c);break;case"together":sl(t,!1,null,null,void 0);break;default:t.memoizedState=null}return t.child}function ja(e,t){(t.mode&1)===0&&e!==null&&(e.alternate=null,t.alternate=null,t.flags|=2)}function wn(e,t,s){if(e!==null&&(t.dependencies=e.dependencies),ls|=t.lanes,(s&t.childLanes)===0)return null;if(e!==null&&t.child!==e.child)throw Error(l(153));if(t.child!==null){for(e=t.child,s=Wn(e,e.pendingProps),t.child=s,s.return=t;e.sibling!==null;)e=e.sibling,s=s.sibling=Wn(e,e.pendingProps),s.return=t;s.sibling=null}return t.child}function Ef(e,t,s){switch(t.tag){case 3:Ac(t),Ps();break;case 5:tc(t);break;case 1:$t(t.type)&&ta(t);break;case 4:Oi(t,t.stateNode.containerInfo);break;case 10:var a=t.type._context,o=t.memoizedProps.value;Ve(la,a._currentValue),a._currentValue=o;break;case 13:if(a=t.memoizedState,a!==null)return a.dehydrated!==null?(Ve(Ye,Ye.current&1),t.flags|=128,null):(s&t.child.childLanes)!==0?Bc(e,t,s):(Ve(Ye,Ye.current&1),e=wn(e,t,s),e!==null?e.sibling:null);Ve(Ye,Ye.current&1);break;case 19:if(a=(s&t.childLanes)!==0,(e.flags&128)!==0){if(a)return zc(e,t,s);t.flags|=128}if(o=t.memoizedState,o!==null&&(o.rendering=null,o.tail=null,o.lastEffect=null),Ve(Ye,Ye.current),a)break;return null;case 22:case 23:return t.lanes=0,$c(e,t,s)}return wn(e,t,s)}var Qc,rl,Uc,qc;Qc=function(e,t){for(var s=t.child;s!==null;){if(s.tag===5||s.tag===6)e.appendChild(s.stateNode);else if(s.tag!==4&&s.child!==null){s.child.return=s,s=s.child;continue}if(s===t)break;for(;s.sibling===null;){if(s.return===null||s.return===t)return;s=s.return}s.sibling.return=s.return,s=s.sibling}},rl=function(){},Uc=function(e,t,s,a){var o=e.memoizedProps;if(o!==a){e=t.stateNode,as(dn.current);var c=null;switch(s){case"input":o=fe(e,o),a=fe(e,a),c=[];break;case"select":o=B({},o,{value:void 0}),a=B({},a,{value:void 0}),c=[];break;case"textarea":o=Ze(e,o),a=Ze(e,a),c=[];break;default:typeof o.onClick!="function"&&typeof a.onClick=="function"&&(e.onclick=Xr)}an(s,a);var f;s=null;for(D in o)if(!a.hasOwnProperty(D)&&o.hasOwnProperty(D)&&o[D]!=null)if(D==="style"){var h=o[D];for(f in h)h.hasOwnProperty(f)&&(s||(s={}),s[f]="")}else D!=="dangerouslySetInnerHTML"&&D!=="children"&&D!=="suppressContentEditableWarning"&&D!=="suppressHydrationWarning"&&D!=="autoFocus"&&(d.hasOwnProperty(D)?c||(c=[]):(c=c||[]).push(D,null));for(D in a){var y=a[D];if(h=o!=null?o[D]:void 0,a.hasOwnProperty(D)&&y!==h&&(y!=null||h!=null))if(D==="style")if(h){for(f in h)!h.hasOwnProperty(f)||y&&y.hasOwnProperty(f)||(s||(s={}),s[f]="");for(f in y)y.hasOwnProperty(f)&&h[f]!==y[f]&&(s||(s={}),s[f]=y[f])}else s||(c||(c=[]),c.push(D,s)),s=y;else D==="dangerouslySetInnerHTML"?(y=y?y.__html:void 0,h=h?h.__html:void 0,y!=null&&h!==y&&(c=c||[]).push(D,y)):D==="children"?typeof y!="string"&&typeof y!="number"||(c=c||[]).push(D,""+y):D!=="suppressContentEditableWarning"&&D!=="suppressHydrationWarning"&&(d.hasOwnProperty(D)?(y!=null&&D==="onScroll"&&Ke("scroll",e),c||h===y||(c=[])):(c=c||[]).push(D,y))}s&&(c=c||[]).push("style",s);var D=c;(t.updateQueue=D)&&(t.flags|=4)}},qc=function(e,t,s,a){s!==a&&(t.flags|=4)};function br(e,t){if(!We)switch(e.tailMode){case"hidden":t=e.tail;for(var s=null;t!==null;)t.alternate!==null&&(s=t),t=t.sibling;s===null?e.tail=null:s.sibling=null;break;case"collapsed":s=e.tail;for(var a=null;s!==null;)s.alternate!==null&&(a=s),s=s.sibling;a===null?t||e.tail===null?e.tail=null:e.tail.sibling=null:a.sibling=null}}function Nt(e){var t=e.alternate!==null&&e.alternate.child===e.child,s=0,a=0;if(t)for(var o=e.child;o!==null;)s|=o.lanes|o.childLanes,a|=o.subtreeFlags&14680064,a|=o.flags&14680064,o.return=e,o=o.sibling;else for(o=e.child;o!==null;)s|=o.lanes|o.childLanes,a|=o.subtreeFlags,a|=o.flags,o.return=e,o=o.sibling;return e.subtreeFlags|=a,e.childLanes=s,t}function Tf(e,t,s){var a=t.pendingProps;switch(Ci(t),t.tag){case 2:case 16:case 15:case 0:case 11:case 7:case 8:case 12:case 9:case 14:return Nt(t),null;case 1:return $t(t.type)&&ea(),Nt(t),null;case 3:return a=t.stateNode,Os(),Je(Lt),Je(bt),Bi(),a.pendingContext&&(a.context=a.pendingContext,a.pendingContext=null),(e===null||e.child===null)&&(aa(t)?t.flags|=4:e===null||e.memoizedState.isDehydrated&&(t.flags&256)===0||(t.flags|=1024,en!==null&&(hl(en),en=null))),rl(e,t),Nt(t),null;case 5:Ai(t);var o=as(xr.current);if(s=t.type,e!==null&&t.stateNode!=null)Uc(e,t,s,a,o),e.ref!==t.ref&&(t.flags|=512,t.flags|=2097152);else{if(!a){if(t.stateNode===null)throw Error(l(166));return Nt(t),null}if(e=as(dn.current),aa(t)){a=t.stateNode,s=t.type;var c=t.memoizedProps;switch(a[un]=t,a[dr]=c,e=(t.mode&1)!==0,s){case"dialog":Ke("cancel",a),Ke("close",a);break;case"iframe":case"object":case"embed":Ke("load",a);break;case"video":case"audio":for(o=0;o<\/script>",e=e.removeChild(e.firstChild)):typeof a.is=="string"?e=f.createElement(s,{is:a.is}):(e=f.createElement(s),s==="select"&&(f=e,a.multiple?f.multiple=!0:a.size&&(f.size=a.size))):e=f.createElementNS(e,s),e[un]=t,e[dr]=a,Qc(e,t,!1,!1),t.stateNode=e;e:{switch(f=dt(s,a),s){case"dialog":Ke("cancel",e),Ke("close",e),o=a;break;case"iframe":case"object":case"embed":Ke("load",e),o=a;break;case"video":case"audio":for(o=0;oFs&&(t.flags|=128,a=!0,br(c,!1),t.lanes=4194304)}else{if(!a)if(e=da(f),e!==null){if(t.flags|=128,a=!0,s=e.updateQueue,s!==null&&(t.updateQueue=s,t.flags|=4),br(c,!0),c.tail===null&&c.tailMode==="hidden"&&!f.alternate&&!We)return Nt(t),null}else 2*nt()-c.renderingStartTime>Fs&&s!==1073741824&&(t.flags|=128,a=!0,br(c,!1),t.lanes=4194304);c.isBackwards?(f.sibling=t.child,t.child=f):(s=c.last,s!==null?s.sibling=f:t.child=f,c.last=f)}return c.tail!==null?(t=c.tail,c.rendering=t,c.tail=t.sibling,c.renderingStartTime=nt(),t.sibling=null,s=Ye.current,Ve(Ye,a?s&1|2:s&1),t):(Nt(t),null);case 22:case 23:return gl(),a=t.memoizedState!==null,e!==null&&e.memoizedState!==null!==a&&(t.flags|=8192),a&&(t.mode&1)!==0?(qt&1073741824)!==0&&(Nt(t),t.subtreeFlags&6&&(t.flags|=8192)):Nt(t),null;case 24:return null;case 25:return null}throw Error(l(156,t.tag))}function Pf(e,t){switch(Ci(t),t.tag){case 1:return $t(t.type)&&ea(),e=t.flags,e&65536?(t.flags=e&-65537|128,t):null;case 3:return Os(),Je(Lt),Je(bt),Bi(),e=t.flags,(e&65536)!==0&&(e&128)===0?(t.flags=e&-65537|128,t):null;case 5:return Ai(t),null;case 13:if(Je(Ye),e=t.memoizedState,e!==null&&e.dehydrated!==null){if(t.alternate===null)throw Error(l(340));Ps()}return e=t.flags,e&65536?(t.flags=e&-65537|128,t):null;case 19:return Je(Ye),null;case 4:return Os(),null;case 10:return Pi(t.type._context),null;case 22:case 23:return gl(),null;case 24:return null;default:return null}}var va=!1,wt=!1,Lf=typeof WeakSet=="function"?WeakSet:Set,X=null;function _s(e,t){var s=e.ref;if(s!==null)if(typeof s=="function")try{s(null)}catch(a){et(e,t,a)}else s.current=null}function al(e,t,s){try{s()}catch(a){et(e,t,a)}}var Vc=!1;function $f(e,t){if(gi=Fr,e=No(),ci(e)){if("selectionStart"in e)var s={start:e.selectionStart,end:e.selectionEnd};else e:{s=(s=e.ownerDocument)&&s.defaultView||window;var a=s.getSelection&&s.getSelection();if(a&&a.rangeCount!==0){s=a.anchorNode;var o=a.anchorOffset,c=a.focusNode;a=a.focusOffset;try{s.nodeType,c.nodeType}catch{s=null;break e}var f=0,h=-1,y=-1,D=0,O=0,F=e,M=null;t:for(;;){for(var G;F!==s||o!==0&&F.nodeType!==3||(h=f+o),F!==c||a!==0&&F.nodeType!==3||(y=f+a),F.nodeType===3&&(f+=F.nodeValue.length),(G=F.firstChild)!==null;)M=F,F=G;for(;;){if(F===e)break t;if(M===s&&++D===o&&(h=f),M===c&&++O===a&&(y=f),(G=F.nextSibling)!==null)break;F=M,M=F.parentNode}F=G}s=h===-1||y===-1?null:{start:h,end:y}}else s=null}s=s||{start:0,end:0}}else s=null;for(yi={focusedElem:e,selectionRange:s},Fr=!1,X=t;X!==null;)if(t=X,e=t.child,(t.subtreeFlags&1028)!==0&&e!==null)e.return=t,X=e;else for(;X!==null;){t=X;try{var te=t.alternate;if((t.flags&1024)!==0)switch(t.tag){case 0:case 11:case 15:break;case 1:if(te!==null){var ae=te.memoizedProps,st=te.memoizedState,k=t.stateNode,b=k.getSnapshotBeforeUpdate(t.elementType===t.type?ae:tn(t.type,ae),st);k.__reactInternalSnapshotBeforeUpdate=b}break;case 3:var R=t.stateNode.containerInfo;R.nodeType===1?R.textContent="":R.nodeType===9&&R.documentElement&&R.removeChild(R.documentElement);break;case 5:case 6:case 4:case 17:break;default:throw Error(l(163))}}catch(Q){et(t,t.return,Q)}if(e=t.sibling,e!==null){e.return=t.return,X=e;break}X=t.return}return te=Vc,Vc=!1,te}function Sr(e,t,s){var a=t.updateQueue;if(a=a!==null?a.lastEffect:null,a!==null){var o=a=a.next;do{if((o.tag&e)===e){var c=o.destroy;o.destroy=void 0,c!==void 0&&al(t,s,c)}o=o.next}while(o!==a)}}function ba(e,t){if(t=t.updateQueue,t=t!==null?t.lastEffect:null,t!==null){var s=t=t.next;do{if((s.tag&e)===e){var a=s.create;s.destroy=a()}s=s.next}while(s!==t)}}function il(e){var t=e.ref;if(t!==null){var s=e.stateNode;switch(e.tag){case 5:e=s;break;default:e=s}typeof t=="function"?t(e):t.current=e}}function Kc(e){var t=e.alternate;t!==null&&(e.alternate=null,Kc(t)),e.child=null,e.deletions=null,e.sibling=null,e.tag===5&&(t=e.stateNode,t!==null&&(delete t[un],delete t[dr],delete t[Si],delete t[xf],delete t[gf])),e.stateNode=null,e.return=null,e.dependencies=null,e.memoizedProps=null,e.memoizedState=null,e.pendingProps=null,e.stateNode=null,e.updateQueue=null}function Jc(e){return e.tag===5||e.tag===3||e.tag===4}function Hc(e){e:for(;;){for(;e.sibling===null;){if(e.return===null||Jc(e.return))return null;e=e.return}for(e.sibling.return=e.return,e=e.sibling;e.tag!==5&&e.tag!==6&&e.tag!==18;){if(e.flags&2||e.child===null||e.tag===4)continue e;e.child.return=e,e=e.child}if(!(e.flags&2))return e.stateNode}}function ll(e,t,s){var a=e.tag;if(a===5||a===6)e=e.stateNode,t?s.nodeType===8?s.parentNode.insertBefore(e,t):s.insertBefore(e,t):(s.nodeType===8?(t=s.parentNode,t.insertBefore(e,s)):(t=s,t.appendChild(e)),s=s._reactRootContainer,s!=null||t.onclick!==null||(t.onclick=Xr));else if(a!==4&&(e=e.child,e!==null))for(ll(e,t,s),e=e.sibling;e!==null;)ll(e,t,s),e=e.sibling}function ol(e,t,s){var a=e.tag;if(a===5||a===6)e=e.stateNode,t?s.insertBefore(e,t):s.appendChild(e);else if(a!==4&&(e=e.child,e!==null))for(ol(e,t,s),e=e.sibling;e!==null;)ol(e,t,s),e=e.sibling}var gt=null,nn=!1;function qn(e,t,s){for(s=s.child;s!==null;)Wc(e,t,s),s=s.sibling}function Wc(e,t,s){if(cn&&typeof cn.onCommitFiberUnmount=="function")try{cn.onCommitFiberUnmount($r,s)}catch{}switch(s.tag){case 5:wt||_s(s,t);case 6:var a=gt,o=nn;gt=null,qn(e,t,s),gt=a,nn=o,gt!==null&&(nn?(e=gt,s=s.stateNode,e.nodeType===8?e.parentNode.removeChild(s):e.removeChild(s)):gt.removeChild(s.stateNode));break;case 18:gt!==null&&(nn?(e=gt,s=s.stateNode,e.nodeType===8?bi(e.parentNode,s):e.nodeType===1&&bi(e,s),er(e)):bi(gt,s.stateNode));break;case 4:a=gt,o=nn,gt=s.stateNode.containerInfo,nn=!0,qn(e,t,s),gt=a,nn=o;break;case 0:case 11:case 14:case 15:if(!wt&&(a=s.updateQueue,a!==null&&(a=a.lastEffect,a!==null))){o=a=a.next;do{var c=o,f=c.destroy;c=c.tag,f!==void 0&&((c&2)!==0||(c&4)!==0)&&al(s,t,f),o=o.next}while(o!==a)}qn(e,t,s);break;case 1:if(!wt&&(_s(s,t),a=s.stateNode,typeof a.componentWillUnmount=="function"))try{a.props=s.memoizedProps,a.state=s.memoizedState,a.componentWillUnmount()}catch(h){et(s,t,h)}qn(e,t,s);break;case 21:qn(e,t,s);break;case 22:s.mode&1?(wt=(a=wt)||s.memoizedState!==null,qn(e,t,s),wt=a):qn(e,t,s);break;default:qn(e,t,s)}}function Gc(e){var t=e.updateQueue;if(t!==null){e.updateQueue=null;var s=e.stateNode;s===null&&(s=e.stateNode=new Lf),t.forEach(function(a){var o=Uf.bind(null,e,a);s.has(a)||(s.add(a),a.then(o,o))})}}function sn(e,t){var s=t.deletions;if(s!==null)for(var a=0;ao&&(o=f),a&=~c}if(a=o,a=nt()-a,a=(120>a?120:480>a?480:1080>a?1080:1920>a?1920:3e3>a?3e3:4320>a?4320:1960*Of(a/1960))-a,10e?16:e,Kn===null)var a=!1;else{if(e=Kn,Kn=null,Ca=0,(Me&6)!==0)throw Error(l(331));var o=Me;for(Me|=4,X=e.current;X!==null;){var c=X,f=c.child;if((X.flags&16)!==0){var h=c.deletions;if(h!==null){for(var y=0;ynt()-dl?cs(e,0):ul|=s),At(e,t)}function cu(e,t){t===0&&((e.mode&1)===0?t=1:(t=Or,Or<<=1,(Or&130023424)===0&&(Or=4194304)));var s=Rt();e=Sn(e,t),e!==null&&(Ws(e,t,s),At(e,s))}function Qf(e){var t=e.memoizedState,s=0;t!==null&&(s=t.retryLane),cu(e,s)}function Uf(e,t){var s=0;switch(e.tag){case 13:var a=e.stateNode,o=e.memoizedState;o!==null&&(s=o.retryLane);break;case 19:a=e.stateNode;break;default:throw Error(l(314))}a!==null&&a.delete(t),cu(e,s)}var uu;uu=function(e,t,s){if(e!==null)if(e.memoizedProps!==t.pendingProps||Lt.current)Mt=!0;else{if((e.lanes&s)===0&&(t.flags&128)===0)return Mt=!1,Ef(e,t,s);Mt=(e.flags&131072)!==0}else Mt=!1,We&&(t.flags&1048576)!==0&&Uo(t,ra,t.index);switch(t.lanes=0,t.tag){case 2:var a=t.type;ja(e,t),e=t.pendingProps;var o=Is(t,bt.current);Ms(t,s),o=Qi(null,t,a,e,o,s);var c=Ui();return t.flags|=1,typeof o=="object"&&o!==null&&typeof o.render=="function"&&o.$$typeof===void 0?(t.tag=1,t.memoizedState=null,t.updateQueue=null,$t(a)?(c=!0,ta(t)):c=!1,t.memoizedState=o.state!==null&&o.state!==void 0?o.state:null,Mi(t),o.updater=ga,t.stateNode=o,o._reactInternals=t,Wi(t,a,e,s),t=Zi(null,t,a,!0,c,s)):(t.tag=0,We&&c&&ki(t),Ct(null,t,o,s),t=t.child),t;case 16:a=t.elementType;e:{switch(ja(e,t),e=t.pendingProps,o=a._init,a=o(a._payload),t.type=a,o=t.tag=Vf(a),e=tn(a,e),o){case 0:t=Xi(null,t,a,e,s);break e;case 1:t=Oc(null,t,a,e,s);break e;case 11:t=Tc(null,t,a,e,s);break e;case 14:t=Pc(null,t,a,tn(a.type,e),s);break e}throw Error(l(306,a,""))}return t;case 0:return a=t.type,o=t.pendingProps,o=t.elementType===a?o:tn(a,o),Xi(e,t,a,o,s);case 1:return a=t.type,o=t.pendingProps,o=t.elementType===a?o:tn(a,o),Oc(e,t,a,o,s);case 3:e:{if(Ac(t),e===null)throw Error(l(387));a=t.pendingProps,c=t.memoizedState,o=c.element,Xo(e,t),ua(t,a,null,s);var f=t.memoizedState;if(a=f.element,c.isDehydrated)if(c={element:a,isDehydrated:!1,cache:f.cache,pendingSuspenseBoundaries:f.pendingSuspenseBoundaries,transitions:f.transitions},t.updateQueue.baseState=c,t.memoizedState=c,t.flags&256){o=As(Error(l(423)),t),t=_c(e,t,a,s,o);break e}else if(a!==o){o=As(Error(l(424)),t),t=_c(e,t,a,s,o);break e}else for(Ut=_n(t.stateNode.containerInfo.firstChild),Qt=t,We=!0,en=null,s=Go(t,null,a,s),t.child=s;s;)s.flags=s.flags&-3|4096,s=s.sibling;else{if(Ps(),a===o){t=wn(e,t,s);break e}Ct(e,t,a,s)}t=t.child}return t;case 5:return tc(t),e===null&&Di(t),a=t.type,o=t.pendingProps,c=e!==null?e.memoizedProps:null,f=o.children,ji(a,o)?f=null:c!==null&&ji(a,c)&&(t.flags|=32),Mc(e,t),Ct(e,t,f,s),t.child;case 6:return e===null&&Di(t),null;case 13:return Bc(e,t,s);case 4:return Oi(t,t.stateNode.containerInfo),a=t.pendingProps,e===null?t.child=Ls(t,null,a,s):Ct(e,t,a,s),t.child;case 11:return a=t.type,o=t.pendingProps,o=t.elementType===a?o:tn(a,o),Tc(e,t,a,o,s);case 7:return Ct(e,t,t.pendingProps,s),t.child;case 8:return Ct(e,t,t.pendingProps.children,s),t.child;case 12:return Ct(e,t,t.pendingProps.children,s),t.child;case 10:e:{if(a=t.type._context,o=t.pendingProps,c=t.memoizedProps,f=o.value,Ve(la,a._currentValue),a._currentValue=f,c!==null)if(Zt(c.value,f)){if(c.children===o.children&&!Lt.current){t=wn(e,t,s);break e}}else for(c=t.child,c!==null&&(c.return=t);c!==null;){var h=c.dependencies;if(h!==null){f=c.child;for(var y=h.firstContext;y!==null;){if(y.context===a){if(c.tag===1){y=Nn(-1,s&-s),y.tag=2;var D=c.updateQueue;if(D!==null){D=D.shared;var O=D.pending;O===null?y.next=y:(y.next=O.next,O.next=y),D.pending=y}}c.lanes|=s,y=c.alternate,y!==null&&(y.lanes|=s),Li(c.return,s,t),h.lanes|=s;break}y=y.next}}else if(c.tag===10)f=c.type===t.type?null:c.child;else if(c.tag===18){if(f=c.return,f===null)throw Error(l(341));f.lanes|=s,h=f.alternate,h!==null&&(h.lanes|=s),Li(f,s,t),f=c.sibling}else f=c.child;if(f!==null)f.return=c;else for(f=c;f!==null;){if(f===t){f=null;break}if(c=f.sibling,c!==null){c.return=f.return,f=c;break}f=f.return}c=f}Ct(e,t,o.children,s),t=t.child}return t;case 9:return o=t.type,a=t.pendingProps.children,Ms(t,s),o=Ht(o),a=a(o),t.flags|=1,Ct(e,t,a,s),t.child;case 14:return a=t.type,o=tn(a,t.pendingProps),o=tn(a.type,o),Pc(e,t,a,o,s);case 15:return Lc(e,t,t.type,t.pendingProps,s);case 17:return a=t.type,o=t.pendingProps,o=t.elementType===a?o:tn(a,o),ja(e,t),t.tag=1,$t(a)?(e=!0,ta(t)):e=!1,Ms(t,s),wc(t,a,o),Wi(t,a,o,s),Zi(null,t,a,!0,e,s);case 19:return zc(e,t,s);case 22:return $c(e,t,s)}throw Error(l(156,t.tag))};function du(e,t){return Js(e,t)}function qf(e,t,s,a){this.tag=e,this.key=s,this.sibling=this.child=this.return=this.stateNode=this.type=this.elementType=null,this.index=0,this.ref=null,this.pendingProps=t,this.dependencies=this.memoizedState=this.updateQueue=this.memoizedProps=null,this.mode=a,this.subtreeFlags=this.flags=0,this.deletions=null,this.childLanes=this.lanes=0,this.alternate=null}function Yt(e,t,s,a){return new qf(e,t,s,a)}function jl(e){return e=e.prototype,!(!e||!e.isReactComponent)}function Vf(e){if(typeof e=="function")return jl(e)?1:0;if(e!=null){if(e=e.$$typeof,e===he)return 11;if(e===be)return 14}return 2}function Wn(e,t){var s=e.alternate;return s===null?(s=Yt(e.tag,t,e.key,e.mode),s.elementType=e.elementType,s.type=e.type,s.stateNode=e.stateNode,s.alternate=e,e.alternate=s):(s.pendingProps=t,s.type=e.type,s.flags=0,s.subtreeFlags=0,s.deletions=null),s.flags=e.flags&14680064,s.childLanes=e.childLanes,s.lanes=e.lanes,s.child=e.child,s.memoizedProps=e.memoizedProps,s.memoizedState=e.memoizedState,s.updateQueue=e.updateQueue,t=e.dependencies,s.dependencies=t===null?null:{lanes:t.lanes,firstContext:t.firstContext},s.sibling=e.sibling,s.index=e.index,s.ref=e.ref,s}function Ea(e,t,s,a,o,c){var f=2;if(a=e,typeof e=="function")jl(e)&&(f=1);else if(typeof e=="string")f=5;else e:switch(e){case Z:return ds(s.children,o,c,t);case Re:f=8,o|=8;break;case ee:return e=Yt(12,s,t,o|2),e.elementType=ee,e.lanes=c,e;case z:return e=Yt(13,s,t,o),e.elementType=z,e.lanes=c,e;case H:return e=Yt(19,s,t,o),e.elementType=H,e.lanes=c,e;case Ne:return Ta(s,o,c,t);default:if(typeof e=="object"&&e!==null)switch(e.$$typeof){case pe:f=10;break e;case Se:f=9;break e;case he:f=11;break e;case be:f=14;break e;case Pe:f=16,a=null;break e}throw Error(l(130,e==null?e:typeof e,""))}return t=Yt(f,s,t,o),t.elementType=e,t.type=a,t.lanes=c,t}function ds(e,t,s,a){return e=Yt(7,e,a,t),e.lanes=s,e}function Ta(e,t,s,a){return e=Yt(22,e,a,t),e.elementType=Ne,e.lanes=s,e.stateNode={isHidden:!1},e}function vl(e,t,s){return e=Yt(6,e,null,t),e.lanes=s,e}function bl(e,t,s){return t=Yt(4,e.children!==null?e.children:[],e.key,t),t.lanes=s,t.stateNode={containerInfo:e.containerInfo,pendingChildren:null,implementation:e.implementation},t}function Kf(e,t,s,a,o){this.tag=t,this.containerInfo=e,this.finishedWork=this.pingCache=this.current=this.pendingChildren=null,this.timeoutHandle=-1,this.callbackNode=this.pendingContext=this.context=null,this.callbackPriority=0,this.eventTimes=Ha(0),this.expirationTimes=Ha(-1),this.entangledLanes=this.finishedLanes=this.mutableReadLanes=this.expiredLanes=this.pingedLanes=this.suspendedLanes=this.pendingLanes=0,this.entanglements=Ha(0),this.identifierPrefix=a,this.onRecoverableError=o,this.mutableSourceEagerHydrationData=null}function Sl(e,t,s,a,o,c,f,h,y){return e=new Kf(e,t,s,h,y),t===1?(t=1,c===!0&&(t|=8)):t=0,c=Yt(3,null,null,t),e.current=c,c.stateNode=e,c.memoizedState={element:a,isDehydrated:s,cache:null,transitions:null,pendingSuspenseBoundaries:null},Mi(c),e}function Jf(e,t,s){var a=3"u"||typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE!="function"))try{__REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE(r)}catch(i){console.error(i)}}return r(),Dl.exports=lp(),Dl.exports}var ku;function cp(){if(ku)return _a;ku=1;var r=op();return _a.createRoot=r.createRoot,_a.hydrateRoot=r.hydrateRoot,_a}var up=cp();const dp=zu(up);class Ba extends Error{constructor(i,l,u){super(i),this.code=l,this.status=u,this.name="DashboardRequestError"}}const Ml=new Set;let Tr;function fp(){return Tr}function pp(r){return Ml.add(r),Tr&&r(Tr),()=>{Ml.delete(r)}}async function q(r,i={}){return Fl(r,l=>l.json(),i)}async function mp(r,i={}){return Fl(r,l=>l.text(),i)}async function mn(r,i={}){return Fl(r,async()=>{},i)}async function Fl(r,i,l={}){const u=l.timeoutMs??5e3,d=l.method??"GET",p=new AbortController,x=window.setTimeout(()=>p.abort(),u),g=new Headers(l.headers);g.has("Accept")||g.set("Accept","application/json"),l.body!==void 0&&!g.has("Content-Type")&&g.set("Content-Type","application/json");const S=l.rawBody??(l.body===void 0?void 0:JSON.stringify(l.body));try{Dr({path:r,status:"pending"});const v=await fetch(r,{headers:g,body:S,method:d,signal:p.signal});if(!v.ok){const w=await hp(v);throw Dr({path:r,status:"error",statusCode:v.status,message:w}),new Ba(w,v.status===503?"disabled":"http",v.status)}const N=await i(v);return Dr({path:r,status:"success",statusCode:v.status}),N}catch(v){if(v instanceof Ba)throw v;if(v instanceof DOMException&&v.name==="AbortError")throw Dr({path:r,status:"error",message:"Dashboard request timed out"}),new Ba("Dashboard request timed out","timeout");const N=v instanceof Error?v.message:"Dashboard request failed";throw Dr({path:r,status:"error",message:N}),new Ba(N,"network")}finally{window.clearTimeout(x)}}function Dr(r){Tr={...r,at:new Date().toISOString()},Ml.forEach(i=>i(Tr))}async function hp(r){if((r.headers.get("Content-Type")??"").includes("application/json")){const u=await r.json().catch(()=>{}),d=(u==null?void 0:u.error)??(u==null?void 0:u.message);if(typeof d=="string"&&d.trim()!=="")return d}const l=await r.text().catch(()=>"");return l.trim()!==""?l.trim():`Dashboard request failed with status ${r.status}`}async function xp(r=5e3){return q("/api/dashboard/services",{timeoutMs:r})}let Yn=Qu();function Qu(){return{status:"pending",promise:xp().then(i=>{Yn={status:"success",value:i}}).catch(i=>{Yn={status:"error",error:i}})}}function gp(){if(Yn.status==="pending")throw Yn.promise;if(Yn.status==="error")throw Yn.error;return Yn.value}function yp(){Yn=Qu()}function ne({children:r,className:i,type:l="button",...u}){return n.jsx("button",{className:i?`button ${i}`:"button",type:l,...u,children:r})}function A({title:r,description:i,actionLabel:l,onAction:u}){return n.jsxs("div",{className:"empty-state",children:[n.jsx("strong",{children:r}),n.jsx("p",{children:i}),l&&u?n.jsx(ne,{onClick:u,children:l}):null]})}function ue({title:r,children:i}){return n.jsxs("section",{className:"panel",children:[r?n.jsx("header",{className:"panel-head",children:n.jsx("h2",{children:r})}):null,n.jsx("div",{className:"panel-body",children:i})]})}const Cn="/dashboard";function Er(r){return r===Cn||r===`${Cn}/`?"/":r.startsWith(`${Cn}/`)?r.slice(Cn.length)||"/":r}function za(r,i=window.location.pathname){return r===Cn||r.startsWith(`${Cn}/`)||!i.startsWith(Cn)?r:r==="/"?`${Cn}/`:`${Cn}${r}`}function jp({services:r}){const i=r.filter(u=>u.status==="running").length,l=r.filter(u=>u.status==="disabled").length;return r.length===0?n.jsx("section",{className:"service-index-page","aria-label":"Services",children:n.jsx(A,{title:"No dashboard services",description:"Configured services will appear here when they are available."})}):n.jsxs("section",{className:"service-index-page","aria-label":"Services",children:[n.jsxs("header",{className:"service-index-summary",children:[n.jsxs("div",{children:[n.jsx("h1",{children:"Services"}),n.jsx("p",{children:"Local dashboard registry from /api/dashboard/services."})]}),n.jsxs("dl",{className:"health-summary","aria-label":"Service health summary",children:[n.jsxs("div",{children:[n.jsx("dt",{children:"Running"}),n.jsx("dd",{children:i})]}),n.jsxs("div",{children:[n.jsx("dt",{children:"Disabled"}),n.jsx("dd",{children:l})]}),n.jsxs("div",{children:[n.jsx("dt",{children:"Total"}),n.jsx("dd",{children:r.length})]})]})]}),n.jsx("div",{className:"service-index",children:r.map(u=>n.jsx("a",{className:"service-card",href:za(u.path),children:n.jsxs(ue,{children:[n.jsxs("div",{className:"service-card-head",children:[n.jsx("h2",{children:u.name}),n.jsx("span",{className:`service-status ${u.status}`,children:u.status})]}),n.jsx("p",{children:u.description}),u.endpoint?n.jsxs("dl",{className:"endpoint-summary",children:[n.jsx("dt",{children:"Endpoint"}),n.jsx("dd",{children:n.jsx("code",{children:u.endpoint})})]}):null]})},u.id))})]})}const Uu=m.createContext(null);function vp({children:r}){const[i,l]=m.useState(null),u=m.useCallback(p=>new Promise(x=>{l({options:p,resolve:x})}),[]),d=m.useCallback(p=>{l(x=>(x==null||x.resolve(p),null))},[]);return n.jsxs(Uu.Provider,{value:u,children:[r,i?n.jsx(bp,{options:i.options,onCancel:()=>d(!1),onConfirm:()=>d(!0)}):null]})}function Pr(){const r=m.useContext(Uu);if(!r)throw new Error("useConfirm must be called inside ");return r}function bp({options:r,onCancel:i,onConfirm:l}){const u=m.useId(),d=m.useId(),p=m.useId(),x=r.confirmLabel??"Confirm",g=r.cancelLabel??"Cancel",S=r.tone??"default",v=r.requirePhrase??"",[N,w]=m.useState(""),L=m.useRef(null),C=v===""||N===v;m.useEffect(()=>{w("")},[r]),m.useEffect(()=>{v!==""&&L.current&&L.current.focus()},[v]),m.useEffect(()=>{function I(E){if(E.key==="Escape")E.preventDefault(),i();else if(E.key==="Enter"&&C){const J=E.target;if((J==null?void 0:J.tagName)==="TEXTAREA")return;E.preventDefault(),l()}}return document.addEventListener("keydown",I),()=>document.removeEventListener("keydown",I)},[i,l,C]);function _(I){I.target===I.currentTarget&&i()}return n.jsx("div",{className:"confirm-backdrop",onMouseDown:_,role:"presentation",children:n.jsxs("section",{"aria-describedby":r.description||r.detail?d:void 0,"aria-labelledby":u,"aria-modal":"true",className:`confirm-dialog tone-${S}`,role:"alertdialog",children:[n.jsx("h2",{className:"confirm-title",id:u,children:r.title}),r.description||r.detail?n.jsxs("div",{className:"confirm-body",id:d,children:[r.description?n.jsx("p",{className:"confirm-description",children:r.description}):null,r.detail?n.jsx("p",{className:"confirm-detail",children:r.detail}):null]}):null,v!==""?n.jsxs("div",{className:"confirm-phrase",children:[n.jsx("label",{htmlFor:p,children:r.requirePhraseLabel??n.jsxs(n.Fragment,{children:["Type ",n.jsx("code",{children:v})," to confirm"]})}),n.jsx("input",{autoComplete:"off",autoCorrect:"off",className:"confirm-phrase-input",id:p,onChange:I=>w(I.target.value),ref:L,spellCheck:!1,type:"text",value:N})]}):null,n.jsxs("div",{className:"confirm-actions",children:[n.jsx(ne,{onClick:i,type:"button",children:g}),n.jsx(ne,{autoFocus:v==="",className:S==="danger"?"danger primary":"primary",disabled:!C,onClick:l,type:"button",children:x})]})]})})}function Rn(r){return{title:r.title,description:r.description,detail:n.jsxs(n.Fragment,{children:["Target: ",n.jsx("code",{children:r.target})]}),confirmLabel:r.confirmLabel??"Delete",tone:"danger",requirePhrase:r.target}}const Cu=[1e3,2e3,4e3,8e3,16e3,3e4];function Sp(){return`${window.location.protocol==="https:"?"wss:":"ws:"}//${window.location.host}/api/events`}class Np{constructor(){fs(this,"ws",null);fs(this,"listeners",new Map);fs(this,"nextID",0);fs(this,"retryIndex",0);fs(this,"retryTimeout",null)}subscribe(i,l,u){const d=this.nextID;this.nextID+=1,this.listeners.set(d,{topics:new Set(i),cb:l,onResync:u}),this.ensureOpen();let p=!1;return()=>{p||(p=!0,this.listeners.delete(d),this.listeners.size===0&&this.close())}}ensureOpen(){this.ws||this.retryTimeout!==null||this.connect()}connect(){if(typeof WebSocket>"u")return;let i;try{i=new WebSocket(Sp())}catch{this.scheduleReconnect();return}this.ws=i,i.onopen=()=>{this.ws===i&&(this.retryIndex=0)},i.onmessage=u=>{if(!(this.ws!==i||typeof u.data!="string"))try{const d=JSON.parse(u.data);if(!Ol(d))return;d.type==="event"&&wp(d.event)?this.dispatch(d.event):d.type==="hello"?this.resync():d.type==="resync"&&this.resync()}catch{}};const l=()=>{if(this.ws===i){this.ws=null,this.detach(i);try{i.close()}catch{}this.listeners.size!==0&&this.scheduleReconnect()}};i.onerror=l,i.onclose=l}scheduleReconnect(){if(this.retryTimeout!==null)return;const i=Cu[Math.min(this.retryIndex,Cu.length-1)];this.retryIndex+=1,this.retryTimeout=window.setTimeout(()=>{this.retryTimeout=null,this.listeners.size>0&&!this.ws&&this.connect()},i)}dispatch(i){for(const l of this.listeners.values())if(!(l.topics.size>0&&!l.topics.has(i.service)))try{l.cb(i)}catch{}}resync(){var i;for(const l of this.listeners.values())try{(i=l.onResync)==null||i.call(l)}catch{}}detach(i){i.onopen=null,i.onmessage=null,i.onerror=null,i.onclose=null}close(){if(this.retryTimeout!==null&&(window.clearTimeout(this.retryTimeout),this.retryTimeout=null),this.ws){this.detach(this.ws);try{this.ws.close(1e3,"no listeners")}catch{}}this.ws=null,this.retryIndex=0}}function Ol(r){return typeof r=="object"&&r!==null&&!Array.isArray(r)}function wp(r){return Ol(r)&&typeof r.type=="string"&&typeof r.service=="string"&&typeof r.timestamp=="string"&&(r.payload===void 0||Ol(r.payload))}const kp=new Np;function Cp(r,i,l){return kp.subscribe(r,i,l)}function Vt({topics:r,onEvent:i,onResync:l,enabled:u=!0}){const d=m.useRef(i),p=m.useRef(l);m.useEffect(()=>{d.current=i,p.current=l},[i,l]);const x=r.slice().sort().join(",");m.useEffect(()=>{if(!u)return;const g=x?x.split(","):[];return Cp(g,v=>d.current(v),()=>{var v;return(v=p.current)==null?void 0:v.call(p)})},[x,u])}function ht(){const r=m.useRef(0),i=m.useRef(!0);return m.useEffect(()=>(i.current=!0,()=>{i.current=!1,r.current+=1}),[]),m.useCallback(()=>{const l=++r.current;return()=>i.current&&r.current===l},[])}async function Rp(){return q("/api/messages")}async function Dp(r){return q(`/api/messages/${encodeURIComponent(r)}`)}async function Ip(r){return mp(`/api/messages/${encodeURIComponent(r)}/raw`)}async function Ep(){return mn("/api/messages",{method:"DELETE"})}function ms(r){return!r||r.indexOf("=?")===-1?r:r.replace(/=\?([^?]+)\?([BbQq])\?([^?]*)\?=(\s+(?==\?))?/g,(i,l,u,d)=>{try{const p=u.toUpperCase()==="B"?Tp(d):Pp(d);return new TextDecoder(Lp(l)).decode(p)}catch{return i}})}function Dn(r){return r?ms(r).trim():""}function Tp(r){const i=atob(r.replace(/\s+/g,"")),l=new Uint8Array(i.length);for(let u=0;u{const I=v();if(S){u({status:"success",messages:[]});return}u({status:"loading"}),Rp().then(({messages:E})=>{I()&&(u({status:"success",messages:E}),p(J=>{var de;return J??((de=E[0])==null?void 0:de.id)}))}).catch(E=>{I()&&u({status:"error",message:E.message})})},[v,S]);Vt({topics:["mail"],onEvent:N,onResync:N,enabled:!S});async function w(){S||!await i(Rn({title:"Clear inbox",description:"Delete all stored Mail messages from this local devcloud inbox.",target:"CLEAR INBOX",confirmLabel:"Clear inbox"}))||(u({status:"loading"}),Ep().then(()=>{p(void 0),u({status:"success",messages:[]})}).catch(E=>{u({status:"error",message:E.message})}))}m.useEffect(()=>{N()},[N]);const L=l.status==="success"?l.messages:[],C=m.useMemo(()=>zp(L,x),[L,x]),_=C.find(I=>I.id===d)??C[0]??(x.trim()===""?L[0]:void 0);return S?n.jsx(ue,{title:"Mail",children:n.jsx(A,{title:"Mail is disabled",description:"Enable services.mail.enabled in .devcloud/config.yaml to inspect received messages."})}):n.jsxs("div",{className:"mail-shell",children:[n.jsxs(ue,{title:"Inbox",children:[n.jsxs("div",{className:"mail-inbox-toolbar",children:[n.jsxs("label",{className:"mail-inbox-filter",children:[n.jsx("span",{children:"Filter"}),n.jsx("input",{"aria-label":"Filter messages",onChange:I=>g(I.target.value),placeholder:"sender, recipient, subject",type:"search",value:x})]}),n.jsxs("div",{className:"mail-inbox-actions",children:[n.jsx(ne,{onClick:N,children:"Refresh"}),n.jsx(ne,{className:"danger",disabled:L.length===0,onClick:w,children:"Clear all"})]})]}),l.status==="loading"?n.jsx(A,{title:"Loading messages",description:"Reading the local Mail inbox."}):null,l.status==="error"?n.jsx(A,{title:"Mail messages unavailable",description:l.message,actionLabel:"Retry",onAction:N}):null,l.status==="success"?n.jsx(Mp,{messages:C,selectedID:_==null?void 0:_.id,totalMessages:L.length,onSelectMessage:p}):null]}),n.jsx(ue,{title:"Message",children:n.jsx(Op,{message:_})})]})}function Mp({messages:r,selectedID:i,totalMessages:l,onSelectMessage:u}){return l===0?n.jsx(A,{title:"Inbox is empty",description:"Send mail to localhost:11025 (relaxed SMTP AUTH) and refresh."}):r.length===0?n.jsx(A,{title:"No matches",description:"Try a different filter or clear the search."}):n.jsx("ul",{className:"mail-inbox-list","aria-label":"Messages",children:r.map(d=>{const p=d.id===i,x=ms(d.subject)||"(No subject)",g=Dn(d.from)||"(unknown sender)",S=qu(d);return n.jsx("li",{children:n.jsxs("button",{"aria-current":p?"true":void 0,className:p?"mail-inbox-row active":"mail-inbox-row",onClick:()=>u(d.id),type:"button",children:[n.jsxs("span",{className:"mail-inbox-row-top",children:[n.jsx("span",{className:"mail-inbox-sender",children:g}),n.jsx("time",{className:"mail-inbox-time",dateTime:d.receivedAt,title:Al(d.receivedAt),children:Vu(d.receivedAt)})]}),n.jsx("span",{className:"mail-inbox-subject",children:x}),S?n.jsx("span",{className:"mail-inbox-snippet",children:S}):null]})},d.id)})})}function Op({message:r}){const[i,l]=m.useState("preview"),[u,d]=m.useState(!1);if(m.useEffect(()=>{l("preview"),d(!1)},[r==null?void 0:r.id]),!r)return n.jsx(A,{title:"Select a message",description:"Messages accepted via SMTP appear here. Pick a row on the left to inspect."});const p=ms(r.subject)||"(No subject)",x=Dn(r.from),g=r.to.map(Dn).filter(Boolean),S=Tl(r.headers,"Cc"),v=Tl(r.headers,"Bcc"),N=Tl(r.headers,"Reply-To"),w=r.attachments??[],L=r.textBody||r.htmlBody||r.parseError||"",C=L.trim().length>0,_=[{id:"preview",label:"Preview"},{id:"attachments",label:"Attachments",count:w.length},{id:"raw",label:"Raw"}];return n.jsxs("article",{className:"mail-inspector",children:[n.jsxs("header",{className:"mail-inspector-header",children:[n.jsx("h1",{className:"mail-subject",children:p}),n.jsxs("dl",{className:"mail-recipients",children:[n.jsx(Ir,{label:"From",values:x?[x]:["(unknown sender)"]}),n.jsx(Ir,{label:"To",values:g.length>0?g:["(no recipients)"]}),S.length>0?n.jsx(Ir,{label:"Cc",values:S}):null,v.length>0?n.jsx(Ir,{label:"Bcc",values:v}):null,N.length>0?n.jsx(Ir,{label:"Reply-To",values:N}):null,n.jsxs("div",{className:"mail-recipients-row",children:[n.jsx("dt",{children:"Date"}),n.jsxs("dd",{children:[n.jsx("time",{dateTime:r.receivedAt,title:Al(r.receivedAt),children:Al(r.receivedAt)}),n.jsxs("span",{className:"mail-recipients-aux",children:["· ",Vu(r.receivedAt)]})]})]})]})]}),n.jsx("nav",{className:"mail-tabs",role:"tablist","aria-label":"Message view",children:_.map(I=>n.jsxs("button",{role:"tab","aria-selected":i===I.id,className:i===I.id?"mail-tab active":"mail-tab",onClick:()=>l(I.id),type:"button",children:[I.label,I.count!==void 0&&I.count>0?n.jsx("span",{className:"mail-tab-count",children:I.count}):null]},I.id))}),i==="preview"?n.jsx(Ap,{body:L,hasBody:C,parseError:r.parseError}):null,i==="attachments"?n.jsx(_p,{attachments:w}):null,i==="raw"?n.jsx(Bp,{messageID:r.id}):null,n.jsxs("details",{className:"mail-headers",open:u,onToggle:I=>d(I.target.open),children:[n.jsxs("summary",{children:[n.jsx("span",{children:"Headers"}),n.jsx("span",{className:"mail-headers-count",children:Object.keys(r.headers??{}).length})]}),n.jsx(Fp,{headers:r.headers??{}})]})]})}function Ir({label:r,values:i}){return n.jsxs("div",{className:"mail-recipients-row",children:[n.jsx("dt",{children:r}),n.jsx("dd",{children:i.join(", ")})]})}function Ap({body:r,hasBody:i,parseError:l}){return!i&&!l?n.jsx(A,{title:"No body",description:"The message contains no decoded text content."}):l&&!i?n.jsxs("div",{className:"mail-preview-error",children:[n.jsx("span",{className:"mail-eyebrow",children:"Parse error"}),n.jsx("p",{children:l})]}):n.jsx("div",{className:"mail-preview",lang:"auto",children:Up(r)})}function _p({attachments:r}){return r.length===0?n.jsx(A,{title:"No attachments",description:"This message has no parsed attachments."}):n.jsx("ul",{className:"mail-attachment-list","aria-label":"Attachments",children:r.map(i=>n.jsxs("li",{className:"mail-attachment-item",children:[n.jsx("span",{className:"mail-attachment-icon","aria-hidden":"true",children:qp(i.contentType)}),n.jsxs("div",{className:"mail-attachment-body",children:[n.jsx("span",{className:"mail-attachment-name",children:i.fileName||i.id}),n.jsxs("span",{className:"mail-attachment-meta",children:[i.contentType||"application/octet-stream"," · ",Vp(i.size)]})]})]},i.id))})}function Bp({messageID:r}){const[i,l]=m.useState({status:"loading"});if(m.useEffect(()=>{let p=!1;return l({status:"loading"}),Ip(r).then(x=>{p||l({status:"success",raw:x})}).catch(x=>{p||l({status:"error",message:x.message})}),()=>{p=!0}},[r]),i.status==="loading")return n.jsx(A,{title:"Loading raw source",description:"Reading the stored RFC 822 message."});if(i.status==="error")return n.jsx(A,{title:"Raw source unavailable",description:i.message});const u=i.raw;function d(){navigator.clipboard&&navigator.clipboard.writeText(u).catch(()=>{})}return n.jsxs("div",{className:"mail-raw",children:[n.jsxs("div",{className:"mail-raw-toolbar",children:[n.jsxs("span",{className:"mail-raw-meta",children:[u.length.toLocaleString()," bytes"]}),n.jsx("button",{className:"mail-raw-copy",onClick:d,type:"button",children:"Copy"})]}),n.jsx("pre",{className:"mail-raw-pre",children:u||"(empty)"})]})}function Fp({headers:r}){const i=Object.entries(r).sort(([l],[u])=>l.localeCompare(u));return i.length===0?n.jsx("p",{className:"mail-headers-empty",children:"No parsed headers."}):n.jsx("table",{className:"mail-headers-table",children:n.jsx("tbody",{children:i.map(([l,u])=>n.jsxs("tr",{children:[n.jsx("th",{scope:"row",children:l}),n.jsx("td",{children:u.map(ms).join(", ")})]},l))})})}function zp(r,i){const l=i.trim().toLowerCase();return l===""?r:r.filter(u=>[ms(u.subject),Dn(u.from),...u.to.map(Dn),qu(u)].join(" ").toLowerCase().includes(l))}function qu(r){return(r.textBody||r.htmlBody||r.parseError||"").replace(/\s+/g," ").trim()}function Tl(r,i){if(!r)return[];const l=i.toLowerCase();for(const[u,d]of Object.entries(r))if(u.toLowerCase()===l)return d.flatMap(p=>Qp(p)).map(Dn).filter(Boolean);return[]}function Qp(r){return r.split(/,(?![^<]*>)/).map(i=>i.trim()).filter(i=>i.length>0)}function Up(r){const i=[],l=/(https?:\/\/[^\s<>"']+)|((?:[a-zA-Z0-9._%+-]+)@(?:[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}))/g;let u=0,d=0,p=l.exec(r);for(;p!==null;){const g=r.slice(u,p.index);g&&(d+=1,i.push(n.jsx("span",{children:g},`t-${d}`))),d+=1,p[1]?i.push(n.jsx("a",{href:p[1],rel:"noopener noreferrer",target:"_blank",children:p[1]},`l-${d}`)):i.push(n.jsx("a",{href:`mailto:${p[2]}`,children:p[2]},`m-${d}`)),u=l.lastIndex,p=l.exec(r)}const x=r.slice(u);return x&&(d+=1,i.push(n.jsx("span",{children:x},`t-${d}`))),i}function qp(r){if(!r)return"📎";const i=r.toLowerCase();return i.startsWith("image/")?"🖼":i==="application/pdf"?"📄":i.startsWith("audio/")?"🎵":i.startsWith("video/")?"🎬":i.includes("zip")||i.includes("tar")||i.includes("gzip")?"🗜":i.startsWith("text/")?"📝":"📎"}function Al(r){const i=new Date(r);return Number.isNaN(i.getTime())?r||"unknown":i.toLocaleString()}function Vu(r){const i=new Date(r);if(Number.isNaN(i.getTime()))return r||"unknown";const l=Date.now()-i.getTime();if(l<0)return"in the future";const u=Math.floor(l/1e3);if(u<60)return"just now";const d=Math.floor(u/60);if(d<60)return`${d}m ago`;const p=Math.floor(d/60);if(p<24)return`${p}h ago`;const x=Math.floor(p/24);return x<7?`${x}d ago`:i.toLocaleDateString()}function Vp(r){if(!Number.isFinite(r)||r<0)return"unknown";if(r<1024)return`${r} B`;const i=["KB","MB","GB","TB"];let l=r/1024,u=0;for(;l>=1024&&u=10?0:1)} ${i[u]}`}function Kp({buckets:r,activeBucket:i,disabled:l,onDeleteBucket:u,onSelectBucket:d}){return r.length===0?n.jsx(A,{title:"No buckets",description:"Buckets created through the S3 API will appear here."}):n.jsx("ul",{className:"s3-bucket-list","aria-label":"S3 buckets",children:r.map(p=>{const x=p.name===i,g=Yp(p.creationDate),S=Wp(p.objectCount),v=`${p.name} +${S} · created ${Gp(p.creationDate)}`;return n.jsxs("li",{className:x?"s3-bucket-row active":"s3-bucket-row",title:v,children:[n.jsxs("button",{className:"s3-bucket-select",onClick:()=>d(p.name),title:v,type:"button",children:[n.jsx("span",{className:"s3-bucket-icon","aria-hidden":!0,children:n.jsx(Jp,{})}),n.jsxs("span",{className:"s3-bucket-info",children:[n.jsx("span",{className:"s3-bucket-name",title:p.name,children:p.name}),n.jsxs("span",{className:"s3-bucket-meta",children:[S," · ",g]})]})]}),n.jsx("button",{"aria-label":`Delete bucket ${p.name}`,className:"s3-icon-button danger s3-bucket-delete",disabled:l,onClick:()=>u(p.name),type:"button",children:n.jsx(Hp,{})})]},p.name)})})}function Jp(){return n.jsxs("svg",{width:"16",height:"16",viewBox:"0 0 16 16",fill:"none",stroke:"currentColor",strokeWidth:"1.6",strokeLinecap:"round",strokeLinejoin:"round",children:[n.jsx("path",{d:"M2.5 4.5h11l-1 8.5a1 1 0 0 1-1 .9H4.5a1 1 0 0 1-1-.9z"}),n.jsx("path",{d:"M2.5 4.5L4 2.5h8l1.5 2"}),n.jsx("path",{d:"M6 7v4"}),n.jsx("path",{d:"M10 7v4"})]})}function Hp(){return n.jsxs("svg",{"aria-hidden":!0,width:"14",height:"14",viewBox:"0 0 16 16",fill:"none",stroke:"currentColor",strokeWidth:"1.6",strokeLinecap:"round",strokeLinejoin:"round",children:[n.jsx("path",{d:"M2.5 4h11"}),n.jsx("path",{d:"M6 4V2.5h4V4"}),n.jsx("path",{d:"M4 4l.7 9.2a1 1 0 0 0 1 .9h4.6a1 1 0 0 0 1-.9L12 4"}),n.jsx("path",{d:"M6.5 7v5"}),n.jsx("path",{d:"M9.5 7v5"})]})}function Wp(r){return!Number.isFinite(r)||r<0?"— objects":r===1?"1 object":`${r} objects`}function Gp(r){const i=new Date(r);return Number.isNaN(i.getTime())?r||"unknown":i.toLocaleString()}function Yp(r){const i=new Date(r);if(Number.isNaN(i.getTime()))return"created —";const l=Date.now()-i.getTime(),u=60*1e3,d=60*u,p=24*d,x=7*p,g=30*p,S=365*p;return l{if(d(),!r){w({status:"idle"});return}let C=!1;return w({status:"loading"}),tm(r,S.trim()).then(({objects:_})=>{C||w({status:"success",objects:_})}).catch(_=>{C||w({status:"error",message:_.message})}),()=>{C=!0}},[r,d,S,u]),!r)return n.jsx(A,{title:"Select a bucket",description:"Choose a bucket to browse its objects."});const L=N.status==="success"?N.objects:[];return n.jsxs("div",{className:"s3-browser",children:[n.jsxs("div",{className:"s3-browser-toolbar",children:[n.jsx(cm,{prefix:S,onChange:v}),n.jsx(um,{disabled:l,onUploadObject:x})]}),n.jsx(om,{bucketName:r,disabled:l}),N.status==="loading"?n.jsx(A,{title:"Loading objects",description:`Reading objects in ${r}.`}):null,N.status==="error"?n.jsx(A,{title:"Objects unavailable",description:N.message}):null,N.status==="success"&&L.length===0?n.jsx(A,{title:"No objects",description:S.trim()===""?`Objects uploaded to ${r} will appear here.`:`No objects in ${r} match this prefix.`}):null,N.status==="success"&&L.length>0?n.jsx("div",{className:"s3-object-table-wrap",children:n.jsxs("table",{className:"s3-object-table",children:[n.jsx("thead",{children:n.jsxs("tr",{children:[n.jsx("th",{scope:"col",children:"Key"}),n.jsx("th",{scope:"col",children:"Size"}),n.jsx("th",{scope:"col",children:"Modified"}),n.jsx("th",{scope:"col",children:"Type"}),n.jsx("th",{"aria-label":"Row actions",className:"s3-object-actions-head",scope:"col"})]})}),n.jsx("tbody",{children:L.map(C=>{const _=C.key===i;return n.jsxs("tr",{className:_?"s3-object-row active":"s3-object-row",children:[n.jsx("td",{children:n.jsx("button",{className:"s3-object-key",onClick:()=>g(C),title:C.key,type:"button",children:C.key})}),n.jsx("td",{className:"s3-object-numeric",children:fm(C.size)}),n.jsx("td",{className:"s3-object-numeric",children:Ju(C.lastModified)}),n.jsx("td",{className:"s3-object-type",children:C.contentType||"application/octet-stream"}),n.jsxs("td",{className:"s3-object-actions",children:[n.jsx("a",{"aria-label":`Download ${C.key}`,className:"s3-icon-button neutral",href:pm(C.downloadUrl),title:"Download",children:n.jsx(xm,{})}),n.jsx("button",{"aria-label":`Delete ${C.key}`,className:"s3-icon-button danger",disabled:l,onClick:()=>p(C),title:"Delete",type:"button",children:n.jsx(gm,{})})]})]},C.key)})})]})}):null]})}function om({bucketName:r,disabled:i}){const[l,u]=m.useState({status:"loading"}),[d,p]=m.useState(0),[x,g]=m.useState(""),S=Pr();m.useEffect(()=>{let N=!1;return u({status:"loading"}),sm(r).then(({uploads:w})=>{N||u({status:"success",uploads:w})}).catch(w=>{N||u({status:"error",message:w.message})}),()=>{N=!0}},[r,d]);async function v(N){if(!(i||x!==""||!await S(Rn({title:"Abort multipart upload",description:"Pending parts will be discarded and the upload will be unrecoverable.",target:N.uploadId,confirmLabel:"Abort upload"})))){g(N.uploadId);try{await rm(r,N.uploadId),p(L=>L+1)}finally{g("")}}}return l.status!=="success"||l.uploads.length===0?null:n.jsxs("details",{className:"s3-multipart-details",children:[n.jsxs("summary",{children:[n.jsx("span",{children:"Incomplete multipart uploads"}),n.jsx("span",{className:"s3-multipart-count",children:l.uploads.length})]}),n.jsx("ul",{className:"s3-multipart-list",children:l.uploads.map(N=>n.jsxs("li",{className:"s3-multipart-item",children:[n.jsxs("div",{className:"s3-multipart-meta",children:[n.jsx("strong",{children:N.key}),n.jsx("code",{children:N.uploadId}),n.jsx("span",{children:Ju(N.initiated)})]}),n.jsx(ne,{"aria-label":`Abort multipart upload ${N.uploadId} with confirmation`,className:"danger",disabled:i||x!=="",onClick:()=>void v(N),children:x===N.uploadId?"Aborting":"Abort"})]},N.uploadId))})]})}function cm({prefix:r,onChange:i}){return n.jsxs("label",{className:"s3-search",children:[n.jsx(mm,{}),n.jsx("input",{"aria-label":"Filter objects by prefix",onChange:l=>i(l.target.value),placeholder:"Filter by prefix (e.g. docs/)",type:"search",value:r})]})}function um({disabled:r,onUploadObject:i}){const l=m.useRef(null),[u,d]=m.useState(""),[p,x]=m.useState(""),[g,S]=m.useState(""),[v,N]=m.useState(!1),w=m.useRef(null);async function L(C){var I,E;C.preventDefault();const _=(E=(I=l.current)==null?void 0:I.files)==null?void 0:E[0];if(!(r||v||u.trim()===""||!_)){N(!0);try{await i({key:u.trim(),file:_,contentType:p.trim()||_.type||"application/octet-stream",metadata:dm(g)}),d(""),x(""),S(""),l.current&&(l.current.value=""),w.current&&(w.current.open=!1)}finally{N(!1)}}}return n.jsxs("details",{className:"s3-upload-details",ref:w,children:[n.jsxs("summary",{children:[n.jsx(hm,{}),n.jsx("span",{children:"Upload object"})]}),n.jsxs("form",{className:"s3-upload-form",onSubmit:L,children:[n.jsxs("label",{className:"s3-field",children:[n.jsx("span",{children:"Key"}),n.jsx("input",{"aria-label":"S3 upload object key",className:"s3-text-input",disabled:r||v,onChange:C=>d(C.target.value),placeholder:"docs/readme.txt",value:u})]}),n.jsxs("label",{className:"s3-field",children:[n.jsx("span",{children:"Content-Type"}),n.jsx("input",{"aria-label":"S3 upload content type",className:"s3-text-input",disabled:r||v,onChange:C=>x(C.target.value),placeholder:"text/plain",value:p})]}),n.jsxs("label",{className:"s3-field s3-field-full",children:[n.jsx("span",{children:"File"}),n.jsx("input",{"aria-label":"S3 upload file",className:"s3-file-input",disabled:r||v,ref:l,type:"file"})]}),n.jsxs("label",{className:"s3-field s3-field-full",children:[n.jsx("span",{children:"Metadata (x-amz-meta)"}),n.jsx("textarea",{"aria-label":"S3 upload metadata",className:"s3-text-input",disabled:r||v,onChange:C=>S(C.target.value),placeholder:`source=dashboard +purpose=local-test`,rows:2,value:g})]}),n.jsx("div",{className:"s3-upload-actions",children:n.jsx(ne,{disabled:r||v||u.trim()==="",type:"submit",children:v?"Uploading…":"Upload"})})]})]})}function dm(r){return r.split(/\r?\n/).reduce((i,l)=>{const u=l.trim();if(u==="")return i;const[d,...p]=u.split("="),x=d.trim(),g=p.join("=").trim();return x!==""&&(i[x]=g),i},{})}function fm(r){if(!Number.isFinite(r)||r<0)return"unknown";if(r<1024)return`${r} B`;const i=["KB","MB","GB","TB"];let l=r/1024,u=0;for(;l>=1024&&u=10?0:1)} ${i[u]}`}function Ju(r){const i=new Date(r);return Number.isNaN(i.getTime())?r||"unknown":i.toLocaleString()}function pm(r){return r.startsWith("/api/s3/")?r:"#"}function mm(){return n.jsxs("svg",{"aria-hidden":!0,width:"14",height:"14",viewBox:"0 0 16 16",fill:"none",stroke:"currentColor",strokeWidth:"1.6",strokeLinecap:"round",strokeLinejoin:"round",children:[n.jsx("circle",{cx:"7",cy:"7",r:"4.5"}),n.jsx("path",{d:"M13.5 13.5L10.5 10.5"})]})}function hm(){return n.jsxs("svg",{"aria-hidden":!0,width:"14",height:"14",viewBox:"0 0 16 16",fill:"none",stroke:"currentColor",strokeWidth:"1.8",strokeLinecap:"round",children:[n.jsx("path",{d:"M8 3v10"}),n.jsx("path",{d:"M3 8h10"})]})}function xm(){return n.jsxs("svg",{"aria-hidden":!0,width:"14",height:"14",viewBox:"0 0 16 16",fill:"none",stroke:"currentColor",strokeWidth:"1.6",strokeLinecap:"round",strokeLinejoin:"round",children:[n.jsx("path",{d:"M8 2.5v8"}),n.jsx("path",{d:"M4.5 7L8 10.5 11.5 7"}),n.jsx("path",{d:"M3 13h10"})]})}function gm(){return n.jsxs("svg",{"aria-hidden":!0,width:"14",height:"14",viewBox:"0 0 16 16",fill:"none",stroke:"currentColor",strokeWidth:"1.6",strokeLinecap:"round",strokeLinejoin:"round",children:[n.jsx("path",{d:"M2.5 4h11"}),n.jsx("path",{d:"M6 4V2.5h4V4"}),n.jsx("path",{d:"M4 4l.7 9.2a1 1 0 0 0 1 .9h4.6a1 1 0 0 0 1-.9L12 4"}),n.jsx("path",{d:"M6.5 7v5"}),n.jsx("path",{d:"M9.5 7v5"})]})}function ym({bucketName:r,disabled:i,object:l,onCopyObject:u}){if(!l)return n.jsxs("div",{className:"s3-inspector-empty",children:[n.jsx(Nm,{}),n.jsx("p",{className:"s3-inspector-empty-title",children:"No object selected"}),n.jsx("p",{className:"s3-inspector-empty-body",children:r?`Pick an object in ${r} to see metadata, copy, and download actions.`:"Select a bucket first, then click an object to inspect it here."})]});const d=Object.entries(l.metadata??{});return n.jsxs("div",{className:"s3-inspector",children:[n.jsxs("div",{className:"s3-inspector-section",children:[n.jsx("span",{className:"s3-field-label",children:"Object"}),n.jsx("code",{className:"s3-inspector-key",title:l.key,children:l.key}),n.jsx("code",{className:"s3-inspector-uri",title:l.s3Uri,children:l.s3Uri})]}),n.jsxs("dl",{className:"s3-inspector-list",children:[n.jsxs("div",{children:[n.jsx("dt",{children:"Size"}),n.jsx("dd",{children:vm(l.size)})]}),n.jsxs("div",{children:[n.jsx("dt",{children:"Content type"}),n.jsx("dd",{children:l.contentType||"application/octet-stream"})]}),n.jsxs("div",{children:[n.jsx("dt",{children:"Last modified"}),n.jsx("dd",{children:bm(l.lastModified)})]}),n.jsxs("div",{children:[n.jsx("dt",{children:"ETag"}),n.jsx("dd",{children:n.jsx("code",{children:l.etag||"unknown"})})]})]}),n.jsxs("div",{className:"s3-inspector-section",children:[n.jsx("span",{className:"s3-field-label",children:"Metadata"}),d.length===0?n.jsx("p",{className:"s3-inspector-muted",children:"No user metadata."}):n.jsx("dl",{className:"s3-inspector-metadata",children:d.map(([p,x])=>n.jsxs("div",{children:[n.jsx("dt",{children:p}),n.jsx("dd",{children:x})]},p))})]}),n.jsxs("a",{className:"s3-inspector-download",href:Sm(l.downloadUrl),children:[n.jsx(wm,{}),n.jsx("span",{children:"Download object"})]}),n.jsx(jm,{disabled:i,object:l,onCopyObject:u})]})}function jm({disabled:r,object:i,onCopyObject:l}){const[u,d]=m.useState(`${i.key}.copy`),[p,x]=m.useState(!1);m.useEffect(()=>{d(`${i.key}.copy`)},[i.key]);async function g(S){S.preventDefault();const v=u.trim();if(!(r||p||v==="")){x(!0);try{await l(v)}finally{x(!1)}}}return n.jsxs("form",{className:"s3-inspector-copy",onSubmit:g,children:[n.jsxs("label",{className:"s3-field",children:[n.jsx("span",{children:"Copy to key"}),n.jsx("input",{"aria-label":"S3 copy destination key",className:"s3-text-input",disabled:r||p,onChange:S=>d(S.target.value),value:u})]}),n.jsx(ne,{disabled:r||p||u.trim()==="",type:"submit",children:p?"Copying…":"Copy"})]})}function vm(r){if(!Number.isFinite(r)||r<0)return"unknown";if(r<1024)return`${r} B`;const i=["KB","MB","GB","TB"];let l=r/1024,u=0;for(;l>=1024&&u=10?0:1)} ${i[u]}`}function bm(r){const i=new Date(r);return Number.isNaN(i.getTime())?r||"unknown":i.toLocaleString()}function Sm(r){return r.startsWith("/api/s3/")?r:"#"}function Nm(){return n.jsxs("svg",{"aria-hidden":!0,width:"36",height:"36",viewBox:"0 0 36 36",fill:"none",stroke:"currentColor",strokeWidth:"1.6",strokeLinecap:"round",strokeLinejoin:"round",children:[n.jsx("path",{d:"M7 11l11-6 11 6v14L18 31 7 25z"}),n.jsx("path",{d:"M7 11l11 6 11-6"}),n.jsx("path",{d:"M18 17v14"})]})}function wm(){return n.jsxs("svg",{"aria-hidden":!0,width:"14",height:"14",viewBox:"0 0 16 16",fill:"none",stroke:"currentColor",strokeWidth:"1.6",strokeLinecap:"round",strokeLinejoin:"round",children:[n.jsx("path",{d:"M8 2.5v8"}),n.jsx("path",{d:"M4.5 7L8 10.5 11.5 7"}),n.jsx("path",{d:"M3 13h10"})]})}function km({service:r}){var he;const[i,l]=m.useState({status:"loading"}),[u,d]=m.useState(),[p,x]=m.useState(),[g,S]=m.useState(""),[v,N]=m.useState(""),[w,L]=m.useState(0),C=(r==null?void 0:r.status)==="disabled",_=Pr(),I=ht(),E=m.useCallback(()=>{const z=I();if(C){l({status:"success",buckets:[]}),d(void 0),x(void 0);return}l({status:"loading"}),Xp().then(({buckets:H})=>{z()&&(l({status:"success",buckets:H}),d(be=>{var Pe;return be&&H.some(Ne=>Ne.name===be)?be:(Pe=H[0])==null?void 0:Pe.name}))}).catch(H=>{z()&&l({status:"error",message:H.message})})},[I,C]);m.useEffect(()=>{E()},[E]);const J=m.useCallback(z=>{E(),z.type.startsWith("s3.object.")&&L(H=>H+1)},[E]);Vt({topics:["s3"],onEvent:J,onResync:()=>{E(),L(z=>z+1)},enabled:!C});const de=i.status==="success"?i.buckets:[],V=(he=de.find(z=>z.name===u))==null?void 0:he.name,$=p&&V?p:void 0,oe=m.useCallback(()=>{x(void 0)},[]);if(C)return n.jsx(ue,{title:"S3",children:n.jsx(A,{title:"S3 is disabled",description:"Enable the S3 service in devcloud config to browse buckets and objects."})});function me(z){d(z),x(void 0)}function Z(){const z=g.trim();C||z===""||Zp(z).then(H=>{N(`Created bucket ${H.name}`),S(""),d(H.name),E()}).catch(H=>N(H.message))}async function Re(z){C||!await _(Rn({title:"Delete bucket",description:"All objects and metadata in this bucket will be removed. This cannot be undone.",target:z}))||em(z).then(()=>{N(`Deleted bucket ${z}`),d(be=>be===z?void 0:be),x(void 0),E()}).catch(be=>N(be.message))}async function ee(z){!V||C||!await _(Rn({title:"Delete object",description:"This object will be permanently removed from the bucket.",target:z.key}))||nm(V,z.key).then(()=>{N(`Deleted object ${z.s3Uri}`),x(void 0),L(be=>be+1),E()}).catch(be=>N(be.message))}async function pe(z){if(!(!V||C))try{const H=await am({bucketName:V,objectKey:z.key,body:z.file,contentType:z.contentType,metadata:z.metadata});N(`Uploaded object ${H.s3Uri}`),x(H),L(be=>be+1),E()}catch(H){throw N(H instanceof Error?H.message:"S3 upload failed"),H}}async function Se(z){if(!(!V||!$||C))try{const H=await im({sourceBucketName:V,sourceObjectKey:$.key,destinationBucketName:V,destinationObjectKey:z});N(`Copied object ${$.s3Uri} to ${H.s3Uri}`),x(H),L(be=>be+1),E()}catch(H){throw N(H instanceof Error?H.message:"S3 copy failed"),H}}return n.jsxs("div",{className:"s3-workspace",children:[n.jsxs(ue,{title:"Buckets",children:[n.jsxs("div",{className:"s3-bucket-toolbar",children:[n.jsx("span",{className:"s3-bucket-count",children:de.length===1?"1 bucket":`${de.length} buckets`}),n.jsx("button",{"aria-label":"Refresh buckets",className:"s3-icon-button neutral",onClick:E,type:"button",children:n.jsx(Rm,{})})]}),n.jsx(Cm,{bucketName:g,disabled:C,onChange:S,onCreate:Z}),i.status==="loading"?n.jsx(A,{title:"Loading buckets",description:"Reading the local S3 bucket registry."}):null,i.status==="error"?n.jsx(A,{title:"S3 buckets unavailable",description:i.message,actionLabel:"Retry",onAction:E}):null,i.status==="success"?n.jsx(Kp,{buckets:de,activeBucket:V,disabled:C,onDeleteBucket:Re,onSelectBucket:me}):null]}),n.jsx(ue,{title:V?`Objects · ${V}`:"Objects",children:n.jsx(lm,{bucketName:V,activeObjectKey:$==null?void 0:$.key,disabled:C,refreshNonce:w,onClearObject:oe,onDeleteObject:ee,onUploadObject:pe,onSelectObject:x})}),n.jsx(ue,{title:"Inspector",children:n.jsx(ym,{bucketName:V,disabled:C,object:$,onCopyObject:Se})}),v?n.jsx("p",{className:"s3-status-banner",children:v}):null]})}function Cm({bucketName:r,disabled:i,onChange:l,onCreate:u}){return n.jsxs("form",{className:"s3-bucket-create",onSubmit:d=>{d.preventDefault(),u()},children:[n.jsx("input",{"aria-label":"New bucket name",className:"s3-text-input",disabled:i,onChange:d=>l(d.target.value),placeholder:"New bucket name",value:r}),n.jsx(ne,{disabled:i||r.trim()==="",type:"submit",children:"Create"})]})}function Rm(){return n.jsxs("svg",{"aria-hidden":!0,width:"16",height:"16",viewBox:"0 0 16 16",fill:"none",stroke:"currentColor",strokeWidth:"1.6",strokeLinecap:"round",strokeLinejoin:"round",children:[n.jsx("path",{d:"M13.5 8a5.5 5.5 0 1 1-1.61-3.89"}),n.jsx("path",{d:"M13.5 3v3h-3"})]})}async function Dm(){return q("/api/dynamodb/status")}async function Im(){return q("/api/dynamodb/tables")}async function Em(r){return q("/api/dynamodb/tables",{method:"POST",body:{input:r}})}async function Tm(r,i=100){return q(`/api/dynamodb/tables/${encodeURIComponent(r)}/items?limit=${encodeURIComponent(String(i))}`)}async function Pm(r,i){return q(`/api/dynamodb/tables/${encodeURIComponent(r)}/items`,{method:"POST",body:{input:i}})}async function Lm(r,i){return q(`/api/dynamodb/tables/${encodeURIComponent(r)}/items/update`,{method:"POST",body:{input:i}})}async function $m(r,i,l){return q(`/api/dynamodb/tables/${encodeURIComponent(r)}/items/delete`,{method:"POST",body:{input:i,confirmation:l}})}async function Mm(r,i){return q(`/api/dynamodb/tables/${encodeURIComponent(r)}/query`,{method:"POST",body:{input:i}})}async function Om(r,i){return q(`/api/dynamodb/tables/${encodeURIComponent(r)}/scan`,{method:"POST",body:{input:i}})}async function Am(r){return q(`/api/dynamodb/tables/${encodeURIComponent(r)}`)}async function _m(r){return q(`/api/dynamodb/tables/${encodeURIComponent(r)}/indexes`)}async function Bm(r){return q(`/api/dynamodb/tables/${encodeURIComponent(r)}/ttl`)}async function Fm(r,i){return q(`/api/dynamodb/tables/${encodeURIComponent(r)}/ttl`,{method:"POST",body:{input:i}})}async function zm(r,i,l){return q(`/api/dynamodb/tables/${encodeURIComponent(r)}/delete`,{method:"POST",body:{input:i,confirmation:l}})}async function Qm(r){return q(`/api/dynamodb/tables/${encodeURIComponent(r)}/streams`)}const _l="devcloud.dynamodb.recentOperations.v1",zl=10,Um=`{ "TableName": "Demo", "AttributeDefinitions": [ { "AttributeName": "pk", "AttributeType": "S" } @@ -75,15 +75,15 @@ purpose=local-test`,rows:2,value:y})]}),n.jsx("div",{className:"s3-upload-action } }`,Hm=`{ ":pk": { "S": "user#1" } -}`;function Wm(r){return{status:"disabled",running:!1,endpoint:(r==null?void 0:r.endpoint)??"http://127.0.0.1:18000",region:"us-east-1",storagePath:(r==null?void 0:r.storagePath)??".devcloud/data/dynamodb",tableCount:0}}function Qi(r){const l=r.keySchema??[];return l.length===0?"No key schema":l.map(i=>`${i.AttributeName} ${i.KeyType}`).join(" / ")}function Gm(r){return(r.globalSecondaryIndexes??[]).length+(r.localSecondaryIndexes??[]).length}function Ym(r){const l=r.attributeDefinitions??[];return l.length===0?"none":l.map(i=>`${i.AttributeName} ${i.AttributeType}`).join(", ")}function Xm(r){const l=r.timeToLiveDescription;return!l||l.TimeToLiveStatus===""?"not configured":l.AttributeName?`${l.TimeToLiveStatus} on ${l.AttributeName}`:l.TimeToLiveStatus}function Zm(r){var i;if(!r.streamEnabled)return"disabled";const l=((i=r.streamSpecification)==null?void 0:i.StreamViewType)??"enabled";return r.latestStreamLabel?`${l} (${r.latestStreamLabel})`:l}function $i(r){const l=Number.parseInt(r,10);return!Number.isFinite(l)||l<=0?100:Math.min(l,1e3)}function ps(r){try{const l=JSON.parse(r);return!l||Array.isArray(l)||typeof l!="object"?{ok:!1,message:"Input must be a JSON object."}:{ok:!0,value:l}}catch(l){return{ok:!1,message:l instanceof Error?l.message:"Input must be valid JSON."}}}function eh(r){if(r.trim()==="")return{ok:!0};const l=ps(r);return l.ok?{ok:!0,value:l.value}:l}function th(r){return r==null?"null":typeof r=="object"?JSON.stringify(r):String(r)}function nh(r){return r==null?"":typeof r=="string"?r:typeof r=="number"||typeof r=="boolean"?String(r):JSON.stringify(r)}function sh(r){return!Number.isFinite(r)||r<0?"unknown":r<1024?`${r} B`:`${(r/1024).toFixed(1)} KB`}function rh(){if(typeof window>"u")return[];try{const r=window.localStorage.getItem(_i);if(!r)return[];const l=JSON.parse(r);return Array.isArray(l)?l.filter(lh).slice(0,zi):[]}catch{return[]}}function ah(r){if(!(typeof window>"u"))try{if(r.length===0){window.localStorage.removeItem(_i);return}window.localStorage.setItem(_i,JSON.stringify(r.slice(0,zi)))}catch{}}function lh(r){if(!r||Array.isArray(r)||typeof r!="object")return!1;const l=r;return(l.operation==="Query"||l.operation==="Scan")&&typeof l.id=="string"&&typeof l.tableName=="string"&&typeof l.limit=="number"&&typeof l.expressionSummary=="string"&&typeof l.count=="number"&&typeof l.scannedCount=="number"&&typeof l.page=="number"&&typeof l.hasMore=="boolean"&&typeof l.createdAt=="string"}function ih(r){return{id:`${Date.now()}-${r.mode}-${r.tableName}-${r.page}`,operation:r.mode,tableName:r.tableName,indexName:r.indexName.trim()||void 0,limit:r.limit,expressionSummary:r.usesExpressionAttributeValues?"expression values present":"no expression values",count:r.count,scannedCount:r.scannedCount,page:r.page,hasMore:r.hasMore,createdAt:new Date().toISOString()}}function oh(r){const l=new Date(r);return Number.isNaN(l.getTime())?"unknown":l.toLocaleString()}function ch({activeIndex:r,items:l,itemsState:i,onSelectIndex:u,tableName:d}){return d?i.status==="loading"?n.jsx(F,{title:"Loading items",description:`Reading items from ${d}.`}):i.status==="error"?n.jsx(F,{title:"DynamoDB items unavailable",description:i.message}):l.length===0?n.jsx(F,{title:"No items",description:`No loaded items in ${d} match the current filter.`}):n.jsx("div",{className:"dynamodb-item-table-wrap",children:n.jsxs("table",{className:"dynamodb-item-table",children:[n.jsx("thead",{children:n.jsxs("tr",{children:[n.jsx("th",{scope:"col",children:"Key"}),n.jsx("th",{scope:"col",children:"Attributes"}),n.jsx("th",{scope:"col",children:"Size"})]})}),n.jsx("tbody",{children:l.map((p,g)=>n.jsxs("tr",{className:g===r?"item-row active":"item-row",onClick:()=>u(g),children:[n.jsx("td",{children:n.jsx("code",{children:JSON.stringify(p.key)})}),n.jsx("td",{children:n.jsx(Hu,{item:p.item})}),n.jsx("td",{children:sh(JSON.stringify(p.item).length)})]},`${JSON.stringify(p.key)}-${g}`))})]})}):n.jsx(F,{title:"No table selected",description:"Choose a table to inspect its stored items."})}function Hu({item:r}){const l=Object.entries(r).filter(([i])=>i!=="pk"&&i!=="sk").slice(0,6);return l.length===0?n.jsx("span",{className:"service-status",children:"key only"}):n.jsx("span",{className:"attribute-preview",children:l.map(([i,u])=>n.jsxs("span",{className:"attribute-chip",children:[i,": ",th(u)]},i))})}function uh({message:r,onFind:l,onUpdateValue:i,table:u,values:d}){const p=(u==null?void 0:u.keySchema)??[];return!u||p.length===0?null:n.jsxs("div",{className:"dynamodb-key-lookup",children:[n.jsx("span",{className:"inspector-label",children:"Key lookup"}),n.jsxs("div",{className:"pubsub-action-row",children:[p.map(g=>n.jsxs("label",{className:"compact-filter",children:[n.jsxs("span",{children:[g.AttributeName," ",g.KeyType]}),n.jsx("input",{"aria-label":`DynamoDB key lookup ${g.AttributeName}`,onChange:y=>i(g.AttributeName,y.target.value),placeholder:g.AttributeName,value:d[g.AttributeName]??""})]},g.AttributeName)),n.jsx(ae,{onClick:l,children:"Find loaded item"})]}),r?n.jsx("p",{className:"inspector-muted",children:r}):null]})}function dh({activeTableName:r,busyOperation:l,createTableJSON:i,putItemJSON:u,updateItemJSON:d,deleteItemJSON:p,ttlJSON:g,deleteTableJSON:y,deleteItemConfirmation:S,deleteTableConfirmation:v,deleteItemAcknowledged:N,deleteTableAcknowledged:w,onCreateTable:L,onPutItem:C,onUpdateItem:A,onDeleteItem:I,onUpdateTTL:E,onDeleteTable:J,setCreateTableJSON:ue,setPutItemJSON:q,setUpdateItemJSON:P,setDeleteItemJSON:oe,setTTLJSON:pe,setDeleteTableJSON:Z,setDeleteItemConfirmation:Re,setDeleteTableConfirmation:ee,setDeleteItemAcknowledged:fe,setDeleteTableAcknowledged:Se}){const me=!r;return n.jsxs("div",{className:"dynamodb-operation-stack",children:[n.jsx(Qs,{buttonLabel:"Create table",disabled:l==="create-table",json:i,label:"CreateTable input",onChange:ue,onSubmit:L}),n.jsx(Qs,{buttonLabel:"Put item",disabled:me||l==="put-item",json:u,label:"PutItem input",onChange:q,onSubmit:C}),n.jsx(Qs,{buttonLabel:"Update item",disabled:me||l==="update-item",json:d,label:"UpdateItem input",onChange:P,onSubmit:A}),n.jsx(Qs,{buttonLabel:"Update TTL",disabled:me||l==="ttl",json:g,label:"UpdateTimeToLive input",onChange:pe,onSubmit:E}),n.jsx(Qs,{buttonClassName:"danger",buttonLabel:"Delete item",confirmation:S,destructiveAcknowledgement:N,destructiveAcknowledgementLabel:"Step 1: I understand this deletes the item identified by the JSON key.",confirmationLabel:`Type ${r??"table name"} to delete item`,disabled:me||!N||S!==r||l==="delete-item",json:p,label:"DeleteItem input",onChange:oe,onDestructiveAcknowledgementChange:fe,onConfirmationChange:Re,onSubmit:I}),n.jsx(Qs,{buttonClassName:"danger",buttonLabel:"Delete table",confirmation:v,destructiveAcknowledgement:w,destructiveAcknowledgementLabel:"Step 1: I understand this deletes the selected table and its local items.",confirmationLabel:`Type ${r??"table name"} to delete table`,disabled:me||!w||v!==r||l==="delete-table",json:y,label:"DeleteTable input",onChange:Z,onDestructiveAcknowledgementChange:Se,onConfirmationChange:ee,onSubmit:J})]})}function Qs({buttonClassName:r,buttonLabel:l,confirmation:i,confirmationLabel:u,destructiveAcknowledgement:d,destructiveAcknowledgementLabel:p,disabled:g,json:y,label:S,onChange:v,onConfirmationChange:N,onDestructiveAcknowledgementChange:w,onSubmit:L}){return n.jsxs("form",{className:"dynamodb-operation-form",onSubmit:L,children:[n.jsxs("label",{className:"redshift-sql-editor",children:[n.jsx("span",{children:S}),n.jsx("textarea",{"aria-label":S,onChange:C=>v(C.target.value),spellCheck:!1,value:y})]}),w?n.jsxs("label",{className:"destructive-confirmation",children:[n.jsx("input",{"aria-label":p,checked:!!d,onChange:C=>w(C.target.checked),type:"checkbox"}),n.jsx("span",{children:p})]}):null,N?n.jsxs("label",{className:"compact-filter",children:[n.jsxs("span",{children:["Step 2: ",u]}),n.jsx("input",{"aria-label":u,onChange:C=>N(C.target.value),value:i??""})]}):null,n.jsx(ae,{className:r,disabled:g,type:"submit",children:l})]})}function fh({activeTableName:r,busyOperation:l,error:i,expressionAttributeValues:u,filterExpression:d,indexName:p,keyConditionExpression:g,limit:y,message:S,mode:v,onNextPage:N,onPreviousPage:w,onSubmit:L,pageIndex:C,result:A,selectedItemIndex:I,setExpressionAttributeValues:E,setFilterExpression:J,setIndexName:ue,setKeyConditionExpression:q,setLimit:P,setMode:oe,setSelectedItemIndex:pe}){const Z=!r||l==="query"||l==="scan",Re=(A==null?void 0:A.Items)??[],ee=C>0,fe=!!(A!=null&&A.LastEvaluatedKey),Se=Re[Math.min(I,Math.max(Re.length-1,0))];return n.jsxs("div",{className:"dynamodb-query-scan",children:[i?n.jsx("p",{className:"operation-message error",children:i}):null,S?n.jsx("p",{className:"operation-message success",children:S}):null,n.jsxs("form",{className:"dynamodb-query-scan-form",onSubmit:L,children:[n.jsxs("div",{className:"segmented-control","aria-label":"DynamoDB read operation",children:[n.jsx("button",{className:v==="Query"?"active":"",onClick:()=>oe("Query"),type:"button",children:"Query"}),n.jsx("button",{className:v==="Scan"?"active":"",onClick:()=>oe("Scan"),type:"button",children:"Scan"})]}),n.jsxs("label",{className:"compact-filter",children:[n.jsx("span",{children:"TableName"}),n.jsx("input",{"aria-label":"DynamoDB Query Scan table name",disabled:!0,value:r??""})]}),n.jsxs("label",{className:"compact-filter",children:[n.jsx("span",{children:"IndexName"}),n.jsx("input",{"aria-label":"DynamoDB Query Scan index name",onChange:me=>ue(me.target.value),placeholder:"optional",value:p})]}),n.jsxs("label",{className:"compact-filter small",children:[n.jsx("span",{children:"Limit"}),n.jsx("input",{"aria-label":"DynamoDB Query Scan limit",inputMode:"numeric",onChange:me=>P(me.target.value),value:y})]}),v==="Query"?n.jsxs("label",{className:"compact-filter wide",children:[n.jsx("span",{children:"KeyConditionExpression"}),n.jsx("input",{"aria-label":"DynamoDB Query key condition expression",onChange:me=>q(me.target.value),placeholder:"pk = :pk",value:g})]}):n.jsxs("label",{className:"compact-filter wide",children:[n.jsx("span",{children:"FilterExpression"}),n.jsx("input",{"aria-label":"DynamoDB Scan filter expression",onChange:me=>J(me.target.value),placeholder:"optional",value:d})]}),n.jsxs("label",{className:"redshift-sql-editor",children:[n.jsx("span",{children:"ExpressionAttributeValues JSON"}),n.jsx("textarea",{"aria-label":"DynamoDB Query Scan expression attribute values JSON",onChange:me=>E(me.target.value),spellCheck:!1,value:u})]}),n.jsxs(ae,{disabled:Z,type:"submit",children:["Run ",v]})]}),A?n.jsxs("div",{className:"redshift-query-result",children:[n.jsxs("div",{className:"dynamodb-toolbar",children:[n.jsxs("span",{className:"toolbar-count",children:["Page ",C+1," / Count ",A.Count??Re.length," / Scanned ",A.ScannedCount??0]}),n.jsx("span",{className:"toolbar-count",children:fe?"More results available":"End of results"}),n.jsxs("div",{className:"pubsub-action-row",children:[n.jsx(ae,{disabled:Z||!ee,onClick:w,type:"button",children:"Previous page"}),n.jsx(ae,{disabled:Z||!fe,onClick:N,type:"button",children:"Next page"})]})]}),Re.length>0?n.jsxs("div",{className:"dynamodb-query-result-grid",children:[n.jsx("div",{className:"dynamodb-item-table-wrap",children:n.jsxs("table",{className:"dynamodb-item-table compact",children:[n.jsx("thead",{children:n.jsxs("tr",{children:[n.jsx("th",{scope:"col",children:"#"}),n.jsx("th",{scope:"col",children:"Item preview"})]})}),n.jsx("tbody",{children:Re.map((me,z)=>n.jsxs("tr",{className:z===I?"item-row active":"item-row",onClick:()=>pe(z),children:[n.jsx("td",{children:z+1}),n.jsx("td",{children:n.jsx(Hu,{item:me})})]},`${C}-${z}-${JSON.stringify(me).length}`))})]})}),n.jsxs("section",{children:[n.jsx("span",{className:"inspector-label",children:"Selected result item JSON"}),n.jsx("pre",{className:"mail-preview",children:JSON.stringify(Se,null,2)})]})]}):n.jsx("p",{className:"inspector-muted",children:"No items returned."})]}):null]})}function ph({operations:r,onClear:l}){return n.jsxs("section",{className:"dynamodb-recent-operations",children:[n.jsxs("div",{className:"inspector-heading",children:[n.jsxs("div",{children:[n.jsx("span",{className:"inspector-label",children:"Recent operation history"}),n.jsx("p",{className:"inspector-muted",children:"Stored locally without item payloads, credentials, or pagination keys."})]}),n.jsx(ae,{disabled:r.length===0,onClick:l,type:"button",children:"Clear"})]}),r.length===0?n.jsx("p",{className:"inspector-muted",children:"Run Query or Scan to record local operation metadata."}):n.jsx("div",{className:"dynamodb-item-table-wrap",children:n.jsxs("table",{className:"dynamodb-item-table compact",children:[n.jsx("thead",{children:n.jsxs("tr",{children:[n.jsx("th",{scope:"col",children:"When"}),n.jsx("th",{scope:"col",children:"Operation"}),n.jsx("th",{scope:"col",children:"Table"}),n.jsx("th",{scope:"col",children:"Result"})]})}),n.jsx("tbody",{children:r.map(i=>n.jsxs("tr",{children:[n.jsx("td",{children:oh(i.createdAt)}),n.jsx("td",{children:n.jsxs("span",{className:"attribute-preview",children:[n.jsx("span",{className:"attribute-chip",children:i.operation}),n.jsxs("span",{className:"attribute-chip",children:["page ",i.page]}),i.indexName?n.jsxs("span",{className:"attribute-chip",children:["index: ",i.indexName]}):null]})}),n.jsx("td",{children:n.jsx("code",{children:i.tableName})}),n.jsx("td",{children:n.jsxs("span",{className:"attribute-preview",children:[n.jsxs("span",{className:"attribute-chip",children:["count: ",i.count]}),n.jsxs("span",{className:"attribute-chip",children:["scanned: ",i.scannedCount]}),n.jsxs("span",{className:"attribute-chip",children:["limit: ",i.limit]}),n.jsx("span",{className:"attribute-chip",children:i.hasMore?"has next page":"last page"}),n.jsx("span",{className:"attribute-chip",children:i.expressionSummary})]})})]},i.id))})]})})]})}function mh({detailState:r,table:l,item:i,onRefreshDetail:u,status:d}){var N;if(!l)return n.jsx(F,{title:"Inspector",description:"Table schema and selected item JSON will appear here."});const p=r.status==="success"?r.table:l,g=r.status==="success"?r.globalSecondaryIndexes:l.globalSecondaryIndexes??[],y=r.status==="success"?r.localSecondaryIndexes:l.localSecondaryIndexes??[],S=r.status==="success"?r.ttl:l.timeToLiveDescription,v=r.status==="success"?r.streams:{tableName:l.tableName,streamEnabled:((N=l.streamSpecification)==null?void 0:N.StreamEnabled)??!1,latestStreamArn:l.latestStreamArn,latestStreamLabel:l.latestStreamLabel,streamSpecification:l.streamSpecification};return n.jsxs("div",{className:"dynamodb-inspector",children:[n.jsxs("section",{children:[n.jsxs("div",{className:"inspector-heading",children:[n.jsxs("div",{children:[n.jsx("span",{className:"inspector-label",children:"Table"}),n.jsx("h3",{children:p.tableName})]}),n.jsx(ae,{onClick:u,children:"Refresh detail"})]}),r.status==="loading"?n.jsx("p",{className:"inspector-muted",children:"Loading detail metadata."}):null,r.status==="error"?n.jsx("p",{className:"operation-message error",children:r.message}):null,n.jsxs("dl",{className:"inspector-list",children:[n.jsxs("div",{children:[n.jsx("dt",{children:"Status"}),n.jsx("dd",{children:p.tableStatus})]}),n.jsxs("div",{children:[n.jsx("dt",{children:"Endpoint"}),n.jsx("dd",{children:n.jsx("code",{children:(d==null?void 0:d.endpoint)??"unknown"})})]}),n.jsxs("div",{children:[n.jsx("dt",{children:"Region"}),n.jsx("dd",{children:(d==null?void 0:d.region)??"unknown"})]}),n.jsxs("div",{children:[n.jsx("dt",{children:"Key schema"}),n.jsx("dd",{children:Qi(p)})]}),n.jsxs("div",{children:[n.jsx("dt",{children:"Attributes"}),n.jsx("dd",{children:Ym(p)})]}),n.jsxs("div",{children:[n.jsx("dt",{children:"TTL"}),n.jsx("dd",{children:Xm({...p,timeToLiveDescription:S})})]}),n.jsxs("div",{children:[n.jsx("dt",{children:"Streams"}),n.jsx("dd",{children:Zm(v)})]})]})]}),n.jsxs("section",{children:[n.jsx("span",{className:"inspector-label",children:"Indexes"}),n.jsx(hh,{globalSecondaryIndexes:g,localSecondaryIndexes:y})]}),n.jsxs("section",{children:[n.jsx("span",{className:"inspector-label",children:"Streams"}),n.jsx("pre",{className:"mail-preview",children:JSON.stringify(v,null,2)})]}),n.jsxs("section",{children:[n.jsx("span",{className:"inspector-label",children:"Selected item"}),i?n.jsx("pre",{className:"mail-preview",children:JSON.stringify(i.item,null,2)}):n.jsx("p",{className:"inspector-muted",children:"Select an item row to inspect JSON."})]})]})}function hh({globalSecondaryIndexes:r,localSecondaryIndexes:l}){const i=[...r.map(u=>({...u,type:"GSI"})),...l.map(u=>({...u,type:"LSI"}))];return i.length===0?n.jsx("p",{className:"inspector-muted",children:"No secondary indexes."}):n.jsx("div",{className:"dynamodb-item-table-wrap",children:n.jsxs("table",{className:"dynamodb-item-table compact",children:[n.jsx("thead",{children:n.jsxs("tr",{children:[n.jsx("th",{scope:"col",children:"Type"}),n.jsx("th",{scope:"col",children:"Name"}),n.jsx("th",{scope:"col",children:"Key schema"}),n.jsx("th",{scope:"col",children:"Items"})]})}),n.jsx("tbody",{children:i.map(u=>n.jsxs("tr",{children:[n.jsx("td",{children:u.type}),n.jsx("td",{children:u.IndexName}),n.jsx("td",{children:Qi({tableName:u.IndexName,keySchema:u.KeySchema})}),n.jsx("td",{children:u.ItemCount??0})]},`${u.type}-${u.IndexName}`))})]})})}function gh({tables:r,activeTableName:l,onSelectTable:i}){return r.length===0?n.jsx(F,{title:"No tables",description:"Tables created through the DynamoDB API will appear here."}):n.jsx("div",{className:"dynamodb-table-list","aria-label":"DynamoDB tables",children:r.map(u=>{var d;return n.jsxs("button",{className:u.tableName===l?"dynamodb-table-row active":"dynamodb-table-row",onClick:()=>i(u.tableName),children:[n.jsxs("span",{className:"table-row-top",children:[n.jsx("span",{className:"table-row-name",children:u.tableName}),n.jsx("span",{className:"count-pill",children:u.itemCount})]}),n.jsx("span",{className:"table-row-meta",children:Qi(u)}),n.jsxs("span",{className:"table-row-tags",children:[n.jsx("span",{children:u.tableStatus}),n.jsxs("span",{children:[Gm(u)," indexes"]}),n.jsx("span",{children:(d=u.streamSpecification)!=null&&d.StreamEnabled?"streams on":"streams off"})]})]},u.tableName)})})}function xh({service:r}){const[l,i]=m.useState({status:"loading"}),[u,d]=m.useState({status:"idle"}),[p,g]=m.useState({status:"idle"}),[y,S]=m.useState(),[v,N]=m.useState(0),[w,L]=m.useState(""),[C,A]=m.useState(""),[I,E]=m.useState("100"),[J,ue]=m.useState({}),[q,P]=m.useState(""),[oe,pe]=m.useState(Um),[Z,Re]=m.useState(qm),[ee,fe]=m.useState(Vm),[Se,me]=m.useState(Km),[z,H]=m.useState(Jm),[be,$e]=m.useState("{}"),[Ne,$]=m.useState(""),[U,_]=m.useState(""),[j,T]=m.useState(!1),[ne,ce]=m.useState(!1),[ge,we]=m.useState(""),[De,se]=m.useState(""),[Ie,Qe]=m.useState(),[tt,Dt]=m.useState("Query"),[ut,de]=m.useState(""),[ke,Y]=m.useState("25"),[je,rt]=m.useState("pk = :pk"),[at,yt]=m.useState(""),[It,Ze]=m.useState(Hm),[Et,K]=m.useState(""),[Be,Ue]=m.useState(""),[Le,kt]=m.useState(),[_e,jt]=m.useState({pageHistory:[void 0],pageIndex:0,selectedItemIndex:0}),[Ae,hn]=m.useState(rh),it=(r==null?void 0:r.status)==="disabled";m.useEffect(()=>{ah(Ae)},[Ae]);const Xn=wt(),Tt=m.useCallback(()=>{const le=Xn();if(it){i({status:"success",statusPayload:Wm(r),tables:[]}),d({status:"idle"}),g({status:"idle"}),S(void 0);return}i({status:"loading"}),Promise.all([Dm(),Im()]).then(([W,{tables:Pe}])=>{le()&&(i({status:"success",statusPayload:W,tables:Pe}),S(He=>{var Ge;return He&&Pe.some(qe=>qe.tableName===He)?He:(Ge=Pe[0])==null?void 0:Ge.tableName}))}).catch(W=>{le()&&i({status:"error",message:W.message})})},[Xn,it,r]);m.useEffect(()=>{Tt()},[Tt]),Vt({topics:["dynamodb"],onEvent:Tt,onResync:Tt,enabled:!it});const an=l.status==="success"?l.tables:[],dt=an.find(le=>le.tableName===y),$t=wt(),ln=m.useCallback(()=>{const le=$t();if(!y||it){d({status:"idle"});return}d({status:"loading"}),Tm(y,$i(I)).then(({items:W})=>{le()&&(N(0),d({status:"success",items:W}))}).catch(W=>{le()&&d({status:"error",message:W.message})})},[$t,y,it,I]);m.useEffect(()=>{ln()},[ln]);const In=wt(),ht=m.useCallback(()=>{const le=In();if(!y||it){g({status:"idle"});return}g({status:"loading"}),Promise.all([Am(y),_m(y),Bm(y),Qm(y)]).then(([W,Pe,He,Ge])=>{le()&&g({status:"success",table:W.table,globalSecondaryIndexes:Pe.globalSecondaryIndexes??[],localSecondaryIndexes:Pe.localSecondaryIndexes??[],ttl:He.timeToLiveDescription,streams:Ge})}).catch(W=>{le()&&g({status:"error",message:W.message})})},[In,y,it]);m.useEffect(()=>{ht()},[ht]);const on=m.useMemo(()=>{const le=w.trim().toLowerCase();return le===""?an:an.filter(W=>W.tableName.toLowerCase().includes(le))},[an,w]),gn=m.useMemo(()=>{const le=u.status==="success"?u.items:[],W=C.trim().toLowerCase();return W===""?le:le.filter(Pe=>JSON.stringify(Pe).toLowerCase().includes(W))},[u,C]),hs=gn[Math.min(v,Math.max(gn.length-1,0))];if(it)return n.jsx(he,{title:"DynamoDB",children:n.jsx(F,{title:"DynamoDB is disabled",description:"Enable the DynamoDB service in devcloud config to inspect tables and items."})});function gs(le){S(le),N(0),A(""),ue({}),P(""),T(!1),ce(!1),$(""),_("")}function xs(le,W){ue(Pe=>({...Pe,[le]:W})),P("")}function Ee(){var He;if(!dt||u.status!=="success"){P("Load table items before using key lookup.");return}const W=(((He=dt.keySchema)==null?void 0:He.map(Ge=>Ge.AttributeName))??[]).map(Ge=>{var qe;return[Ge,((qe=J[Ge])==null?void 0:qe.trim())??""]}).filter(([,Ge])=>Ge!=="");if(W.length===0){P("Enter at least one key value.");return}const Pe=u.items.findIndex(Ge=>W.every(([qe,js])=>nh(Ge.item[qe])===js));if(Pe<0){P(`No loaded item matched ${W.map(([Ge])=>Ge).join(" / ")}.`);return}A(""),N(Pe),P(`Selected loaded item ${Pe+1}.`)}async function Ce(le,W){Qe(le),se(""),we("");try{const Pe=await W();we(Pe),Tt(),ln(),ht()}catch(Pe){se(Pe instanceof Error?Pe.message:"DynamoDB operation failed")}finally{Qe(void 0)}}function Fe(le){le.preventDefault();const W=ps(oe);if(!W.ok){se(W.message);return}const Pe=typeof W.value.TableName=="string"?W.value.TableName:"";if(Pe.trim()===""){se("CreateTable input requires TableName.");return}Ce("create-table",async()=>(await Em(W.value),S(Pe),`Created table ${Pe}.`))}function Ft(le){if(le.preventDefault(),!y){se("Select a table before putting an item.");return}const W=ps(Z);if(!W.ok){se(W.message);return}Ce("put-item",async()=>(await $m(y,W.value),`Put item in ${y}.`))}function En(le){if(le.preventDefault(),!y){se("Select a table before updating an item.");return}const W=ps(ee);if(!W.ok){se(W.message);return}Ce("update-item",async()=>(await Lm(y,W.value),`Updated item in ${y}.`))}function xn(le){if(le.preventDefault(),!y){se("Select a table before deleting an item.");return}if(!j){se("Acknowledge the DeleteItem destructive action before confirming the table name.");return}if(Ne!==y){se("DeleteItem confirmation must match the selected table name.");return}const W=ps(Se);if(!W.ok){se(W.message);return}Ce("delete-item",async()=>(await Pm(y,W.value,Ne),$(""),T(!1),`Deleted item from ${y}.`))}function Ua(le){if(le.preventDefault(),!y){se("Select a table before updating TTL.");return}const W=ps(z);if(!W.ok){se(W.message);return}Ce("ttl",async()=>(await Fm(y,W.value),`Updated TTL for ${y}.`))}function Zn(le){if(le.preventDefault(),!y){se("Select a table before deleting it.");return}if(!ne){se("Acknowledge the DeleteTable destructive action before confirming the table name.");return}if(U!==y){se("DeleteTable confirmation must match the selected table name.");return}const W=ps(be);if(!W.ok){se(W.message);return}Ce("delete-table",async()=>(await zm(y,W.value,U),_(""),ce(!1),S(void 0),`Deleted table ${y}.`))}function ys(le){if(!y)return{ok:!1,message:"Select a table before running Query or Scan."};const W=$i(ke),Pe=eh(It);if(!Pe.ok)return{ok:!1,message:Pe.message};const He={Limit:W},Ge=ut.trim();if(Ge!==""&&(He.IndexName=Ge),Pe.value&&(He.ExpressionAttributeValues=Pe.value),tt==="Query"){const qe=je.trim();if(qe==="")return{ok:!1,message:"Query requires KeyConditionExpression."};He.KeyConditionExpression=qe}else{const qe=at.trim();qe!==""&&(He.FilterExpression=qe)}return le&&(He.ExclusiveStartKey=le),{ok:!0,value:He}}async function Tn(le,W,Pe){var Ge;if(!y){Ue("Select a table before running Query or Scan.");return}const He=ys(le);if(!He.ok){Ue(He.message);return}Qe(tt.toLowerCase()),Ue(""),K(""),kt(void 0);try{const qe=tt==="Query"?await Mm(y,He.value):await Om(y,He.value);kt(qe),jt({pageHistory:Pe,pageIndex:W,selectedItemIndex:0}),hn(js=>{var Js;return[ih({count:qe.Count??((Js=qe.Items)==null?void 0:Js.length)??0,hasMore:!!qe.LastEvaluatedKey,indexName:ut,limit:$i(ke),mode:tt,page:W+1,scannedCount:qe.ScannedCount??0,tableName:y,usesExpressionAttributeValues:!!He.value.ExpressionAttributeValues}),...js].slice(0,zi)}),K(`${tt} page ${W+1} returned ${qe.Count??((Ge=qe.Items)==null?void 0:Ge.length)??0} item(s).`)}catch(qe){Ue(qe instanceof Error?qe.message:`${tt} failed`)}finally{Qe(void 0)}}async function Ks(le){le.preventDefault(),await Tn(void 0,0,[void 0])}function qa(){if(!(Le!=null&&Le.LastEvaluatedKey))return;const le=_e.pageIndex+1,W=_e.pageHistory.slice(0,le);W[le]=Le.LastEvaluatedKey,Tn(Le.LastEvaluatedKey,le,W)}function Va(){if(_e.pageIndex<=0)return;const le=_e.pageIndex-1,W=_e.pageHistory[le];Tn(W,le,_e.pageHistory)}return n.jsxs("div",{className:"dynamodb-workspace",children:[n.jsxs(he,{title:"Tables",children:[n.jsxs("div",{className:"dynamodb-toolbar",children:[n.jsxs("label",{className:"compact-filter",children:[n.jsx("span",{children:"Filter"}),n.jsx("input",{"aria-label":"Filter DynamoDB tables",onChange:le=>L(le.target.value),placeholder:"table name",type:"search",value:w})]}),n.jsx(ae,{onClick:Tt,children:"Refresh"})]}),l.status==="loading"?n.jsx(F,{title:"Loading tables",description:"Reading local DynamoDB table metadata."}):null,l.status==="error"?n.jsx(F,{title:"DynamoDB tables unavailable",description:l.message,actionLabel:"Retry",onAction:Tt}):null,l.status==="success"?n.jsx(gh,{tables:on,activeTableName:y,onSelectTable:gs}):null]}),n.jsxs(he,{title:"Items",children:[n.jsxs("div",{className:"dynamodb-toolbar",children:[n.jsx("span",{className:"toolbar-count",children:dt?`${gn.length} shown / ${dt.itemCount} reported`:"Select a table"}),n.jsxs("label",{className:"compact-filter",children:[n.jsx("span",{children:"Filter"}),n.jsx("input",{"aria-label":"Filter DynamoDB items",disabled:!dt,onChange:le=>{N(0),A(le.target.value)},placeholder:"attribute value",type:"search",value:C})]}),n.jsxs("label",{className:"compact-filter small",children:[n.jsx("span",{children:"Limit"}),n.jsx("input",{"aria-label":"Limit DynamoDB items",disabled:!dt,inputMode:"numeric",onChange:le=>E(le.target.value),value:I})]}),n.jsx(ae,{disabled:!dt,onClick:ln,children:"Refresh"})]}),n.jsx(ch,{activeIndex:v,items:gn,itemsState:u,onSelectIndex:N,tableName:y}),n.jsx(uh,{message:q,onFind:Ee,onUpdateValue:xs,table:dt,values:J})]}),n.jsxs(he,{title:"Operations",children:[De?n.jsx("p",{className:"operation-message error",children:De}):null,ge?n.jsx("p",{className:"operation-message success",children:ge}):null,n.jsx(dh,{activeTableName:y,busyOperation:Ie,createTableJSON:oe,deleteItemConfirmation:Ne,deleteItemAcknowledged:j,deleteItemJSON:Se,deleteTableConfirmation:U,deleteTableAcknowledged:ne,deleteTableJSON:be,onCreateTable:Fe,onDeleteItem:xn,onDeleteTable:Zn,onPutItem:Ft,onUpdateItem:En,onUpdateTTL:Ua,putItemJSON:Z,setCreateTableJSON:pe,setDeleteItemConfirmation:$,setDeleteItemAcknowledged:T,setDeleteItemJSON:me,setDeleteTableConfirmation:_,setDeleteTableAcknowledged:ce,setDeleteTableJSON:$e,setPutItemJSON:Re,setTTLJSON:H,setUpdateItemJSON:fe,ttlJSON:z,updateItemJSON:ee})]}),n.jsxs(he,{title:"Query / Scan",children:[n.jsx(fh,{activeTableName:y,busyOperation:Ie,expressionAttributeValues:It,filterExpression:at,indexName:ut,keyConditionExpression:je,limit:ke,message:Et,mode:tt,onNextPage:qa,onPreviousPage:Va,onSubmit:le=>{Ks(le)},pageIndex:_e.pageIndex,result:Le,selectedItemIndex:_e.selectedItemIndex,setExpressionAttributeValues:Ze,setFilterExpression:yt,setIndexName:de,setKeyConditionExpression:rt,setLimit:Y,setMode:Dt,setSelectedItemIndex:le=>jt(W=>({...W,selectedItemIndex:le})),error:Be}),n.jsx(ph,{operations:Ae,onClear:()=>hn([])})]}),n.jsx(he,{title:"Inspector",children:n.jsx(mh,{detailState:p,item:hs,onRefreshDetail:ht,status:l.status==="success"?l.statusPayload:void 0,table:dt})})]})}async function yh(){return V("/api/bigquery/status")}async function jh(){return V("/api/bigquery/projects")}async function vh(r){return V(`/api/bigquery/projects/${encodeURIComponent(r)}/datasets`)}async function bh(r,l,i,u=100){return V(`/api/bigquery/projects/${encodeURIComponent(r)}/datasets/${encodeURIComponent(l)}/tables/${encodeURIComponent(i)}/rows?limit=${encodeURIComponent(String(u))}`)}async function Sh(r){return V(`/api/bigquery/projects/${encodeURIComponent(r)}/jobs`)}async function Nh(r,l){return V(`/api/bigquery/projects/${encodeURIComponent(r)}/jobs/${encodeURIComponent(l)}`)}async function wh(r,l){return V(`/api/bigquery/projects/${encodeURIComponent(r)}/queries`,{method:"POST",body:l,timeoutMs:15e3})}async function kh(r,l){return V(`/api/bigquery/projects/${encodeURIComponent(r)}/datasets`,{method:"POST",body:l})}async function Ch(r,l,i){return V(`/api/bigquery/projects/${encodeURIComponent(r)}/datasets/${encodeURIComponent(l)}/tables`,{method:"POST",body:i})}async function Rh(r,l,i,u){const d=u?"?deleteContents=true":"";return mn(`/api/bigquery/projects/${encodeURIComponent(r)}/datasets/${encodeURIComponent(l)}${d}`,{method:"DELETE",body:{confirmation:i}})}async function Dh(r,l,i,u){return mn(`/api/bigquery/projects/${encodeURIComponent(r)}/datasets/${encodeURIComponent(l)}/tables/${encodeURIComponent(i)}`,{method:"DELETE",body:{confirmation:u}})}async function Ih(r,l,i,u){return V(`/api/bigquery/projects/${encodeURIComponent(r)}/datasets/${encodeURIComponent(l)}/tables/${encodeURIComponent(i)}/insertAll`,{method:"POST",body:u})}function Eh({activeDatasetId:r,activeTableId:l,datasets:i,onSelectDataset:u,onSelectTable:d}){return i.length===0?n.jsx(F,{title:"No datasets",description:"Datasets created through the BigQuery API will appear here."}):n.jsx("div",{className:"dynamodb-table-list","aria-label":"BigQuery datasets",children:i.map(p=>n.jsxs("section",{className:p.datasetId===r?"dynamodb-table-row active":"dynamodb-table-row",children:[n.jsxs("button",{className:"object-select",onClick:()=>u(p),children:[n.jsxs("span",{className:"table-row-top",children:[n.jsx("span",{className:"table-row-name",children:p.datasetId}),n.jsx("span",{className:"count-pill",children:p.tables.length})]}),n.jsxs("span",{className:"table-row-meta",children:[p.location||"default location"," dataset"]})]}),n.jsxs("span",{className:"table-row-tags",children:[p.tables.length===0?n.jsx("span",{children:"no tables"}):null,p.tables.map(g=>n.jsxs("button",{className:"attribute-chip",onClick:()=>d(g),type:"button",children:[g.tableId===l?">":""," ",g.tableId," (",g.numRows,")"]},g.tableId))]})]},p.datasetId))})}function Th(r){return{status:"disabled",running:!1,endpoint:(r==null?void 0:r.endpoint)??"http://127.0.0.1:19050",project:"devcloud",location:"US",authMode:"relaxed",storagePath:(r==null?void 0:r.storagePath)??".devcloud/data/bigquery",datasetCount:0,jobCount:0}}function $h(r){const l=r.fields??[];return l.length===0?"No schema fields":l.map(i=>`${i.name} ${i.type??"STRING"} ${i.mode??"NULLABLE"}`).join(" / ")}function Lh(r){return[...r].sort((l,i)=>Ru(i)-Ru(l)).slice(0,8)}function Ru(r){var u,d;const l=(d=(u=r.job)==null?void 0:u.statistics)==null?void 0:d.creationTime;if(!l)return 0;const i=Number.parseInt(l,10);return Number.isFinite(i)?i:0}function Ph(r){var p,g;const l=(g=(p=r.job)==null?void 0:p.statistics)==null?void 0:g.query,i=(l==null?void 0:l.totalRows)??"0",u=l!=null&&l.dryRun?"dry run":"executed",d=l!=null&&l.cacheHit?"cache hit":"cache miss";return`${u} / ${i} rows / ${d}`}function Mh(r){const l=r.job??r,i=JSON.parse(JSON.stringify(l)),u=i.configuration;return Vs(u)&&Vs(u.query)&&delete u.query.queryParameters,i}function Oh(r,l){return l?l.f.reduce((i,u,d)=>{var p;return i[((p=r[d])==null?void 0:p.name)??`column_${d+1}`]=u.v,i},{}):{}}function Du(r,l){try{const i=JSON.parse(r);return Vs(i)?i:new Error(`${l} must be a JSON object.`)}catch{return new Error(`${l} has a JSON validation error.`)}}function Ah(r){const l=r.split(` -`).map(u=>u.trim()).filter(Boolean).map(u=>{const[d,p="STRING",g]=u.split(":").map(y=>y.trim());return d?{name:d,type:p||"STRING",mode:g||void 0}:new Error("Schema field names are required.")}),i=l.find(u=>u instanceof Error);return i||{fields:l}}function _h(r,l){let i;try{i=JSON.parse(r)}catch{return new Error("Row JSON has a JSON validation error.")}if(Array.isArray(i)){const u=[];for(const[d,p]of i.entries()){if(!Vs(p))return new Error(`Row ${d+1} must be a JSON object.`);u.push({json:p})}return u}return Vs(i)?[{insertId:l||void 0,json:i}]:new Error("Row JSON must be a JSON object or an array of JSON objects.")}function Iu(r,l){let i=r;for(const u of l){if(!Vs(i))return;i=i[u]}return typeof i=="string"&&i.trim()!==""?i.trim():void 0}function Vs(r){return typeof r=="object"&&r!==null&&!Array.isArray(r)}function Wu(r){return r==null?"null":typeof r=="object"?JSON.stringify(r):String(r)}function Bh({dataset:r,jobs:l,project:i,row:u,status:d,table:p}){const[g,y]=m.useState(),[S,v]=m.useState({status:"idle"}),N=m.useMemo(()=>Lh(l),[l]);return m.useEffect(()=>{if(l.length===0){y(void 0),v({status:"idle"});return}y(w=>w&&l.some(L=>L.jobId===w)?w:l[0].jobId)},[l]),m.useEffect(()=>{if(!g){v({status:"idle"});return}v({status:"loading",jobId:g}),Nh(i,g).then(w=>{v({status:"success",job:w.job})}).catch(w=>{v({status:"error",message:w.message})})},[i,g]),r?n.jsxs("div",{className:"dynamodb-inspector",children:[n.jsxs("section",{children:[n.jsx("span",{className:"inspector-label",children:"Catalog"}),n.jsx("h3",{children:p?`${r.datasetId}.${p.tableId}`:r.datasetId}),n.jsxs("dl",{className:"inspector-list",children:[n.jsxs("div",{children:[n.jsx("dt",{children:"Project"}),n.jsx("dd",{children:i})]}),n.jsxs("div",{children:[n.jsx("dt",{children:"Endpoint"}),n.jsx("dd",{children:n.jsx("code",{children:(d==null?void 0:d.endpoint)??"unknown"})})]}),n.jsxs("div",{children:[n.jsx("dt",{children:"Location"}),n.jsx("dd",{children:(d==null?void 0:d.location)??r.location??"unknown"})]}),n.jsxs("div",{children:[n.jsx("dt",{children:"Rows"}),n.jsx("dd",{children:(p==null?void 0:p.numRows)??"select a table"})]}),n.jsxs("div",{children:[n.jsx("dt",{children:"Schema"}),n.jsx("dd",{children:p?$h(p.schema):"select a table"})]}),n.jsxs("div",{children:[n.jsx("dt",{children:"Jobs"}),n.jsx("dd",{children:l.length===0?"none":l.map(w=>`${w.jobId} ${w.state}`).join(", ")})]})]})]}),n.jsxs("section",{children:[n.jsx("span",{className:"inspector-label",children:"Recent query metadata"}),N.length===0?n.jsx("p",{className:"inspector-muted",children:"Query jobs will appear after running SQL."}):n.jsx("div",{className:"dynamodb-table-list compact-list","aria-label":"BigQuery recent query jobs",children:N.map(w=>n.jsxs("button",{className:w.jobId===g?"object-select active":"object-select",onClick:()=>y(w.jobId),type:"button",children:[n.jsxs("span",{className:"table-row-top",children:[n.jsx("span",{className:"table-row-name",children:w.jobId}),n.jsx("span",{className:"count-pill",children:w.state})]}),n.jsx("span",{className:"table-row-meta",children:Ph(w)})]},w.jobId))})]}),n.jsxs("section",{children:[n.jsx("span",{className:"inspector-label",children:"Selected job JSON"}),S.status==="loading"?n.jsx("p",{className:"inspector-muted",children:"Loading job detail."}):null,S.status==="error"?n.jsx("p",{className:"operation-message error",children:S.message}):null,S.status==="success"?n.jsx("pre",{className:"mail-preview",children:JSON.stringify(Mh(S.job),null,2)}):null]}),n.jsxs("section",{children:[n.jsx("span",{className:"inspector-label",children:"Selected row"}),u?n.jsx("pre",{className:"mail-preview",children:JSON.stringify(u.json,null,2)}):n.jsx("p",{className:"inspector-muted",children:"Select a row to inspect JSON."})]})]}):n.jsx(F,{title:"Inspector",description:"Dataset, table schema, selected row, and jobs will appear here."})}function Fh({activeDatasetId:r,activeTableId:l,disabled:i,onMutationSuccess:u,projectId:d}){var Et;const[p,g]=m.useState(""),[y,S]=m.useState("US"),[v,N]=m.useState(""),[w,L]=m.useState(!1),[C,A]=m.useState(`{ +}`;function Wm(r){return{status:"disabled",running:!1,endpoint:(r==null?void 0:r.endpoint)??"http://127.0.0.1:18000",region:"us-east-1",storagePath:(r==null?void 0:r.storagePath)??".devcloud/data/dynamodb",tableCount:0}}function Ql(r){const i=r.keySchema??[];return i.length===0?"No key schema":i.map(l=>`${l.AttributeName} ${l.KeyType}`).join(" / ")}function Gm(r){return(r.globalSecondaryIndexes??[]).length+(r.localSecondaryIndexes??[]).length}function Ym(r){const i=r.attributeDefinitions??[];return i.length===0?"none":i.map(l=>`${l.AttributeName} ${l.AttributeType}`).join(", ")}function Xm(r){const i=r.timeToLiveDescription;return!i||i.TimeToLiveStatus===""?"not configured":i.AttributeName?`${i.TimeToLiveStatus} on ${i.AttributeName}`:i.TimeToLiveStatus}function Zm(r){var l;if(!r.streamEnabled)return"disabled";const i=((l=r.streamSpecification)==null?void 0:l.StreamViewType)??"enabled";return r.latestStreamLabel?`${i} (${r.latestStreamLabel})`:i}function Pl(r){const i=Number.parseInt(r,10);return!Number.isFinite(i)||i<=0?100:Math.min(i,1e3)}function ps(r){try{const i=JSON.parse(r);return!i||Array.isArray(i)||typeof i!="object"?{ok:!1,message:"Input must be a JSON object."}:{ok:!0,value:i}}catch(i){return{ok:!1,message:i instanceof Error?i.message:"Input must be valid JSON."}}}function eh(r){if(r.trim()==="")return{ok:!0};const i=ps(r);return i.ok?{ok:!0,value:i.value}:i}function th(r){return r==null?"null":typeof r=="object"?JSON.stringify(r):String(r)}function nh(r){return r==null?"":typeof r=="string"?r:typeof r=="number"||typeof r=="boolean"?String(r):JSON.stringify(r)}function sh(r){return!Number.isFinite(r)||r<0?"unknown":r<1024?`${r} B`:`${(r/1024).toFixed(1)} KB`}function rh(){if(typeof window>"u")return[];try{const r=window.localStorage.getItem(_l);if(!r)return[];const i=JSON.parse(r);return Array.isArray(i)?i.filter(ih).slice(0,zl):[]}catch{return[]}}function ah(r){if(!(typeof window>"u"))try{if(r.length===0){window.localStorage.removeItem(_l);return}window.localStorage.setItem(_l,JSON.stringify(r.slice(0,zl)))}catch{}}function ih(r){if(!r||Array.isArray(r)||typeof r!="object")return!1;const i=r;return(i.operation==="Query"||i.operation==="Scan")&&typeof i.id=="string"&&typeof i.tableName=="string"&&typeof i.limit=="number"&&typeof i.expressionSummary=="string"&&typeof i.count=="number"&&typeof i.scannedCount=="number"&&typeof i.page=="number"&&typeof i.hasMore=="boolean"&&typeof i.createdAt=="string"}function lh(r){return{id:`${Date.now()}-${r.mode}-${r.tableName}-${r.page}`,operation:r.mode,tableName:r.tableName,indexName:r.indexName.trim()||void 0,limit:r.limit,expressionSummary:r.usesExpressionAttributeValues?"expression values present":"no expression values",count:r.count,scannedCount:r.scannedCount,page:r.page,hasMore:r.hasMore,createdAt:new Date().toISOString()}}function oh(r){const i=new Date(r);return Number.isNaN(i.getTime())?"unknown":i.toLocaleString()}function ch({activeIndex:r,items:i,itemsState:l,onSelectIndex:u,tableName:d}){return d?l.status==="loading"?n.jsx(A,{title:"Loading items",description:`Reading items from ${d}.`}):l.status==="error"?n.jsx(A,{title:"DynamoDB items unavailable",description:l.message}):i.length===0?n.jsx(A,{title:"No items",description:`No loaded items in ${d} match the current filter.`}):n.jsx("div",{className:"dynamodb-item-table-wrap",children:n.jsxs("table",{className:"dynamodb-item-table",children:[n.jsx("thead",{children:n.jsxs("tr",{children:[n.jsx("th",{scope:"col",children:"Key"}),n.jsx("th",{scope:"col",children:"Attributes"}),n.jsx("th",{scope:"col",children:"Size"})]})}),n.jsx("tbody",{children:i.map((p,x)=>n.jsxs("tr",{className:x===r?"item-row active":"item-row",onClick:()=>u(x),children:[n.jsx("td",{children:n.jsx("code",{children:JSON.stringify(p.key)})}),n.jsx("td",{children:n.jsx(Hu,{item:p.item})}),n.jsx("td",{children:sh(JSON.stringify(p.item).length)})]},`${JSON.stringify(p.key)}-${x}`))})]})}):n.jsx(A,{title:"No table selected",description:"Choose a table to inspect its stored items."})}function Hu({item:r}){const i=Object.entries(r).filter(([l])=>l!=="pk"&&l!=="sk").slice(0,6);return i.length===0?n.jsx("span",{className:"service-status",children:"key only"}):n.jsx("span",{className:"attribute-preview",children:i.map(([l,u])=>n.jsxs("span",{className:"attribute-chip",children:[l,": ",th(u)]},l))})}function uh({message:r,onFind:i,onUpdateValue:l,table:u,values:d}){const p=(u==null?void 0:u.keySchema)??[];return!u||p.length===0?null:n.jsxs("div",{className:"dynamodb-key-lookup",children:[n.jsx("span",{className:"inspector-label",children:"Key lookup"}),n.jsxs("div",{className:"pubsub-action-row",children:[p.map(x=>n.jsxs("label",{className:"compact-filter",children:[n.jsxs("span",{children:[x.AttributeName," ",x.KeyType]}),n.jsx("input",{"aria-label":`DynamoDB key lookup ${x.AttributeName}`,onChange:g=>l(x.AttributeName,g.target.value),placeholder:x.AttributeName,value:d[x.AttributeName]??""})]},x.AttributeName)),n.jsx(ne,{onClick:i,children:"Find loaded item"})]}),r?n.jsx("p",{className:"inspector-muted",children:r}):null]})}function dh({activeTableName:r,busyOperation:i,createTableJSON:l,putItemJSON:u,updateItemJSON:d,deleteItemJSON:p,ttlJSON:x,deleteTableJSON:g,deleteItemConfirmation:S,deleteTableConfirmation:v,deleteItemAcknowledged:N,deleteTableAcknowledged:w,onCreateTable:L,onPutItem:C,onUpdateItem:_,onDeleteItem:I,onUpdateTTL:E,onDeleteTable:J,setCreateTableJSON:de,setPutItemJSON:V,setUpdateItemJSON:$,setDeleteItemJSON:oe,setTTLJSON:me,setDeleteTableJSON:Z,setDeleteItemConfirmation:Re,setDeleteTableConfirmation:ee,setDeleteItemAcknowledged:pe,setDeleteTableAcknowledged:Se}){const he=!r;return n.jsxs("div",{className:"dynamodb-operation-stack",children:[n.jsx(Qs,{buttonLabel:"Create table",disabled:i==="create-table",json:l,label:"CreateTable input",onChange:de,onSubmit:L}),n.jsx(Qs,{buttonLabel:"Put item",disabled:he||i==="put-item",json:u,label:"PutItem input",onChange:V,onSubmit:C}),n.jsx(Qs,{buttonLabel:"Update item",disabled:he||i==="update-item",json:d,label:"UpdateItem input",onChange:$,onSubmit:_}),n.jsx(Qs,{buttonLabel:"Update TTL",disabled:he||i==="ttl",json:x,label:"UpdateTimeToLive input",onChange:me,onSubmit:E}),n.jsx(Qs,{buttonClassName:"danger",buttonLabel:"Delete item",confirmation:S,destructiveAcknowledgement:N,destructiveAcknowledgementLabel:"Step 1: I understand this deletes the item identified by the JSON key.",confirmationLabel:`Type ${r??"table name"} to delete item`,disabled:he||!N||S!==r||i==="delete-item",json:p,label:"DeleteItem input",onChange:oe,onDestructiveAcknowledgementChange:pe,onConfirmationChange:Re,onSubmit:I}),n.jsx(Qs,{buttonClassName:"danger",buttonLabel:"Delete table",confirmation:v,destructiveAcknowledgement:w,destructiveAcknowledgementLabel:"Step 1: I understand this deletes the selected table and its local items.",confirmationLabel:`Type ${r??"table name"} to delete table`,disabled:he||!w||v!==r||i==="delete-table",json:g,label:"DeleteTable input",onChange:Z,onDestructiveAcknowledgementChange:Se,onConfirmationChange:ee,onSubmit:J})]})}function Qs({buttonClassName:r,buttonLabel:i,confirmation:l,confirmationLabel:u,destructiveAcknowledgement:d,destructiveAcknowledgementLabel:p,disabled:x,json:g,label:S,onChange:v,onConfirmationChange:N,onDestructiveAcknowledgementChange:w,onSubmit:L}){return n.jsxs("form",{className:"dynamodb-operation-form",onSubmit:L,children:[n.jsxs("label",{className:"redshift-sql-editor",children:[n.jsx("span",{children:S}),n.jsx("textarea",{"aria-label":S,onChange:C=>v(C.target.value),spellCheck:!1,value:g})]}),w?n.jsxs("label",{className:"destructive-confirmation",children:[n.jsx("input",{"aria-label":p,checked:!!d,onChange:C=>w(C.target.checked),type:"checkbox"}),n.jsx("span",{children:p})]}):null,N?n.jsxs("label",{className:"compact-filter",children:[n.jsxs("span",{children:["Step 2: ",u]}),n.jsx("input",{"aria-label":u,onChange:C=>N(C.target.value),value:l??""})]}):null,n.jsx(ne,{className:r,disabled:x,type:"submit",children:i})]})}function fh({activeTableName:r,busyOperation:i,error:l,expressionAttributeValues:u,filterExpression:d,indexName:p,keyConditionExpression:x,limit:g,message:S,mode:v,onNextPage:N,onPreviousPage:w,onSubmit:L,pageIndex:C,result:_,selectedItemIndex:I,setExpressionAttributeValues:E,setFilterExpression:J,setIndexName:de,setKeyConditionExpression:V,setLimit:$,setMode:oe,setSelectedItemIndex:me}){const Z=!r||i==="query"||i==="scan",Re=(_==null?void 0:_.Items)??[],ee=C>0,pe=!!(_!=null&&_.LastEvaluatedKey),Se=Re[Math.min(I,Math.max(Re.length-1,0))];return n.jsxs("div",{className:"dynamodb-query-scan",children:[l?n.jsx("p",{className:"operation-message error",children:l}):null,S?n.jsx("p",{className:"operation-message success",children:S}):null,n.jsxs("form",{className:"dynamodb-query-scan-form",onSubmit:L,children:[n.jsxs("div",{className:"segmented-control","aria-label":"DynamoDB read operation",children:[n.jsx("button",{className:v==="Query"?"active":"",onClick:()=>oe("Query"),type:"button",children:"Query"}),n.jsx("button",{className:v==="Scan"?"active":"",onClick:()=>oe("Scan"),type:"button",children:"Scan"})]}),n.jsxs("label",{className:"compact-filter",children:[n.jsx("span",{children:"TableName"}),n.jsx("input",{"aria-label":"DynamoDB Query Scan table name",disabled:!0,value:r??""})]}),n.jsxs("label",{className:"compact-filter",children:[n.jsx("span",{children:"IndexName"}),n.jsx("input",{"aria-label":"DynamoDB Query Scan index name",onChange:he=>de(he.target.value),placeholder:"optional",value:p})]}),n.jsxs("label",{className:"compact-filter small",children:[n.jsx("span",{children:"Limit"}),n.jsx("input",{"aria-label":"DynamoDB Query Scan limit",inputMode:"numeric",onChange:he=>$(he.target.value),value:g})]}),v==="Query"?n.jsxs("label",{className:"compact-filter wide",children:[n.jsx("span",{children:"KeyConditionExpression"}),n.jsx("input",{"aria-label":"DynamoDB Query key condition expression",onChange:he=>V(he.target.value),placeholder:"pk = :pk",value:x})]}):n.jsxs("label",{className:"compact-filter wide",children:[n.jsx("span",{children:"FilterExpression"}),n.jsx("input",{"aria-label":"DynamoDB Scan filter expression",onChange:he=>J(he.target.value),placeholder:"optional",value:d})]}),n.jsxs("label",{className:"redshift-sql-editor",children:[n.jsx("span",{children:"ExpressionAttributeValues JSON"}),n.jsx("textarea",{"aria-label":"DynamoDB Query Scan expression attribute values JSON",onChange:he=>E(he.target.value),spellCheck:!1,value:u})]}),n.jsxs(ne,{disabled:Z,type:"submit",children:["Run ",v]})]}),_?n.jsxs("div",{className:"redshift-query-result",children:[n.jsxs("div",{className:"dynamodb-toolbar",children:[n.jsxs("span",{className:"toolbar-count",children:["Page ",C+1," / Count ",_.Count??Re.length," / Scanned ",_.ScannedCount??0]}),n.jsx("span",{className:"toolbar-count",children:pe?"More results available":"End of results"}),n.jsxs("div",{className:"pubsub-action-row",children:[n.jsx(ne,{disabled:Z||!ee,onClick:w,type:"button",children:"Previous page"}),n.jsx(ne,{disabled:Z||!pe,onClick:N,type:"button",children:"Next page"})]})]}),Re.length>0?n.jsxs("div",{className:"dynamodb-query-result-grid",children:[n.jsx("div",{className:"dynamodb-item-table-wrap",children:n.jsxs("table",{className:"dynamodb-item-table compact",children:[n.jsx("thead",{children:n.jsxs("tr",{children:[n.jsx("th",{scope:"col",children:"#"}),n.jsx("th",{scope:"col",children:"Item preview"})]})}),n.jsx("tbody",{children:Re.map((he,z)=>n.jsxs("tr",{className:z===I?"item-row active":"item-row",onClick:()=>me(z),children:[n.jsx("td",{children:z+1}),n.jsx("td",{children:n.jsx(Hu,{item:he})})]},`${C}-${z}-${JSON.stringify(he).length}`))})]})}),n.jsxs("section",{children:[n.jsx("span",{className:"inspector-label",children:"Selected result item JSON"}),n.jsx("pre",{className:"mail-preview",children:JSON.stringify(Se,null,2)})]})]}):n.jsx("p",{className:"inspector-muted",children:"No items returned."})]}):null]})}function ph({operations:r,onClear:i}){return n.jsxs("section",{className:"dynamodb-recent-operations",children:[n.jsxs("div",{className:"inspector-heading",children:[n.jsxs("div",{children:[n.jsx("span",{className:"inspector-label",children:"Recent operation history"}),n.jsx("p",{className:"inspector-muted",children:"Stored locally without item payloads, credentials, or pagination keys."})]}),n.jsx(ne,{disabled:r.length===0,onClick:i,type:"button",children:"Clear"})]}),r.length===0?n.jsx("p",{className:"inspector-muted",children:"Run Query or Scan to record local operation metadata."}):n.jsx("div",{className:"dynamodb-item-table-wrap",children:n.jsxs("table",{className:"dynamodb-item-table compact",children:[n.jsx("thead",{children:n.jsxs("tr",{children:[n.jsx("th",{scope:"col",children:"When"}),n.jsx("th",{scope:"col",children:"Operation"}),n.jsx("th",{scope:"col",children:"Table"}),n.jsx("th",{scope:"col",children:"Result"})]})}),n.jsx("tbody",{children:r.map(l=>n.jsxs("tr",{children:[n.jsx("td",{children:oh(l.createdAt)}),n.jsx("td",{children:n.jsxs("span",{className:"attribute-preview",children:[n.jsx("span",{className:"attribute-chip",children:l.operation}),n.jsxs("span",{className:"attribute-chip",children:["page ",l.page]}),l.indexName?n.jsxs("span",{className:"attribute-chip",children:["index: ",l.indexName]}):null]})}),n.jsx("td",{children:n.jsx("code",{children:l.tableName})}),n.jsx("td",{children:n.jsxs("span",{className:"attribute-preview",children:[n.jsxs("span",{className:"attribute-chip",children:["count: ",l.count]}),n.jsxs("span",{className:"attribute-chip",children:["scanned: ",l.scannedCount]}),n.jsxs("span",{className:"attribute-chip",children:["limit: ",l.limit]}),n.jsx("span",{className:"attribute-chip",children:l.hasMore?"has next page":"last page"}),n.jsx("span",{className:"attribute-chip",children:l.expressionSummary})]})})]},l.id))})]})})]})}function mh({detailState:r,table:i,item:l,onRefreshDetail:u,status:d}){var N;if(!i)return n.jsx(A,{title:"Inspector",description:"Table schema and selected item JSON will appear here."});const p=r.status==="success"?r.table:i,x=r.status==="success"?r.globalSecondaryIndexes:i.globalSecondaryIndexes??[],g=r.status==="success"?r.localSecondaryIndexes:i.localSecondaryIndexes??[],S=r.status==="success"?r.ttl:i.timeToLiveDescription,v=r.status==="success"?r.streams:{tableName:i.tableName,streamEnabled:((N=i.streamSpecification)==null?void 0:N.StreamEnabled)??!1,latestStreamArn:i.latestStreamArn,latestStreamLabel:i.latestStreamLabel,streamSpecification:i.streamSpecification};return n.jsxs("div",{className:"dynamodb-inspector",children:[n.jsxs("section",{children:[n.jsxs("div",{className:"inspector-heading",children:[n.jsxs("div",{children:[n.jsx("span",{className:"inspector-label",children:"Table"}),n.jsx("h3",{children:p.tableName})]}),n.jsx(ne,{onClick:u,children:"Refresh detail"})]}),r.status==="loading"?n.jsx("p",{className:"inspector-muted",children:"Loading detail metadata."}):null,r.status==="error"?n.jsx("p",{className:"operation-message error",children:r.message}):null,n.jsxs("dl",{className:"inspector-list",children:[n.jsxs("div",{children:[n.jsx("dt",{children:"Status"}),n.jsx("dd",{children:p.tableStatus})]}),n.jsxs("div",{children:[n.jsx("dt",{children:"Endpoint"}),n.jsx("dd",{children:n.jsx("code",{children:(d==null?void 0:d.endpoint)??"unknown"})})]}),n.jsxs("div",{children:[n.jsx("dt",{children:"Region"}),n.jsx("dd",{children:(d==null?void 0:d.region)??"unknown"})]}),n.jsxs("div",{children:[n.jsx("dt",{children:"Key schema"}),n.jsx("dd",{children:Ql(p)})]}),n.jsxs("div",{children:[n.jsx("dt",{children:"Attributes"}),n.jsx("dd",{children:Ym(p)})]}),n.jsxs("div",{children:[n.jsx("dt",{children:"TTL"}),n.jsx("dd",{children:Xm({...p,timeToLiveDescription:S})})]}),n.jsxs("div",{children:[n.jsx("dt",{children:"Streams"}),n.jsx("dd",{children:Zm(v)})]})]})]}),n.jsxs("section",{children:[n.jsx("span",{className:"inspector-label",children:"Indexes"}),n.jsx(hh,{globalSecondaryIndexes:x,localSecondaryIndexes:g})]}),n.jsxs("section",{children:[n.jsx("span",{className:"inspector-label",children:"Streams"}),n.jsx("pre",{className:"mail-preview",children:JSON.stringify(v,null,2)})]}),n.jsxs("section",{children:[n.jsx("span",{className:"inspector-label",children:"Selected item"}),l?n.jsx("pre",{className:"mail-preview",children:JSON.stringify(l.item,null,2)}):n.jsx("p",{className:"inspector-muted",children:"Select an item row to inspect JSON."})]})]})}function hh({globalSecondaryIndexes:r,localSecondaryIndexes:i}){const l=[...r.map(u=>({...u,type:"GSI"})),...i.map(u=>({...u,type:"LSI"}))];return l.length===0?n.jsx("p",{className:"inspector-muted",children:"No secondary indexes."}):n.jsx("div",{className:"dynamodb-item-table-wrap",children:n.jsxs("table",{className:"dynamodb-item-table compact",children:[n.jsx("thead",{children:n.jsxs("tr",{children:[n.jsx("th",{scope:"col",children:"Type"}),n.jsx("th",{scope:"col",children:"Name"}),n.jsx("th",{scope:"col",children:"Key schema"}),n.jsx("th",{scope:"col",children:"Items"})]})}),n.jsx("tbody",{children:l.map(u=>n.jsxs("tr",{children:[n.jsx("td",{children:u.type}),n.jsx("td",{children:u.IndexName}),n.jsx("td",{children:Ql({tableName:u.IndexName,keySchema:u.KeySchema})}),n.jsx("td",{children:u.ItemCount??0})]},`${u.type}-${u.IndexName}`))})]})})}function xh({tables:r,activeTableName:i,onSelectTable:l}){return r.length===0?n.jsx(A,{title:"No tables",description:"Tables created through the DynamoDB API will appear here."}):n.jsx("div",{className:"dynamodb-table-list","aria-label":"DynamoDB tables",children:r.map(u=>{var d;return n.jsxs("button",{className:u.tableName===i?"dynamodb-table-row active":"dynamodb-table-row",onClick:()=>l(u.tableName),children:[n.jsxs("span",{className:"table-row-top",children:[n.jsx("span",{className:"table-row-name",children:u.tableName}),n.jsx("span",{className:"count-pill",children:u.itemCount})]}),n.jsx("span",{className:"table-row-meta",children:Ql(u)}),n.jsxs("span",{className:"table-row-tags",children:[n.jsx("span",{children:u.tableStatus}),n.jsxs("span",{children:[Gm(u)," indexes"]}),n.jsx("span",{children:(d=u.streamSpecification)!=null&&d.StreamEnabled?"streams on":"streams off"})]})]},u.tableName)})})}function gh({service:r}){const[i,l]=m.useState({status:"loading"}),[u,d]=m.useState({status:"idle"}),[p,x]=m.useState({status:"idle"}),[g,S]=m.useState(),[v,N]=m.useState(0),[w,L]=m.useState(""),[C,_]=m.useState(""),[I,E]=m.useState("100"),[J,de]=m.useState({}),[V,$]=m.useState(""),[oe,me]=m.useState(Um),[Z,Re]=m.useState(qm),[ee,pe]=m.useState(Vm),[Se,he]=m.useState(Km),[z,H]=m.useState(Jm),[be,Pe]=m.useState("{}"),[Ne,P]=m.useState(""),[U,B]=m.useState(""),[j,T]=m.useState(!1),[se,ce]=m.useState(!1),[xe,we]=m.useState(""),[De,re]=m.useState(""),[Ie,Qe]=m.useState(),[tt,Dt]=m.useState("Query"),[ut,fe]=m.useState(""),[ke,Y]=m.useState("25"),[je,rt]=m.useState("pk = :pk"),[at,jt]=m.useState(""),[It,Ze]=m.useState(Hm),[Et,K]=m.useState(""),[Be,Ue]=m.useState(""),[Le,kt]=m.useState(),[_e,vt]=m.useState({pageHistory:[void 0],pageIndex:0,selectedItemIndex:0}),[Ae,hn]=m.useState(rh),lt=(r==null?void 0:r.status)==="disabled";m.useEffect(()=>{ah(Ae)},[Ae]);const Xn=ht(),Tt=m.useCallback(()=>{const ie=Xn();if(lt){l({status:"success",statusPayload:Wm(r),tables:[]}),d({status:"idle"}),x({status:"idle"}),S(void 0);return}l({status:"loading"}),Promise.all([Dm(),Im()]).then(([W,{tables:$e}])=>{ie()&&(l({status:"success",statusPayload:W,tables:$e}),S(He=>{var Ge;return He&&$e.some(qe=>qe.tableName===He)?He:(Ge=$e[0])==null?void 0:Ge.tableName}))}).catch(W=>{ie()&&l({status:"error",message:W.message})})},[Xn,lt,r]);m.useEffect(()=>{Tt()},[Tt]),Vt({topics:["dynamodb"],onEvent:Tt,onResync:Tt,enabled:!lt});const an=i.status==="success"?i.tables:[],dt=an.find(ie=>ie.tableName===g),Pt=ht(),ln=m.useCallback(()=>{const ie=Pt();if(!g||lt){d({status:"idle"});return}d({status:"loading"}),Tm(g,Pl(I)).then(({items:W})=>{ie()&&(N(0),d({status:"success",items:W}))}).catch(W=>{ie()&&d({status:"error",message:W.message})})},[Pt,g,lt,I]);m.useEffect(()=>{ln()},[ln]);const In=ht(),xt=m.useCallback(()=>{const ie=In();if(!g||lt){x({status:"idle"});return}x({status:"loading"}),Promise.all([Am(g),_m(g),Bm(g),Qm(g)]).then(([W,$e,He,Ge])=>{ie()&&x({status:"success",table:W.table,globalSecondaryIndexes:$e.globalSecondaryIndexes??[],localSecondaryIndexes:$e.localSecondaryIndexes??[],ttl:He.timeToLiveDescription,streams:Ge})}).catch(W=>{ie()&&x({status:"error",message:W.message})})},[In,g,lt]);m.useEffect(()=>{xt()},[xt]);const on=m.useMemo(()=>{const ie=w.trim().toLowerCase();return ie===""?an:an.filter(W=>W.tableName.toLowerCase().includes(ie))},[an,w]),xn=m.useMemo(()=>{const ie=u.status==="success"?u.items:[],W=C.trim().toLowerCase();return W===""?ie:ie.filter($e=>JSON.stringify($e).toLowerCase().includes(W))},[u,C]),hs=xn[Math.min(v,Math.max(xn.length-1,0))];if(lt)return n.jsx(ue,{title:"DynamoDB",children:n.jsx(A,{title:"DynamoDB is disabled",description:"Enable the DynamoDB service in devcloud config to inspect tables and items."})});function xs(ie){S(ie),N(0),_(""),de({}),$(""),T(!1),ce(!1),P(""),B("")}function gs(ie,W){de($e=>({...$e,[ie]:W})),$("")}function Ee(){var He;if(!dt||u.status!=="success"){$("Load table items before using key lookup.");return}const W=(((He=dt.keySchema)==null?void 0:He.map(Ge=>Ge.AttributeName))??[]).map(Ge=>{var qe;return[Ge,((qe=J[Ge])==null?void 0:qe.trim())??""]}).filter(([,Ge])=>Ge!=="");if(W.length===0){$("Enter at least one key value.");return}const $e=u.items.findIndex(Ge=>W.every(([qe,js])=>nh(Ge.item[qe])===js));if($e<0){$(`No loaded item matched ${W.map(([Ge])=>Ge).join(" / ")}.`);return}_(""),N($e),$(`Selected loaded item ${$e+1}.`)}async function Ce(ie,W){Qe(ie),re(""),we("");try{const $e=await W();we($e),Tt(),ln(),xt()}catch($e){re($e instanceof Error?$e.message:"DynamoDB operation failed")}finally{Qe(void 0)}}function Fe(ie){ie.preventDefault();const W=ps(oe);if(!W.ok){re(W.message);return}const $e=typeof W.value.TableName=="string"?W.value.TableName:"";if($e.trim()===""){re("CreateTable input requires TableName.");return}Ce("create-table",async()=>(await Em(W.value),S($e),`Created table ${$e}.`))}function Ft(ie){if(ie.preventDefault(),!g){re("Select a table before putting an item.");return}const W=ps(Z);if(!W.ok){re(W.message);return}Ce("put-item",async()=>(await Pm(g,W.value),`Put item in ${g}.`))}function En(ie){if(ie.preventDefault(),!g){re("Select a table before updating an item.");return}const W=ps(ee);if(!W.ok){re(W.message);return}Ce("update-item",async()=>(await Lm(g,W.value),`Updated item in ${g}.`))}function gn(ie){if(ie.preventDefault(),!g){re("Select a table before deleting an item.");return}if(!j){re("Acknowledge the DeleteItem destructive action before confirming the table name.");return}if(Ne!==g){re("DeleteItem confirmation must match the selected table name.");return}const W=ps(Se);if(!W.ok){re(W.message);return}Ce("delete-item",async()=>(await $m(g,W.value,Ne),P(""),T(!1),`Deleted item from ${g}.`))}function Ua(ie){if(ie.preventDefault(),!g){re("Select a table before updating TTL.");return}const W=ps(z);if(!W.ok){re(W.message);return}Ce("ttl",async()=>(await Fm(g,W.value),`Updated TTL for ${g}.`))}function Zn(ie){if(ie.preventDefault(),!g){re("Select a table before deleting it.");return}if(!se){re("Acknowledge the DeleteTable destructive action before confirming the table name.");return}if(U!==g){re("DeleteTable confirmation must match the selected table name.");return}const W=ps(be);if(!W.ok){re(W.message);return}Ce("delete-table",async()=>(await zm(g,W.value,U),B(""),ce(!1),S(void 0),`Deleted table ${g}.`))}function ys(ie){if(!g)return{ok:!1,message:"Select a table before running Query or Scan."};const W=Pl(ke),$e=eh(It);if(!$e.ok)return{ok:!1,message:$e.message};const He={Limit:W},Ge=ut.trim();if(Ge!==""&&(He.IndexName=Ge),$e.value&&(He.ExpressionAttributeValues=$e.value),tt==="Query"){const qe=je.trim();if(qe==="")return{ok:!1,message:"Query requires KeyConditionExpression."};He.KeyConditionExpression=qe}else{const qe=at.trim();qe!==""&&(He.FilterExpression=qe)}return ie&&(He.ExclusiveStartKey=ie),{ok:!0,value:He}}async function Tn(ie,W,$e){var Ge;if(!g){Ue("Select a table before running Query or Scan.");return}const He=ys(ie);if(!He.ok){Ue(He.message);return}Qe(tt.toLowerCase()),Ue(""),K(""),kt(void 0);try{const qe=tt==="Query"?await Mm(g,He.value):await Om(g,He.value);kt(qe),vt({pageHistory:$e,pageIndex:W,selectedItemIndex:0}),hn(js=>{var Js;return[lh({count:qe.Count??((Js=qe.Items)==null?void 0:Js.length)??0,hasMore:!!qe.LastEvaluatedKey,indexName:ut,limit:Pl(ke),mode:tt,page:W+1,scannedCount:qe.ScannedCount??0,tableName:g,usesExpressionAttributeValues:!!He.value.ExpressionAttributeValues}),...js].slice(0,zl)}),K(`${tt} page ${W+1} returned ${qe.Count??((Ge=qe.Items)==null?void 0:Ge.length)??0} item(s).`)}catch(qe){Ue(qe instanceof Error?qe.message:`${tt} failed`)}finally{Qe(void 0)}}async function Ks(ie){ie.preventDefault(),await Tn(void 0,0,[void 0])}function qa(){if(!(Le!=null&&Le.LastEvaluatedKey))return;const ie=_e.pageIndex+1,W=_e.pageHistory.slice(0,ie);W[ie]=Le.LastEvaluatedKey,Tn(Le.LastEvaluatedKey,ie,W)}function Va(){if(_e.pageIndex<=0)return;const ie=_e.pageIndex-1,W=_e.pageHistory[ie];Tn(W,ie,_e.pageHistory)}return n.jsxs("div",{className:"dynamodb-workspace",children:[n.jsxs(ue,{title:"Tables",children:[n.jsxs("div",{className:"dynamodb-toolbar",children:[n.jsxs("label",{className:"compact-filter",children:[n.jsx("span",{children:"Filter"}),n.jsx("input",{"aria-label":"Filter DynamoDB tables",onChange:ie=>L(ie.target.value),placeholder:"table name",type:"search",value:w})]}),n.jsx(ne,{onClick:Tt,children:"Refresh"})]}),i.status==="loading"?n.jsx(A,{title:"Loading tables",description:"Reading local DynamoDB table metadata."}):null,i.status==="error"?n.jsx(A,{title:"DynamoDB tables unavailable",description:i.message,actionLabel:"Retry",onAction:Tt}):null,i.status==="success"?n.jsx(xh,{tables:on,activeTableName:g,onSelectTable:xs}):null]}),n.jsxs(ue,{title:"Items",children:[n.jsxs("div",{className:"dynamodb-toolbar",children:[n.jsx("span",{className:"toolbar-count",children:dt?`${xn.length} shown / ${dt.itemCount} reported`:"Select a table"}),n.jsxs("label",{className:"compact-filter",children:[n.jsx("span",{children:"Filter"}),n.jsx("input",{"aria-label":"Filter DynamoDB items",disabled:!dt,onChange:ie=>{N(0),_(ie.target.value)},placeholder:"attribute value",type:"search",value:C})]}),n.jsxs("label",{className:"compact-filter small",children:[n.jsx("span",{children:"Limit"}),n.jsx("input",{"aria-label":"Limit DynamoDB items",disabled:!dt,inputMode:"numeric",onChange:ie=>E(ie.target.value),value:I})]}),n.jsx(ne,{disabled:!dt,onClick:ln,children:"Refresh"})]}),n.jsx(ch,{activeIndex:v,items:xn,itemsState:u,onSelectIndex:N,tableName:g}),n.jsx(uh,{message:V,onFind:Ee,onUpdateValue:gs,table:dt,values:J})]}),n.jsxs(ue,{title:"Operations",children:[De?n.jsx("p",{className:"operation-message error",children:De}):null,xe?n.jsx("p",{className:"operation-message success",children:xe}):null,n.jsx(dh,{activeTableName:g,busyOperation:Ie,createTableJSON:oe,deleteItemConfirmation:Ne,deleteItemAcknowledged:j,deleteItemJSON:Se,deleteTableConfirmation:U,deleteTableAcknowledged:se,deleteTableJSON:be,onCreateTable:Fe,onDeleteItem:gn,onDeleteTable:Zn,onPutItem:Ft,onUpdateItem:En,onUpdateTTL:Ua,putItemJSON:Z,setCreateTableJSON:me,setDeleteItemConfirmation:P,setDeleteItemAcknowledged:T,setDeleteItemJSON:he,setDeleteTableConfirmation:B,setDeleteTableAcknowledged:ce,setDeleteTableJSON:Pe,setPutItemJSON:Re,setTTLJSON:H,setUpdateItemJSON:pe,ttlJSON:z,updateItemJSON:ee})]}),n.jsxs(ue,{title:"Query / Scan",children:[n.jsx(fh,{activeTableName:g,busyOperation:Ie,expressionAttributeValues:It,filterExpression:at,indexName:ut,keyConditionExpression:je,limit:ke,message:Et,mode:tt,onNextPage:qa,onPreviousPage:Va,onSubmit:ie=>{Ks(ie)},pageIndex:_e.pageIndex,result:Le,selectedItemIndex:_e.selectedItemIndex,setExpressionAttributeValues:Ze,setFilterExpression:jt,setIndexName:fe,setKeyConditionExpression:rt,setLimit:Y,setMode:Dt,setSelectedItemIndex:ie=>vt(W=>({...W,selectedItemIndex:ie})),error:Be}),n.jsx(ph,{operations:Ae,onClear:()=>hn([])})]}),n.jsx(ue,{title:"Inspector",children:n.jsx(mh,{detailState:p,item:hs,onRefreshDetail:xt,status:i.status==="success"?i.statusPayload:void 0,table:dt})})]})}async function yh(){return q("/api/bigquery/status")}async function jh(){return q("/api/bigquery/projects")}async function vh(r){return q(`/api/bigquery/projects/${encodeURIComponent(r)}/datasets`)}async function bh(r,i,l,u=100){return q(`/api/bigquery/projects/${encodeURIComponent(r)}/datasets/${encodeURIComponent(i)}/tables/${encodeURIComponent(l)}/rows?limit=${encodeURIComponent(String(u))}`)}async function Sh(r){return q(`/api/bigquery/projects/${encodeURIComponent(r)}/jobs`)}async function Nh(r,i){return q(`/api/bigquery/projects/${encodeURIComponent(r)}/jobs/${encodeURIComponent(i)}`)}async function wh(r,i){return q(`/api/bigquery/projects/${encodeURIComponent(r)}/queries`,{method:"POST",body:i,timeoutMs:15e3})}async function kh(r,i){return q(`/api/bigquery/projects/${encodeURIComponent(r)}/datasets`,{method:"POST",body:i})}async function Ch(r,i,l){return q(`/api/bigquery/projects/${encodeURIComponent(r)}/datasets/${encodeURIComponent(i)}/tables`,{method:"POST",body:l})}async function Rh(r,i,l,u){const d=u?"?deleteContents=true":"";return mn(`/api/bigquery/projects/${encodeURIComponent(r)}/datasets/${encodeURIComponent(i)}${d}`,{method:"DELETE",body:{confirmation:l}})}async function Dh(r,i,l,u){return mn(`/api/bigquery/projects/${encodeURIComponent(r)}/datasets/${encodeURIComponent(i)}/tables/${encodeURIComponent(l)}`,{method:"DELETE",body:{confirmation:u}})}async function Ih(r,i,l,u){return q(`/api/bigquery/projects/${encodeURIComponent(r)}/datasets/${encodeURIComponent(i)}/tables/${encodeURIComponent(l)}/insertAll`,{method:"POST",body:u})}function Eh({activeDatasetId:r,activeTableId:i,datasets:l,onSelectDataset:u,onSelectTable:d}){return l.length===0?n.jsx(A,{title:"No datasets",description:"Datasets created through the BigQuery API will appear here."}):n.jsx("div",{className:"dynamodb-table-list","aria-label":"BigQuery datasets",children:l.map(p=>n.jsxs("section",{className:p.datasetId===r?"dynamodb-table-row active":"dynamodb-table-row",children:[n.jsxs("button",{className:"object-select",onClick:()=>u(p),children:[n.jsxs("span",{className:"table-row-top",children:[n.jsx("span",{className:"table-row-name",children:p.datasetId}),n.jsx("span",{className:"count-pill",children:p.tables.length})]}),n.jsxs("span",{className:"table-row-meta",children:[p.location||"default location"," dataset"]})]}),n.jsxs("span",{className:"table-row-tags",children:[p.tables.length===0?n.jsx("span",{children:"no tables"}):null,p.tables.map(x=>n.jsxs("button",{className:"attribute-chip",onClick:()=>d(x),type:"button",children:[x.tableId===i?">":""," ",x.tableId," (",x.numRows,")"]},x.tableId))]})]},p.datasetId))})}function Th(r){return{status:"disabled",running:!1,endpoint:(r==null?void 0:r.endpoint)??"http://127.0.0.1:19050",project:"devcloud",location:"US",authMode:"relaxed",storagePath:(r==null?void 0:r.storagePath)??".devcloud/data/bigquery",datasetCount:0,jobCount:0}}function Ph(r){const i=r.fields??[];return i.length===0?"No schema fields":i.map(l=>`${l.name} ${l.type??"STRING"} ${l.mode??"NULLABLE"}`).join(" / ")}function Lh(r){return[...r].sort((i,l)=>Ru(l)-Ru(i)).slice(0,8)}function Ru(r){var u,d;const i=(d=(u=r.job)==null?void 0:u.statistics)==null?void 0:d.creationTime;if(!i)return 0;const l=Number.parseInt(i,10);return Number.isFinite(l)?l:0}function $h(r){var p,x;const i=(x=(p=r.job)==null?void 0:p.statistics)==null?void 0:x.query,l=(i==null?void 0:i.totalRows)??"0",u=i!=null&&i.dryRun?"dry run":"executed",d=i!=null&&i.cacheHit?"cache hit":"cache miss";return`${u} / ${l} rows / ${d}`}function Mh(r){const i=r.job??r,l=JSON.parse(JSON.stringify(i)),u=l.configuration;return Vs(u)&&Vs(u.query)&&delete u.query.queryParameters,l}function Oh(r,i){return i?i.f.reduce((l,u,d)=>{var p;return l[((p=r[d])==null?void 0:p.name)??`column_${d+1}`]=u.v,l},{}):{}}function Du(r,i){try{const l=JSON.parse(r);return Vs(l)?l:new Error(`${i} must be a JSON object.`)}catch{return new Error(`${i} has a JSON validation error.`)}}function Ah(r){const i=r.split(` +`).map(u=>u.trim()).filter(Boolean).map(u=>{const[d,p="STRING",x]=u.split(":").map(g=>g.trim());return d?{name:d,type:p||"STRING",mode:x||void 0}:new Error("Schema field names are required.")}),l=i.find(u=>u instanceof Error);return l||{fields:i}}function _h(r,i){let l;try{l=JSON.parse(r)}catch{return new Error("Row JSON has a JSON validation error.")}if(Array.isArray(l)){const u=[];for(const[d,p]of l.entries()){if(!Vs(p))return new Error(`Row ${d+1} must be a JSON object.`);u.push({json:p})}return u}return Vs(l)?[{insertId:i||void 0,json:l}]:new Error("Row JSON must be a JSON object or an array of JSON objects.")}function Iu(r,i){let l=r;for(const u of i){if(!Vs(l))return;l=l[u]}return typeof l=="string"&&l.trim()!==""?l.trim():void 0}function Vs(r){return typeof r=="object"&&r!==null&&!Array.isArray(r)}function Wu(r){return r==null?"null":typeof r=="object"?JSON.stringify(r):String(r)}function Bh({dataset:r,jobs:i,project:l,row:u,status:d,table:p}){const[x,g]=m.useState(),[S,v]=m.useState({status:"idle"}),N=m.useMemo(()=>Lh(i),[i]);return m.useEffect(()=>{if(i.length===0){g(void 0),v({status:"idle"});return}g(w=>w&&i.some(L=>L.jobId===w)?w:i[0].jobId)},[i]),m.useEffect(()=>{if(!x){v({status:"idle"});return}v({status:"loading",jobId:x}),Nh(l,x).then(w=>{v({status:"success",job:w.job})}).catch(w=>{v({status:"error",message:w.message})})},[l,x]),r?n.jsxs("div",{className:"dynamodb-inspector",children:[n.jsxs("section",{children:[n.jsx("span",{className:"inspector-label",children:"Catalog"}),n.jsx("h3",{children:p?`${r.datasetId}.${p.tableId}`:r.datasetId}),n.jsxs("dl",{className:"inspector-list",children:[n.jsxs("div",{children:[n.jsx("dt",{children:"Project"}),n.jsx("dd",{children:l})]}),n.jsxs("div",{children:[n.jsx("dt",{children:"Endpoint"}),n.jsx("dd",{children:n.jsx("code",{children:(d==null?void 0:d.endpoint)??"unknown"})})]}),n.jsxs("div",{children:[n.jsx("dt",{children:"Location"}),n.jsx("dd",{children:(d==null?void 0:d.location)??r.location??"unknown"})]}),n.jsxs("div",{children:[n.jsx("dt",{children:"Rows"}),n.jsx("dd",{children:(p==null?void 0:p.numRows)??"select a table"})]}),n.jsxs("div",{children:[n.jsx("dt",{children:"Schema"}),n.jsx("dd",{children:p?Ph(p.schema):"select a table"})]}),n.jsxs("div",{children:[n.jsx("dt",{children:"Jobs"}),n.jsx("dd",{children:i.length===0?"none":i.map(w=>`${w.jobId} ${w.state}`).join(", ")})]})]})]}),n.jsxs("section",{children:[n.jsx("span",{className:"inspector-label",children:"Recent query metadata"}),N.length===0?n.jsx("p",{className:"inspector-muted",children:"Query jobs will appear after running SQL."}):n.jsx("div",{className:"dynamodb-table-list compact-list","aria-label":"BigQuery recent query jobs",children:N.map(w=>n.jsxs("button",{className:w.jobId===x?"object-select active":"object-select",onClick:()=>g(w.jobId),type:"button",children:[n.jsxs("span",{className:"table-row-top",children:[n.jsx("span",{className:"table-row-name",children:w.jobId}),n.jsx("span",{className:"count-pill",children:w.state})]}),n.jsx("span",{className:"table-row-meta",children:$h(w)})]},w.jobId))})]}),n.jsxs("section",{children:[n.jsx("span",{className:"inspector-label",children:"Selected job JSON"}),S.status==="loading"?n.jsx("p",{className:"inspector-muted",children:"Loading job detail."}):null,S.status==="error"?n.jsx("p",{className:"operation-message error",children:S.message}):null,S.status==="success"?n.jsx("pre",{className:"mail-preview",children:JSON.stringify(Mh(S.job),null,2)}):null]}),n.jsxs("section",{children:[n.jsx("span",{className:"inspector-label",children:"Selected row"}),u?n.jsx("pre",{className:"mail-preview",children:JSON.stringify(u.json,null,2)}):n.jsx("p",{className:"inspector-muted",children:"Select a row to inspect JSON."})]})]}):n.jsx(A,{title:"Inspector",description:"Dataset, table schema, selected row, and jobs will appear here."})}function Fh({activeDatasetId:r,activeTableId:i,disabled:l,onMutationSuccess:u,projectId:d}){var Et;const[p,x]=m.useState(""),[g,S]=m.useState("US"),[v,N]=m.useState(""),[w,L]=m.useState(!1),[C,_]=m.useState(`{ "datasetReference": { "datasetId": "dashboard_ops" }, "location": "US" -}`),[I,E]=m.useState(""),[J,ue]=m.useState(""),[q,P]=m.useState(""),[oe,pe]=m.useState(`event_id:STRING:REQUIRED +}`),[I,E]=m.useState(""),[J,de]=m.useState(""),[V,$]=m.useState(""),[oe,me]=m.useState(`event_id:STRING:REQUIRED created_at:TIMESTAMP -payload:STRING`),[Z,Re]=m.useState(!1),[ee,fe]=m.useState(`{ +payload:STRING`),[Z,Re]=m.useState(!1),[ee,pe]=m.useState(`{ "tableReference": { "tableId": "events" }, @@ -92,18 +92,18 @@ payload:STRING`),[Z,Re]=m.useState(!1),[ee,fe]=m.useState(`{ { "name": "event_id", "type": "STRING", "mode": "REQUIRED" } ] } -}`),[Se,me]=m.useState(""),[z,H]=m.useState(""),[be,$e]=m.useState(""),[Ne,$]=m.useState(`{ +}`),[Se,he]=m.useState(""),[z,H]=m.useState(""),[be,Pe]=m.useState(""),[Ne,P]=m.useState(`{ "event_id": "evt-1", "payload": "local test" -}`),[U,_]=m.useState(""),[j,T]=m.useState(!1),[ne,ce]=m.useState(!1),[ge,we]=m.useState(""),[De,se]=m.useState(""),[Ie,Qe]=m.useState(""),[tt,Dt]=m.useState(!1),[ut,de]=m.useState(""),[ke,Y]=m.useState({status:"idle"});m.useEffect(()=>{r&&(E(K=>K||r),me(K=>K||r),_(K=>K||r),se(K=>K||r))},[r]),m.useEffect(()=>{l&&(H(K=>K||l),Qe(K=>K||l))},[l]);const je=i||ke.status==="running";function rt(K){K.preventDefault();const Be=w?Du(C,"Dataset raw JSON"):{datasetReference:{datasetId:p.trim()},location:y.trim()||void 0,description:v.trim()||void 0};if(Be instanceof Error){Y({status:"error",message:Be.message});return}if(!Iu(Be,["datasetReference","datasetId"])){Y({status:"error",message:"Dataset ID is required."});return}Y({status:"running",label:"Creating dataset"}),kh(d,Be).then(Le=>{Y({status:"success",message:`Created dataset ${Le.datasetReference.datasetId}.`}),E(Le.datasetReference.datasetId),me(Le.datasetReference.datasetId),u()}).catch(Le=>{Y({status:"error",message:Le.message})})}function at(K){K.preventDefault();const Be=I.trim()||r;if(!Be){Y({status:"error",message:"Choose a dataset before creating a table."});return}const Ue=Z?void 0:Ah(oe);if(Ue instanceof Error){Y({status:"error",message:Ue.message});return}const Le=Z?Du(ee,"Table raw JSON"):{tableReference:{tableId:J.trim()},schema:Ue,description:q.trim()||void 0};if(Le instanceof Error){Y({status:"error",message:Le.message});return}if(!Iu(Le,["tableReference","tableId"])){Y({status:"error",message:"Table ID is required."});return}Y({status:"running",label:"Creating table"}),Ch(d,Be,Le).then(_e=>{Y({status:"success",message:`Created table ${Be}.${_e.tableReference.tableId}.`}),me(Be),H(_e.tableReference.tableId),u()}).catch(_e=>{Y({status:"error",message:_e.message})})}function yt(K){K.preventDefault();const Be=Se.trim()||r,Ue=z.trim()||l;if(!Be||!Ue){Y({status:"error",message:"Choose a dataset and table before inserting rows."});return}const Le=_h(Ne,be.trim());if(Le instanceof Error){Y({status:"error",message:Le.message});return}Y({status:"running",label:"Inserting rows"}),Ih(d,Be,Ue,{rows:Le}).then(kt=>{var jt;const _e=(jt=kt.insertErrors)==null?void 0:jt.map(Ae=>{const hn=Ae.errors.map(it=>it.message).join("; ");return`row ${Ae.index}: ${hn}`});Y({status:"success",message:_e!=null&&_e.length?"Insert completed with partial insert errors.":"Inserted row data.",insertErrors:_e}),u()}).catch(kt=>{Y({status:"error",message:kt.message})})}function It(K){K.preventDefault();const Be=U.trim();if(!Be){Y({status:"error",message:"Choose a dataset to delete."});return}if(!ne){Y({status:"error",message:"Acknowledge the delete-dataset destructive action before confirming the dataset ID."});return}if(ge!==Be){Y({status:"error",message:"Delete-dataset confirmation must match the dataset ID."});return}Y({status:"running",label:"Deleting dataset"}),Rh(d,Be,ge,j).then(()=>{Y({status:"success",message:`Deleted dataset ${Be}.`}),we(""),ce(!1),u()}).catch(Ue=>{Y({status:"error",message:Ue.message})})}function Ze(K){K.preventDefault();const Be=De.trim(),Ue=Ie.trim();if(!Be||!Ue){Y({status:"error",message:"Choose a dataset and table to delete."});return}if(!tt){Y({status:"error",message:"Acknowledge the delete-table destructive action before confirming the table ID."});return}if(ut!==Ue){Y({status:"error",message:"Delete-table confirmation must match the table ID."});return}Y({status:"running",label:"Deleting table"}),Dh(d,Be,Ue,ut).then(()=>{Y({status:"success",message:`Deleted table ${Be}.${Ue}.`}),de(""),Dt(!1),u()}).catch(Le=>{Y({status:"error",message:Le.message})})}return n.jsxs("section",{className:"redshift-query-runner","aria-label":"BigQuery management controls",children:[n.jsxs("div",{className:"dynamodb-toolbar",children:[n.jsx("span",{className:"inspector-label",children:"Local management"}),n.jsx("span",{className:"toolbar-count",children:"datasets.insert / tables.insert / tabledata.insertAll"})]}),i?n.jsx("p",{className:"inspector-muted",children:"BigQuery management controls are disabled until the service is active."}):null,ke.status==="running"?n.jsxs("p",{className:"operation-message",children:[ke.label,"."]}):null,ke.status==="success"?n.jsxs("div",{className:"operation-message success",children:[ke.message,(Et=ke.insertErrors)!=null&&Et.length?n.jsx("ul",{children:ke.insertErrors.map(K=>n.jsx("li",{children:K},K))}):null]}):null,ke.status==="error"?n.jsx("p",{className:"operation-message error",children:ke.message}):null,n.jsxs("form",{className:"dynamodb-operation-form",onSubmit:rt,children:[n.jsxs("div",{className:"dynamodb-toolbar",children:[n.jsx("span",{className:"inspector-label",children:"Create dataset"}),n.jsxs("label",{className:"compact-filter small",children:[n.jsx("span",{children:"raw JSON"}),n.jsx("input",{"aria-label":"Use raw JSON for BigQuery dataset",checked:w,disabled:je,onChange:K=>L(K.target.checked),type:"checkbox"})]})]}),w?n.jsxs("label",{className:"redshift-sql-editor",children:[n.jsx("span",{children:"Dataset raw JSON"}),n.jsx("textarea",{"aria-label":"BigQuery dataset raw JSON",disabled:je,onChange:K=>A(K.target.value),rows:7,spellCheck:!1,value:C})]}):n.jsxs("div",{className:"pubsub-action-row",children:[n.jsxs("label",{className:"compact-filter",children:[n.jsx("span",{children:"Dataset ID"}),n.jsx("input",{"aria-label":"BigQuery dataset ID",disabled:je,onChange:K=>g(K.target.value),placeholder:"dashboard_ops",value:p})]}),n.jsxs("label",{className:"compact-filter small",children:[n.jsx("span",{children:"Location"}),n.jsx("input",{"aria-label":"BigQuery dataset location",disabled:je,onChange:K=>S(K.target.value),placeholder:"US",value:y})]}),n.jsxs("label",{className:"compact-filter wide",children:[n.jsx("span",{children:"Description"}),n.jsx("input",{"aria-label":"BigQuery dataset description",disabled:je,onChange:K=>N(K.target.value),placeholder:"local dashboard dataset",value:v})]})]}),n.jsx(ae,{disabled:je,type:"submit",children:"Create dataset"})]}),n.jsxs("form",{className:"dynamodb-operation-form",onSubmit:at,children:[n.jsxs("div",{className:"dynamodb-toolbar",children:[n.jsx("span",{className:"inspector-label",children:"Create table"}),n.jsxs("label",{className:"compact-filter small",children:[n.jsx("span",{children:"raw JSON"}),n.jsx("input",{"aria-label":"Use raw JSON for BigQuery table",checked:Z,disabled:je,onChange:K=>Re(K.target.checked),type:"checkbox"})]})]}),n.jsxs("div",{className:"pubsub-action-row",children:[n.jsxs("label",{className:"compact-filter",children:[n.jsx("span",{children:"Dataset ID"}),n.jsx("input",{"aria-label":"BigQuery table dataset ID",disabled:je,onChange:K=>E(K.target.value),placeholder:r??"dataset",value:I})]}),Z?null:n.jsxs(n.Fragment,{children:[n.jsxs("label",{className:"compact-filter",children:[n.jsx("span",{children:"Table ID"}),n.jsx("input",{"aria-label":"BigQuery table ID",disabled:je,onChange:K=>ue(K.target.value),placeholder:"events",value:J})]}),n.jsxs("label",{className:"compact-filter wide",children:[n.jsx("span",{children:"Description"}),n.jsx("input",{"aria-label":"BigQuery table description",disabled:je,onChange:K=>P(K.target.value),placeholder:"local events table",value:q})]})]})]}),Z?n.jsxs("label",{className:"redshift-sql-editor",children:[n.jsx("span",{children:"Table raw JSON"}),n.jsx("textarea",{"aria-label":"BigQuery table raw JSON",disabled:je,onChange:K=>fe(K.target.value),rows:9,spellCheck:!1,value:ee})]}):n.jsxs("label",{className:"redshift-sql-editor",children:[n.jsx("span",{children:"Schema fields"}),n.jsx("textarea",{"aria-label":"BigQuery table schema fields",disabled:je,onChange:K=>pe(K.target.value),rows:4,spellCheck:!1,value:oe})]}),n.jsx(ae,{disabled:je,type:"submit",children:"Create table"})]}),n.jsxs("form",{className:"dynamodb-operation-form",onSubmit:yt,children:[n.jsx("span",{className:"inspector-label",children:"Insert row"}),n.jsxs("div",{className:"pubsub-action-row",children:[n.jsxs("label",{className:"compact-filter",children:[n.jsx("span",{children:"Dataset ID"}),n.jsx("input",{"aria-label":"BigQuery insert dataset ID",disabled:je,onChange:K=>me(K.target.value),placeholder:r??"dataset",value:Se})]}),n.jsxs("label",{className:"compact-filter",children:[n.jsx("span",{children:"Table ID"}),n.jsx("input",{"aria-label":"BigQuery insert table ID",disabled:je,onChange:K=>H(K.target.value),placeholder:l??"table",value:z})]}),n.jsxs("label",{className:"compact-filter",children:[n.jsx("span",{children:"Insert ID"}),n.jsx("input",{"aria-label":"BigQuery insert ID",disabled:je,onChange:K=>$e(K.target.value),placeholder:"optional",value:be})]})]}),n.jsxs("label",{className:"redshift-sql-editor",children:[n.jsx("span",{children:"Row JSON"}),n.jsx("textarea",{"aria-label":"BigQuery insert row JSON",disabled:je,onChange:K=>$(K.target.value),rows:6,spellCheck:!1,value:Ne})]}),n.jsx("p",{className:"inspector-muted",children:"JSON validation runs locally before tabledata.insertAll. Row payloads are not written to logs."}),n.jsx(ae,{disabled:je,type:"submit",children:"Insert row"})]}),n.jsxs("form",{className:"dynamodb-operation-form",onSubmit:It,children:[n.jsx("span",{className:"inspector-label",children:"Delete dataset"}),n.jsxs("div",{className:"pubsub-action-row",children:[n.jsxs("label",{className:"compact-filter",children:[n.jsx("span",{children:"Dataset ID"}),n.jsx("input",{"aria-label":"BigQuery delete dataset ID",disabled:je,onChange:K=>_(K.target.value),placeholder:r??"dataset",value:U})]}),n.jsxs("label",{className:"compact-filter small",children:[n.jsx("span",{children:"Delete contents"}),n.jsx("input",{"aria-label":"Delete BigQuery dataset contents",checked:j,disabled:je,onChange:K=>T(K.target.checked),type:"checkbox"})]})]}),n.jsxs("label",{className:"destructive-confirmation",children:[n.jsx("input",{"aria-label":"I understand this deletes the dataset and, if selected, its tables.",checked:ne,disabled:je,onChange:K=>ce(K.target.checked),type:"checkbox"}),n.jsxs("span",{children:["Step 1: I understand this deletes the dataset",j?" and all of its tables":"","."]})]}),n.jsxs("label",{className:"compact-filter",children:[n.jsxs("span",{children:["Step 2: Type ",U||"dataset id"," to delete dataset"]}),n.jsx("input",{"aria-label":"BigQuery delete dataset confirmation",disabled:je,onChange:K=>we(K.target.value),value:ge})]}),n.jsx(ae,{className:"danger",disabled:je||!ne||ge!==U.trim()||U.trim()==="",type:"submit",children:"Delete dataset"})]}),n.jsxs("form",{className:"dynamodb-operation-form",onSubmit:Ze,children:[n.jsx("span",{className:"inspector-label",children:"Delete table"}),n.jsxs("div",{className:"pubsub-action-row",children:[n.jsxs("label",{className:"compact-filter",children:[n.jsx("span",{children:"Dataset ID"}),n.jsx("input",{"aria-label":"BigQuery delete table dataset ID",disabled:je,onChange:K=>se(K.target.value),placeholder:r??"dataset",value:De})]}),n.jsxs("label",{className:"compact-filter",children:[n.jsx("span",{children:"Table ID"}),n.jsx("input",{"aria-label":"BigQuery delete table ID",disabled:je,onChange:K=>Qe(K.target.value),placeholder:l??"table",value:Ie})]})]}),n.jsxs("label",{className:"destructive-confirmation",children:[n.jsx("input",{"aria-label":"I understand this deletes the table and its local rows.",checked:tt,disabled:je,onChange:K=>Dt(K.target.checked),type:"checkbox"}),n.jsx("span",{children:"Step 1: I understand this deletes the table and its local rows."})]}),n.jsxs("label",{className:"compact-filter",children:[n.jsxs("span",{children:["Step 2: Type ",Ie||"table id"," to delete table"]}),n.jsx("input",{"aria-label":"BigQuery delete table confirmation",disabled:je,onChange:K=>de(K.target.value),value:ut})]}),n.jsx(ae,{className:"danger",disabled:je||!tt||ut!==Ie.trim()||Ie.trim()==="",type:"submit",children:"Delete table"})]})]})}function zh({disabled:r,projectId:l,onQuerySuccess:i}){const[u,d]=m.useState("SELECT * FROM `analytics.people` LIMIT 10"),[p,g]=m.useState("25"),[y,S]=m.useState(!1),[v,N]=m.useState({status:"idle"}),[w,L]=m.useState(0),C=v.status==="success"?v.response:void 0,A=(C==null?void 0:C.rows)??[],I=A[Math.min(w,Math.max(A.length-1,0))];function E(J){J.preventDefault();const ue=u.trim();if(ue===""){N({status:"error",message:"Query text is required."});return}const q=Number.parseInt(p,10);if(!Number.isInteger(q)||q<1||q>1e3){N({status:"error",message:"Max results must be between 1 and 1000."});return}N({status:"running"}),wh(l,{query:ue,maxResults:q,dryRun:y,useLegacySql:!1}).then(P=>{L(0),N({status:"success",response:P}),i()}).catch(P=>{N({status:"error",message:P.message})})}return n.jsxs("section",{className:"redshift-query-runner","aria-label":"BigQuery SQL query runner",children:[n.jsxs("div",{className:"dynamodb-toolbar",children:[n.jsx("span",{className:"inspector-label",children:"SQL query runner"}),n.jsx("span",{className:"toolbar-count",children:C?`${C.totalRows} rows / ${C.jobReference.jobId}`:"useLegacySql=false"})]}),n.jsxs("form",{className:"redshift-query-form",onSubmit:E,children:[n.jsxs("label",{className:"redshift-sql-editor",children:[n.jsx("span",{children:"Query text"}),n.jsx("textarea",{"aria-label":"BigQuery SQL query text",disabled:r||v.status==="running",onChange:J=>d(J.target.value),rows:5,spellCheck:!1,value:u})]}),n.jsxs("div",{className:"pubsub-action-row",children:[n.jsxs("label",{className:"compact-filter small",children:[n.jsx("span",{children:"Max results"}),n.jsx("input",{"aria-label":"BigQuery query max results",disabled:r||v.status==="running",max:1e3,min:1,onChange:J=>g(J.target.value),type:"number",value:p})]}),n.jsxs("label",{className:"compact-filter small",children:[n.jsx("span",{children:"Dry run"}),n.jsx("input",{"aria-label":"BigQuery dry run",checked:y,disabled:r||v.status==="running",onChange:J=>S(J.target.checked),type:"checkbox"})]}),n.jsx(ae,{disabled:r||v.status==="running",type:"submit",children:v.status==="running"?"Running":y?"Dry run":"Run query"})]})]}),r?n.jsx("p",{className:"inspector-muted",children:"BigQuery query controls are disabled until the service is active."}):null,v.status==="error"?n.jsx("p",{className:"operation-message error",children:v.message}):null,C?n.jsx(Qh,{response:C,selectedRow:I,selectedRowIndex:w,onSelectRow:L}):null]})}function Qh({onSelectRow:r,response:l,selectedRow:i,selectedRowIndex:u}){var S,v;const d=((S=l.schema)==null?void 0:S.fields)??[],p=l.rows??[],g=Math.max(d.length,((v=p[0])==null?void 0:v.f.length)??0),y=Array.from({length:g},(N,w)=>{var L,C;return{name:((L=d[w])==null?void 0:L.name)??`column_${w+1}`,type:((C=d[w])==null?void 0:C.type)??"UNKNOWN"}});return n.jsxs("div",{className:"redshift-query-result",children:[n.jsxs("div",{className:"attribute-preview","aria-label":"BigQuery query summary",children:[n.jsx("span",{className:"attribute-chip",children:l.jobComplete?"complete":"running"}),n.jsx("span",{className:"attribute-chip",children:l.cacheHit?"cache hit":"cache miss"}),n.jsxs("span",{className:"attribute-chip",children:[l.totalRows," rows"]}),n.jsx("span",{className:"attribute-chip",children:l.jobReference.jobId})]}),p.length===0?n.jsx("p",{className:"inspector-muted",children:"Query completed without loaded result rows."}):n.jsxs("div",{className:"dynamodb-query-result-grid",children:[n.jsx("div",{className:"dynamodb-item-table-wrap",children:n.jsxs("table",{className:"dynamodb-item-table compact",children:[n.jsx("thead",{children:n.jsx("tr",{children:y.map(N=>n.jsxs("th",{scope:"col",children:[N.name,n.jsx("span",{className:"query-column-type",children:N.type})]},N.name))})}),n.jsx("tbody",{children:p.map((N,w)=>n.jsx("tr",{className:w===u?"item-row active":"item-row",onClick:()=>r(w),children:y.map((L,C)=>{var A;return n.jsx("td",{children:Wu((A=N.f[C])==null?void 0:A.v)},L.name)})},`${l.jobReference.jobId}-${w}`))})]})}),n.jsxs("section",{children:[n.jsx("span",{className:"inspector-label",children:"Selected result JSON"}),n.jsx("pre",{className:"mail-preview",children:JSON.stringify(Oh(d,i),null,2)})]})]}),n.jsxs("section",{children:[n.jsx("span",{className:"inspector-label",children:"Job reference"}),n.jsx("pre",{className:"mail-preview",children:JSON.stringify(l.jobReference,null,2)})]})]})}function Uh({activeIndex:r,rows:l,rowsState:i,tableName:u,onSelectIndex:d}){return u?i.status==="loading"?n.jsx(F,{title:"Loading rows",description:`Reading rows from ${u}.`}):i.status==="error"?n.jsx(F,{title:"BigQuery rows unavailable",description:i.message}):l.length===0?n.jsx(F,{title:"No rows",description:`No loaded rows in ${u} match the current filter.`}):n.jsx("div",{className:"dynamodb-item-table-wrap",children:n.jsxs("table",{className:"dynamodb-item-table",children:[n.jsx("thead",{children:n.jsxs("tr",{children:[n.jsx("th",{scope:"col",children:"Insert ID"}),n.jsx("th",{scope:"col",children:"Values"}),n.jsx("th",{scope:"col",children:"Inserted"})]})}),n.jsx("tbody",{children:l.map((p,g)=>n.jsxs("tr",{className:g===r?"item-row active":"item-row",onClick:()=>d(g),children:[n.jsx("td",{children:n.jsx("code",{children:p.insertId??g+1})}),n.jsx("td",{children:n.jsx(qh,{item:p.json})}),n.jsx("td",{children:p.insertedAt??"unknown"})]},`${p.insertId??"row"}-${g}`))})]})}):n.jsx(F,{title:"No table selected",description:"Choose a BigQuery table to inspect stored rows."})}function qh({item:r}){const l=Object.entries(r).slice(0,6);return l.length===0?n.jsx("span",{className:"service-status",children:"empty row"}):n.jsx("span",{className:"attribute-preview",children:l.map(([i,u])=>n.jsxs("span",{className:"attribute-chip",children:[i,": ",Wu(u)]},i))})}function Vh({service:r}){const[l,i]=m.useState({status:"loading"}),[u,d]=m.useState({status:"idle"}),[p,g]=m.useState(),[y,S]=m.useState(),[v,N]=m.useState(0),[w,L]=m.useState(""),[C,A]=m.useState(""),I=(r==null?void 0:r.status)==="disabled",E=wt(),J=m.useCallback(()=>{const z=E();if(I){i({status:"success",statusPayload:Th(r),datasets:[],jobs:[]}),d({status:"idle"}),g(void 0),S(void 0);return}i({status:"loading"}),Promise.all([yh(),jh()]).then(async([H,be])=>{const $e=be.projects[0],Ne=($e==null?void 0:$e.projectId)??H.project,[{datasets:$},{jobs:U}]=await Promise.all([vh(Ne),Sh(Ne)]);z()&&(i({status:"success",statusPayload:H,datasets:$,jobs:U}),g(_=>{var j;return _&&$.some(T=>T.datasetId===_)?_:(j=$[0])==null?void 0:j.datasetId}),S(_=>{var T;const j=$.find(ne=>ne.datasetId===p)??$[0];return _&&(j!=null&&j.tables.some(ne=>ne.tableId===_))?_:(T=j==null?void 0:j.tables[0])==null?void 0:T.tableId}))}).catch(H=>{z()&&i({status:"error",message:H.message})})},[E,p,I,r]);m.useEffect(()=>{J()},[J]),Vt({topics:["bigquery"],onEvent:J,onResync:J,enabled:!I});const ue=l.status==="success"?l.datasets:[],q=ue.find(z=>z.datasetId===p),P=q==null?void 0:q.tables.find(z=>z.tableId===y),oe=l.status==="success"?l.statusPayload.project:r==null?void 0:r.endpoint,pe=wt(),Z=m.useCallback(()=>{const z=pe();if(!q||!P||l.status!=="success"||I){d({status:"idle"});return}d({status:"loading"}),bh(l.statusPayload.project,q.datasetId,P.tableId).then(({rows:H})=>{z()&&(N(0),d({status:"success",rows:H}))}).catch(H=>{z()&&d({status:"error",message:H.message})})},[pe,q,P,l,I]);m.useEffect(()=>{Z()},[Z]);const Re=m.useMemo(()=>{const z=w.trim().toLowerCase();return z===""?ue:ue.filter(H=>H.datasetId.toLowerCase().includes(z))},[ue,w]),ee=m.useMemo(()=>{const z=u.status==="success"?u.rows:[],H=C.trim().toLowerCase();return H===""?z:z.filter(be=>JSON.stringify(be).toLowerCase().includes(H))},[u,C]),fe=ee[Math.min(v,Math.max(ee.length-1,0))];if(I)return n.jsx(he,{title:"BigQuery",children:n.jsx(F,{title:"BigQuery is disabled",description:"Enable the BigQuery service in devcloud config to inspect projects, datasets, tables, rows, and jobs."})});function Se(z){var H;g(z.datasetId),S((H=z.tables[0])==null?void 0:H.tableId),N(0),A("")}function me(z){S(z.tableId),N(0),A("")}return n.jsxs("div",{className:"dynamodb-workspace",children:[n.jsxs(he,{title:"Datasets",children:[n.jsxs("div",{className:"dynamodb-toolbar",children:[n.jsxs("label",{className:"compact-filter",children:[n.jsx("span",{children:"Filter"}),n.jsx("input",{"aria-label":"Filter BigQuery datasets",onChange:z=>L(z.target.value),placeholder:"dataset id",type:"search",value:w})]}),n.jsx(ae,{onClick:J,children:"Refresh"})]}),l.status==="loading"?n.jsx(F,{title:"Loading datasets",description:"Reading local BigQuery catalog metadata."}):null,l.status==="error"?n.jsx(F,{title:"BigQuery catalog unavailable",description:l.message,actionLabel:"Retry",onAction:J}):null,l.status==="success"?n.jsx(Eh,{activeDatasetId:p,activeTableId:y,datasets:Re,onSelectDataset:Se,onSelectTable:me}):null]}),n.jsxs(he,{title:"Rows",children:[n.jsx(zh,{disabled:I||l.status!=="success",projectId:oe??"devcloud",onQuerySuccess:J}),n.jsx(Fh,{activeDatasetId:q==null?void 0:q.datasetId,activeTableId:P==null?void 0:P.tableId,disabled:I||l.status!=="success",projectId:oe??"devcloud",onMutationSuccess:()=>{J(),Z()}}),n.jsxs("div",{className:"dynamodb-toolbar",children:[n.jsx("span",{className:"toolbar-count",children:P?`${ee.length} shown / ${P.numRows} reported`:"Select a table"}),n.jsxs("label",{className:"compact-filter",children:[n.jsx("span",{children:"Filter"}),n.jsx("input",{"aria-label":"Filter BigQuery rows",disabled:!P,onChange:z=>{N(0),A(z.target.value)},placeholder:"row value",type:"search",value:C})]}),n.jsx(ae,{disabled:!P,onClick:Z,children:"Refresh"})]}),n.jsx(Uh,{activeIndex:v,rows:ee,rowsState:u,tableName:P==null?void 0:P.tableId,onSelectIndex:N})]}),n.jsx(he,{title:"Inspector",children:n.jsx(Bh,{dataset:q,jobs:l.status==="success"?l.jobs:[],project:oe??"unknown",row:fe,status:l.status==="success"?l.statusPayload:void 0,table:P})})]})}async function Kh(){return V("/api/redshift/status")}async function Jh(){return V("/api/redshift/clusters")}async function Hh(){return V("/api/redshift/catalog")}async function Wh(){return V("/api/redshift/statements")}async function Gh(r){return V("/api/redshift/query",{method:"POST",body:r,timeoutMs:15e3})}function Yh({service:r}){const[l,i]=m.useState({status:"loading"}),[u,d]=m.useState(),[p,g]=m.useState(""),y=(r==null?void 0:r.status)==="disabled",S=wt(),v=m.useCallback(()=>{const I=S();if(y){i({status:"success",statusPayload:sg(r),clusters:[],catalog:{database:"dev",schemas:[],tables:[],columns:[]},statements:[]}),d(void 0);return}i({status:"loading"}),Promise.all([Kh(),Jh(),Hh(),Wh()]).then(([E,J,ue,q])=>{if(!I())return;const P=ue.catalog.tables;i({status:"success",statusPayload:E,clusters:J.clusters,catalog:ue.catalog,statements:q.statements}),d(oe=>oe&&P.some(pe=>Us(pe)===oe)?oe:Us(P[0]))}).catch(E=>{I()&&i({status:"error",message:E.message})})},[S,y,r]);m.useEffect(()=>{v()},[v]),Vt({topics:["redshift"],onEvent:v,onResync:v,enabled:!y});const N=l.status==="success"?l.catalog:void 0,w=(N==null?void 0:N.tables)??[],L=m.useMemo(()=>{const I=p.trim().toLowerCase();return I===""?w:w.filter(E=>`${E.schema}.${E.name}`.toLowerCase().includes(I))},[p,w]),C=w.find(I=>Us(I)===u),A=(N==null?void 0:N.columns.filter(I=>C&&I.schema===C.schema&&I.table===C.name))??[];return y?n.jsx(he,{title:"Redshift",children:n.jsx(F,{title:"Redshift is disabled",description:"Enable the Redshift service in devcloud config to inspect clusters, catalog metadata, and statements."})}):n.jsxs("div",{className:"redshift-workspace",children:[n.jsxs(he,{title:"Clusters",children:[n.jsxs("div",{className:"dynamodb-toolbar",children:[n.jsx("span",{className:"toolbar-count",children:l.status==="success"?`${l.clusters.length} clusters`:"Loading"}),n.jsx(ae,{onClick:v,children:"Refresh"})]}),l.status==="loading"?n.jsx(F,{title:"Loading Redshift",description:"Reading local Redshift metadata."}):null,l.status==="error"?n.jsx(F,{title:"Redshift unavailable",description:l.message,actionLabel:"Retry",onAction:v}):null,l.status==="success"?n.jsx(Xh,{clusters:l.clusters,status:l.statusPayload}):null]}),n.jsxs(he,{title:"Catalog",children:[n.jsxs("div",{className:"dynamodb-toolbar",children:[n.jsx("span",{className:"toolbar-count",children:`${L.length} shown / ${w.length} tables`}),n.jsxs("label",{className:"compact-filter",children:[n.jsx("span",{children:"Filter"}),n.jsx("input",{"aria-label":"Filter Redshift tables",onChange:I=>g(I.target.value),placeholder:"schema or table",type:"search",value:p})]})]}),n.jsx(Zh,{activeTableKey:u,tables:L,onSelectTable:I=>d(Us(I))})]}),n.jsx(he,{title:"Inspector",children:n.jsx(eg,{columns:A,onQuerySuccess:v,statements:l.status==="success"?l.statements:[],status:l.status==="success"?l.statusPayload:void 0,table:C})})]})}function Xh({clusters:r,status:l}){return r.length===0?n.jsx(F,{title:"No clusters",description:"Local Redshift cluster metadata will appear here."}):n.jsx("div",{className:"dynamodb-table-list","aria-label":"Redshift clusters",children:r.map(i=>n.jsxs("section",{className:"dynamodb-table-row",children:[n.jsxs("span",{className:"table-row-top",children:[n.jsx("span",{className:"table-row-name",children:i.clusterIdentifier}),n.jsx("span",{className:"count-pill",children:i.clusterStatus})]}),n.jsxs("span",{className:"table-row-meta",children:[i.databaseName," on ",i.endpoint.address,":",i.endpoint.port]}),n.jsxs("span",{className:"table-row-tags",children:[n.jsx("span",{children:i.nodeType}),n.jsxs("span",{children:[i.numberOfNodes," node"]}),n.jsx("span",{children:l.region})]})]},i.clusterIdentifier))})}function Zh({activeTableKey:r,tables:l,onSelectTable:i}){return l.length===0?n.jsx(F,{title:"No tables",description:"Tables created through Redshift SQL or Data API clients will appear here."}):n.jsx("div",{className:"dynamodb-table-list","aria-label":"Redshift tables",children:l.map(u=>n.jsxs("button",{className:Us(u)===r?"dynamodb-table-row active":"dynamodb-table-row",onClick:()=>i(u),type:"button",children:[n.jsxs("span",{className:"table-row-top",children:[n.jsx("span",{className:"table-row-name",children:u.name}),n.jsx("span",{className:"count-pill",children:u.rowCount})]}),n.jsxs("span",{className:"table-row-meta",children:[u.schema," schema"]}),n.jsxs("span",{className:"table-row-tags",children:[n.jsxs("span",{children:[u.columnCount," columns"]}),u.distKey?n.jsxs("span",{children:["distkey ",u.distKey]}):null,u.sortKeys.length>0?n.jsxs("span",{children:["sortkey ",u.sortKeys.join(", ")]}):null]})]},Us(u)))})}function eg({columns:r,onQuerySuccess:l,statements:i,status:u,table:d}){return n.jsxs("div",{className:"dynamodb-inspector",children:[n.jsx(tg,{onQuerySuccess:l}),d?null:n.jsx(F,{title:"Inspector",description:"Table columns and recent statements will appear here."}),d?n.jsxs("section",{children:[n.jsx("span",{className:"inspector-label",children:"Table"}),n.jsx("h3",{children:`${d.schema}.${d.name}`}),n.jsxs("dl",{className:"inspector-list",children:[n.jsxs("div",{children:[n.jsx("dt",{children:"SQL endpoint"}),n.jsx("dd",{children:n.jsx("code",{children:(u==null?void 0:u.sqlEndpoint)??"127.0.0.1:15439"})})]}),n.jsxs("div",{children:[n.jsx("dt",{children:"API endpoint"}),n.jsx("dd",{children:n.jsx("code",{children:(u==null?void 0:u.apiEndpoint)??"http://127.0.0.1:19099"})})]}),n.jsxs("div",{children:[n.jsx("dt",{children:"Rows"}),n.jsx("dd",{children:d.rowCount})]}),n.jsxs("div",{children:[n.jsx("dt",{children:"Distribution"}),n.jsxs("dd",{children:[d.distStyle||"auto"," ",d.distKey?`on ${d.distKey}`:""]})]}),n.jsxs("div",{children:[n.jsx("dt",{children:"Sort keys"}),n.jsx("dd",{children:d.sortKeys.length>0?d.sortKeys.join(", "):"none"})]})]})]}):null,d?n.jsxs("section",{children:[n.jsx("span",{className:"inspector-label",children:"Columns"}),r.length===0?n.jsx("p",{className:"inspector-muted",children:"No column metadata recorded."}):n.jsx("div",{className:"attribute-preview",children:r.map(p=>n.jsxs("span",{className:"attribute-chip",children:[p.name," ",p.dataType,p.encoding?` encode ${p.encoding}`:"",p.identity?" identity":""]},`${p.table}-${p.name}`))})]}):null,n.jsxs("section",{children:[n.jsx("span",{className:"inspector-label",children:"Recent statements"}),i.length===0?n.jsx("p",{className:"inspector-muted",children:"No Redshift statements recorded."}):n.jsx("div",{className:"dynamodb-item-table-wrap",children:n.jsxs("table",{className:"dynamodb-item-table",children:[n.jsx("thead",{children:n.jsxs("tr",{children:[n.jsx("th",{scope:"col",children:"ID"}),n.jsx("th",{scope:"col",children:"Status"}),n.jsx("th",{scope:"col",children:"Rows"}),n.jsx("th",{scope:"col",children:"SQL"})]})}),n.jsx("tbody",{children:i.slice(0,8).map(p=>n.jsxs("tr",{className:"item-row",children:[n.jsx("td",{children:n.jsx("code",{children:p.redshiftQueryId})}),n.jsx("td",{children:p.status}),n.jsx("td",{children:p.resultRows}),n.jsxs("td",{children:[p.queryPreview,p.queryRedacted||p.queryTruncated?n.jsx("span",{className:"attribute-chip",children:p.queryRedacted?"redacted":"truncated"}):null]})]},p.id))})]})})]})]})}function tg({onQuerySuccess:r}){const[l,i]=m.useState("select 1"),[u,d]=m.useState("100"),[p,g]=m.useState(),[y,S]=m.useState(),[v,N]=m.useState(),[w,L]=m.useState(!1),C=A=>{if(A.preventDefault(),l.trim()===""){g(void 0),N(void 0),S("SQL is required.");return}const I=Number.parseInt(u,10),E=Number.isFinite(I)?Math.max(1,I):100;d(String(E)),L(!0),S(void 0),N(void 0),Gh({sql:l,maxRows:E}).then(J=>{g(J.result),N(`${J.result.statement.status}: ${J.result.commandTag||"statement completed"}`),r()}).catch(()=>{g(void 0),N(void 0),S("Query failed. Check SQL syntax and local Redshift state.")}).finally(()=>L(!1))};return n.jsxs("section",{className:"redshift-query-runner","aria-label":"Redshift query runner",children:[n.jsxs("div",{className:"dynamodb-toolbar",children:[n.jsx("span",{className:"inspector-label",children:"Query runner"}),n.jsx("span",{className:"toolbar-count",children:p?`${p.rowCount} rows`:"Ready"})]}),n.jsxs("form",{className:"redshift-query-form",onSubmit:C,children:[n.jsxs("label",{className:"redshift-sql-editor",children:[n.jsx("span",{children:"SQL"}),n.jsx("textarea",{"aria-label":"Redshift SQL",onChange:A=>i(A.target.value),rows:5,spellCheck:!1,value:l})]}),n.jsxs("div",{className:"pubsub-action-row",children:[n.jsxs("label",{className:"compact-filter small",children:[n.jsx("span",{children:"Max rows"}),n.jsx("input",{min:1,onChange:A=>d(A.target.value),type:"number",value:u})]}),n.jsx(ae,{disabled:w,type:"submit",children:w?"Running":"Run query"})]})]}),y?n.jsx("p",{className:"operation-message error",children:y}):null,v?n.jsx("p",{className:"operation-message success",children:v}):null,p?n.jsx(ng,{queryResult:p}):null]})}function ng({queryResult:r}){return n.jsxs("div",{className:"redshift-query-result",children:[n.jsxs("div",{className:"attribute-preview","aria-label":"Redshift query summary",children:[n.jsx("span",{className:"attribute-chip",children:r.statement.status}),n.jsx("span",{className:"attribute-chip",children:r.commandTag}),n.jsxs("span",{className:"attribute-chip",children:[r.rowCount," rows"]})]}),r.columns.length===0?n.jsx("p",{className:"inspector-muted",children:"Statement completed without a result set."}):n.jsx("div",{className:"dynamodb-item-table-wrap",children:n.jsxs("table",{className:"dynamodb-item-table",children:[n.jsx("thead",{children:n.jsx("tr",{children:r.columns.map(l=>n.jsxs("th",{scope:"col",children:[l.name,n.jsx("span",{className:"query-column-type",children:l.typeName})]},l.name))})}),n.jsx("tbody",{children:r.rows.length===0?n.jsx("tr",{children:n.jsx("td",{colSpan:r.columns.length,children:"No rows returned."})}):r.rows.map((l,i)=>n.jsx("tr",{className:"item-row",children:r.columns.map((u,d)=>n.jsx("td",{children:String(l[d]??"")},`${i}-${u.name}`))},i))})]})})]})}function sg(r){return{service:"redshift",status:"disabled",running:!1,sqlEndpoint:"127.0.0.1:15439",apiEndpoint:(r==null?void 0:r.endpoint)??"http://127.0.0.1:19099",region:"us-east-1",clusterCount:0,storagePath:(r==null?void 0:r.storagePath)??".devcloud/data/redshift",backendKind:"postgres",backendMode:"managed"}}function Us(r){if(r)return`${r.schema}.${r.name}`}async function rg(){return V("/api/redis/status")}async function Eu(r=0,l="*",i=100){const u=new URLSearchParams({cursor:String(r),match:l,count:String(i)});return V(`/api/redis/keys?${u.toString()}`)}async function Tu(r){return V(`/api/redis/keys/${encodeURIComponent(r)}`)}async function ag(r){return V("/api/redis/command",{method:"POST",body:r})}async function lg(r){return V(`/api/redis/keys/${encodeURIComponent(r)}`,{method:"DELETE"})}async function ig(r,l){return V(`/api/redis/keys/${encodeURIComponent(r)}/expire`,{method:"POST",body:{ttlSeconds:l}})}async function og(){return V("/api/redis/keys?confirm=FLUSHDB",{method:"DELETE"})}async function cg(r){return V("/api/redis/select-db",{method:"POST",body:{db:r}})}const $u=/("(?:\\.|[^"\\])*")(\s*:)?|(\b(?:true|false|null)\b)|(-?\d+(?:\.\d+)?(?:[eE][+-]?\d+)?)/g;function Gu(r){const l=[];let i=0,u=0;$u.lastIndex=0;let d;for(;(d=$u.exec(r))!==null;){const[p,g,y,S,v]=d,N=d.index;if(N>i&&l.push(r.slice(i,N)),g!==void 0){const w=y!==void 0;l.push(n.jsx("span",{className:w?"json-token-key":"json-token-string",children:g},`t${u++}`)),y&&l.push(y)}else if(S!==void 0){const w=S==="null"?"json-token-null":"json-token-bool";l.push(n.jsx("span",{className:w,children:S},`t${u++}`))}else v!==void 0?l.push(n.jsx("span",{className:"json-token-number",children:v},`t${u++}`)):l.push(p);i=N+p.length}return i{const ee=I();if(A){u({status:"success",statusPayload:Ig(r),keys:[],nextCursor:0}),p(void 0),y(void 0);return}u({status:"loading"}),Promise.all([rg(),Eu(0,N)]).then(([fe,Se])=>{ee()&&(u({status:"success",statusPayload:fe,keys:Se.keys,nextCursor:Se.nextCursor}),p(me=>{var z;return me&&Se.keys.some(H=>H.key===me)?me:(z=Se.keys[0])==null?void 0:z.key}))}).catch(fe=>{ee()&&u({status:"error",message:fe.message})})},[I,A,N,r]),J=m.useCallback(()=>{i.status!=="success"||i.nextCursor===0||A||Eu(i.nextCursor,N).then(ee=>{u(fe=>fe.status!=="success"?fe:{...fe,keys:[...fe.keys,...ee.keys],nextCursor:ee.nextCursor})}).catch(ee=>C({tone:"error",text:ee.message}))},[A,N,i]);m.useEffect(()=>{E()},[E]),Vt({topics:["redis"],onEvent:E,onResync:E,enabled:!A}),m.useEffect(()=>{if(!d||A){y(void 0),v(void 0);return}let ee=!1;return v(void 0),Tu(d).then(fe=>{ee||y(fe)}).catch(fe=>{ee||(y(void 0),v(fe.message))}),()=>{ee=!0}},[d,A]);const ue=i.status==="success"?i.keys:[],q=i.status==="success"?i.nextCursor:0,P=i.status==="success"?i.statusPayload:void 0;async function oe(ee){await l(Rn({title:"Delete Redis key",description:"This key will be permanently removed from the selected Redis database.",target:ee}))&&lg(ee).then(Se=>{C({tone:"success",text:`Deleted ${ee} (${Se.deleted} key)`}),p(void 0),y(void 0),E()}).catch(Se=>C({tone:"error",text:Se.message}))}function pe(ee,fe){ig(ee,fe).then(Se=>{C({tone:Se.updated?"success":"warning",text:Se.updated?`Set TTL on ${ee} to ${fe}s`:`TTL not updated on ${ee} (key may be missing)`}),Tu(ee).then(y).catch(()=>{}),E()}).catch(Se=>C({tone:"error",text:Se.message}))}function Z(ee){cg(ee).then(()=>{p(void 0),y(void 0),v(void 0),C({tone:"success",text:`Switched to db${ee}`}),E()}).catch(fe=>C({tone:"error",text:fe.message}))}async function Re(){await l(Rn({title:"Flush Redis DB",description:"This removes every key from the selected Redis database. FLUSHALL is never used.",target:"FLUSHDB",confirmLabel:"Flush DB"}))&&og().then(fe=>{C({tone:"success",text:`FLUSHDB ${fe.result}`}),p(void 0),y(void 0),E()}).catch(fe=>C({tone:"error",text:fe.message}))}return A?n.jsx(he,{title:"Redis",children:n.jsx(F,{title:"Redis is disabled",description:"Enable services.redis.enabled in .devcloud/config.yaml and restart devcloud."})}):n.jsxs("div",{className:"redis-shell",children:[n.jsx(mg,{status:P,loading:i.status==="loading",error:i.status==="error"?i.message:void 0,onSelectDB:Z}),n.jsxs("div",{className:"redis-workspace",children:[n.jsxs(he,{title:"Keys",children:[n.jsxs("div",{className:"redis-keys-toolbar",children:[n.jsx(pg,{loading:i.status==="loading",keys:ue.length,total:P==null?void 0:P.currentDBKeys}),n.jsxs("div",{className:"redis-toolbar-actions",children:[n.jsx(ae,{onClick:E,children:"Refresh"}),n.jsx(ae,{disabled:i.status!=="success"||q===0,onClick:J,children:"Load more"}),n.jsx(ae,{className:"danger",disabled:ue.length===0,onClick:Re,children:"Flush DB"})]})]}),n.jsxs("label",{className:"redis-match-field",children:[n.jsx("span",{children:"Match pattern"}),n.jsx("input",{"aria-label":"Match Redis keys",onChange:ee=>w(ee.target.value),placeholder:"* | prefix:* | exact-key",spellCheck:!1,type:"search",value:N})]}),L?n.jsx(fg,{message:L,onDismiss:()=>C(void 0)}):null,i.status==="loading"?n.jsx(F,{title:"Loading Redis",description:"Reading key metadata via SCAN."}):null,i.status==="error"?n.jsx(F,{title:"Redis unavailable",description:i.message,actionLabel:"Retry",onAction:E}):null,i.status==="success"?n.jsx(hg,{activeKey:d,keys:ue,onSelectKey:p}):null]}),n.jsx(he,{title:"Inspector",children:n.jsx(gg,{detail:g,error:S,onDeleteKey:oe,onApplyTTL:pe})}),n.jsx(he,{title:"Command Runner",children:n.jsx(Dg,{})})]})]})}function fg({message:r,onDismiss:l}){return n.jsxs("div",{className:`redis-message redis-message-${r.tone}`,role:"status",children:[n.jsx("span",{children:r.text}),n.jsx("button",{"aria-label":"Dismiss",className:"redis-message-close",onClick:l,type:"button",children:"×"})]})}function pg({keys:r,total:l,loading:i}){if(i)return n.jsx("span",{className:"redis-keys-count",children:"Loading…"});const u=l!==void 0&&l>=0?` / ${l}`:"";return n.jsxs("span",{className:"redis-keys-count",children:[n.jsx("strong",{children:r}),n.jsxs("span",{className:"redis-keys-count-total",children:[u," key",r===1?"":"s"]})]})}function mg({status:r,loading:l,error:i,onSelectDB:u}){if(l&&!r)return n.jsx("header",{className:"redis-status-bar redis-status-bar-loading",children:n.jsx("span",{children:"Connecting to Redis…"})});if(i&&!r)return n.jsxs("header",{className:"redis-status-bar redis-status-bar-error",role:"alert",children:[n.jsxs("span",{className:"redis-status-cell",children:[n.jsx("span",{className:"cell-label",children:"State"}),n.jsx("span",{className:"cell-value",children:"unreachable"})]}),n.jsxs("span",{className:"redis-status-cell redis-status-cell-wide",children:[n.jsx("span",{className:"cell-label",children:"Reason"}),n.jsx("span",{className:"cell-value",children:i})]})]});if(!r)return n.jsx("header",{className:"redis-status-bar redis-status-bar-loading",children:n.jsx("span",{children:"—"})});const d=r.databaseCount>0?r.databaseCount:1;return n.jsxs("header",{className:"redis-status-bar",children:[n.jsxs("span",{className:`redis-status-cell redis-mode-pill redis-mode-${r.mode||"unknown"}`,children:[n.jsx("span",{className:"cell-label",children:"Mode"}),n.jsx("span",{className:"cell-value",children:r.mode||"unknown"})]}),n.jsxs("span",{className:"redis-status-cell",children:[n.jsx("span",{className:"cell-label",children:"Address"}),n.jsx("code",{children:r.address||"—"})]}),n.jsxs("span",{className:"redis-status-cell",children:[n.jsx("span",{className:"cell-label",children:"Server"}),n.jsx("span",{className:"cell-value",children:r.serverVersion||"—"})]}),n.jsxs("span",{className:"redis-status-cell",children:[n.jsx("span",{className:"cell-label",children:"Clients"}),n.jsx("span",{className:"cell-value",children:r.connectedClients})]}),n.jsxs("span",{className:"redis-status-cell",children:[n.jsx("span",{className:"cell-label",children:"Memory"}),n.jsx("span",{className:"cell-value",children:r.usedMemoryHuman||"—"})]}),n.jsxs("span",{className:"redis-status-cell",children:[n.jsx("span",{className:"cell-label",children:"Database"}),n.jsx("select",{"aria-label":"Select Redis database",className:"redis-db-select",disabled:d<=1,onChange:p=>u(Number(p.target.value)),value:r.currentDB,children:Array.from({length:d},(p,g)=>n.jsx("option",{value:g,children:`db${g}`},g))})]}),n.jsxs("span",{className:"redis-status-cell",children:[n.jsx("span",{className:"cell-label",children:`Keys (db${r.currentDB})`}),n.jsx("span",{className:"cell-value",children:r.currentDBKeys})]})]})}function hg({activeKey:r,keys:l,onSelectKey:i}){return l.length===0?n.jsx(F,{title:"No keys",description:"Send SET / HSET / LPUSH from a Redis client and refresh."}):n.jsx("ul",{className:"redis-key-list","aria-label":"Redis keys",children:l.map(u=>{const d=u.key===r;return n.jsx("li",{children:n.jsxs("button",{"aria-current":d?"true":void 0,className:d?"redis-key-row active":"redis-key-row",onClick:()=>i(u.key),title:u.key,type:"button",children:[n.jsxs("span",{className:"redis-key-row-top",children:[n.jsx(Xu,{type:u.type}),n.jsx(Zu,{ttlSeconds:u.ttlSeconds,compact:!0})]}),n.jsx("span",{className:"redis-key-row-name",children:u.key})]})},u.key)})})}function gg({detail:r,error:l,onApplyTTL:i,onDeleteKey:u}){return l?n.jsx(F,{title:"Key unavailable",description:l}):r?n.jsxs("div",{className:"redis-inspector",children:[n.jsx(xg,{detail:r}),n.jsx(yg,{detail:r}),n.jsx(Cg,{detail:r,onApplyTTL:i,onDeleteKey:u})]}):n.jsx(F,{title:"Select a key",description:"Choose a key on the left to inspect its type, TTL, and value."})}function xg({detail:r}){const[l,i]=m.useState(!1);function u(){navigator.clipboard&&navigator.clipboard.writeText(r.key).then(()=>{i(!0),window.setTimeout(()=>i(!1),1500)}).catch(()=>{})}return n.jsxs("header",{className:"redis-inspector-header",children:[n.jsx("span",{className:"redis-inspector-eyebrow",children:"Key"}),n.jsxs("div",{className:"redis-key-title-row",children:[n.jsx("code",{className:"redis-key-title",children:r.key}),n.jsx("button",{className:"redis-copy-button",onClick:u,title:"Copy key",type:"button",children:l?"copied":"copy"})]}),n.jsxs("div",{className:"redis-key-badges",children:[n.jsx(Xu,{type:r.type}),n.jsx(Zu,{ttlSeconds:r.ttlSeconds}),n.jsx(Rg,{detail:r})]})]})}function yg({detail:r}){return!r.preview||r.preview.length===0?n.jsxs("section",{className:"redis-inspector-section",children:[n.jsx("span",{className:"redis-inspector-eyebrow",children:"Value"}),n.jsx(F,{title:"Empty value",description:"The key exists but has no previewable content in this slice."})]}):n.jsxs("section",{className:"redis-inspector-section",children:[n.jsx("header",{className:"redis-section-head",children:n.jsx("span",{className:"redis-inspector-eyebrow",children:"Value"})}),jg(r)]})}function jg(r){switch(r.type){case"string":return n.jsx(vg,{raw:r.preview[0]??""});case"list":return n.jsx(bg,{items:r.preview});case"hash":return n.jsx(Sg,{entries:r.preview});case"set":return n.jsx(wg,{members:r.preview});case"zset":return n.jsx(kg,{items:r.preview});default:return n.jsx("pre",{className:"redis-value-pre",children:r.preview.join(` -`)})}}function vg({raw:r}){const l=m.useMemo(()=>Yu(r),[r]),[i,u]=m.useState(l?"pretty":"raw"),d=i==="pretty"&&l!==void 0;return n.jsxs("div",{className:"redis-value-string",children:[n.jsxs("div",{className:"redis-value-toolbar",children:[n.jsxs("span",{className:"redis-value-meta",children:[r.length.toLocaleString()," char",r.length===1?"":"s"]}),l?n.jsxs("div",{role:"tablist","aria-label":"Value format",className:"redis-segmented",children:[n.jsx("button",{type:"button",role:"tab","aria-selected":i==="pretty",className:i==="pretty"?"active":void 0,onClick:()=>u("pretty"),children:"Pretty"}),n.jsx("button",{type:"button",role:"tab","aria-selected":i==="raw",className:i==="raw"?"active":void 0,onClick:()=>u("raw"),children:"Raw"})]}):null]}),n.jsx("pre",{className:"redis-value-pre json-code",children:d?Gu(l):r})]})}function bg({items:r}){return n.jsx("ol",{className:"redis-list-view",children:r.map((l,i)=>n.jsxs("li",{children:[n.jsx("span",{className:"redis-list-index",children:i}),n.jsx("code",{className:"redis-list-value",children:l})]},i))})}function Sg({entries:r}){return n.jsxs("table",{className:"redis-hash-table","aria-label":"Hash fields",children:[n.jsx("thead",{children:n.jsxs("tr",{children:[n.jsx("th",{scope:"col",children:"Field"}),n.jsx("th",{scope:"col",children:"Value"})]})}),n.jsx("tbody",{children:r.map((l,i)=>{const[u,d]=ed(l,": ");return n.jsxs("tr",{children:[n.jsx("th",{scope:"row",children:n.jsx("code",{children:u})}),n.jsx("td",{children:n.jsx(Ng,{raw:d})})]},i)})})]})}function Ng({raw:r}){const l=m.useMemo(()=>Yu(r),[r]);return l===void 0?n.jsx("code",{children:r}):n.jsx("pre",{className:"redis-value-pre json-code redis-value-pre-inline",children:Gu(l)})}function wg({members:r}){return n.jsx("ul",{className:"redis-set-view","aria-label":"Set members",children:r.map((l,i)=>n.jsx("li",{children:n.jsx("code",{children:l})},i))})}function kg({items:r}){return n.jsxs("table",{className:"redis-zset-table","aria-label":"Sorted set members",children:[n.jsx("thead",{children:n.jsxs("tr",{children:[n.jsx("th",{scope:"col",children:"Member"}),n.jsx("th",{scope:"col",children:"Score"})]})}),n.jsx("tbody",{children:r.map((l,i)=>{const[u,d]=ed(l,": ");return n.jsxs("tr",{children:[n.jsx("th",{scope:"row",children:n.jsx("code",{children:u})}),n.jsx("td",{className:"redis-zset-score",children:d})]},i)})})]})}function Cg({detail:r,onApplyTTL:l,onDeleteKey:i}){const[u,d]=m.useState("60"),p=Number(u),g=Number.isInteger(p)&&p>0;function y(v){v.preventDefault(),g&&l(r.key,p)}function S(){l(r.key,-1)}return n.jsxs("section",{className:"redis-inspector-section",children:[n.jsx("header",{className:"redis-section-head",children:n.jsx("span",{className:"redis-inspector-eyebrow",children:"Actions"})}),n.jsxs("form",{className:"redis-ttl-form",onSubmit:y,children:[n.jsxs("label",{children:[n.jsx("span",{children:"TTL seconds"}),n.jsx("input",{"aria-label":"TTL seconds",min:1,onChange:v=>d(v.target.value),type:"number",value:u})]}),n.jsx(ae,{disabled:!g,type:"submit",children:"Set TTL"}),n.jsx(ae,{className:"ghost",onClick:S,type:"button",children:"Persist (no TTL)"})]}),n.jsx("div",{className:"redis-danger-row",children:n.jsx(ae,{className:"danger",onClick:()=>i(r.key),type:"button",children:"Delete key"})})]})}function Xu({type:r}){const l=(r||"unknown").toLowerCase();return n.jsx("span",{className:`redis-badge redis-type-${l}`,children:l})}function Zu({ttlSeconds:r,compact:l}){return r===-2?n.jsx("span",{className:"redis-badge redis-ttl-missing",children:l?"✕":"missing"}):r===-1?n.jsx("span",{className:"redis-badge redis-ttl-persistent",children:l?"∞":"persistent"}):n.jsx("span",{className:"redis-badge redis-ttl-active",title:`${r}s`,children:l?`${Pu(r)}`:`TTL ${Pu(r)}`})}function Rg({detail:r}){if(!r.preview||r.preview.length===0)return null;const l=r.preview.length;let i;switch(r.type){case"string":i=`${(r.preview[0]??"").length} char${(r.preview[0]??"").length===1?"":"s"}`;break;case"list":i=`${l} item${l===1?"":"s"}`;break;case"hash":i=`${l} field${l===1?"":"s"}`;break;case"set":i=`${l} member${l===1?"":"s"}`;break;case"zset":i=`${l} entr${l===1?"y":"ies"}`;break;default:i=`${l}`}return n.jsx("span",{className:"redis-badge redis-size",children:i})}function Dg(){const[r,l]=m.useState("GET your-key"),[i,u]=m.useState([]),d=m.useRef(0),p=m.useMemo(()=>Lu(r),[r]);function g(w){d.current+=1;const L=d.current;u(C=>[{id:L,...w},...C].slice(0,ug))}function y(w,L){if(!w.command){g({input:L,outcome:"error",detail:"No command supplied"});return}ag(w).then(C=>{g({input:L,outcome:"success",detail:`${C.command} (${C.class})`,rows:C.rows??[]})}).catch(C=>{g({input:L,outcome:"error",detail:C.message})})}function S(w){w.preventDefault(),y(p,r)}function v(w){(w.metaKey||w.ctrlKey)&&w.key==="Enter"&&(w.preventDefault(),y(p,r))}function N(w){l(w),y(Lu(w),w)}return n.jsxs("div",{className:"redis-runner",children:[n.jsxs("form",{className:"redis-runner-form",onSubmit:S,children:[n.jsxs("label",{className:"redis-runner-label",children:[n.jsx("span",{children:"Command"}),n.jsx("textarea",{"aria-label":"Redis command",onChange:w=>l(w.target.value),onKeyDown:v,rows:3,spellCheck:!1,value:r})]}),n.jsxs("div",{className:"redis-runner-toolbar",children:[n.jsxs("span",{className:"redis-runner-hint",children:["Allowlisted commands only · ",n.jsx("kbd",{children:"⌘"}),"/",n.jsx("kbd",{children:"Ctrl"}),"+",n.jsx("kbd",{children:"Enter"})," to run"]}),n.jsx(ae,{type:"submit",children:"Run"})]})]}),i.length===0?n.jsx(F,{title:"No commands yet",description:"Try GET, HGETALL, LRANGE, SCAN 0 MATCH * COUNT 50, or TYPE."}):null,i.length>0?n.jsx("ol",{className:"redis-runner-history","aria-label":"Command history",children:i.map(w=>n.jsxs("li",{className:`redis-runner-entry redis-runner-${w.outcome}`,children:[n.jsxs("header",{className:"redis-runner-entry-head",children:[n.jsx("code",{className:"redis-runner-input",children:w.input}),n.jsx("button",{className:"redis-runner-rerun",onClick:()=>N(w.input),title:"Run again",type:"button",children:"rerun"})]}),n.jsxs("p",{className:"redis-runner-detail",children:[n.jsx("span",{className:"redis-runner-tag",children:w.outcome==="success"?"OK":"ERR"}),n.jsx("span",{children:w.detail})]}),w.rows&&w.rows.length>0?n.jsx("pre",{className:"redis-runner-result",children:w.rows.join(` -`)}):null]},w.id))}):null]})}function Ig(r){var l;return{service:"redis",status:"disabled",running:!1,mode:"managed",address:((l=r==null?void 0:r.endpoint)==null?void 0:l.replace(/^redis:\/\//,""))??"127.0.0.1:16379",serverVersion:"",connectedClients:0,usedMemoryHuman:"",currentDB:0,databaseCount:0,currentDBKeys:0,storagePath:(r==null?void 0:r.storagePath)??".devcloud/data/redis"}}function Lu(r){const l=r.trim().split(/\s+/).filter(d=>d.length>0),[i="",...u]=l;return{command:i,args:u}}function ed(r,l){const i=r.indexOf(l);return i===-1?[r,""]:[r.slice(0,i),r.slice(i+l.length)]}function Pu(r){return r<60?`${r}s`:r<3600?`${Math.floor(r/60)}m`:r<86400?`${Math.floor(r/3600)}h`:`${Math.floor(r/86400)}d`}async function Eg(){return V("/api/sqs/status")}async function Tg(){return V("/api/sqs/queues")}async function $g(r){return V("/api/sqs/queues",{method:"POST",body:{input:r}})}async function Lg(r){return V(`/api/sqs/queues/${encodeURIComponent(r)}/messages`)}async function Pg(r,l){return V(`/api/sqs/queues/${encodeURIComponent(r)}/messages`,{method:"POST",body:{input:l}})}async function Mg(r,l){return V(`/api/sqs/queues/${encodeURIComponent(r)}/receive`,{method:"POST",body:{input:l}})}async function Og(r,l){await V(`/api/sqs/queues/${encodeURIComponent(r)}/delete`,{method:"POST",body:{input:l}})}async function Ag(r,l){await V(`/api/sqs/queues/${encodeURIComponent(r)}/visibility`,{method:"POST",body:{input:l}})}async function _g(r){return V(`/api/sqs/queues/${encodeURIComponent(r)}/leases`)}async function Bg(r){return V(`/api/sqs/queues/${encodeURIComponent(r)}/dlq`)}async function Fg(r){return mn(`/api/sqs/queues/${encodeURIComponent(r)}/purge`,{method:"POST"})}function zg({busyAction:r,newQueueName:l,newQueueKind:i,newQueueVisibility:u,newQueueDelay:d,onSubmit:p,setNewQueueName:g,setNewQueueKind:y,setNewQueueVisibility:S,setNewQueueDelay:v}){return n.jsxs("form",{className:"pubsub-action-form stacked",onSubmit:p,children:[n.jsxs("label",{className:"compact-filter",children:[n.jsx("span",{children:"Queue name"}),n.jsx("input",{"aria-label":"New SQS queue name",onChange:N=>g(N.target.value),placeholder:i==="fifo"?"jobs.fifo":"jobs",value:l})]}),n.jsxs("label",{className:"compact-filter small",children:[n.jsx("span",{children:"Type"}),n.jsxs("select",{"aria-label":"New SQS queue type",onChange:N=>y(N.target.value==="fifo"?"fifo":"standard"),value:i,children:[n.jsx("option",{value:"standard",children:"Standard"}),n.jsx("option",{value:"fifo",children:"FIFO"})]})]}),n.jsxs("label",{className:"compact-filter small",children:[n.jsx("span",{children:"Visibility"}),n.jsx("input",{"aria-label":"New SQS queue visibility timeout seconds",inputMode:"numeric",onChange:N=>S(N.target.value),value:u})]}),n.jsxs("label",{className:"compact-filter small",children:[n.jsx("span",{children:"Delay"}),n.jsx("input",{"aria-label":"New SQS queue delay seconds",inputMode:"numeric",onChange:N=>v(N.target.value),value:d})]}),n.jsx(ae,{disabled:r==="create-queue",type:"submit",children:"Create"})]})}function Qg(r){return{service:"sqs",status:"disabled",running:!1,endpoint:(r==null?void 0:r.endpoint)??"http://127.0.0.1:19324",region:"us-east-1",authMode:"relaxed",storagePath:(r==null?void 0:r.storagePath)??".devcloud/data/sqs",queueCount:0}}function Mu(r){const l=Number.parseInt(r,10);if(!(!Number.isFinite(l)||l<0))return String(l)}function Ou(r){const l=Number.parseInt(r,10);if(!(!Number.isFinite(l)||l<=0))return l}function Li(r){const l=Number.parseInt(r,10);if(!(!Number.isFinite(l)||l<0))return l}function Ug(r){return Object.fromEntries(Object.entries(r).filter(l=>typeof l[1]=="string"))}function qg(r){const l=r.trim();if(l==="")return;let i;try{i=JSON.parse(l)}catch{return new Error("Message attributes JSON is invalid")}if(!Qa(i))return new Error("Message attributes must be a JSON object");for(const[u,d]of Object.entries(i))if(!Qa(d)||typeof d.DataType!="string"||d.DataType.trim()==="")return new Error(`Message attribute ${u} must include DataType`);return i}function Au(r){const l=r.split(",").map(i=>i.trim()).filter(Boolean);return l.length>0?l:void 0}function Vg(r){if(r)try{const l=JSON.parse(r);if(!Qa(l))return;const i=l.deadLetterTargetArn,u=l.maxReceiveCount;return typeof i!="string"?void 0:{deadLetterTargetArn:i,maxReceiveCount:typeof u=="string"?u:String(u??"")}}catch{return}}function Kg(r){if(r)try{const l=JSON.parse(r);return!Qa(l)||typeof l.redrivePermission!="string"?void 0:{redrivePermission:l.redrivePermission}}catch{return}}function td(r){return r.length<=12?r===""?"":"...":`${r.slice(0,6)}...${r.slice(-6)}`}function Qa(r){return typeof r=="object"&&r!==null&&!Array.isArray(r)}function Jg({activeQueue:r,busyAction:l,purgeConfirmation:i,onSubmit:u,setPurgeConfirmation:d}){return n.jsxs("form",{className:"pubsub-action-form stacked",onSubmit:u,children:[n.jsx("span",{className:"toolbar-count",children:r?r.name:"No queue selected"}),n.jsxs("label",{className:"compact-filter",children:[n.jsx("span",{children:"Purge confirmation"}),n.jsx("input",{"aria-label":"Confirm SQS purge queue",disabled:!r,onChange:p=>d(p.target.value),placeholder:(r==null?void 0:r.name)??"queue name",value:i})]}),n.jsx(ae,{className:"danger",disabled:!r||i!==r.name||l==="purge-queue",type:"submit",children:"Purge"})]})}function Hg({activeQueue:r,busyAction:l,selectedReceiptHandle:i,visibilityTimeout:u,visibilityConfirmation:d,onSubmit:p,setVisibilityTimeout:g,setVisibilityConfirmation:y}){const S=!r||i==="";return n.jsxs("form",{className:"pubsub-action-form stacked",onSubmit:p,children:[n.jsxs("label",{className:"compact-filter small",children:[n.jsx("span",{children:"Visibility timeout"}),n.jsx("input",{"aria-label":"SQS change visibility timeout seconds",disabled:S,inputMode:"numeric",onChange:v=>g(v.target.value),value:u})]}),n.jsxs("label",{className:"compact-filter small",children:[n.jsx("span",{children:"Confirm"}),n.jsx("input",{"aria-label":"Confirm SQS change message visibility",disabled:S,onChange:v=>y(v.target.value),placeholder:"visibility",value:d})]}),n.jsx(ae,{disabled:S||d!=="visibility"||l==="change-visibility",type:"submit",children:"Change visibility"})]})}function Wg({activeQueue:r,busyAction:l,selectedReceivedMessage:i,selectedReceiptHandle:u,pastedReceiptHandle:d,deleteConfirmation:p,onSubmit:g,setPastedReceiptHandle:y,setDeleteConfirmation:S}){return n.jsxs("form",{className:"pubsub-action-form stacked",onSubmit:g,children:[n.jsxs("label",{className:"compact-filter wide",children:[n.jsx("span",{children:"Receipt handle"}),n.jsx("input",{"aria-label":"SQS delete receipt handle",disabled:!r,onChange:v=>y(v.target.value),placeholder:i?td(i.ReceiptHandle):"paste receipt handle or select received message",value:d})]}),n.jsxs("label",{className:"compact-filter small",children:[n.jsx("span",{children:"Confirm"}),n.jsx("input",{"aria-label":"Confirm SQS delete message",disabled:!r||u==="",onChange:v=>S(v.target.value),placeholder:"delete",value:p})]}),n.jsx(ae,{className:"danger",disabled:!r||u===""||p!=="delete"||l==="delete-message",type:"submit",children:"Delete"})]})}function Gg({activeIndex:r,detailState:l,messages:i,onSelectIndex:u,queueName:d}){return d?l.status==="loading"?n.jsx(F,{title:"Loading messages",description:`Reading messages from ${d}.`}):l.status==="error"?n.jsx(F,{title:"SQS messages unavailable",description:l.message}):i.length===0?n.jsx(F,{title:"No messages",description:`No retained messages in ${d} match the current filter.`}):n.jsx("div",{className:"dynamodb-item-table-wrap",children:n.jsxs("table",{className:"dynamodb-item-table",children:[n.jsx("thead",{children:n.jsxs("tr",{children:[n.jsx("th",{scope:"col",children:"State"}),n.jsx("th",{scope:"col",children:"Body"}),n.jsx("th",{scope:"col",children:"Receive"})]})}),n.jsx("tbody",{children:i.map((p,g)=>n.jsxs("tr",{className:g===r?"item-row active":"item-row",onClick:()=>u(g),children:[n.jsx("td",{children:p.state}),n.jsx("td",{children:n.jsx(Yg,{message:p})}),n.jsx("td",{children:p.receiveCount})]},`${p.messageId}-${g}`))})]})}):n.jsx(F,{title:"No queue selected",description:"Choose a queue to inspect retained messages."})}function Yg({message:r}){return n.jsxs("span",{className:"attribute-preview",children:[n.jsx("span",{className:"attribute-chip",children:r.body||"(empty body)"}),r.messageGroupId?n.jsxs("span",{className:"attribute-chip",children:["group: ",r.messageGroupId]}):null,r.sequenceNumber?n.jsxs("span",{className:"attribute-chip",children:["seq: ",r.sequenceNumber]}):null]})}function Xg({activeQueue:r,busyAction:l,sendBody:i,sendDelay:u,sendAttributesJSON:d,sendGroupId:p,sendDeduplicationId:g,onSubmit:y,setSendBody:S,setSendDelay:v,setSendAttributesJSON:N,setSendGroupId:w,setSendDeduplicationId:L}){return n.jsxs("form",{className:"pubsub-action-form stacked",onSubmit:y,children:[n.jsxs("label",{className:"compact-filter wide",children:[n.jsx("span",{children:"Message body"}),n.jsx("textarea",{"aria-label":"SQS message body",disabled:!r,onChange:C=>S(C.target.value),placeholder:"message body",rows:3,value:i})]}),n.jsxs("label",{className:"compact-filter small",children:[n.jsx("span",{children:"Delay"}),n.jsx("input",{"aria-label":"SQS message delay seconds",disabled:!r,inputMode:"numeric",onChange:C=>v(C.target.value),value:u})]}),n.jsxs("label",{className:"compact-filter",children:[n.jsx("span",{children:"Attributes JSON"}),n.jsx("input",{"aria-label":"SQS message attributes JSON",disabled:!r,onChange:C=>N(C.target.value),placeholder:'{"kind":{"DataType":"String","StringValue":"test"}}',value:d})]}),n.jsxs("label",{className:"compact-filter small",children:[n.jsx("span",{children:"Group"}),n.jsx("input",{"aria-label":"SQS FIFO message group ID",disabled:!r,onChange:C=>w(C.target.value),placeholder:"FIFO only",value:p})]}),n.jsxs("label",{className:"compact-filter small",children:[n.jsx("span",{children:"Dedup"}),n.jsx("input",{"aria-label":"SQS FIFO message deduplication ID",disabled:!r,onChange:C=>L(C.target.value),placeholder:"optional",value:g})]}),n.jsx(ae,{disabled:!r||l==="send-message",type:"submit",children:"Send"})]})}function Zg({activeQueue:r,busyAction:l,receiveMaxMessages:i,receiveVisibilityTimeout:u,receiveWaitTime:d,receiveAttributeNames:p,receiveMessageAttributeNames:g,onSubmit:y,setReceiveMaxMessages:S,setReceiveVisibilityTimeout:v,setReceiveWaitTime:N,setReceiveAttributeNames:w,setReceiveMessageAttributeNames:L}){return n.jsxs("form",{className:"pubsub-action-form stacked",onSubmit:y,children:[n.jsxs("label",{className:"compact-filter small",children:[n.jsx("span",{children:"Max"}),n.jsx("input",{"aria-label":"SQS receive max messages",disabled:!r,inputMode:"numeric",onChange:C=>S(C.target.value),value:i})]}),n.jsxs("label",{className:"compact-filter small",children:[n.jsx("span",{children:"Visibility"}),n.jsx("input",{"aria-label":"SQS receive visibility timeout seconds",disabled:!r,inputMode:"numeric",onChange:C=>v(C.target.value),value:u})]}),n.jsxs("label",{className:"compact-filter small",children:[n.jsx("span",{children:"Wait"}),n.jsx("input",{"aria-label":"SQS receive wait time seconds",disabled:!r,inputMode:"numeric",onChange:C=>N(C.target.value),value:d})]}),n.jsxs("label",{className:"compact-filter",children:[n.jsx("span",{children:"Attrs"}),n.jsx("input",{"aria-label":"SQS receive attribute names",disabled:!r,onChange:C=>w(C.target.value),placeholder:"All or comma-separated names",value:p})]}),n.jsxs("label",{className:"compact-filter",children:[n.jsx("span",{children:"Msg attrs"}),n.jsx("input",{"aria-label":"SQS receive message attribute names",disabled:!r,onChange:C=>L(C.target.value),placeholder:"All or comma-separated names",value:g})]}),n.jsx(ae,{disabled:!r||l==="receive-message",type:"submit",children:"Receive"})]})}function ex({activeQueueName:r,onSelectQueue:l,queues:i}){return i.length===0?n.jsx(F,{title:"No queues",description:"Queues created through the SQS API will appear here."}):n.jsx("div",{className:"dynamodb-table-list","aria-label":"SQS queues",children:i.map(u=>n.jsxs("button",{className:u.name===r?"dynamodb-table-row active":"dynamodb-table-row",onClick:()=>l(u.name),children:[n.jsxs("span",{className:"table-row-top",children:[n.jsx("span",{className:"table-row-name",children:u.name}),n.jsx("span",{className:"count-pill",children:u.totalRetainedMessages})]}),n.jsx("span",{className:"table-row-meta",children:u.url}),n.jsxs("span",{className:"table-row-tags",children:[n.jsxs("span",{children:[u.visibleMessages," visible"]}),n.jsxs("span",{children:[u.notVisibleMessages," in flight"]}),n.jsxs("span",{children:[u.delayedMessages," delayed"]})]})]},u.name))})}function tx({messages:r,onSelectIndex:l,selectedIndex:i}){return r.length===0?null:n.jsx("div",{className:"dynamodb-item-table-wrap","aria-label":"Received SQS messages",children:n.jsxs("table",{className:"dynamodb-item-table",children:[n.jsx("thead",{children:n.jsxs("tr",{children:[n.jsx("th",{scope:"col",children:"Use"}),n.jsx("th",{scope:"col",children:"Message"}),n.jsx("th",{scope:"col",children:"Receipt handle"})]})}),n.jsx("tbody",{children:r.map((u,d)=>n.jsxs("tr",{className:d===i?"item-row active":"item-row",onClick:()=>l(d),children:[n.jsx("td",{children:d===i?"selected":"select"}),n.jsx("td",{children:n.jsxs("span",{className:"attribute-preview",children:[n.jsx("span",{className:"attribute-chip",children:u.Body||"(empty body)"}),n.jsx("span",{className:"attribute-chip",children:u.MessageId})]})}),n.jsx("td",{children:n.jsx("code",{children:td(u.ReceiptHandle)})})]},`${u.MessageId}-${d}`))})]})})}function nx({detailState:r,message:l,queue:i,status:u}){var S;if(!i)return n.jsx(F,{title:"Inspector",description:"Queue attributes and selected message JSON will appear here."});const d=r.status==="success"?r.leases:[],p=r.status==="success"?r.dlq:void 0,g=Vg(i.attributes.RedrivePolicy),y=Kg(i.attributes.RedriveAllowPolicy);return n.jsxs("div",{className:"dynamodb-inspector",children:[n.jsxs("section",{children:[n.jsx("span",{className:"inspector-label",children:"Queue"}),n.jsx("h3",{children:i.name}),n.jsxs("dl",{className:"inspector-list",children:[n.jsxs("div",{children:[n.jsx("dt",{children:"Endpoint"}),n.jsx("dd",{children:n.jsx("code",{children:(u==null?void 0:u.endpoint)??"unknown"})})]}),n.jsxs("div",{children:[n.jsx("dt",{children:"Region"}),n.jsx("dd",{children:(u==null?void 0:u.region)??"unknown"})]}),n.jsxs("div",{children:[n.jsx("dt",{children:"ARN"}),n.jsx("dd",{children:n.jsx("code",{children:i.arn})})]}),n.jsxs("div",{children:[n.jsx("dt",{children:"Visibility"}),n.jsxs("dd",{children:[i.attributes.VisibilityTimeout??"unknown","s"]})]}),n.jsxs("div",{children:[n.jsx("dt",{children:"Leases"}),n.jsx("dd",{children:d.length})]}),n.jsxs("div",{children:[n.jsx("dt",{children:"DLQ sources"}),n.jsx("dd",{children:(p==null?void 0:p.deadLetterSourceQueues.map(v=>v.name).join(", "))||"none"})]}),n.jsxs("div",{children:[n.jsx("dt",{children:"DLQ target"}),n.jsx("dd",{children:((S=p==null?void 0:p.deadLetterQueue)==null?void 0:S.name)??(g==null?void 0:g.deadLetterTargetArn)??"none"})]}),n.jsxs("div",{children:[n.jsx("dt",{children:"Redrive"}),n.jsxs("dd",{children:[g?`maxReceiveCount ${g.maxReceiveCount}`:"none",y?`, allow ${y.redrivePermission}`:""]})]})]})]}),n.jsxs("section",{children:[n.jsx("span",{className:"inspector-label",children:"Selected message"}),l?n.jsx("pre",{className:"mail-preview",children:JSON.stringify(l,null,2)}):n.jsx("p",{className:"inspector-muted",children:"Select a message row to inspect JSON."})]})]})}function sx({service:r}){const[l,i]=m.useState({status:"loading"}),[u,d]=m.useState({status:"idle"}),[p,g]=m.useState(),[y,S]=m.useState(0),[v,N]=m.useState(""),[w,L]=m.useState(""),[C,A]=m.useState(""),[I,E]=m.useState("standard"),[J,ue]=m.useState("30"),[q,P]=m.useState("0"),[oe,pe]=m.useState(""),[Z,Re]=m.useState("0"),[ee,fe]=m.useState(""),[Se,me]=m.useState(""),[z,H]=m.useState(""),[be,$e]=m.useState("1"),[Ne,$]=m.useState("30"),[U,_]=m.useState("0"),[j,T]=m.useState("All"),[ne,ce]=m.useState("All"),[ge,we]=m.useState([]),[De,se]=m.useState(0),[Ie,Qe]=m.useState(""),[tt,Dt]=m.useState(""),[ut,de]=m.useState("0"),[ke,Y]=m.useState(""),[je,rt]=m.useState(""),[at,yt]=m.useState(""),[It,Ze]=m.useState(""),[Et,K]=m.useState(),[Be,Ue]=m.useState(),Le=(r==null?void 0:r.status)==="disabled",kt=wt(),_e=m.useCallback(()=>{const Ee=kt();if(Le){i({status:"success",statusPayload:Qg(r),queues:[]}),d({status:"idle"}),g(void 0);return}i({status:"loading"}),Promise.all([Eg(),Tg()]).then(([Ce,{queues:Fe}])=>{Ee()&&(i({status:"success",statusPayload:Ce,queues:Fe}),g(Ft=>{var En;return Ft&&Fe.some(xn=>xn.name===Ft)?Ft:(En=Fe[0])==null?void 0:En.name}))}).catch(Ce=>{Ee()&&i({status:"error",message:Ce.message})})},[kt,Le,r]);m.useEffect(()=>{_e()},[_e]),Vt({topics:["sqs"],onEvent:_e,onResync:_e,enabled:!Le});const jt=l.status==="success"?l.queues:[],Ae=jt.find(Ee=>Ee.name===p),hn=wt(),it=m.useCallback(()=>{const Ee=hn();if(!p||Le){d({status:"idle"});return}d({status:"loading"}),Promise.all([Lg(p),_g(p),Bg(p)]).then(([Ce,Fe,Ft])=>{Ee()&&(S(0),d({status:"success",messages:Ce.messages,leases:Fe.leases,dlq:Ft}))}).catch(Ce=>{Ee()&&d({status:"error",message:Ce.message})})},[hn,p,Le]);m.useEffect(()=>{it()},[it]);const Xn=m.useMemo(()=>{const Ee=v.trim().toLowerCase();return Ee===""?jt:jt.filter(Ce=>Ce.name.toLowerCase().includes(Ee))},[jt,v]),Tt=m.useMemo(()=>{const Ee=u.status==="success"?u.messages:[],Ce=w.trim().toLowerCase();return Ce===""?Ee:Ee.filter(Fe=>JSON.stringify(Fe).toLowerCase().includes(Ce))},[u,w]),an=Tt[Math.min(y,Math.max(Tt.length-1,0))],dt=ge[Math.min(De,Math.max(ge.length-1,0))],$t=Ie.trim()||(dt==null?void 0:dt.ReceiptHandle)||"";if(Le)return n.jsx(he,{title:"SQS",children:n.jsx(F,{title:"SQS is disabled",description:"Enable the SQS service in devcloud config to inspect queues and messages."})});function ln(Ee){g(Ee),S(0),we([]),se(0),Qe(""),Dt(""),Y(""),rt(""),L(""),Ue(void 0)}function In(Ee){if(Ee.preventDefault(),!!p){if(je!==p){Ue("Type the queue name to confirm purge confirmation");return}Ue(void 0),Fg(p).then(()=>{rt(""),_e(),it()}).catch(Ce=>{Ue(Ce.message)})}}async function ht(Ee,Ce){K(Ee),Ze(""),yt("");try{const Fe=await Ce();yt(Fe),_e(),it()}catch(Fe){Ze(Fe instanceof Error?Fe.message:"SQS operation failed")}finally{K(void 0)}}function on(Ee){Ee.preventDefault();const Ce=C.trim();if(Ce===""){Ze("Queue name is required");return}const Fe=I==="fifo"&&!Ce.endsWith(".fifo")?`${Ce}.fifo`:Ce;ht("create-queue",async()=>(await $g({QueueName:Fe,Attributes:Ug({VisibilityTimeout:Mu(J),DelaySeconds:Mu(q),FifoQueue:I==="fifo"?"true":void 0,ContentBasedDeduplication:I==="fifo"?"true":void 0}),Tags:{source:"dashboard"}}),A(""),g(Fe),`Created queue ${Fe}`))}function gn(Ee){if(Ee.preventDefault(),!Ae){Ze("Queue is required");return}if(oe===""){Ze("Message body is required");return}const Ce=qg(ee);if(Ce instanceof Error){Ze(Ce.message);return}ht("send-message",async()=>{const Fe=await Pg(Ae.name,{MessageBody:oe,DelaySeconds:Ou(Z),MessageAttributes:Ce,MessageGroupId:Se.trim()||void 0,MessageDeduplicationId:z.trim()||void 0});return pe(""),fe(""),`Sent message ${Fe.MessageId}`})}function hs(Ee){if(Ee.preventDefault(),!Ae){Ze("Queue is required");return}ht("receive-message",async()=>{const Fe=(await Mg(Ae.name,{MaxNumberOfMessages:Ou(be),VisibilityTimeout:Li(Ne),WaitTimeSeconds:Li(U),AttributeNames:Au(j),MessageAttributeNames:Au(ne)})).Messages??[];return we(Fe),se(0),Qe(""),Dt(""),Fe.length===1?"Received 1 message":`Received ${Fe.length} messages`})}function gs(Ee){if(Ee.preventDefault(),!Ae){Ze("Queue is required");return}if($t===""){Ze("Receipt handle is required");return}if(tt!=="delete"){Ze("Type delete to confirm DeleteMessage");return}ht("delete-message",async()=>(await Og(Ae.name,{ReceiptHandle:$t}),we(Ce=>Ce.filter(Fe=>Fe.ReceiptHandle!==$t)),se(0),Qe(""),Dt(""),Y(""),"Deleted message for the selected receipt handle"))}function xs(Ee){if(Ee.preventDefault(),!Ae){Ze("Queue is required");return}if($t===""){Ze("Receipt handle is required");return}if(ke!=="visibility"){Ze("Type visibility to confirm ChangeMessageVisibility");return}const Ce=Li(ut);if(typeof Ce!="number"){Ze("Visibility timeout must be a non-negative number");return}ht("change-visibility",async()=>(await Ag(Ae.name,{ReceiptHandle:$t,VisibilityTimeout:Ce}),Y(""),`Changed visibility timeout to ${Ce}s`))}return n.jsxs("div",{className:"dynamodb-workspace",children:[n.jsxs(he,{title:"Queues",children:[n.jsxs("div",{className:"dynamodb-toolbar",children:[n.jsxs("label",{className:"compact-filter",children:[n.jsx("span",{children:"Filter"}),n.jsx("input",{"aria-label":"Filter SQS queues",onChange:Ee=>N(Ee.target.value),placeholder:"queue name",type:"search",value:v})]}),n.jsx(ae,{onClick:_e,children:"Refresh"})]}),n.jsx(zg,{busyAction:Et,newQueueDelay:q,newQueueKind:I,newQueueName:C,newQueueVisibility:J,onSubmit:on,setNewQueueDelay:P,setNewQueueKind:E,setNewQueueName:A,setNewQueueVisibility:ue}),l.status==="loading"?n.jsx(F,{title:"Loading queues",description:"Reading local SQS queue metadata."}):null,l.status==="error"?n.jsx(F,{title:"SQS queues unavailable",description:l.message,actionLabel:"Retry",onAction:_e}):null,l.status==="success"?n.jsx(ex,{activeQueueName:p,onSelectQueue:ln,queues:Xn}):null]}),n.jsxs(he,{title:"Messages",children:[n.jsxs("div",{className:"dynamodb-toolbar",children:[n.jsx("span",{className:"toolbar-count",children:Ae?`${Tt.length} shown / ${Ae.totalRetainedMessages} retained`:"Select a queue"}),n.jsxs("label",{className:"compact-filter",children:[n.jsx("span",{children:"Filter"}),n.jsx("input",{"aria-label":"Filter SQS messages",disabled:!Ae,onChange:Ee=>{S(0),L(Ee.target.value)},placeholder:"body or attribute",type:"search",value:w})]}),n.jsx(ae,{disabled:!Ae,onClick:it,children:"Refresh"})]}),n.jsx(Xg,{activeQueue:!!Ae,busyAction:Et,onSubmit:gn,sendAttributesJSON:ee,sendBody:oe,sendDeduplicationId:z,sendDelay:Z,sendGroupId:Se,setSendAttributesJSON:fe,setSendBody:pe,setSendDeduplicationId:H,setSendDelay:Re,setSendGroupId:me}),n.jsx(Gg,{activeIndex:y,detailState:u,messages:Tt,onSelectIndex:S,queueName:p}),n.jsx(Zg,{activeQueue:!!Ae,busyAction:Et,onSubmit:hs,receiveAttributeNames:j,receiveMaxMessages:be,receiveMessageAttributeNames:ne,receiveVisibilityTimeout:Ne,receiveWaitTime:U,setReceiveAttributeNames:T,setReceiveMaxMessages:$e,setReceiveMessageAttributeNames:ce,setReceiveVisibilityTimeout:$,setReceiveWaitTime:_}),n.jsx(tx,{messages:ge,onSelectIndex:se,selectedIndex:De})]}),n.jsxs(he,{title:"Inspector",children:[It?n.jsx("p",{className:"operation-message error",children:It}):null,at?n.jsx("p",{className:"operation-message success",children:at}):null,n.jsx(Jg,{activeQueue:Ae,busyAction:Et,onSubmit:In,purgeConfirmation:je,setPurgeConfirmation:rt}),Be?n.jsx("p",{className:"inspector-muted",children:Be}):null,n.jsx(Hg,{activeQueue:!!Ae,busyAction:Et,onSubmit:xs,selectedReceiptHandle:$t,setVisibilityConfirmation:Y,setVisibilityTimeout:de,visibilityConfirmation:ke,visibilityTimeout:ut}),n.jsx(Wg,{activeQueue:!!Ae,busyAction:Et,deleteConfirmation:tt,onSubmit:gs,pastedReceiptHandle:Ie,selectedReceiptHandle:$t,selectedReceivedMessage:dt,setDeleteConfirmation:Dt,setPastedReceiptHandle:Qe}),n.jsx(nx,{detailState:u,message:an,queue:Ae,status:l.status==="success"?l.statusPayload:void 0})]})]})}async function rx(){return V("/api/pubsub/status")}async function ax(){return V("/api/pubsub/topics")}async function lx(r){return V("/api/pubsub/topics",{method:"POST",body:{topicId:r}})}async function ix(){return V("/api/pubsub/subscriptions")}async function ox(r){return V("/api/pubsub/subscriptions",{method:"POST",body:r})}async function cx(r){return V(`/api/pubsub/topics/${encodeURIComponent(r.topicId)}/publish`,{method:"POST",body:{messages:[{data:r.data,attributes:r.attributes,orderingKey:r.orderingKey}]}})}async function ux(r,l){return V(`/api/pubsub/subscriptions/${encodeURIComponent(r)}/pull`,{method:"POST",body:{maxMessages:l}})}async function dx(r,l){await V(`/api/pubsub/subscriptions/${encodeURIComponent(r)}/ack`,{method:"POST",body:{ackIds:l}})}function fx({service:r}){const[l,i]=m.useState({status:"loading"}),[u,d]=m.useState(),[p,g]=m.useState(),[y,S]=m.useState(""),[v,N]=m.useState(""),[w,L]=m.useState(""),[C,A]=m.useState(""),[I,E]=m.useState(""),[J,ue]=m.useState("10"),[q,P]=m.useState(""),[oe,pe]=m.useState(""),[Z,Re]=m.useState("1"),[ee,fe]=m.useState([]),[Se,me]=m.useState(""),[z,H]=m.useState(""),[be,$e]=m.useState(""),[Ne,$]=m.useState(),U=(r==null?void 0:r.status)==="disabled",_=wt(),j=m.useCallback(()=>{const de=_();if(U){i({status:"success",statusPayload:vx(r),topics:[],subscriptions:[]}),g(void 0);return}i({status:"loading"}),Promise.all([rx(),ax(),ix()]).then(([ke,Y,je])=>{de()&&(i({status:"success",statusPayload:ke,topics:Y.topics,subscriptions:je.subscriptions}),d(rt=>{var at;return rt&&Y.topics.some(yt=>yt.name===rt)?rt:(at=Y.topics[0])==null?void 0:at.name}),E(rt=>{var at;return rt||Bt(((at=Y.topics[0])==null?void 0:at.name)??"")}),g(rt=>{var at;return rt&&je.subscriptions.some(yt=>yt.name===rt)?rt:(at=je.subscriptions[0])==null?void 0:at.name}))}).catch(ke=>{de()&&i({status:"error",message:ke.message})})},[_,U,r]);m.useEffect(()=>{j()},[j]),Vt({topics:["pubsub"],onEvent:j,onResync:j,enabled:!U});const T=l.status==="success"?l.topics:[],ne=l.status==="success"?l.subscriptions:[],ce=T.find(de=>de.name===u),ge=ne.find(de=>de.name===p),we=m.useMemo(()=>{const de=y.trim().toLowerCase();return de===""?T:T.filter(ke=>ke.name.toLowerCase().includes(de))},[T,y]),De=m.useMemo(()=>{const de=v.trim().toLowerCase();return de===""?ne:ne.filter(ke=>JSON.stringify(ke).toLowerCase().includes(de))},[ne,v]);async function se(de,ke){$(de),$e(""),H("");try{const Y=await ke();H(Y),j()}catch(Y){$e(Y instanceof Error?Y.message:"Pub/Sub operation failed")}finally{$(void 0)}}function Ie(de){de.preventDefault();const ke=w.trim();if(ke===""){$e("Topic ID is required");return}se("create-topic",async()=>{const Y=await lx(ke);return L(""),d(Y.name),E(Bt(Y.name)),`Created topic ${Bt(Y.name)}`})}function Qe(de){de.preventDefault();const ke=C.trim(),Y=I.trim()||Bt((ce==null?void 0:ce.name)??"");if(ke===""||Y===""){$e("Subscription ID and topic are required");return}se("create-subscription",async()=>{const je=await ox({subscriptionId:ke,topicId:Y,ackDeadlineSeconds:Number.parseInt(J,10)||void 0});return A(""),g(je.name),`Created subscription ${Bt(je.name)}`})}function tt(de){de.preventDefault();const ke=Bt((ce==null?void 0:ce.name)??"");if(ke===""||q===""){$e("Topic and message are required");return}se("publish",async()=>{const Y=await cx({topicId:ke,data:gx(q),attributes:{source:"dashboard"},orderingKey:oe.trim()||void 0});return P(""),pe(""),`Published ${Y.messageIds.length} message`})}function Dt(){const de=Bt((ge==null?void 0:ge.name)??"");if(de===""){$e("Subscription is required");return}se("pull",async()=>{var je;const Y=(await ux(de,Number.parseInt(Z,10)||1)).receivedMessages??[];return fe(Y),me(((je=Y[0])==null?void 0:je.ackId)??""),`Pulled ${Y.length} message`})}function ut(){const de=Bt((ge==null?void 0:ge.name)??""),ke=Se?[Se]:ee.map(Y=>Y.ackId).filter(Boolean);if(de===""||ke.length===0){$e("Ack ID is required");return}se("ack",async()=>(await dx(de,ke),fe(Y=>Y.filter(je=>!ke.includes(je.ackId))),me(""),`Acknowledged ${ke.length} message`))}return U?n.jsx(he,{title:"Pub/Sub",children:n.jsx(F,{title:"Pub/Sub is disabled",description:"Enable the Pub/Sub service in devcloud config to inspect topics and subscriptions."})}):n.jsxs("div",{className:"dynamodb-workspace",children:[n.jsxs(he,{title:"Topics",children:[n.jsxs("div",{className:"dynamodb-toolbar",children:[n.jsxs("label",{className:"compact-filter",children:[n.jsx("span",{children:"Filter"}),n.jsx("input",{"aria-label":"Filter Pub/Sub topics",onChange:de=>S(de.target.value),placeholder:"topic name",type:"search",value:y})]}),n.jsx(ae,{onClick:j,children:"Refresh"})]}),n.jsxs("form",{className:"pubsub-action-form",onSubmit:Ie,children:[n.jsxs("label",{className:"compact-filter",children:[n.jsx("span",{children:"Topic ID"}),n.jsx("input",{"aria-label":"New Pub/Sub topic ID",onChange:de=>L(de.target.value),placeholder:"orders",value:w})]}),n.jsx(ae,{disabled:Ne==="create-topic",type:"submit",children:"Create"})]}),l.status==="loading"?n.jsx(F,{title:"Loading topics",description:"Reading local Pub/Sub resource metadata."}):null,l.status==="error"?n.jsx(F,{title:"Pub/Sub unavailable",description:l.message,actionLabel:"Retry",onAction:j}):null,l.status==="success"?n.jsx(px,{activeTopicName:u,onSelectTopic:d,topics:we}):null,n.jsxs("form",{className:"pubsub-action-form stacked",onSubmit:tt,children:[n.jsxs("label",{className:"compact-filter",children:[n.jsx("span",{children:"Message"}),n.jsx("input",{"aria-label":"Pub/Sub publish message",disabled:!ce,onChange:de=>P(de.target.value),placeholder:"message body",value:q})]}),n.jsxs("label",{className:"compact-filter",children:[n.jsx("span",{children:"Ordering key"}),n.jsx("input",{"aria-label":"Pub/Sub publish ordering key",disabled:!ce,onChange:de=>pe(de.target.value),placeholder:"optional",value:oe})]}),n.jsx(ae,{disabled:!ce||Ne==="publish",type:"submit",children:"Publish"})]})]}),n.jsxs(he,{title:"Subscriptions",children:[n.jsxs("div",{className:"dynamodb-toolbar",children:[n.jsx("span",{className:"toolbar-count",children:l.status==="success"?`${De.length} shown / ${ne.length} subscriptions`:"Loading"}),n.jsxs("label",{className:"compact-filter",children:[n.jsx("span",{children:"Filter"}),n.jsx("input",{"aria-label":"Filter Pub/Sub subscriptions",onChange:de=>N(de.target.value),placeholder:"subscription or topic",type:"search",value:v})]})]}),n.jsxs("form",{className:"pubsub-action-form",onSubmit:Qe,children:[n.jsxs("label",{className:"compact-filter",children:[n.jsx("span",{children:"Subscription ID"}),n.jsx("input",{"aria-label":"New Pub/Sub subscription ID",onChange:de=>A(de.target.value),placeholder:"orders-sub",value:C})]}),n.jsxs("label",{className:"compact-filter",children:[n.jsx("span",{children:"Topic"}),n.jsxs("select",{"aria-label":"Topic for new Pub/Sub subscription",onChange:de=>E(de.target.value),value:I,children:[n.jsx("option",{value:"",children:"Select topic"}),T.map(de=>n.jsx("option",{value:Bt(de.name),children:Bt(de.name)},de.name))]})]}),n.jsxs("label",{className:"compact-filter small",children:[n.jsx("span",{children:"Ack deadline"}),n.jsx("input",{"aria-label":"New Pub/Sub subscription ack deadline seconds",inputMode:"numeric",onChange:de=>ue(de.target.value),value:J})]}),n.jsx(ae,{disabled:Ne==="create-subscription",type:"submit",children:"Create"})]}),l.status==="success"?n.jsx(mx,{activeSubscriptionName:p,onSelectSubscription:g,subscriptions:De}):null]}),n.jsxs(he,{title:"Inspector",children:[be?n.jsx("p",{className:"operation-message error",children:be}):null,z?n.jsx("p",{className:"operation-message success",children:z}):null,n.jsx(hx,{busyAction:Ne,onAck:ut,onPull:Dt,pulledMessages:ee,pullMaxMessages:Z,selectedAckId:Se,setPullMaxMessages:Re,setSelectedAckId:me,status:l.status==="success"?l.statusPayload:void 0,subscription:ge,topicCount:T.length})]})]})}function px({activeTopicName:r,onSelectTopic:l,topics:i}){return i.length===0?n.jsx(F,{title:"No topics",description:"Topics created through Pub/Sub REST or SDK clients will appear here."}):n.jsx("div",{className:"dynamodb-table-list","aria-label":"Pub/Sub topics",children:i.map(u=>n.jsxs("button",{className:u.name===r?"dynamodb-table-row active":"dynamodb-table-row",onClick:()=>l(u.name),children:[n.jsxs("span",{className:"table-row-top",children:[n.jsx("span",{className:"table-row-name",children:Bt(u.name)}),n.jsx("span",{className:"count-pill",children:u.subscriptionCount})]}),n.jsx("span",{className:"table-row-meta",children:u.name}),n.jsx("span",{className:"table-row-tags",children:n.jsxs("span",{children:[u.subscriptionCount," subscriptions"]})})]},u.name))})}function mx({activeSubscriptionName:r,onSelectSubscription:l,subscriptions:i}){return i.length===0?n.jsx(F,{title:"No subscriptions",description:"Subscriptions created through Pub/Sub REST or SDK clients will appear here."}):n.jsx("div",{className:"dynamodb-item-table-wrap",children:n.jsxs("table",{className:"dynamodb-item-table",children:[n.jsx("thead",{children:n.jsxs("tr",{children:[n.jsx("th",{scope:"col",children:"Subscription"}),n.jsx("th",{scope:"col",children:"Backlog"}),n.jsx("th",{scope:"col",children:"In flight"}),n.jsx("th",{scope:"col",children:"Attempts"})]})}),n.jsx("tbody",{children:i.map(u=>n.jsxs("tr",{className:u.name===r?"item-row active":"item-row",onClick:()=>l(u.name),children:[n.jsx("td",{children:n.jsxs("span",{className:"attribute-preview",children:[n.jsx("span",{className:"attribute-chip",children:Bt(u.name)}),n.jsx("span",{className:"attribute-chip",children:Bt(u.topic)})]})}),n.jsx("td",{children:u.backlogMessages}),n.jsx("td",{children:u.inFlightMessages}),n.jsx("td",{children:u.maxDeliveryAttemptSeen})]},u.name))})]})})}function hx({busyAction:r,onAck:l,onPull:i,pulledMessages:u,pullMaxMessages:d,selectedAckId:p,setPullMaxMessages:g,setSelectedAckId:y,status:S,subscription:v,topicCount:N}){return v?n.jsxs("div",{className:"dynamodb-inspector",children:[n.jsxs("section",{children:[n.jsx("span",{className:"inspector-label",children:"Service"}),n.jsx("h3",{children:Bt(v.name)}),n.jsxs("dl",{className:"inspector-list",children:[n.jsxs("div",{children:[n.jsx("dt",{children:"REST"}),n.jsx("dd",{children:n.jsx("code",{children:(S==null?void 0:S.restEndpoint)??"unknown"})})]}),n.jsxs("div",{children:[n.jsx("dt",{children:"gRPC"}),n.jsx("dd",{children:n.jsx("code",{children:(S==null?void 0:S.grpcEndpoint)??"unknown"})})]}),n.jsxs("div",{children:[n.jsx("dt",{children:"Project"}),n.jsx("dd",{children:(S==null?void 0:S.project)??"unknown"})]}),n.jsxs("div",{children:[n.jsx("dt",{children:"Topics"}),n.jsx("dd",{children:N})]}),n.jsxs("div",{children:[n.jsx("dt",{children:"Ack deadline"}),n.jsxs("dd",{children:[v.ackDeadlineSeconds,"s"]})]}),n.jsxs("div",{children:[n.jsx("dt",{children:"Retained"}),n.jsx("dd",{children:v.totalRetainedMessages})]})]})]}),n.jsxs("section",{children:[n.jsx("span",{className:"inspector-label",children:"Actions"}),n.jsxs("div",{className:"pubsub-action-row",children:[n.jsxs("label",{className:"compact-filter small",children:[n.jsx("span",{children:"Pull max"}),n.jsx("input",{"aria-label":"Pub/Sub pull max messages",inputMode:"numeric",onChange:w=>g(w.target.value),value:d})]}),n.jsx(ae,{disabled:r==="pull",onClick:i,children:"Pull"}),n.jsx(ae,{disabled:u.length===0||r==="ack",onClick:l,children:"Ack"})]}),u.length>0?n.jsx("div",{className:"pulled-message-list",children:u.map(w=>n.jsxs("label",{className:"pulled-message",children:[n.jsx("input",{checked:p===w.ackId,name:"pubsub-ack-id",onChange:()=>y(w.ackId),type:"radio"}),n.jsxs("span",{children:[n.jsx("strong",{children:w.message.messageId??"pulled message"}),n.jsx("code",{children:xx(w.message.data??"")})]})]},w.ackId))}):null]}),n.jsxs("section",{children:[n.jsx("span",{className:"inspector-label",children:"Recent deliveries"}),v.recentDeliveries&&v.recentDeliveries.length>0?n.jsx(yx,{deliveries:v.recentDeliveries}):n.jsx("p",{className:"inspector-muted",children:"No retained delivery records for this subscription."})]})]}):n.jsx(F,{title:"Inspector",description:"Subscription backlog, leases, and recent deliveries will appear here."})}function gx(r){return window.btoa(unescape(encodeURIComponent(r)))}function xx(r){if(r==="")return"";try{return decodeURIComponent(escape(window.atob(r)))}catch{return r}}function yx({deliveries:r}){return n.jsx("div",{className:"dynamodb-item-table-wrap",children:n.jsxs("table",{className:"dynamodb-item-table",children:[n.jsx("thead",{children:n.jsxs("tr",{children:[n.jsx("th",{scope:"col",children:"Message"}),n.jsx("th",{scope:"col",children:"State"}),n.jsx("th",{scope:"col",children:"Attempt"}),n.jsx("th",{scope:"col",children:"Ready"})]})}),n.jsx("tbody",{children:r.map(l=>n.jsxs("tr",{className:"item-row",children:[n.jsx("td",{children:n.jsxs("span",{className:"attribute-preview",children:[n.jsx("span",{className:"attribute-chip",children:l.messageId}),l.orderingKey?n.jsx("span",{className:"attribute-chip",children:l.orderingKey}):null]})}),n.jsx("td",{children:l.state}),n.jsx("td",{children:l.deliveryAttempt}),n.jsx("td",{children:jx(l)})]},`${l.messageId}-${l.deliveryAttempt}-${l.state}`))})]})})}function jx(r){return r.leaseDeadline?`lease until ${Pi(r.leaseDeadline)}`:r.nextDeliveryTime?`retry at ${Pi(r.nextDeliveryTime)}`:r.publishTime?`published ${Pi(r.publishTime)}`:"available"}function vx(r){return{service:"pubsub",status:"disabled",running:!1,grpcEndpoint:"127.0.0.1:18085",restEndpoint:(r==null?void 0:r.endpoint)??"http://127.0.0.1:18086",project:"devcloud",storagePath:(r==null?void 0:r.storagePath)??".devcloud/data/pubsub",topicCount:0,subscriptionCount:0}}function Bt(r){return r.split("/").pop()??r}function Pi(r){const l=new Date(r);return Number.isNaN(l.getTime())?r:l.toLocaleString()}async function bx(){return V("/api/gcs/status")}async function Sx(){return V("/api/gcs/buckets")}async function Nx(r){return V("/api/gcs/buckets",{method:"POST",body:{name:r}})}async function wx(r){return mn(`/api/gcs/buckets/${encodeURIComponent(r)}`,{method:"DELETE"})}async function kx(r,l=""){const i=new URLSearchParams;l!==""&&i.set("prefix",l);const u=i.toString();return V(`/api/gcs/buckets/${encodeURIComponent(r)}/objects${u?`?${u}`:""}`)}async function Cx(r,l){return V(`/api/gcs/buckets/${encodeURIComponent(r)}/objects/${encodeURIComponent(l)}`)}async function Rx(r,l){return mn(`/api/gcs/buckets/${encodeURIComponent(r)}/objects/${encodeURIComponent(l)}`,{method:"DELETE"})}async function Dx(){return V("/api/gcs/upload-sessions")}async function Ix(r){return mn(`/api/gcs/uploads/${encodeURIComponent(r)}`,{method:"DELETE"})}function Ex({service:r}){const[l,i]=m.useState({status:"loading"}),[u,d]=m.useState({status:"idle"}),[p,g]=m.useState(),[y,S]=m.useState(),[v,N]=m.useState(),[w,L]=m.useState(""),[C,A]=m.useState(""),[I,E]=m.useState(),[J,ue]=m.useState(0),q=(r==null?void 0:r.status)==="disabled",P=$r(),oe=(v==null?void 0:v.bucket)===p?v==null?void 0:v.name:void 0,pe=wt(),Z=m.useCallback(()=>{const $=pe();if(q){i({status:"success",statusPayload:Bu(r),buckets:[],sessions:[]}),d({status:"idle"}),g(void 0),S(void 0),N(void 0);return}i({status:"loading"}),Promise.all([bx(),Sx(),Dx()]).then(([U,_,j])=>{$()&&(i({status:"success",statusPayload:U,buckets:_.buckets,sessions:j.sessions}),g(T=>{var ne;return T&&_.buckets.some(ce=>ce.name===T)?T:(ne=_.buckets[0])==null?void 0:ne.name}))}).catch(U=>{$()&&i({status:"error",message:U.message})})},[pe,q,r]);m.useEffect(()=>{Z()},[Z]);const Re=m.useCallback($=>{Z(),$.type.startsWith("gcs.object.")&&ue(U=>U+1)},[Z]);Vt({topics:["gcs"],onEvent:Re,onResync:()=>{Z(),ue($=>$+1)},enabled:!q}),m.useEffect(()=>{if(S(void 0),N(void 0),!p||q){d({status:"idle"});return}let $=!1;return d({status:"loading"}),kx(p,w.trim()).then(({objects:U})=>{$||d({status:"success",objects:U})}).catch(U=>{$||d({status:"error",message:U.message})}),()=>{$=!0}},[p,q,w,J]),m.useEffect(()=>{if(S(void 0),!v||v.bucket!==p||q)return;let $=!1;return Cx(v.bucket,v.name).then(U=>{$||S(U)}).catch(U=>{$||E(U.message)}),()=>{$=!0}},[p,v,q]);const ee=l.status==="success"?l.buckets:[],fe=l.status==="success"?l.sessions:[],Se=l.status==="success"?l.statusPayload:void 0;function me($){g($),S(void 0),N(void 0)}function z($){p&&N({bucket:p,name:$.name})}function H(){const $=C.trim();!$||q||Nx($).then(()=>{A(""),E(`Created bucket ${$}`),Z()}).catch(U=>E(U.message))}async function be($){q||!await P(Rn({title:"Delete bucket",description:"All objects and metadata in this bucket will be removed. This cannot be undone.",target:$}))||wx($).then(()=>{E(`Deleted bucket ${$}`),Z()}).catch(_=>E(_.message))}async function $e($){!p||q||!await P(Rn({title:"Delete object",description:"This object will be permanently removed from the bucket.",target:$.name}))||Rx(p,$.name).then(()=>{E(`Deleted object ${$.gcsUri}`),ue(_=>_+1)}).catch(_=>E(_.message))}async function Ne($){q||!await P(Rn({title:"Delete upload session",description:"In-progress upload state will be discarded.",target:$.id}))||Ix($.id).then(()=>{E(`Deleted upload session ${$.id}`),Z()}).catch(_=>E(_.message))}return q?n.jsxs(he,{title:"devcloud GCS",children:[n.jsx(F,{title:"GCS is disabled",description:"Enable the GCS service in devcloud config to inspect local buckets and objects."}),n.jsx(_u,{status:Se??Bu(r)})]}):n.jsxs("div",{className:"gcs-workspace",children:[n.jsxs(he,{title:"devcloud GCS",children:[n.jsxs("div",{className:"s3-toolbar",children:[n.jsxs("span",{children:[ee.length," buckets"]}),n.jsx(ae,{onClick:Z,children:"Refresh"})]}),l.status==="loading"?n.jsx(F,{title:"Loading GCS",description:"Reading local GCS status."}):null,l.status==="error"?n.jsx(F,{title:"GCS unavailable",description:l.message,actionLabel:"Retry",onAction:Z}):null,l.status==="success"?n.jsxs("div",{className:"gcs-sidebar",children:[n.jsx(_u,{status:l.statusPayload}),n.jsx(Tx,{bucketName:C,disabled:q,onChange:A,onCreate:H}),n.jsx($x,{activeBucket:p,buckets:ee,disabled:q,onDeleteBucket:be,onSelectBucket:me})]}):null]}),n.jsx(he,{title:"Objects",children:n.jsx(Lx,{activeObjectName:oe,bucketName:p,disabled:q,objectsState:u,onDeleteObject:$e,onInspectObject:z,onPrefixChange:L,prefix:w})}),n.jsx(he,{title:"Inspector",children:n.jsx(Px,{bucketName:p,object:y})}),n.jsx(he,{title:"Upload sessions",children:n.jsx(Mx,{disabled:q,onDeleteSession:Ne,sessions:fe})}),I?n.jsx("p",{className:"inspector-muted gcs-message",children:I}):null]})}function _u({status:r}){return n.jsxs("dl",{className:"inspector-list",children:[n.jsxs("div",{children:[n.jsx("dt",{children:"Status"}),n.jsx("dd",{children:r.running?"running":"disabled"})]}),n.jsxs("div",{children:[n.jsx("dt",{children:"Project"}),n.jsx("dd",{children:r.project})]}),n.jsxs("div",{children:[n.jsx("dt",{children:"Endpoint"}),n.jsx("dd",{children:n.jsx("code",{children:r.endpoint})})]}),n.jsxs("div",{children:[n.jsx("dt",{children:"Upload sessions"}),n.jsx("dd",{children:n.jsx("code",{children:r.uploadSessionPath})})]})]})}function Tx({bucketName:r,disabled:l,onChange:i,onCreate:u}){return n.jsxs("div",{className:"gcs-create-bucket",children:[n.jsxs("label",{className:"prefix-filter",children:[n.jsx("span",{children:"Create bucket"}),n.jsx("input",{"aria-label":"GCS bucket name",disabled:l,onChange:d=>i(d.target.value),placeholder:"local-bucket",value:r})]}),n.jsx(ae,{disabled:l||r.trim()==="",onClick:u,children:"Create"})]})}function $x({activeBucket:r,buckets:l,disabled:i,onDeleteBucket:u,onSelectBucket:d}){return l.length===0?n.jsx(F,{title:"No buckets",description:"Buckets created through the GCS API will appear here."}):n.jsx("div",{className:"bucket-list",children:l.map(p=>n.jsxs("div",{className:p.name===r?"bucket-item active gcs-bucket-row":"bucket-item gcs-bucket-row",children:[n.jsxs("button",{className:"object-select",onClick:()=>d(p.name),children:[n.jsx("span",{className:"bucket-name",children:p.name}),n.jsx("span",{className:"bucket-meta",children:p.gcsUri}),n.jsxs("span",{className:"bucket-meta",children:["Created ",Ui(p.timeCreated)]})]}),n.jsx("span",{className:"count-pill",children:p.objectCount}),n.jsx(ae,{className:"danger",disabled:i,onClick:()=>u(p.name),children:"Delete bucket"})]},p.name))})}function Lx({activeObjectName:r,bucketName:l,disabled:i,objectsState:u,onDeleteObject:d,onInspectObject:p,onPrefixChange:g,prefix:y}){return l?n.jsxs("div",{className:"object-browser",children:[n.jsxs("label",{className:"prefix-filter",children:[n.jsx("span",{children:"Prefix"}),n.jsx("input",{"aria-label":"Filter GCS objects by prefix",onChange:S=>g(S.target.value),placeholder:"docs/",type:"search",value:y})]}),u.status==="loading"?n.jsx(F,{title:"Loading objects",description:`Reading objects in ${l}.`}):null,u.status==="error"?n.jsx(F,{title:"Objects unavailable",description:u.message}):null,u.status==="success"&&u.objects.length===0?n.jsx(F,{title:"No objects",description:`No objects in ${l} match the current prefix.`}):null,u.status==="success"&&u.objects.length>0?n.jsx("div",{className:"object-table-wrap",children:n.jsxs("table",{className:"object-table",children:[n.jsx("thead",{children:n.jsxs("tr",{children:[n.jsx("th",{scope:"col",children:"Name"}),n.jsx("th",{scope:"col",children:"Size"}),n.jsx("th",{scope:"col",children:"Updated"}),n.jsx("th",{scope:"col",children:"Class"}),n.jsx("th",{scope:"col",children:"Actions"})]})}),n.jsx("tbody",{children:u.objects.map(S=>n.jsxs("tr",{className:S.name===r?"object-row active":"object-row",children:[n.jsxs("td",{children:[n.jsx("button",{className:"object-select",onClick:()=>p(S),children:n.jsx("span",{className:"object-key",children:S.name})}),n.jsx("code",{children:S.gcsUri})]}),n.jsx("td",{children:nd(S.size)}),n.jsx("td",{children:Ui(S.updated)}),n.jsx("td",{children:S.storageClass||"STANDARD"}),n.jsxs("td",{children:[n.jsx("a",{className:"object-action",href:sd(S.downloadUrl),children:"Download"}),n.jsx(ae,{className:"danger",disabled:i,onClick:()=>d(S),children:"Delete object"})]})]},S.name))})]})}):null]}):n.jsx(F,{title:"Select a bucket",description:"Choose a bucket to browse objects and metadata."})}function Px({bucketName:r,object:l}){if(!l)return n.jsx(F,{title:"Inspector",description:r?`Select an object in ${r} to inspect GCS metadata.`:"Object metadata, generation, and download links will appear here."});const i=Object.entries(l.metadata??{});return n.jsxs("div",{className:"object-inspector",children:[n.jsxs("div",{children:[n.jsx("span",{className:"inspector-label",children:"gs:// URI"}),n.jsx("code",{children:l.gcsUri})]}),n.jsxs("dl",{className:"inspector-list",children:[n.jsxs("div",{children:[n.jsx("dt",{children:"generation"}),n.jsx("dd",{children:l.generation})]}),n.jsxs("div",{children:[n.jsx("dt",{children:"metageneration"}),n.jsx("dd",{children:l.metageneration})]}),n.jsxs("div",{children:[n.jsx("dt",{children:"storageClass"}),n.jsx("dd",{children:l.storageClass||"STANDARD"})]}),n.jsxs("div",{children:[n.jsx("dt",{children:"crc32c"}),n.jsx("dd",{children:l.crc32c||"unknown"})]}),n.jsxs("div",{children:[n.jsx("dt",{children:"contentType"}),n.jsx("dd",{children:l.contentType||"application/octet-stream"})]}),n.jsxs("div",{children:[n.jsx("dt",{children:"ETag"}),n.jsx("dd",{children:n.jsx("code",{children:l.etag||"unknown"})})]})]}),n.jsxs("div",{children:[n.jsx("span",{className:"inspector-label",children:"metadata"}),i.length===0?n.jsx("p",{className:"inspector-muted",children:"No user metadata."}):n.jsx("dl",{className:"metadata-list",children:i.map(([u,d])=>n.jsxs("div",{children:[n.jsx("dt",{children:u}),n.jsx("dd",{children:d})]},u))})]}),n.jsx("a",{className:"compat-link inspector-download",href:sd(l.downloadUrl),children:"Download object"})]})}function Mx({disabled:r,onDeleteSession:l,sessions:i}){return i.length===0?n.jsx(F,{title:"No upload sessions",description:"Active resumable upload sessions will appear here."}):n.jsx("div",{className:"object-table-wrap",children:n.jsxs("table",{className:"object-table",children:[n.jsx("thead",{children:n.jsxs("tr",{children:[n.jsx("th",{scope:"col",children:"Session"}),n.jsx("th",{scope:"col",children:"Object"}),n.jsx("th",{scope:"col",children:"Received"}),n.jsx("th",{scope:"col",children:"Created"}),n.jsx("th",{scope:"col",children:"Action"})]})}),n.jsx("tbody",{children:i.map(u=>n.jsxs("tr",{children:[n.jsx("td",{children:n.jsx("code",{children:u.id})}),n.jsxs("td",{children:[n.jsx("span",{className:"object-key",children:u.name}),n.jsx("code",{children:`gs://${u.bucket}/${u.name}`})]}),n.jsx("td",{children:nd(u.receivedBytes)}),n.jsx("td",{children:Ui(u.createdAt)}),n.jsx("td",{children:n.jsx(ae,{className:"danger",disabled:r,onClick:()=>l(u),children:"Delete session"})})]},u.id))})]})})}function Bu(r){return{status:"disabled",running:!1,endpoint:(r==null?void 0:r.endpoint)??"http://127.0.0.1:14443",project:"devcloud",storagePath:(r==null?void 0:r.storagePath)??".devcloud/data/s3",uploadSessionPath:".devcloud/data/gcs/upload_sessions"}}function nd(r){if(!Number.isFinite(r)||r<0)return"unknown";if(r<1024)return`${r} B`;const l=["KB","MB","GB","TB"];let i=r/1024,u=0;for(;i>=1024&&u=10?0:1)} ${l[u]}`}function Ui(r){const l=new Date(r);return Number.isNaN(l.getTime())?r||"unknown":l.toLocaleString()}function sd(r){return r.startsWith("/api/gcs/")?r:"#"}async function Ox(){return V("/api/applicationautoscaling/status")}async function Ax(){return V("/api/applicationautoscaling/scalable-targets")}async function _x(){return V("/api/applicationautoscaling/scaling-policies")}async function Bx(){return V("/api/applicationautoscaling/scheduled-actions")}function Fx({service:r}){const[l,i]=m.useState({status:"loading"}),u=(r==null?void 0:r.status)==="disabled",d=wt(),p=m.useCallback(()=>{const g=d();if(u){i({status:"success",statusPayload:Vx(r),scalableTargets:[],scalingPolicies:[],scheduledActions:[]});return}i({status:"loading"}),Promise.all([Ox(),Ax(),_x(),Bx()]).then(([y,S,v,N])=>{g()&&i({status:"success",statusPayload:y,scalableTargets:S.scalableTargets,scalingPolicies:v.scalingPolicies,scheduledActions:N.scheduledActions})}).catch(y=>{g()&&i({status:"error",message:y.message})})},[d,u,r]);return m.useEffect(()=>{p()},[p]),Vt({topics:["applicationautoscaling"],onEvent:p,onResync:p,enabled:!u}),u?n.jsx(he,{title:"Application Auto Scaling",children:n.jsx(F,{title:"Application Auto Scaling is disabled",description:"Enable the Application Auto Scaling service in devcloud config to inspect scalable targets, policies, and scheduled actions."})}):n.jsxs("div",{className:"dynamodb-workspace",children:[n.jsxs(he,{title:"Status",children:[n.jsxs("div",{className:"dynamodb-toolbar",children:[n.jsx("span",{className:"toolbar-count",children:l.status==="success"?`${l.statusPayload.status} / ${l.statusPayload.region}`:"Loading"}),n.jsx(ae,{onClick:p,children:"Refresh"})]}),l.status==="loading"?n.jsx(F,{title:"Loading Application Auto Scaling",description:"Reading local scalable targets, policies, and scheduled actions."}):null,l.status==="error"?n.jsx(F,{title:"Application Auto Scaling unavailable",description:l.message,actionLabel:"Retry",onAction:p}):null,l.status==="success"?n.jsx(zx,{status:l.statusPayload}):null]}),n.jsx(he,{title:"Scalable targets",children:n.jsx(Qx,{targets:l.status==="success"?l.scalableTargets:[]})}),n.jsx(he,{title:"Scaling policies",children:n.jsx(Ux,{policies:l.status==="success"?l.scalingPolicies:[]})}),n.jsx(he,{title:"Scheduled actions",children:n.jsx(qx,{actions:l.status==="success"?l.scheduledActions:[]})})]})}function zx({status:r}){return n.jsxs("dl",{className:"inspector-list",children:[n.jsxs("div",{children:[n.jsx("dt",{children:"Endpoint"}),n.jsx("dd",{children:n.jsx("code",{children:r.endpoint})})]}),n.jsxs("div",{children:[n.jsx("dt",{children:"Region"}),n.jsx("dd",{children:r.region})]}),n.jsxs("div",{children:[n.jsx("dt",{children:"Storage path"}),n.jsx("dd",{children:n.jsx("code",{children:r.storagePath})})]}),n.jsxs("div",{children:[n.jsx("dt",{children:"Scalable targets"}),n.jsx("dd",{children:r.scalableTargetCount})]}),n.jsxs("div",{children:[n.jsx("dt",{children:"Scaling policies"}),n.jsx("dd",{children:r.scalingPolicyCount})]}),n.jsxs("div",{children:[n.jsx("dt",{children:"Scheduled actions"}),n.jsx("dd",{children:r.scheduledActionCount})]})]})}function Qx({targets:r}){return r.length===0?n.jsx(F,{title:"No scalable targets",description:"Targets registered via RegisterScalableTarget will appear here."}):n.jsx("div",{className:"dynamodb-table-list","aria-label":"Application Auto Scaling scalable targets",children:r.map(l=>{var i,u,d;return n.jsxs("section",{className:"dynamodb-table-row",children:[n.jsxs("span",{className:"table-row-top",children:[n.jsx("span",{className:"table-row-name",children:l.ResourceId}),n.jsxs("span",{className:"count-pill",children:[l.MinCapacity,"-",l.MaxCapacity]})]}),n.jsx("span",{className:"table-row-meta",children:l.ScalableDimension}),n.jsxs("span",{className:"table-row-tags",children:[n.jsx("span",{children:l.ServiceNamespace}),(i=l.SuspendedState)!=null&&i.DynamicScalingInSuspended||(u=l.SuspendedState)!=null&&u.DynamicScalingOutSuspended||(d=l.SuspendedState)!=null&&d.ScheduledScalingSuspended?n.jsx("span",{children:"suspended"}):null]})]},`${l.ResourceId}-${l.ScalableDimension}`)})})}function Ux({policies:r}){return r.length===0?n.jsx(F,{title:"No scaling policies",description:"Policies created via PutScalingPolicy will appear here."}):n.jsx("div",{className:"dynamodb-table-list","aria-label":"Application Auto Scaling scaling policies",children:r.map(l=>n.jsxs("section",{className:"dynamodb-table-row",children:[n.jsxs("span",{className:"table-row-top",children:[n.jsx("span",{className:"table-row-name",children:l.PolicyName}),n.jsx("span",{className:"count-pill",children:l.PolicyType})]}),n.jsx("span",{className:"table-row-meta",children:l.ResourceId}),n.jsx("span",{className:"table-row-tags",children:n.jsx("span",{children:l.ScalableDimension})})]},l.PolicyARN))})}function qx({actions:r}){return r.length===0?n.jsx(F,{title:"No scheduled actions",description:"Actions created via PutScheduledAction will appear here."}):n.jsx("div",{className:"dynamodb-table-list","aria-label":"Application Auto Scaling scheduled actions",children:r.map(l=>n.jsxs("section",{className:"dynamodb-table-row",children:[n.jsx("span",{className:"table-row-top",children:n.jsx("span",{className:"table-row-name",children:l.ScheduledActionName})}),n.jsx("span",{className:"table-row-meta",children:l.ResourceId}),n.jsxs("span",{className:"table-row-tags",children:[n.jsx("span",{children:l.Schedule}),l.Timezone?n.jsx("span",{children:l.Timezone}):null]})]},l.ScheduledActionName))})}function Vx(r){return{service:"applicationautoscaling",status:"disabled",running:!1,endpoint:(r==null?void 0:r.endpoint)??"http://127.0.0.1:18030",region:"us-east-1",storagePath:(r==null?void 0:r.storagePath)??".devcloud/data/applicationautoscaling",scalableTargetCount:0,scalingPolicyCount:0,scheduledActionCount:0}}async function Kx(){return V("/api/lambda/status")}async function Jx(){return V("/api/lambda/functions")}async function Hx(){return V("/api/lambda/invocations")}async function Wx(r,l){return V(`/api/lambda/functions/${encodeURIComponent(r)}/invoke`,{method:"POST",rawBody:l,headers:{"Content-Type":"application/json"},timeoutMs:91e4})}function Gx({service:r}){const[l,i]=m.useState({status:"loading"}),[u,d]=m.useState(),[p,g]=m.useState("{}"),[y,S]=m.useState(()=>new Map),v=m.useRef(new Map),N=m.useRef(0),w=(r==null?void 0:r.status)==="disabled",L=wt(),C=m.useCallback(()=>{const P=L();w||Promise.all([Kx(),Jx(),Hx()]).then(([oe,pe,Z])=>{P()&&(i({status:"success",statusPayload:oe,functions:pe.functions,invocations:Z.invocations}),d(Re=>{var ee;return Re&&pe.functions.some(fe=>fe.FunctionName===Re)?Re:(ee=pe.functions[0])==null?void 0:ee.FunctionName}))}).catch(oe=>{P()&&i({status:"error",message:oe.message})})},[L,w]);if(m.useEffect(()=>{C()},[C]),Vt({topics:["lambda"],onEvent:C,onResync:C,enabled:!w}),w)return n.jsx(he,{title:"Lambda",children:n.jsx(F,{title:"Lambda is disabled",description:"Enable the Lambda service in devcloud config to deploy and invoke local functions."})});const A=l.status==="success"?l.functions:[],I=l.status==="success"?l.invocations:[],E=A.find(P=>P.FunctionName===u),J=E&&y.get(E.FunctionName)||{status:"idle"},ue=(P,oe,pe)=>{v.current.get(P)===oe&&S(Z=>new Map(Z).set(P,pe))},q=P=>{if(P.preventDefault(),!E)return;const oe=E.FunctionName;N.current+=1;const pe=N.current;v.current.set(oe,pe);try{JSON.parse(p||"{}")}catch(Z){ue(oe,pe,{status:"error",message:`Event is not valid JSON: ${Z.message}`});return}ue(oe,pe,{status:"running"}),Wx(oe,p||"{}").then(Z=>ue(oe,pe,{status:"done",result:Z})).catch(Z=>ue(oe,pe,{status:"error",message:Z.message}))};return n.jsxs("div",{className:"dynamodb-workspace",children:[n.jsxs(he,{title:"Status",children:[n.jsxs("div",{className:"dynamodb-toolbar",children:[n.jsx("span",{className:"toolbar-count",children:l.status==="success"?`${l.statusPayload.status} / ${l.statusPayload.region}`:"Loading"}),n.jsx(ae,{onClick:C,children:"Refresh"})]}),l.status==="loading"?n.jsx(F,{title:"Loading Lambda",description:"Reading local functions and recent invocations."}):null,l.status==="error"?n.jsx(F,{title:"Lambda unavailable",description:l.message,actionLabel:"Retry",onAction:C}):null,l.status==="success"?n.jsx(Yx,{status:l.statusPayload}):null]}),n.jsx(he,{title:"Functions",children:A.length===0?n.jsx(F,{title:"No functions",description:"Functions created via CreateFunction will appear here."}):n.jsx("div",{className:"dynamodb-table-list","aria-label":"Lambda functions",children:A.map(P=>n.jsxs("button",{type:"button",className:`dynamodb-table-row${P.FunctionName===u?" active":""}`,"aria-pressed":P.FunctionName===u,onClick:()=>d(P.FunctionName),children:[n.jsxs("span",{className:"table-row-top",children:[n.jsx("span",{className:"table-row-name",children:P.FunctionName}),n.jsx("span",{className:"count-pill",children:P.Runtime})]}),n.jsx("span",{className:"table-row-meta",children:P.Handler}),n.jsxs("span",{className:"table-row-tags",children:[n.jsxs("span",{children:[P.MemorySize," MB"]}),n.jsxs("span",{children:[P.Timeout,"s"]}),n.jsx("span",{children:P.State})]})]},P.FunctionArn))})}),E?n.jsxs(he,{title:`Function: ${E.FunctionName}`,children:[n.jsx(Xx,{fn:E}),n.jsxs("form",{className:"pubsub-action-form stacked",onSubmit:q,children:[n.jsxs("label",{className:"compact-filter wide",children:[n.jsx("span",{children:"Test event (JSON)"}),n.jsx("textarea",{"aria-label":"Lambda test event JSON",onChange:P=>g(P.target.value),rows:4,value:p})]}),n.jsx(ae,{type:"submit",disabled:J.status==="running",children:J.status==="running"?"Invoking…":"Invoke"})]}),n.jsx(Zx,{state:J})]}):null,n.jsx(he,{title:"Recent invocations",children:I.length===0?n.jsx(F,{title:"No invocations yet",description:"Invoke a function to see its result and log tail here."}):n.jsx("div",{className:"dynamodb-table-list","aria-label":"Lambda invocations",children:I.map(P=>n.jsxs("details",{className:"dynamodb-table-row",children:[n.jsxs("summary",{className:"table-row-top",children:[n.jsx("span",{className:"table-row-name",children:P.functionName}),n.jsx("span",{className:"count-pill",children:P.status})]}),n.jsxs("span",{className:"table-row-meta",children:[P.startedAt," · ",P.invocationType," · ",P.durationMs.toFixed(1)," ms"]}),n.jsx("pre",{className:"redis-value-pre",children:P.logTail||"(no log output)"})]},P.requestId))})})]})}function Yx({status:r}){return n.jsxs("dl",{className:"inspector-list",children:[n.jsxs("div",{children:[n.jsx("dt",{children:"Endpoint"}),n.jsx("dd",{children:n.jsx("code",{children:r.endpoint})})]}),n.jsxs("div",{children:[n.jsx("dt",{children:"Region"}),n.jsx("dd",{children:r.region})]}),n.jsxs("div",{children:[n.jsx("dt",{children:"Storage path"}),n.jsx("dd",{children:n.jsx("code",{children:r.storagePath})})]}),n.jsxs("div",{children:[n.jsx("dt",{children:"Functions"}),n.jsx("dd",{children:r.functionCount})]}),n.jsxs("div",{children:[n.jsx("dt",{children:"Recorded invocations"}),n.jsx("dd",{children:r.invocationCount})]})]})}function Xx({fn:r}){var i;const l=Object.keys(((i=r.Environment)==null?void 0:i.Variables)??{});return n.jsxs("dl",{className:"inspector-list",children:[n.jsxs("div",{children:[n.jsx("dt",{children:"ARN"}),n.jsx("dd",{children:n.jsx("code",{children:r.FunctionArn})})]}),n.jsxs("div",{children:[n.jsx("dt",{children:"Role"}),n.jsx("dd",{children:n.jsx("code",{children:r.Role})})]}),n.jsxs("div",{children:[n.jsx("dt",{children:"Code"}),n.jsxs("dd",{children:[r.CodeSize," bytes · ",n.jsx("code",{children:r.CodeSha256})]})]}),n.jsxs("div",{children:[n.jsx("dt",{children:"Last modified"}),n.jsx("dd",{children:r.LastModified})]}),n.jsxs("div",{children:[n.jsx("dt",{children:"Environment keys"}),n.jsx("dd",{children:l.length>0?l.join(", "):"none"})]})]})}function Zx({state:r}){if(r.status==="idle"||r.status==="running")return null;if(r.status==="error")return n.jsx(F,{title:"Invoke failed",description:r.message});const{result:l}=r;return n.jsxs("div",{"aria-label":"Lambda invoke result",children:[n.jsx("p",{className:"inspector-muted",children:l.functionError?`Function error: ${l.functionError}`:`Succeeded (HTTP ${l.statusCode})`}),n.jsx("pre",{className:"redis-value-pre",children:l.payload}),n.jsx("pre",{className:"redis-value-pre",children:l.log})]})}async function ey(){return V("/api/cloudrun/status")}async function ty(){return V("/api/cloudrun/services")}async function ny(){return V("/api/cloudrun/instances")}function rd(r){const[,l,,i,,u]=r.split("/");return[l,i,u].map(encodeURIComponent).join("/")}async function sy(r){return V(`/api/cloudrun/services/${rd(r)}/revisions`)}async function ry(r){return V(`/api/cloudrun/services/${rd(r)}/logs`)}function Fa(r){return r.split("/").pop()??r}function ay({service:r}){const[l,i]=m.useState({status:"loading"}),[u,d]=m.useState(),[p,g]=m.useState(),y=(r==null?void 0:r.status)==="disabled",S=wt(),v=m.useCallback(()=>{const I=S();y||Promise.all([ey(),ty(),ny()]).then(([E,J,ue])=>{I()&&(i({status:"success",statusPayload:E,services:J.services,instances:ue.instances}),d(q=>{var P;return q&&J.services.some(oe=>oe.name===q)?q:(P=J.services[0])==null?void 0:P.name}))}).catch(E=>{I()&&i({status:"error",message:E.message})})},[S,y]);if(m.useEffect(()=>{v()},[v]),m.useEffect(()=>{if(!u||y){g(void 0);return}let I=!1;return Promise.all([sy(u),ry(u)]).then(([E,J])=>{I||g({service:u,revisions:E.revisions,logs:J.lines})}).catch(()=>{I||g({service:u,revisions:[],logs:[]})}),()=>{I=!0}},[u,y,l]),Vt({topics:["cloudrun"],onEvent:v,onResync:v,enabled:!y}),y)return n.jsx(he,{title:"Cloud Run",children:n.jsx(F,{title:"Cloud Run is disabled",description:"Enable the Cloud Run service in devcloud config to deploy and run local services."})});const N=l.status==="success"?l.services:[],w=l.status==="success"?l.instances:[],L=N.find(I=>I.name===u),C=w.find(I=>I.service===u),A=p&&p.service===u?p:void 0;return n.jsxs("div",{className:"dynamodb-workspace",children:[n.jsxs(he,{title:"Status",children:[n.jsxs("div",{className:"dynamodb-toolbar",children:[n.jsx("span",{className:"toolbar-count",children:l.status==="success"?`${l.statusPayload.status} / ${l.statusPayload.project} / ${l.statusPayload.region}`:"Loading"}),n.jsx(ae,{onClick:v,children:"Refresh"})]}),l.status==="loading"?n.jsx(F,{title:"Loading Cloud Run",description:"Reading local services and running instances."}):null,l.status==="error"?n.jsx(F,{title:"Cloud Run unavailable",description:l.message,actionLabel:"Retry",onAction:v}):null,l.status==="success"?n.jsx(ly,{status:l.statusPayload}):null]}),n.jsx(he,{title:"Services",children:N.length===0?n.jsx(F,{title:"No services",description:"Services created via the Cloud Run Admin API v2 will appear here."}):n.jsx("div",{className:"dynamodb-table-list","aria-label":"Cloud Run services",children:N.map(I=>{const E=w.some(J=>J.service===I.name);return n.jsxs("button",{type:"button",className:`dynamodb-table-row${I.name===u?" active":""}`,"aria-pressed":I.name===u,onClick:()=>d(I.name),children:[n.jsxs("span",{className:"table-row-top",children:[n.jsx("span",{className:"table-row-name",children:Fa(I.name)}),n.jsx("span",{className:"count-pill",children:E?"running":"idle"})]}),n.jsx("span",{className:"table-row-meta",children:I.name.split("/services/")[0]}),n.jsxs("span",{className:"table-row-tags",children:[n.jsxs("span",{children:["gen ",I.generation]}),n.jsx("span",{children:Fa(I.latestReadyRevision)})]})]},I.name)})})}),L?n.jsxs(he,{title:`Service: ${Fa(L.name)}`,children:[n.jsx(iy,{svc:L,instance:C}),n.jsx("h3",{className:"inspector-heading",children:"Revisions"}),A&&A.revisions.length>0?n.jsx("div",{className:"dynamodb-table-list","aria-label":"Cloud Run revisions",children:A.revisions.map(I=>{var E;return n.jsxs("section",{className:"dynamodb-table-row",children:[n.jsxs("span",{className:"table-row-top",children:[n.jsx("span",{className:"table-row-name",children:Fa(I.name)}),I.name===L.latestReadyRevision?n.jsx("span",{className:"count-pill",children:"latest"}):null]}),n.jsx("span",{className:"table-row-meta",children:I.createTime}),n.jsx("span",{className:"table-row-tags",children:n.jsx("span",{children:(E=I.containers[0])==null?void 0:E.image})})]},I.name)})}):n.jsx("p",{className:"inspector-muted",children:A?"No revisions.":"Loading revisions…"}),n.jsx("h3",{className:"inspector-heading",children:"Instance logs"}),n.jsx("pre",{className:"redis-value-pre","aria-label":"Cloud Run instance logs",children:A&&A.logs.length>0?A.logs.join(` -`):"(no output yet — send a request to start the instance)"})]}):null]})}function ly({status:r}){return n.jsxs("dl",{className:"inspector-list",children:[n.jsxs("div",{children:[n.jsx("dt",{children:"Endpoint"}),n.jsx("dd",{children:n.jsx("code",{children:r.endpoint})})]}),n.jsxs("div",{children:[n.jsx("dt",{children:"Default project / region"}),n.jsxs("dd",{children:[r.project," / ",r.region]})]}),n.jsxs("div",{children:[n.jsx("dt",{children:"Storage path"}),n.jsx("dd",{children:n.jsx("code",{children:r.storagePath})})]}),n.jsxs("div",{children:[n.jsx("dt",{children:"Services"}),n.jsx("dd",{children:r.serviceCount})]}),n.jsxs("div",{children:[n.jsx("dt",{children:"Running instances"}),n.jsx("dd",{children:r.instanceCount})]})]})}function iy({svc:r,instance:l}){var u;const i=r.template.containers[0];return n.jsxs("dl",{className:"inspector-list",children:[(r.urls??[r.uri]).map(d=>n.jsxs("div",{children:[n.jsx("dt",{children:"URL"}),n.jsx("dd",{children:n.jsx("a",{href:d,target:"_blank",rel:"noreferrer",children:n.jsx("code",{children:d})})})]},d)),n.jsxs("div",{children:[n.jsx("dt",{children:"Image"}),n.jsx("dd",{children:n.jsx("code",{children:i==null?void 0:i.image})})]}),n.jsxs("div",{children:[n.jsx("dt",{children:"Command"}),n.jsx("dd",{children:n.jsx("code",{children:(u=i==null?void 0:i.command)!=null&&u.length?[...i.command,...i.args??[]].join(" "):"(image entrypoint)"})})]}),n.jsxs("div",{children:[n.jsx("dt",{children:"Ingress"}),n.jsx("dd",{children:r.ingress})]}),n.jsxs("div",{children:[n.jsx("dt",{children:"Instance"}),n.jsx("dd",{children:l?`${l.mode} · port ${l.port}${l.pid?` · pid ${l.pid}`:""} · ${l.requestCount} requests`:"not running (starts on first request)"})]})]})}function oy({services:r,path:l}){return l==="/mail"?n.jsx(Pp,{service:r.find(i=>i.id==="mail")}):l==="/s3"?n.jsx(km,{service:r.find(i=>i.id==="s3")}):l==="/gcs"?n.jsx(Ex,{service:r.find(i=>i.id==="gcs")}):l==="/dynamodb"?n.jsx(xh,{service:r.find(i=>i.id==="dynamodb")}):l==="/bigquery"?n.jsx(Vh,{service:r.find(i=>i.id==="bigquery")}):l==="/redshift"?n.jsx(Yh,{service:r.find(i=>i.id==="redshift")}):l==="/redis"?n.jsx(dg,{service:r.find(i=>i.id==="redis")}):l==="/sqs"?n.jsx(sx,{service:r.find(i=>i.id==="sqs")}):l==="/pubsub"?n.jsx(fx,{service:r.find(i=>i.id==="pubsub")}):l==="/applicationautoscaling"?n.jsx(Fx,{service:r.find(i=>i.id==="applicationautoscaling")}):l==="/lambda"?n.jsx(Gx,{service:r.find(i=>i.id==="lambda")}):l==="/cloudrun"?n.jsx(ay,{service:r.find(i=>i.id==="cloudrun")}):n.jsx(jp,{services:r})}function cy({services:r}){const l=Er(window.location.pathname);return n.jsxs("nav",{className:"service-switcher","aria-label":"Services",children:[n.jsx("a",{"aria-current":l==="/"?"page":void 0,className:l==="/"?"active":void 0,href:za("/"),children:"Services"}),r.map(i=>n.jsxs("a",{"aria-current":l===Er(i.path)?"page":void 0,"aria-label":`${i.name}: ${i.status}`,className:l===Er(i.path)?"active":void 0,href:za(i.path),children:[n.jsx("span",{children:i.name}),n.jsxs("span",{className:`switcher-status ${uy(i.status)}`,children:[n.jsx("span",{className:"status-dot"}),i.status]})]},i.id))]})}function uy(r){return r==="running"?"running":"disabled"}function dy({services:r}){const l=r.filter(d=>d.status==="running").length,i=`${l}/${r.length} running`,u=r.length>0&&l===r.length?"running":"attention";return n.jsxs("div",{className:`status-pill ${u}`,"aria-label":`Service status: ${i}`,children:[n.jsx("span",{className:"status-dot"}),i]})}function fy({services:r}){const l=Er(window.location.pathname),i=r.find(p=>p.path===l),[u,d]=m.useState(()=>fp());return m.useEffect(()=>pp(d),[]),n.jsxs("footer",{className:"activity-footer",children:[n.jsxs("span",{children:["Last request: ",(u==null?void 0:u.path)??"/api/dashboard/services"]}),n.jsxs("span",{children:["Active: ",(i==null?void 0:i.name)??"Service index"]}),n.jsxs("span",{children:["API status: ",n.jsx(py,{activity:u,fallback:(i==null?void 0:i.status)??"running"})]}),n.jsxs("span",{children:["Storage: ",(i==null?void 0:i.storagePath)??"registry"]})]})}function py({activity:r,fallback:l}){if(!r)return n.jsx("span",{children:l});const i=r.status==="success"&&r.statusCode?`${r.status} ${r.statusCode}`:r.status;return n.jsx("span",{className:`activity-status ${r.status}`,title:r.message,children:i})}function my({notifications:r}){const{permission:l,enabled:i,setEnabled:u,requestPermission:d}=r;return l==="unsupported"?null:l==="default"?n.jsx(ae,{className:"notifications-toggle",onClick:()=>{d()},title:"Show desktop notifications when local services emit events",children:"Enable notifications"}):l==="denied"?n.jsx(ae,{className:"notifications-toggle is-disabled",disabled:!0,title:"Notifications are blocked. Allow them in your browser site settings to enable.",children:"Notifications blocked"}):n.jsx(ae,{"aria-pressed":i,className:i?"notifications-toggle is-on":"notifications-toggle",onClick:()=>u(!i),title:i?"Click to mute desktop notifications":"Click to enable desktop notifications",children:i?"Notifications: on":"Notifications: off"})}const ad="devcloud.notifications.enabled";function Fu(){return typeof window>"u"||typeof Notification>"u"?"unsupported":Notification.permission}function hy(){if(typeof window>"u")return!1;try{return window.localStorage.getItem(ad)==="1"}catch{return!1}}function gy(r){if(!(typeof window>"u"))try{window.localStorage.setItem(ad,r?"1":"0")}catch{}}function xy(){const[r,l]=m.useState(()=>Fu()),[i,u]=m.useState(()=>hy()),d=m.useCallback(g=>{u(g),gy(g)},[]),p=m.useCallback(async()=>{if(typeof Notification>"u")return"unsupported";try{const y=await Notification.requestPermission();return l(y),y==="granted"&&d(!0),y}catch{return Fu()}},[d]);return{permission:r,enabled:i,setEnabled:d,requestPermission:p}}const yy={mail:"Mail",s3:"S3",gcs:"GCS",redis:"Redis",dynamodb:"DynamoDB",bigquery:"BigQuery",sqs:"SQS",pubsub:"Pub/Sub",redshift:"Redshift"};function jy(r){return yy[r]??r}function qs(r,l){const i=r==null?void 0:r[l];if(typeof i=="string")return i;if(typeof i=="number"&&Number.isFinite(i))return String(i)}function ld(r,l){const i=r==null?void 0:r[l];return Array.isArray(i)?i.filter(u=>typeof u=="string"):[]}function id(r){return r.map(l=>Dn(l)).filter(l=>l.length>0).join(", ")}function vy(r){if(r===void 0)return;const l=Number(r);if(!(!Number.isFinite(l)||l<0))return l<1024?`${l} B`:l<1024*1024?`${(l/1024).toFixed(1)} KB`:l<1024*1024*1024?`${(l/1024/1024).toFixed(1)} MB`:`${(l/1024/1024/1024).toFixed(2)} GB`}const by=180;function Sy(r){return r.replace(/<(script|style)[\s\S]*?<\/\1>/gi," ").replace(/<[^>]+>/g," ").replace(/ /gi," ").replace(/&/gi,"&").replace(/</gi,"<").replace(/>/gi,">").replace(/"/gi,'"').replace(/'/gi,"'").replace(/\s+/g," ").trim()}function Ny(r,l){const i=r.replace(/\r/g,"").replace(/\n{3,}/g,` +}`),[U,B]=m.useState(""),[j,T]=m.useState(!1),[se,ce]=m.useState(!1),[xe,we]=m.useState(""),[De,re]=m.useState(""),[Ie,Qe]=m.useState(""),[tt,Dt]=m.useState(!1),[ut,fe]=m.useState(""),[ke,Y]=m.useState({status:"idle"});m.useEffect(()=>{r&&(E(K=>K||r),he(K=>K||r),B(K=>K||r),re(K=>K||r))},[r]),m.useEffect(()=>{i&&(H(K=>K||i),Qe(K=>K||i))},[i]);const je=l||ke.status==="running";function rt(K){K.preventDefault();const Be=w?Du(C,"Dataset raw JSON"):{datasetReference:{datasetId:p.trim()},location:g.trim()||void 0,description:v.trim()||void 0};if(Be instanceof Error){Y({status:"error",message:Be.message});return}if(!Iu(Be,["datasetReference","datasetId"])){Y({status:"error",message:"Dataset ID is required."});return}Y({status:"running",label:"Creating dataset"}),kh(d,Be).then(Le=>{Y({status:"success",message:`Created dataset ${Le.datasetReference.datasetId}.`}),E(Le.datasetReference.datasetId),he(Le.datasetReference.datasetId),u()}).catch(Le=>{Y({status:"error",message:Le.message})})}function at(K){K.preventDefault();const Be=I.trim()||r;if(!Be){Y({status:"error",message:"Choose a dataset before creating a table."});return}const Ue=Z?void 0:Ah(oe);if(Ue instanceof Error){Y({status:"error",message:Ue.message});return}const Le=Z?Du(ee,"Table raw JSON"):{tableReference:{tableId:J.trim()},schema:Ue,description:V.trim()||void 0};if(Le instanceof Error){Y({status:"error",message:Le.message});return}if(!Iu(Le,["tableReference","tableId"])){Y({status:"error",message:"Table ID is required."});return}Y({status:"running",label:"Creating table"}),Ch(d,Be,Le).then(_e=>{Y({status:"success",message:`Created table ${Be}.${_e.tableReference.tableId}.`}),he(Be),H(_e.tableReference.tableId),u()}).catch(_e=>{Y({status:"error",message:_e.message})})}function jt(K){K.preventDefault();const Be=Se.trim()||r,Ue=z.trim()||i;if(!Be||!Ue){Y({status:"error",message:"Choose a dataset and table before inserting rows."});return}const Le=_h(Ne,be.trim());if(Le instanceof Error){Y({status:"error",message:Le.message});return}Y({status:"running",label:"Inserting rows"}),Ih(d,Be,Ue,{rows:Le}).then(kt=>{var vt;const _e=(vt=kt.insertErrors)==null?void 0:vt.map(Ae=>{const hn=Ae.errors.map(lt=>lt.message).join("; ");return`row ${Ae.index}: ${hn}`});Y({status:"success",message:_e!=null&&_e.length?"Insert completed with partial insert errors.":"Inserted row data.",insertErrors:_e}),u()}).catch(kt=>{Y({status:"error",message:kt.message})})}function It(K){K.preventDefault();const Be=U.trim();if(!Be){Y({status:"error",message:"Choose a dataset to delete."});return}if(!se){Y({status:"error",message:"Acknowledge the delete-dataset destructive action before confirming the dataset ID."});return}if(xe!==Be){Y({status:"error",message:"Delete-dataset confirmation must match the dataset ID."});return}Y({status:"running",label:"Deleting dataset"}),Rh(d,Be,xe,j).then(()=>{Y({status:"success",message:`Deleted dataset ${Be}.`}),we(""),ce(!1),u()}).catch(Ue=>{Y({status:"error",message:Ue.message})})}function Ze(K){K.preventDefault();const Be=De.trim(),Ue=Ie.trim();if(!Be||!Ue){Y({status:"error",message:"Choose a dataset and table to delete."});return}if(!tt){Y({status:"error",message:"Acknowledge the delete-table destructive action before confirming the table ID."});return}if(ut!==Ue){Y({status:"error",message:"Delete-table confirmation must match the table ID."});return}Y({status:"running",label:"Deleting table"}),Dh(d,Be,Ue,ut).then(()=>{Y({status:"success",message:`Deleted table ${Be}.${Ue}.`}),fe(""),Dt(!1),u()}).catch(Le=>{Y({status:"error",message:Le.message})})}return n.jsxs("section",{className:"redshift-query-runner","aria-label":"BigQuery management controls",children:[n.jsxs("div",{className:"dynamodb-toolbar",children:[n.jsx("span",{className:"inspector-label",children:"Local management"}),n.jsx("span",{className:"toolbar-count",children:"datasets.insert / tables.insert / tabledata.insertAll"})]}),l?n.jsx("p",{className:"inspector-muted",children:"BigQuery management controls are disabled until the service is active."}):null,ke.status==="running"?n.jsxs("p",{className:"operation-message",children:[ke.label,"."]}):null,ke.status==="success"?n.jsxs("div",{className:"operation-message success",children:[ke.message,(Et=ke.insertErrors)!=null&&Et.length?n.jsx("ul",{children:ke.insertErrors.map(K=>n.jsx("li",{children:K},K))}):null]}):null,ke.status==="error"?n.jsx("p",{className:"operation-message error",children:ke.message}):null,n.jsxs("form",{className:"dynamodb-operation-form",onSubmit:rt,children:[n.jsxs("div",{className:"dynamodb-toolbar",children:[n.jsx("span",{className:"inspector-label",children:"Create dataset"}),n.jsxs("label",{className:"compact-filter small",children:[n.jsx("span",{children:"raw JSON"}),n.jsx("input",{"aria-label":"Use raw JSON for BigQuery dataset",checked:w,disabled:je,onChange:K=>L(K.target.checked),type:"checkbox"})]})]}),w?n.jsxs("label",{className:"redshift-sql-editor",children:[n.jsx("span",{children:"Dataset raw JSON"}),n.jsx("textarea",{"aria-label":"BigQuery dataset raw JSON",disabled:je,onChange:K=>_(K.target.value),rows:7,spellCheck:!1,value:C})]}):n.jsxs("div",{className:"pubsub-action-row",children:[n.jsxs("label",{className:"compact-filter",children:[n.jsx("span",{children:"Dataset ID"}),n.jsx("input",{"aria-label":"BigQuery dataset ID",disabled:je,onChange:K=>x(K.target.value),placeholder:"dashboard_ops",value:p})]}),n.jsxs("label",{className:"compact-filter small",children:[n.jsx("span",{children:"Location"}),n.jsx("input",{"aria-label":"BigQuery dataset location",disabled:je,onChange:K=>S(K.target.value),placeholder:"US",value:g})]}),n.jsxs("label",{className:"compact-filter wide",children:[n.jsx("span",{children:"Description"}),n.jsx("input",{"aria-label":"BigQuery dataset description",disabled:je,onChange:K=>N(K.target.value),placeholder:"local dashboard dataset",value:v})]})]}),n.jsx(ne,{disabled:je,type:"submit",children:"Create dataset"})]}),n.jsxs("form",{className:"dynamodb-operation-form",onSubmit:at,children:[n.jsxs("div",{className:"dynamodb-toolbar",children:[n.jsx("span",{className:"inspector-label",children:"Create table"}),n.jsxs("label",{className:"compact-filter small",children:[n.jsx("span",{children:"raw JSON"}),n.jsx("input",{"aria-label":"Use raw JSON for BigQuery table",checked:Z,disabled:je,onChange:K=>Re(K.target.checked),type:"checkbox"})]})]}),n.jsxs("div",{className:"pubsub-action-row",children:[n.jsxs("label",{className:"compact-filter",children:[n.jsx("span",{children:"Dataset ID"}),n.jsx("input",{"aria-label":"BigQuery table dataset ID",disabled:je,onChange:K=>E(K.target.value),placeholder:r??"dataset",value:I})]}),Z?null:n.jsxs(n.Fragment,{children:[n.jsxs("label",{className:"compact-filter",children:[n.jsx("span",{children:"Table ID"}),n.jsx("input",{"aria-label":"BigQuery table ID",disabled:je,onChange:K=>de(K.target.value),placeholder:"events",value:J})]}),n.jsxs("label",{className:"compact-filter wide",children:[n.jsx("span",{children:"Description"}),n.jsx("input",{"aria-label":"BigQuery table description",disabled:je,onChange:K=>$(K.target.value),placeholder:"local events table",value:V})]})]})]}),Z?n.jsxs("label",{className:"redshift-sql-editor",children:[n.jsx("span",{children:"Table raw JSON"}),n.jsx("textarea",{"aria-label":"BigQuery table raw JSON",disabled:je,onChange:K=>pe(K.target.value),rows:9,spellCheck:!1,value:ee})]}):n.jsxs("label",{className:"redshift-sql-editor",children:[n.jsx("span",{children:"Schema fields"}),n.jsx("textarea",{"aria-label":"BigQuery table schema fields",disabled:je,onChange:K=>me(K.target.value),rows:4,spellCheck:!1,value:oe})]}),n.jsx(ne,{disabled:je,type:"submit",children:"Create table"})]}),n.jsxs("form",{className:"dynamodb-operation-form",onSubmit:jt,children:[n.jsx("span",{className:"inspector-label",children:"Insert row"}),n.jsxs("div",{className:"pubsub-action-row",children:[n.jsxs("label",{className:"compact-filter",children:[n.jsx("span",{children:"Dataset ID"}),n.jsx("input",{"aria-label":"BigQuery insert dataset ID",disabled:je,onChange:K=>he(K.target.value),placeholder:r??"dataset",value:Se})]}),n.jsxs("label",{className:"compact-filter",children:[n.jsx("span",{children:"Table ID"}),n.jsx("input",{"aria-label":"BigQuery insert table ID",disabled:je,onChange:K=>H(K.target.value),placeholder:i??"table",value:z})]}),n.jsxs("label",{className:"compact-filter",children:[n.jsx("span",{children:"Insert ID"}),n.jsx("input",{"aria-label":"BigQuery insert ID",disabled:je,onChange:K=>Pe(K.target.value),placeholder:"optional",value:be})]})]}),n.jsxs("label",{className:"redshift-sql-editor",children:[n.jsx("span",{children:"Row JSON"}),n.jsx("textarea",{"aria-label":"BigQuery insert row JSON",disabled:je,onChange:K=>P(K.target.value),rows:6,spellCheck:!1,value:Ne})]}),n.jsx("p",{className:"inspector-muted",children:"JSON validation runs locally before tabledata.insertAll. Row payloads are not written to logs."}),n.jsx(ne,{disabled:je,type:"submit",children:"Insert row"})]}),n.jsxs("form",{className:"dynamodb-operation-form",onSubmit:It,children:[n.jsx("span",{className:"inspector-label",children:"Delete dataset"}),n.jsxs("div",{className:"pubsub-action-row",children:[n.jsxs("label",{className:"compact-filter",children:[n.jsx("span",{children:"Dataset ID"}),n.jsx("input",{"aria-label":"BigQuery delete dataset ID",disabled:je,onChange:K=>B(K.target.value),placeholder:r??"dataset",value:U})]}),n.jsxs("label",{className:"compact-filter small",children:[n.jsx("span",{children:"Delete contents"}),n.jsx("input",{"aria-label":"Delete BigQuery dataset contents",checked:j,disabled:je,onChange:K=>T(K.target.checked),type:"checkbox"})]})]}),n.jsxs("label",{className:"destructive-confirmation",children:[n.jsx("input",{"aria-label":"I understand this deletes the dataset and, if selected, its tables.",checked:se,disabled:je,onChange:K=>ce(K.target.checked),type:"checkbox"}),n.jsxs("span",{children:["Step 1: I understand this deletes the dataset",j?" and all of its tables":"","."]})]}),n.jsxs("label",{className:"compact-filter",children:[n.jsxs("span",{children:["Step 2: Type ",U||"dataset id"," to delete dataset"]}),n.jsx("input",{"aria-label":"BigQuery delete dataset confirmation",disabled:je,onChange:K=>we(K.target.value),value:xe})]}),n.jsx(ne,{className:"danger",disabled:je||!se||xe!==U.trim()||U.trim()==="",type:"submit",children:"Delete dataset"})]}),n.jsxs("form",{className:"dynamodb-operation-form",onSubmit:Ze,children:[n.jsx("span",{className:"inspector-label",children:"Delete table"}),n.jsxs("div",{className:"pubsub-action-row",children:[n.jsxs("label",{className:"compact-filter",children:[n.jsx("span",{children:"Dataset ID"}),n.jsx("input",{"aria-label":"BigQuery delete table dataset ID",disabled:je,onChange:K=>re(K.target.value),placeholder:r??"dataset",value:De})]}),n.jsxs("label",{className:"compact-filter",children:[n.jsx("span",{children:"Table ID"}),n.jsx("input",{"aria-label":"BigQuery delete table ID",disabled:je,onChange:K=>Qe(K.target.value),placeholder:i??"table",value:Ie})]})]}),n.jsxs("label",{className:"destructive-confirmation",children:[n.jsx("input",{"aria-label":"I understand this deletes the table and its local rows.",checked:tt,disabled:je,onChange:K=>Dt(K.target.checked),type:"checkbox"}),n.jsx("span",{children:"Step 1: I understand this deletes the table and its local rows."})]}),n.jsxs("label",{className:"compact-filter",children:[n.jsxs("span",{children:["Step 2: Type ",Ie||"table id"," to delete table"]}),n.jsx("input",{"aria-label":"BigQuery delete table confirmation",disabled:je,onChange:K=>fe(K.target.value),value:ut})]}),n.jsx(ne,{className:"danger",disabled:je||!tt||ut!==Ie.trim()||Ie.trim()==="",type:"submit",children:"Delete table"})]})]})}function zh({disabled:r,projectId:i,onQuerySuccess:l}){const[u,d]=m.useState("SELECT * FROM `analytics.people` LIMIT 10"),[p,x]=m.useState("25"),[g,S]=m.useState(!1),[v,N]=m.useState({status:"idle"}),[w,L]=m.useState(0),C=v.status==="success"?v.response:void 0,_=(C==null?void 0:C.rows)??[],I=_[Math.min(w,Math.max(_.length-1,0))];function E(J){J.preventDefault();const de=u.trim();if(de===""){N({status:"error",message:"Query text is required."});return}const V=Number.parseInt(p,10);if(!Number.isInteger(V)||V<1||V>1e3){N({status:"error",message:"Max results must be between 1 and 1000."});return}N({status:"running"}),wh(i,{query:de,maxResults:V,dryRun:g,useLegacySql:!1}).then($=>{L(0),N({status:"success",response:$}),l()}).catch($=>{N({status:"error",message:$.message})})}return n.jsxs("section",{className:"redshift-query-runner","aria-label":"BigQuery SQL query runner",children:[n.jsxs("div",{className:"dynamodb-toolbar",children:[n.jsx("span",{className:"inspector-label",children:"SQL query runner"}),n.jsx("span",{className:"toolbar-count",children:C?`${C.totalRows} rows / ${C.jobReference.jobId}`:"useLegacySql=false"})]}),n.jsxs("form",{className:"redshift-query-form",onSubmit:E,children:[n.jsxs("label",{className:"redshift-sql-editor",children:[n.jsx("span",{children:"Query text"}),n.jsx("textarea",{"aria-label":"BigQuery SQL query text",disabled:r||v.status==="running",onChange:J=>d(J.target.value),rows:5,spellCheck:!1,value:u})]}),n.jsxs("div",{className:"pubsub-action-row",children:[n.jsxs("label",{className:"compact-filter small",children:[n.jsx("span",{children:"Max results"}),n.jsx("input",{"aria-label":"BigQuery query max results",disabled:r||v.status==="running",max:1e3,min:1,onChange:J=>x(J.target.value),type:"number",value:p})]}),n.jsxs("label",{className:"compact-filter small",children:[n.jsx("span",{children:"Dry run"}),n.jsx("input",{"aria-label":"BigQuery dry run",checked:g,disabled:r||v.status==="running",onChange:J=>S(J.target.checked),type:"checkbox"})]}),n.jsx(ne,{disabled:r||v.status==="running",type:"submit",children:v.status==="running"?"Running":g?"Dry run":"Run query"})]})]}),r?n.jsx("p",{className:"inspector-muted",children:"BigQuery query controls are disabled until the service is active."}):null,v.status==="error"?n.jsx("p",{className:"operation-message error",children:v.message}):null,C?n.jsx(Qh,{response:C,selectedRow:I,selectedRowIndex:w,onSelectRow:L}):null]})}function Qh({onSelectRow:r,response:i,selectedRow:l,selectedRowIndex:u}){var S,v;const d=((S=i.schema)==null?void 0:S.fields)??[],p=i.rows??[],x=Math.max(d.length,((v=p[0])==null?void 0:v.f.length)??0),g=Array.from({length:x},(N,w)=>{var L,C;return{name:((L=d[w])==null?void 0:L.name)??`column_${w+1}`,type:((C=d[w])==null?void 0:C.type)??"UNKNOWN"}});return n.jsxs("div",{className:"redshift-query-result",children:[n.jsxs("div",{className:"attribute-preview","aria-label":"BigQuery query summary",children:[n.jsx("span",{className:"attribute-chip",children:i.jobComplete?"complete":"running"}),n.jsx("span",{className:"attribute-chip",children:i.cacheHit?"cache hit":"cache miss"}),n.jsxs("span",{className:"attribute-chip",children:[i.totalRows," rows"]}),n.jsx("span",{className:"attribute-chip",children:i.jobReference.jobId})]}),p.length===0?n.jsx("p",{className:"inspector-muted",children:"Query completed without loaded result rows."}):n.jsxs("div",{className:"dynamodb-query-result-grid",children:[n.jsx("div",{className:"dynamodb-item-table-wrap",children:n.jsxs("table",{className:"dynamodb-item-table compact",children:[n.jsx("thead",{children:n.jsx("tr",{children:g.map(N=>n.jsxs("th",{scope:"col",children:[N.name,n.jsx("span",{className:"query-column-type",children:N.type})]},N.name))})}),n.jsx("tbody",{children:p.map((N,w)=>n.jsx("tr",{className:w===u?"item-row active":"item-row",onClick:()=>r(w),children:g.map((L,C)=>{var _;return n.jsx("td",{children:Wu((_=N.f[C])==null?void 0:_.v)},L.name)})},`${i.jobReference.jobId}-${w}`))})]})}),n.jsxs("section",{children:[n.jsx("span",{className:"inspector-label",children:"Selected result JSON"}),n.jsx("pre",{className:"mail-preview",children:JSON.stringify(Oh(d,l),null,2)})]})]}),n.jsxs("section",{children:[n.jsx("span",{className:"inspector-label",children:"Job reference"}),n.jsx("pre",{className:"mail-preview",children:JSON.stringify(i.jobReference,null,2)})]})]})}function Uh({activeIndex:r,rows:i,rowsState:l,tableName:u,onSelectIndex:d}){return u?l.status==="loading"?n.jsx(A,{title:"Loading rows",description:`Reading rows from ${u}.`}):l.status==="error"?n.jsx(A,{title:"BigQuery rows unavailable",description:l.message}):i.length===0?n.jsx(A,{title:"No rows",description:`No loaded rows in ${u} match the current filter.`}):n.jsx("div",{className:"dynamodb-item-table-wrap",children:n.jsxs("table",{className:"dynamodb-item-table",children:[n.jsx("thead",{children:n.jsxs("tr",{children:[n.jsx("th",{scope:"col",children:"Insert ID"}),n.jsx("th",{scope:"col",children:"Values"}),n.jsx("th",{scope:"col",children:"Inserted"})]})}),n.jsx("tbody",{children:i.map((p,x)=>n.jsxs("tr",{className:x===r?"item-row active":"item-row",onClick:()=>d(x),children:[n.jsx("td",{children:n.jsx("code",{children:p.insertId??x+1})}),n.jsx("td",{children:n.jsx(qh,{item:p.json})}),n.jsx("td",{children:p.insertedAt??"unknown"})]},`${p.insertId??"row"}-${x}`))})]})}):n.jsx(A,{title:"No table selected",description:"Choose a BigQuery table to inspect stored rows."})}function qh({item:r}){const i=Object.entries(r).slice(0,6);return i.length===0?n.jsx("span",{className:"service-status",children:"empty row"}):n.jsx("span",{className:"attribute-preview",children:i.map(([l,u])=>n.jsxs("span",{className:"attribute-chip",children:[l,": ",Wu(u)]},l))})}function Vh({service:r}){const[i,l]=m.useState({status:"loading"}),[u,d]=m.useState({status:"idle"}),[p,x]=m.useState(),[g,S]=m.useState(),[v,N]=m.useState(0),[w,L]=m.useState(""),[C,_]=m.useState(""),I=(r==null?void 0:r.status)==="disabled",E=ht(),J=m.useCallback(()=>{const z=E();if(I){l({status:"success",statusPayload:Th(r),datasets:[],jobs:[]}),d({status:"idle"}),x(void 0),S(void 0);return}l({status:"loading"}),Promise.all([yh(),jh()]).then(async([H,be])=>{const Pe=be.projects[0],Ne=(Pe==null?void 0:Pe.projectId)??H.project,[{datasets:P},{jobs:U}]=await Promise.all([vh(Ne),Sh(Ne)]);z()&&(l({status:"success",statusPayload:H,datasets:P,jobs:U}),x(B=>{var j;return B&&P.some(T=>T.datasetId===B)?B:(j=P[0])==null?void 0:j.datasetId}),S(B=>{var T;const j=P.find(se=>se.datasetId===p)??P[0];return B&&(j!=null&&j.tables.some(se=>se.tableId===B))?B:(T=j==null?void 0:j.tables[0])==null?void 0:T.tableId}))}).catch(H=>{z()&&l({status:"error",message:H.message})})},[E,p,I,r]);m.useEffect(()=>{J()},[J]),Vt({topics:["bigquery"],onEvent:J,onResync:J,enabled:!I});const de=i.status==="success"?i.datasets:[],V=de.find(z=>z.datasetId===p),$=V==null?void 0:V.tables.find(z=>z.tableId===g),oe=i.status==="success"?i.statusPayload.project:r==null?void 0:r.endpoint,me=ht(),Z=m.useCallback(()=>{const z=me();if(!V||!$||i.status!=="success"||I){d({status:"idle"});return}d({status:"loading"}),bh(i.statusPayload.project,V.datasetId,$.tableId).then(({rows:H})=>{z()&&(N(0),d({status:"success",rows:H}))}).catch(H=>{z()&&d({status:"error",message:H.message})})},[me,V,$,i,I]);m.useEffect(()=>{Z()},[Z]);const Re=m.useMemo(()=>{const z=w.trim().toLowerCase();return z===""?de:de.filter(H=>H.datasetId.toLowerCase().includes(z))},[de,w]),ee=m.useMemo(()=>{const z=u.status==="success"?u.rows:[],H=C.trim().toLowerCase();return H===""?z:z.filter(be=>JSON.stringify(be).toLowerCase().includes(H))},[u,C]),pe=ee[Math.min(v,Math.max(ee.length-1,0))];if(I)return n.jsx(ue,{title:"BigQuery",children:n.jsx(A,{title:"BigQuery is disabled",description:"Enable the BigQuery service in devcloud config to inspect projects, datasets, tables, rows, and jobs."})});function Se(z){var H;x(z.datasetId),S((H=z.tables[0])==null?void 0:H.tableId),N(0),_("")}function he(z){S(z.tableId),N(0),_("")}return n.jsxs("div",{className:"dynamodb-workspace",children:[n.jsxs(ue,{title:"Datasets",children:[n.jsxs("div",{className:"dynamodb-toolbar",children:[n.jsxs("label",{className:"compact-filter",children:[n.jsx("span",{children:"Filter"}),n.jsx("input",{"aria-label":"Filter BigQuery datasets",onChange:z=>L(z.target.value),placeholder:"dataset id",type:"search",value:w})]}),n.jsx(ne,{onClick:J,children:"Refresh"})]}),i.status==="loading"?n.jsx(A,{title:"Loading datasets",description:"Reading local BigQuery catalog metadata."}):null,i.status==="error"?n.jsx(A,{title:"BigQuery catalog unavailable",description:i.message,actionLabel:"Retry",onAction:J}):null,i.status==="success"?n.jsx(Eh,{activeDatasetId:p,activeTableId:g,datasets:Re,onSelectDataset:Se,onSelectTable:he}):null]}),n.jsxs(ue,{title:"Rows",children:[n.jsx(zh,{disabled:I||i.status!=="success",projectId:oe??"devcloud",onQuerySuccess:J}),n.jsx(Fh,{activeDatasetId:V==null?void 0:V.datasetId,activeTableId:$==null?void 0:$.tableId,disabled:I||i.status!=="success",projectId:oe??"devcloud",onMutationSuccess:()=>{J(),Z()}}),n.jsxs("div",{className:"dynamodb-toolbar",children:[n.jsx("span",{className:"toolbar-count",children:$?`${ee.length} shown / ${$.numRows} reported`:"Select a table"}),n.jsxs("label",{className:"compact-filter",children:[n.jsx("span",{children:"Filter"}),n.jsx("input",{"aria-label":"Filter BigQuery rows",disabled:!$,onChange:z=>{N(0),_(z.target.value)},placeholder:"row value",type:"search",value:C})]}),n.jsx(ne,{disabled:!$,onClick:Z,children:"Refresh"})]}),n.jsx(Uh,{activeIndex:v,rows:ee,rowsState:u,tableName:$==null?void 0:$.tableId,onSelectIndex:N})]}),n.jsx(ue,{title:"Inspector",children:n.jsx(Bh,{dataset:V,jobs:i.status==="success"?i.jobs:[],project:oe??"unknown",row:pe,status:i.status==="success"?i.statusPayload:void 0,table:$})})]})}async function Kh(){return q("/api/redshift/status")}async function Jh(){return q("/api/redshift/clusters")}async function Hh(){return q("/api/redshift/catalog")}async function Wh(){return q("/api/redshift/statements")}async function Gh(r){return q("/api/redshift/query",{method:"POST",body:r,timeoutMs:15e3})}function Yh({service:r}){const[i,l]=m.useState({status:"loading"}),[u,d]=m.useState(),[p,x]=m.useState(""),g=(r==null?void 0:r.status)==="disabled",S=ht(),v=m.useCallback(()=>{const I=S();if(g){l({status:"success",statusPayload:sx(r),clusters:[],catalog:{database:"dev",schemas:[],tables:[],columns:[]},statements:[]}),d(void 0);return}l({status:"loading"}),Promise.all([Kh(),Jh(),Hh(),Wh()]).then(([E,J,de,V])=>{if(!I())return;const $=de.catalog.tables;l({status:"success",statusPayload:E,clusters:J.clusters,catalog:de.catalog,statements:V.statements}),d(oe=>oe&&$.some(me=>Us(me)===oe)?oe:Us($[0]))}).catch(E=>{I()&&l({status:"error",message:E.message})})},[S,g,r]);m.useEffect(()=>{v()},[v]),Vt({topics:["redshift"],onEvent:v,onResync:v,enabled:!g});const N=i.status==="success"?i.catalog:void 0,w=(N==null?void 0:N.tables)??[],L=m.useMemo(()=>{const I=p.trim().toLowerCase();return I===""?w:w.filter(E=>`${E.schema}.${E.name}`.toLowerCase().includes(I))},[p,w]),C=w.find(I=>Us(I)===u),_=(N==null?void 0:N.columns.filter(I=>C&&I.schema===C.schema&&I.table===C.name))??[];return g?n.jsx(ue,{title:"Redshift",children:n.jsx(A,{title:"Redshift is disabled",description:"Enable the Redshift service in devcloud config to inspect clusters, catalog metadata, and statements."})}):n.jsxs("div",{className:"redshift-workspace",children:[n.jsxs(ue,{title:"Clusters",children:[n.jsxs("div",{className:"dynamodb-toolbar",children:[n.jsx("span",{className:"toolbar-count",children:i.status==="success"?`${i.clusters.length} clusters`:"Loading"}),n.jsx(ne,{onClick:v,children:"Refresh"})]}),i.status==="loading"?n.jsx(A,{title:"Loading Redshift",description:"Reading local Redshift metadata."}):null,i.status==="error"?n.jsx(A,{title:"Redshift unavailable",description:i.message,actionLabel:"Retry",onAction:v}):null,i.status==="success"?n.jsx(Xh,{clusters:i.clusters,status:i.statusPayload}):null]}),n.jsxs(ue,{title:"Catalog",children:[n.jsxs("div",{className:"dynamodb-toolbar",children:[n.jsx("span",{className:"toolbar-count",children:`${L.length} shown / ${w.length} tables`}),n.jsxs("label",{className:"compact-filter",children:[n.jsx("span",{children:"Filter"}),n.jsx("input",{"aria-label":"Filter Redshift tables",onChange:I=>x(I.target.value),placeholder:"schema or table",type:"search",value:p})]})]}),n.jsx(Zh,{activeTableKey:u,tables:L,onSelectTable:I=>d(Us(I))})]}),n.jsx(ue,{title:"Inspector",children:n.jsx(ex,{columns:_,onQuerySuccess:v,statements:i.status==="success"?i.statements:[],status:i.status==="success"?i.statusPayload:void 0,table:C})})]})}function Xh({clusters:r,status:i}){return r.length===0?n.jsx(A,{title:"No clusters",description:"Local Redshift cluster metadata will appear here."}):n.jsx("div",{className:"dynamodb-table-list","aria-label":"Redshift clusters",children:r.map(l=>n.jsxs("section",{className:"dynamodb-table-row",children:[n.jsxs("span",{className:"table-row-top",children:[n.jsx("span",{className:"table-row-name",children:l.clusterIdentifier}),n.jsx("span",{className:"count-pill",children:l.clusterStatus})]}),n.jsxs("span",{className:"table-row-meta",children:[l.databaseName," on ",l.endpoint.address,":",l.endpoint.port]}),n.jsxs("span",{className:"table-row-tags",children:[n.jsx("span",{children:l.nodeType}),n.jsxs("span",{children:[l.numberOfNodes," node"]}),n.jsx("span",{children:i.region})]})]},l.clusterIdentifier))})}function Zh({activeTableKey:r,tables:i,onSelectTable:l}){return i.length===0?n.jsx(A,{title:"No tables",description:"Tables created through Redshift SQL or Data API clients will appear here."}):n.jsx("div",{className:"dynamodb-table-list","aria-label":"Redshift tables",children:i.map(u=>n.jsxs("button",{className:Us(u)===r?"dynamodb-table-row active":"dynamodb-table-row",onClick:()=>l(u),type:"button",children:[n.jsxs("span",{className:"table-row-top",children:[n.jsx("span",{className:"table-row-name",children:u.name}),n.jsx("span",{className:"count-pill",children:u.rowCount})]}),n.jsxs("span",{className:"table-row-meta",children:[u.schema," schema"]}),n.jsxs("span",{className:"table-row-tags",children:[n.jsxs("span",{children:[u.columnCount," columns"]}),u.distKey?n.jsxs("span",{children:["distkey ",u.distKey]}):null,u.sortKeys.length>0?n.jsxs("span",{children:["sortkey ",u.sortKeys.join(", ")]}):null]})]},Us(u)))})}function ex({columns:r,onQuerySuccess:i,statements:l,status:u,table:d}){return n.jsxs("div",{className:"dynamodb-inspector",children:[n.jsx(tx,{onQuerySuccess:i}),d?null:n.jsx(A,{title:"Inspector",description:"Table columns and recent statements will appear here."}),d?n.jsxs("section",{children:[n.jsx("span",{className:"inspector-label",children:"Table"}),n.jsx("h3",{children:`${d.schema}.${d.name}`}),n.jsxs("dl",{className:"inspector-list",children:[n.jsxs("div",{children:[n.jsx("dt",{children:"SQL endpoint"}),n.jsx("dd",{children:n.jsx("code",{children:(u==null?void 0:u.sqlEndpoint)??"127.0.0.1:15439"})})]}),n.jsxs("div",{children:[n.jsx("dt",{children:"API endpoint"}),n.jsx("dd",{children:n.jsx("code",{children:(u==null?void 0:u.apiEndpoint)??"http://127.0.0.1:19099"})})]}),n.jsxs("div",{children:[n.jsx("dt",{children:"Rows"}),n.jsx("dd",{children:d.rowCount})]}),n.jsxs("div",{children:[n.jsx("dt",{children:"Distribution"}),n.jsxs("dd",{children:[d.distStyle||"auto"," ",d.distKey?`on ${d.distKey}`:""]})]}),n.jsxs("div",{children:[n.jsx("dt",{children:"Sort keys"}),n.jsx("dd",{children:d.sortKeys.length>0?d.sortKeys.join(", "):"none"})]})]})]}):null,d?n.jsxs("section",{children:[n.jsx("span",{className:"inspector-label",children:"Columns"}),r.length===0?n.jsx("p",{className:"inspector-muted",children:"No column metadata recorded."}):n.jsx("div",{className:"attribute-preview",children:r.map(p=>n.jsxs("span",{className:"attribute-chip",children:[p.name," ",p.dataType,p.encoding?` encode ${p.encoding}`:"",p.identity?" identity":""]},`${p.table}-${p.name}`))})]}):null,n.jsxs("section",{children:[n.jsx("span",{className:"inspector-label",children:"Recent statements"}),l.length===0?n.jsx("p",{className:"inspector-muted",children:"No Redshift statements recorded."}):n.jsx("div",{className:"dynamodb-item-table-wrap",children:n.jsxs("table",{className:"dynamodb-item-table",children:[n.jsx("thead",{children:n.jsxs("tr",{children:[n.jsx("th",{scope:"col",children:"ID"}),n.jsx("th",{scope:"col",children:"Status"}),n.jsx("th",{scope:"col",children:"Rows"}),n.jsx("th",{scope:"col",children:"SQL"})]})}),n.jsx("tbody",{children:l.slice(0,8).map(p=>n.jsxs("tr",{className:"item-row",children:[n.jsx("td",{children:n.jsx("code",{children:p.redshiftQueryId})}),n.jsx("td",{children:p.status}),n.jsx("td",{children:p.resultRows}),n.jsxs("td",{children:[p.queryPreview,p.queryRedacted||p.queryTruncated?n.jsx("span",{className:"attribute-chip",children:p.queryRedacted?"redacted":"truncated"}):null]})]},p.id))})]})})]})]})}function tx({onQuerySuccess:r}){const[i,l]=m.useState("select 1"),[u,d]=m.useState("100"),[p,x]=m.useState(),[g,S]=m.useState(),[v,N]=m.useState(),[w,L]=m.useState(!1),C=_=>{if(_.preventDefault(),i.trim()===""){x(void 0),N(void 0),S("SQL is required.");return}const I=Number.parseInt(u,10),E=Number.isFinite(I)?Math.max(1,I):100;d(String(E)),L(!0),S(void 0),N(void 0),Gh({sql:i,maxRows:E}).then(J=>{x(J.result),N(`${J.result.statement.status}: ${J.result.commandTag||"statement completed"}`),r()}).catch(()=>{x(void 0),N(void 0),S("Query failed. Check SQL syntax and local Redshift state.")}).finally(()=>L(!1))};return n.jsxs("section",{className:"redshift-query-runner","aria-label":"Redshift query runner",children:[n.jsxs("div",{className:"dynamodb-toolbar",children:[n.jsx("span",{className:"inspector-label",children:"Query runner"}),n.jsx("span",{className:"toolbar-count",children:p?`${p.rowCount} rows`:"Ready"})]}),n.jsxs("form",{className:"redshift-query-form",onSubmit:C,children:[n.jsxs("label",{className:"redshift-sql-editor",children:[n.jsx("span",{children:"SQL"}),n.jsx("textarea",{"aria-label":"Redshift SQL",onChange:_=>l(_.target.value),rows:5,spellCheck:!1,value:i})]}),n.jsxs("div",{className:"pubsub-action-row",children:[n.jsxs("label",{className:"compact-filter small",children:[n.jsx("span",{children:"Max rows"}),n.jsx("input",{min:1,onChange:_=>d(_.target.value),type:"number",value:u})]}),n.jsx(ne,{disabled:w,type:"submit",children:w?"Running":"Run query"})]})]}),g?n.jsx("p",{className:"operation-message error",children:g}):null,v?n.jsx("p",{className:"operation-message success",children:v}):null,p?n.jsx(nx,{queryResult:p}):null]})}function nx({queryResult:r}){return n.jsxs("div",{className:"redshift-query-result",children:[n.jsxs("div",{className:"attribute-preview","aria-label":"Redshift query summary",children:[n.jsx("span",{className:"attribute-chip",children:r.statement.status}),n.jsx("span",{className:"attribute-chip",children:r.commandTag}),n.jsxs("span",{className:"attribute-chip",children:[r.rowCount," rows"]})]}),r.columns.length===0?n.jsx("p",{className:"inspector-muted",children:"Statement completed without a result set."}):n.jsx("div",{className:"dynamodb-item-table-wrap",children:n.jsxs("table",{className:"dynamodb-item-table",children:[n.jsx("thead",{children:n.jsx("tr",{children:r.columns.map(i=>n.jsxs("th",{scope:"col",children:[i.name,n.jsx("span",{className:"query-column-type",children:i.typeName})]},i.name))})}),n.jsx("tbody",{children:r.rows.length===0?n.jsx("tr",{children:n.jsx("td",{colSpan:r.columns.length,children:"No rows returned."})}):r.rows.map((i,l)=>n.jsx("tr",{className:"item-row",children:r.columns.map((u,d)=>n.jsx("td",{children:String(i[d]??"")},`${l}-${u.name}`))},l))})]})})]})}function sx(r){return{service:"redshift",status:"disabled",running:!1,sqlEndpoint:"127.0.0.1:15439",apiEndpoint:(r==null?void 0:r.endpoint)??"http://127.0.0.1:19099",region:"us-east-1",clusterCount:0,storagePath:(r==null?void 0:r.storagePath)??".devcloud/data/redshift",backendKind:"postgres",backendMode:"managed"}}function Us(r){if(r)return`${r.schema}.${r.name}`}async function rx(){return q("/api/redis/status")}async function Eu(r=0,i="*",l=100){const u=new URLSearchParams({cursor:String(r),match:i,count:String(l)});return q(`/api/redis/keys?${u.toString()}`)}async function Tu(r){return q(`/api/redis/keys/${encodeURIComponent(r)}`)}async function ax(r){return q("/api/redis/command",{method:"POST",body:r})}async function ix(r){return q(`/api/redis/keys/${encodeURIComponent(r)}`,{method:"DELETE"})}async function lx(r,i){return q(`/api/redis/keys/${encodeURIComponent(r)}/expire`,{method:"POST",body:{ttlSeconds:i}})}async function ox(){return q("/api/redis/keys?confirm=FLUSHDB",{method:"DELETE"})}async function cx(r){return q("/api/redis/select-db",{method:"POST",body:{db:r}})}const Pu=/("(?:\\.|[^"\\])*")(\s*:)?|(\b(?:true|false|null)\b)|(-?\d+(?:\.\d+)?(?:[eE][+-]?\d+)?)/g;function Gu(r){const i=[];let l=0,u=0;Pu.lastIndex=0;let d;for(;(d=Pu.exec(r))!==null;){const[p,x,g,S,v]=d,N=d.index;if(N>l&&i.push(r.slice(l,N)),x!==void 0){const w=g!==void 0;i.push(n.jsx("span",{className:w?"json-token-key":"json-token-string",children:x},`t${u++}`)),g&&i.push(g)}else if(S!==void 0){const w=S==="null"?"json-token-null":"json-token-bool";i.push(n.jsx("span",{className:w,children:S},`t${u++}`))}else v!==void 0?i.push(n.jsx("span",{className:"json-token-number",children:v},`t${u++}`)):i.push(p);l=N+p.length}return l{const ee=I();if(_){u({status:"success",statusPayload:Ix(r),keys:[],nextCursor:0}),p(void 0),g(void 0);return}u({status:"loading"}),Promise.all([rx(),Eu(0,N)]).then(([pe,Se])=>{ee()&&(u({status:"success",statusPayload:pe,keys:Se.keys,nextCursor:Se.nextCursor}),p(he=>{var z;return he&&Se.keys.some(H=>H.key===he)?he:(z=Se.keys[0])==null?void 0:z.key}))}).catch(pe=>{ee()&&u({status:"error",message:pe.message})})},[I,_,N,r]),J=m.useCallback(()=>{l.status!=="success"||l.nextCursor===0||_||Eu(l.nextCursor,N).then(ee=>{u(pe=>pe.status!=="success"?pe:{...pe,keys:[...pe.keys,...ee.keys],nextCursor:ee.nextCursor})}).catch(ee=>C({tone:"error",text:ee.message}))},[_,N,l]);m.useEffect(()=>{E()},[E]),Vt({topics:["redis"],onEvent:E,onResync:E,enabled:!_}),m.useEffect(()=>{if(!d||_){g(void 0),v(void 0);return}let ee=!1;return v(void 0),Tu(d).then(pe=>{ee||g(pe)}).catch(pe=>{ee||(g(void 0),v(pe.message))}),()=>{ee=!0}},[d,_]);const de=l.status==="success"?l.keys:[],V=l.status==="success"?l.nextCursor:0,$=l.status==="success"?l.statusPayload:void 0;async function oe(ee){await i(Rn({title:"Delete Redis key",description:"This key will be permanently removed from the selected Redis database.",target:ee}))&&ix(ee).then(Se=>{C({tone:"success",text:`Deleted ${ee} (${Se.deleted} key)`}),p(void 0),g(void 0),E()}).catch(Se=>C({tone:"error",text:Se.message}))}function me(ee,pe){lx(ee,pe).then(Se=>{C({tone:Se.updated?"success":"warning",text:Se.updated?`Set TTL on ${ee} to ${pe}s`:`TTL not updated on ${ee} (key may be missing)`}),Tu(ee).then(g).catch(()=>{}),E()}).catch(Se=>C({tone:"error",text:Se.message}))}function Z(ee){cx(ee).then(()=>{p(void 0),g(void 0),v(void 0),C({tone:"success",text:`Switched to db${ee}`}),E()}).catch(pe=>C({tone:"error",text:pe.message}))}async function Re(){await i(Rn({title:"Flush Redis DB",description:"This removes every key from the selected Redis database. FLUSHALL is never used.",target:"FLUSHDB",confirmLabel:"Flush DB"}))&&ox().then(pe=>{C({tone:"success",text:`FLUSHDB ${pe.result}`}),p(void 0),g(void 0),E()}).catch(pe=>C({tone:"error",text:pe.message}))}return _?n.jsx(ue,{title:"Redis",children:n.jsx(A,{title:"Redis is disabled",description:"Enable services.redis.enabled in .devcloud/config.yaml and restart devcloud."})}):n.jsxs("div",{className:"redis-shell",children:[n.jsx(mx,{status:$,loading:l.status==="loading",error:l.status==="error"?l.message:void 0,onSelectDB:Z}),n.jsxs("div",{className:"redis-workspace",children:[n.jsxs(ue,{title:"Keys",children:[n.jsxs("div",{className:"redis-keys-toolbar",children:[n.jsx(px,{loading:l.status==="loading",keys:de.length,total:$==null?void 0:$.currentDBKeys}),n.jsxs("div",{className:"redis-toolbar-actions",children:[n.jsx(ne,{onClick:E,children:"Refresh"}),n.jsx(ne,{disabled:l.status!=="success"||V===0,onClick:J,children:"Load more"}),n.jsx(ne,{className:"danger",disabled:de.length===0,onClick:Re,children:"Flush DB"})]})]}),n.jsxs("label",{className:"redis-match-field",children:[n.jsx("span",{children:"Match pattern"}),n.jsx("input",{"aria-label":"Match Redis keys",onChange:ee=>w(ee.target.value),placeholder:"* | prefix:* | exact-key",spellCheck:!1,type:"search",value:N})]}),L?n.jsx(fx,{message:L,onDismiss:()=>C(void 0)}):null,l.status==="loading"?n.jsx(A,{title:"Loading Redis",description:"Reading key metadata via SCAN."}):null,l.status==="error"?n.jsx(A,{title:"Redis unavailable",description:l.message,actionLabel:"Retry",onAction:E}):null,l.status==="success"?n.jsx(hx,{activeKey:d,keys:de,onSelectKey:p}):null]}),n.jsx(ue,{title:"Inspector",children:n.jsx(xx,{detail:x,error:S,onDeleteKey:oe,onApplyTTL:me})}),n.jsx(ue,{title:"Command Runner",children:n.jsx(Dx,{})})]})]})}function fx({message:r,onDismiss:i}){return n.jsxs("div",{className:`redis-message redis-message-${r.tone}`,role:"status",children:[n.jsx("span",{children:r.text}),n.jsx("button",{"aria-label":"Dismiss",className:"redis-message-close",onClick:i,type:"button",children:"×"})]})}function px({keys:r,total:i,loading:l}){if(l)return n.jsx("span",{className:"redis-keys-count",children:"Loading…"});const u=i!==void 0&&i>=0?` / ${i}`:"";return n.jsxs("span",{className:"redis-keys-count",children:[n.jsx("strong",{children:r}),n.jsxs("span",{className:"redis-keys-count-total",children:[u," key",r===1?"":"s"]})]})}function mx({status:r,loading:i,error:l,onSelectDB:u}){if(i&&!r)return n.jsx("header",{className:"redis-status-bar redis-status-bar-loading",children:n.jsx("span",{children:"Connecting to Redis…"})});if(l&&!r)return n.jsxs("header",{className:"redis-status-bar redis-status-bar-error",role:"alert",children:[n.jsxs("span",{className:"redis-status-cell",children:[n.jsx("span",{className:"cell-label",children:"State"}),n.jsx("span",{className:"cell-value",children:"unreachable"})]}),n.jsxs("span",{className:"redis-status-cell redis-status-cell-wide",children:[n.jsx("span",{className:"cell-label",children:"Reason"}),n.jsx("span",{className:"cell-value",children:l})]})]});if(!r)return n.jsx("header",{className:"redis-status-bar redis-status-bar-loading",children:n.jsx("span",{children:"—"})});const d=r.databaseCount>0?r.databaseCount:1;return n.jsxs("header",{className:"redis-status-bar",children:[n.jsxs("span",{className:`redis-status-cell redis-mode-pill redis-mode-${r.mode||"unknown"}`,children:[n.jsx("span",{className:"cell-label",children:"Mode"}),n.jsx("span",{className:"cell-value",children:r.mode||"unknown"})]}),n.jsxs("span",{className:"redis-status-cell",children:[n.jsx("span",{className:"cell-label",children:"Address"}),n.jsx("code",{children:r.address||"—"})]}),n.jsxs("span",{className:"redis-status-cell",children:[n.jsx("span",{className:"cell-label",children:"Server"}),n.jsx("span",{className:"cell-value",children:r.serverVersion||"—"})]}),n.jsxs("span",{className:"redis-status-cell",children:[n.jsx("span",{className:"cell-label",children:"Clients"}),n.jsx("span",{className:"cell-value",children:r.connectedClients})]}),n.jsxs("span",{className:"redis-status-cell",children:[n.jsx("span",{className:"cell-label",children:"Memory"}),n.jsx("span",{className:"cell-value",children:r.usedMemoryHuman||"—"})]}),n.jsxs("span",{className:"redis-status-cell",children:[n.jsx("span",{className:"cell-label",children:"Database"}),n.jsx("select",{"aria-label":"Select Redis database",className:"redis-db-select",disabled:d<=1,onChange:p=>u(Number(p.target.value)),value:r.currentDB,children:Array.from({length:d},(p,x)=>n.jsx("option",{value:x,children:`db${x}`},x))})]}),n.jsxs("span",{className:"redis-status-cell",children:[n.jsx("span",{className:"cell-label",children:`Keys (db${r.currentDB})`}),n.jsx("span",{className:"cell-value",children:r.currentDBKeys})]})]})}function hx({activeKey:r,keys:i,onSelectKey:l}){return i.length===0?n.jsx(A,{title:"No keys",description:"Send SET / HSET / LPUSH from a Redis client and refresh."}):n.jsx("ul",{className:"redis-key-list","aria-label":"Redis keys",children:i.map(u=>{const d=u.key===r;return n.jsx("li",{children:n.jsxs("button",{"aria-current":d?"true":void 0,className:d?"redis-key-row active":"redis-key-row",onClick:()=>l(u.key),title:u.key,type:"button",children:[n.jsxs("span",{className:"redis-key-row-top",children:[n.jsx(Xu,{type:u.type}),n.jsx(Zu,{ttlSeconds:u.ttlSeconds,compact:!0})]}),n.jsx("span",{className:"redis-key-row-name",children:u.key})]})},u.key)})})}function xx({detail:r,error:i,onApplyTTL:l,onDeleteKey:u}){return i?n.jsx(A,{title:"Key unavailable",description:i}):r?n.jsxs("div",{className:"redis-inspector",children:[n.jsx(gx,{detail:r}),n.jsx(yx,{detail:r}),n.jsx(Cx,{detail:r,onApplyTTL:l,onDeleteKey:u})]}):n.jsx(A,{title:"Select a key",description:"Choose a key on the left to inspect its type, TTL, and value."})}function gx({detail:r}){const[i,l]=m.useState(!1);function u(){navigator.clipboard&&navigator.clipboard.writeText(r.key).then(()=>{l(!0),window.setTimeout(()=>l(!1),1500)}).catch(()=>{})}return n.jsxs("header",{className:"redis-inspector-header",children:[n.jsx("span",{className:"redis-inspector-eyebrow",children:"Key"}),n.jsxs("div",{className:"redis-key-title-row",children:[n.jsx("code",{className:"redis-key-title",children:r.key}),n.jsx("button",{className:"redis-copy-button",onClick:u,title:"Copy key",type:"button",children:i?"copied":"copy"})]}),n.jsxs("div",{className:"redis-key-badges",children:[n.jsx(Xu,{type:r.type}),n.jsx(Zu,{ttlSeconds:r.ttlSeconds}),n.jsx(Rx,{detail:r})]})]})}function yx({detail:r}){return!r.preview||r.preview.length===0?n.jsxs("section",{className:"redis-inspector-section",children:[n.jsx("span",{className:"redis-inspector-eyebrow",children:"Value"}),n.jsx(A,{title:"Empty value",description:"The key exists but has no previewable content in this slice."})]}):n.jsxs("section",{className:"redis-inspector-section",children:[n.jsx("header",{className:"redis-section-head",children:n.jsx("span",{className:"redis-inspector-eyebrow",children:"Value"})}),jx(r)]})}function jx(r){switch(r.type){case"string":return n.jsx(vx,{raw:r.preview[0]??""});case"list":return n.jsx(bx,{items:r.preview});case"hash":return n.jsx(Sx,{entries:r.preview});case"set":return n.jsx(wx,{members:r.preview});case"zset":return n.jsx(kx,{items:r.preview});default:return n.jsx("pre",{className:"redis-value-pre",children:r.preview.join(` +`)})}}function vx({raw:r}){const i=m.useMemo(()=>Yu(r),[r]),[l,u]=m.useState(i?"pretty":"raw"),d=l==="pretty"&&i!==void 0;return n.jsxs("div",{className:"redis-value-string",children:[n.jsxs("div",{className:"redis-value-toolbar",children:[n.jsxs("span",{className:"redis-value-meta",children:[r.length.toLocaleString()," char",r.length===1?"":"s"]}),i?n.jsxs("div",{role:"tablist","aria-label":"Value format",className:"redis-segmented",children:[n.jsx("button",{type:"button",role:"tab","aria-selected":l==="pretty",className:l==="pretty"?"active":void 0,onClick:()=>u("pretty"),children:"Pretty"}),n.jsx("button",{type:"button",role:"tab","aria-selected":l==="raw",className:l==="raw"?"active":void 0,onClick:()=>u("raw"),children:"Raw"})]}):null]}),n.jsx("pre",{className:"redis-value-pre json-code",children:d?Gu(i):r})]})}function bx({items:r}){return n.jsx("ol",{className:"redis-list-view",children:r.map((i,l)=>n.jsxs("li",{children:[n.jsx("span",{className:"redis-list-index",children:l}),n.jsx("code",{className:"redis-list-value",children:i})]},l))})}function Sx({entries:r}){return n.jsxs("table",{className:"redis-hash-table","aria-label":"Hash fields",children:[n.jsx("thead",{children:n.jsxs("tr",{children:[n.jsx("th",{scope:"col",children:"Field"}),n.jsx("th",{scope:"col",children:"Value"})]})}),n.jsx("tbody",{children:r.map((i,l)=>{const[u,d]=ed(i,": ");return n.jsxs("tr",{children:[n.jsx("th",{scope:"row",children:n.jsx("code",{children:u})}),n.jsx("td",{children:n.jsx(Nx,{raw:d})})]},l)})})]})}function Nx({raw:r}){const i=m.useMemo(()=>Yu(r),[r]);return i===void 0?n.jsx("code",{children:r}):n.jsx("pre",{className:"redis-value-pre json-code redis-value-pre-inline",children:Gu(i)})}function wx({members:r}){return n.jsx("ul",{className:"redis-set-view","aria-label":"Set members",children:r.map((i,l)=>n.jsx("li",{children:n.jsx("code",{children:i})},l))})}function kx({items:r}){return n.jsxs("table",{className:"redis-zset-table","aria-label":"Sorted set members",children:[n.jsx("thead",{children:n.jsxs("tr",{children:[n.jsx("th",{scope:"col",children:"Member"}),n.jsx("th",{scope:"col",children:"Score"})]})}),n.jsx("tbody",{children:r.map((i,l)=>{const[u,d]=ed(i,": ");return n.jsxs("tr",{children:[n.jsx("th",{scope:"row",children:n.jsx("code",{children:u})}),n.jsx("td",{className:"redis-zset-score",children:d})]},l)})})]})}function Cx({detail:r,onApplyTTL:i,onDeleteKey:l}){const[u,d]=m.useState("60"),p=Number(u),x=Number.isInteger(p)&&p>0;function g(v){v.preventDefault(),x&&i(r.key,p)}function S(){i(r.key,-1)}return n.jsxs("section",{className:"redis-inspector-section",children:[n.jsx("header",{className:"redis-section-head",children:n.jsx("span",{className:"redis-inspector-eyebrow",children:"Actions"})}),n.jsxs("form",{className:"redis-ttl-form",onSubmit:g,children:[n.jsxs("label",{children:[n.jsx("span",{children:"TTL seconds"}),n.jsx("input",{"aria-label":"TTL seconds",min:1,onChange:v=>d(v.target.value),type:"number",value:u})]}),n.jsx(ne,{disabled:!x,type:"submit",children:"Set TTL"}),n.jsx(ne,{className:"ghost",onClick:S,type:"button",children:"Persist (no TTL)"})]}),n.jsx("div",{className:"redis-danger-row",children:n.jsx(ne,{className:"danger",onClick:()=>l(r.key),type:"button",children:"Delete key"})})]})}function Xu({type:r}){const i=(r||"unknown").toLowerCase();return n.jsx("span",{className:`redis-badge redis-type-${i}`,children:i})}function Zu({ttlSeconds:r,compact:i}){return r===-2?n.jsx("span",{className:"redis-badge redis-ttl-missing",children:i?"✕":"missing"}):r===-1?n.jsx("span",{className:"redis-badge redis-ttl-persistent",children:i?"∞":"persistent"}):n.jsx("span",{className:"redis-badge redis-ttl-active",title:`${r}s`,children:i?`${$u(r)}`:`TTL ${$u(r)}`})}function Rx({detail:r}){if(!r.preview||r.preview.length===0)return null;const i=r.preview.length;let l;switch(r.type){case"string":l=`${(r.preview[0]??"").length} char${(r.preview[0]??"").length===1?"":"s"}`;break;case"list":l=`${i} item${i===1?"":"s"}`;break;case"hash":l=`${i} field${i===1?"":"s"}`;break;case"set":l=`${i} member${i===1?"":"s"}`;break;case"zset":l=`${i} entr${i===1?"y":"ies"}`;break;default:l=`${i}`}return n.jsx("span",{className:"redis-badge redis-size",children:l})}function Dx(){const[r,i]=m.useState("GET your-key"),[l,u]=m.useState([]),d=m.useRef(0),p=m.useMemo(()=>Lu(r),[r]);function x(w){d.current+=1;const L=d.current;u(C=>[{id:L,...w},...C].slice(0,ux))}function g(w,L){if(!w.command){x({input:L,outcome:"error",detail:"No command supplied"});return}ax(w).then(C=>{x({input:L,outcome:"success",detail:`${C.command} (${C.class})`,rows:C.rows??[]})}).catch(C=>{x({input:L,outcome:"error",detail:C.message})})}function S(w){w.preventDefault(),g(p,r)}function v(w){(w.metaKey||w.ctrlKey)&&w.key==="Enter"&&(w.preventDefault(),g(p,r))}function N(w){i(w),g(Lu(w),w)}return n.jsxs("div",{className:"redis-runner",children:[n.jsxs("form",{className:"redis-runner-form",onSubmit:S,children:[n.jsxs("label",{className:"redis-runner-label",children:[n.jsx("span",{children:"Command"}),n.jsx("textarea",{"aria-label":"Redis command",onChange:w=>i(w.target.value),onKeyDown:v,rows:3,spellCheck:!1,value:r})]}),n.jsxs("div",{className:"redis-runner-toolbar",children:[n.jsxs("span",{className:"redis-runner-hint",children:["Allowlisted commands only · ",n.jsx("kbd",{children:"⌘"}),"/",n.jsx("kbd",{children:"Ctrl"}),"+",n.jsx("kbd",{children:"Enter"})," to run"]}),n.jsx(ne,{type:"submit",children:"Run"})]})]}),l.length===0?n.jsx(A,{title:"No commands yet",description:"Try GET, HGETALL, LRANGE, SCAN 0 MATCH * COUNT 50, or TYPE."}):null,l.length>0?n.jsx("ol",{className:"redis-runner-history","aria-label":"Command history",children:l.map(w=>n.jsxs("li",{className:`redis-runner-entry redis-runner-${w.outcome}`,children:[n.jsxs("header",{className:"redis-runner-entry-head",children:[n.jsx("code",{className:"redis-runner-input",children:w.input}),n.jsx("button",{className:"redis-runner-rerun",onClick:()=>N(w.input),title:"Run again",type:"button",children:"rerun"})]}),n.jsxs("p",{className:"redis-runner-detail",children:[n.jsx("span",{className:"redis-runner-tag",children:w.outcome==="success"?"OK":"ERR"}),n.jsx("span",{children:w.detail})]}),w.rows&&w.rows.length>0?n.jsx("pre",{className:"redis-runner-result",children:w.rows.join(` +`)}):null]},w.id))}):null]})}function Ix(r){var i;return{service:"redis",status:"disabled",running:!1,mode:"managed",address:((i=r==null?void 0:r.endpoint)==null?void 0:i.replace(/^redis:\/\//,""))??"127.0.0.1:16379",serverVersion:"",connectedClients:0,usedMemoryHuman:"",currentDB:0,databaseCount:0,currentDBKeys:0,storagePath:(r==null?void 0:r.storagePath)??".devcloud/data/redis"}}function Lu(r){const i=r.trim().split(/\s+/).filter(d=>d.length>0),[l="",...u]=i;return{command:l,args:u}}function ed(r,i){const l=r.indexOf(i);return l===-1?[r,""]:[r.slice(0,l),r.slice(l+i.length)]}function $u(r){return r<60?`${r}s`:r<3600?`${Math.floor(r/60)}m`:r<86400?`${Math.floor(r/3600)}h`:`${Math.floor(r/86400)}d`}async function Ex(){return q("/api/sqs/status")}async function Tx(){return q("/api/sqs/queues")}async function Px(r){return q("/api/sqs/queues",{method:"POST",body:{input:r}})}async function Lx(r){return q(`/api/sqs/queues/${encodeURIComponent(r)}/messages`)}async function $x(r,i){return q(`/api/sqs/queues/${encodeURIComponent(r)}/messages`,{method:"POST",body:{input:i}})}async function Mx(r,i){return q(`/api/sqs/queues/${encodeURIComponent(r)}/receive`,{method:"POST",body:{input:i}})}async function Ox(r,i){await q(`/api/sqs/queues/${encodeURIComponent(r)}/delete`,{method:"POST",body:{input:i}})}async function Ax(r,i){await q(`/api/sqs/queues/${encodeURIComponent(r)}/visibility`,{method:"POST",body:{input:i}})}async function _x(r){return q(`/api/sqs/queues/${encodeURIComponent(r)}/leases`)}async function Bx(r){return q(`/api/sqs/queues/${encodeURIComponent(r)}/dlq`)}async function Fx(r){return mn(`/api/sqs/queues/${encodeURIComponent(r)}/purge`,{method:"POST"})}function zx({busyAction:r,newQueueName:i,newQueueKind:l,newQueueVisibility:u,newQueueDelay:d,onSubmit:p,setNewQueueName:x,setNewQueueKind:g,setNewQueueVisibility:S,setNewQueueDelay:v}){return n.jsxs("form",{className:"pubsub-action-form stacked",onSubmit:p,children:[n.jsxs("label",{className:"compact-filter",children:[n.jsx("span",{children:"Queue name"}),n.jsx("input",{"aria-label":"New SQS queue name",onChange:N=>x(N.target.value),placeholder:l==="fifo"?"jobs.fifo":"jobs",value:i})]}),n.jsxs("label",{className:"compact-filter small",children:[n.jsx("span",{children:"Type"}),n.jsxs("select",{"aria-label":"New SQS queue type",onChange:N=>g(N.target.value==="fifo"?"fifo":"standard"),value:l,children:[n.jsx("option",{value:"standard",children:"Standard"}),n.jsx("option",{value:"fifo",children:"FIFO"})]})]}),n.jsxs("label",{className:"compact-filter small",children:[n.jsx("span",{children:"Visibility"}),n.jsx("input",{"aria-label":"New SQS queue visibility timeout seconds",inputMode:"numeric",onChange:N=>S(N.target.value),value:u})]}),n.jsxs("label",{className:"compact-filter small",children:[n.jsx("span",{children:"Delay"}),n.jsx("input",{"aria-label":"New SQS queue delay seconds",inputMode:"numeric",onChange:N=>v(N.target.value),value:d})]}),n.jsx(ne,{disabled:r==="create-queue",type:"submit",children:"Create"})]})}function Qx(r){return{service:"sqs",status:"disabled",running:!1,endpoint:(r==null?void 0:r.endpoint)??"http://127.0.0.1:19324",region:"us-east-1",authMode:"relaxed",storagePath:(r==null?void 0:r.storagePath)??".devcloud/data/sqs",queueCount:0}}function Mu(r){const i=Number.parseInt(r,10);if(!(!Number.isFinite(i)||i<0))return String(i)}function Ou(r){const i=Number.parseInt(r,10);if(!(!Number.isFinite(i)||i<=0))return i}function Ll(r){const i=Number.parseInt(r,10);if(!(!Number.isFinite(i)||i<0))return i}function Ux(r){return Object.fromEntries(Object.entries(r).filter(i=>typeof i[1]=="string"))}function qx(r){const i=r.trim();if(i==="")return;let l;try{l=JSON.parse(i)}catch{return new Error("Message attributes JSON is invalid")}if(!Qa(l))return new Error("Message attributes must be a JSON object");for(const[u,d]of Object.entries(l))if(!Qa(d)||typeof d.DataType!="string"||d.DataType.trim()==="")return new Error(`Message attribute ${u} must include DataType`);return l}function Au(r){const i=r.split(",").map(l=>l.trim()).filter(Boolean);return i.length>0?i:void 0}function Vx(r){if(r)try{const i=JSON.parse(r);if(!Qa(i))return;const l=i.deadLetterTargetArn,u=i.maxReceiveCount;return typeof l!="string"?void 0:{deadLetterTargetArn:l,maxReceiveCount:typeof u=="string"?u:String(u??"")}}catch{return}}function Kx(r){if(r)try{const i=JSON.parse(r);return!Qa(i)||typeof i.redrivePermission!="string"?void 0:{redrivePermission:i.redrivePermission}}catch{return}}function td(r){return r.length<=12?r===""?"":"...":`${r.slice(0,6)}...${r.slice(-6)}`}function Qa(r){return typeof r=="object"&&r!==null&&!Array.isArray(r)}function Jx({activeQueue:r,busyAction:i,purgeConfirmation:l,onSubmit:u,setPurgeConfirmation:d}){return n.jsxs("form",{className:"pubsub-action-form stacked",onSubmit:u,children:[n.jsx("span",{className:"toolbar-count",children:r?r.name:"No queue selected"}),n.jsxs("label",{className:"compact-filter",children:[n.jsx("span",{children:"Purge confirmation"}),n.jsx("input",{"aria-label":"Confirm SQS purge queue",disabled:!r,onChange:p=>d(p.target.value),placeholder:(r==null?void 0:r.name)??"queue name",value:l})]}),n.jsx(ne,{className:"danger",disabled:!r||l!==r.name||i==="purge-queue",type:"submit",children:"Purge"})]})}function Hx({activeQueue:r,busyAction:i,selectedReceiptHandle:l,visibilityTimeout:u,visibilityConfirmation:d,onSubmit:p,setVisibilityTimeout:x,setVisibilityConfirmation:g}){const S=!r||l==="";return n.jsxs("form",{className:"pubsub-action-form stacked",onSubmit:p,children:[n.jsxs("label",{className:"compact-filter small",children:[n.jsx("span",{children:"Visibility timeout"}),n.jsx("input",{"aria-label":"SQS change visibility timeout seconds",disabled:S,inputMode:"numeric",onChange:v=>x(v.target.value),value:u})]}),n.jsxs("label",{className:"compact-filter small",children:[n.jsx("span",{children:"Confirm"}),n.jsx("input",{"aria-label":"Confirm SQS change message visibility",disabled:S,onChange:v=>g(v.target.value),placeholder:"visibility",value:d})]}),n.jsx(ne,{disabled:S||d!=="visibility"||i==="change-visibility",type:"submit",children:"Change visibility"})]})}function Wx({activeQueue:r,busyAction:i,selectedReceivedMessage:l,selectedReceiptHandle:u,pastedReceiptHandle:d,deleteConfirmation:p,onSubmit:x,setPastedReceiptHandle:g,setDeleteConfirmation:S}){return n.jsxs("form",{className:"pubsub-action-form stacked",onSubmit:x,children:[n.jsxs("label",{className:"compact-filter wide",children:[n.jsx("span",{children:"Receipt handle"}),n.jsx("input",{"aria-label":"SQS delete receipt handle",disabled:!r,onChange:v=>g(v.target.value),placeholder:l?td(l.ReceiptHandle):"paste receipt handle or select received message",value:d})]}),n.jsxs("label",{className:"compact-filter small",children:[n.jsx("span",{children:"Confirm"}),n.jsx("input",{"aria-label":"Confirm SQS delete message",disabled:!r||u==="",onChange:v=>S(v.target.value),placeholder:"delete",value:p})]}),n.jsx(ne,{className:"danger",disabled:!r||u===""||p!=="delete"||i==="delete-message",type:"submit",children:"Delete"})]})}function Gx({activeIndex:r,detailState:i,messages:l,onSelectIndex:u,queueName:d}){return d?i.status==="loading"?n.jsx(A,{title:"Loading messages",description:`Reading messages from ${d}.`}):i.status==="error"?n.jsx(A,{title:"SQS messages unavailable",description:i.message}):l.length===0?n.jsx(A,{title:"No messages",description:`No retained messages in ${d} match the current filter.`}):n.jsx("div",{className:"dynamodb-item-table-wrap",children:n.jsxs("table",{className:"dynamodb-item-table",children:[n.jsx("thead",{children:n.jsxs("tr",{children:[n.jsx("th",{scope:"col",children:"State"}),n.jsx("th",{scope:"col",children:"Body"}),n.jsx("th",{scope:"col",children:"Receive"})]})}),n.jsx("tbody",{children:l.map((p,x)=>n.jsxs("tr",{className:x===r?"item-row active":"item-row",onClick:()=>u(x),children:[n.jsx("td",{children:p.state}),n.jsx("td",{children:n.jsx(Yx,{message:p})}),n.jsx("td",{children:p.receiveCount})]},`${p.messageId}-${x}`))})]})}):n.jsx(A,{title:"No queue selected",description:"Choose a queue to inspect retained messages."})}function Yx({message:r}){return n.jsxs("span",{className:"attribute-preview",children:[n.jsx("span",{className:"attribute-chip",children:r.body||"(empty body)"}),r.messageGroupId?n.jsxs("span",{className:"attribute-chip",children:["group: ",r.messageGroupId]}):null,r.sequenceNumber?n.jsxs("span",{className:"attribute-chip",children:["seq: ",r.sequenceNumber]}):null]})}function Xx({activeQueue:r,busyAction:i,sendBody:l,sendDelay:u,sendAttributesJSON:d,sendGroupId:p,sendDeduplicationId:x,onSubmit:g,setSendBody:S,setSendDelay:v,setSendAttributesJSON:N,setSendGroupId:w,setSendDeduplicationId:L}){return n.jsxs("form",{className:"pubsub-action-form stacked",onSubmit:g,children:[n.jsxs("label",{className:"compact-filter wide",children:[n.jsx("span",{children:"Message body"}),n.jsx("textarea",{"aria-label":"SQS message body",disabled:!r,onChange:C=>S(C.target.value),placeholder:"message body",rows:3,value:l})]}),n.jsxs("label",{className:"compact-filter small",children:[n.jsx("span",{children:"Delay"}),n.jsx("input",{"aria-label":"SQS message delay seconds",disabled:!r,inputMode:"numeric",onChange:C=>v(C.target.value),value:u})]}),n.jsxs("label",{className:"compact-filter",children:[n.jsx("span",{children:"Attributes JSON"}),n.jsx("input",{"aria-label":"SQS message attributes JSON",disabled:!r,onChange:C=>N(C.target.value),placeholder:'{"kind":{"DataType":"String","StringValue":"test"}}',value:d})]}),n.jsxs("label",{className:"compact-filter small",children:[n.jsx("span",{children:"Group"}),n.jsx("input",{"aria-label":"SQS FIFO message group ID",disabled:!r,onChange:C=>w(C.target.value),placeholder:"FIFO only",value:p})]}),n.jsxs("label",{className:"compact-filter small",children:[n.jsx("span",{children:"Dedup"}),n.jsx("input",{"aria-label":"SQS FIFO message deduplication ID",disabled:!r,onChange:C=>L(C.target.value),placeholder:"optional",value:x})]}),n.jsx(ne,{disabled:!r||i==="send-message",type:"submit",children:"Send"})]})}function Zx({activeQueue:r,busyAction:i,receiveMaxMessages:l,receiveVisibilityTimeout:u,receiveWaitTime:d,receiveAttributeNames:p,receiveMessageAttributeNames:x,onSubmit:g,setReceiveMaxMessages:S,setReceiveVisibilityTimeout:v,setReceiveWaitTime:N,setReceiveAttributeNames:w,setReceiveMessageAttributeNames:L}){return n.jsxs("form",{className:"pubsub-action-form stacked",onSubmit:g,children:[n.jsxs("label",{className:"compact-filter small",children:[n.jsx("span",{children:"Max"}),n.jsx("input",{"aria-label":"SQS receive max messages",disabled:!r,inputMode:"numeric",onChange:C=>S(C.target.value),value:l})]}),n.jsxs("label",{className:"compact-filter small",children:[n.jsx("span",{children:"Visibility"}),n.jsx("input",{"aria-label":"SQS receive visibility timeout seconds",disabled:!r,inputMode:"numeric",onChange:C=>v(C.target.value),value:u})]}),n.jsxs("label",{className:"compact-filter small",children:[n.jsx("span",{children:"Wait"}),n.jsx("input",{"aria-label":"SQS receive wait time seconds",disabled:!r,inputMode:"numeric",onChange:C=>N(C.target.value),value:d})]}),n.jsxs("label",{className:"compact-filter",children:[n.jsx("span",{children:"Attrs"}),n.jsx("input",{"aria-label":"SQS receive attribute names",disabled:!r,onChange:C=>w(C.target.value),placeholder:"All or comma-separated names",value:p})]}),n.jsxs("label",{className:"compact-filter",children:[n.jsx("span",{children:"Msg attrs"}),n.jsx("input",{"aria-label":"SQS receive message attribute names",disabled:!r,onChange:C=>L(C.target.value),placeholder:"All or comma-separated names",value:x})]}),n.jsx(ne,{disabled:!r||i==="receive-message",type:"submit",children:"Receive"})]})}function eg({activeQueueName:r,onSelectQueue:i,queues:l}){return l.length===0?n.jsx(A,{title:"No queues",description:"Queues created through the SQS API will appear here."}):n.jsx("div",{className:"dynamodb-table-list","aria-label":"SQS queues",children:l.map(u=>n.jsxs("button",{className:u.name===r?"dynamodb-table-row active":"dynamodb-table-row",onClick:()=>i(u.name),children:[n.jsxs("span",{className:"table-row-top",children:[n.jsx("span",{className:"table-row-name",children:u.name}),n.jsx("span",{className:"count-pill",children:u.totalRetainedMessages})]}),n.jsx("span",{className:"table-row-meta",children:u.url}),n.jsxs("span",{className:"table-row-tags",children:[n.jsxs("span",{children:[u.visibleMessages," visible"]}),n.jsxs("span",{children:[u.notVisibleMessages," in flight"]}),n.jsxs("span",{children:[u.delayedMessages," delayed"]})]})]},u.name))})}function tg({messages:r,onSelectIndex:i,selectedIndex:l}){return r.length===0?null:n.jsx("div",{className:"dynamodb-item-table-wrap","aria-label":"Received SQS messages",children:n.jsxs("table",{className:"dynamodb-item-table",children:[n.jsx("thead",{children:n.jsxs("tr",{children:[n.jsx("th",{scope:"col",children:"Use"}),n.jsx("th",{scope:"col",children:"Message"}),n.jsx("th",{scope:"col",children:"Receipt handle"})]})}),n.jsx("tbody",{children:r.map((u,d)=>n.jsxs("tr",{className:d===l?"item-row active":"item-row",onClick:()=>i(d),children:[n.jsx("td",{children:d===l?"selected":"select"}),n.jsx("td",{children:n.jsxs("span",{className:"attribute-preview",children:[n.jsx("span",{className:"attribute-chip",children:u.Body||"(empty body)"}),n.jsx("span",{className:"attribute-chip",children:u.MessageId})]})}),n.jsx("td",{children:n.jsx("code",{children:td(u.ReceiptHandle)})})]},`${u.MessageId}-${d}`))})]})})}function ng({detailState:r,message:i,queue:l,status:u}){var S;if(!l)return n.jsx(A,{title:"Inspector",description:"Queue attributes and selected message JSON will appear here."});const d=r.status==="success"?r.leases:[],p=r.status==="success"?r.dlq:void 0,x=Vx(l.attributes.RedrivePolicy),g=Kx(l.attributes.RedriveAllowPolicy);return n.jsxs("div",{className:"dynamodb-inspector",children:[n.jsxs("section",{children:[n.jsx("span",{className:"inspector-label",children:"Queue"}),n.jsx("h3",{children:l.name}),n.jsxs("dl",{className:"inspector-list",children:[n.jsxs("div",{children:[n.jsx("dt",{children:"Endpoint"}),n.jsx("dd",{children:n.jsx("code",{children:(u==null?void 0:u.endpoint)??"unknown"})})]}),n.jsxs("div",{children:[n.jsx("dt",{children:"Region"}),n.jsx("dd",{children:(u==null?void 0:u.region)??"unknown"})]}),n.jsxs("div",{children:[n.jsx("dt",{children:"ARN"}),n.jsx("dd",{children:n.jsx("code",{children:l.arn})})]}),n.jsxs("div",{children:[n.jsx("dt",{children:"Visibility"}),n.jsxs("dd",{children:[l.attributes.VisibilityTimeout??"unknown","s"]})]}),n.jsxs("div",{children:[n.jsx("dt",{children:"Leases"}),n.jsx("dd",{children:d.length})]}),n.jsxs("div",{children:[n.jsx("dt",{children:"DLQ sources"}),n.jsx("dd",{children:(p==null?void 0:p.deadLetterSourceQueues.map(v=>v.name).join(", "))||"none"})]}),n.jsxs("div",{children:[n.jsx("dt",{children:"DLQ target"}),n.jsx("dd",{children:((S=p==null?void 0:p.deadLetterQueue)==null?void 0:S.name)??(x==null?void 0:x.deadLetterTargetArn)??"none"})]}),n.jsxs("div",{children:[n.jsx("dt",{children:"Redrive"}),n.jsxs("dd",{children:[x?`maxReceiveCount ${x.maxReceiveCount}`:"none",g?`, allow ${g.redrivePermission}`:""]})]})]})]}),n.jsxs("section",{children:[n.jsx("span",{className:"inspector-label",children:"Selected message"}),i?n.jsx("pre",{className:"mail-preview",children:JSON.stringify(i,null,2)}):n.jsx("p",{className:"inspector-muted",children:"Select a message row to inspect JSON."})]})]})}function sg({service:r}){const[i,l]=m.useState({status:"loading"}),[u,d]=m.useState({status:"idle"}),[p,x]=m.useState(),[g,S]=m.useState(0),[v,N]=m.useState(""),[w,L]=m.useState(""),[C,_]=m.useState(""),[I,E]=m.useState("standard"),[J,de]=m.useState("30"),[V,$]=m.useState("0"),[oe,me]=m.useState(""),[Z,Re]=m.useState("0"),[ee,pe]=m.useState(""),[Se,he]=m.useState(""),[z,H]=m.useState(""),[be,Pe]=m.useState("1"),[Ne,P]=m.useState("30"),[U,B]=m.useState("0"),[j,T]=m.useState("All"),[se,ce]=m.useState("All"),[xe,we]=m.useState([]),[De,re]=m.useState(0),[Ie,Qe]=m.useState(""),[tt,Dt]=m.useState(""),[ut,fe]=m.useState("0"),[ke,Y]=m.useState(""),[je,rt]=m.useState(""),[at,jt]=m.useState(""),[It,Ze]=m.useState(""),[Et,K]=m.useState(),[Be,Ue]=m.useState(),Le=(r==null?void 0:r.status)==="disabled",kt=ht(),_e=m.useCallback(()=>{const Ee=kt();if(Le){l({status:"success",statusPayload:Qx(r),queues:[]}),d({status:"idle"}),x(void 0);return}l({status:"loading"}),Promise.all([Ex(),Tx()]).then(([Ce,{queues:Fe}])=>{Ee()&&(l({status:"success",statusPayload:Ce,queues:Fe}),x(Ft=>{var En;return Ft&&Fe.some(gn=>gn.name===Ft)?Ft:(En=Fe[0])==null?void 0:En.name}))}).catch(Ce=>{Ee()&&l({status:"error",message:Ce.message})})},[kt,Le,r]);m.useEffect(()=>{_e()},[_e]),Vt({topics:["sqs"],onEvent:_e,onResync:_e,enabled:!Le});const vt=i.status==="success"?i.queues:[],Ae=vt.find(Ee=>Ee.name===p),hn=ht(),lt=m.useCallback(()=>{const Ee=hn();if(!p||Le){d({status:"idle"});return}d({status:"loading"}),Promise.all([Lx(p),_x(p),Bx(p)]).then(([Ce,Fe,Ft])=>{Ee()&&(S(0),d({status:"success",messages:Ce.messages,leases:Fe.leases,dlq:Ft}))}).catch(Ce=>{Ee()&&d({status:"error",message:Ce.message})})},[hn,p,Le]);m.useEffect(()=>{lt()},[lt]);const Xn=m.useMemo(()=>{const Ee=v.trim().toLowerCase();return Ee===""?vt:vt.filter(Ce=>Ce.name.toLowerCase().includes(Ee))},[vt,v]),Tt=m.useMemo(()=>{const Ee=u.status==="success"?u.messages:[],Ce=w.trim().toLowerCase();return Ce===""?Ee:Ee.filter(Fe=>JSON.stringify(Fe).toLowerCase().includes(Ce))},[u,w]),an=Tt[Math.min(g,Math.max(Tt.length-1,0))],dt=xe[Math.min(De,Math.max(xe.length-1,0))],Pt=Ie.trim()||(dt==null?void 0:dt.ReceiptHandle)||"";if(Le)return n.jsx(ue,{title:"SQS",children:n.jsx(A,{title:"SQS is disabled",description:"Enable the SQS service in devcloud config to inspect queues and messages."})});function ln(Ee){x(Ee),S(0),we([]),re(0),Qe(""),Dt(""),Y(""),rt(""),L(""),Ue(void 0)}function In(Ee){if(Ee.preventDefault(),!!p){if(je!==p){Ue("Type the queue name to confirm purge confirmation");return}Ue(void 0),Fx(p).then(()=>{rt(""),_e(),lt()}).catch(Ce=>{Ue(Ce.message)})}}async function xt(Ee,Ce){K(Ee),Ze(""),jt("");try{const Fe=await Ce();jt(Fe),_e(),lt()}catch(Fe){Ze(Fe instanceof Error?Fe.message:"SQS operation failed")}finally{K(void 0)}}function on(Ee){Ee.preventDefault();const Ce=C.trim();if(Ce===""){Ze("Queue name is required");return}const Fe=I==="fifo"&&!Ce.endsWith(".fifo")?`${Ce}.fifo`:Ce;xt("create-queue",async()=>(await Px({QueueName:Fe,Attributes:Ux({VisibilityTimeout:Mu(J),DelaySeconds:Mu(V),FifoQueue:I==="fifo"?"true":void 0,ContentBasedDeduplication:I==="fifo"?"true":void 0}),Tags:{source:"dashboard"}}),_(""),x(Fe),`Created queue ${Fe}`))}function xn(Ee){if(Ee.preventDefault(),!Ae){Ze("Queue is required");return}if(oe===""){Ze("Message body is required");return}const Ce=qx(ee);if(Ce instanceof Error){Ze(Ce.message);return}xt("send-message",async()=>{const Fe=await $x(Ae.name,{MessageBody:oe,DelaySeconds:Ou(Z),MessageAttributes:Ce,MessageGroupId:Se.trim()||void 0,MessageDeduplicationId:z.trim()||void 0});return me(""),pe(""),`Sent message ${Fe.MessageId}`})}function hs(Ee){if(Ee.preventDefault(),!Ae){Ze("Queue is required");return}xt("receive-message",async()=>{const Fe=(await Mx(Ae.name,{MaxNumberOfMessages:Ou(be),VisibilityTimeout:Ll(Ne),WaitTimeSeconds:Ll(U),AttributeNames:Au(j),MessageAttributeNames:Au(se)})).Messages??[];return we(Fe),re(0),Qe(""),Dt(""),Fe.length===1?"Received 1 message":`Received ${Fe.length} messages`})}function xs(Ee){if(Ee.preventDefault(),!Ae){Ze("Queue is required");return}if(Pt===""){Ze("Receipt handle is required");return}if(tt!=="delete"){Ze("Type delete to confirm DeleteMessage");return}xt("delete-message",async()=>(await Ox(Ae.name,{ReceiptHandle:Pt}),we(Ce=>Ce.filter(Fe=>Fe.ReceiptHandle!==Pt)),re(0),Qe(""),Dt(""),Y(""),"Deleted message for the selected receipt handle"))}function gs(Ee){if(Ee.preventDefault(),!Ae){Ze("Queue is required");return}if(Pt===""){Ze("Receipt handle is required");return}if(ke!=="visibility"){Ze("Type visibility to confirm ChangeMessageVisibility");return}const Ce=Ll(ut);if(typeof Ce!="number"){Ze("Visibility timeout must be a non-negative number");return}xt("change-visibility",async()=>(await Ax(Ae.name,{ReceiptHandle:Pt,VisibilityTimeout:Ce}),Y(""),`Changed visibility timeout to ${Ce}s`))}return n.jsxs("div",{className:"dynamodb-workspace",children:[n.jsxs(ue,{title:"Queues",children:[n.jsxs("div",{className:"dynamodb-toolbar",children:[n.jsxs("label",{className:"compact-filter",children:[n.jsx("span",{children:"Filter"}),n.jsx("input",{"aria-label":"Filter SQS queues",onChange:Ee=>N(Ee.target.value),placeholder:"queue name",type:"search",value:v})]}),n.jsx(ne,{onClick:_e,children:"Refresh"})]}),n.jsx(zx,{busyAction:Et,newQueueDelay:V,newQueueKind:I,newQueueName:C,newQueueVisibility:J,onSubmit:on,setNewQueueDelay:$,setNewQueueKind:E,setNewQueueName:_,setNewQueueVisibility:de}),i.status==="loading"?n.jsx(A,{title:"Loading queues",description:"Reading local SQS queue metadata."}):null,i.status==="error"?n.jsx(A,{title:"SQS queues unavailable",description:i.message,actionLabel:"Retry",onAction:_e}):null,i.status==="success"?n.jsx(eg,{activeQueueName:p,onSelectQueue:ln,queues:Xn}):null]}),n.jsxs(ue,{title:"Messages",children:[n.jsxs("div",{className:"dynamodb-toolbar",children:[n.jsx("span",{className:"toolbar-count",children:Ae?`${Tt.length} shown / ${Ae.totalRetainedMessages} retained`:"Select a queue"}),n.jsxs("label",{className:"compact-filter",children:[n.jsx("span",{children:"Filter"}),n.jsx("input",{"aria-label":"Filter SQS messages",disabled:!Ae,onChange:Ee=>{S(0),L(Ee.target.value)},placeholder:"body or attribute",type:"search",value:w})]}),n.jsx(ne,{disabled:!Ae,onClick:lt,children:"Refresh"})]}),n.jsx(Xx,{activeQueue:!!Ae,busyAction:Et,onSubmit:xn,sendAttributesJSON:ee,sendBody:oe,sendDeduplicationId:z,sendDelay:Z,sendGroupId:Se,setSendAttributesJSON:pe,setSendBody:me,setSendDeduplicationId:H,setSendDelay:Re,setSendGroupId:he}),n.jsx(Gx,{activeIndex:g,detailState:u,messages:Tt,onSelectIndex:S,queueName:p}),n.jsx(Zx,{activeQueue:!!Ae,busyAction:Et,onSubmit:hs,receiveAttributeNames:j,receiveMaxMessages:be,receiveMessageAttributeNames:se,receiveVisibilityTimeout:Ne,receiveWaitTime:U,setReceiveAttributeNames:T,setReceiveMaxMessages:Pe,setReceiveMessageAttributeNames:ce,setReceiveVisibilityTimeout:P,setReceiveWaitTime:B}),n.jsx(tg,{messages:xe,onSelectIndex:re,selectedIndex:De})]}),n.jsxs(ue,{title:"Inspector",children:[It?n.jsx("p",{className:"operation-message error",children:It}):null,at?n.jsx("p",{className:"operation-message success",children:at}):null,n.jsx(Jx,{activeQueue:Ae,busyAction:Et,onSubmit:In,purgeConfirmation:je,setPurgeConfirmation:rt}),Be?n.jsx("p",{className:"inspector-muted",children:Be}):null,n.jsx(Hx,{activeQueue:!!Ae,busyAction:Et,onSubmit:gs,selectedReceiptHandle:Pt,setVisibilityConfirmation:Y,setVisibilityTimeout:fe,visibilityConfirmation:ke,visibilityTimeout:ut}),n.jsx(Wx,{activeQueue:!!Ae,busyAction:Et,deleteConfirmation:tt,onSubmit:xs,pastedReceiptHandle:Ie,selectedReceiptHandle:Pt,selectedReceivedMessage:dt,setDeleteConfirmation:Dt,setPastedReceiptHandle:Qe}),n.jsx(ng,{detailState:u,message:an,queue:Ae,status:i.status==="success"?i.statusPayload:void 0})]})]})}async function rg(){return q("/api/pubsub/status")}async function ag(){return q("/api/pubsub/topics")}async function ig(r){return q("/api/pubsub/topics",{method:"POST",body:{topicId:r}})}async function lg(){return q("/api/pubsub/subscriptions")}async function og(r){return q("/api/pubsub/subscriptions",{method:"POST",body:r})}async function cg(r){return q(`/api/pubsub/topics/${encodeURIComponent(r.topicId)}/publish`,{method:"POST",body:{messages:[{data:r.data,attributes:r.attributes,orderingKey:r.orderingKey}]}})}async function ug(r,i){return q(`/api/pubsub/subscriptions/${encodeURIComponent(r)}/pull`,{method:"POST",body:{maxMessages:i}})}async function dg(r,i){await q(`/api/pubsub/subscriptions/${encodeURIComponent(r)}/ack`,{method:"POST",body:{ackIds:i}})}function fg({service:r}){const[i,l]=m.useState({status:"loading"}),[u,d]=m.useState(),[p,x]=m.useState(),[g,S]=m.useState(""),[v,N]=m.useState(""),[w,L]=m.useState(""),[C,_]=m.useState(""),[I,E]=m.useState(""),[J,de]=m.useState("10"),[V,$]=m.useState(""),[oe,me]=m.useState(""),[Z,Re]=m.useState("1"),[ee,pe]=m.useState([]),[Se,he]=m.useState(""),[z,H]=m.useState(""),[be,Pe]=m.useState(""),[Ne,P]=m.useState(),U=(r==null?void 0:r.status)==="disabled",B=ht(),j=m.useCallback(()=>{const fe=B();if(U){l({status:"success",statusPayload:vg(r),topics:[],subscriptions:[]}),x(void 0);return}l({status:"loading"}),Promise.all([rg(),ag(),lg()]).then(([ke,Y,je])=>{fe()&&(l({status:"success",statusPayload:ke,topics:Y.topics,subscriptions:je.subscriptions}),d(rt=>{var at;return rt&&Y.topics.some(jt=>jt.name===rt)?rt:(at=Y.topics[0])==null?void 0:at.name}),E(rt=>{var at;return rt||Bt(((at=Y.topics[0])==null?void 0:at.name)??"")}),x(rt=>{var at;return rt&&je.subscriptions.some(jt=>jt.name===rt)?rt:(at=je.subscriptions[0])==null?void 0:at.name}))}).catch(ke=>{fe()&&l({status:"error",message:ke.message})})},[B,U,r]);m.useEffect(()=>{j()},[j]),Vt({topics:["pubsub"],onEvent:j,onResync:j,enabled:!U});const T=i.status==="success"?i.topics:[],se=i.status==="success"?i.subscriptions:[],ce=T.find(fe=>fe.name===u),xe=se.find(fe=>fe.name===p),we=m.useMemo(()=>{const fe=g.trim().toLowerCase();return fe===""?T:T.filter(ke=>ke.name.toLowerCase().includes(fe))},[T,g]),De=m.useMemo(()=>{const fe=v.trim().toLowerCase();return fe===""?se:se.filter(ke=>JSON.stringify(ke).toLowerCase().includes(fe))},[se,v]);async function re(fe,ke){P(fe),Pe(""),H("");try{const Y=await ke();H(Y),j()}catch(Y){Pe(Y instanceof Error?Y.message:"Pub/Sub operation failed")}finally{P(void 0)}}function Ie(fe){fe.preventDefault();const ke=w.trim();if(ke===""){Pe("Topic ID is required");return}re("create-topic",async()=>{const Y=await ig(ke);return L(""),d(Y.name),E(Bt(Y.name)),`Created topic ${Bt(Y.name)}`})}function Qe(fe){fe.preventDefault();const ke=C.trim(),Y=I.trim()||Bt((ce==null?void 0:ce.name)??"");if(ke===""||Y===""){Pe("Subscription ID and topic are required");return}re("create-subscription",async()=>{const je=await og({subscriptionId:ke,topicId:Y,ackDeadlineSeconds:Number.parseInt(J,10)||void 0});return _(""),x(je.name),`Created subscription ${Bt(je.name)}`})}function tt(fe){fe.preventDefault();const ke=Bt((ce==null?void 0:ce.name)??"");if(ke===""||V===""){Pe("Topic and message are required");return}re("publish",async()=>{const Y=await cg({topicId:ke,data:xg(V),attributes:{source:"dashboard"},orderingKey:oe.trim()||void 0});return $(""),me(""),`Published ${Y.messageIds.length} message`})}function Dt(){const fe=Bt((xe==null?void 0:xe.name)??"");if(fe===""){Pe("Subscription is required");return}re("pull",async()=>{var je;const Y=(await ug(fe,Number.parseInt(Z,10)||1)).receivedMessages??[];return pe(Y),he(((je=Y[0])==null?void 0:je.ackId)??""),`Pulled ${Y.length} message`})}function ut(){const fe=Bt((xe==null?void 0:xe.name)??""),ke=Se?[Se]:ee.map(Y=>Y.ackId).filter(Boolean);if(fe===""||ke.length===0){Pe("Ack ID is required");return}re("ack",async()=>(await dg(fe,ke),pe(Y=>Y.filter(je=>!ke.includes(je.ackId))),he(""),`Acknowledged ${ke.length} message`))}return U?n.jsx(ue,{title:"Pub/Sub",children:n.jsx(A,{title:"Pub/Sub is disabled",description:"Enable the Pub/Sub service in devcloud config to inspect topics and subscriptions."})}):n.jsxs("div",{className:"dynamodb-workspace",children:[n.jsxs(ue,{title:"Topics",children:[n.jsxs("div",{className:"dynamodb-toolbar",children:[n.jsxs("label",{className:"compact-filter",children:[n.jsx("span",{children:"Filter"}),n.jsx("input",{"aria-label":"Filter Pub/Sub topics",onChange:fe=>S(fe.target.value),placeholder:"topic name",type:"search",value:g})]}),n.jsx(ne,{onClick:j,children:"Refresh"})]}),n.jsxs("form",{className:"pubsub-action-form",onSubmit:Ie,children:[n.jsxs("label",{className:"compact-filter",children:[n.jsx("span",{children:"Topic ID"}),n.jsx("input",{"aria-label":"New Pub/Sub topic ID",onChange:fe=>L(fe.target.value),placeholder:"orders",value:w})]}),n.jsx(ne,{disabled:Ne==="create-topic",type:"submit",children:"Create"})]}),i.status==="loading"?n.jsx(A,{title:"Loading topics",description:"Reading local Pub/Sub resource metadata."}):null,i.status==="error"?n.jsx(A,{title:"Pub/Sub unavailable",description:i.message,actionLabel:"Retry",onAction:j}):null,i.status==="success"?n.jsx(pg,{activeTopicName:u,onSelectTopic:d,topics:we}):null,n.jsxs("form",{className:"pubsub-action-form stacked",onSubmit:tt,children:[n.jsxs("label",{className:"compact-filter",children:[n.jsx("span",{children:"Message"}),n.jsx("input",{"aria-label":"Pub/Sub publish message",disabled:!ce,onChange:fe=>$(fe.target.value),placeholder:"message body",value:V})]}),n.jsxs("label",{className:"compact-filter",children:[n.jsx("span",{children:"Ordering key"}),n.jsx("input",{"aria-label":"Pub/Sub publish ordering key",disabled:!ce,onChange:fe=>me(fe.target.value),placeholder:"optional",value:oe})]}),n.jsx(ne,{disabled:!ce||Ne==="publish",type:"submit",children:"Publish"})]})]}),n.jsxs(ue,{title:"Subscriptions",children:[n.jsxs("div",{className:"dynamodb-toolbar",children:[n.jsx("span",{className:"toolbar-count",children:i.status==="success"?`${De.length} shown / ${se.length} subscriptions`:"Loading"}),n.jsxs("label",{className:"compact-filter",children:[n.jsx("span",{children:"Filter"}),n.jsx("input",{"aria-label":"Filter Pub/Sub subscriptions",onChange:fe=>N(fe.target.value),placeholder:"subscription or topic",type:"search",value:v})]})]}),n.jsxs("form",{className:"pubsub-action-form",onSubmit:Qe,children:[n.jsxs("label",{className:"compact-filter",children:[n.jsx("span",{children:"Subscription ID"}),n.jsx("input",{"aria-label":"New Pub/Sub subscription ID",onChange:fe=>_(fe.target.value),placeholder:"orders-sub",value:C})]}),n.jsxs("label",{className:"compact-filter",children:[n.jsx("span",{children:"Topic"}),n.jsxs("select",{"aria-label":"Topic for new Pub/Sub subscription",onChange:fe=>E(fe.target.value),value:I,children:[n.jsx("option",{value:"",children:"Select topic"}),T.map(fe=>n.jsx("option",{value:Bt(fe.name),children:Bt(fe.name)},fe.name))]})]}),n.jsxs("label",{className:"compact-filter small",children:[n.jsx("span",{children:"Ack deadline"}),n.jsx("input",{"aria-label":"New Pub/Sub subscription ack deadline seconds",inputMode:"numeric",onChange:fe=>de(fe.target.value),value:J})]}),n.jsx(ne,{disabled:Ne==="create-subscription",type:"submit",children:"Create"})]}),i.status==="success"?n.jsx(mg,{activeSubscriptionName:p,onSelectSubscription:x,subscriptions:De}):null]}),n.jsxs(ue,{title:"Inspector",children:[be?n.jsx("p",{className:"operation-message error",children:be}):null,z?n.jsx("p",{className:"operation-message success",children:z}):null,n.jsx(hg,{busyAction:Ne,onAck:ut,onPull:Dt,pulledMessages:ee,pullMaxMessages:Z,selectedAckId:Se,setPullMaxMessages:Re,setSelectedAckId:he,status:i.status==="success"?i.statusPayload:void 0,subscription:xe,topicCount:T.length})]})]})}function pg({activeTopicName:r,onSelectTopic:i,topics:l}){return l.length===0?n.jsx(A,{title:"No topics",description:"Topics created through Pub/Sub REST or SDK clients will appear here."}):n.jsx("div",{className:"dynamodb-table-list","aria-label":"Pub/Sub topics",children:l.map(u=>n.jsxs("button",{className:u.name===r?"dynamodb-table-row active":"dynamodb-table-row",onClick:()=>i(u.name),children:[n.jsxs("span",{className:"table-row-top",children:[n.jsx("span",{className:"table-row-name",children:Bt(u.name)}),n.jsx("span",{className:"count-pill",children:u.subscriptionCount})]}),n.jsx("span",{className:"table-row-meta",children:u.name}),n.jsx("span",{className:"table-row-tags",children:n.jsxs("span",{children:[u.subscriptionCount," subscriptions"]})})]},u.name))})}function mg({activeSubscriptionName:r,onSelectSubscription:i,subscriptions:l}){return l.length===0?n.jsx(A,{title:"No subscriptions",description:"Subscriptions created through Pub/Sub REST or SDK clients will appear here."}):n.jsx("div",{className:"dynamodb-item-table-wrap",children:n.jsxs("table",{className:"dynamodb-item-table",children:[n.jsx("thead",{children:n.jsxs("tr",{children:[n.jsx("th",{scope:"col",children:"Subscription"}),n.jsx("th",{scope:"col",children:"Backlog"}),n.jsx("th",{scope:"col",children:"In flight"}),n.jsx("th",{scope:"col",children:"Attempts"})]})}),n.jsx("tbody",{children:l.map(u=>n.jsxs("tr",{className:u.name===r?"item-row active":"item-row",onClick:()=>i(u.name),children:[n.jsx("td",{children:n.jsxs("span",{className:"attribute-preview",children:[n.jsx("span",{className:"attribute-chip",children:Bt(u.name)}),n.jsx("span",{className:"attribute-chip",children:Bt(u.topic)})]})}),n.jsx("td",{children:u.backlogMessages}),n.jsx("td",{children:u.inFlightMessages}),n.jsx("td",{children:u.maxDeliveryAttemptSeen})]},u.name))})]})})}function hg({busyAction:r,onAck:i,onPull:l,pulledMessages:u,pullMaxMessages:d,selectedAckId:p,setPullMaxMessages:x,setSelectedAckId:g,status:S,subscription:v,topicCount:N}){return v?n.jsxs("div",{className:"dynamodb-inspector",children:[n.jsxs("section",{children:[n.jsx("span",{className:"inspector-label",children:"Service"}),n.jsx("h3",{children:Bt(v.name)}),n.jsxs("dl",{className:"inspector-list",children:[n.jsxs("div",{children:[n.jsx("dt",{children:"REST"}),n.jsx("dd",{children:n.jsx("code",{children:(S==null?void 0:S.restEndpoint)??"unknown"})})]}),n.jsxs("div",{children:[n.jsx("dt",{children:"gRPC"}),n.jsx("dd",{children:n.jsx("code",{children:(S==null?void 0:S.grpcEndpoint)??"unknown"})})]}),n.jsxs("div",{children:[n.jsx("dt",{children:"Project"}),n.jsx("dd",{children:(S==null?void 0:S.project)??"unknown"})]}),n.jsxs("div",{children:[n.jsx("dt",{children:"Topics"}),n.jsx("dd",{children:N})]}),n.jsxs("div",{children:[n.jsx("dt",{children:"Ack deadline"}),n.jsxs("dd",{children:[v.ackDeadlineSeconds,"s"]})]}),n.jsxs("div",{children:[n.jsx("dt",{children:"Retained"}),n.jsx("dd",{children:v.totalRetainedMessages})]})]})]}),n.jsxs("section",{children:[n.jsx("span",{className:"inspector-label",children:"Actions"}),n.jsxs("div",{className:"pubsub-action-row",children:[n.jsxs("label",{className:"compact-filter small",children:[n.jsx("span",{children:"Pull max"}),n.jsx("input",{"aria-label":"Pub/Sub pull max messages",inputMode:"numeric",onChange:w=>x(w.target.value),value:d})]}),n.jsx(ne,{disabled:r==="pull",onClick:l,children:"Pull"}),n.jsx(ne,{disabled:u.length===0||r==="ack",onClick:i,children:"Ack"})]}),u.length>0?n.jsx("div",{className:"pulled-message-list",children:u.map(w=>n.jsxs("label",{className:"pulled-message",children:[n.jsx("input",{checked:p===w.ackId,name:"pubsub-ack-id",onChange:()=>g(w.ackId),type:"radio"}),n.jsxs("span",{children:[n.jsx("strong",{children:w.message.messageId??"pulled message"}),n.jsx("code",{children:gg(w.message.data??"")})]})]},w.ackId))}):null]}),n.jsxs("section",{children:[n.jsx("span",{className:"inspector-label",children:"Recent deliveries"}),v.recentDeliveries&&v.recentDeliveries.length>0?n.jsx(yg,{deliveries:v.recentDeliveries}):n.jsx("p",{className:"inspector-muted",children:"No retained delivery records for this subscription."})]})]}):n.jsx(A,{title:"Inspector",description:"Subscription backlog, leases, and recent deliveries will appear here."})}function xg(r){return window.btoa(unescape(encodeURIComponent(r)))}function gg(r){if(r==="")return"";try{return decodeURIComponent(escape(window.atob(r)))}catch{return r}}function yg({deliveries:r}){return n.jsx("div",{className:"dynamodb-item-table-wrap",children:n.jsxs("table",{className:"dynamodb-item-table",children:[n.jsx("thead",{children:n.jsxs("tr",{children:[n.jsx("th",{scope:"col",children:"Message"}),n.jsx("th",{scope:"col",children:"State"}),n.jsx("th",{scope:"col",children:"Attempt"}),n.jsx("th",{scope:"col",children:"Ready"})]})}),n.jsx("tbody",{children:r.map(i=>n.jsxs("tr",{className:"item-row",children:[n.jsx("td",{children:n.jsxs("span",{className:"attribute-preview",children:[n.jsx("span",{className:"attribute-chip",children:i.messageId}),i.orderingKey?n.jsx("span",{className:"attribute-chip",children:i.orderingKey}):null]})}),n.jsx("td",{children:i.state}),n.jsx("td",{children:i.deliveryAttempt}),n.jsx("td",{children:jg(i)})]},`${i.messageId}-${i.deliveryAttempt}-${i.state}`))})]})})}function jg(r){return r.leaseDeadline?`lease until ${$l(r.leaseDeadline)}`:r.nextDeliveryTime?`retry at ${$l(r.nextDeliveryTime)}`:r.publishTime?`published ${$l(r.publishTime)}`:"available"}function vg(r){return{service:"pubsub",status:"disabled",running:!1,grpcEndpoint:"127.0.0.1:18085",restEndpoint:(r==null?void 0:r.endpoint)??"http://127.0.0.1:18086",project:"devcloud",storagePath:(r==null?void 0:r.storagePath)??".devcloud/data/pubsub",topicCount:0,subscriptionCount:0}}function Bt(r){return r.split("/").pop()??r}function $l(r){const i=new Date(r);return Number.isNaN(i.getTime())?r:i.toLocaleString()}async function bg(){return q("/api/gcs/status")}async function Sg(){return q("/api/gcs/buckets")}async function Ng(r){return q("/api/gcs/buckets",{method:"POST",body:{name:r}})}async function wg(r){return mn(`/api/gcs/buckets/${encodeURIComponent(r)}`,{method:"DELETE"})}async function kg(r,i=""){const l=new URLSearchParams;i!==""&&l.set("prefix",i);const u=l.toString();return q(`/api/gcs/buckets/${encodeURIComponent(r)}/objects${u?`?${u}`:""}`)}async function Cg(r,i){return q(`/api/gcs/buckets/${encodeURIComponent(r)}/objects/${encodeURIComponent(i)}`)}async function Rg(r,i){return mn(`/api/gcs/buckets/${encodeURIComponent(r)}/objects/${encodeURIComponent(i)}`,{method:"DELETE"})}async function Dg(){return q("/api/gcs/upload-sessions")}async function Ig(r){return mn(`/api/gcs/uploads/${encodeURIComponent(r)}`,{method:"DELETE"})}function Eg({service:r}){const[i,l]=m.useState({status:"loading"}),[u,d]=m.useState({status:"idle"}),[p,x]=m.useState(),[g,S]=m.useState(),[v,N]=m.useState(),[w,L]=m.useState(""),[C,_]=m.useState(""),[I,E]=m.useState(),[J,de]=m.useState(0),V=(r==null?void 0:r.status)==="disabled",$=Pr(),oe=(v==null?void 0:v.bucket)===p?v==null?void 0:v.name:void 0,me=ht(),Z=m.useCallback(()=>{const P=me();if(V){l({status:"success",statusPayload:Bu(r),buckets:[],sessions:[]}),d({status:"idle"}),x(void 0),S(void 0),N(void 0);return}l({status:"loading"}),Promise.all([bg(),Sg(),Dg()]).then(([U,B,j])=>{P()&&(l({status:"success",statusPayload:U,buckets:B.buckets,sessions:j.sessions}),x(T=>{var se;return T&&B.buckets.some(ce=>ce.name===T)?T:(se=B.buckets[0])==null?void 0:se.name}))}).catch(U=>{P()&&l({status:"error",message:U.message})})},[me,V,r]);m.useEffect(()=>{Z()},[Z]);const Re=m.useCallback(P=>{Z(),P.type.startsWith("gcs.object.")&&de(U=>U+1)},[Z]);Vt({topics:["gcs"],onEvent:Re,onResync:()=>{Z(),de(P=>P+1)},enabled:!V}),m.useEffect(()=>{if(S(void 0),N(void 0),!p||V){d({status:"idle"});return}let P=!1;return d({status:"loading"}),kg(p,w.trim()).then(({objects:U})=>{P||d({status:"success",objects:U})}).catch(U=>{P||d({status:"error",message:U.message})}),()=>{P=!0}},[p,V,w,J]),m.useEffect(()=>{if(S(void 0),!v||v.bucket!==p||V)return;let P=!1;return Cg(v.bucket,v.name).then(U=>{P||S(U)}).catch(U=>{P||E(U.message)}),()=>{P=!0}},[p,v,V]);const ee=i.status==="success"?i.buckets:[],pe=i.status==="success"?i.sessions:[],Se=i.status==="success"?i.statusPayload:void 0;function he(P){x(P),S(void 0),N(void 0)}function z(P){p&&N({bucket:p,name:P.name})}function H(){const P=C.trim();!P||V||Ng(P).then(()=>{_(""),E(`Created bucket ${P}`),Z()}).catch(U=>E(U.message))}async function be(P){V||!await $(Rn({title:"Delete bucket",description:"All objects and metadata in this bucket will be removed. This cannot be undone.",target:P}))||wg(P).then(()=>{E(`Deleted bucket ${P}`),Z()}).catch(B=>E(B.message))}async function Pe(P){!p||V||!await $(Rn({title:"Delete object",description:"This object will be permanently removed from the bucket.",target:P.name}))||Rg(p,P.name).then(()=>{E(`Deleted object ${P.gcsUri}`),de(B=>B+1)}).catch(B=>E(B.message))}async function Ne(P){V||!await $(Rn({title:"Delete upload session",description:"In-progress upload state will be discarded.",target:P.id}))||Ig(P.id).then(()=>{E(`Deleted upload session ${P.id}`),Z()}).catch(B=>E(B.message))}return V?n.jsxs(ue,{title:"devcloud GCS",children:[n.jsx(A,{title:"GCS is disabled",description:"Enable the GCS service in devcloud config to inspect local buckets and objects."}),n.jsx(_u,{status:Se??Bu(r)})]}):n.jsxs("div",{className:"gcs-workspace",children:[n.jsxs(ue,{title:"devcloud GCS",children:[n.jsxs("div",{className:"s3-toolbar",children:[n.jsxs("span",{children:[ee.length," buckets"]}),n.jsx(ne,{onClick:Z,children:"Refresh"})]}),i.status==="loading"?n.jsx(A,{title:"Loading GCS",description:"Reading local GCS status."}):null,i.status==="error"?n.jsx(A,{title:"GCS unavailable",description:i.message,actionLabel:"Retry",onAction:Z}):null,i.status==="success"?n.jsxs("div",{className:"gcs-sidebar",children:[n.jsx(_u,{status:i.statusPayload}),n.jsx(Tg,{bucketName:C,disabled:V,onChange:_,onCreate:H}),n.jsx(Pg,{activeBucket:p,buckets:ee,disabled:V,onDeleteBucket:be,onSelectBucket:he})]}):null]}),n.jsx(ue,{title:"Objects",children:n.jsx(Lg,{activeObjectName:oe,bucketName:p,disabled:V,objectsState:u,onDeleteObject:Pe,onInspectObject:z,onPrefixChange:L,prefix:w})}),n.jsx(ue,{title:"Inspector",children:n.jsx($g,{bucketName:p,object:g})}),n.jsx(ue,{title:"Upload sessions",children:n.jsx(Mg,{disabled:V,onDeleteSession:Ne,sessions:pe})}),I?n.jsx("p",{className:"inspector-muted gcs-message",children:I}):null]})}function _u({status:r}){return n.jsxs("dl",{className:"inspector-list",children:[n.jsxs("div",{children:[n.jsx("dt",{children:"Status"}),n.jsx("dd",{children:r.running?"running":"disabled"})]}),n.jsxs("div",{children:[n.jsx("dt",{children:"Project"}),n.jsx("dd",{children:r.project})]}),n.jsxs("div",{children:[n.jsx("dt",{children:"Endpoint"}),n.jsx("dd",{children:n.jsx("code",{children:r.endpoint})})]}),n.jsxs("div",{children:[n.jsx("dt",{children:"Upload sessions"}),n.jsx("dd",{children:n.jsx("code",{children:r.uploadSessionPath})})]})]})}function Tg({bucketName:r,disabled:i,onChange:l,onCreate:u}){return n.jsxs("div",{className:"gcs-create-bucket",children:[n.jsxs("label",{className:"prefix-filter",children:[n.jsx("span",{children:"Create bucket"}),n.jsx("input",{"aria-label":"GCS bucket name",disabled:i,onChange:d=>l(d.target.value),placeholder:"local-bucket",value:r})]}),n.jsx(ne,{disabled:i||r.trim()==="",onClick:u,children:"Create"})]})}function Pg({activeBucket:r,buckets:i,disabled:l,onDeleteBucket:u,onSelectBucket:d}){return i.length===0?n.jsx(A,{title:"No buckets",description:"Buckets created through the GCS API will appear here."}):n.jsx("div",{className:"bucket-list",children:i.map(p=>n.jsxs("div",{className:p.name===r?"bucket-item active gcs-bucket-row":"bucket-item gcs-bucket-row",children:[n.jsxs("button",{className:"object-select",onClick:()=>d(p.name),children:[n.jsx("span",{className:"bucket-name",children:p.name}),n.jsx("span",{className:"bucket-meta",children:p.gcsUri}),n.jsxs("span",{className:"bucket-meta",children:["Created ",Ul(p.timeCreated)]})]}),n.jsx("span",{className:"count-pill",children:p.objectCount}),n.jsx(ne,{className:"danger",disabled:l,onClick:()=>u(p.name),children:"Delete bucket"})]},p.name))})}function Lg({activeObjectName:r,bucketName:i,disabled:l,objectsState:u,onDeleteObject:d,onInspectObject:p,onPrefixChange:x,prefix:g}){return i?n.jsxs("div",{className:"object-browser",children:[n.jsxs("label",{className:"prefix-filter",children:[n.jsx("span",{children:"Prefix"}),n.jsx("input",{"aria-label":"Filter GCS objects by prefix",onChange:S=>x(S.target.value),placeholder:"docs/",type:"search",value:g})]}),u.status==="loading"?n.jsx(A,{title:"Loading objects",description:`Reading objects in ${i}.`}):null,u.status==="error"?n.jsx(A,{title:"Objects unavailable",description:u.message}):null,u.status==="success"&&u.objects.length===0?n.jsx(A,{title:"No objects",description:`No objects in ${i} match the current prefix.`}):null,u.status==="success"&&u.objects.length>0?n.jsx("div",{className:"object-table-wrap",children:n.jsxs("table",{className:"object-table",children:[n.jsx("thead",{children:n.jsxs("tr",{children:[n.jsx("th",{scope:"col",children:"Name"}),n.jsx("th",{scope:"col",children:"Size"}),n.jsx("th",{scope:"col",children:"Updated"}),n.jsx("th",{scope:"col",children:"Class"}),n.jsx("th",{scope:"col",children:"Actions"})]})}),n.jsx("tbody",{children:u.objects.map(S=>n.jsxs("tr",{className:S.name===r?"object-row active":"object-row",children:[n.jsxs("td",{children:[n.jsx("button",{className:"object-select",onClick:()=>p(S),children:n.jsx("span",{className:"object-key",children:S.name})}),n.jsx("code",{children:S.gcsUri})]}),n.jsx("td",{children:nd(S.size)}),n.jsx("td",{children:Ul(S.updated)}),n.jsx("td",{children:S.storageClass||"STANDARD"}),n.jsxs("td",{children:[n.jsx("a",{className:"object-action",href:sd(S.downloadUrl),children:"Download"}),n.jsx(ne,{className:"danger",disabled:l,onClick:()=>d(S),children:"Delete object"})]})]},S.name))})]})}):null]}):n.jsx(A,{title:"Select a bucket",description:"Choose a bucket to browse objects and metadata."})}function $g({bucketName:r,object:i}){if(!i)return n.jsx(A,{title:"Inspector",description:r?`Select an object in ${r} to inspect GCS metadata.`:"Object metadata, generation, and download links will appear here."});const l=Object.entries(i.metadata??{});return n.jsxs("div",{className:"object-inspector",children:[n.jsxs("div",{children:[n.jsx("span",{className:"inspector-label",children:"gs:// URI"}),n.jsx("code",{children:i.gcsUri})]}),n.jsxs("dl",{className:"inspector-list",children:[n.jsxs("div",{children:[n.jsx("dt",{children:"generation"}),n.jsx("dd",{children:i.generation})]}),n.jsxs("div",{children:[n.jsx("dt",{children:"metageneration"}),n.jsx("dd",{children:i.metageneration})]}),n.jsxs("div",{children:[n.jsx("dt",{children:"storageClass"}),n.jsx("dd",{children:i.storageClass||"STANDARD"})]}),n.jsxs("div",{children:[n.jsx("dt",{children:"crc32c"}),n.jsx("dd",{children:i.crc32c||"unknown"})]}),n.jsxs("div",{children:[n.jsx("dt",{children:"contentType"}),n.jsx("dd",{children:i.contentType||"application/octet-stream"})]}),n.jsxs("div",{children:[n.jsx("dt",{children:"ETag"}),n.jsx("dd",{children:n.jsx("code",{children:i.etag||"unknown"})})]})]}),n.jsxs("div",{children:[n.jsx("span",{className:"inspector-label",children:"metadata"}),l.length===0?n.jsx("p",{className:"inspector-muted",children:"No user metadata."}):n.jsx("dl",{className:"metadata-list",children:l.map(([u,d])=>n.jsxs("div",{children:[n.jsx("dt",{children:u}),n.jsx("dd",{children:d})]},u))})]}),n.jsx("a",{className:"compat-link inspector-download",href:sd(i.downloadUrl),children:"Download object"})]})}function Mg({disabled:r,onDeleteSession:i,sessions:l}){return l.length===0?n.jsx(A,{title:"No upload sessions",description:"Active resumable upload sessions will appear here."}):n.jsx("div",{className:"object-table-wrap",children:n.jsxs("table",{className:"object-table",children:[n.jsx("thead",{children:n.jsxs("tr",{children:[n.jsx("th",{scope:"col",children:"Session"}),n.jsx("th",{scope:"col",children:"Object"}),n.jsx("th",{scope:"col",children:"Received"}),n.jsx("th",{scope:"col",children:"Created"}),n.jsx("th",{scope:"col",children:"Action"})]})}),n.jsx("tbody",{children:l.map(u=>n.jsxs("tr",{children:[n.jsx("td",{children:n.jsx("code",{children:u.id})}),n.jsxs("td",{children:[n.jsx("span",{className:"object-key",children:u.name}),n.jsx("code",{children:`gs://${u.bucket}/${u.name}`})]}),n.jsx("td",{children:nd(u.receivedBytes)}),n.jsx("td",{children:Ul(u.createdAt)}),n.jsx("td",{children:n.jsx(ne,{className:"danger",disabled:r,onClick:()=>i(u),children:"Delete session"})})]},u.id))})]})})}function Bu(r){return{status:"disabled",running:!1,endpoint:(r==null?void 0:r.endpoint)??"http://127.0.0.1:14443",project:"devcloud",storagePath:(r==null?void 0:r.storagePath)??".devcloud/data/s3",uploadSessionPath:".devcloud/data/gcs/upload_sessions"}}function nd(r){if(!Number.isFinite(r)||r<0)return"unknown";if(r<1024)return`${r} B`;const i=["KB","MB","GB","TB"];let l=r/1024,u=0;for(;l>=1024&&u=10?0:1)} ${i[u]}`}function Ul(r){const i=new Date(r);return Number.isNaN(i.getTime())?r||"unknown":i.toLocaleString()}function sd(r){return r.startsWith("/api/gcs/")?r:"#"}async function Og(){return q("/api/applicationautoscaling/status")}async function Ag(){return q("/api/applicationautoscaling/scalable-targets")}async function _g(){return q("/api/applicationautoscaling/scaling-policies")}async function Bg(){return q("/api/applicationautoscaling/scheduled-actions")}function Fg({service:r}){const[i,l]=m.useState({status:"loading"}),u=(r==null?void 0:r.status)==="disabled",d=ht(),p=m.useCallback(()=>{const x=d();if(u){l({status:"success",statusPayload:Vg(r),scalableTargets:[],scalingPolicies:[],scheduledActions:[]});return}l({status:"loading"}),Promise.all([Og(),Ag(),_g(),Bg()]).then(([g,S,v,N])=>{x()&&l({status:"success",statusPayload:g,scalableTargets:S.scalableTargets,scalingPolicies:v.scalingPolicies,scheduledActions:N.scheduledActions})}).catch(g=>{x()&&l({status:"error",message:g.message})})},[d,u,r]);return m.useEffect(()=>{p()},[p]),Vt({topics:["applicationautoscaling"],onEvent:p,onResync:p,enabled:!u}),u?n.jsx(ue,{title:"Application Auto Scaling",children:n.jsx(A,{title:"Application Auto Scaling is disabled",description:"Enable the Application Auto Scaling service in devcloud config to inspect scalable targets, policies, and scheduled actions."})}):n.jsxs("div",{className:"dynamodb-workspace",children:[n.jsxs(ue,{title:"Status",children:[n.jsxs("div",{className:"dynamodb-toolbar",children:[n.jsx("span",{className:"toolbar-count",children:i.status==="success"?`${i.statusPayload.status} / ${i.statusPayload.region}`:"Loading"}),n.jsx(ne,{onClick:p,children:"Refresh"})]}),i.status==="loading"?n.jsx(A,{title:"Loading Application Auto Scaling",description:"Reading local scalable targets, policies, and scheduled actions."}):null,i.status==="error"?n.jsx(A,{title:"Application Auto Scaling unavailable",description:i.message,actionLabel:"Retry",onAction:p}):null,i.status==="success"?n.jsx(zg,{status:i.statusPayload}):null]}),n.jsx(ue,{title:"Scalable targets",children:n.jsx(Qg,{targets:i.status==="success"?i.scalableTargets:[]})}),n.jsx(ue,{title:"Scaling policies",children:n.jsx(Ug,{policies:i.status==="success"?i.scalingPolicies:[]})}),n.jsx(ue,{title:"Scheduled actions",children:n.jsx(qg,{actions:i.status==="success"?i.scheduledActions:[]})})]})}function zg({status:r}){return n.jsxs("dl",{className:"inspector-list",children:[n.jsxs("div",{children:[n.jsx("dt",{children:"Endpoint"}),n.jsx("dd",{children:n.jsx("code",{children:r.endpoint})})]}),n.jsxs("div",{children:[n.jsx("dt",{children:"Region"}),n.jsx("dd",{children:r.region})]}),n.jsxs("div",{children:[n.jsx("dt",{children:"Storage path"}),n.jsx("dd",{children:n.jsx("code",{children:r.storagePath})})]}),n.jsxs("div",{children:[n.jsx("dt",{children:"Scalable targets"}),n.jsx("dd",{children:r.scalableTargetCount})]}),n.jsxs("div",{children:[n.jsx("dt",{children:"Scaling policies"}),n.jsx("dd",{children:r.scalingPolicyCount})]}),n.jsxs("div",{children:[n.jsx("dt",{children:"Scheduled actions"}),n.jsx("dd",{children:r.scheduledActionCount})]})]})}function Qg({targets:r}){return r.length===0?n.jsx(A,{title:"No scalable targets",description:"Targets registered via RegisterScalableTarget will appear here."}):n.jsx("div",{className:"dynamodb-table-list","aria-label":"Application Auto Scaling scalable targets",children:r.map(i=>{var l,u,d;return n.jsxs("section",{className:"dynamodb-table-row",children:[n.jsxs("span",{className:"table-row-top",children:[n.jsx("span",{className:"table-row-name",children:i.ResourceId}),n.jsxs("span",{className:"count-pill",children:[i.MinCapacity,"-",i.MaxCapacity]})]}),n.jsx("span",{className:"table-row-meta",children:i.ScalableDimension}),n.jsxs("span",{className:"table-row-tags",children:[n.jsx("span",{children:i.ServiceNamespace}),(l=i.SuspendedState)!=null&&l.DynamicScalingInSuspended||(u=i.SuspendedState)!=null&&u.DynamicScalingOutSuspended||(d=i.SuspendedState)!=null&&d.ScheduledScalingSuspended?n.jsx("span",{children:"suspended"}):null]})]},`${i.ResourceId}-${i.ScalableDimension}`)})})}function Ug({policies:r}){return r.length===0?n.jsx(A,{title:"No scaling policies",description:"Policies created via PutScalingPolicy will appear here."}):n.jsx("div",{className:"dynamodb-table-list","aria-label":"Application Auto Scaling scaling policies",children:r.map(i=>n.jsxs("section",{className:"dynamodb-table-row",children:[n.jsxs("span",{className:"table-row-top",children:[n.jsx("span",{className:"table-row-name",children:i.PolicyName}),n.jsx("span",{className:"count-pill",children:i.PolicyType})]}),n.jsx("span",{className:"table-row-meta",children:i.ResourceId}),n.jsx("span",{className:"table-row-tags",children:n.jsx("span",{children:i.ScalableDimension})})]},i.PolicyARN))})}function qg({actions:r}){return r.length===0?n.jsx(A,{title:"No scheduled actions",description:"Actions created via PutScheduledAction will appear here."}):n.jsx("div",{className:"dynamodb-table-list","aria-label":"Application Auto Scaling scheduled actions",children:r.map(i=>n.jsxs("section",{className:"dynamodb-table-row",children:[n.jsx("span",{className:"table-row-top",children:n.jsx("span",{className:"table-row-name",children:i.ScheduledActionName})}),n.jsx("span",{className:"table-row-meta",children:i.ResourceId}),n.jsxs("span",{className:"table-row-tags",children:[n.jsx("span",{children:i.Schedule}),i.Timezone?n.jsx("span",{children:i.Timezone}):null]})]},i.ScheduledActionName))})}function Vg(r){return{service:"applicationautoscaling",status:"disabled",running:!1,endpoint:(r==null?void 0:r.endpoint)??"http://127.0.0.1:18030",region:"us-east-1",storagePath:(r==null?void 0:r.storagePath)??".devcloud/data/applicationautoscaling",scalableTargetCount:0,scalingPolicyCount:0,scheduledActionCount:0}}async function Kg(){return q("/api/ssm/status")}async function Jg(){return q("/api/ssm/parameters")}function Hg({service:r}){const[i,l]=m.useState({status:"loading"}),u=(r==null?void 0:r.status)==="disabled",d=ht(),p=m.useCallback(()=>{const x=d();u||(l({status:"loading"}),Promise.all([Kg(),Jg()]).then(([g,S])=>{x()&&l({status:"success",statusPayload:g,parameters:S.parameters})}).catch(g=>{x()&&l({status:"error",message:g.message})}))},[d,u]);return m.useEffect(()=>{p()},[p]),u?n.jsx(ue,{title:"SSM Parameter Store",children:n.jsx(A,{title:"SSM Parameter Store is disabled",description:"Enable the ssm service in devcloud config to inspect parameters."})}):n.jsxs("div",{className:"dynamodb-workspace",children:[n.jsxs(ue,{title:"Status",children:[n.jsxs("div",{className:"dynamodb-toolbar",children:[n.jsx("span",{className:"toolbar-count",children:i.status==="success"?`${i.statusPayload.status} / ${i.statusPayload.region}`:"Loading"}),n.jsx(ne,{onClick:p,children:"Refresh"})]}),i.status==="loading"?n.jsx(A,{title:"Loading SSM Parameter Store",description:"Reading local parameter metadata."}):null,i.status==="error"?n.jsx(A,{title:"SSM Parameter Store unavailable",description:i.message,actionLabel:"Retry",onAction:p}):null,i.status==="success"?n.jsxs("dl",{className:"inspector-list",children:[n.jsxs("div",{children:[n.jsx("dt",{children:"Endpoint"}),n.jsx("dd",{children:n.jsx("code",{children:i.statusPayload.endpoint})})]}),n.jsxs("div",{children:[n.jsx("dt",{children:"Region"}),n.jsx("dd",{children:i.statusPayload.region})]}),n.jsxs("div",{children:[n.jsx("dt",{children:"Storage path"}),n.jsx("dd",{children:n.jsx("code",{children:i.statusPayload.storagePath})})]}),n.jsxs("div",{children:[n.jsx("dt",{children:"Parameters"}),n.jsx("dd",{children:i.statusPayload.parameterCount})]})]}):null]}),n.jsx(ue,{title:"Parameters",children:n.jsx(Wg,{parameters:i.status==="success"?i.parameters:[]})})]})}function Wg({parameters:r}){return r.length===0?n.jsx(A,{title:"No parameters",description:"Parameters created via PutParameter will appear here. Values are not shown."}):n.jsx("div",{className:"dynamodb-table-list","aria-label":"SSM parameters",children:r.map(i=>n.jsxs("section",{className:"dynamodb-table-row",children:[n.jsxs("span",{className:"table-row-top",children:[n.jsx("span",{className:"table-row-name",children:i.Name}),n.jsxs("span",{className:"count-pill",children:["v",i.Version]})]}),n.jsx("span",{className:"table-row-meta",children:i.Description??new Date(i.LastModifiedDate*1e3).toISOString()}),n.jsxs("span",{className:"table-row-tags",children:[n.jsx("span",{children:i.Type}),n.jsx("span",{children:i.Tier}),i.KeyId?n.jsx("span",{children:i.KeyId}):null]})]},i.Name))})}async function Gg(){return q("/api/sts/status")}async function Yg(){return q("/api/sts/caller-identity")}function Xg({service:r}){const[i,l]=m.useState({status:"loading"}),u=(r==null?void 0:r.status)==="disabled",d=ht(),p=m.useCallback(()=>{const x=d();u||(l({status:"loading"}),Promise.all([Gg(),Yg()]).then(([g,S])=>{x()&&l({status:"success",statusPayload:g,identity:S})}).catch(g=>{x()&&l({status:"error",message:g.message})}))},[d,u]);return m.useEffect(()=>{p()},[p]),u?n.jsx(ue,{title:"STS",children:n.jsx(A,{title:"STS is disabled",description:"Enable the sts service in devcloud config to issue temporary credentials."})}):n.jsxs(ue,{title:"Status",children:[n.jsxs("div",{className:"dynamodb-toolbar",children:[n.jsx("span",{className:"toolbar-count",children:i.status==="success"?`${i.statusPayload.status} / ${i.statusPayload.region}`:"Loading"}),n.jsx(ne,{onClick:p,children:"Refresh"})]}),i.status==="loading"?n.jsx(A,{title:"Loading STS",description:"Calling GetCallerIdentity."}):null,i.status==="error"?n.jsx(A,{title:"STS unavailable",description:i.message,actionLabel:"Retry",onAction:p}):null,i.status==="success"?n.jsxs("dl",{className:"inspector-list",children:[n.jsxs("div",{children:[n.jsx("dt",{children:"Endpoint"}),n.jsx("dd",{children:n.jsx("code",{children:i.statusPayload.endpoint})})]}),n.jsxs("div",{children:[n.jsx("dt",{children:"Account"}),n.jsx("dd",{children:i.identity.account})]}),n.jsxs("div",{children:[n.jsx("dt",{children:"Caller ARN"}),n.jsx("dd",{children:n.jsx("code",{children:i.identity.arn})})]}),n.jsxs("div",{children:[n.jsx("dt",{children:"Operations"}),n.jsx("dd",{children:"AssumeRole (inline session policies accepted, not enforced), GetCallerIdentity"})]})]}):null]})}async function Zg(){return q("/api/lambda/status")}async function ey(){return q("/api/lambda/functions")}async function ty(){return q("/api/lambda/invocations")}async function ny(r,i){return q(`/api/lambda/functions/${encodeURIComponent(r)}/invoke`,{method:"POST",rawBody:i,headers:{"Content-Type":"application/json"},timeoutMs:91e4})}function sy({service:r}){const[i,l]=m.useState({status:"loading"}),[u,d]=m.useState(),[p,x]=m.useState("{}"),[g,S]=m.useState(()=>new Map),v=m.useRef(new Map),N=m.useRef(0),w=(r==null?void 0:r.status)==="disabled",L=ht(),C=m.useCallback(()=>{const $=L();w||Promise.all([Zg(),ey(),ty()]).then(([oe,me,Z])=>{$()&&(l({status:"success",statusPayload:oe,functions:me.functions,invocations:Z.invocations}),d(Re=>{var ee;return Re&&me.functions.some(pe=>pe.FunctionName===Re)?Re:(ee=me.functions[0])==null?void 0:ee.FunctionName}))}).catch(oe=>{$()&&l({status:"error",message:oe.message})})},[L,w]);if(m.useEffect(()=>{C()},[C]),Vt({topics:["lambda"],onEvent:C,onResync:C,enabled:!w}),w)return n.jsx(ue,{title:"Lambda",children:n.jsx(A,{title:"Lambda is disabled",description:"Enable the Lambda service in devcloud config to deploy and invoke local functions."})});const _=i.status==="success"?i.functions:[],I=i.status==="success"?i.invocations:[],E=_.find($=>$.FunctionName===u),J=E&&g.get(E.FunctionName)||{status:"idle"},de=($,oe,me)=>{v.current.get($)===oe&&S(Z=>new Map(Z).set($,me))},V=$=>{if($.preventDefault(),!E)return;const oe=E.FunctionName;N.current+=1;const me=N.current;v.current.set(oe,me);try{JSON.parse(p||"{}")}catch(Z){de(oe,me,{status:"error",message:`Event is not valid JSON: ${Z.message}`});return}de(oe,me,{status:"running"}),ny(oe,p||"{}").then(Z=>de(oe,me,{status:"done",result:Z})).catch(Z=>de(oe,me,{status:"error",message:Z.message}))};return n.jsxs("div",{className:"dynamodb-workspace",children:[n.jsxs(ue,{title:"Status",children:[n.jsxs("div",{className:"dynamodb-toolbar",children:[n.jsx("span",{className:"toolbar-count",children:i.status==="success"?`${i.statusPayload.status} / ${i.statusPayload.region}`:"Loading"}),n.jsx(ne,{onClick:C,children:"Refresh"})]}),i.status==="loading"?n.jsx(A,{title:"Loading Lambda",description:"Reading local functions and recent invocations."}):null,i.status==="error"?n.jsx(A,{title:"Lambda unavailable",description:i.message,actionLabel:"Retry",onAction:C}):null,i.status==="success"?n.jsx(ry,{status:i.statusPayload}):null]}),n.jsx(ue,{title:"Functions",children:_.length===0?n.jsx(A,{title:"No functions",description:"Functions created via CreateFunction will appear here."}):n.jsx("div",{className:"dynamodb-table-list","aria-label":"Lambda functions",children:_.map($=>n.jsxs("button",{type:"button",className:`dynamodb-table-row${$.FunctionName===u?" active":""}`,"aria-pressed":$.FunctionName===u,onClick:()=>d($.FunctionName),children:[n.jsxs("span",{className:"table-row-top",children:[n.jsx("span",{className:"table-row-name",children:$.FunctionName}),n.jsx("span",{className:"count-pill",children:$.Runtime})]}),n.jsx("span",{className:"table-row-meta",children:$.Handler}),n.jsxs("span",{className:"table-row-tags",children:[n.jsxs("span",{children:[$.MemorySize," MB"]}),n.jsxs("span",{children:[$.Timeout,"s"]}),n.jsx("span",{children:$.State})]})]},$.FunctionArn))})}),E?n.jsxs(ue,{title:`Function: ${E.FunctionName}`,children:[n.jsx(ay,{fn:E}),n.jsxs("form",{className:"pubsub-action-form stacked",onSubmit:V,children:[n.jsxs("label",{className:"compact-filter wide",children:[n.jsx("span",{children:"Test event (JSON)"}),n.jsx("textarea",{"aria-label":"Lambda test event JSON",onChange:$=>x($.target.value),rows:4,value:p})]}),n.jsx(ne,{type:"submit",disabled:J.status==="running",children:J.status==="running"?"Invoking…":"Invoke"})]}),n.jsx(iy,{state:J})]}):null,n.jsx(ue,{title:"Recent invocations",children:I.length===0?n.jsx(A,{title:"No invocations yet",description:"Invoke a function to see its result and log tail here."}):n.jsx("div",{className:"dynamodb-table-list","aria-label":"Lambda invocations",children:I.map($=>n.jsxs("details",{className:"dynamodb-table-row",children:[n.jsxs("summary",{className:"table-row-top",children:[n.jsx("span",{className:"table-row-name",children:$.functionName}),n.jsx("span",{className:"count-pill",children:$.status})]}),n.jsxs("span",{className:"table-row-meta",children:[$.startedAt," · ",$.invocationType," · ",$.durationMs.toFixed(1)," ms"]}),n.jsx("pre",{className:"redis-value-pre",children:$.logTail||"(no log output)"})]},$.requestId))})})]})}function ry({status:r}){return n.jsxs("dl",{className:"inspector-list",children:[n.jsxs("div",{children:[n.jsx("dt",{children:"Endpoint"}),n.jsx("dd",{children:n.jsx("code",{children:r.endpoint})})]}),n.jsxs("div",{children:[n.jsx("dt",{children:"Region"}),n.jsx("dd",{children:r.region})]}),n.jsxs("div",{children:[n.jsx("dt",{children:"Storage path"}),n.jsx("dd",{children:n.jsx("code",{children:r.storagePath})})]}),n.jsxs("div",{children:[n.jsx("dt",{children:"Functions"}),n.jsx("dd",{children:r.functionCount})]}),n.jsxs("div",{children:[n.jsx("dt",{children:"Recorded invocations"}),n.jsx("dd",{children:r.invocationCount})]})]})}function ay({fn:r}){var l;const i=Object.keys(((l=r.Environment)==null?void 0:l.Variables)??{});return n.jsxs("dl",{className:"inspector-list",children:[n.jsxs("div",{children:[n.jsx("dt",{children:"ARN"}),n.jsx("dd",{children:n.jsx("code",{children:r.FunctionArn})})]}),n.jsxs("div",{children:[n.jsx("dt",{children:"Role"}),n.jsx("dd",{children:n.jsx("code",{children:r.Role})})]}),n.jsxs("div",{children:[n.jsx("dt",{children:"Code"}),n.jsxs("dd",{children:[r.CodeSize," bytes · ",n.jsx("code",{children:r.CodeSha256})]})]}),n.jsxs("div",{children:[n.jsx("dt",{children:"Last modified"}),n.jsx("dd",{children:r.LastModified})]}),n.jsxs("div",{children:[n.jsx("dt",{children:"Environment keys"}),n.jsx("dd",{children:i.length>0?i.join(", "):"none"})]})]})}function iy({state:r}){if(r.status==="idle"||r.status==="running")return null;if(r.status==="error")return n.jsx(A,{title:"Invoke failed",description:r.message});const{result:i}=r;return n.jsxs("div",{"aria-label":"Lambda invoke result",children:[n.jsx("p",{className:"inspector-muted",children:i.functionError?`Function error: ${i.functionError}`:`Succeeded (HTTP ${i.statusCode})`}),n.jsx("pre",{className:"redis-value-pre",children:i.payload}),n.jsx("pre",{className:"redis-value-pre",children:i.log})]})}async function ly(){return q("/api/cloudrun/status")}async function oy(){return q("/api/cloudrun/services")}async function cy(){return q("/api/cloudrun/instances")}function rd(r){const[,i,,l,,u]=r.split("/");return[i,l,u].map(encodeURIComponent).join("/")}async function uy(r){return q(`/api/cloudrun/services/${rd(r)}/revisions`)}async function dy(r){return q(`/api/cloudrun/services/${rd(r)}/logs`)}function Fa(r){return r.split("/").pop()??r}function fy({service:r}){const[i,l]=m.useState({status:"loading"}),[u,d]=m.useState(),[p,x]=m.useState(),g=(r==null?void 0:r.status)==="disabled",S=ht(),v=m.useCallback(()=>{const I=S();g||Promise.all([ly(),oy(),cy()]).then(([E,J,de])=>{I()&&(l({status:"success",statusPayload:E,services:J.services,instances:de.instances}),d(V=>{var $;return V&&J.services.some(oe=>oe.name===V)?V:($=J.services[0])==null?void 0:$.name}))}).catch(E=>{I()&&l({status:"error",message:E.message})})},[S,g]);if(m.useEffect(()=>{v()},[v]),m.useEffect(()=>{if(!u||g){x(void 0);return}let I=!1;return Promise.all([uy(u),dy(u)]).then(([E,J])=>{I||x({service:u,revisions:E.revisions,logs:J.lines})}).catch(()=>{I||x({service:u,revisions:[],logs:[]})}),()=>{I=!0}},[u,g,i]),Vt({topics:["cloudrun"],onEvent:v,onResync:v,enabled:!g}),g)return n.jsx(ue,{title:"Cloud Run",children:n.jsx(A,{title:"Cloud Run is disabled",description:"Enable the Cloud Run service in devcloud config to deploy and run local services."})});const N=i.status==="success"?i.services:[],w=i.status==="success"?i.instances:[],L=N.find(I=>I.name===u),C=w.find(I=>I.service===u),_=p&&p.service===u?p:void 0;return n.jsxs("div",{className:"dynamodb-workspace",children:[n.jsxs(ue,{title:"Status",children:[n.jsxs("div",{className:"dynamodb-toolbar",children:[n.jsx("span",{className:"toolbar-count",children:i.status==="success"?`${i.statusPayload.status} / ${i.statusPayload.project} / ${i.statusPayload.region}`:"Loading"}),n.jsx(ne,{onClick:v,children:"Refresh"})]}),i.status==="loading"?n.jsx(A,{title:"Loading Cloud Run",description:"Reading local services and running instances."}):null,i.status==="error"?n.jsx(A,{title:"Cloud Run unavailable",description:i.message,actionLabel:"Retry",onAction:v}):null,i.status==="success"?n.jsx(py,{status:i.statusPayload}):null]}),n.jsx(ue,{title:"Services",children:N.length===0?n.jsx(A,{title:"No services",description:"Services created via the Cloud Run Admin API v2 will appear here."}):n.jsx("div",{className:"dynamodb-table-list","aria-label":"Cloud Run services",children:N.map(I=>{const E=w.some(J=>J.service===I.name);return n.jsxs("button",{type:"button",className:`dynamodb-table-row${I.name===u?" active":""}`,"aria-pressed":I.name===u,onClick:()=>d(I.name),children:[n.jsxs("span",{className:"table-row-top",children:[n.jsx("span",{className:"table-row-name",children:Fa(I.name)}),n.jsx("span",{className:"count-pill",children:E?"running":"idle"})]}),n.jsx("span",{className:"table-row-meta",children:I.name.split("/services/")[0]}),n.jsxs("span",{className:"table-row-tags",children:[n.jsxs("span",{children:["gen ",I.generation]}),n.jsx("span",{children:Fa(I.latestReadyRevision)})]})]},I.name)})})}),L?n.jsxs(ue,{title:`Service: ${Fa(L.name)}`,children:[n.jsx(my,{svc:L,instance:C}),n.jsx("h3",{className:"inspector-heading",children:"Revisions"}),_&&_.revisions.length>0?n.jsx("div",{className:"dynamodb-table-list","aria-label":"Cloud Run revisions",children:_.revisions.map(I=>{var E;return n.jsxs("section",{className:"dynamodb-table-row",children:[n.jsxs("span",{className:"table-row-top",children:[n.jsx("span",{className:"table-row-name",children:Fa(I.name)}),I.name===L.latestReadyRevision?n.jsx("span",{className:"count-pill",children:"latest"}):null]}),n.jsx("span",{className:"table-row-meta",children:I.createTime}),n.jsx("span",{className:"table-row-tags",children:n.jsx("span",{children:(E=I.containers[0])==null?void 0:E.image})})]},I.name)})}):n.jsx("p",{className:"inspector-muted",children:_?"No revisions.":"Loading revisions…"}),n.jsx("h3",{className:"inspector-heading",children:"Instance logs"}),n.jsx("pre",{className:"redis-value-pre","aria-label":"Cloud Run instance logs",children:_&&_.logs.length>0?_.logs.join(` +`):"(no output yet — send a request to start the instance)"})]}):null]})}function py({status:r}){return n.jsxs("dl",{className:"inspector-list",children:[n.jsxs("div",{children:[n.jsx("dt",{children:"Endpoint"}),n.jsx("dd",{children:n.jsx("code",{children:r.endpoint})})]}),n.jsxs("div",{children:[n.jsx("dt",{children:"Default project / region"}),n.jsxs("dd",{children:[r.project," / ",r.region]})]}),n.jsxs("div",{children:[n.jsx("dt",{children:"Storage path"}),n.jsx("dd",{children:n.jsx("code",{children:r.storagePath})})]}),n.jsxs("div",{children:[n.jsx("dt",{children:"Services"}),n.jsx("dd",{children:r.serviceCount})]}),n.jsxs("div",{children:[n.jsx("dt",{children:"Running instances"}),n.jsx("dd",{children:r.instanceCount})]})]})}function my({svc:r,instance:i}){var u;const l=r.template.containers[0];return n.jsxs("dl",{className:"inspector-list",children:[(r.urls??[r.uri]).map(d=>n.jsxs("div",{children:[n.jsx("dt",{children:"URL"}),n.jsx("dd",{children:n.jsx("a",{href:d,target:"_blank",rel:"noreferrer",children:n.jsx("code",{children:d})})})]},d)),n.jsxs("div",{children:[n.jsx("dt",{children:"Image"}),n.jsx("dd",{children:n.jsx("code",{children:l==null?void 0:l.image})})]}),n.jsxs("div",{children:[n.jsx("dt",{children:"Command"}),n.jsx("dd",{children:n.jsx("code",{children:(u=l==null?void 0:l.command)!=null&&u.length?[...l.command,...l.args??[]].join(" "):"(image entrypoint)"})})]}),n.jsxs("div",{children:[n.jsx("dt",{children:"Ingress"}),n.jsx("dd",{children:r.ingress})]}),n.jsxs("div",{children:[n.jsx("dt",{children:"Instance"}),n.jsx("dd",{children:i?`${i.mode} · port ${i.port}${i.pid?` · pid ${i.pid}`:""} · ${i.requestCount} requests`:"not running (starts on first request)"})]})]})}function hy({services:r,path:i}){return i==="/mail"?n.jsx($p,{service:r.find(l=>l.id==="mail")}):i==="/s3"?n.jsx(km,{service:r.find(l=>l.id==="s3")}):i==="/gcs"?n.jsx(Eg,{service:r.find(l=>l.id==="gcs")}):i==="/dynamodb"?n.jsx(gh,{service:r.find(l=>l.id==="dynamodb")}):i==="/bigquery"?n.jsx(Vh,{service:r.find(l=>l.id==="bigquery")}):i==="/redshift"?n.jsx(Yh,{service:r.find(l=>l.id==="redshift")}):i==="/redis"?n.jsx(dx,{service:r.find(l=>l.id==="redis")}):i==="/sqs"?n.jsx(sg,{service:r.find(l=>l.id==="sqs")}):i==="/pubsub"?n.jsx(fg,{service:r.find(l=>l.id==="pubsub")}):i==="/applicationautoscaling"?n.jsx(Fg,{service:r.find(l=>l.id==="applicationautoscaling")}):i==="/ssm"?n.jsx(Hg,{service:r.find(l=>l.id==="ssm")}):i==="/sts"?n.jsx(Xg,{service:r.find(l=>l.id==="sts")}):i==="/lambda"?n.jsx(sy,{service:r.find(l=>l.id==="lambda")}):i==="/cloudrun"?n.jsx(fy,{service:r.find(l=>l.id==="cloudrun")}):n.jsx(jp,{services:r})}function xy({services:r}){const i=Er(window.location.pathname);return n.jsxs("nav",{className:"service-switcher","aria-label":"Services",children:[n.jsx("a",{"aria-current":i==="/"?"page":void 0,className:i==="/"?"active":void 0,href:za("/"),children:"Services"}),r.map(l=>n.jsxs("a",{"aria-current":i===Er(l.path)?"page":void 0,"aria-label":`${l.name}: ${l.status}`,className:i===Er(l.path)?"active":void 0,href:za(l.path),children:[n.jsx("span",{children:l.name}),n.jsxs("span",{className:`switcher-status ${gy(l.status)}`,children:[n.jsx("span",{className:"status-dot"}),l.status]})]},l.id))]})}function gy(r){return r==="running"?"running":"disabled"}function yy({services:r}){const i=r.filter(d=>d.status==="running").length,l=`${i}/${r.length} running`,u=r.length>0&&i===r.length?"running":"attention";return n.jsxs("div",{className:`status-pill ${u}`,"aria-label":`Service status: ${l}`,children:[n.jsx("span",{className:"status-dot"}),l]})}function jy({services:r}){const i=Er(window.location.pathname),l=r.find(p=>p.path===i),[u,d]=m.useState(()=>fp());return m.useEffect(()=>pp(d),[]),n.jsxs("footer",{className:"activity-footer",children:[n.jsxs("span",{children:["Last request: ",(u==null?void 0:u.path)??"/api/dashboard/services"]}),n.jsxs("span",{children:["Active: ",(l==null?void 0:l.name)??"Service index"]}),n.jsxs("span",{children:["API status: ",n.jsx(vy,{activity:u,fallback:(l==null?void 0:l.status)??"running"})]}),n.jsxs("span",{children:["Storage: ",(l==null?void 0:l.storagePath)??"registry"]})]})}function vy({activity:r,fallback:i}){if(!r)return n.jsx("span",{children:i});const l=r.status==="success"&&r.statusCode?`${r.status} ${r.statusCode}`:r.status;return n.jsx("span",{className:`activity-status ${r.status}`,title:r.message,children:l})}function by({notifications:r}){const{permission:i,enabled:l,setEnabled:u,requestPermission:d}=r;return i==="unsupported"?null:i==="default"?n.jsx(ne,{className:"notifications-toggle",onClick:()=>{d()},title:"Show desktop notifications when local services emit events",children:"Enable notifications"}):i==="denied"?n.jsx(ne,{className:"notifications-toggle is-disabled",disabled:!0,title:"Notifications are blocked. Allow them in your browser site settings to enable.",children:"Notifications blocked"}):n.jsx(ne,{"aria-pressed":l,className:l?"notifications-toggle is-on":"notifications-toggle",onClick:()=>u(!l),title:l?"Click to mute desktop notifications":"Click to enable desktop notifications",children:l?"Notifications: on":"Notifications: off"})}const ad="devcloud.notifications.enabled";function Fu(){return typeof window>"u"||typeof Notification>"u"?"unsupported":Notification.permission}function Sy(){if(typeof window>"u")return!1;try{return window.localStorage.getItem(ad)==="1"}catch{return!1}}function Ny(r){if(!(typeof window>"u"))try{window.localStorage.setItem(ad,r?"1":"0")}catch{}}function wy(){const[r,i]=m.useState(()=>Fu()),[l,u]=m.useState(()=>Sy()),d=m.useCallback(x=>{u(x),Ny(x)},[]),p=m.useCallback(async()=>{if(typeof Notification>"u")return"unsupported";try{const g=await Notification.requestPermission();return i(g),g==="granted"&&d(!0),g}catch{return Fu()}},[d]);return{permission:r,enabled:l,setEnabled:d,requestPermission:p}}const ky={mail:"Mail",s3:"S3",gcs:"GCS",redis:"Redis",dynamodb:"DynamoDB",bigquery:"BigQuery",sqs:"SQS",pubsub:"Pub/Sub",redshift:"Redshift"};function Cy(r){return ky[r]??r}function qs(r,i){const l=r==null?void 0:r[i];if(typeof l=="string")return l;if(typeof l=="number"&&Number.isFinite(l))return String(l)}function id(r,i){const l=r==null?void 0:r[i];return Array.isArray(l)?l.filter(u=>typeof u=="string"):[]}function ld(r){return r.map(i=>Dn(i)).filter(i=>i.length>0).join(", ")}function Ry(r){if(r===void 0)return;const i=Number(r);if(!(!Number.isFinite(i)||i<0))return i<1024?`${i} B`:i<1024*1024?`${(i/1024).toFixed(1)} KB`:i<1024*1024*1024?`${(i/1024/1024).toFixed(1)} MB`:`${(i/1024/1024/1024).toFixed(2)} GB`}const Dy=180;function Iy(r){return r.replace(/<(script|style)[\s\S]*?<\/\1>/gi," ").replace(/<[^>]+>/g," ").replace(/ /gi," ").replace(/&/gi,"&").replace(/</gi,"<").replace(/>/gi,">").replace(/"/gi,'"').replace(/'/gi,"'").replace(/\s+/g," ").trim()}function Ey(r,i){const l=r.replace(/\r/g,"").replace(/\n{3,}/g,` -`).trim();return i.length<=l?i:`${i.slice(0,l).trimEnd()}…`}function od(r){const l=qs(r.payload,"from")??"(unknown sender)",i=qs(r.payload,"subject")??"",u=Dn(l)||l,d=id(ld(r.payload,"to")),p=ms(i).trim()||"(no subject)",g=qs(r.payload,"messageID"),y=[`From: ${u}`];return d&&y.push(`To: ${d}`),{title:p,body:y.join(` -`),tag:`mail.received:${g??p}`}}async function wy(r){var u,d;if(typeof Notification>"u")return;const l=qs(r.payload,"messageID");let i=od(r);if(l)try{const p=await Dp(l),g=ms(p.subject??"").trim()||"(no subject)",y=((u=p.from)==null?void 0:u.trim())||qs(r.payload,"from")||"(unknown sender)",S=Dn(y)||y,v=id(p.to??ld(r.payload,"to")),N=((d=p.textBody)==null?void 0:d.trim())||Sy(p.htmlBody??""),w=N?Ny(N,by):"",L=v?`From: ${S} -To: ${v}`:`From: ${S}`;i={title:g,body:w?`${L} +`).trim();return l.length<=i?l:`${l.slice(0,i).trimEnd()}…`}function od(r){const i=qs(r.payload,"from")??"(unknown sender)",l=qs(r.payload,"subject")??"",u=Dn(i)||i,d=ld(id(r.payload,"to")),p=ms(l).trim()||"(no subject)",x=qs(r.payload,"messageID"),g=[`From: ${u}`];return d&&g.push(`To: ${d}`),{title:p,body:g.join(` +`),tag:`mail.received:${x??p}`}}async function Ty(r){var u,d;if(typeof Notification>"u")return;const i=qs(r.payload,"messageID");let l=od(r);if(i)try{const p=await Dp(i),x=ms(p.subject??"").trim()||"(no subject)",g=((u=p.from)==null?void 0:u.trim())||qs(r.payload,"from")||"(unknown sender)",S=Dn(g)||g,v=ld(p.to??id(r.payload,"to")),N=((d=p.textBody)==null?void 0:d.trim())||Iy(p.htmlBody??""),w=N?Ey(N,Dy):"",L=v?`From: ${S} +To: ${v}`:`From: ${S}`;l={title:x,body:w?`${L} -${w}`:L,tag:`mail.received:${l}`}}catch{}try{new Notification(i.title,{body:i.body,tag:i.tag})}catch{}}function ky(r){const l=r.payload,i=d=>qs(l,d),u=jy(r.service);switch(r.type){case"mail.received":return od(r);case"mail.deleted":return{title:"Mail message deleted",body:"A message was removed from the inbox.",tag:`mail.deleted:${i("messageID")??r.timestamp}`};case"mail.cleared":return{title:"Mail inbox cleared",body:"All messages were removed.",tag:"mail.cleared"};case"s3.object.put":case"gcs.object.put":{const d=i("bucket")??"?",p=i("key")??"?",g=vy(i("contentLength")),y=g?`${d}/${p} -${g}`:`${d}/${p}`;return{title:`${u} object uploaded`,body:y,tag:`${r.type}:${d}/${p}`}}case"s3.object.deleted":case"gcs.object.deleted":{const d=i("bucket")??"?",p=i("key")??"?";return{title:`${u} object deleted`,body:`${d}/${p}`,tag:`${r.type}:${d}/${p}`}}case"s3.bucket.created":case"gcs.bucket.created":{const d=i("bucket")??"?";return{title:`${u} bucket created`,body:d,tag:`${r.type}:${d}`}}case"s3.bucket.deleted":case"gcs.bucket.deleted":{const d=i("bucket")??"?";return{title:`${u} bucket deleted`,body:d,tag:`${r.type}:${d}`}}case"dynamodb.table.created":case"dynamodb.table.deleted":{const d=i("table")??"?";return{title:`DynamoDB table ${r.type.endsWith(".created")?"created":"deleted"}`,body:d,tag:`${r.type}:${d}`}}case"dynamodb.item.put":case"dynamodb.item.updated":case"dynamodb.item.deleted":{const d=i("table")??"?";return{title:`DynamoDB item ${r.type.split(".").pop()??"changed"}`,body:`Table: ${d}`,tag:`${r.type}:${d}`}}case"bigquery.dataset.created":case"bigquery.dataset.deleted":{const d=i("project")??"?",p=i("dataset")??"?";return{title:`BigQuery dataset ${r.type.endsWith(".created")?"created":"deleted"}`,body:`${d}.${p}`,tag:`${r.type}:${d}.${p}`}}case"bigquery.table.created":case"bigquery.table.deleted":{const d=i("project")??"?",p=i("dataset")??"?",g=i("table")??"?";return{title:`BigQuery table ${r.type.endsWith(".created")?"created":"deleted"}`,body:`${d}.${p}.${g}`,tag:`${r.type}:${d}.${p}.${g}`}}case"bigquery.job.inserted":{const d=i("project")??"?",p=i("jobType")??"job";return{title:`BigQuery ${p} job started`,body:`Project: ${d}`,tag:`bigquery.job.inserted:${d}:${p}:${r.timestamp}`}}case"sqs.message.sent":case"sqs.message.deleted":{const d=i("queue")??"?",p=i("count"),g=r.type.endsWith(".sent")?"sent":"deleted",y=p?`Queue: ${d} (${p} message${p==="1"?"":"s"})`:`Queue: ${d}`;return{title:`SQS message ${g}`,body:y,tag:`${r.type}:${d}:${r.timestamp}`}}case"pubsub.topic.created":case"pubsub.topic.deleted":{const d=i("topic")??"?";return{title:`Pub/Sub topic ${r.type.endsWith(".created")?"created":"deleted"}`,body:d,tag:`${r.type}:${d}`}}case"pubsub.subscription.created":{const d=i("subscription")??"?",p=i("topic")??"?";return{title:"Pub/Sub subscription created",body:`${d} -on topic ${p}`,tag:`${r.type}:${d}`}}case"pubsub.message.published":{const d=i("topic")??"?",p=i("count")??"1";return{title:"Pub/Sub message published",body:`${p} message${p==="1"?"":"s"} → ${d}`,tag:`${r.type}:${d}:${r.timestamp}`}}case"pubsub.message.pulled":{const d=i("subscription")??"?";return{title:"Pub/Sub messages pulled",body:`${i("count")??"0"} from ${d}`,tag:`${r.type}:${d}:${r.timestamp}`}}case"redshift.statement.executed":case"redshift.statement.batch_executed":{const d=i("statementID")??"?";return{title:`Redshift ${(r.type.endsWith(".batch_executed")?"Batch statement":"Statement").toLowerCase()} executed`,body:`ID: ${d}`,tag:`${r.type}:${d}`}}case"redis.command.mutation":{const d=i("command")??"?",p=i("key");return{title:`Redis ${d}`,body:p?`Key: ${p}`:"Mutation executed",tag:`redis.command.mutation:${d}:${p??""}:${r.timestamp}`}}default:return{title:`${u} event`,body:r.type,tag:`${r.type}:${r.timestamp}`}}}function Cy({enabled:r,permission:l,suppressWhileVisible:i=!1}){const u=r&&l==="granted",d=m.useCallback(p=>{if(typeof Notification>"u"||i&&typeof document<"u"&&document.visibilityState==="visible")return;if(p.type==="mail.received"){wy(p);return}const g=ky(p);if(g)try{new Notification(g.title,{body:g.body,tag:g.tag})}catch{}},[i]);Vt({topics:[],onEvent:d,enabled:u})}function Ry({services:r,children:l}){const i=xy();return Cy({enabled:i.enabled,permission:i.permission}),n.jsxs("div",{className:"app-shell",children:[n.jsxs("header",{className:"top-bar",children:[n.jsxs("div",{className:"brand-block",children:[n.jsx("a",{className:"brand-title",href:za("/"),children:"devcloud"}),n.jsx(dy,{services:r})]}),n.jsxs("div",{className:"top-actions",children:[n.jsx(my,{notifications:i}),n.jsx(cy,{services:r}),n.jsx(ae,{onClick:()=>window.location.reload(),children:"Refresh"})]})]}),n.jsx("main",{className:"service-surface",children:l}),n.jsx(fy,{services:r})]})}class Dy extends m.Component{constructor(){super(...arguments);fs(this,"state",{})}static getDerivedStateFromError(i){return{error:i}}render(){return this.state.error?n.jsx(F,{title:"Dashboard request failed",description:this.state.error.message,actionLabel:"Retry",onAction:()=>{yp(),this.setState({error:void 0})}}):this.props.children}}function Iy(){const{services:r}=xp(),l=Er(window.location.pathname);return n.jsx(Ry,{services:r,children:oy({services:r,path:l})})}function Ey(){return n.jsx(Dy,{children:n.jsx(m.Suspense,{fallback:n.jsx(F,{title:"Loading dashboard",description:"Checking local service status."}),children:n.jsx(vp,{children:n.jsx(Iy,{})})})})}dp.createRoot(document.getElementById("root")).render(n.jsx(rp.StrictMode,{children:n.jsx(Ey,{})})); +${w}`:L,tag:`mail.received:${i}`}}catch{}try{new Notification(l.title,{body:l.body,tag:l.tag})}catch{}}function Py(r){const i=r.payload,l=d=>qs(i,d),u=Cy(r.service);switch(r.type){case"mail.received":return od(r);case"mail.deleted":return{title:"Mail message deleted",body:"A message was removed from the inbox.",tag:`mail.deleted:${l("messageID")??r.timestamp}`};case"mail.cleared":return{title:"Mail inbox cleared",body:"All messages were removed.",tag:"mail.cleared"};case"s3.object.put":case"gcs.object.put":{const d=l("bucket")??"?",p=l("key")??"?",x=Ry(l("contentLength")),g=x?`${d}/${p} +${x}`:`${d}/${p}`;return{title:`${u} object uploaded`,body:g,tag:`${r.type}:${d}/${p}`}}case"s3.object.deleted":case"gcs.object.deleted":{const d=l("bucket")??"?",p=l("key")??"?";return{title:`${u} object deleted`,body:`${d}/${p}`,tag:`${r.type}:${d}/${p}`}}case"s3.bucket.created":case"gcs.bucket.created":{const d=l("bucket")??"?";return{title:`${u} bucket created`,body:d,tag:`${r.type}:${d}`}}case"s3.bucket.deleted":case"gcs.bucket.deleted":{const d=l("bucket")??"?";return{title:`${u} bucket deleted`,body:d,tag:`${r.type}:${d}`}}case"dynamodb.table.created":case"dynamodb.table.deleted":{const d=l("table")??"?";return{title:`DynamoDB table ${r.type.endsWith(".created")?"created":"deleted"}`,body:d,tag:`${r.type}:${d}`}}case"dynamodb.item.put":case"dynamodb.item.updated":case"dynamodb.item.deleted":{const d=l("table")??"?";return{title:`DynamoDB item ${r.type.split(".").pop()??"changed"}`,body:`Table: ${d}`,tag:`${r.type}:${d}`}}case"bigquery.dataset.created":case"bigquery.dataset.deleted":{const d=l("project")??"?",p=l("dataset")??"?";return{title:`BigQuery dataset ${r.type.endsWith(".created")?"created":"deleted"}`,body:`${d}.${p}`,tag:`${r.type}:${d}.${p}`}}case"bigquery.table.created":case"bigquery.table.deleted":{const d=l("project")??"?",p=l("dataset")??"?",x=l("table")??"?";return{title:`BigQuery table ${r.type.endsWith(".created")?"created":"deleted"}`,body:`${d}.${p}.${x}`,tag:`${r.type}:${d}.${p}.${x}`}}case"bigquery.job.inserted":{const d=l("project")??"?",p=l("jobType")??"job";return{title:`BigQuery ${p} job started`,body:`Project: ${d}`,tag:`bigquery.job.inserted:${d}:${p}:${r.timestamp}`}}case"sqs.message.sent":case"sqs.message.deleted":{const d=l("queue")??"?",p=l("count"),x=r.type.endsWith(".sent")?"sent":"deleted",g=p?`Queue: ${d} (${p} message${p==="1"?"":"s"})`:`Queue: ${d}`;return{title:`SQS message ${x}`,body:g,tag:`${r.type}:${d}:${r.timestamp}`}}case"pubsub.topic.created":case"pubsub.topic.deleted":{const d=l("topic")??"?";return{title:`Pub/Sub topic ${r.type.endsWith(".created")?"created":"deleted"}`,body:d,tag:`${r.type}:${d}`}}case"pubsub.subscription.created":{const d=l("subscription")??"?",p=l("topic")??"?";return{title:"Pub/Sub subscription created",body:`${d} +on topic ${p}`,tag:`${r.type}:${d}`}}case"pubsub.message.published":{const d=l("topic")??"?",p=l("count")??"1";return{title:"Pub/Sub message published",body:`${p} message${p==="1"?"":"s"} → ${d}`,tag:`${r.type}:${d}:${r.timestamp}`}}case"pubsub.message.pulled":{const d=l("subscription")??"?";return{title:"Pub/Sub messages pulled",body:`${l("count")??"0"} from ${d}`,tag:`${r.type}:${d}:${r.timestamp}`}}case"redshift.statement.executed":case"redshift.statement.batch_executed":{const d=l("statementID")??"?";return{title:`Redshift ${(r.type.endsWith(".batch_executed")?"Batch statement":"Statement").toLowerCase()} executed`,body:`ID: ${d}`,tag:`${r.type}:${d}`}}case"redis.command.mutation":{const d=l("command")??"?",p=l("key");return{title:`Redis ${d}`,body:p?`Key: ${p}`:"Mutation executed",tag:`redis.command.mutation:${d}:${p??""}:${r.timestamp}`}}default:return{title:`${u} event`,body:r.type,tag:`${r.type}:${r.timestamp}`}}}function Ly({enabled:r,permission:i,suppressWhileVisible:l=!1}){const u=r&&i==="granted",d=m.useCallback(p=>{if(typeof Notification>"u"||l&&typeof document<"u"&&document.visibilityState==="visible")return;if(p.type==="mail.received"){Ty(p);return}const x=Py(p);if(x)try{new Notification(x.title,{body:x.body,tag:x.tag})}catch{}},[l]);Vt({topics:[],onEvent:d,enabled:u})}function $y({services:r,children:i}){const l=wy();return Ly({enabled:l.enabled,permission:l.permission}),n.jsxs("div",{className:"app-shell",children:[n.jsxs("header",{className:"top-bar",children:[n.jsxs("div",{className:"brand-block",children:[n.jsx("a",{className:"brand-title",href:za("/"),children:"devcloud"}),n.jsx(yy,{services:r})]}),n.jsxs("div",{className:"top-actions",children:[n.jsx(by,{notifications:l}),n.jsx(xy,{services:r}),n.jsx(ne,{onClick:()=>window.location.reload(),children:"Refresh"})]})]}),n.jsx("main",{className:"service-surface",children:i}),n.jsx(jy,{services:r})]})}class My extends m.Component{constructor(){super(...arguments);fs(this,"state",{})}static getDerivedStateFromError(l){return{error:l}}render(){return this.state.error?n.jsx(A,{title:"Dashboard request failed",description:this.state.error.message,actionLabel:"Retry",onAction:()=>{yp(),this.setState({error:void 0})}}):this.props.children}}function Oy(){const{services:r}=gp(),i=Er(window.location.pathname);return n.jsx($y,{services:r,children:hy({services:r,path:i})})}function Ay(){return n.jsx(My,{children:n.jsx(m.Suspense,{fallback:n.jsx(A,{title:"Loading dashboard",description:"Checking local service status."}),children:n.jsx(vp,{children:n.jsx(Oy,{})})})})}dp.createRoot(document.getElementById("root")).render(n.jsx(rp.StrictMode,{children:n.jsx(Ay,{})})); diff --git a/services/dashboard/assets/react/index.html b/services/dashboard/assets/react/index.html index 305ad52..46a4e0f 100644 --- a/services/dashboard/assets/react/index.html +++ b/services/dashboard/assets/react/index.html @@ -4,7 +4,7 @@ devcloud Dashboard - + diff --git a/services/dashboard/src/config.rs b/services/dashboard/src/config.rs index 20a204d..caa15f6 100644 --- a/services/dashboard/src/config.rs +++ b/services/dashboard/src/config.rs @@ -81,6 +81,15 @@ pub struct Config { pub app_auto_scaling_storage_path: String, pub app_auto_scaling_region: String, + pub ssm_base: String, + pub ssm_endpoint: String, + pub ssm_storage_path: String, + pub ssm_region: String, + + pub sts_base: String, + pub sts_endpoint: String, + pub sts_region: String, + pub lambda_base: String, pub lambda_endpoint: String, pub lambda_storage_path: String, @@ -190,6 +199,15 @@ impl Config { "us-east-1", ), + ssm_base: env("DEVCLOUD_DASHBOARD_SSM_BASE"), + ssm_endpoint: env_or("DEVCLOUD_DASHBOARD_SSM_ENDPOINT", "http://127.0.0.1:18031"), + ssm_storage_path: env_or("DEVCLOUD_DASHBOARD_SSM_STORAGE", ".devcloud/data/ssm"), + ssm_region: env_or("DEVCLOUD_DASHBOARD_SSM_REGION", "us-east-1"), + + sts_base: env("DEVCLOUD_DASHBOARD_STS_BASE"), + sts_endpoint: env_or("DEVCLOUD_DASHBOARD_STS_ENDPOINT", "http://127.0.0.1:18032"), + sts_region: env_or("DEVCLOUD_DASHBOARD_STS_REGION", "us-east-1"), + lambda_base: env("DEVCLOUD_DASHBOARD_LAMBDA_BASE"), lambda_endpoint: env_or( "DEVCLOUD_DASHBOARD_LAMBDA_ENDPOINT", diff --git a/services/dashboard/src/http.rs b/services/dashboard/src/http.rs index 076b2db..d356d53 100644 --- a/services/dashboard/src/http.rs +++ b/services/dashboard/src/http.rs @@ -20,7 +20,7 @@ use tokio::net::{TcpListener, TcpStream}; use crate::config::Config; use crate::{ applicationautoscaling, assets, bigquery, cloudrun, dynamodb, events, gcs, lambda, mail, - pubsub, redis, redshift, s3, services, sqs, + pubsub, redis, redshift, s3, services, sqs, ssm, sts, }; const MAX_HEADER_BYTES: usize = 64 * 1024; @@ -336,6 +336,20 @@ pub async fn route(config: &Config, req: &Request) -> Response { return pubsub::handle_message(config, req).await; } + // SSM / STS. + if path == "/api/ssm/status" { + return ssm::handle_status(config, req).await; + } + if path == "/api/ssm/parameters" { + return ssm::handle_parameters(config, req).await; + } + if path == "/api/sts/status" { + return sts::handle_status(config, req).await; + } + if path == "/api/sts/caller-identity" { + return sts::handle_caller_identity(config, req).await; + } + // Application Auto Scaling. if path == "/api/applicationautoscaling/status" { return applicationautoscaling::handle_status(config, req).await; diff --git a/services/dashboard/src/lib.rs b/services/dashboard/src/lib.rs index 230d0d9..dc0ec4b 100644 --- a/services/dashboard/src/lib.rs +++ b/services/dashboard/src/lib.rs @@ -36,5 +36,7 @@ pub mod s3; pub mod services; pub mod sigv4; pub mod sqs; +pub mod ssm; +pub mod sts; pub use config::Config; diff --git a/services/dashboard/src/services.rs b/services/dashboard/src/services.rs index ec1f2d6..9fe7fce 100644 --- a/services/dashboard/src/services.rs +++ b/services/dashboard/src/services.rs @@ -157,6 +157,26 @@ fn build(c: &Config) -> Vec { description: "Inspect local Application Auto Scaling targets, policies, and scheduled actions.", }, + DashboardService { + id: "ssm", + name: "SSM Parameter Store", + path: "/dashboard/ssm", + status: status(!c.ssm_base.is_empty()), + endpoint: c.ssm_endpoint.clone(), + storage_path: c.ssm_storage_path.clone(), + description: + "Inspect local SSM parameter names, types, and versions (values stay hidden).", + }, + DashboardService { + id: "sts", + name: "STS", + path: "/dashboard/sts", + status: status(!c.sts_base.is_empty()), + endpoint: c.sts_endpoint.clone(), + storage_path: String::new(), + description: + "Check the local STS endpoint that issues temporary credentials for AssumeRole.", + }, DashboardService { id: "lambda", name: "Lambda", @@ -230,6 +250,8 @@ mod tests { "sqs", "pubsub", "applicationautoscaling", + "ssm", + "sts", "lambda", "cloudrun" ] diff --git a/services/dashboard/src/ssm.rs b/services/dashboard/src/ssm.rs new file mode 100644 index 0000000..bb713d8 --- /dev/null +++ b/services/dashboard/src/ssm.rs @@ -0,0 +1,153 @@ +//! SSM Parameter Store dashboard handler — read-only, over the service's AWS +//! JSON 1.1 provider protocol (`services/ssm`). Only `DescribeParameters` is +//! issued, so parameter values (including `SecureString` plaintext) never +//! reach the dashboard. + +use serde_json::{json, Value}; + +use crate::config::Config; +use crate::forward::{forward, ForwardError, ForwardRequest, ForwardResponse}; +use crate::http::{Request, Response}; + +/// Upper bound on `DescribeParameters` pages fetched for one listing (50 each). +const MAX_PAGES: usize = 40; + +/// `GET /api/ssm/status`. +pub async fn handle_status(config: &Config, req: &Request) -> Response { + if req.method != "GET" { + return Response::method_not_allowed("GET"); + } + let running = !config.ssm_base.is_empty(); + let parameter_count = if running { + list_parameters(config).await.map(|p| p.len()).unwrap_or(0) + } else { + 0 + }; + Response::json( + 200, + &json!({ + "service": "ssm", + "status": if running { "running" } else { "disabled" }, + "running": running, + "endpoint": config.ssm_endpoint, + "region": config.ssm_region, + "storagePath": config.ssm_storage_path, + "parameterCount": parameter_count, + }), + ) +} + +/// `GET /api/ssm/parameters` -> `{parameters}` (metadata only). +pub async fn handle_parameters(config: &Config, req: &Request) -> Response { + if req.method != "GET" { + return Response::method_not_allowed("GET"); + } + if config.ssm_base.is_empty() { + return Response::text_error(503, "ssm service is disabled"); + } + match list_parameters(config).await { + Ok(parameters) => Response::json(200, &json!({ "parameters": parameters })), + Err(resp) => resp, + } +} + +async fn list_parameters(config: &Config) -> Result, Response> { + let mut out = Vec::new(); + let mut next_token = String::new(); + for _ in 0..MAX_PAGES { + let mut body = json!({ "MaxResults": 50 }); + if !next_token.is_empty() { + body["NextToken"] = json!(next_token); + } + let page = describe(config, body).await?; + if let Some(items) = page.get("Parameters").and_then(Value::as_array) { + out.extend(items.iter().cloned()); + } + match page.get("NextToken").and_then(Value::as_str) { + Some(token) if !token.is_empty() => next_token = token.to_string(), + _ => break, + } + } + Ok(out) +} + +async fn describe(config: &Config, body: Value) -> Result { + match forward(ForwardRequest { + base: &config.ssm_base, + method: "POST", + path: "/", + headers: vec![ + ( + "Content-Type".to_string(), + "application/x-amz-json-1.1".to_string(), + ), + ( + "X-Amz-Target".to_string(), + "AmazonSSM.DescribeParameters".to_string(), + ), + ], + body: serde_json::to_vec(&body).unwrap_or_default(), + }) + .await + { + Ok(resp) if resp.status == 200 => serde_json::from_slice(&resp.body) + .map_err(|_| Response::text_error(502, "ssm service returned invalid json")), + Ok(resp) => Err(relay(resp)), + Err(e) => Err(forward_failure(e)), + } +} + +fn relay(resp: ForwardResponse) -> Response { + let ct = resp.header("content-type"); + let content_type = if ct.is_empty() { + "application/json" + } else { + ct + } + .to_string(); + Response::new(resp.status, &content_type, resp.body) +} + +fn forward_failure(err: ForwardError) -> Response { + match err { + ForwardError::Unreachable(_) => Response::text_error(502, "ssm service is unreachable"), + ForwardError::BadBase => Response::text_error(500, "ssm service address is misconfigured"), + ForwardError::BadResponse => { + Response::text_error(502, "ssm service returned an invalid response") + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + fn req(method: &str, path: &str) -> Request { + Request { + method: method.to_string(), + path: path.to_string(), + raw_path: path.to_string(), + query: String::new(), + headers: std::collections::HashMap::new(), + body: Vec::new(), + } + } + + #[tokio::test] + async fn disabled_service() { + let cfg = Config::default(); + let resp = handle_status(&cfg, &req("GET", "/api/ssm/status")).await; + assert_eq!(resp.status, 200); + let v: Value = serde_json::from_slice(&resp.body).unwrap(); + assert_eq!(v["status"], "disabled"); + let resp = handle_parameters(&cfg, &req("GET", "/api/ssm/parameters")).await; + assert_eq!(resp.status, 503); + } + + #[tokio::test] + async fn rejects_non_get() { + let cfg = Config::default(); + let resp = handle_parameters(&cfg, &req("POST", "/api/ssm/parameters")).await; + assert_eq!(resp.status, 405); + } +} diff --git a/services/dashboard/src/sts.rs b/services/dashboard/src/sts.rs new file mode 100644 index 0000000..75dd36d --- /dev/null +++ b/services/dashboard/src/sts.rs @@ -0,0 +1,119 @@ +//! STS dashboard handler — read-only, over the service's AWS Query provider +//! protocol (`services/sts`). Only `GetCallerIdentity` is issued; the +//! dashboard never calls `AssumeRole`, so it never handles credentials. + +use serde_json::json; + +use crate::config::Config; +use crate::forward::{forward, ForwardError, ForwardRequest}; +use crate::http::{Request, Response}; + +/// `GET /api/sts/status`. +pub async fn handle_status(config: &Config, req: &Request) -> Response { + if req.method != "GET" { + return Response::method_not_allowed("GET"); + } + let running = !config.sts_base.is_empty(); + Response::json( + 200, + &json!({ + "service": "sts", + "status": if running { "running" } else { "disabled" }, + "running": running, + "endpoint": config.sts_endpoint, + "region": config.sts_region, + }), + ) +} + +/// `GET /api/sts/caller-identity` -> `{account, arn, userId}`. +pub async fn handle_caller_identity(config: &Config, req: &Request) -> Response { + if req.method != "GET" { + return Response::method_not_allowed("GET"); + } + if config.sts_base.is_empty() { + return Response::text_error(503, "sts service is disabled"); + } + let result = forward(ForwardRequest { + base: &config.sts_base, + method: "POST", + path: "/", + headers: vec![( + "Content-Type".to_string(), + "application/x-www-form-urlencoded".to_string(), + )], + body: b"Action=GetCallerIdentity&Version=2011-06-15".to_vec(), + }) + .await; + match result { + Ok(resp) if resp.status == 200 => { + let xml = String::from_utf8_lossy(&resp.body); + Response::json( + 200, + &json!({ + "account": tag(&xml, "Account"), + "arn": tag(&xml, "Arn"), + "userId": tag(&xml, "UserId"), + }), + ) + } + Ok(resp) => { + Response::text_error(502, &format!("sts service returned status {}", resp.status)) + } + Err(ForwardError::Unreachable(_)) => { + Response::text_error(502, "sts service is unreachable") + } + Err(ForwardError::BadBase) => { + Response::text_error(500, "sts service address is misconfigured") + } + Err(ForwardError::BadResponse) => { + Response::text_error(502, "sts service returned an invalid response") + } + } +} + +fn tag(xml: &str, name: &str) -> String { + let open = format!("<{name}>"); + let close = format!(""); + xml.find(&open) + .map(|i| i + open.len()) + .and_then(|start| { + xml[start..] + .find(&close) + .map(|end| xml[start..start + end].to_string()) + }) + .unwrap_or_default() +} + +#[cfg(test)] +mod tests { + use super::*; + + fn req(method: &str, path: &str) -> Request { + Request { + method: method.to_string(), + path: path.to_string(), + raw_path: path.to_string(), + query: String::new(), + headers: std::collections::HashMap::new(), + body: Vec::new(), + } + } + + #[tokio::test] + async fn disabled_service() { + let cfg = Config::default(); + let resp = handle_status(&cfg, &req("GET", "/api/sts/status")).await; + let v: serde_json::Value = serde_json::from_slice(&resp.body).unwrap(); + assert_eq!(v["status"], "disabled"); + let resp = handle_caller_identity(&cfg, &req("GET", "/api/sts/caller-identity")).await; + assert_eq!(resp.status, 503); + } + + #[test] + fn tag_extraction() { + let xml = "123arn:x"; + assert_eq!(tag(xml, "Account"), "123"); + assert_eq!(tag(xml, "Missing"), ""); + } +} diff --git a/services/lambda/src/lib.rs b/services/lambda/src/lib.rs index 3133627..2d9e07b 100644 --- a/services/lambda/src/lib.rs +++ b/services/lambda/src/lib.rs @@ -1,8 +1,9 @@ //! Local AWS Lambda emulator for devcloud. //! //! Serves the Lambda REST API (`/2015-03-31/functions`, invoke, tags, account -//! settings) and executes `python3.x` / `nodejs*` handlers from zip deployment -//! packages as local child processes. Function state persists to `state.json` +//! settings) and executes `python3.x` / `nodejs*` handlers and `provided*` +//! `bootstrap` executables (through a local Lambda Runtime API) from zip +//! deployment packages as local child processes. Function state persists to `state.json` //! under the configured storage path; packages may come inline (`ZipFile`) or //! from the shared local S3 store (`S3Bucket`/`S3Key`). @@ -29,6 +30,7 @@ pub mod container; mod function_url; pub mod http; pub mod runtime; +mod runtime_api; pub mod server; pub mod sigv4; pub mod time_fmt; diff --git a/services/lambda/src/runtime.rs b/services/lambda/src/runtime.rs index 8e7e2ae..1b12704 100644 --- a/services/lambda/src/runtime.rs +++ b/services/lambda/src/runtime.rs @@ -11,6 +11,10 @@ //! variables, and the function's own `Environment.Variables`), so host //! credentials never leak into handler code; the only credentials a handler //! sees are the ones configured for functions ([`FunctionCredentials`]). +//! +//! `provided*` (custom runtime) functions run the package's own `bootstrap` +//! executable instead, talking to a per-environment Lambda Runtime API +//! ([`crate::runtime_api`]) that speaks the same marker/result-file protocol. use std::collections::{BTreeMap, HashMap, HashSet}; use std::path::{Path, PathBuf}; @@ -24,6 +28,7 @@ use tokio::io::{AsyncReadExt, AsyncWriteExt}; use tokio::sync::mpsc; use crate::container::{self, ImageSpec}; +use crate::runtime_api::{ApiInvocation, RuntimeApi}; /// Per-stream log bytes kept while an invocation runs (the API tail is 4 KiB). const LOG_CAPTURE_BYTES: usize = 64 * 1024; @@ -42,6 +47,8 @@ const NODE_BOOTSTRAP: &str = include_str!("bootstrap/node.js"); pub enum Family { Python, Node, + /// `provided*`: the package's `bootstrap` executable is the runtime. + Provided, } /// Maps a Lambda `Runtime` identifier (`python3.12`, `nodejs20.x`) to the @@ -51,6 +58,8 @@ pub fn family(runtime: &str) -> Option { Some(Family::Python) } else if runtime.starts_with("nodejs") { Some(Family::Node) + } else if runtime.starts_with("provided") { + Some(Family::Provided) } else { None } @@ -115,6 +124,7 @@ impl Interpreters { versioned.or_else(|| resolve_program(&self.python)) } Family::Node => resolve_program(&self.node), + Family::Provided => None, } } } @@ -478,6 +488,8 @@ struct Environment { control: Option, /// Set when the process is a `docker run` of an image function. container: Option, + /// Set for a `provided*` function: the bootstrap polls it for requests. + runtime_api: Option, events: mpsc::UnboundedReceiver, stdout: Arc>, stderr: Arc>>, @@ -684,7 +696,14 @@ pub async fn run( Some(env) => (env, None), None => { let started = Instant::now(); - let mut env = spawn(family, inv, interpreters, pool, code)?; + let mut env = if family == Family::Provided { + match spawn_provided(inv, pool, code).await? { + Ok(env) => env, + Err(failed) => return Ok(failed), + } + } else { + spawn(family, inv, interpreters, pool, code)? + }; // Init gets its own budget of one function timeout. match env.wait(tokio::time::Instant::now() + timeout).await { Wait::Marker { tag, log } if tag == "ready" => { @@ -693,7 +712,7 @@ pub async fn run( } (env, Some((started.elapsed(), log))) } - other => return Ok(failed_init(inv, other, started.elapsed())), + other => return Ok(failed_init(inv, other, started.elapsed(), &env.work_dir)), } } }; @@ -717,8 +736,16 @@ pub async fn run( } let started = Instant::now(); let deadline = tokio::time::Instant::now() + timeout; - // A dead environment fails the write; `wait` then reports how it ended. - if let Some(control) = env.control.as_mut() { + if let Some(api) = env.runtime_api.as_ref() { + api.submit(ApiInvocation { + request_id: inv.request_id.clone(), + deadline_ms: deadline_ms as u64, + function_arn: inv.function_arn.clone(), + event: inv.payload.clone(), + result_path: result_path.clone(), + }); + } else if let Some(control) = env.control.as_mut() { + // A dead environment fails the write; `wait` then reports how it ended. let _ = tokio::time::timeout_at(deadline, control.write_all(&line)).await; } let waited = env.wait(deadline).await; @@ -782,8 +809,14 @@ pub async fn run( } /// The invocation's result when the environment never became ready. -fn failed_init(inv: &Invocation, waited: Wait, init: Duration) -> Completed { +fn failed_init(inv: &Invocation, waited: Wait, init: Duration, work_dir: &Path) -> Completed { let (outcome, log, timed_out) = match waited { + // A custom runtime reported its init failure through the Runtime API. + Wait::Marker { tag, log } if tag == "initerror" => ( + read_result(&work_dir.join(INIT_ERROR_FILE), &inv.request_id), + log, + false, + ), Wait::TimedOut { log, .. } => (timed_out_error(inv), log, true), Wait::Exited { crashed, log } => ( Outcome::FunctionError(error_doc( @@ -849,6 +882,7 @@ fn spawn( let bin = match family { Family::Python => &interpreters.python, Family::Node => &interpreters.node, + Family::Provided => unreachable!("provided runtimes start through spawn_provided"), }; // Resolve the interpreter against devcloud's own PATH: the function may set // its own PATH, which applies to the handler, not to finding python/node. @@ -865,6 +899,7 @@ fn spawn( // reach the log (a pipe would otherwise be block-buffered). Family::Python => cmd.arg("-u").arg("-c").arg(PYTHON_BOOTSTRAP), Family::Node => cmd.arg("-e").arg(NODE_BOOTSTRAP), + Family::Provided => unreachable!("provided runtimes start through spawn_provided"), }; // Precedence: devcloud defaults < the function's Environment.Variables < // Lambda-reserved variables (which CreateFunction refuses to accept). @@ -929,6 +964,7 @@ fn spawn( child, control: Some(control), container: None, + runtime_api: None, events, stdout, stderr, @@ -939,6 +975,241 @@ fn spawn( }) } +/// Where a custom runtime's `POST /runtime/init/error` is recorded, in the +/// environment's work directory. +pub(crate) const INIT_ERROR_FILE: &str = "init-error.json"; + +/// Custom runtimes rely on Unix pipes and process groups. +#[cfg(not(unix))] +async fn spawn_provided( + _inv: &Invocation, + _pool: &Pool, + _code: Box, +) -> Result, RuntimeError> { + Err(RuntimeError::Spawn( + "custom runtimes (provided*) need a Unix host".to_string(), + )) +} + +/// Starts a `provided*` function's `bootstrap` with its own Runtime API. +/// `Err(Completed)` is a failed invocation (no usable bootstrap), reported to +/// the caller as a function error the way Lambda does. +#[cfg(unix)] +async fn spawn_provided( + inv: &Invocation, + pool: &Pool, + code: Box, +) -> Result, RuntimeError> { + let started = Instant::now(); + let candidates = [ + inv.code_dir.join("bootstrap"), + inv.opt_dir.join("bootstrap"), + ]; + let Some(bootstrap) = candidates.iter().find(|p| p.is_file()) else { + let searched: Vec = candidates + .iter() + .map(|p| p.to_string_lossy().into_owned()) + .collect(); + return Ok(Err(invalid_entrypoint( + inv, + &format!("Couldn't find valid bootstrap(s): [{}]", searched.join(" ")), + "", + started.elapsed(), + ))); + }; + + let id = pool.seq.fetch_add(1, Ordering::Relaxed); + let work_dir = pool.work_root.join(format!("env-{id}")); + std::fs::create_dir_all(&work_dir).map_err(|e| RuntimeError::Spawn(e.to_string()))?; + let cleanup = |msg: String| { + let _ = std::fs::remove_dir_all(&work_dir); + RuntimeError::Spawn(msg) + }; + let marker = new_marker(id); + + // The bootstrap's stdout and stderr share one pipe; devcloud keeps a write + // end of it so the Runtime API can append marker lines behind the + // bootstrap's own output. + let (read, write) = new_pipe().map_err(|e| cleanup(format!("create output pipe: {e}")))?; + let child_out = write + .try_clone() + .map_err(|e| cleanup(format!("create output pipe: {e}")))?; + let api = RuntimeApi::start( + marker.clone(), + std::fs::File::from(write), + work_dir.join(INIT_ERROR_FILE), + serde_json::json!({ + "functionName": inv.function_name, + "functionVersion": "$LATEST", + "handler": inv.handler, + }), + ) + .await + .map_err(|e| cleanup(format!("start runtime api: {e}")))?; + + let mut env_vars = reserved_env(inv, id); + // Lambda sets no AWS_EXECUTION_ENV for custom runtimes, and the + // _DEVCLOUD_* variables are for devcloud's own bootstraps only. + env_vars.retain(|(k, _)| k != "AWS_EXECUTION_ENV" && !k.starts_with("_DEVCLOUD_")); + env_vars.push(("AWS_LAMBDA_RUNTIME_API".into(), api.addr.clone())); + + let mut cmd = tokio::process::Command::new(bootstrap); + cmd.env_clear() + .envs(default_env(Family::Provided, inv)) + .envs(&inv.environment) + .envs(env_vars) + .current_dir(&inv.code_dir) + .stdin(Stdio::null()) + .stdout(Stdio::from(child_out)) + .stderr(Stdio::null()) + .kill_on_drop(true); + cmd.process_group(0); + // SAFETY: dup2 is async-signal-safe; fd 1 is the output pipe by now. + unsafe { + cmd.pre_exec(|| { + if libc::dup2(1, 2) < 0 { + return Err(std::io::Error::last_os_error()); + } + Ok(()) + }); + } + // A binary for another OS would otherwise be handed to /bin/sh by + // execvp's ENOEXEC fallback and fail as an obscure shell error. + let spawned = match foreign_executable(bootstrap) { + Some(format) => Err(( + std::io::Error::from_raw_os_error(libc::ENOEXEC), + format!(" ({format})"), + )), + None => cmd.spawn().map_err(|e| (e, String::new())), + }; + let child = match spawned { + Ok(child) => child, + Err((e, format)) => { + let _ = std::fs::remove_dir_all(&work_dir); + let mut hint = String::new(); + if e.raw_os_error() == Some(libc::ENOEXEC) { + hint = format!( + "[ERROR] devcloud: {} is not an executable for this host{format}; build the bootstrap for {}/{} (Go: GOOS={} GOARCH={})\n", + bootstrap.display(), + std::env::consts::OS, + std::env::consts::ARCH, + go_os(), + go_arch(), + ); + } + return Ok(Err(invalid_entrypoint( + inv, + &format!("fork/exec {}: {}", bootstrap.display(), os_error_text(&e)), + &hint, + started.elapsed(), + ))); + } + }; + drop(cmd); + + let reader = tokio::net::unix::pipe::Receiver::from_owned_fd(read) + .map_err(|e| cleanup(format!("read output pipe: {e}")))?; + let (tx, events) = mpsc::unbounded_channel(); + let stdout = Arc::new(Mutex::new(Scanner::new(format!("{marker} ").into_bytes()))); + let out_task = tokio::spawn(scan(reader, Arc::clone(&stdout), tx)); + Ok(Ok(Environment { + id, + key: inv.env_key.clone(), + pgid: child.id(), + child, + control: None, + container: None, + runtime_api: Some(api), + events, + stdout, + stderr: Arc::new(Mutex::new(Vec::new())), + tasks: vec![out_task.abort_handle()], + work_dir, + idle_since: Instant::now(), + _code: code, + })) +} + +/// Lambda's answer when a custom runtime has no runnable bootstrap. +#[cfg(unix)] +fn invalid_entrypoint(inv: &Invocation, reason: &str, log: &str, init: Duration) -> Completed { + Completed { + outcome: Outcome::FunctionError(error_doc( + &format!("RequestId: {} Error: {reason}", inv.request_id), + "Runtime.InvalidEntrypoint", + )), + log: frame_log( + inv, + Some((init, log.to_string())), + "", + Duration::ZERO, + false, + None, + ), + duration: Duration::ZERO, + init_duration: Some(init), + } +} + +/// An OS error without Rust's ` (os error N)` suffix, as Go prints it. +#[cfg(unix)] +fn os_error_text(e: &std::io::Error) -> String { + match e.raw_os_error() { + Some(libc::ENOEXEC) => "exec format error".to_string(), + Some(libc::EACCES) => "permission denied".to_string(), + _ => { + let text = e.to_string(); + match text.find(" (os error") { + Some(i) => text[..i].to_lowercase(), + None => text, + } + } + } +} + +/// Names the executable format of `path` when it is one this host's OS +/// cannot run: an ELF binary off Linux, or a Mach-O binary off macOS. +#[cfg(unix)] +fn foreign_executable(path: &Path) -> Option<&'static str> { + let mut magic = [0u8; 4]; + use std::io::Read; + std::fs::File::open(path) + .ok()? + .read_exact(&mut magic) + .ok()?; + let elf = magic == [0x7f, b'E', b'L', b'F']; + let mach_o = matches!( + u32::from_be_bytes(magic), + 0xfeedface | 0xfeedfacf | 0xcefaedfe | 0xcffaedfe | 0xcafebabe + ); + if elf && std::env::consts::OS != "linux" { + Some("a Linux ELF binary") + } else if mach_o && std::env::consts::OS != "macos" { + Some("a macOS Mach-O binary") + } else { + None + } +} + +/// `std::env::consts::OS` in Go's `GOOS` spelling. +#[cfg(unix)] +fn go_os() -> &'static str { + match std::env::consts::OS { + "macos" => "darwin", + other => other, + } +} + +/// `std::env::consts::ARCH` in Go's `GOARCH` spelling. +#[cfg(unix)] +fn go_arch() -> &'static str { + match std::env::consts::ARCH { + "x86_64" => "amd64", + "aarch64" => "arm64", + other => other, + } +} + /// [`run`] for a container image function: the environment is a `docker run` /// of the image, invoked through the RIE inside it. async fn run_container( @@ -1157,6 +1428,7 @@ async fn start_container( name, removals: Arc::clone(&pool.removals), }), + runtime_api: None, events, stdout, stderr, @@ -1378,7 +1650,33 @@ const CONTROL_FD: i32 = 3; /// reads its stdin would consume them (or block forever waiting for EOF). #[cfg(unix)] fn attach_control_pipe(cmd: &mut tokio::process::Command) -> std::io::Result { - use std::os::fd::{AsRawFd, FromRawFd, OwnedFd}; + use std::os::fd::AsRawFd; + let (read, write) = new_pipe()?; + let read_fd = read.as_raw_fd(); + // SAFETY: the closure only calls async-signal-safe functions (dup2, + // fcntl) between fork and exec. `read` outlives the spawn: it moves into + // the closure, which `cmd` keeps until it is dropped. + unsafe { + cmd.pre_exec(move || { + let _keep = &read; + if read_fd == CONTROL_FD { + // dup2 onto itself would keep FD_CLOEXEC set. + if libc::fcntl(read_fd, libc::F_SETFD, 0) != 0 { + return Err(std::io::Error::last_os_error()); + } + } else if libc::dup2(read_fd, CONTROL_FD) < 0 { + return Err(std::io::Error::last_os_error()); + } + Ok(()) + }); + } + tokio::net::unix::pipe::Sender::from_owned_fd(write) +} + +/// A new pipe `(read, write)` with `FD_CLOEXEC` set on both ends. +#[cfg(unix)] +fn new_pipe() -> std::io::Result<(std::os::fd::OwnedFd, std::os::fd::OwnedFd)> { + use std::os::fd::{FromRawFd, OwnedFd}; let mut fds = [0 as libc::c_int; 2]; // SAFETY: `fds` is a valid two-element buffer; on success both // descriptors are new and owned here. FD_CLOEXEC keeps them out of other @@ -1396,28 +1694,11 @@ fn attach_control_pipe(cmd: &mut tokio::process::Command) -> std::io::Result Vec<(String, String)> { let opt = inv.opt_dir.to_string_lossy(); let v = version(&inv.runtime); let layer_path = match (family, v) { + (Family::Provided, _) => return env, (Family::Python, Some(v)) => ( "PYTHONPATH", format!("{opt}/python/lib/python{v}/site-packages:{opt}/python"), @@ -1784,7 +2066,9 @@ mod tests { assert_eq!(family("python3.12"), Some(Family::Python)); assert_eq!(family("nodejs20.x"), Some(Family::Node)); assert_eq!(family("java21"), None); - assert_eq!(family("provided.al2023"), None); + assert_eq!(family("provided.al2023"), Some(Family::Provided)); + assert_eq!(family("provided"), Some(Family::Provided)); + assert_eq!(family("go1.x"), None); } #[test] diff --git a/services/lambda/src/runtime_api.rs b/services/lambda/src/runtime_api.rs new file mode 100644 index 0000000..aabde16 --- /dev/null +++ b/services/lambda/src/runtime_api.rs @@ -0,0 +1,609 @@ +//! The Lambda Runtime API (`AWS_LAMBDA_RUNTIME_API`) for `provided*` runtimes. +//! +//! Each execution environment of a custom-runtime function gets its own +//! server on a loopback port; the function's `bootstrap` (for example a Go +//! binary built with `aws-lambda-go`) polls it for invocations and posts the +//! outcome back: +//! +//! - `GET /2018-06-01/runtime/invocation/next` — long-polls for the next event +//! - `POST /2018-06-01/runtime/invocation/{id}/response` +//! - `POST /2018-06-01/runtime/invocation/{id}/error` +//! - `POST /2018-06-01/runtime/init/error` +//! +//! Extensions can register (`POST /2020-01-01/extension/register`) so that +//! runtime clients which register an internal extension for SIGTERM handling +//! (`aws-lambda-go`'s `WithEnableSIGTERM`) start cleanly, but no lifecycle +//! events are delivered: `GET /2020-01-01/extension/event/next` never +//! answers, and a stopped environment is killed without a SIGTERM. +//! +//! The server speaks the same protocol to [`crate::runtime`] as the embedded +//! Python/Node bootstraps: an outcome is written to the request's result file +//! and a ` ` line is written into the environment's output pipe. +//! The bootstrap's own output went into that pipe before it made the HTTP +//! call, so the marker lands after it and the log is split at the right place. + +use std::collections::HashMap; +use std::io::Write; +use std::path::PathBuf; +use std::sync::atomic::{AtomicBool, Ordering}; +use std::sync::{Arc, Mutex}; + +use base64::Engine; +use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt}; +use tokio::net::TcpListener; +use tokio::sync::mpsc; + +const API_PREFIX: &str = "/2018-06-01/runtime/"; +const EXTENSION_PREFIX: &str = "/2020-01-01/extension/"; +const MAX_HEADER_BYTES: usize = 64 * 1024; +/// Lambda's 6 MB response limit, with room for an error document. +const MAX_BODY_BYTES: usize = 7 * 1024 * 1024; + +/// One invocation handed to the bootstrap. +pub(crate) struct ApiInvocation { + pub request_id: String, + pub deadline_ms: u64, + pub function_arn: String, + pub event: Vec, + /// Where the outcome is written, in the bootstrap result format. + pub result_path: PathBuf, +} + +struct State { + marker_prefix: String, + /// devcloud's own write end of the environment's output pipe. + marker_out: Mutex, + init_error_path: PathBuf, + queue: tokio::sync::Mutex>, + /// Request id → result file of invocations handed out and not yet answered. + in_flight: Mutex>, + ready_sent: AtomicBool, + /// Body of an extension registration response. + extension_info: serde_json::Value, +} + +/// A running Runtime API server; dropping it stops the server. +pub(crate) struct RuntimeApi { + pub addr: String, + submit: mpsc::UnboundedSender, + task: tokio::task::AbortHandle, +} + +impl Drop for RuntimeApi { + fn drop(&mut self) { + self.task.abort(); + } +} + +impl RuntimeApi { + /// Binds a loopback port and starts serving. `marker_out` is a write end + /// of the pipe the bootstrap's stdout/stderr go to. + pub async fn start( + marker_prefix: String, + marker_out: std::fs::File, + init_error_path: PathBuf, + extension_info: serde_json::Value, + ) -> std::io::Result { + let listener = TcpListener::bind("127.0.0.1:0").await?; + let addr = listener.local_addr()?.to_string(); + let (submit, queue) = mpsc::unbounded_channel(); + let state = Arc::new(State { + marker_prefix, + marker_out: Mutex::new(marker_out), + init_error_path, + queue: tokio::sync::Mutex::new(queue), + in_flight: Mutex::new(HashMap::new()), + ready_sent: AtomicBool::new(false), + extension_info, + }); + let task = tokio::spawn(async move { + let mut connections = tokio::task::JoinSet::new(); + loop { + let Ok((stream, _)) = listener.accept().await else { + continue; + }; + while connections.try_join_next().is_some() {} + let state = Arc::clone(&state); + connections.spawn(async move { + let _ = serve_connection(stream, state).await; + }); + } + }) + .abort_handle(); + Ok(RuntimeApi { addr, submit, task }) + } + + /// Queues an invocation for the bootstrap's next `GET .../next`. + pub fn submit(&self, invocation: ApiInvocation) { + let _ = self.submit.send(invocation); + } +} + +impl State { + /// Writes a marker line after everything already in the output pipe. + async fn marker(self: &Arc, tag: &str) { + let state = Arc::clone(self); + let line = format!("{} {tag}\n", self.marker_prefix); + let _ = tokio::task::spawn_blocking(move || { + let mut out = state.marker_out.lock().unwrap(); + let _ = out.write_all(line.as_bytes()); + }) + .await; + } +} + +struct Request { + method: String, + path: String, + headers: HashMap, + trailers: HashMap, + body: Vec, + close: bool, +} + +struct Response { + status: u16, + headers: Vec<(&'static str, String)>, + body: Vec, +} + +impl Response { + fn json(status: u16, value: serde_json::Value) -> Self { + Response { + status, + headers: Vec::new(), + body: serde_json::to_vec(&value).unwrap_or_default(), + } + } + + fn accepted() -> Self { + Response::json(202, serde_json::json!({ "status": "OK" })) + } + + fn error(status: u16, error_type: &str, message: &str) -> Self { + Response::json( + status, + serde_json::json!({ "errorType": error_type, "errorMessage": message }), + ) + } +} + +async fn serve_connection( + mut stream: tokio::net::TcpStream, + state: Arc, +) -> std::io::Result<()> { + let mut buf = Vec::new(); + loop { + let Some(req) = read_request(&mut stream, &mut buf).await? else { + return Ok(()); + }; + let close = req.close; + let resp = handle(&state, req).await; + write_response(&mut stream, resp).await?; + if close { + return Ok(()); + } + } +} + +async fn handle(state: &Arc, req: Request) -> Response { + if let Some(route) = req.path.strip_prefix(EXTENSION_PREFIX) { + return match (req.method.as_str(), route) { + ("POST", "register") => { + let name = req + .headers + .get("lambda-extension-name") + .cloned() + .unwrap_or_default(); + let mut resp = Response::json(200, state.extension_info.clone()); + resp.headers + .push(("Lambda-Extension-Identifier", format!("devcloud-{name}"))); + resp + } + // No lifecycle events are emitted; the poll lasts as long as + // the environment. + ("GET", "event/next") => std::future::pending().await, + _ => Response::error(404, "NotFound", "unknown path"), + }; + } + let Some(route) = req.path.strip_prefix(API_PREFIX) else { + return Response::error(404, "NotFound", "unknown path"); + }; + match (req.method.as_str(), route) { + ("GET", "invocation/next") => next_invocation(state).await, + ("POST", "init/error") => { + let doc = error_document(&req); + let _ = write_json(&state.init_error_path, &serde_json::json!({ "error": doc })); + state.marker("initerror").await; + Response::accepted() + } + ("POST", rest) => { + let Some(rest) = rest.strip_prefix("invocation/") else { + return Response::error(404, "NotFound", "unknown path"); + }; + let (request_id, kind) = match rest.rsplit_once('/') { + Some((id, kind @ ("response" | "error"))) => (id, kind), + _ => return Response::error(404, "NotFound", "unknown path"), + }; + let Some(result_path) = state.in_flight.lock().unwrap().remove(request_id) else { + return Response::error(400, "InvalidRequestID", "Invalid request ID"); + }; + let outcome = if kind == "error" + || req + .trailers + .contains_key("lambda-runtime-function-error-type") + { + serde_json::json!({ "error": error_document(&req) }) + } else { + serde_json::json!({ "result": String::from_utf8_lossy(&req.body) }) + }; + let _ = write_json(&result_path, &outcome); + state.marker("done").await; + Response::accepted() + } + _ => Response::error(405, "MethodNotAllowed", "method not allowed"), + } +} + +async fn next_invocation(state: &Arc) -> Response { + // The first poll means init is over. + if !state.ready_sent.swap(true, Ordering::SeqCst) { + state.marker("ready").await; + } + let Some(inv) = state.queue.lock().await.recv().await else { + return Response::error(500, "ServiceException", "runtime api closed"); + }; + state + .in_flight + .lock() + .unwrap() + .insert(inv.request_id.clone(), inv.result_path); + Response { + status: 200, + headers: vec![ + ("Lambda-Runtime-Aws-Request-Id", inv.request_id.clone()), + ("Lambda-Runtime-Deadline-Ms", inv.deadline_ms.to_string()), + ("Lambda-Runtime-Invoked-Function-Arn", inv.function_arn), + ( + "Lambda-Runtime-Trace-Id", + format!( + "Root=1-00000000-{};Sampled=0", + trace_suffix(&inv.request_id) + ), + ), + ], + body: inv.event, + } +} + +/// 24 hex characters for the X-Ray root id, derived from the request id. +fn trace_suffix(request_id: &str) -> String { + let mut hex: String = request_id.chars().filter(char::is_ascii_hexdigit).collect(); + hex.truncate(24); + while hex.len() < 24 { + hex.push('0'); + } + hex +} + +/// The error document a bootstrap reported: the JSON body of an `/error` +/// post, or the `Lambda-Runtime-Function-Error-Body` trailer of a streamed +/// response, with `errorType` falling back to the error-type header. +fn error_document(req: &Request) -> serde_json::Value { + let header_type = req + .trailers + .get("lambda-runtime-function-error-type") + .or_else(|| req.headers.get("lambda-runtime-function-error-type")) + .cloned() + .unwrap_or_default(); + let body = match req.trailers.get("lambda-runtime-function-error-body") { + Some(b64) => base64::engine::general_purpose::STANDARD + .decode(b64.trim()) + .unwrap_or_default(), + None => req.body.clone(), + }; + let mut doc = match serde_json::from_slice::(&body) { + Ok(v @ serde_json::Value::Object(_)) => v, + _ => serde_json::json!({ "errorMessage": String::from_utf8_lossy(&body) }), + }; + let missing_type = doc + .get("errorType") + .and_then(|t| t.as_str()) + .is_none_or(str::is_empty); + if missing_type { + doc["errorType"] = serde_json::json!(if header_type.is_empty() { + "Unhandled".to_string() + } else { + header_type + }); + } + doc +} + +fn write_json(path: &std::path::Path, value: &serde_json::Value) -> std::io::Result<()> { + std::fs::write(path, serde_json::to_vec(value).unwrap_or_default()) +} + +/// Reads one request from a keep-alive connection; `buf` carries bytes read +/// past the previous request. `None` on a clean close or malformed input. +async fn read_request( + stream: &mut S, + buf: &mut Vec, +) -> std::io::Result> { + let head_end = loop { + if let Some(pos) = find(buf, b"\r\n\r\n") { + break pos; + } + if buf.len() > MAX_HEADER_BYTES || !fill(stream, buf).await? { + return Ok(None); + } + }; + let head = String::from_utf8_lossy(&buf[..head_end]).into_owned(); + buf.drain(..head_end + 4); + + let mut lines = head.split("\r\n"); + let mut request_line = lines.next().unwrap_or("").split(' '); + let method = request_line.next().unwrap_or("").to_string(); + let target = request_line.next().unwrap_or("/"); + let path = target.split('?').next().unwrap_or("/").to_string(); + let version = request_line.next().unwrap_or("HTTP/1.1"); + let headers = parse_fields(lines); + + let chunked = headers + .get("transfer-encoding") + .is_some_and(|v| v.to_ascii_lowercase().contains("chunked")); + let mut trailers = HashMap::new(); + let body = if chunked { + match read_chunked(stream, buf, &mut trailers).await? { + Some(b) => b, + None => return Ok(None), + } + } else { + let len: usize = match headers.get("content-length") { + None => 0, + Some(v) => match v.parse() { + Ok(n) if n <= MAX_BODY_BYTES => n, + _ => return Ok(None), + }, + }; + while buf.len() < len { + if !fill(stream, buf).await? { + return Ok(None); + } + } + buf.drain(..len).collect() + }; + let connection = headers.get("connection").map(|v| v.to_ascii_lowercase()); + let close = match connection.as_deref() { + Some("close") => true, + Some("keep-alive") => false, + _ => version == "HTTP/1.0", + }; + Ok(Some(Request { + method, + path, + headers, + trailers, + body, + close, + })) +} + +/// Decodes a chunked body, collecting trailer fields into `trailers`. +async fn read_chunked( + stream: &mut S, + buf: &mut Vec, + trailers: &mut HashMap, +) -> std::io::Result>> { + let mut body = Vec::new(); + loop { + let Some(line) = read_line(stream, buf).await? else { + return Ok(None); + }; + let size_text = line.split(';').next().unwrap_or("").trim(); + let Ok(size) = usize::from_str_radix(size_text, 16) else { + return Ok(None); + }; + if size == 0 { + // Trailer section: fields until an empty line. + loop { + let Some(line) = read_line(stream, buf).await? else { + return Ok(None); + }; + if line.is_empty() { + return Ok(Some(body)); + } + trailers.extend(parse_fields(std::iter::once(line.as_str()))); + } + } + if body.len() + size > MAX_BODY_BYTES { + return Ok(None); + } + while buf.len() < size + 2 { + if !fill(stream, buf).await? { + return Ok(None); + } + } + body.extend_from_slice(&buf[..size]); + if &buf[size..size + 2] != b"\r\n" { + return Ok(None); + } + buf.drain(..size + 2); + } +} + +async fn read_line( + stream: &mut S, + buf: &mut Vec, +) -> std::io::Result> { + loop { + if let Some(pos) = find(buf, b"\r\n") { + let line = String::from_utf8_lossy(&buf[..pos]).into_owned(); + buf.drain(..pos + 2); + return Ok(Some(line)); + } + if buf.len() > MAX_HEADER_BYTES || !fill(stream, buf).await? { + return Ok(None); + } + } +} + +fn parse_fields<'a>(lines: impl Iterator) -> HashMap { + lines + .filter_map(|l| l.split_once(':')) + .map(|(k, v)| (k.trim().to_ascii_lowercase(), v.trim().to_string())) + .collect() +} + +/// Reads more bytes into `buf`; false at end of stream. +async fn fill(stream: &mut S, buf: &mut Vec) -> std::io::Result { + let mut chunk = [0u8; 8192]; + let n = stream.read(&mut chunk).await?; + buf.extend_from_slice(&chunk[..n]); + Ok(n > 0) +} + +fn find(haystack: &[u8], needle: &[u8]) -> Option { + haystack.windows(needle.len()).position(|w| w == needle) +} + +async fn write_response( + stream: &mut S, + resp: Response, +) -> std::io::Result<()> { + let reason = match resp.status { + 200 => "OK", + 202 => "Accepted", + 400 => "Bad Request", + 404 => "Not Found", + 405 => "Method Not Allowed", + _ => "Internal Server Error", + }; + let mut head = format!("HTTP/1.1 {} {reason}\r\n", resp.status); + head.push_str("Content-Type: application/json\r\n"); + for (k, v) in &resp.headers { + head.push_str(&format!("{k}: {v}\r\n")); + } + head.push_str(&format!("Content-Length: {}\r\n\r\n", resp.body.len())); + stream.write_all(head.as_bytes()).await?; + stream.write_all(&resp.body).await?; + stream.flush().await +} + +#[cfg(test)] +mod tests { + use super::*; + + async fn parse(wire: &[u8]) -> Option { + let mut stream = wire; + let mut buf = Vec::new(); + read_request(&mut stream, &mut buf).await.unwrap() + } + + #[tokio::test] + async fn content_length_body() { + let req = parse(b"POST /2018-06-01/runtime/invocation/abc/response HTTP/1.1\r\nContent-Length: 4\r\n\r\n\"ok\"").await.unwrap(); + assert_eq!(req.method, "POST"); + assert_eq!(req.path, "/2018-06-01/runtime/invocation/abc/response"); + assert_eq!(req.body, b"\"ok\""); + assert!(!req.close); + } + + #[tokio::test] + async fn chunked_body_with_trailers() { + let req = parse( + b"POST /x HTTP/1.1\r\nTransfer-Encoding: chunked\r\nTrailer: Lambda-Runtime-Function-Error-Type\r\n\r\n\ + 3\r\nabc\r\n2;ext=1\r\nde\r\n0\r\nLambda-Runtime-Function-Error-Type: Runtime.TruncatedResponse\r\n\r\n", + ) + .await + .unwrap(); + assert_eq!(req.body, b"abcde"); + assert_eq!( + req.trailers + .get("lambda-runtime-function-error-type") + .map(String::as_str), + Some("Runtime.TruncatedResponse") + ); + } + + #[tokio::test] + async fn keep_alive_reads_consecutive_requests() { + let wire: &[u8] = b"GET /a HTTP/1.1\r\n\r\nPOST /b HTTP/1.1\r\nContent-Length: 1\r\nConnection: close\r\n\r\nx"; + let mut stream = wire; + let mut buf = Vec::new(); + let a = read_request(&mut stream, &mut buf).await.unwrap().unwrap(); + let b = read_request(&mut stream, &mut buf).await.unwrap().unwrap(); + assert_eq!((a.path.as_str(), b.path.as_str()), ("/a", "/b")); + assert_eq!(b.body, b"x"); + assert!(b.close); + assert!(read_request(&mut stream, &mut buf).await.unwrap().is_none()); + } + + #[tokio::test] + async fn malformed_chunk_is_rejected() { + assert!( + parse(b"POST /x HTTP/1.1\r\nTransfer-Encoding: chunked\r\n\r\nzz\r\n") + .await + .is_none() + ); + assert!( + parse(b"POST /x HTTP/1.1\r\nTransfer-Encoding: chunked\r\n\r\n3\r\nabcX\r\n") + .await + .is_none() + ); + } + + #[test] + fn error_document_sources() { + let req = |body: &[u8], header: Option<&str>, trailer_body: Option<&str>| Request { + method: "POST".into(), + path: String::new(), + headers: header + .map(|h| { + HashMap::from([( + "lambda-runtime-function-error-type".to_string(), + h.to_string(), + )]) + }) + .unwrap_or_default(), + trailers: trailer_body + .map(|b| { + HashMap::from([( + "lambda-runtime-function-error-body".to_string(), + b.to_string(), + )]) + }) + .unwrap_or_default(), + body: body.to_vec(), + close: false, + }; + let doc = error_document(&req( + br#"{"errorMessage":"boom","errorType":"MyError"}"#, + None, + None, + )); + assert_eq!(doc["errorType"], "MyError"); + let doc = error_document(&req( + br#"{"errorMessage":"boom"}"#, + Some("errorString"), + None, + )); + assert_eq!(doc["errorType"], "errorString"); + let doc = error_document(&req(b"not json", None, None)); + assert_eq!(doc["errorMessage"], "not json"); + assert_eq!(doc["errorType"], "Unhandled"); + let b64 = base64::engine::general_purpose::STANDARD + .encode(br#"{"errorMessage":"cut","errorType":"Runtime.TruncatedResponse"}"#); + let doc = error_document(&req(b"partial", None, Some(&b64))); + assert_eq!(doc["errorMessage"], "cut"); + } + + #[test] + fn trace_suffix_is_24_hex() { + assert_eq!( + trace_suffix("0a1b2c3d-4e5f-6789-abcd-ef0123456789").len(), + 24 + ); + assert_eq!(trace_suffix("x"), "0".repeat(24)); + } +} diff --git a/services/lambda/src/server.rs b/services/lambda/src/server.rs index ef5d2ad..06744ff 100644 --- a/services/lambda/src/server.rs +++ b/services/lambda/src/server.rs @@ -32,7 +32,8 @@ const LOG_TAIL_BYTES: usize = 4096; const MAX_INVOCATION_RECORDS: usize = 100; const DEFAULT_LIST_MAX_ITEMS: usize = 50; -/// Runtime identifiers devcloud accepts. Python/Node execute locally; the +/// Runtime identifiers devcloud accepts. Python/Node and `provided*` (the +/// package's own `bootstrap`) execute locally; the /// remaining families are accepted for control-plane compatibility (IaC tools /// deploy them) but cannot be invoked. const KNOWN_RUNTIME_PREFIXES: &[&str] = &[ diff --git a/services/lambda/tests/provided_runtime_test.rs b/services/lambda/tests/provided_runtime_test.rs new file mode 100644 index 0000000..035a12e --- /dev/null +++ b/services/lambda/tests/provided_runtime_test.rs @@ -0,0 +1,403 @@ +//! `provided*` (custom runtime) functions: the package's `bootstrap` talks to +//! devcloud's Lambda Runtime API. The bootstrap here is a small Python script +//! (skipped when `python3` is absent) that follows the same protocol as +//! `aws-lambda-go` and the other runtime interface clients. + +use std::collections::HashMap; +use std::path::PathBuf; +use std::sync::Arc; +use std::time::Duration; + +use base64::Engine; +use devcloud_lambda::http::{process, Request}; +use devcloud_lambda::zip::build_stored_with_modes; +use devcloud_lambda::{Config, Server}; +use serde_json::{json, Value}; + +const EXECUTABLE: u32 = 0o100755; + +/// A Runtime API client. `event["mode"]` selects the outcome: "error" posts +/// to /error, "sleep" sleeps past the timeout, "exit" exits mid-invocation; +/// anything else echoes the event with a per-process counter. With +/// `INIT_FAIL` set, it reports an init error instead of polling. +const BOOTSTRAP: &str = r#"#!/usr/bin/env python3 +import json, os, sys, time, urllib.request + +api = "http://" + os.environ["AWS_LAMBDA_RUNTIME_API"] + "/2018-06-01/runtime" + +def post(path, body, headers=None): + req = urllib.request.Request(api + path, data=json.dumps(body).encode(), method="POST", headers=headers or {}) + urllib.request.urlopen(req).read() + +print("init ran", flush=True) +if os.environ.get("INIT_FAIL"): + post("/init/error", {"errorMessage": "cannot init", "errorType": "Runtime.InitError"}) + sys.exit(1) + +count = 0 +while True: + with urllib.request.urlopen(api + "/invocation/next") as r: + request_id = r.headers["Lambda-Runtime-Aws-Request-Id"] + deadline = int(r.headers["Lambda-Runtime-Deadline-Ms"]) + arn = r.headers["Lambda-Runtime-Invoked-Function-Arn"] + event = json.loads(r.read() or b"{}") + count += 1 + print("handling", count, event.get("tag"), flush=True) + mode = event.get("mode") + if mode == "error": + post("/invocation/%s/error" % request_id, {"errorMessage": "boom", "errorType": "MyError"}, + {"Lambda-Runtime-Function-Error-Type": "MyError"}) + elif mode == "sleep": + time.sleep(30) + elif mode == "exit": + sys.exit(3) + else: + post("/invocation/%s/response" % request_id, { + "event": event, "count": count, "pid": os.getpid(), "requestId": request_id, + "remainingMs": deadline - int(time.time() * 1000), "arn": arn, + "runtimeApi": os.environ["AWS_LAMBDA_RUNTIME_API"], + "executionEnv": os.environ.get("AWS_EXECUTION_ENV"), + "devcloudVars": [k for k in os.environ if k.startswith("_DEVCLOUD_")], + }) +"#; + +struct Env { + server: Arc, + dir: PathBuf, +} + +impl Drop for Env { + fn drop(&mut self) { + let _ = std::fs::remove_dir_all(&self.dir); + } +} + +fn env(tag: &str) -> Env { + let nanos = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap() + .as_nanos(); + let dir = std::env::temp_dir().join(format!( + "devcloud-lambda-provided-{tag}-{}-{nanos}", + std::process::id() + )); + std::fs::create_dir_all(&dir).unwrap(); + let server = Arc::new(Server::new(Config { + region: "us-east-1".into(), + account_id: "000000000000".into(), + auth_mode: "relaxed".into(), + storage_path: dir.join("lambda").to_string_lossy().into_owned(), + ..Config::default() + })); + Env { server, dir } +} + +fn has_python() -> bool { + std::process::Command::new("python3") + .arg("--version") + .output() + .map(|o| o.status.success()) + .unwrap_or(false) +} + +struct Resp { + status: u16, + headers: Vec<(String, String)>, + body: Vec, +} + +impl Resp { + fn json(&self) -> Value { + serde_json::from_slice(&self.body).unwrap_or(Value::Null) + } + fn header(&self, name: &str) -> Option<&str> { + self.headers + .iter() + .find(|(k, _)| k.eq_ignore_ascii_case(name)) + .map(|(_, v)| v.as_str()) + } + fn log(&self) -> String { + let raw = base64::engine::general_purpose::STANDARD + .decode(self.header("X-Amz-Log-Result").unwrap_or_default()) + .unwrap(); + String::from_utf8(raw).unwrap() + } +} + +async fn call( + server: &Arc, + method: &str, + path: &str, + headers: &[(&str, &str)], + body: &[u8], +) -> Resp { + let req = Request { + method: method.into(), + raw_path: path.into(), + query: String::new(), + headers: headers + .iter() + .map(|(k, v)| (k.to_ascii_lowercase(), v.to_string())) + .collect::>(), + body: body.to_vec(), + }; + let r = process(server, &req).await; + Resp { + status: r.status, + headers: r.headers, + body: r.body, + } +} + +async fn create( + server: &Arc, + name: &str, + files: &[(&str, &[u8], u32)], + timeout: i64, + vars: Value, +) { + let zip = build_stored_with_modes(files); + let body = json!({ + "FunctionName": name, + "Runtime": "provided.al2023", + "Role": "r", + "Handler": "bootstrap", + "Timeout": timeout, + "Environment": { "Variables": vars }, + "Code": { "ZipFile": base64::engine::general_purpose::STANDARD.encode(&zip) }, + }); + let r = call( + server, + "POST", + "/2015-03-31/functions", + &[], + body.to_string().as_bytes(), + ) + .await; + assert_eq!(r.status, 201, "{}", String::from_utf8_lossy(&r.body)); +} + +async fn invoke(server: &Arc, name: &str, payload: Value) -> Resp { + let r = call( + server, + "POST", + &format!("/2015-03-31/functions/{name}/invocations"), + &[("X-Amz-Log-Type", "Tail")], + payload.to_string().as_bytes(), + ) + .await; + assert_eq!(r.status, 200, "{}", String::from_utf8_lossy(&r.body)); + r +} + +#[tokio::test] +async fn bootstrap_serves_invocations_through_the_runtime_api() { + if !has_python() { + eprintln!("skipping: python3 not available"); + return; + } + let e = env("serve"); + create( + &e.server, + "custom", + &[("bootstrap", BOOTSTRAP.as_bytes(), EXECUTABLE)], + 10, + json!({}), + ) + .await; + + let first = invoke(&e.server, "custom", json!({"tag": "one"})).await; + assert!( + first.header("X-Amz-Function-Error").is_none(), + "{}", + first.log() + ); + let a = first.json(); + assert_eq!(a["event"], json!({"tag": "one"})); + assert_eq!(a["count"], 1); + assert_eq!(a["requestId"], first.header("X-Amzn-RequestId").unwrap()); + assert_eq!( + a["arn"], + "arn:aws:lambda:us-east-1:000000000000:function:custom" + ); + assert!(a["remainingMs"].as_i64().unwrap() > 5_000); + assert!(a["runtimeApi"].as_str().unwrap().starts_with("127.0.0.1:")); + assert_eq!(a["executionEnv"], Value::Null); + assert_eq!(a["devcloudVars"], json!([])); + + let log = first.log(); + assert!(log.contains("INIT_START"), "{log}"); + assert!(log.contains("init ran"), "{log}"); + assert!(log.contains("handling 1 one"), "{log}"); + assert!(log.contains("Init Duration"), "{log}"); + + // Warm start: same process, its own slice of the log. + let second = invoke(&e.server, "custom", json!({"tag": "two"})).await; + let b = second.json(); + assert_eq!(b["pid"], a["pid"]); + assert_eq!(b["count"], 2); + let log = second.log(); + assert!(log.contains("handling 2 two"), "{log}"); + assert!(!log.contains("handling 1"), "{log}"); + assert!(!log.contains("INIT_START"), "{log}"); +} + +#[tokio::test] +async fn function_error_keeps_the_environment() { + if !has_python() { + eprintln!("skipping: python3 not available"); + return; + } + let e = env("error"); + create( + &e.server, + "custom", + &[("bootstrap", BOOTSTRAP.as_bytes(), EXECUTABLE)], + 10, + json!({}), + ) + .await; + let failed = invoke(&e.server, "custom", json!({"mode": "error"})).await; + assert_eq!(failed.header("X-Amz-Function-Error"), Some("Unhandled")); + assert_eq!( + failed.json(), + json!({"errorMessage": "boom", "errorType": "MyError"}) + ); + + let ok = invoke(&e.server, "custom", json!({})).await; + assert!(ok.header("X-Amz-Function-Error").is_none()); + assert_eq!( + ok.json()["count"], + 2, + "same environment after a function error" + ); +} + +#[tokio::test] +async fn init_error_is_reported() { + if !has_python() { + eprintln!("skipping: python3 not available"); + return; + } + let e = env("init"); + create( + &e.server, + "custom", + &[("bootstrap", BOOTSTRAP.as_bytes(), EXECUTABLE)], + 10, + json!({"INIT_FAIL": "1"}), + ) + .await; + let r = invoke(&e.server, "custom", json!({})).await; + assert_eq!(r.header("X-Amz-Function-Error"), Some("Unhandled")); + assert_eq!(r.json()["errorType"], "Runtime.InitError"); + assert!(r.log().contains("init ran"), "{}", r.log()); +} + +#[tokio::test] +async fn crash_and_timeout_end_the_environment() { + if !has_python() { + eprintln!("skipping: python3 not available"); + return; + } + let e = env("crash"); + create( + &e.server, + "custom", + &[("bootstrap", BOOTSTRAP.as_bytes(), EXECUTABLE)], + 1, + json!({}), + ) + .await; + + let crashed = invoke(&e.server, "custom", json!({"mode": "exit"})).await; + assert_eq!(crashed.json()["errorType"], "Runtime.ExitError"); + assert!(crashed.json()["errorMessage"] + .as_str() + .unwrap() + .ends_with("exit status 3")); + + let timed_out = invoke(&e.server, "custom", json!({"mode": "sleep"})).await; + assert_eq!(timed_out.json()["errorType"], "Sandbox.Timedout"); + + // Each failure discarded its environment: the next call is a cold start. + let ok = invoke(&e.server, "custom", json!({})).await; + assert_eq!(ok.json()["count"], 1); + assert!(ok.log().contains("INIT_START")); +} + +#[tokio::test] +async fn missing_or_foreign_bootstrap_is_an_invalid_entrypoint() { + let e = env("entrypoint"); + create( + &e.server, + "nobootstrap", + &[("main", b"x", EXECUTABLE)], + 3, + json!({}), + ) + .await; + let r = invoke(&e.server, "nobootstrap", json!({})).await; + assert_eq!(r.header("X-Amz-Function-Error"), Some("Unhandled")); + let doc = r.json(); + assert_eq!(doc["errorType"], "Runtime.InvalidEntrypoint"); + assert!(doc["errorMessage"] + .as_str() + .unwrap() + .contains("Couldn't find valid bootstrap(s)")); + + // A binary for another OS (e.g. a Linux build on macOS). + let garbage: &[u8] = if cfg!(target_os = "linux") { + &[0xcf, 0xfa, 0xed, 0xfe, 0, 0, 0, 0, 1, 2, 3, 4] // Mach-O + } else { + &[0x7f, b'E', b'L', b'F', 0, 0, 0, 0, 1, 2, 3, 4] // ELF + }; + create( + &e.server, + "foreign", + &[("bootstrap", garbage, EXECUTABLE)], + 3, + json!({}), + ) + .await; + let r = invoke(&e.server, "foreign", json!({})).await; + let doc = r.json(); + assert_eq!(doc["errorType"], "Runtime.InvalidEntrypoint"); + assert!( + doc["errorMessage"] + .as_str() + .unwrap() + .contains("exec format error"), + "{doc}" + ); + assert!( + r.log().contains("not an executable for this host"), + "{}", + r.log() + ); +} + +#[tokio::test] +async fn concurrent_invocations_get_separate_environments() { + if !has_python() { + eprintln!("skipping: python3 not available"); + return; + } + let e = env("concurrent"); + create( + &e.server, + "custom", + &[("bootstrap", BOOTSTRAP.as_bytes(), EXECUTABLE)], + 10, + json!({}), + ) + .await; + let (a, b) = tokio::join!( + invoke(&e.server, "custom", json!({"tag": "a"})), + invoke(&e.server, "custom", json!({"tag": "b"})), + ); + assert_eq!(a.json()["event"]["tag"], "a"); + assert_eq!(b.json()["event"]["tag"], "b"); + assert_ne!(a.json()["pid"], b.json()["pid"]); + tokio::time::sleep(Duration::from_millis(10)).await; +} diff --git a/services/redshift/Cargo.toml b/services/redshift/Cargo.toml index 82d1c59..9ae8ed7 100644 --- a/services/redshift/Cargo.toml +++ b/services/redshift/Cargo.toml @@ -14,6 +14,8 @@ path = "src/main.rs" [dependencies] devcloud-s3 = { path = "../s3" } +# gzip COPY sources (pure-Rust backend; miniz_oxide is already in the tree). +flate2 = { version = "1", default-features = false, features = ["rust_backend"] } md-5 = "0.10" sha2 = "0.10" hmac = "0.12" diff --git a/services/redshift/src/backend.rs b/services/redshift/src/backend.rs index 97873de..427082c 100644 --- a/services/redshift/src/backend.rs +++ b/services/redshift/src/backend.rs @@ -16,6 +16,24 @@ pub trait SqlBackend: Send + Sync { fn begin(&self) -> Result, SqlError>; fn catalog(&self) -> Result; fn close(&self) -> Result<(), SqlError>; + + /// A connection of its own for one SQL client, so session state (temp + /// tables, an open transaction, `SET`s) stays with that client and ends + /// when it disconnects. `None`: statements go through [`Self::exec`]. + fn session(&self) -> Result>, SqlError> { + Ok(None) + } + + /// Whether `exec` implements Redshift `COPY` itself (the memory engine + /// does); otherwise the server loads COPY data through the backend. + fn handles_copy(&self) -> bool { + false + } +} + +/// One client's backend connection; dropping it ends the session. +pub trait SqlSession: Send { + fn exec(&mut self, statement: &str) -> Result; } pub trait SqlTransaction: Send { diff --git a/services/redshift/src/backend_memory.rs b/services/redshift/src/backend_memory.rs index 7c6cfc6..3b7d0a5 100644 --- a/services/redshift/src/backend_memory.rs +++ b/services/redshift/src/backend_memory.rs @@ -84,6 +84,10 @@ impl SqlBackend for MemoryBackend { self.inner.exec(statement) } + fn handles_copy(&self) -> bool { + true + } + fn begin(&self) -> Result, SqlError> { self.inner.ready()?; let snapshot = self.inner.snapshot.as_ref().map(|snapshot| snapshot()); diff --git a/services/redshift/src/backend_postgres.rs b/services/redshift/src/backend_postgres.rs index a7eb232..fee706a 100644 --- a/services/redshift/src/backend_postgres.rs +++ b/services/redshift/src/backend_postgres.rs @@ -26,7 +26,8 @@ use tokio::net::TcpStream; use tokio::runtime::Runtime; use crate::backend::{ - CatalogSnapshot, Column, ExecResult, Field, Schema, SqlBackend, SqlTransaction, Table, + CatalogSnapshot, Column, ExecResult, Field, Schema, SqlBackend, SqlSession, SqlTransaction, + Table, }; use crate::errors::SqlError; @@ -159,6 +160,24 @@ impl SqlBackend for Backend { })) } + /// A dedicated PostgreSQL connection for one SQL client: its temp tables + /// and transactions are its own and end with it, as on Redshift. + fn session(&self) -> Result>, SqlError> { + if self.conn.lock().unwrap().is_none() { + return Err(SqlError::new("postgres redshift backend is not open")); + } + let runtime = tokio::runtime::Builder::new_current_thread() + .enable_all() + .build() + .map_err(|err| wrap_error("session", err.to_string()))?; + let conn = drive(&runtime, PgConn::connect(&self.info)) + .map_err(|err| wrap_error("session", err))?; + Ok(Some(Box::new(Session { + runtime: SafeRuntime::new(runtime), + conn: Some(conn), + }))) + } + /// Mirrors `Backend.Catalog`. fn catalog(&self) -> Result { if self.conn.lock().unwrap().is_none() { @@ -179,6 +198,32 @@ impl SqlBackend for Backend { } } +/// One client's connection (see [`SqlBackend::session`]). +struct Session { + runtime: SafeRuntime, + conn: Option, +} + +impl SqlSession for Session { + fn exec(&mut self, statement: &str) -> Result { + let conn = self + .conn + .as_mut() + .ok_or_else(|| SqlError::new("session is closed"))?; + drive(&self.runtime, conn.simple_query(statement)).map_err(|err| wrap_error("exec", err)) + } +} + +impl Drop for Session { + fn drop(&mut self) { + // Terminate tells PostgreSQL the session is over, which drops its + // temp tables and rolls back an open transaction. + if let Some(mut conn) = self.conn.take() { + let _ = drive(&self.runtime, conn.terminate()); + } + } +} + /// Mirrors `transaction`. struct Transaction { runtime: SafeRuntime, diff --git a/services/redshift/src/copy_backend.rs b/services/redshift/src/copy_backend.rs new file mode 100644 index 0000000..1b45710 --- /dev/null +++ b/services/redshift/src/copy_backend.rs @@ -0,0 +1,762 @@ +//! Redshift `COPY ... FROM 's3://...'` for SQL backends that do not implement +//! it themselves (PostgreSQL). +//! +//! devcloud reads the objects from the local S3 store, parses them the way +//! Redshift does, and loads the rows with `INSERT` statements on the client's +//! own backend session — so a COPY into a temp table, or inside an open +//! transaction, behaves as it does on Redshift. +//! +//! Supported: `FROM 's3://bucket/prefix'` (every object whose key starts with +//! the prefix, in key order), the default text format with `DELIMITER`, +//! `CSV` / `FORMAT AS CSV`, `JSON 'auto'` / `JSON 'auto ignorecase'`, `GZIP`, +//! `IGNOREHEADER`, `NULL AS`, `EMPTYASNULL`, `BLANKSASNULL`. Authorization +//! (`IAM_ROLE`, `CREDENTIALS`, ...) and `REGION` are accepted and ignored. +//! Any other option is refused rather than silently ignored. + +use std::io::Read; + +use crate::backend::{ExecResult, SqlSession}; +use crate::copy_unload::{parse_csv_delimiter, tokenize_sql_options}; +use crate::engine::QueryResult; +use crate::errors::SqlError; +use crate::server::Server; +use crate::sql_parse::parse_leading_sql_string_literal; + +/// Rows per generated INSERT. +const INSERT_BATCH_ROWS: usize = 500; + +/// Whether `statement` is a COPY (Redshift has only `COPY ... FROM`). +pub(crate) fn is_copy(statement: &str) -> bool { + // Bytes, not a str slice: the statement may start with any UTF-8 text. + let b = statement.trim_start().as_bytes(); + b.len() > 4 && b[..4].eq_ignore_ascii_case(b"copy") && b[4].is_ascii_whitespace() +} + +/// Byte offset of the `FROM` keyword: whitespace before it, whitespace or a +/// quote after it, and not inside a double-quoted identifier. +fn find_from_keyword(body: &str) -> Option { + let b = body.as_bytes(); + let mut in_ident = false; + let mut i = 0; + while i + 4 <= b.len() { + match b[i] { + b'"' => in_ident = !in_ident, + _ if !in_ident + && i > 0 + && b[i - 1].is_ascii_whitespace() + && b[i..i + 4].eq_ignore_ascii_case(b"from") + && b.get(i + 4) + .is_some_and(|c| c.is_ascii_whitespace() || *c == b'\'') => + { + return Some(i); + } + _ => {} + } + i += 1; + } + None +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum Format { + /// Redshift's default: delimited text, no quoting. + Text, + Csv, + Json { + ignore_case: bool, + }, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +struct Options { + format: Format, + delimiter: Option, + gzip: bool, + ignore_header: usize, + null_as: Option, + empty_as_null: bool, + blanks_as_null: bool, +} + +impl Default for Options { + fn default() -> Self { + Options { + format: Format::Text, + delimiter: None, + gzip: false, + ignore_header: 0, + null_as: None, + empty_as_null: false, + blanks_as_null: false, + } + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +struct CopyStatement { + /// The target exactly as written (it is put back into SQL unchanged). + table: String, + source: String, + options: Options, +} + +/// A target column: its name and whether it holds character data (which +/// keeps an empty field as `''` rather than NULL). +struct TargetColumn { + name: String, + is_string: bool, +} + +/// One field as read from the file. `quoted` is a CSV field in quotes. +struct RawField { + text: String, + quoted: bool, +} + +impl Server { + pub(crate) fn copy_through_backend( + &self, + backend_session: &mut Option>, + statement: &str, + ) -> Result { + let copy = parse_copy(statement)?; + let mut exec = |sql: &str| -> Result { + match backend_session.as_mut() { + Some(s) => s.exec(sql), + None => self.backend().exec(sql), + } + }; + let columns = target_columns(&mut exec, ©.table)?; + // Every object is read and parsed before anything is written, so a + // bad file anywhere under the prefix loads nothing. + let mut rows = Vec::new(); + for data in self.read_copy_objects(©)? { + let text = String::from_utf8(data) + .map_err(|_| SqlError::new("COPY input is not valid UTF-8"))?; + rows.extend(match copy.options.format { + Format::Json { ignore_case } => json_rows(&text, &columns, ignore_case)?, + Format::Csv | Format::Text => delimited_rows(&text, ©.options, &columns)?, + }); + } + self.check_row_sizes(&rows)?; + if !rows.is_empty() { + // One simple-protocol Query: PostgreSQL runs its statements as a + // single implicit transaction, so a failing row (a type error) + // undoes the whole COPY; inside an explicit transaction the + // failure aborts that transaction, as a failed COPY does on + // Redshift. + let inserts: Vec = rows + .chunks(INSERT_BATCH_ROWS) + .map(|batch| insert_statement(©.table, &columns, batch)) + .collect(); + exec(&inserts.join(";\n"))?; + } + Ok(QueryResult::tag_only(&format!("COPY {}", rows.len()))) + } + + /// The objects a COPY source names: every key under the prefix, in key + /// order, decompressed when `GZIP` is set. + fn read_copy_objects(&self, copy: &CopyStatement) -> Result>, SqlError> { + let Some(store) = &self.shared.config.object_store else { + return Err(SqlError::new( + "COPY from s3 URI requires local S3 service to be enabled", + )); + }; + let Some(rest) = copy + .source + .get(..5) + .filter(|scheme| scheme.eq_ignore_ascii_case("s3://")) + .map(|_| ©.source[5..]) + else { + return Err(SqlError::new("COPY supports only s3:// sources")); + }; + let (bucket, prefix) = rest.split_once('/').unwrap_or((rest, "")); + let objects = store + .list_objects(bucket, prefix) + .map_err(|err| SqlError::new(format!("COPY list S3 objects: {err:?}")))? + .ok_or_else(|| SqlError::new(format!("COPY source bucket {bucket} does not exist")))?; + if objects.is_empty() { + return Err(SqlError::new(format!( + "The specified S3 prefix '{prefix}' does not exist" + ))); + } + let mut out = Vec::with_capacity(objects.len()); + for object in objects { + let Some((_, data)) = store + .get_object(bucket, &object.key) + .map_err(|err| SqlError::new(format!("COPY read S3 object: {err:?}")))? + else { + continue; + }; + out.push(if copy.options.gzip { + gunzip(&data).map_err(|err| { + SqlError::new(format!("COPY gunzip s3://{bucket}/{}: {err}", object.key)) + })? + } else { + data + }); + } + Ok(out) + } + + /// Applies the configured per-row input limit (`maxInputRowBytes`). + fn check_row_sizes(&self, rows: &[Vec>]) -> Result<(), SqlError> { + let max = self.shared.config.max_copy_input_bytes; + if max <= 0 { + return Ok(()); + } + for row in rows { + let size: usize = row.iter().flatten().map(String::len).sum(); + if size as i64 > max { + return Err(SqlError::new("COPY input row exceeds maxCopyInputBytes")); + } + } + Ok(()) + } +} + +fn parse_copy(statement: &str) -> Result { + let body = statement.trim().trim_end_matches(';').trim(); + let from = find_from_keyword(body).ok_or_else(|| SqlError::new("COPY requires FROM"))?; + let table = body["copy".len()..from].trim().to_string(); + if table.is_empty() { + return Err(SqlError::new("COPY requires a target table")); + } + if table.contains('(') { + return Err(SqlError::new( + "COPY with a column list is not supported by devcloud", + )); + } + let after_from = body[from + "from".len()..].trim_start(); + let (source, rest) = parse_leading_sql_string_literal(after_from) + .map_err(|err| SqlError::new(format!("COPY requires an s3:// source: {err}")))?; + Ok(CopyStatement { + table, + source, + options: parse_options(&rest)?, + }) +} + +fn parse_options(text: &str) -> Result { + let tokens = tokenize_sql_options(text)?; + let mut options = Options::default(); + let mut i = 0; + // The value after `name [AS]`. + let value = |i: &mut usize, name: &str| -> Result { + if tokens + .get(*i + 1) + .is_some_and(|t| t.eq_ignore_ascii_case("as")) + { + *i += 1; + } + *i += 1; + tokens + .get(*i) + .cloned() + .ok_or_else(|| SqlError::new(format!("COPY {name} requires a value"))) + }; + while i < tokens.len() { + let token = tokens[i].to_ascii_lowercase(); + match token.as_str() { + "format" => { + if tokens + .get(i + 1) + .is_some_and(|t| t.eq_ignore_ascii_case("as")) + { + i += 1; + } + // The format keyword itself is read on the next turn. + } + "csv" => options.format = Format::Csv, + "json" => { + let arg = tokens.get(i + 1).map(|t| t.to_ascii_lowercase()); + options.format = match arg.as_deref() { + Some("auto") => Format::Json { ignore_case: false }, + Some("auto ignorecase") => Format::Json { ignore_case: true }, + _ => return Err(SqlError::new( + "COPY JSON is supported by devcloud only with 'auto' or 'auto ignorecase'", + )), + }; + i += 1; + } + "delimiter" => { + options.delimiter = Some(parse_csv_delimiter(&value(&mut i, "DELIMITER")?)?) + } + "gzip" => options.gzip = true, + "ignoreheader" => { + let n = value(&mut i, "IGNOREHEADER")?; + options.ignore_header = n.parse().map_err(|_| { + SqlError::new("COPY IGNOREHEADER requires a non-negative row count") + })?; + } + "null" => options.null_as = Some(value(&mut i, "NULL AS")?), + "emptyasnull" => options.empty_as_null = true, + "blanksasnull" => options.blanks_as_null = true, + "iam_role" + | "credentials" + | "access_key_id" + | "secret_access_key" + | "session_token" + | "master_symmetric_key" => { + i += 1; + } + "region" => { + value(&mut i, "REGION")?; + } + "compupdate" | "statupdate" => { + if tokens.get(i + 1).is_some_and(|t| { + matches!( + t.to_ascii_lowercase().as_str(), + "on" | "off" | "true" | "false" | "preset" + ) + }) { + i += 1; + } + } + "" => {} + other => { + return Err(SqlError::new(format!( + "COPY option {} is not supported by devcloud", + other.to_ascii_uppercase() + ))) + } + } + i += 1; + } + Ok(options) +} + +/// The target's columns in table order, read on the session that will load +/// them (so a temp table of this session resolves). +fn target_columns( + exec: &mut impl FnMut(&str) -> Result, + table: &str, +) -> Result, SqlError> { + let query = format!( + "SELECT a.attname, t.typcategory FROM pg_catalog.pg_attribute a \ + JOIN pg_catalog.pg_type t ON t.oid = a.atttypid \ + WHERE a.attrelid = to_regclass({}) AND a.attnum > 0 AND NOT a.attisdropped \ + ORDER BY a.attnum", + sql_literal(table) + ); + let result = exec(&query)?; + if result.rows.is_empty() { + return Err(SqlError::new(format!("relation {table} does not exist"))); + } + Ok(result + .rows + .into_iter() + .map(|row| TargetColumn { + is_string: row.get(1).is_some_and(|c| c == "S"), + name: row.into_iter().next().unwrap_or_default(), + }) + .collect()) +} + +/// Text or CSV input to rows, with Redshift's NULL rules: a field equal to +/// `NULL AS` (default `\N`) is NULL; an empty field is NULL except in a +/// character column, where it stays `''` unless `EMPTYASNULL`. +fn delimited_rows( + text: &str, + options: &Options, + columns: &[TargetColumn], +) -> Result>>, SqlError> { + let records = match options.format { + Format::Csv => csv_records(text, options.delimiter.unwrap_or(','))?, + _ => { + let delimiter = options.delimiter.unwrap_or('|'); + text.split('\n') + .map(|line| line.strip_suffix('\r').unwrap_or(line)) + .filter(|line| !line.is_empty()) + .map(|line| { + line.split(delimiter) + .map(|f| RawField { + text: f.to_string(), + quoted: false, + }) + .collect::>() + }) + .collect::>() + } + }; + let null_as = options.null_as.as_deref().unwrap_or("\\N"); + let mut rows = Vec::with_capacity(records.len()); + for (index, record) in records.into_iter().enumerate().skip(options.ignore_header) { + if record.len() != columns.len() { + return Err(SqlError::new(format!( + "COPY line {} has {} fields for {} columns", + index + 1, + record.len(), + columns.len() + ))); + } + let row = record + .into_iter() + .zip(columns) + .map(|(field, column)| field_value(field, column, null_as, options)) + .collect(); + rows.push(row); + } + Ok(rows) +} + +fn field_value( + field: RawField, + column: &TargetColumn, + null_as: &str, + options: &Options, +) -> Option { + if !field.quoted && field.text == null_as { + return None; + } + if field.text.is_empty() { + return (column.is_string && !options.empty_as_null).then(String::new); + } + if column.is_string && options.blanks_as_null && field.text.trim().is_empty() { + return None; + } + Some(field.text) +} + +/// RFC 4180 records: quoted fields may hold the delimiter, `""`, and line +/// breaks. Blank lines are skipped. +fn csv_records(text: &str, delimiter: char) -> Result>, SqlError> { + let mut records = Vec::new(); + let mut record: Vec = Vec::new(); + let mut field = String::new(); + let mut quoted = false; + let mut in_quotes = false; + let mut chars = text.chars().peekable(); + let mut line_has_content = false; + while let Some(c) = chars.next() { + if in_quotes { + if c == '"' { + if chars.peek() == Some(&'"') { + chars.next(); + field.push('"'); + } else { + in_quotes = false; + } + } else { + field.push(c); + } + continue; + } + match c { + '"' if field.is_empty() && !quoted => { + in_quotes = true; + quoted = true; + line_has_content = true; + } + c if c == delimiter => { + record.push(RawField { + text: std::mem::take(&mut field), + quoted, + }); + quoted = false; + line_has_content = true; + } + '\r' if chars.peek() == Some(&'\n') => {} + '\n' => { + if line_has_content { + record.push(RawField { + text: std::mem::take(&mut field), + quoted, + }); + records.push(std::mem::take(&mut record)); + } + quoted = false; + line_has_content = false; + } + c => { + field.push(c); + line_has_content = true; + } + } + } + if in_quotes { + return Err(SqlError::new("COPY CSV input ends inside a quoted field")); + } + if line_has_content { + record.push(RawField { + text: field, + quoted, + }); + records.push(record); + } + Ok(records) +} + +/// `JSON 'auto'`: a stream of objects (one per line or concatenated); keys +/// match column names exactly (`ignorecase`: case-insensitively). Missing +/// keys and JSON null are NULL; nested values load as their JSON text. +fn json_rows( + text: &str, + columns: &[TargetColumn], + ignore_case: bool, +) -> Result>>, SqlError> { + let mut rows = Vec::new(); + for (index, value) in serde_json::Deserializer::from_str(text) + .into_iter::() + .enumerate() + { + let value = value.map_err(|err| SqlError::new(format!("COPY read JSON: {err}")))?; + let serde_json::Value::Object(object) = value else { + return Err(SqlError::new(format!( + "COPY JSON record {} is not an object", + index + 1 + ))); + }; + let lookup = |name: &str| { + if ignore_case { + object + .iter() + .find(|(k, _)| k.eq_ignore_ascii_case(name)) + .map(|(_, v)| v) + } else { + object.get(name) + } + }; + let row = columns + .iter() + .map(|column| match lookup(&column.name) { + None | Some(serde_json::Value::Null) => None, + Some(serde_json::Value::String(s)) if s.is_empty() && !column.is_string => None, + Some(serde_json::Value::String(s)) => Some(s.clone()), + Some(serde_json::Value::Bool(b)) => Some(b.to_string()), + Some(serde_json::Value::Number(n)) => Some(n.to_string()), + Some(other) => Some(other.to_string()), + }) + .collect(); + rows.push(row); + } + Ok(rows) +} + +fn insert_statement(table: &str, columns: &[TargetColumn], rows: &[Vec>]) -> String { + let names: Vec = columns + .iter() + .map(|c| format!("\"{}\"", c.name.replace('"', "\"\""))) + .collect(); + let values: Vec = rows + .iter() + .map(|row| { + let fields: Vec = row + .iter() + .map(|v| v.as_deref().map_or_else(|| "NULL".to_string(), sql_literal)) + .collect(); + format!("({})", fields.join(", ")) + }) + .collect(); + format!( + "INSERT INTO {table} ({}) VALUES {}", + names.join(", "), + values.join(", ") + ) +} + +/// An escape-string literal: its meaning does not depend on the server's +/// `standard_conforming_strings`. +fn sql_literal(value: &str) -> String { + format!("E'{}'", value.replace('\\', "\\\\").replace('\'', "''")) +} + +fn gunzip(data: &[u8]) -> std::io::Result> { + let mut out = Vec::new(); + flate2::read::MultiGzDecoder::new(data).read_to_end(&mut out)?; + Ok(out) +} + +#[cfg(test)] +mod tests { + use super::*; + + fn cols(spec: &[(&str, bool)]) -> Vec { + spec.iter() + .map(|(name, is_string)| TargetColumn { + name: name.to_string(), + is_string: *is_string, + }) + .collect() + } + + #[test] + fn parses_the_statements_pushone_csa_sends() { + let c = parse_copy("COPY csa_stg_file FROM 's3://b/in/a.csv' IAM_ROLE 'arn:aws:iam::1:role/r' FORMAT AS CSV GZIP").unwrap(); + assert_eq!(c.table, "csa_stg_file"); + assert_eq!(c.source, "s3://b/in/a.csv"); + assert_eq!(c.options.format, Format::Csv); + assert!(c.options.gzip); + + let c = parse_copy(r"COPY csa_stg_file FROM 's3://b/a.tsv' IAM_ROLE 'r' DELIMITER '\t'") + .unwrap(); + assert_eq!( + (c.options.format, c.options.delimiter), + (Format::Text, Some('\t')) + ); + + let c = + parse_copy("COPY \"s\".\"t\" FROM 's3://b/a.json' IAM_ROLE 'r' FORMAT AS JSON 'auto';") + .unwrap(); + assert_eq!(c.table, "\"s\".\"t\""); + assert_eq!(c.options.format, Format::Json { ignore_case: false }); + } + + #[test] + fn other_options() { + let o = parse_options("CSV IGNOREHEADER AS 1 NULL AS 'NA' EMPTYASNULL BLANKSASNULL REGION AS 'us-east-1' COMPUPDATE OFF").unwrap(); + assert_eq!(o.ignore_header, 1); + assert_eq!(o.null_as.as_deref(), Some("NA")); + assert!(o.empty_as_null && o.blanks_as_null); + let o = parse_options("JSON 'auto ignorecase' DELIMITER ','").unwrap(); + assert_eq!(o.format, Format::Json { ignore_case: true }); + for bad in [ + "MANIFEST", + "JSON 's3://b/paths.json'", + "BZIP2", + "FIXEDWIDTH 'a:1'", + "FORMAT AS PARQUET", + ] { + assert!(parse_options(bad).is_err(), "{bad}"); + } + assert!(parse_copy("COPY t (a, b) FROM 's3://b/k'").is_err()); + } + + #[test] + fn csv_null_rules() { + let options = Options { + format: Format::Csv, + ..Options::default() + }; + let columns = cols(&[("id", false), ("name", true), ("note", true), ("n", false)]); + let rows = delimited_rows("1,\"a, b\",,\n2,\"\",\\N,\"\"\n", &options, &columns).unwrap(); + assert_eq!( + rows, + vec![ + vec![ + Some("1".into()), + Some("a, b".into()), + Some(String::new()), + None + ], + vec![Some("2".into()), Some(String::new()), None, None], + ] + ); + let options = Options { + format: Format::Csv, + empty_as_null: true, + ignore_header: 1, + ..Options::default() + }; + let rows = delimited_rows("id,name,note,n\n1,x,,5\n", &options, &columns).unwrap(); + assert_eq!( + rows, + vec![vec![ + Some("1".into()), + Some("x".into()), + None, + Some("5".into()) + ]] + ); + } + + #[test] + fn csv_quotes_span_lines_and_escape_quotes() { + let records = csv_records("\"a\nb\",\"say \"\"hi\"\"\"\r\n\r\nc,d", ',').unwrap(); + let texts: Vec> = records + .iter() + .map(|r| r.iter().map(|f| f.text.as_str()).collect()) + .collect(); + assert_eq!(texts, vec![vec!["a\nb", "say \"hi\""], vec!["c", "d"]]); + assert!(csv_records("\"open", ',').is_err()); + } + + #[test] + fn text_format_is_unquoted() { + let options = Options { + delimiter: Some('\t'), + ..Options::default() + }; + let columns = cols(&[("id", false), ("name", true)]); + let rows = delimited_rows("1\t\"q\"\r\n2\t\\N\n", &options, &columns).unwrap(); + assert_eq!( + rows, + vec![ + vec![Some("1".into()), Some("\"q\"".into())], + vec![Some("2".into()), None] + ] + ); + assert!(delimited_rows("1\t2\t3\n", &options, &columns).is_err()); + } + + #[test] + fn json_auto_matches_column_names() { + let columns = cols(&[ + ("id", false), + ("name", true), + ("meta", true), + ("missing", true), + ]); + let text = "{\"id\":1,\"name\":\"a\",\"meta\":{\"k\":[1]}}\n{\"id\":2,\"Name\":\"B\",\"meta\":null}"; + let rows = json_rows(text, &columns, false).unwrap(); + assert_eq!( + rows[0], + vec![ + Some("1".into()), + Some("a".into()), + Some("{\"k\":[1]}".into()), + None + ] + ); + assert_eq!(rows[1], vec![Some("2".into()), None, None, None]); + let rows = json_rows(text, &columns, true).unwrap(); + assert_eq!(rows[1][1].as_deref(), Some("B")); + assert!(json_rows("[1]", &columns, false).is_err()); + } + + #[test] + fn insert_literals_are_escaped() { + let columns = cols(&[("a\"b", true), ("n", false)]); + let sql = insert_statement("t", &columns, &[vec![Some("it's \\ ok".into()), None]]); + assert_eq!( + sql, + "INSERT INTO t (\"a\"\"b\", \"n\") VALUES (E'it''s \\\\ ok', NULL)" + ); + } + + #[test] + fn gunzip_reads_concatenated_members() { + use std::io::Write; + let gz = |s: &str| { + let mut e = flate2::write::GzEncoder::new(Vec::new(), flate2::Compression::default()); + e.write_all(s.as_bytes()).unwrap(); + e.finish().unwrap() + }; + let mut data = gz("a\n"); + data.extend(gz("b\n")); + assert_eq!(gunzip(&data).unwrap(), b"a\nb\n"); + assert!(gunzip(b"plain").is_err()); + } + + #[test] + fn is_copy_detects_the_keyword() { + assert!(is_copy(" copy t from 's3://b/k'")); + assert!(is_copy("COPY\n t FROM 's3://b/k'")); + assert!(!is_copy("COPYRIGHT")); + assert!(!is_copy("select 'copy '")); + // Multi-byte text where a fixed-width str slice would split a char. + assert!(!is_copy("-- 猫\nSELECT 1;")); + assert!(!is_copy("猫猫")); + } + + #[test] + fn from_may_follow_any_whitespace() { + let c = parse_copy("COPY t\nFROM\n's3://b/k'\tIAM_ROLE 'r'\tCSV").unwrap(); + assert_eq!((c.table.as_str(), c.source.as_str()), ("t", "s3://b/k")); + assert_eq!(c.options.format, Format::Csv); + let c = parse_copy("COPY\t\"from table\" FROM's3://b/k'").unwrap(); + assert_eq!(c.table, "\"from table\""); + let c = parse_copy("COPY fromage FROM 's3://b/k'").unwrap(); + assert_eq!(c.table, "fromage"); + } +} diff --git a/services/redshift/src/copy_unload.rs b/services/redshift/src/copy_unload.rs index ced57d8..7edee8a 100644 --- a/services/redshift/src/copy_unload.rs +++ b/services/redshift/src/copy_unload.rs @@ -472,7 +472,7 @@ fn parse_copy_csv_options(value: &str) -> Result { /// Mirrors `tokenizeSQLOptions`: whitespace/`;`-separated tokens with `'...'` /// string literals. -fn tokenize_sql_options(value: &str) -> Result, SqlError> { +pub(crate) fn tokenize_sql_options(value: &str) -> Result, SqlError> { let bytes = value.as_bytes(); let mut tokens = Vec::new(); let mut i = 0; @@ -498,7 +498,7 @@ fn tokenize_sql_options(value: &str) -> Result, SqlError> { } /// Mirrors `parseCSVDelimiter`. -fn parse_csv_delimiter(value: &str) -> Result { +pub(crate) fn parse_csv_delimiter(value: &str) -> Result { if value == "\\t" { return Ok('\t'); } diff --git a/services/redshift/src/lib.rs b/services/redshift/src/lib.rs index c175130..96e47ad 100644 --- a/services/redshift/src/lib.rs +++ b/services/redshift/src/lib.rs @@ -35,6 +35,7 @@ pub mod backend_memory; pub mod backend_postgres; pub mod catalog; pub mod cluster; +mod copy_backend; pub mod copy_unload; pub mod dataapi; pub mod engine; diff --git a/services/redshift/src/pgwire.rs b/services/redshift/src/pgwire.rs index d3ab642..ba4a72e 100644 --- a/services/redshift/src/pgwire.rs +++ b/services/redshift/src/pgwire.rs @@ -16,6 +16,7 @@ use std::time::SystemTime; use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt}; use tokio::net::TcpListener; +use crate::backend::SqlSession; use crate::engine::QueryResult; use crate::errors::SqlError; use crate::pgwire_codec::{ @@ -89,13 +90,28 @@ impl Server { .await?; let mut extended = ExtendedQuerySession::new(); + // One backend connection per client, held until it disconnects. + match self.backend().session() { + Ok(session) => extended.backend_session = session, + Err(err) => { + write_buffered(conn, |buf| { + write_error_response(buf, "08006", &err.to_string()) + }) + .await?; + return Ok(()); + } + } loop { let mut message_type = [0u8; 1]; conn.read_exact(&mut message_type).await?; let payload = read_message_payload(conn).await?; let mut out = Vec::new(); match message_type[0] { - b'Q' => self.handle_simple_query(&mut out, &read_cstring(&payload)), + b'Q' => self.handle_simple_query_in( + &mut extended.backend_session, + &mut out, + &read_cstring(&payload), + ), b'P' => { if !extended.failed { extended.handle_parse(&mut out, &payload); @@ -169,6 +185,16 @@ impl Server { /// Mirrors `handleSimpleQuery`. pub fn handle_simple_query(&self, w: &mut impl Write, query: &str) { + self.handle_simple_query_in(&mut None, w, query) + } + + /// [`Self::handle_simple_query`] on a client's own backend session. + pub fn handle_simple_query_in( + &self, + session: &mut Option>, + w: &mut impl Write, + query: &str, + ) { let statements = split_sql_statements(query); if statements.is_empty() { let _ = write_message(w, b'I', &[]); @@ -185,7 +211,7 @@ impl Server { let _ = write_error_response(w, "54000", &err.to_string()); break; } - match self.execute_sql(statement) { + match self.execute_sql_in(session, statement) { Ok(result) => { self.record_sql_history(statement, &result, None); if !result.fields.is_empty() { diff --git a/services/redshift/src/pgwire_extended.rs b/services/redshift/src/pgwire_extended.rs index 98af4e4..4ae0ffc 100644 --- a/services/redshift/src/pgwire_extended.rs +++ b/services/redshift/src/pgwire_extended.rs @@ -10,6 +10,7 @@ use std::collections::HashMap; use std::io::Write; +use crate::backend::SqlSession; use crate::engine::QueryResult; use crate::errors::SqlError; use crate::pgwire_codec::{ @@ -23,6 +24,9 @@ pub struct ExtendedQuerySession { statements: HashMap, portals: HashMap, pub failed: bool, + /// The client's own backend connection, when the backend keeps one per + /// client (see `SqlBackend::session`). + pub backend_session: Option>, } /// Mirrors `extendedPreparedStatement`. @@ -62,6 +66,7 @@ impl ExtendedQuerySession { statements: HashMap::new(), portals: HashMap::new(), failed: false, + backend_session: None, } } @@ -253,7 +258,7 @@ impl ExtendedQuerySession { .unwrap_or_default(); let _ = write_parameter_description(w, ¶meter_oids); } - match server.describe_extended_query(&statement) { + match server.describe_extended_query(&mut self.backend_session, &statement) { Some(result) if !result.fields.is_empty() => { let _ = write_row_description(w, &result.fields); } @@ -290,7 +295,7 @@ impl ExtendedQuerySession { .map(|prepared| prepared.statement.clone()) .unwrap_or_default(); if !portal.executed { - match server.execute_sql(&portal.executable_statement) { + match server.execute_sql_in(&mut self.backend_session, &portal.executable_statement) { Ok(result) => { server.record_sql_history(&prepared_statement, &result, None); portal.result = result; @@ -438,12 +443,16 @@ fn sql_literal_for_extended_bind(parameter: &ExtendedBindParameter) -> String { impl Server { /// Mirrors `describeExtendedQuery`: SELECT statements are dry-run through /// the engine to obtain a row description (rows discarded). - pub(crate) fn describe_extended_query(&self, statement: &str) -> Option { + pub(crate) fn describe_extended_query( + &self, + session: &mut Option>, + statement: &str, + ) -> Option { let normalized = statement.trim_end_matches(';').trim(); if !normalized.to_lowercase().starts_with("select ") { return None; } - let mut result = self.execute_sql(normalized).ok()?; + let mut result = self.execute_sql_in(session, normalized).ok()?; result.rows = Vec::new(); Some(result) } diff --git a/services/redshift/src/server.rs b/services/redshift/src/server.rs index 0765cb4..2925de2 100644 --- a/services/redshift/src/server.rs +++ b/services/redshift/src/server.rs @@ -11,7 +11,7 @@ use std::time::SystemTime; use devcloud_s3::store::FileBucketStore; -use crate::backend::{self, SqlBackend}; +use crate::backend::{self, SqlBackend, SqlSession}; use crate::backend_memory::MemoryBackend; use crate::cluster::{ cluster_snapshot_from_config, default_cluster_identifier, normalize_cluster_endpoints, @@ -238,6 +238,20 @@ impl Server { /// `RedshiftTranslator` (default: passthrough), executes the backend SQL, /// then applies any translator metadata effects. pub fn execute_sql(&self, statement: &str) -> Result { + self.execute_sql_in(&mut None, statement) + } + + /// [`Self::execute_sql`] on a client's own backend session, when it has + /// one. Redshift `COPY` is loaded by devcloud itself unless the backend + /// implements it (the memory engine). + pub fn execute_sql_in( + &self, + backend_session: &mut Option>, + statement: &str, + ) -> Result { + if !self.backend.handles_copy() && crate::copy_backend::is_copy(statement) { + return self.copy_through_backend(backend_session, statement); + } let session = Session { database: default_str(&self.shared.config.database, "dev"), user: default_str(&self.shared.config.user, "dev"), @@ -264,7 +278,10 @@ impl Server { } else { translated.backend_sql.as_str() }; - let result = self.backend.exec(backend_sql)?; + let result = match backend_session.as_mut() { + Some(s) => s.exec(backend_sql)?, + None => self.backend.exec(backend_sql)?, + }; self.shared .apply_translation_metadata_effects(&translated.metadata_effects)?; Ok(query_result_from_backend(result)) diff --git a/services/ssm/Cargo.toml b/services/ssm/Cargo.toml new file mode 100644 index 0000000..2542b10 --- /dev/null +++ b/services/ssm/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "devcloud-ssm" +version = "0.1.0" +edition = "2021" +description = "devcloud SSM Parameter Store emulator (AWS JSON 1.1)." + +[lib] +name = "devcloud_ssm" +path = "src/lib.rs" + +[dependencies] +tokio = { version = "1", features = ["rt", "rt-multi-thread", "macros", "net", "io-util", "time"] } +serde = { version = "1", features = ["derive"] } +serde_json = "1" +hmac = "0.12" +sha2 = "0.10" +hex = "0.4" diff --git a/services/ssm/src/http.rs b/services/ssm/src/http.rs new file mode 100644 index 0000000..f588d0f --- /dev/null +++ b/services/ssm/src/http.rs @@ -0,0 +1,373 @@ +//! Minimal HTTP/1.1 front-end for the SSM AWS JSON 1.1 protocol: only `POST /`; +//! content-type must be `application/x-amz-json-1.1` (when present); SigV4 is +//! verified per auth mode; then `X-Amz-Target: AmazonSSM.` is dispatched. +//! Error bodies carry `X-Amzn-Errortype`. Same transport as +//! `services/applicationautoscaling/src/http.rs`. + +use std::collections::HashMap; +use std::sync::Arc; + +use tokio::io::{AsyncRead, AsyncReadExt, AsyncWriteExt}; +use tokio::net::{TcpListener, TcpStream}; + +use crate::server::{error_outcome, Outcome, Server}; +use crate::sigv4::{verify_signature, Credentials, SignedRequest}; + +const TARGET_PREFIX: &str = "AmazonSSM."; +const MAX_HEADER_BYTES: usize = 64 * 1024; +const MAX_BODY_BYTES: usize = 8 * 1024 * 1024; + +/// A parsed HTTP request: method, path, query, and lowercase-keyed headers. +struct Request { + method: String, + path: String, + query: String, + headers: HashMap, + body: Vec, +} + +impl Request { + fn header(&self, name: &str) -> &str { + self.headers.get(name).map(String::as_str).unwrap_or("") + } +} + +/// Runs the accept loop until `shutdown` resolves. Each connection is served on +/// its own task; connections are closed after one request (`Connection: close`). +pub async fn serve( + listener: TcpListener, + server: Arc, + shutdown: impl std::future::Future, +) -> std::io::Result<()> { + tokio::pin!(shutdown); + loop { + tokio::select! { + _ = &mut shutdown => return Ok(()), + accepted = listener.accept() => { + let (stream, _) = match accepted { + Ok(a) => a, + // Out of descriptors, or a peer gone before the accept: + // back off and keep serving instead of stopping (which + // would take every devcloud service down with it). + Err(e) => { + // Only transient errors; a broken listener still stops. + let transient = matches!( + e.kind(), + std::io::ErrorKind::ConnectionAborted + | std::io::ErrorKind::ConnectionReset + | std::io::ErrorKind::Interrupted + | std::io::ErrorKind::WouldBlock + | std::io::ErrorKind::TimedOut + ) || matches!(e.raw_os_error(), Some(12 | 23 | 24)); // ENOMEM, ENFILE, EMFILE + if !transient { + return Err(e); + } + tokio::time::sleep(std::time::Duration::from_millis(100)).await; + continue; + } + }; + let server = Arc::clone(&server); + tokio::spawn(async move { + let _ = handle_conn(stream, server).await; + }); + } + } + } +} + +async fn handle_conn(mut stream: TcpStream, server: Arc) -> std::io::Result<()> { + let request = match read_request(&mut stream).await { + Ok(Some(req)) => req, + Ok(None) => return Ok(()), // closed or malformed; drop quietly + Err(_) => return Ok(()), + }; + let outcome = process(&server, &request); + write_response(&mut stream, outcome).await +} + +/// Reads one HTTP/1.1 request: request line + headers (until CRLFCRLF) then a +/// `Content-Length`-bounded body. Returns `None` on clean EOF or malformed head. +async fn read_request(stream: &mut (impl AsyncRead + Unpin)) -> std::io::Result> { + let mut buf = Vec::new(); + let mut tmp = [0u8; 4096]; + let header_end = loop { + if let Some(pos) = find_subslice(&buf, b"\r\n\r\n") { + if pos + 4 > MAX_HEADER_BYTES { + return Ok(None); + } + break pos; + } + if buf.len() > MAX_HEADER_BYTES { + return Ok(None); + } + let n = stream.read(&mut tmp).await?; + if n == 0 { + return Ok(None); + } + buf.extend_from_slice(&tmp[..n]); + }; + + let head = String::from_utf8_lossy(&buf[..header_end]).into_owned(); + let mut lines = head.split("\r\n"); + let request_line = lines.next().unwrap_or(""); + let mut parts = request_line.split(' '); + let method = parts.next().unwrap_or("").to_string(); + let target = parts.next().unwrap_or("/"); + let (path, query) = match target.split_once('?') { + Some((p, q)) => (p.to_string(), q.to_string()), + None => (target.to_string(), String::new()), + }; + + let mut headers = HashMap::new(); + for line in lines { + if let Some((k, v)) = line.split_once(':') { + let key = k.trim().to_ascii_lowercase(); + if matches!(key.as_str(), "content-length" | "transfer-encoding") + && headers.contains_key(&key) + { + return Ok(None); + } + headers.insert(key, v.trim().to_string()); + } + } + + // This transport supports fixed-length request bodies only. Never + // dispatch an unsupported transfer coding as an empty request body. + if headers.contains_key("transfer-encoding") { + return Ok(None); + } + let content_length: usize = match headers.get("content-length") { + None => 0, + Some(value) if !value.is_empty() && value.bytes().all(|b| b.is_ascii_digit()) => { + match value.parse() { + Ok(length) => length, + Err(_) => return Ok(None), + } + } + Some(_) => return Ok(None), + }; + if content_length > MAX_BODY_BYTES { + return Ok(None); + } + + let mut body = buf[header_end + 4..].to_vec(); + while body.len() < content_length { + let n = stream.read(&mut tmp).await?; + if n == 0 { + return Ok(None); + } + body.extend_from_slice(&tmp[..n]); + } + body.truncate(content_length); + + Ok(Some(Request { + method, + path, + query, + headers, + body, + })) +} + +async fn write_response(stream: &mut TcpStream, outcome: Outcome) -> std::io::Result<()> { + // legacy json.Encoder appends a trailing newline; match it byte-for-byte. + let mut body = serde_json::to_vec(&outcome.body).unwrap_or_default(); + body.push(b'\n'); + + let mut head = format!( + "HTTP/1.1 {} {}\r\n", + outcome.status, + reason_phrase(outcome.status) + ); + head.push_str("Server: devcloud-ssm\r\n"); + head.push_str("Content-Type: application/x-amz-json-1.1\r\n"); + if let Some(error_type) = &outcome.error_type { + head.push_str(&format!("X-Amzn-Errortype: {error_type}\r\n")); + } + if outcome.status == 405 { + head.push_str("Allow: POST\r\n"); + } + head.push_str(&format!("Content-Length: {}\r\n", body.len())); + head.push_str("Connection: close\r\n\r\n"); + + stream.write_all(head.as_bytes()).await?; + stream.write_all(&body).await?; + stream.flush().await +} + +fn process(server: &Server, req: &Request) -> Outcome { + if server.load_err().is_some() { + return error_outcome(500, "InternalServerError", "failed to load ssm state"); + } + if req.path != "/" { + return error_outcome(404, "ResourceNotFoundException", "not found"); + } + if req.method != "POST" { + return error_outcome(405, "ValidationException", "method not allowed"); + } + let content_type = req.header("content-type"); + if !content_type.is_empty() && !content_type.starts_with("application/x-amz-json-1.1") { + return error_outcome(400, "ValidationException", "unsupported content type"); + } + + let header_fn = |name: &str| -> Option { req.headers.get(name).cloned() }; + let signed = SignedRequest { + method: &req.method, + path: &req.path, + query: &req.query, + host: req.header("host"), + authorization: req.header("authorization"), + amz_date: req.header("x-amz-date"), + content_sha256: req.header("x-amz-content-sha256"), + header: &header_fn, + body: &req.body, + }; + let cfg = server.config(); + let creds = Credentials { + auth_mode: &cfg.auth_mode, + access_key_id: &cfg.access_key_id, + secret_access_key: &cfg.secret_access_key, + region: &cfg.region, + }; + if let Err(e) = verify_signature(&signed, &creds) { + return error_outcome(e.status, e.name, e.name); + } + + let target = req.header("x-amz-target"); + if !target.starts_with(TARGET_PREFIX) { + return error_outcome(400, "UnknownOperationException", "unknown operation"); + } + server.dispatch(&target[TARGET_PREFIX.len()..], &req.body) +} + +fn reason_phrase(status: u16) -> &'static str { + match status { + 200 => "OK", + 400 => "Bad Request", + 403 => "Forbidden", + 404 => "Not Found", + 405 => "Method Not Allowed", + 500 => "Internal Server Error", + 501 => "Not Implemented", + _ => "Status", + } +} + +fn find_subslice(haystack: &[u8], needle: &[u8]) -> Option { + if needle.is_empty() || haystack.len() < needle.len() { + return None; + } + haystack + .windows(needle.len()) + .position(|window| window == needle) +} + +// --- In-process test harness (mirrors what a real HTTP request would produce) --- + +/// A rendered response for in-process parity tests. +pub struct TestResponse { + pub status: u16, + pub error_type: Option, + pub allow: Option, + pub body: Vec, +} + +/// Drives the full request pipeline (gate → SigV4 → dispatch) without a socket, +/// so parity tests exercise the same code path as a real HTTP request. +/// `headers` are `(name, value)` pairs; `path` may include a `?query`. +pub fn serve_for_test( + server: &Server, + method: &str, + path: &str, + headers: &[(&str, &str)], + body: &[u8], +) -> TestResponse { + let (p, q) = match path.split_once('?') { + Some((p, q)) => (p.to_string(), q.to_string()), + None => (path.to_string(), String::new()), + }; + let header_map: HashMap = headers + .iter() + .map(|(k, v)| (k.to_ascii_lowercase(), v.to_string())) + .collect(); + let req = Request { + method: method.to_string(), + path: p, + query: q, + headers: header_map, + body: body.to_vec(), + }; + let outcome = process(server, &req); + let status = outcome.status; + let error_type = outcome.error_type.clone(); + let mut json = serde_json::to_vec(&outcome.body).unwrap_or_default(); + json.push(b'\n'); + TestResponse { + status, + error_type, + allow: if status == 405 { + Some("POST".to_string()) + } else { + None + }, + body: json, + } +} + +#[cfg(test)] +mod request_framing_tests { + use super::*; + + #[tokio::test] + async fn incomplete_fixed_length_body_is_rejected() { + // A complete JSON value is still an incomplete HTTP message. It must + // never reach a mutating handler after the client disconnects early. + let mut wire = &b"POST / HTTP/1.1\r\nContent-Length: 9\r\n\r\n{}"[..]; + assert!(read_request(&mut wire).await.unwrap().is_none()); + } + + #[tokio::test] + async fn invalid_content_length_is_rejected() { + for value in ["bad", "-1", "+2", "", "18446744073709551616"] { + let wire = format!("POST / HTTP/1.1\r\nContent-Length: {value}\r\n\r\n{{}}"); + assert!( + read_request(&mut wire.as_bytes()).await.unwrap().is_none(), + "{value:?}" + ); + } + } + + #[tokio::test] + async fn ambiguous_or_unsupported_body_framing_is_rejected() { + for headers in [ + "Transfer-Encoding: gzip\r\n", + "Transfer-Encoding: xchunked\r\n", + "Transfer-Encoding: chunked, gzip\r\n", + "Content-Length: 9\r\nContent-Length: 2\r\n", + "Content-Length: 2\r\nTransfer-Encoding: chunked\r\n", + "Transfer-Encoding: chunked\r\nTransfer-Encoding: chunked\r\n", + ] { + let wire = format!("POST / HTTP/1.1\r\n{headers}\r\n{{}}"); + assert!( + read_request(&mut wire.as_bytes()).await.unwrap().is_none(), + "{headers:?}" + ); + } + } + + #[tokio::test] + async fn complete_fixed_length_body_is_preserved() { + let mut wire = &b"POST / HTTP/1.1\r\nContent-Length: 2\r\n\r\n{}"[..]; + let request = read_request(&mut wire).await.unwrap().unwrap(); + assert_eq!(request.body, b"{}"); + } + + #[tokio::test] + async fn oversized_header_is_rejected_even_when_terminator_arrives() { + let wire = format!( + "POST / HTTP/1.1\r\nX-Padding: {}\r\n\r\n", + "a".repeat(MAX_HEADER_BYTES) + ); + assert!(read_request(&mut wire.as_bytes()).await.unwrap().is_none()); + } +} diff --git a/services/ssm/src/lib.rs b/services/ssm/src/lib.rs new file mode 100644 index 0000000..f758882 --- /dev/null +++ b/services/ssm/src/lib.rs @@ -0,0 +1,10 @@ +//! devcloud SSM Parameter Store: `PutParameter`, `GetParameter` (including +//! `SecureString` with `WithDecryption`), `DeleteParameter`, and +//! `DescribeParameters` over the AWS JSON 1.1 protocol, persisted to +//! `state.json`. + +pub mod http; +pub mod server; +pub mod sigv4; + +pub use server::{Config, Server}; diff --git a/services/ssm/src/server.rs b/services/ssm/src/server.rs new file mode 100644 index 0000000..e0e542c --- /dev/null +++ b/services/ssm/src/server.rs @@ -0,0 +1,543 @@ +//! SSM Parameter Store over the AWS JSON 1.1 protocol. +//! +//! Supported operations: `PutParameter`, `GetParameter`, `DeleteParameter`, +//! and `DescribeParameters` (metadata only, used by the dashboard). State is +//! one map behind a `Mutex`, persisted atomically to `state.json`. +//! +//! `SecureString` values are stored as plaintext in `state.json` — this is a +//! local emulator, there is no KMS. `GetParameter` without `WithDecryption` +//! returns an opaque placeholder instead of the value, so a caller that forgets +//! `WithDecryption=true` fails here the same way it would against AWS. + +use std::collections::BTreeMap; +use std::path::PathBuf; +use std::sync::Mutex; +use std::time::{SystemTime, UNIX_EPOCH}; + +use serde::{Deserialize, Serialize}; +use serde_json::{json, Value}; +use sha2::{Digest, Sha256}; + +const MAX_NAME_CHARS: usize = 2048; +const MAX_STANDARD_VALUE_BYTES: usize = 4096; +const MAX_ADVANCED_VALUE_BYTES: usize = 8192; +const DEFAULT_DESCRIBE_PAGE: usize = 50; +const MAX_DESCRIBE_PAGE: usize = 50; + +#[derive(Clone, Debug, Default)] +pub struct Config { + pub addr: String, + pub region: String, + pub account_id: String, + pub auth_mode: String, + pub access_key_id: String, + pub secret_access_key: String, + pub storage_path: String, +} + +/// One operation result: HTTP status, optional `X-Amzn-Errortype`, JSON body. +pub struct Outcome { + pub status: u16, + pub error_type: Option, + pub body: Value, +} + +impl Outcome { + fn ok(body: Value) -> Self { + Outcome { + status: 200, + error_type: None, + body, + } + } +} + +pub fn error_outcome(status: u16, name: &str, message: &str) -> Outcome { + Outcome { + status, + error_type: Some(name.to_string()), + body: json!({ "__type": name, "message": message }), + } +} + +fn validation(message: &str) -> Outcome { + error_outcome(400, "ValidationException", message) +} + +fn not_found(name: &str) -> Outcome { + error_outcome( + 400, + "ParameterNotFound", + &format!("Parameter {name} not found."), + ) +} + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] +struct Parameter { + #[serde(rename = "Type")] + kind: String, + #[serde(rename = "Value")] + value: String, + #[serde(rename = "Version")] + version: u64, + #[serde(rename = "LastModifiedDate")] + last_modified: f64, + #[serde( + rename = "Description", + default, + skip_serializing_if = "String::is_empty" + )] + description: String, + #[serde(rename = "KeyId", default, skip_serializing_if = "String::is_empty")] + key_id: String, + #[serde(rename = "Tier")] + tier: String, + #[serde(rename = "DataType")] + data_type: String, +} + +#[derive(Default, Serialize, Deserialize)] +struct PersistedState { + #[serde(rename = "Parameters", default)] + parameters: BTreeMap, +} + +pub struct Server { + config: Config, + state: Mutex>, + load_err: Option, +} + +#[derive(Deserialize)] +#[serde(rename_all = "PascalCase")] +struct PutParameterRequest { + #[serde(default)] + name: String, + value: Option, + #[serde(rename = "Type", default)] + kind: String, + #[serde(default)] + overwrite: bool, + #[serde(default)] + description: String, + #[serde(default)] + key_id: String, + #[serde(default)] + tier: String, + #[serde(default)] + data_type: String, +} + +#[derive(Deserialize)] +#[serde(rename_all = "PascalCase")] +struct GetParameterRequest { + #[serde(default)] + name: String, + #[serde(default)] + with_decryption: bool, +} + +#[derive(Deserialize)] +#[serde(rename_all = "PascalCase")] +struct DeleteParameterRequest { + #[serde(default)] + name: String, +} + +#[derive(Deserialize)] +#[serde(rename_all = "PascalCase")] +struct DescribeParametersRequest { + max_results: Option, + #[serde(default)] + next_token: String, + #[serde(default)] + filters: Vec, + #[serde(default)] + parameter_filters: Vec, +} + +fn decode Deserialize<'de>>(body: &[u8]) -> Result { + let body = if body.is_empty() { + b"{}" as &[u8] + } else { + body + }; + serde_json::from_slice(body) + .map_err(|_| error_outcome(400, "SerializationException", "invalid request body")) +} + +fn now_epoch_seconds() -> f64 { + let d = SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap_or_default(); + // Millisecond precision, as the AWS wire form carries. + (d.as_millis() as f64) / 1000.0 +} + +/// AWS name rules this emulator enforces: non-empty, bounded length, a +/// hierarchical name starts with `/`, no reserved `aws`/`ssm` prefix, and only +/// the documented character set (`a-zA-Z0-9_.-/`). +fn validate_name(name: &str) -> Result<(), Outcome> { + if name.is_empty() { + return Err(validation("Parameter name must not be empty.")); + } + if name.chars().count() > MAX_NAME_CHARS { + return Err(validation( + "Parameter name exceeds the maximum length of 2048 characters.", + )); + } + if name.contains('/') && !name.starts_with('/') { + return Err(validation( + "Parameter name: hierarchical names must begin with a forward slash (/).", + )); + } + let first_segment = name.trim_start_matches('/').to_ascii_lowercase(); + if first_segment.starts_with("aws") || first_segment.starts_with("ssm") { + return Err(validation( + "Parameter name: can't be prefixed with \"aws\" or \"ssm\" (case-insensitive).", + )); + } + if !name + .bytes() + .all(|b| b.is_ascii_alphanumeric() || matches!(b, b'_' | b'.' | b'-' | b'/')) + { + return Err(validation( + "Parameter name: only a mix of letters, numbers and the following 4 symbols .-_/ are allowed.", + )); + } + Ok(()) +} + +/// Splits an optional `:` selector off a `GetParameter` name. +/// Labels are not supported; a non-numeric selector is rejected. +fn split_selector(name: &str) -> Result<(&str, Option), Outcome> { + let Some((base, selector)) = name.rsplit_once(':') else { + return Ok((name, None)); + }; + match selector.parse::() { + Ok(v) => Ok((base, Some(v))), + Err(_) => Err(validation( + "Parameter labels are not supported by devcloud; use a numeric version selector.", + )), + } +} + +/// Stand-in for the KMS ciphertext AWS returns when `WithDecryption` is false: +/// opaque, stable per value, and never the plaintext. +fn encrypted_placeholder(value: &str) -> String { + let digest = Sha256::digest(value.as_bytes()); + format!("devcloud-kms-encrypted:{}", hex::encode(digest)) +} + +impl Server { + pub fn new(config: Config) -> Self { + let mut server = Server { + config, + state: Mutex::new(BTreeMap::new()), + load_err: None, + }; + if !server.config.storage_path.is_empty() { + if let Err(e) = server.load() { + server.load_err = Some(e); + } + } + server + } + + pub fn config(&self) -> &Config { + &self.config + } + + pub fn load_err(&self) -> Option<&str> { + self.load_err.as_deref() + } + + fn state_path(&self) -> PathBuf { + PathBuf::from(&self.config.storage_path).join("state.json") + } + + fn load(&mut self) -> Result<(), String> { + let data = match std::fs::read(self.state_path()) { + Ok(d) => d, + Err(e) if e.kind() == std::io::ErrorKind::NotFound => return Ok(()), + Err(e) => return Err(e.to_string()), + }; + let persisted: PersistedState = serde_json::from_slice(&data).map_err(|e| e.to_string())?; + *self.state.get_mut().unwrap() = persisted.parameters; + Ok(()) + } + + /// Atomic write via `state.json.tmp` + rename. + fn persist(&self, parameters: &BTreeMap) -> Result<(), String> { + if self.config.storage_path.is_empty() { + return Ok(()); + } + std::fs::create_dir_all(&self.config.storage_path).map_err(|e| e.to_string())?; + let data = serde_json::to_vec(&PersistedState { + parameters: parameters.clone(), + }) + .map_err(|e| e.to_string())?; + let path = self.state_path(); + let tmp = path.with_extension("json.tmp"); + std::fs::write(&tmp, &data).map_err(|e| e.to_string())?; + std::fs::rename(&tmp, &path).map_err(|e| e.to_string()) + } + + fn region(&self) -> &str { + if self.config.region.is_empty() { + "us-east-1" + } else { + &self.config.region + } + } + + fn account_id(&self) -> &str { + if self.config.account_id.is_empty() { + "000000000000" + } else { + &self.config.account_id + } + } + + fn arn(&self, name: &str) -> String { + let path = if name.starts_with('/') { + name.to_string() + } else { + format!("/{name}") + }; + format!( + "arn:aws:ssm:{}:{}:parameter{path}", + self.region(), + self.account_id() + ) + } + + pub fn dispatch(&self, operation: &str, body: &[u8]) -> Outcome { + match operation { + "PutParameter" => self.put_parameter(body), + "GetParameter" => self.get_parameter(body), + "DeleteParameter" => self.delete_parameter(body), + "DescribeParameters" => self.describe_parameters(body), + _ => error_outcome(400, "UnknownOperationException", "unknown operation"), + } + } + + fn put_parameter(&self, body: &[u8]) -> Outcome { + let req: PutParameterRequest = match decode(body) { + Ok(r) => r, + Err(o) => return o, + }; + if let Err(o) = validate_name(&req.name) { + return o; + } + let Some(value) = req.value else { + return validation("Value is required."); + }; + if value.is_empty() { + return validation("Parameter value must not be empty."); + } + if !req.kind.is_empty() + && !matches!(req.kind.as_str(), "String" | "StringList" | "SecureString") + { + return validation("Type must be one of String, StringList, SecureString."); + } + let tier = match req.tier.as_str() { + "" | "Standard" | "Intelligent-Tiering" => "Standard", + "Advanced" => "Advanced", + _ => return validation("Tier must be one of Standard, Advanced, Intelligent-Tiering."), + }; + let limit = if tier == "Advanced" { + MAX_ADVANCED_VALUE_BYTES + } else { + MAX_STANDARD_VALUE_BYTES + }; + if value.len() > limit { + return validation(&format!( + "Parameter value exceeds the maximum size of {limit} bytes for the {tier} tier." + )); + } + let data_type = match req.data_type.as_str() { + "" | "text" => "text", + "aws:ec2:image" | "aws:ssm:integration" => req.data_type.as_str(), + _ => { + return validation( + "DataType must be one of text, aws:ec2:image, aws:ssm:integration.", + ) + } + }; + + let mut parameters = self.state.lock().unwrap(); + let previous = parameters.get(&req.name).cloned(); + let kind = match (&previous, req.kind.as_str()) { + (Some(_), _) if !req.overwrite => { + return error_outcome( + 400, + "ParameterAlreadyExists", + "The parameter already exists. To overwrite this value, set the overwrite option in the request to true.", + ); + } + (_, k) if !k.is_empty() => k.to_string(), + (Some(p), _) => p.kind.clone(), + (None, _) => return validation("Type is required when creating a parameter."), + }; + let version = previous.as_ref().map_or(1, |p| p.version + 1); + let description = if req.description.is_empty() { + previous + .as_ref() + .map(|p| p.description.clone()) + .unwrap_or_default() + } else { + req.description + }; + let key_id = if kind == "SecureString" { + if req.key_id.is_empty() { + "alias/aws/ssm".to_string() + } else { + req.key_id + } + } else { + String::new() + }; + parameters.insert( + req.name.clone(), + Parameter { + kind, + value, + version, + last_modified: now_epoch_seconds(), + description, + key_id, + tier: tier.to_string(), + data_type: data_type.to_string(), + }, + ); + if self.persist(¶meters).is_err() { + match previous { + Some(p) => parameters.insert(req.name, p), + None => parameters.remove(&req.name), + }; + return error_outcome(500, "InternalServerError", "failed to persist ssm state"); + } + Outcome::ok(json!({ "Version": version, "Tier": tier })) + } + + fn get_parameter(&self, body: &[u8]) -> Outcome { + let req: GetParameterRequest = match decode(body) { + Ok(r) => r, + Err(o) => return o, + }; + let (name, version) = match split_selector(&req.name) { + Ok(v) => v, + Err(o) => return o, + }; + if let Err(o) = validate_name(name) { + return o; + } + let parameters = self.state.lock().unwrap(); + let Some(p) = parameters.get(name) else { + return not_found(&req.name); + }; + // Only the current version is retained. + if let Some(v) = version { + if v != p.version { + return error_outcome( + 400, + "ParameterVersionNotFound", + &format!("Systems Manager could not find version {v} of {name}."), + ); + } + } + let value = if p.kind == "SecureString" && !req.with_decryption { + encrypted_placeholder(&p.value) + } else { + p.value.clone() + }; + let mut out = json!({ + "Name": name, + "Type": p.kind, + "Value": value, + "Version": p.version, + "LastModifiedDate": p.last_modified, + "ARN": self.arn(name), + "DataType": p.data_type, + }); + if version.is_some() { + out["Selector"] = json!(format!(":{}", p.version)); + } + Outcome::ok(json!({ "Parameter": out })) + } + + fn delete_parameter(&self, body: &[u8]) -> Outcome { + let req: DeleteParameterRequest = match decode(body) { + Ok(r) => r, + Err(o) => return o, + }; + if let Err(o) = validate_name(&req.name) { + return o; + } + let mut parameters = self.state.lock().unwrap(); + let Some(previous) = parameters.remove(&req.name) else { + return not_found(&req.name); + }; + if self.persist(¶meters).is_err() { + parameters.insert(req.name, previous); + return error_outcome(500, "InternalServerError", "failed to persist ssm state"); + } + Outcome::ok(json!({})) + } + + /// Metadata only — never values. Filters are rejected rather than ignored + /// so a caller relying on them does not silently get every parameter. + fn describe_parameters(&self, body: &[u8]) -> Outcome { + let req: DescribeParametersRequest = match decode(body) { + Ok(r) => r, + Err(o) => return o, + }; + if !req.filters.is_empty() || !req.parameter_filters.is_empty() { + return validation("DescribeParameters filters are not supported by devcloud."); + } + let page = match req.max_results { + None => DEFAULT_DESCRIBE_PAGE, + Some(n) if (1..=MAX_DESCRIBE_PAGE as i64).contains(&n) => n as usize, + Some(_) => return validation("MaxResults must be between 1 and 50."), + }; + let parameters = self.state.lock().unwrap(); + // NextToken is the last name of the previous page (names are unique and + // the map is ordered). + let items: Vec<(&String, &Parameter)> = parameters + .iter() + .filter(|(name, _)| { + req.next_token.is_empty() || name.as_str() > req.next_token.as_str() + }) + .take(page + 1) + .collect(); + let has_more = items.len() > page; + let listed: Vec = items + .iter() + .take(page) + .map(|(name, p)| { + let mut entry = json!({ + "Name": name, + "ARN": self.arn(name), + "Type": p.kind, + "Version": p.version, + "LastModifiedDate": p.last_modified, + "Tier": p.tier, + "DataType": p.data_type, + }); + if !p.description.is_empty() { + entry["Description"] = json!(p.description); + } + if !p.key_id.is_empty() { + entry["KeyId"] = json!(p.key_id); + } + entry + }) + .collect(); + let mut out = json!({ "Parameters": listed }); + if has_more { + out["NextToken"] = json!(items[page - 1].0); + } + Outcome::ok(out) + } +} diff --git a/services/ssm/src/sigv4.rs b/services/ssm/src/sigv4.rs new file mode 100644 index 0000000..1684c68 --- /dev/null +++ b/services/ssm/src/sigv4.rs @@ -0,0 +1,372 @@ +//! AWS SigV4 verification (relaxed / signed-relaxed / strict), shared shape +//! with `services/applicationautoscaling/src/sigv4.rs`. + +use hmac::{Hmac, Mac}; +use sha2::{Digest, Sha256}; + +type HmacSha256 = Hmac; + +pub const SIGV4_ALGORITHM: &str = "AWS4-HMAC-SHA256"; +pub const SIGV4_SERVICE: &str = "ssm"; + +/// A signature verification failure, carrying the AWS error name and HTTP +/// status the legacy server would return. +#[derive(Debug, Clone)] +pub struct SignatureError { + pub name: &'static str, + pub status: u16, +} + +fn err(name: &'static str, status: u16) -> SignatureError { + SignatureError { name, status } +} + +/// Minimal request view the verifier needs (headers + method + path + query + +/// host + body), so it stays independent of the HTTP framework. +pub struct SignedRequest<'a> { + pub method: &'a str, + pub path: &'a str, + pub query: &'a str, + pub host: &'a str, + pub authorization: &'a str, + pub amz_date: &'a str, + pub content_sha256: &'a str, + /// Resolves a header value by lowercase name (for canonical headers). + pub header: &'a dyn Fn(&str) -> Option, + pub body: &'a [u8], +} + +pub struct Credentials<'a> { + pub auth_mode: &'a str, + pub access_key_id: &'a str, + pub secret_access_key: &'a str, + pub region: &'a str, +} + +fn default_str<'a>(value: &'a str, fallback: &'a str) -> &'a str { + if value.is_empty() { + fallback + } else { + value + } +} + +/// Mirrors `Server.verifySignature`. +pub fn verify_signature(req: &SignedRequest, creds: &Credentials) -> Result<(), SignatureError> { + if creds.auth_mode.is_empty() || creds.auth_mode.eq_ignore_ascii_case("relaxed") { + return Ok(()); + } + let auth = req.authorization.trim(); + if auth.is_empty() { + return Err(err("AccessDeniedException", 403)); + } + if creds.auth_mode.eq_ignore_ascii_case("signed-relaxed") { + return verify_shape(req, auth); + } + verify_full(req, auth, creds) +} + +fn verify_full(req: &SignedRequest, auth: &str, creds: &Credentials) -> Result<(), SignatureError> { + let prefix = format!("{SIGV4_ALGORITHM} "); + if !auth.starts_with(&prefix) { + return Err(err("IncompleteSignatureException", 400)); + } + let values = parse_auth_params(&auth[prefix.len()..]); + let credential = values.get("Credential").cloned().unwrap_or_default(); + let signed_headers = values.get("SignedHeaders").cloned().unwrap_or_default(); + let signature = values.get("Signature").cloned().unwrap_or_default(); + let (access_key, date_stamp, region, service) = match parse_credential_scope(&credential) { + Some(v) => v, + None => return Err(err("IncompleteSignatureException", 400)), + }; + if signed_headers.is_empty() || signature.is_empty() { + return Err(err("IncompleteSignatureException", 400)); + } + if !valid_credential(&access_key, ®ion, &service, creds) { + return Err(err("UnrecognizedClientException", 403)); + } + if req.amz_date.is_empty() { + return Err(err("IncompleteSignatureException", 400)); + } + // SDKs sign the body's SHA-256 for this service without sending the + // header (only S3 uses the header-driven `UNSIGNED-PAYLOAD` convention). + let payload_hash = if req.content_sha256.is_empty() { + sha256_hex(req.body) + } else { + verify_payload_hash(req, req.content_sha256)?; + req.content_sha256.to_string() + }; + let expected = signature_for_request( + req, + &date_stamp, + ®ion, + &signed_headers, + &payload_hash, + creds, + ); + if !constant_time_eq(signature.as_bytes(), expected.as_bytes()) { + return Err(err("InvalidSignatureException", 403)); + } + Ok(()) +} + +fn verify_shape(req: &SignedRequest, auth: &str) -> Result<(), SignatureError> { + let prefix = format!("{SIGV4_ALGORITHM} "); + if !auth.starts_with(&prefix) { + return Err(err("IncompleteSignatureException", 400)); + } + let values = parse_auth_params(&auth[prefix.len()..]); + let credential = values.get("Credential").cloned().unwrap_or_default(); + let signed_headers = values.get("SignedHeaders").cloned().unwrap_or_default(); + let signature = values.get("Signature").cloned().unwrap_or_default(); + let (_, _, _, service) = match parse_credential_scope(&credential) { + Some(v) => v, + None => return Err(err("IncompleteSignatureException", 400)), + }; + if signed_headers.is_empty() || signature.is_empty() { + return Err(err("IncompleteSignatureException", 400)); + } + if service != SIGV4_SERVICE || !is_lower_hex(&signature, 64) { + return Err(err("IncompleteSignatureException", 400)); + } + if req.amz_date.is_empty() { + return Err(err("IncompleteSignatureException", 400)); + } + if !req.content_sha256.is_empty() { + return verify_payload_hash(req, req.content_sha256); + } + Ok(()) +} + +fn is_lower_hex(value: &str, length: usize) -> bool { + value.len() == length + && value + .bytes() + .all(|c| c.is_ascii_digit() || (b'a'..=b'f').contains(&c)) +} + +fn valid_credential(access_key: &str, region: &str, service: &str, creds: &Credentials) -> bool { + let configured_access_key = default_str(creds.access_key_id, "dev"); + let configured_region = default_str(creds.region, "us-east-1"); + access_key == configured_access_key && region == configured_region && service == SIGV4_SERVICE +} + +fn verify_payload_hash(req: &SignedRequest, payload_hash: &str) -> Result<(), SignatureError> { + if payload_hash == "UNSIGNED-PAYLOAD" { + return Ok(()); + } + if payload_hash.starts_with("STREAMING-") { + return Err(err("NotImplemented", 501)); + } + let got = sha256_hex(req.body); + if !constant_time_eq(payload_hash.to_ascii_lowercase().as_bytes(), got.as_bytes()) { + return Err(err("InvalidSignatureException", 403)); + } + Ok(()) +} + +fn signature_for_request( + req: &SignedRequest, + date_stamp: &str, + region: &str, + signed_headers: &str, + payload_hash: &str, + creds: &Credentials, +) -> String { + let canonical_request = [ + req.method, + &canonical_uri(req.path), + &canonical_query_string(req.query), + &canonical_headers(req, signed_headers), + &signed_headers.to_ascii_lowercase(), + payload_hash, + ] + .join("\n"); + let scope = [date_stamp, region, SIGV4_SERVICE, "aws4_request"].join("/"); + let string_to_sign = [ + SIGV4_ALGORITHM, + req.amz_date, + &scope, + &sha256_hex(canonical_request.as_bytes()), + ] + .join("\n"); + let signing_key = derive_signing_key(creds.secret_access_key, date_stamp, region); + hex::encode(hmac_sha256(&signing_key, string_to_sign.as_bytes())) +} + +fn parse_credential_scope(credential: &str) -> Option<(String, String, String, String)> { + let parts: Vec<&str> = credential.split('/').collect(); + if parts.len() != 5 || parts[4] != "aws4_request" { + return None; + } + Some(( + parts[0].to_string(), + parts[1].to_string(), + parts[2].to_string(), + parts[3].to_string(), + )) +} + +fn parse_auth_params(value: &str) -> std::collections::HashMap { + let mut result = std::collections::HashMap::new(); + for part in value.split(',') { + if let Some((k, v)) = part.trim().split_once('=') { + result.insert(k.to_string(), v.to_string()); + } + } + result +} + +fn canonical_uri(path: &str) -> String { + if path.is_empty() { + return "/".to_string(); + } + aws_percent_encode(path, "/~") +} + +fn canonical_query_string(query: &str) -> String { + if query.is_empty() { + return String::new(); + } + let mut pairs: Vec<(String, String)> = Vec::new(); + for item in query.split('&') { + match item.split_once('=') { + Some((k, v)) => pairs.push((url_decode(k), url_decode(v))), + None => pairs.push((url_decode(item), String::new())), + } + } + pairs.sort_by(|a, b| a.0.cmp(&b.0).then(a.1.cmp(&b.1))); + pairs + .iter() + .map(|(k, v)| { + format!( + "{}={}", + aws_percent_encode(k, "~-_"), + aws_percent_encode(v, "~-_") + ) + }) + .collect::>() + .join("&") +} + +fn canonical_headers(req: &SignedRequest, signed_headers: &str) -> String { + let mut out = String::new(); + for name in signed_headers.to_ascii_lowercase().split(';') { + let name = name.trim(); + if name.is_empty() { + continue; + } + let value = if name == "host" { + req.host.to_string() + } else { + (req.header)(name).unwrap_or_default() + }; + out.push_str(name); + out.push(':'); + out.push_str(&normalize_header_value(&value)); + out.push('\n'); + } + out +} + +fn normalize_header_value(value: &str) -> String { + value.split_whitespace().collect::>().join(" ") +} + +fn aws_percent_encode(value: &str, safe: &str) -> String { + let mut out = String::with_capacity(value.len()); + for &c in value.as_bytes() { + let ch = c as char; + if ch.is_ascii_alphanumeric() + || ch == '-' + || ch == '_' + || ch == '.' + || ch == '~' + || safe.contains(ch) + { + out.push(ch); + } else { + out.push_str(&format!("%{:02X}", c)); + } + } + out +} + +fn url_decode(s: &str) -> String { + let bytes = s.as_bytes(); + let mut out = Vec::with_capacity(bytes.len()); + let mut i = 0; + while i < bytes.len() { + match bytes[i] { + b'%' if i + 2 < bytes.len() => { + if let (Some(hi), Some(lo)) = ( + (bytes[i + 1] as char).to_digit(16), + (bytes[i + 2] as char).to_digit(16), + ) { + out.push((hi * 16 + lo) as u8); + i += 3; + continue; + } + out.push(bytes[i]); + i += 1; + } + b'+' => { + out.push(b' '); + i += 1; + } + b => { + out.push(b); + i += 1; + } + } + } + String::from_utf8_lossy(&out).into_owned() +} + +fn derive_signing_key(secret: &str, date_stamp: &str, region: &str) -> Vec { + let secret = default_str(secret, "dev"); + let date_key = hmac_sha256(format!("AWS4{secret}").as_bytes(), date_stamp.as_bytes()); + let region_key = hmac_sha256(&date_key, region.as_bytes()); + let service_key = hmac_sha256(®ion_key, SIGV4_SERVICE.as_bytes()); + hmac_sha256(&service_key, b"aws4_request") +} + +fn hmac_sha256(key: &[u8], value: &[u8]) -> Vec { + let mut mac = HmacSha256::new_from_slice(key).expect("HMAC accepts any key length"); + mac.update(value); + mac.finalize().into_bytes().to_vec() +} + +fn sha256_hex(value: &[u8]) -> String { + let mut hasher = Sha256::new(); + hasher.update(value); + hex::encode(hasher.finalize()) +} + +fn constant_time_eq(a: &[u8], b: &[u8]) -> bool { + if a.len() != b.len() { + return false; + } + let mut diff = 0u8; + for (x, y) in a.iter().zip(b.iter()) { + diff |= x ^ y; + } + diff == 0 +} + +#[cfg(test)] +mod percent_decode_tests { + use super::*; + + #[test] + fn malformed_percent_escapes_next_to_unicode_do_not_panic() { + for value in ["%あ", "%aあ", "猫%猫", "%é", "%💾", "%", "%2", "%zz"] { + assert_eq!(url_decode(value), value); + } + } + + #[test] + fn valid_percent_escapes_and_plus_are_preserved() { + assert_eq!(url_decode("%E7%8C%AB+%2B%2F"), "猫 +/"); + } +} diff --git a/services/ssm/tests/parameters_test.rs b/services/ssm/tests/parameters_test.rs new file mode 100644 index 0000000..ba78621 --- /dev/null +++ b/services/ssm/tests/parameters_test.rs @@ -0,0 +1,296 @@ +//! SSM Parameter Store behavior through the full request pipeline. + +use devcloud_ssm::http::serve_for_test; +use devcloud_ssm::{Config, Server}; +use serde_json::{json, Value}; + +fn server(storage: &str) -> Server { + Server::new(Config { + region: "ap-northeast-1".to_string(), + account_id: "123456789012".to_string(), + auth_mode: "relaxed".to_string(), + storage_path: storage.to_string(), + ..Default::default() + }) +} + +fn call(s: &Server, op: &str, body: Value) -> (u16, Option, Value) { + let target = format!("AmazonSSM.{op}"); + let resp = serve_for_test( + s, + "POST", + "/", + &[ + ("Content-Type", "application/x-amz-json-1.1"), + ("X-Amz-Target", &target), + ], + body.to_string().as_bytes(), + ); + ( + resp.status, + resp.error_type, + serde_json::from_slice(&resp.body).unwrap(), + ) +} + +fn temp_dir(name: &str) -> String { + let dir = std::env::temp_dir().join(format!( + "devcloud-ssm-{name}-{}-{}", + std::process::id(), + std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap() + .as_nanos() + )); + dir.to_string_lossy().into_owned() +} + +#[test] +fn put_then_get_string() { + let s = server(""); + let (status, _, body) = call( + &s, + "PutParameter", + json!({"Name": "/csa/dwh/user", "Value": "loader", "Type": "String"}), + ); + assert_eq!(status, 200); + assert_eq!(body, json!({"Version": 1, "Tier": "Standard"})); + + let (status, _, body) = call(&s, "GetParameter", json!({"Name": "/csa/dwh/user"})); + assert_eq!(status, 200); + let p = &body["Parameter"]; + assert_eq!(p["Name"], "/csa/dwh/user"); + assert_eq!(p["Type"], "String"); + assert_eq!(p["Value"], "loader"); + assert_eq!(p["Version"], 1); + assert_eq!(p["DataType"], "text"); + assert_eq!( + p["ARN"], + "arn:aws:ssm:ap-northeast-1:123456789012:parameter/csa/dwh/user" + ); + assert!(p["LastModifiedDate"].as_f64().unwrap() > 1_700_000_000.0); +} + +#[test] +fn secure_string_requires_with_decryption() { + let s = server(""); + call( + &s, + "PutParameter", + json!({"Name": "/csa/dwh/password", "Value": "s3cret", "Type": "SecureString"}), + ); + + let (_, _, plain) = call( + &s, + "GetParameter", + json!({"Name": "/csa/dwh/password", "WithDecryption": true}), + ); + assert_eq!(plain["Parameter"]["Value"], "s3cret"); + assert_eq!(plain["Parameter"]["Type"], "SecureString"); + + let (_, _, opaque) = call(&s, "GetParameter", json!({"Name": "/csa/dwh/password"})); + let value = opaque["Parameter"]["Value"].as_str().unwrap(); + assert_ne!(value, "s3cret"); + assert!(!value.contains("s3cret")); +} + +#[test] +fn overwrite_rules_and_versioning() { + let s = server(""); + call( + &s, + "PutParameter", + json!({"Name": "role", "Value": "a", "Type": "String"}), + ); + + let (status, err, _) = call( + &s, + "PutParameter", + json!({"Name": "role", "Value": "b", "Type": "String"}), + ); + assert_eq!( + (status, err.as_deref()), + (400, Some("ParameterAlreadyExists")) + ); + + // Type may be omitted on overwrite: the existing type is kept. + let (status, _, body) = call( + &s, + "PutParameter", + json!({"Name": "role", "Value": "b", "Overwrite": true}), + ); + assert_eq!(status, 200); + assert_eq!(body["Version"], 2); + + let (_, _, body) = call(&s, "GetParameter", json!({"Name": "role"})); + assert_eq!(body["Parameter"]["Value"], "b"); + assert_eq!(body["Parameter"]["Type"], "String"); + + let (status, _, body) = call(&s, "GetParameter", json!({"Name": "role:2"})); + assert_eq!(status, 200); + assert_eq!(body["Parameter"]["Selector"], ":2"); + let (status, err, _) = call(&s, "GetParameter", json!({"Name": "role:1"})); + assert_eq!( + (status, err.as_deref()), + (400, Some("ParameterVersionNotFound")) + ); +} + +#[test] +fn validation_errors() { + let s = server(""); + let cases = [ + json!({"Name": "", "Value": "v", "Type": "String"}), + json!({"Name": "a/b", "Value": "v", "Type": "String"}), + json!({"Name": "/aws/x", "Value": "v", "Type": "String"}), + json!({"Name": "SSM-x", "Value": "v", "Type": "String"}), + json!({"Name": "bad name", "Value": "v", "Type": "String"}), + json!({"Name": "x", "Value": "v", "Type": "Number"}), + json!({"Name": "x", "Value": "", "Type": "String"}), + json!({"Name": "x", "Type": "String"}), + json!({"Name": "x", "Value": "v"}), + json!({"Name": "x", "Value": "v".repeat(4097), "Type": "String"}), + ]; + for body in cases { + let (status, err, _) = call(&s, "PutParameter", body.clone()); + assert_eq!( + (status, err.as_deref()), + (400, Some("ValidationException")), + "{body}" + ); + } + let (status, _, _) = call( + &s, + "PutParameter", + json!({"Name": "x", "Value": "v".repeat(4097), "Type": "String", "Tier": "Advanced"}), + ); + assert_eq!(status, 200); +} + +#[test] +fn missing_parameter_and_unknown_operation() { + let s = server(""); + let (status, err, body) = call(&s, "GetParameter", json!({"Name": "/nope"})); + assert_eq!((status, err.as_deref()), (400, Some("ParameterNotFound"))); + assert_eq!(body["__type"], "ParameterNotFound"); + let (status, err, _) = call(&s, "DeleteParameter", json!({"Name": "/nope"})); + assert_eq!((status, err.as_deref()), (400, Some("ParameterNotFound"))); + let (status, err, _) = call(&s, "GetParametersByPath", json!({"Path": "/"})); + assert_eq!( + (status, err.as_deref()), + (400, Some("UnknownOperationException")) + ); +} + +#[test] +fn delete_removes_parameter() { + let s = server(""); + call( + &s, + "PutParameter", + json!({"Name": "gone", "Value": "v", "Type": "String"}), + ); + let (status, _, body) = call(&s, "DeleteParameter", json!({"Name": "gone"})); + assert_eq!((status, body), (200, json!({}))); + let (status, _, _) = call(&s, "GetParameter", json!({"Name": "gone"})); + assert_eq!(status, 400); +} + +#[test] +fn describe_lists_metadata_without_values_and_paginates() { + let s = server(""); + for i in 0..3 { + call( + &s, + "PutParameter", + json!({"Name": format!("/p/{i}"), "Value": format!("secret-{i}"), "Type": "SecureString", "Description": "d"}), + ); + } + let (status, _, page1) = call(&s, "DescribeParameters", json!({"MaxResults": 2})); + assert_eq!(status, 200); + let names: Vec<_> = page1["Parameters"] + .as_array() + .unwrap() + .iter() + .map(|p| p["Name"].as_str().unwrap().to_string()) + .collect(); + assert_eq!(names, ["/p/0", "/p/1"]); + assert!(!page1.to_string().contains("secret-")); + assert_eq!(page1["Parameters"][0]["KeyId"], "alias/aws/ssm"); + assert_eq!(page1["Parameters"][0]["Description"], "d"); + + let token = page1["NextToken"].as_str().unwrap(); + let (_, _, page2) = call( + &s, + "DescribeParameters", + json!({"MaxResults": 2, "NextToken": token}), + ); + assert_eq!(page2["Parameters"].as_array().unwrap().len(), 1); + assert!(page2.get("NextToken").is_none()); + + let (status, err, _) = call( + &s, + "DescribeParameters", + json!({"ParameterFilters": [{"Key": "Name", "Values": ["/p/0"]}]}), + ); + assert_eq!((status, err.as_deref()), (400, Some("ValidationException"))); +} + +#[test] +fn state_persists_across_restart() { + let dir = temp_dir("persist"); + { + let s = server(&dir); + call( + &s, + "PutParameter", + json!({"Name": "/k", "Value": "v1", "Type": "SecureString"}), + ); + } + let s = server(&dir); + assert!(s.load_err().is_none()); + let (status, _, body) = call( + &s, + "GetParameter", + json!({"Name": "/k", "WithDecryption": true}), + ); + assert_eq!(status, 200); + assert_eq!(body["Parameter"]["Value"], "v1"); + std::fs::remove_dir_all(&dir).unwrap(); +} + +#[test] +fn rejects_wrong_method_path_and_content_type() { + let s = server(""); + let r = serve_for_test(&s, "GET", "/", &[], b""); + assert_eq!(r.status, 405); + let r = serve_for_test(&s, "POST", "/x", &[], b""); + assert_eq!(r.status, 404); + let r = serve_for_test( + &s, + "POST", + "/", + &[ + ("Content-Type", "text/plain"), + ("X-Amz-Target", "AmazonSSM.GetParameter"), + ], + b"{}", + ); + assert_eq!(r.status, 400); +} + +#[test] +fn strict_mode_requires_signature() { + let s = Server::new(Config { + auth_mode: "strict".to_string(), + ..Default::default() + }); + let r = serve_for_test( + &s, + "POST", + "/", + &[("X-Amz-Target", "AmazonSSM.GetParameter")], + br#"{"Name":"x"}"#, + ); + assert_eq!(r.status, 403); +} diff --git a/services/sts/Cargo.toml b/services/sts/Cargo.toml new file mode 100644 index 0000000..bf079b9 --- /dev/null +++ b/services/sts/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "devcloud-sts" +version = "0.1.0" +edition = "2021" +description = "devcloud STS emulator (AWS Query)." + +[lib] +name = "devcloud_sts" +path = "src/lib.rs" + +[dependencies] +tokio = { version = "1", features = ["rt", "rt-multi-thread", "macros", "net", "io-util", "time"] } +serde = { version = "1", features = ["derive"] } +serde_json = "1" +hmac = "0.12" +sha2 = "0.10" +hex = "0.4" diff --git a/services/sts/src/http.rs b/services/sts/src/http.rs new file mode 100644 index 0000000..3e2fe17 --- /dev/null +++ b/services/sts/src/http.rs @@ -0,0 +1,403 @@ +//! Minimal HTTP/1.1 front-end for the STS AWS Query protocol: `POST /` with a +//! form-encoded body (or `GET /?Action=...`); SigV4 is verified per auth mode; +//! the merged query + form parameters are dispatched on `Action`. Responses +//! are XML. Same transport as `services/ssm/src/http.rs`. + +use std::collections::{BTreeMap, HashMap}; +use std::sync::Arc; + +use tokio::io::{AsyncRead, AsyncReadExt, AsyncWriteExt}; +use tokio::net::{TcpListener, TcpStream}; + +use crate::server::{error_outcome, Outcome, Server}; +use crate::sigv4::{verify_signature, Credentials, SignedRequest}; + +const MAX_HEADER_BYTES: usize = 64 * 1024; +const MAX_BODY_BYTES: usize = 8 * 1024 * 1024; + +/// A parsed HTTP request: method, path, query, and lowercase-keyed headers. +struct Request { + method: String, + path: String, + query: String, + headers: HashMap, + body: Vec, +} + +impl Request { + fn header(&self, name: &str) -> &str { + self.headers.get(name).map(String::as_str).unwrap_or("") + } +} + +/// Runs the accept loop until `shutdown` resolves. Each connection is served on +/// its own task; connections are closed after one request (`Connection: close`). +pub async fn serve( + listener: TcpListener, + server: Arc, + shutdown: impl std::future::Future, +) -> std::io::Result<()> { + tokio::pin!(shutdown); + loop { + tokio::select! { + _ = &mut shutdown => return Ok(()), + accepted = listener.accept() => { + let (stream, _) = match accepted { + Ok(a) => a, + // Out of descriptors, or a peer gone before the accept: + // back off and keep serving instead of stopping (which + // would take every devcloud service down with it). + Err(e) => { + // Only transient errors; a broken listener still stops. + let transient = matches!( + e.kind(), + std::io::ErrorKind::ConnectionAborted + | std::io::ErrorKind::ConnectionReset + | std::io::ErrorKind::Interrupted + | std::io::ErrorKind::WouldBlock + | std::io::ErrorKind::TimedOut + ) || matches!(e.raw_os_error(), Some(12 | 23 | 24)); // ENOMEM, ENFILE, EMFILE + if !transient { + return Err(e); + } + tokio::time::sleep(std::time::Duration::from_millis(100)).await; + continue; + } + }; + let server = Arc::clone(&server); + tokio::spawn(async move { + let _ = handle_conn(stream, server).await; + }); + } + } + } +} + +async fn handle_conn(mut stream: TcpStream, server: Arc) -> std::io::Result<()> { + let request = match read_request(&mut stream).await { + Ok(Some(req)) => req, + Ok(None) => return Ok(()), // closed or malformed; drop quietly + Err(_) => return Ok(()), + }; + let outcome = process(&server, &request); + write_response(&mut stream, outcome).await +} + +/// Reads one HTTP/1.1 request: request line + headers (until CRLFCRLF) then a +/// `Content-Length`-bounded body. Returns `None` on clean EOF or malformed head. +async fn read_request(stream: &mut (impl AsyncRead + Unpin)) -> std::io::Result> { + let mut buf = Vec::new(); + let mut tmp = [0u8; 4096]; + let header_end = loop { + if let Some(pos) = find_subslice(&buf, b"\r\n\r\n") { + if pos + 4 > MAX_HEADER_BYTES { + return Ok(None); + } + break pos; + } + if buf.len() > MAX_HEADER_BYTES { + return Ok(None); + } + let n = stream.read(&mut tmp).await?; + if n == 0 { + return Ok(None); + } + buf.extend_from_slice(&tmp[..n]); + }; + + let head = String::from_utf8_lossy(&buf[..header_end]).into_owned(); + let mut lines = head.split("\r\n"); + let request_line = lines.next().unwrap_or(""); + let mut parts = request_line.split(' '); + let method = parts.next().unwrap_or("").to_string(); + let target = parts.next().unwrap_or("/"); + let (path, query) = match target.split_once('?') { + Some((p, q)) => (p.to_string(), q.to_string()), + None => (target.to_string(), String::new()), + }; + + let mut headers = HashMap::new(); + for line in lines { + if let Some((k, v)) = line.split_once(':') { + let key = k.trim().to_ascii_lowercase(); + if matches!(key.as_str(), "content-length" | "transfer-encoding") + && headers.contains_key(&key) + { + return Ok(None); + } + headers.insert(key, v.trim().to_string()); + } + } + + // This transport supports fixed-length request bodies only. Never + // dispatch an unsupported transfer coding as an empty request body. + if headers.contains_key("transfer-encoding") { + return Ok(None); + } + let content_length: usize = match headers.get("content-length") { + None => 0, + Some(value) if !value.is_empty() && value.bytes().all(|b| b.is_ascii_digit()) => { + match value.parse() { + Ok(length) => length, + Err(_) => return Ok(None), + } + } + Some(_) => return Ok(None), + }; + if content_length > MAX_BODY_BYTES { + return Ok(None); + } + + let mut body = buf[header_end + 4..].to_vec(); + while body.len() < content_length { + let n = stream.read(&mut tmp).await?; + if n == 0 { + return Ok(None); + } + body.extend_from_slice(&tmp[..n]); + } + body.truncate(content_length); + + Ok(Some(Request { + method, + path, + query, + headers, + body, + })) +} + +async fn write_response(stream: &mut TcpStream, outcome: Outcome) -> std::io::Result<()> { + let body = outcome.body.into_bytes(); + let mut head = format!( + "HTTP/1.1 {} {}\r\n", + outcome.status, + reason_phrase(outcome.status) + ); + head.push_str("Server: devcloud-sts\r\n"); + head.push_str("Content-Type: text/xml\r\n"); + if outcome.status == 405 { + head.push_str("Allow: GET, POST\r\n"); + } + head.push_str(&format!("Content-Length: {}\r\n", body.len())); + head.push_str("Connection: close\r\n\r\n"); + + stream.write_all(head.as_bytes()).await?; + stream.write_all(&body).await?; + stream.flush().await +} + +fn process(server: &Server, req: &Request) -> Outcome { + if req.path != "/" { + return error_outcome(404, "NotFound", "not found"); + } + if req.method != "POST" && req.method != "GET" { + return error_outcome(405, "InvalidAction", "method not allowed"); + } + let content_type = req.header("content-type"); + if req.method == "POST" + && !content_type.is_empty() + && !content_type.starts_with("application/x-www-form-urlencoded") + { + return error_outcome(400, "InvalidParameterValue", "unsupported content type"); + } + + let header_fn = |name: &str| -> Option { req.headers.get(name).cloned() }; + let signed = SignedRequest { + method: &req.method, + path: &req.path, + query: &req.query, + host: req.header("host"), + authorization: req.header("authorization"), + amz_date: req.header("x-amz-date"), + content_sha256: req.header("x-amz-content-sha256"), + header: &header_fn, + body: &req.body, + }; + let cfg = server.config(); + let creds = Credentials { + auth_mode: &cfg.auth_mode, + access_key_id: &cfg.access_key_id, + secret_access_key: &cfg.secret_access_key, + region: &cfg.region, + }; + if let Err(e) = verify_signature(&signed, &creds) { + return error_outcome(e.status, e.name, e.name); + } + + let mut params = BTreeMap::new(); + parse_form(&req.query, &mut params); + if req.method == "POST" { + parse_form(&String::from_utf8_lossy(&req.body), &mut params); + } + server.dispatch(¶ms) +} + +/// `application/x-www-form-urlencoded` decoding; later keys win. +fn parse_form(input: &str, out: &mut BTreeMap) { + for pair in input.split('&').filter(|p| !p.is_empty()) { + let (k, v) = pair.split_once('=').unwrap_or((pair, "")); + out.insert(form_decode(k), form_decode(v)); + } +} + +fn form_decode(value: &str) -> String { + let bytes = value.as_bytes(); + let mut out = Vec::with_capacity(bytes.len()); + let mut i = 0; + while i < bytes.len() { + match bytes[i] { + b'+' => out.push(b' '), + // Two hex digits exactly; `from_str_radix` alone would also take + // a sign (`%+1`). + b'%' if i + 2 < bytes.len() + && bytes[i + 1].is_ascii_hexdigit() + && bytes[i + 2].is_ascii_hexdigit() => + { + let hex = std::str::from_utf8(&bytes[i + 1..i + 3]).unwrap_or_default(); + out.push(u8::from_str_radix(hex, 16).unwrap_or_default()); + i += 2; + } + b => out.push(b), + } + i += 1; + } + String::from_utf8_lossy(&out).into_owned() +} + +fn reason_phrase(status: u16) -> &'static str { + match status { + 200 => "OK", + 400 => "Bad Request", + 403 => "Forbidden", + 404 => "Not Found", + 405 => "Method Not Allowed", + 500 => "Internal Server Error", + 501 => "Not Implemented", + _ => "Status", + } +} + +fn find_subslice(haystack: &[u8], needle: &[u8]) -> Option { + if needle.is_empty() || haystack.len() < needle.len() { + return None; + } + haystack + .windows(needle.len()) + .position(|window| window == needle) +} + +// --- In-process test harness (mirrors what a real HTTP request would produce) --- + +pub struct TestResponse { + pub status: u16, + pub body: String, +} + +/// Drives the full request pipeline (gate → SigV4 → dispatch) without a socket. +/// `headers` are `(name, value)` pairs; `path` may include a `?query`. +pub fn serve_for_test( + server: &Server, + method: &str, + path: &str, + headers: &[(&str, &str)], + body: &[u8], +) -> TestResponse { + let (p, q) = match path.split_once('?') { + Some((p, q)) => (p.to_string(), q.to_string()), + None => (path.to_string(), String::new()), + }; + let header_map: HashMap = headers + .iter() + .map(|(k, v)| (k.to_ascii_lowercase(), v.to_string())) + .collect(); + let req = Request { + method: method.to_string(), + path: p, + query: q, + headers: header_map, + body: body.to_vec(), + }; + let outcome = process(server, &req); + TestResponse { + status: outcome.status, + body: outcome.body, + } +} + +#[cfg(test)] +mod request_framing_tests { + use super::*; + + #[tokio::test] + async fn incomplete_fixed_length_body_is_rejected() { + // A complete JSON value is still an incomplete HTTP message. It must + // never reach a mutating handler after the client disconnects early. + let mut wire = &b"POST / HTTP/1.1\r\nContent-Length: 9\r\n\r\n{}"[..]; + assert!(read_request(&mut wire).await.unwrap().is_none()); + } + + #[tokio::test] + async fn invalid_content_length_is_rejected() { + for value in ["bad", "-1", "+2", "", "18446744073709551616"] { + let wire = format!("POST / HTTP/1.1\r\nContent-Length: {value}\r\n\r\n{{}}"); + assert!( + read_request(&mut wire.as_bytes()).await.unwrap().is_none(), + "{value:?}" + ); + } + } + + #[tokio::test] + async fn ambiguous_or_unsupported_body_framing_is_rejected() { + for headers in [ + "Transfer-Encoding: gzip\r\n", + "Transfer-Encoding: xchunked\r\n", + "Transfer-Encoding: chunked, gzip\r\n", + "Content-Length: 9\r\nContent-Length: 2\r\n", + "Content-Length: 2\r\nTransfer-Encoding: chunked\r\n", + "Transfer-Encoding: chunked\r\nTransfer-Encoding: chunked\r\n", + ] { + let wire = format!("POST / HTTP/1.1\r\n{headers}\r\n{{}}"); + assert!( + read_request(&mut wire.as_bytes()).await.unwrap().is_none(), + "{headers:?}" + ); + } + } + + #[tokio::test] + async fn complete_fixed_length_body_is_preserved() { + let mut wire = &b"POST / HTTP/1.1\r\nContent-Length: 2\r\n\r\n{}"[..]; + let request = read_request(&mut wire).await.unwrap().unwrap(); + assert_eq!(request.body, b"{}"); + } + + #[tokio::test] + async fn oversized_header_is_rejected_even_when_terminator_arrives() { + let wire = format!( + "POST / HTTP/1.1\r\nX-Padding: {}\r\n\r\n", + "a".repeat(MAX_HEADER_BYTES) + ); + assert!(read_request(&mut wire.as_bytes()).await.unwrap().is_none()); + } +} + +#[cfg(test)] +mod form_tests { + use super::form_decode; + + #[test] + fn decodes_escapes_and_plus() { + assert_eq!(form_decode("a%2Fb+c%3d"), "a/b c="); + } + + #[test] + fn keeps_malformed_escapes() { + for raw in ["%-1", "%zz", "%2", "%", "100%"] { + assert_eq!(form_decode(raw), raw, "{raw}"); + } + // Not an escape: the `%` stays and `+` is a space as usual. + assert_eq!(form_decode("%+1"), "% 1"); + } +} diff --git a/services/sts/src/lib.rs b/services/sts/src/lib.rs new file mode 100644 index 0000000..a312c5f --- /dev/null +++ b/services/sts/src/lib.rs @@ -0,0 +1,8 @@ +//! devcloud STS: `AssumeRole` (inline session policies accepted, not +//! evaluated) and `GetCallerIdentity` over the AWS Query protocol. Stateless. + +pub mod http; +pub mod server; +pub mod sigv4; + +pub use server::{Config, Server}; diff --git a/services/sts/src/server.rs b/services/sts/src/server.rs new file mode 100644 index 0000000..4a38bf4 --- /dev/null +++ b/services/sts/src/server.rs @@ -0,0 +1,311 @@ +//! STS over the AWS Query protocol: `AssumeRole` and `GetCallerIdentity`. +//! +//! Stateless. `AssumeRole` validates its inputs the way AWS does, accepts an +//! inline session `Policy` (it must be JSON) without evaluating it, and issues +//! fresh temporary credentials. devcloud services in `relaxed` auth mode +//! accept those credentials; in `strict` mode they only accept the configured +//! static key, so temporary credentials are rejected there. + +use std::collections::BTreeMap; +use std::sync::atomic::{AtomicU64, Ordering}; +use std::time::{SystemTime, UNIX_EPOCH}; + +use sha2::{Digest, Sha256}; + +const XMLNS: &str = "https://sts.amazonaws.com/doc/2011-06-15/"; +const DEFAULT_DURATION_SECONDS: i64 = 3600; +const MIN_DURATION_SECONDS: i64 = 900; +const MAX_DURATION_SECONDS: i64 = 43200; +const MAX_POLICY_CHARS: usize = 2048; + +#[derive(Clone, Debug, Default)] +pub struct Config { + pub addr: String, + pub region: String, + pub account_id: String, + pub auth_mode: String, + pub access_key_id: String, + pub secret_access_key: String, +} + +/// One operation result: HTTP status and XML body. +pub struct Outcome { + pub status: u16, + pub body: String, +} + +pub fn error_outcome(status: u16, code: &str, message: &str) -> Outcome { + let kind = if status >= 500 { "Receiver" } else { "Sender" }; + Outcome { + status, + body: format!( + "{kind}{}{}{}", + xml_escape(code), + xml_escape(message), + request_id(), + ), + } +} + +fn validation(message: &str) -> Outcome { + error_outcome(400, "ValidationError", message) +} + +pub struct Server { + config: Config, +} + +static COUNTER: AtomicU64 = AtomicU64::new(0); + +/// 32 hex chars, unique per call within the process and across restarts +/// (time + counter + pid hashed). Not a secret: these credentials only gate +/// local emulators. +fn unique_hex(tag: &str) -> String { + let now = SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap_or_default() + .as_nanos(); + let n = COUNTER.fetch_add(1, Ordering::Relaxed); + let digest = Sha256::digest(format!("{tag}:{now}:{n}:{}", std::process::id()).as_bytes()); + hex::encode(digest)[..32].to_string() +} + +fn request_id() -> String { + let h = unique_hex("request"); + format!( + "{}-{}-{}-{}-{}", + &h[0..8], + &h[8..12], + &h[12..16], + &h[16..20], + &h[20..32] + ) +} + +fn xml_escape(value: &str) -> String { + let mut out = String::with_capacity(value.len()); + for c in value.chars() { + match c { + '&' => out.push_str("&"), + '<' => out.push_str("<"), + '>' => out.push_str(">"), + '"' => out.push_str("""), + '\'' => out.push_str("'"), + _ => out.push(c), + } + } + out +} + +/// RFC 3339 UTC with second precision (`2026-11-03T10:00:00Z`). +fn rfc3339(secs: i64) -> String { + let days = secs.div_euclid(86_400); + let sod = secs.rem_euclid(86_400); + // Howard Hinnant's civil_from_days. + let z = days + 719_468; + let era = if z >= 0 { z } else { z - 146_096 } / 146_097; + let doe = z - era * 146_097; + let yoe = (doe - doe / 1_460 + doe / 36_524 - doe / 146_096) / 365; + let doy = doe - (365 * yoe + yoe / 4 - yoe / 100); + let mp = (5 * doy + 2) / 153; + let d = doy - (153 * mp + 2) / 5 + 1; + let m = if mp < 10 { mp + 3 } else { mp - 9 }; + let y = yoe + era * 400 + if m <= 2 { 1 } else { 0 }; + format!( + "{y:04}-{m:02}-{d:02}T{:02}:{:02}:{:02}Z", + sod / 3600, + (sod % 3600) / 60, + sod % 60 + ) +} + +fn valid_session_name(name: &str) -> bool { + (2..=64).contains(&name.len()) + && name + .bytes() + .all(|b| b.is_ascii_alphanumeric() || b"_+=,.@-".contains(&b)) +} + +/// `arn:aws:iam:::role/name` → (account, role name). +fn parse_role_arn(arn: &str) -> Option<(&str, &str)> { + let rest = arn.strip_prefix("arn:")?; + let mut parts = rest.splitn(5, ':'); + let _partition = parts.next()?; + let service = parts.next()?; + let _region = parts.next()?; + let account = parts.next()?; + let resource = parts.next()?; + let role_path = resource.strip_prefix("role/")?; + let name = role_path.rsplit('/').next()?; + if service != "iam" || account.is_empty() || name.is_empty() { + return None; + } + Some((account, name)) +} + +impl Server { + pub fn new(config: Config) -> Self { + Server { config } + } + + pub fn config(&self) -> &Config { + &self.config + } + + fn account_id(&self) -> &str { + if self.config.account_id.is_empty() { + "000000000000" + } else { + &self.config.account_id + } + } + + /// `params` are the merged query-string and form-body parameters. + pub fn dispatch(&self, params: &BTreeMap) -> Outcome { + let action = params.get("Action").map(String::as_str).unwrap_or(""); + match action { + "AssumeRole" => self.assume_role(params), + "GetCallerIdentity" => self.get_caller_identity(), + "" => error_outcome(400, "MissingAction", "Missing Action"), + other => error_outcome( + 400, + "InvalidAction", + &format!("Could not find operation {other} for version 2011-06-15"), + ), + } + } + + fn assume_role(&self, params: &BTreeMap) -> Outcome { + let get = |k: &str| params.get(k).map(String::as_str).unwrap_or(""); + let role_arn = get("RoleArn"); + let session_name = get("RoleSessionName"); + if role_arn.is_empty() { + return validation("1 validation error detected: Value null at 'roleArn' failed to satisfy constraint: Member must not be null"); + } + if session_name.is_empty() { + return validation("1 validation error detected: Value null at 'roleSessionName' failed to satisfy constraint: Member must not be null"); + } + if !valid_session_name(session_name) { + return validation( + "1 validation error detected: Value at 'roleSessionName' failed to satisfy constraint: Member must satisfy regular expression pattern: [\\w+=,.@-]*, with length between 2 and 64", + ); + } + let Some((account, role_name)) = parse_role_arn(role_arn) else { + return validation(&format!("{role_arn} is invalid")); + }; + let duration = match params.get("DurationSeconds") { + None => DEFAULT_DURATION_SECONDS, + Some(v) => match v.parse::() { + Ok(d) if (MIN_DURATION_SECONDS..=MAX_DURATION_SECONDS).contains(&d) => d, + _ => { + return validation( + "1 validation error detected: Value at 'durationSeconds' failed to satisfy constraint: Member must have value between 900 and 43200", + ) + } + }, + }; + let policy = get("Policy"); + let mut packed_policy_size = None; + if !policy.is_empty() { + if policy.chars().count() > MAX_POLICY_CHARS + || serde_json::from_str::(policy).is_err() + { + return error_outcome( + 400, + "MalformedPolicyDocument", + "The policy is not in the valid JSON format.", + ); + } + // AWS reports the packed size as a percentage of the limit. + packed_policy_size = Some((policy.len() * 100 / MAX_POLICY_CHARS).max(1)); + } + + let now = SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap_or_default() + .as_secs() as i64; + let access_key_id = format!("ASIA{}", &unique_hex("akid")[..16].to_ascii_uppercase()); + let secret_access_key = format!("{}{}", unique_hex("secret"), &unique_hex("secret2")[..8]); + let session_token = format!( + "devcloud-sts-{}{}", + unique_hex("token"), + unique_hex("token2") + ); + let role_id = format!("AROA{}", &unique_hex(role_arn)[..17].to_ascii_uppercase()); + + let packed = packed_policy_size + .map(|n| format!("{n}")) + .unwrap_or_default(); + Outcome { + status: 200, + body: format!( + concat!( + "", + "{akid}", + "{secret}", + "{token}", + "{expiration}", + "{role_id}:{session}", + "arn:aws:sts::{account}:assumed-role/{role}/{session}", + "{packed}", + "{rid}", + "" + ), + xmlns = XMLNS, + akid = access_key_id, + secret = secret_access_key, + token = session_token, + expiration = rfc3339(now + duration), + role_id = role_id, + session = xml_escape(session_name), + account = xml_escape(account), + role = xml_escape(role_name), + packed = packed, + rid = request_id(), + ), + } + } + + /// Reports the configured account's root identity. Requests are not + /// mapped back to an assumed role. + fn get_caller_identity(&self) -> Outcome { + let account = xml_escape(self.account_id()); + Outcome { + status: 200, + body: format!( + concat!( + "", + "arn:aws:iam::{account}:root{account}", + "{account}", + "{rid}", + "" + ), + xmlns = XMLNS, + account = account, + rid = request_id(), + ), + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn rfc3339_formats_known_instants() { + assert_eq!(rfc3339(0), "1970-01-01T00:00:00Z"); + assert_eq!(rfc3339(1_793_793_600), "2026-11-04T12:00:00Z"); + } + + #[test] + fn role_arn_parsing() { + assert_eq!( + parse_role_arn("arn:aws:iam::123456789012:role/svc/site-a"), + Some(("123456789012", "site-a")) + ); + assert_eq!(parse_role_arn("arn:aws:iam::123456789012:user/bob"), None); + assert_eq!(parse_role_arn("arn:aws:s3:::bucket"), None); + assert_eq!(parse_role_arn("not-an-arn"), None); + } +} diff --git a/services/sts/src/sigv4.rs b/services/sts/src/sigv4.rs new file mode 100644 index 0000000..0832c71 --- /dev/null +++ b/services/sts/src/sigv4.rs @@ -0,0 +1,372 @@ +//! AWS SigV4 verification (relaxed / signed-relaxed / strict), shared shape +//! with `services/applicationautoscaling/src/sigv4.rs`. + +use hmac::{Hmac, Mac}; +use sha2::{Digest, Sha256}; + +type HmacSha256 = Hmac; + +pub const SIGV4_ALGORITHM: &str = "AWS4-HMAC-SHA256"; +pub const SIGV4_SERVICE: &str = "sts"; + +/// A signature verification failure, carrying the AWS error name and HTTP +/// status the legacy server would return. +#[derive(Debug, Clone)] +pub struct SignatureError { + pub name: &'static str, + pub status: u16, +} + +fn err(name: &'static str, status: u16) -> SignatureError { + SignatureError { name, status } +} + +/// Minimal request view the verifier needs (headers + method + path + query + +/// host + body), so it stays independent of the HTTP framework. +pub struct SignedRequest<'a> { + pub method: &'a str, + pub path: &'a str, + pub query: &'a str, + pub host: &'a str, + pub authorization: &'a str, + pub amz_date: &'a str, + pub content_sha256: &'a str, + /// Resolves a header value by lowercase name (for canonical headers). + pub header: &'a dyn Fn(&str) -> Option, + pub body: &'a [u8], +} + +pub struct Credentials<'a> { + pub auth_mode: &'a str, + pub access_key_id: &'a str, + pub secret_access_key: &'a str, + pub region: &'a str, +} + +fn default_str<'a>(value: &'a str, fallback: &'a str) -> &'a str { + if value.is_empty() { + fallback + } else { + value + } +} + +/// Mirrors `Server.verifySignature`. +pub fn verify_signature(req: &SignedRequest, creds: &Credentials) -> Result<(), SignatureError> { + if creds.auth_mode.is_empty() || creds.auth_mode.eq_ignore_ascii_case("relaxed") { + return Ok(()); + } + let auth = req.authorization.trim(); + if auth.is_empty() { + return Err(err("AccessDeniedException", 403)); + } + if creds.auth_mode.eq_ignore_ascii_case("signed-relaxed") { + return verify_shape(req, auth); + } + verify_full(req, auth, creds) +} + +fn verify_full(req: &SignedRequest, auth: &str, creds: &Credentials) -> Result<(), SignatureError> { + let prefix = format!("{SIGV4_ALGORITHM} "); + if !auth.starts_with(&prefix) { + return Err(err("IncompleteSignatureException", 400)); + } + let values = parse_auth_params(&auth[prefix.len()..]); + let credential = values.get("Credential").cloned().unwrap_or_default(); + let signed_headers = values.get("SignedHeaders").cloned().unwrap_or_default(); + let signature = values.get("Signature").cloned().unwrap_or_default(); + let (access_key, date_stamp, region, service) = match parse_credential_scope(&credential) { + Some(v) => v, + None => return Err(err("IncompleteSignatureException", 400)), + }; + if signed_headers.is_empty() || signature.is_empty() { + return Err(err("IncompleteSignatureException", 400)); + } + if !valid_credential(&access_key, ®ion, &service, creds) { + return Err(err("UnrecognizedClientException", 403)); + } + if req.amz_date.is_empty() { + return Err(err("IncompleteSignatureException", 400)); + } + // SDKs sign the body's SHA-256 for this service without sending the + // header (only S3 uses the header-driven `UNSIGNED-PAYLOAD` convention). + let payload_hash = if req.content_sha256.is_empty() { + sha256_hex(req.body) + } else { + verify_payload_hash(req, req.content_sha256)?; + req.content_sha256.to_string() + }; + let expected = signature_for_request( + req, + &date_stamp, + ®ion, + &signed_headers, + &payload_hash, + creds, + ); + if !constant_time_eq(signature.as_bytes(), expected.as_bytes()) { + return Err(err("InvalidSignatureException", 403)); + } + Ok(()) +} + +fn verify_shape(req: &SignedRequest, auth: &str) -> Result<(), SignatureError> { + let prefix = format!("{SIGV4_ALGORITHM} "); + if !auth.starts_with(&prefix) { + return Err(err("IncompleteSignatureException", 400)); + } + let values = parse_auth_params(&auth[prefix.len()..]); + let credential = values.get("Credential").cloned().unwrap_or_default(); + let signed_headers = values.get("SignedHeaders").cloned().unwrap_or_default(); + let signature = values.get("Signature").cloned().unwrap_or_default(); + let (_, _, _, service) = match parse_credential_scope(&credential) { + Some(v) => v, + None => return Err(err("IncompleteSignatureException", 400)), + }; + if signed_headers.is_empty() || signature.is_empty() { + return Err(err("IncompleteSignatureException", 400)); + } + if service != SIGV4_SERVICE || !is_lower_hex(&signature, 64) { + return Err(err("IncompleteSignatureException", 400)); + } + if req.amz_date.is_empty() { + return Err(err("IncompleteSignatureException", 400)); + } + if !req.content_sha256.is_empty() { + return verify_payload_hash(req, req.content_sha256); + } + Ok(()) +} + +fn is_lower_hex(value: &str, length: usize) -> bool { + value.len() == length + && value + .bytes() + .all(|c| c.is_ascii_digit() || (b'a'..=b'f').contains(&c)) +} + +fn valid_credential(access_key: &str, region: &str, service: &str, creds: &Credentials) -> bool { + let configured_access_key = default_str(creds.access_key_id, "dev"); + let configured_region = default_str(creds.region, "us-east-1"); + access_key == configured_access_key && region == configured_region && service == SIGV4_SERVICE +} + +fn verify_payload_hash(req: &SignedRequest, payload_hash: &str) -> Result<(), SignatureError> { + if payload_hash == "UNSIGNED-PAYLOAD" { + return Ok(()); + } + if payload_hash.starts_with("STREAMING-") { + return Err(err("NotImplemented", 501)); + } + let got = sha256_hex(req.body); + if !constant_time_eq(payload_hash.to_ascii_lowercase().as_bytes(), got.as_bytes()) { + return Err(err("InvalidSignatureException", 403)); + } + Ok(()) +} + +fn signature_for_request( + req: &SignedRequest, + date_stamp: &str, + region: &str, + signed_headers: &str, + payload_hash: &str, + creds: &Credentials, +) -> String { + let canonical_request = [ + req.method, + &canonical_uri(req.path), + &canonical_query_string(req.query), + &canonical_headers(req, signed_headers), + &signed_headers.to_ascii_lowercase(), + payload_hash, + ] + .join("\n"); + let scope = [date_stamp, region, SIGV4_SERVICE, "aws4_request"].join("/"); + let string_to_sign = [ + SIGV4_ALGORITHM, + req.amz_date, + &scope, + &sha256_hex(canonical_request.as_bytes()), + ] + .join("\n"); + let signing_key = derive_signing_key(creds.secret_access_key, date_stamp, region); + hex::encode(hmac_sha256(&signing_key, string_to_sign.as_bytes())) +} + +fn parse_credential_scope(credential: &str) -> Option<(String, String, String, String)> { + let parts: Vec<&str> = credential.split('/').collect(); + if parts.len() != 5 || parts[4] != "aws4_request" { + return None; + } + Some(( + parts[0].to_string(), + parts[1].to_string(), + parts[2].to_string(), + parts[3].to_string(), + )) +} + +fn parse_auth_params(value: &str) -> std::collections::HashMap { + let mut result = std::collections::HashMap::new(); + for part in value.split(',') { + if let Some((k, v)) = part.trim().split_once('=') { + result.insert(k.to_string(), v.to_string()); + } + } + result +} + +fn canonical_uri(path: &str) -> String { + if path.is_empty() { + return "/".to_string(); + } + aws_percent_encode(path, "/~") +} + +fn canonical_query_string(query: &str) -> String { + if query.is_empty() { + return String::new(); + } + let mut pairs: Vec<(String, String)> = Vec::new(); + for item in query.split('&') { + match item.split_once('=') { + Some((k, v)) => pairs.push((url_decode(k), url_decode(v))), + None => pairs.push((url_decode(item), String::new())), + } + } + pairs.sort_by(|a, b| a.0.cmp(&b.0).then(a.1.cmp(&b.1))); + pairs + .iter() + .map(|(k, v)| { + format!( + "{}={}", + aws_percent_encode(k, "~-_"), + aws_percent_encode(v, "~-_") + ) + }) + .collect::>() + .join("&") +} + +fn canonical_headers(req: &SignedRequest, signed_headers: &str) -> String { + let mut out = String::new(); + for name in signed_headers.to_ascii_lowercase().split(';') { + let name = name.trim(); + if name.is_empty() { + continue; + } + let value = if name == "host" { + req.host.to_string() + } else { + (req.header)(name).unwrap_or_default() + }; + out.push_str(name); + out.push(':'); + out.push_str(&normalize_header_value(&value)); + out.push('\n'); + } + out +} + +fn normalize_header_value(value: &str) -> String { + value.split_whitespace().collect::>().join(" ") +} + +fn aws_percent_encode(value: &str, safe: &str) -> String { + let mut out = String::with_capacity(value.len()); + for &c in value.as_bytes() { + let ch = c as char; + if ch.is_ascii_alphanumeric() + || ch == '-' + || ch == '_' + || ch == '.' + || ch == '~' + || safe.contains(ch) + { + out.push(ch); + } else { + out.push_str(&format!("%{:02X}", c)); + } + } + out +} + +fn url_decode(s: &str) -> String { + let bytes = s.as_bytes(); + let mut out = Vec::with_capacity(bytes.len()); + let mut i = 0; + while i < bytes.len() { + match bytes[i] { + b'%' if i + 2 < bytes.len() => { + if let (Some(hi), Some(lo)) = ( + (bytes[i + 1] as char).to_digit(16), + (bytes[i + 2] as char).to_digit(16), + ) { + out.push((hi * 16 + lo) as u8); + i += 3; + continue; + } + out.push(bytes[i]); + i += 1; + } + b'+' => { + out.push(b' '); + i += 1; + } + b => { + out.push(b); + i += 1; + } + } + } + String::from_utf8_lossy(&out).into_owned() +} + +fn derive_signing_key(secret: &str, date_stamp: &str, region: &str) -> Vec { + let secret = default_str(secret, "dev"); + let date_key = hmac_sha256(format!("AWS4{secret}").as_bytes(), date_stamp.as_bytes()); + let region_key = hmac_sha256(&date_key, region.as_bytes()); + let service_key = hmac_sha256(®ion_key, SIGV4_SERVICE.as_bytes()); + hmac_sha256(&service_key, b"aws4_request") +} + +fn hmac_sha256(key: &[u8], value: &[u8]) -> Vec { + let mut mac = HmacSha256::new_from_slice(key).expect("HMAC accepts any key length"); + mac.update(value); + mac.finalize().into_bytes().to_vec() +} + +fn sha256_hex(value: &[u8]) -> String { + let mut hasher = Sha256::new(); + hasher.update(value); + hex::encode(hasher.finalize()) +} + +fn constant_time_eq(a: &[u8], b: &[u8]) -> bool { + if a.len() != b.len() { + return false; + } + let mut diff = 0u8; + for (x, y) in a.iter().zip(b.iter()) { + diff |= x ^ y; + } + diff == 0 +} + +#[cfg(test)] +mod percent_decode_tests { + use super::*; + + #[test] + fn malformed_percent_escapes_next_to_unicode_do_not_panic() { + for value in ["%あ", "%aあ", "猫%猫", "%é", "%💾", "%", "%2", "%zz"] { + assert_eq!(url_decode(value), value); + } + } + + #[test] + fn valid_percent_escapes_and_plus_are_preserved() { + assert_eq!(url_decode("%E7%8C%AB+%2B%2F"), "猫 +/"); + } +} diff --git a/services/sts/tests/sts_test.rs b/services/sts/tests/sts_test.rs new file mode 100644 index 0000000..5d55d7a --- /dev/null +++ b/services/sts/tests/sts_test.rs @@ -0,0 +1,147 @@ +//! STS behavior through the full request pipeline. + +use devcloud_sts::http::serve_for_test; +use devcloud_sts::{Config, Server}; + +const FORM: (&str, &str) = ( + "Content-Type", + "application/x-www-form-urlencoded; charset=utf-8", +); + +fn server() -> Server { + Server::new(Config { + account_id: "123456789012".to_string(), + auth_mode: "relaxed".to_string(), + ..Default::default() + }) +} + +fn post(s: &Server, body: &str) -> (u16, String) { + let r = serve_for_test(s, "POST", "/", &[FORM], body.as_bytes()); + (r.status, r.body) +} + +fn tag<'a>(xml: &'a str, name: &str) -> &'a str { + let open = format!("<{name}>"); + let close = format!(""); + let start = xml + .find(&open) + .unwrap_or_else(|| panic!("no <{name}> in {xml}")) + + open.len(); + let end = start + xml[start..].find(&close).unwrap(); + &xml[start..end] +} + +#[test] +fn assume_role_with_inline_policy_returns_credentials() { + let s = server(); + // What aws-sdk-go-v2 sends: form-encoded, policy JSON percent-encoded. + let body = "Action=AssumeRole&Version=2011-06-15\ + &RoleArn=arn%3Aaws%3Aiam%3A%3A111122223333%3Arole%2Fcsa%2Fsite-runner\ + &RoleSessionName=site-a%40runner\ + &DurationSeconds=900\ + &Policy=%7B%22Version%22%3A%222012-10-17%22%2C%22Statement%22%3A%5B%5D%7D"; + let (status, xml) = post(&s, body); + assert_eq!(status, 200, "{xml}"); + assert!( + xml.starts_with("") + ); + assert!(tag(&xml, "AccessKeyId").starts_with("ASIA")); + assert_eq!(tag(&xml, "AccessKeyId").len(), 20); + assert!(tag(&xml, "SecretAccessKey").len() >= 40); + assert!(!tag(&xml, "SessionToken").is_empty()); + assert_eq!( + tag(&xml, "Arn"), + "arn:aws:sts::111122223333:assumed-role/site-runner/site-a@runner" + ); + assert!(tag(&xml, "AssumedRoleId").ends_with(":site-a@runner")); + assert!(tag(&xml, "PackedPolicySize").parse::().unwrap() >= 1); + let expiration = tag(&xml, "Expiration"); + assert_eq!(expiration.len(), "2026-11-03T10:00:00Z".len()); + assert!(expiration.ends_with('Z')); +} + +#[test] +fn each_call_issues_distinct_credentials() { + let s = server(); + let body = "Action=AssumeRole&RoleArn=arn:aws:iam::1:role/r&RoleSessionName=s1"; + let (_, a) = post(&s, body); + let (_, b) = post(&s, body); + assert_ne!(tag(&a, "AccessKeyId"), tag(&b, "AccessKeyId")); + assert_ne!(tag(&a, "SessionToken"), tag(&b, "SessionToken")); +} + +#[test] +fn assume_role_validation_errors() { + let s = server(); + let cases = [ + ("Action=AssumeRole&RoleSessionName=s1", "ValidationError"), + ( + "Action=AssumeRole&RoleArn=arn:aws:iam::1:role/r", + "ValidationError", + ), + ( + "Action=AssumeRole&RoleArn=arn:aws:iam::1:role/r&RoleSessionName=x", + "ValidationError", + ), + ( + "Action=AssumeRole&RoleArn=arn:aws:iam::1:role/r&RoleSessionName=bad%20name", + "ValidationError", + ), + ( + "Action=AssumeRole&RoleArn=arn:aws:iam::1:user/u&RoleSessionName=s1", + "ValidationError", + ), + ( + "Action=AssumeRole&RoleArn=arn:aws:iam::1:role/r&RoleSessionName=s1&DurationSeconds=60", + "ValidationError", + ), + ( + "Action=AssumeRole&RoleArn=arn:aws:iam::1:role/r&RoleSessionName=s1&Policy=not-json", + "MalformedPolicyDocument", + ), + ]; + for (body, code) in cases { + let (status, xml) = post(&s, body); + assert_eq!(status, 400, "{body}"); + assert_eq!(tag(&xml, "Code"), code, "{body}"); + assert_eq!(tag(&xml, "Type"), "Sender"); + } +} + +#[test] +fn get_caller_identity_via_get_and_post() { + let s = server(); + let (status, xml) = post(&s, "Action=GetCallerIdentity&Version=2011-06-15"); + assert_eq!(status, 200); + assert_eq!(tag(&xml, "Account"), "123456789012"); + assert_eq!(tag(&xml, "Arn"), "arn:aws:iam::123456789012:root"); + + let r = serve_for_test( + &s, + "GET", + "/?Action=GetCallerIdentity&Version=2011-06-15", + &[], + b"", + ); + assert_eq!(r.status, 200); +} + +#[test] +fn unknown_and_missing_action() { + let s = server(); + let (status, xml) = post(&s, "Action=GetSessionToken"); + assert_eq!((status, tag(&xml, "Code")), (400, "InvalidAction")); + let (status, xml) = post(&s, "Version=2011-06-15"); + assert_eq!((status, tag(&xml, "Code")), (400, "MissingAction")); +} + +#[test] +fn strict_mode_requires_signature() { + let s = Server::new(Config { + auth_mode: "strict".to_string(), + ..Default::default() + }); + let (status, _) = post(&s, "Action=GetCallerIdentity"); + assert_eq!(status, 403); +} diff --git a/testdata/golden/dashboard/api-dashboard-services.json b/testdata/golden/dashboard/api-dashboard-services.json index 8063771..5fcc3c7 100644 --- a/testdata/golden/dashboard/api-dashboard-services.json +++ b/testdata/golden/dashboard/api-dashboard-services.json @@ -97,6 +97,23 @@ "status": "running", "storagePath": "\u003cPATH\u003e" }, + { + "description": "Inspect local SSM parameter names, types, and versions (values stay hidden).", + "endpoint": "http://\u003cADDR\u003e", + "id": "\u003cID\u003e", + "name": "SSM Parameter Store", + "path": "\u003cPATH\u003e", + "status": "running", + "storagePath": "\u003cPATH\u003e" + }, + { + "description": "Check the local STS endpoint that issues temporary credentials for AssumeRole.", + "endpoint": "http://\u003cADDR\u003e", + "id": "\u003cID\u003e", + "name": "STS", + "path": "\u003cPATH\u003e", + "status": "running" + }, { "description": "Inspect local Lambda functions, invoke them, and read recent invocation logs.", "endpoint": "http://\u003cADDR\u003e", diff --git a/testdata/golden/dashboard/api-services.json b/testdata/golden/dashboard/api-services.json index d45c78d..10d8a9b 100644 --- a/testdata/golden/dashboard/api-services.json +++ b/testdata/golden/dashboard/api-services.json @@ -97,6 +97,23 @@ "status": "running", "storagePath": "\u003cPATH\u003e" }, + { + "description": "Inspect local SSM parameter names, types, and versions (values stay hidden).", + "endpoint": "http://\u003cADDR\u003e", + "id": "\u003cID\u003e", + "name": "SSM Parameter Store", + "path": "\u003cPATH\u003e", + "status": "running", + "storagePath": "\u003cPATH\u003e" + }, + { + "description": "Check the local STS endpoint that issues temporary credentials for AssumeRole.", + "endpoint": "http://\u003cADDR\u003e", + "id": "\u003cID\u003e", + "name": "STS", + "path": "\u003cPATH\u003e", + "status": "running" + }, { "description": "Inspect local Lambda functions, invoke them, and read recent invocation logs.", "endpoint": "http://\u003cADDR\u003e", diff --git a/web/dashboard/package-lock.json b/web/dashboard/package-lock.json index b93e03f..eb85668 100644 --- a/web/dashboard/package-lock.json +++ b/web/dashboard/package-lock.json @@ -19,7 +19,7 @@ "playwright": "1.64.0", "react-test-renderer": "18.3.1", "typescript": "5.9.3", - "vite": "6.4.4" + "vite": "6.4.5" } }, "node_modules/@babel/code-frame": { @@ -1859,9 +1859,9 @@ } }, "node_modules/vite": { - "version": "6.4.4", - "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.4.tgz", - "integrity": "sha512-xGjU/SyjeNyb0E4KxXBvYz/1C+3JXuujvf2hXNOtnlcE+58o5ajd9N2/n2VhkiJvpCP2qQasnUpx5PXNeI+p1Q==", + "version": "6.4.5", + "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.5.tgz", + "integrity": "sha512-4enwjFfi4IwtEh0w+jPzpLVOFQiPHmmzPxcHXQOyEur47o6u0jE573DmGpjA+7irXoMLYRZD5ZaFUEneS3abPg==", "dev": true, "license": "MIT", "dependencies": { diff --git a/web/dashboard/package.json b/web/dashboard/package.json index b97b740..d5c6307 100644 --- a/web/dashboard/package.json +++ b/web/dashboard/package.json @@ -22,6 +22,6 @@ "playwright": "1.64.0", "react-test-renderer": "18.3.1", "typescript": "5.9.3", - "vite": "6.4.4" + "vite": "6.4.5" } } diff --git a/web/dashboard/src/app/routes.tsx b/web/dashboard/src/app/routes.tsx index 35b0130..f742a7c 100644 --- a/web/dashboard/src/app/routes.tsx +++ b/web/dashboard/src/app/routes.tsx @@ -10,6 +10,8 @@ import { SQSDashboard } from './services/sqs/SQSDashboard' import { PubSubDashboard } from './services/pubsub/PubSubDashboard' import { GCSDashboard } from './services/gcs/GCSDashboard' import { ApplicationAutoScalingDashboard } from './services/applicationautoscaling/ApplicationAutoScalingDashboard' +import { SSMDashboard } from './services/ssm/SSMDashboard' +import { STSDashboard } from './services/sts/STSDashboard' import { LambdaDashboard } from './services/lambda/LambdaDashboard' import { CloudRunDashboard } from './services/cloudrun/CloudRunDashboard' @@ -51,6 +53,12 @@ export function renderRoute({ services, path }: RouteProps): JSX.Element { service.id === 'applicationautoscaling')} /> ) } + if (path === '/ssm') { + return service.id === 'ssm')} /> + } + if (path === '/sts') { + return service.id === 'sts')} /> + } if (path === '/lambda') { return service.id === 'lambda')} /> } diff --git a/web/dashboard/src/app/services/ssm/SSMDashboard.tsx b/web/dashboard/src/app/services/ssm/SSMDashboard.tsx new file mode 100644 index 0000000..d4c177d --- /dev/null +++ b/web/dashboard/src/app/services/ssm/SSMDashboard.tsx @@ -0,0 +1,124 @@ +import { useCallback, useEffect, useState } from 'react' +import { Button } from '../../../ui/Button' +import { EmptyState } from '../../../ui/EmptyState' +import { Panel } from '../../../ui/Panel' +import { useLatestRequest } from '../../api/hooks/useLatestRequest' +import type { DashboardService } from '../dashboard/types' +import { getSSMStatus, listSSMParameters } from './api' +import type { SSMParameter, SSMStatus } from './types' + +type SSMState = + | { status: 'loading' } + | { status: 'success'; statusPayload: SSMStatus; parameters: SSMParameter[] } + | { status: 'error'; message: string } + +type SSMDashboardProps = { + service?: DashboardService +} + +export function SSMDashboard({ service }: SSMDashboardProps): JSX.Element { + const [state, setState] = useState({ status: 'loading' }) + const isDisabled = service?.status === 'disabled' + + const startRefresh = useLatestRequest() + const refresh = useCallback(() => { + const isCurrent = startRefresh() + if (isDisabled) return + setState({ status: 'loading' }) + Promise.all([getSSMStatus(), listSSMParameters()]) + .then(([statusPayload, parametersPayload]) => { + if (!isCurrent()) return + setState({ status: 'success', statusPayload, parameters: parametersPayload.parameters }) + }) + .catch((error: Error) => { + if (!isCurrent()) return + setState({ status: 'error', message: error.message }) + }) + }, [startRefresh, isDisabled]) + + useEffect(() => { + refresh() + }, [refresh]) + + if (isDisabled) { + return ( + + + + ) + } + + return ( +
+ +
+ + {state.status === 'success' ? `${state.statusPayload.status} / ${state.statusPayload.region}` : 'Loading'} + + +
+ {state.status === 'loading' ? ( + + ) : null} + {state.status === 'error' ? ( + + ) : null} + {state.status === 'success' ? ( +
+
+
Endpoint
+
+ {state.statusPayload.endpoint} +
+
+
+
Region
+
{state.statusPayload.region}
+
+
+
Storage path
+
+ {state.statusPayload.storagePath} +
+
+
+
Parameters
+
{state.statusPayload.parameterCount}
+
+
+ ) : null} +
+ + + + +
+ ) +} + +function ParameterList({ parameters }: { parameters: SSMParameter[] }): JSX.Element { + if (parameters.length === 0) { + return + } + return ( +
+ {parameters.map((parameter) => ( +
+ + {parameter.Name} + v{parameter.Version} + + {parameter.Description ?? new Date(parameter.LastModifiedDate * 1000).toISOString()} + + {parameter.Type} + {parameter.Tier} + {parameter.KeyId ? {parameter.KeyId} : null} + +
+ ))} +
+ ) +} diff --git a/web/dashboard/src/app/services/ssm/api.ts b/web/dashboard/src/app/services/ssm/api.ts new file mode 100644 index 0000000..c2ad9db --- /dev/null +++ b/web/dashboard/src/app/services/ssm/api.ts @@ -0,0 +1,10 @@ +import { fetchJSON } from '../../api/client' +import type { SSMParametersResponse, SSMStatus } from './types' + +export async function getSSMStatus(): Promise { + return fetchJSON('/api/ssm/status') +} + +export async function listSSMParameters(): Promise { + return fetchJSON('/api/ssm/parameters') +} diff --git a/web/dashboard/src/app/services/ssm/types.ts b/web/dashboard/src/app/services/ssm/types.ts new file mode 100644 index 0000000..121b2f4 --- /dev/null +++ b/web/dashboard/src/app/services/ssm/types.ts @@ -0,0 +1,26 @@ +export type SSMStatus = { + service: 'ssm' + status: string + running: boolean + endpoint: string + region: string + storagePath: string + parameterCount: number +} + +// Metadata from DescribeParameters. Values are never sent to the dashboard. +export type SSMParameter = { + Name: string + ARN: string + Type: 'String' | 'StringList' | 'SecureString' + Version: number + LastModifiedDate: number + Tier: string + DataType: string + Description?: string + KeyId?: string +} + +export type SSMParametersResponse = { + parameters: SSMParameter[] +} diff --git a/web/dashboard/src/app/services/sts/STSDashboard.tsx b/web/dashboard/src/app/services/sts/STSDashboard.tsx new file mode 100644 index 0000000..85da47e --- /dev/null +++ b/web/dashboard/src/app/services/sts/STSDashboard.tsx @@ -0,0 +1,89 @@ +import { useCallback, useEffect, useState } from 'react' +import { Button } from '../../../ui/Button' +import { EmptyState } from '../../../ui/EmptyState' +import { Panel } from '../../../ui/Panel' +import { useLatestRequest } from '../../api/hooks/useLatestRequest' +import type { DashboardService } from '../dashboard/types' +import { getSTSCallerIdentity, getSTSStatus } from './api' +import type { STSCallerIdentity, STSStatus } from './types' + +type STSState = + | { status: 'loading' } + | { status: 'success'; statusPayload: STSStatus; identity: STSCallerIdentity } + | { status: 'error'; message: string } + +type STSDashboardProps = { + service?: DashboardService +} + +export function STSDashboard({ service }: STSDashboardProps): JSX.Element { + const [state, setState] = useState({ status: 'loading' }) + const isDisabled = service?.status === 'disabled' + + const startRefresh = useLatestRequest() + const refresh = useCallback(() => { + const isCurrent = startRefresh() + if (isDisabled) return + setState({ status: 'loading' }) + Promise.all([getSTSStatus(), getSTSCallerIdentity()]) + .then(([statusPayload, identity]) => { + if (!isCurrent()) return + setState({ status: 'success', statusPayload, identity }) + }) + .catch((error: Error) => { + if (!isCurrent()) return + setState({ status: 'error', message: error.message }) + }) + }, [startRefresh, isDisabled]) + + useEffect(() => { + refresh() + }, [refresh]) + + if (isDisabled) { + return ( + + + + ) + } + + return ( + +
+ + {state.status === 'success' ? `${state.statusPayload.status} / ${state.statusPayload.region}` : 'Loading'} + + +
+ {state.status === 'loading' ? : null} + {state.status === 'error' ? ( + + ) : null} + {state.status === 'success' ? ( +
+
+
Endpoint
+
+ {state.statusPayload.endpoint} +
+
+
+
Account
+
{state.identity.account}
+
+
+
Caller ARN
+
+ {state.identity.arn} +
+
+
+
Operations
+
AssumeRole (inline session policies accepted, not enforced), GetCallerIdentity
+
+
+ ) : null} +
+ ) +} diff --git a/web/dashboard/src/app/services/sts/api.ts b/web/dashboard/src/app/services/sts/api.ts new file mode 100644 index 0000000..444e702 --- /dev/null +++ b/web/dashboard/src/app/services/sts/api.ts @@ -0,0 +1,10 @@ +import { fetchJSON } from '../../api/client' +import type { STSCallerIdentity, STSStatus } from './types' + +export async function getSTSStatus(): Promise { + return fetchJSON('/api/sts/status') +} + +export async function getSTSCallerIdentity(): Promise { + return fetchJSON('/api/sts/caller-identity') +} diff --git a/web/dashboard/src/app/services/sts/types.ts b/web/dashboard/src/app/services/sts/types.ts new file mode 100644 index 0000000..c37104f --- /dev/null +++ b/web/dashboard/src/app/services/sts/types.ts @@ -0,0 +1,13 @@ +export type STSStatus = { + service: 'sts' + status: string + running: boolean + endpoint: string + region: string +} + +export type STSCallerIdentity = { + account: string + arn: string + userId: string +} diff --git a/web/dashboard/tests/browser-smoke.mjs b/web/dashboard/tests/browser-smoke.mjs index b84b3ff..c65d162 100644 --- a/web/dashboard/tests/browser-smoke.mjs +++ b/web/dashboard/tests/browser-smoke.mjs @@ -110,12 +110,12 @@ try { const registryResponse = await fetch(`${dashboard}/api/services`, { signal: AbortSignal.timeout(5000) }) assert.equal(registryResponse.status, 200) const registry = await registryResponse.json() - assert.equal(registry.services.length, 12) + assert.equal(registry.services.length, 14) assert.deepEqual(registry.services.filter((service) => service.status === 'running').map((service) => service.id), ['gcs'], 'devcloud up gcs must report only GCS as running') assert.ok(registry.services.every((service) => service.id === 'gcs' || service.status === 'disabled'), 'unselected services must be disabled') - await page.getByText('1/12 running', { exact: true }).waitFor() + await page.getByText('1/14 running', { exact: true }).waitFor() // A provider mutation must reach the page through the real relay and proxy. await upload('live-event.txt')