From b87e01607660cea132288876f68a882564881bce Mon Sep 17 00:00:00 2001 From: "shingo.imota" Date: Tue, 3 Nov 2026 12:00:26 +0900 Subject: [PATCH 01/15] feat(ssm,sts): add SSM Parameter Store and STS service crates SSM serves PutParameter, GetParameter (SecureString with WithDecryption), DeleteParameter, and DescribeParameters over AWS JSON 1.1, persisted to state.json. STS serves AssumeRole (inline session policy accepted, not evaluated) and GetCallerIdentity over the AWS Query protocol. --- Cargo.lock | 26 ++ Cargo.toml | 2 + services/ssm/Cargo.toml | 17 + services/ssm/src/http.rs | 373 ++++++++++++++++++ services/ssm/src/lib.rs | 10 + services/ssm/src/server.rs | 543 ++++++++++++++++++++++++++ services/ssm/src/sigv4.rs | 370 ++++++++++++++++++ services/ssm/tests/parameters_test.rs | 296 ++++++++++++++ services/sts/Cargo.toml | 17 + services/sts/src/http.rs | 384 ++++++++++++++++++ services/sts/src/lib.rs | 8 + services/sts/src/server.rs | 311 +++++++++++++++ services/sts/src/sigv4.rs | 370 ++++++++++++++++++ services/sts/tests/sts_test.rs | 147 +++++++ 14 files changed, 2874 insertions(+) create mode 100644 services/ssm/Cargo.toml create mode 100644 services/ssm/src/http.rs create mode 100644 services/ssm/src/lib.rs create mode 100644 services/ssm/src/server.rs create mode 100644 services/ssm/src/sigv4.rs create mode 100644 services/ssm/tests/parameters_test.rs create mode 100644 services/sts/Cargo.toml create mode 100644 services/sts/src/http.rs create mode 100644 services/sts/src/lib.rs create mode 100644 services/sts/src/server.rs create mode 100644 services/sts/src/sigv4.rs create mode 100644 services/sts/tests/sts_test.rs diff --git a/Cargo.lock b/Cargo.lock index b5ecf0b..89615bf 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -331,6 +331,8 @@ dependencies = [ "devcloud-redshift", "devcloud-s3", "devcloud-sqs", + "devcloud-ssm", + "devcloud-sts", "serde", "serde_json", "tokio", @@ -409,6 +411,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" 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/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..d817e40 --- /dev/null +++ b/services/ssm/src/sigv4.rs @@ -0,0 +1,370 @@ +//! 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)); + } + let payload_hash = if req.content_sha256.is_empty() { + "UNSIGNED-PAYLOAD".to_string() + } 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..ae8ae53 --- /dev/null +++ b/services/sts/src/http.rs @@ -0,0 +1,384 @@ +//! 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' '), + b'%' if i + 2 < bytes.len() => { + let hex = std::str::from_utf8(&bytes[i + 1..i + 3]).ok(); + match hex.and_then(|h| u8::from_str_radix(h, 16).ok()) { + Some(b) => { + out.push(b); + i += 2; + } + None => out.push(b'%'), + } + } + 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()); + } +} 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..6a64e79 --- /dev/null +++ b/services/sts/src/sigv4.rs @@ -0,0 +1,370 @@ +//! 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)); + } + let payload_hash = if req.content_sha256.is_empty() { + "UNSIGNED-PAYLOAD".to_string() + } 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); +} From 6db17ea1d61231f27187fd10cb177adbaec89b1f Mon Sep 17 00:00:00 2001 From: "shingo.imota" Date: Tue, 3 Nov 2026 12:00:26 +0900 Subject: [PATCH 02/15] feat(orchestrator): run SSM and STS with dashboard pages and e2e smoke Default ports 18031 (ssm) and 18032 (sts). The dashboard lists SSM parameter metadata via DescribeParameters only, so values never reach it. --- orchestrator/Cargo.toml | 2 + orchestrator/src/config.rs | 106 +++++++ orchestrator/src/services/dashboard.rs | 11 + orchestrator/src/services/mod.rs | 2 + orchestrator/src/services/ssm.rs | 40 +++ orchestrator/src/services/sts.rs | 31 ++ orchestrator/src/supervisor.rs | 10 + orchestrator/tests/dashboard_conformance.rs | 6 +- scripts/ssm-e2e.sh | 279 ++++++++++++++++++ scripts/sts-e2e.sh | 269 +++++++++++++++++ .../{index-C7w4pPfk.js => index-BO3HAKPI.js} | 54 ++-- services/dashboard/assets/react/index.html | 2 +- services/dashboard/src/config.rs | 18 ++ services/dashboard/src/http.rs | 16 +- services/dashboard/src/lib.rs | 2 + services/dashboard/src/services.rs | 22 ++ services/dashboard/src/ssm.rs | 153 ++++++++++ services/dashboard/src/sts.rs | 119 ++++++++ .../dashboard/api-dashboard-services.json | 17 ++ testdata/golden/dashboard/api-services.json | 17 ++ web/dashboard/src/app/routes.tsx | 8 + .../src/app/services/ssm/SSMDashboard.tsx | 124 ++++++++ web/dashboard/src/app/services/ssm/api.ts | 10 + web/dashboard/src/app/services/ssm/types.ts | 26 ++ .../src/app/services/sts/STSDashboard.tsx | 89 ++++++ web/dashboard/src/app/services/sts/api.ts | 10 + web/dashboard/src/app/services/sts/types.ts | 13 + 27 files changed, 1425 insertions(+), 31 deletions(-) create mode 100644 orchestrator/src/services/ssm.rs create mode 100644 orchestrator/src/services/sts.rs create mode 100755 scripts/ssm-e2e.sh create mode 100755 scripts/sts-e2e.sh rename services/dashboard/assets/react/assets/{index-C7w4pPfk.js => index-BO3HAKPI.js} (50%) create mode 100644 services/dashboard/src/ssm.rs create mode 100644 services/dashboard/src/sts.rs create mode 100644 web/dashboard/src/app/services/ssm/SSMDashboard.tsx create mode 100644 web/dashboard/src/app/services/ssm/api.ts create mode 100644 web/dashboard/src/app/services/ssm/types.ts create mode 100644 web/dashboard/src/app/services/sts/STSDashboard.tsx create mode 100644 web/dashboard/src/app/services/sts/api.ts create mode 100644 web/dashboard/src/app/services/sts/types.ts 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/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/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/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 +} From 3e7825889ef024f749e0452e9e8625afb778b05a Mon Sep 17 00:00:00 2001 From: "shingo.imota" Date: Tue, 3 Nov 2026 12:00:26 +0900 Subject: [PATCH 03/15] test: keep e2e and verify configs off the SSM/STS default ports --- scripts/applicationautoscaling-autoloop/verify.sh | 4 ++++ scripts/applicationautoscaling-e2e.sh | 4 ++++ scripts/bigquery-autoloop/verify.sh | 4 ++++ scripts/bigquery-e2e.sh | 4 ++++ scripts/cloudrun-autoloop/verify.sh | 4 ++++ scripts/cloudrun-e2e.sh | 4 ++++ scripts/dashboard-design-renewal-autoloop/verify.sh | 4 ++++ scripts/dynamodb-autoloop/verify.sh | 4 ++++ scripts/dynamodb-e2e.sh | 4 ++++ scripts/gcs-autoloop/verify.sh | 4 ++++ scripts/gcs-e2e.sh | 4 ++++ scripts/lambda-autoloop/verify.sh | 4 ++++ scripts/lambda-e2e.sh | 4 ++++ scripts/mail-autoloop/verify.sh | 4 ++++ scripts/mail-e2e.sh | 4 ++++ scripts/pubsub-autoloop/verify.sh | 4 ++++ scripts/pubsub-e2e.sh | 4 ++++ scripts/redis-e2e.sh | 4 ++++ scripts/redshift-autoloop/verify.sh | 4 ++++ scripts/redshift-e2e.sh | 4 ++++ scripts/redshift-translator-e2e.sh | 4 ++++ scripts/s3-autoloop/verify.sh | 4 ++++ scripts/s3-e2e.sh | 4 ++++ scripts/sqs-autoloop/verify.sh | 4 ++++ scripts/sqs-e2e.sh | 4 ++++ 25 files changed, 100 insertions(+) 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..2276cd9 100644 --- a/scripts/cloudrun-autoloop/verify.sh +++ b/scripts/cloudrun-autoloop/verify.sh @@ -174,6 +174,10 @@ services: enabled: false cloudRun: enabled: true + ssm: + enabled: false + sts: + enabled: false project: ${PROJECT} region: ${REGION} EOF diff --git a/scripts/cloudrun-e2e.sh b/scripts/cloudrun-e2e.sh index 4528982..d181178 100755 --- a/scripts/cloudrun-e2e.sh +++ b/scripts/cloudrun-e2e.sh @@ -213,6 +213,10 @@ services: enabled: false cloudRun: enabled: true + ssm: + enabled: false + sts: + enabled: false project: ${PROJECT} region: ${REGION} 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..6b12155 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" 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 } From ce3c4d7bdb2348d454eee7634a8d44320703adf4 Mon Sep 17 00:00:00 2001 From: "shingo.imota" Date: Tue, 3 Nov 2026 12:00:26 +0900 Subject: [PATCH 04/15] docs: document SSM Parameter Store and STS endpoints --- CLAUDE.md | 6 +++--- README.md | 44 +++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 46 insertions(+), 4 deletions(-) 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/README.md b/README.md index c5151f4..501fcf5 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 @@ -472,6 +492,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 +623,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 +649,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. | From 7511f653f9497746391a669f9bee138461eb50e1 Mon Sep 17 00:00:00 2001 From: "shingo.imota" Date: Tue, 3 Nov 2026 12:15:10 +0900 Subject: [PATCH 05/15] feat(lambda): run provided runtimes through a local Runtime API A provided* function's bootstrap runs as a local process against a per-environment Lambda Runtime API (invocation/next, response, error, init/error, chunked responses with error trailers). Environments are reused like interpreter runtimes. A missing or foreign-OS bootstrap fails with Runtime.InvalidEntrypoint. Extensions may register but get no events. --- services/lambda/src/lib.rs | 6 +- services/lambda/src/runtime.rs | 321 ++++++++- services/lambda/src/runtime_api.rs | 609 ++++++++++++++++++ services/lambda/src/server.rs | 3 +- .../lambda/tests/provided_runtime_test.rs | 403 ++++++++++++ 5 files changed, 1313 insertions(+), 29 deletions(-) create mode 100644 services/lambda/src/runtime_api.rs create mode 100644 services/lambda/tests/provided_runtime_test.rs 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..a573dc8 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,226 @@ 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"; + +/// 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. +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); + #[cfg(unix)] + { + 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. +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. +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. +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. +fn go_os() -> &'static str { + match std::env::consts::OS { + "macos" => "darwin", + other => other, + } +} + +/// `std::env::consts::ARCH` in Go's `GOARCH` spelling. +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 +1413,7 @@ async fn start_container( name, removals: Arc::clone(&pool.removals), }), + runtime_api: None, events, stdout, stderr, @@ -1378,7 +1635,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 +1679,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 +2051,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; +} From 39f3c4e4b037d5fa7ba5d11e1aa8aaa5db8161d4 Mon Sep 17 00:00:00 2001 From: "shingo.imota" Date: Tue, 3 Nov 2026 12:15:10 +0900 Subject: [PATCH 06/15] docs(lambda): document custom runtime support --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 501fcf5..fb2ac18 100644 --- a/README.md +++ b/README.md @@ -483,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`. | From a5fc36b94d039bc106b0c0a8186e1fa2b8f770f9 Mon Sep 17 00:00:00 2001 From: "shingo.imota" Date: Tue, 3 Nov 2026 12:28:56 +0900 Subject: [PATCH 07/15] feat(redshift): COPY from local S3 on the PostgreSQL backend with per-client sessions Each pgwire client now gets its own PostgreSQL connection, so temp tables, SETs, and transactions stay with that client and end on disconnect (they used to share one connection across clients). COPY ... FROM 's3://bucket/prefix' is loaded by devcloud on that session: text/DELIMITER, CSV, JSON 'auto', GZIP, IGNOREHEADER, NULL AS, EMPTYASNULL, BLANKSASNULL, with Redshift's empty-field NULL rules. Authorization options are ignored; unsupported options are refused. redshift-e2e covers a staged temp-table load in every format. --- Cargo.lock | 46 +- scripts/redshift-e2e.sh | 57 +- services/redshift/Cargo.toml | 2 + services/redshift/src/backend.rs | 18 + services/redshift/src/backend_memory.rs | 4 + services/redshift/src/backend_postgres.rs | 47 +- services/redshift/src/copy_backend.rs | 714 ++++++++++++++++++++++ services/redshift/src/copy_unload.rs | 4 +- services/redshift/src/lib.rs | 1 + services/redshift/src/pgwire.rs | 30 +- services/redshift/src/pgwire_extended.rs | 17 +- services/redshift/src/server.rs | 21 +- 12 files changed, 944 insertions(+), 17 deletions(-) create mode 100644 services/redshift/src/copy_backend.rs diff --git a/Cargo.lock b/Cargo.lock index 89615bf..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", @@ -376,6 +385,7 @@ name = "devcloud-redshift" version = "0.1.0" dependencies = [ "devcloud-s3", + "flate2", "hex", "hmac", "md-5", @@ -479,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]] @@ -494,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" @@ -747,7 +767,7 @@ dependencies = [ "hyper", "libc", "pin-project-lite", - "socket2 0.5.10", + "socket2 0.6.4", "tokio", "tower-service", "tracing", @@ -876,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" @@ -1111,7 +1141,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -1207,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" @@ -1278,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/scripts/redshift-e2e.sh b/scripts/redshift-e2e.sh index 6b12155..d75f1dc 100755 --- a/scripts/redshift-e2e.sh +++ b/scripts/redshift-e2e.sh @@ -351,10 +351,65 @@ 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" +fi + +log "running Data API smoke" STATEMENT_ID="$(aws redshift-data execute-statement \ --endpoint-url "${API_ENDPOINT}" \ --region "${AWS_DEFAULT_REGION}" \ 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..8ec7ed7 --- /dev/null +++ b/services/redshift/src/copy_backend.rs @@ -0,0 +1,714 @@ +//! 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 { + let trimmed = statement.trim_start(); + trimmed.len() > 5 && trimmed[..5].eq_ignore_ascii_case("copy ") +} + +#[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)?; + let mut count = 0usize; + for data in self.read_copy_objects(©)? { + let text = String::from_utf8(data) + .map_err(|_| SqlError::new("COPY input is not valid UTF-8"))?; + let rows = 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)?; + for batch in rows.chunks(INSERT_BATCH_ROWS) { + exec(&insert_statement(©.table, &columns, batch))?; + } + count += rows.len(); + } + Ok(QueryResult::tag_only(&format!("COPY {count}"))) + } + + /// 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 lower = body.to_ascii_lowercase(); + let from = lower + .find(" from ") + .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("COPYRIGHT")); + assert!(!is_copy("select 'copy '")); + } +} 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)) From 4e7b21a05ad97a229cf4affa32bff5674326a9e2 Mon Sep 17 00:00:00 2001 From: "shingo.imota" Date: Tue, 3 Nov 2026 12:28:56 +0900 Subject: [PATCH 08/15] docs(redshift): describe COPY and session behavior of the PostgreSQL backend --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index fb2ac18..2f7a4dc 100644 --- a/README.md +++ b/README.md @@ -447,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. | From 44c436b1646249acc84f1d4be6bfa4a8bd0d2ef6 Mon Sep 17 00:00:00 2001 From: "shingo.imota" Date: Tue, 3 Nov 2026 12:33:35 +0900 Subject: [PATCH 09/15] test(dashboard): expect SSM and STS in the browser smoke registry --- web/dashboard/tests/browser-smoke.mjs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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') From 8442f7f43ebaa87688417088709cad161a2f3e82 Mon Sep 17 00:00:00 2001 From: "shingo.imota" Date: Tue, 3 Nov 2026 12:33:35 +0900 Subject: [PATCH 10/15] fix(dashboard): update vite to 6.4.5 for GHSA-8g5g-mxqh-grgj --- web/dashboard/package-lock.json | 8 ++++---- web/dashboard/package.json | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) 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" } } From 8ef1f1267957be52b69d1398008434c2f75a0c22 Mon Sep 17 00:00:00 2001 From: "shingo.imota" Date: Tue, 3 Nov 2026 12:52:18 +0900 Subject: [PATCH 11/15] fix(redshift): make backend COPY all-or-nothing and parse it robustly Parse every object under the prefix before writing, then send all INSERTs as one simple Query so PostgreSQL applies them as a single implicit transaction (inside an explicit transaction a failure aborts it, as a failed COPY does on Redshift). Accept FROM after any whitespace, and detect COPY on bytes so a statement starting with multi-byte text cannot panic. --- scripts/redshift-e2e.sh | 38 +++++++++++++ services/redshift/src/copy_backend.rs | 82 +++++++++++++++++++++------ 2 files changed, 103 insertions(+), 17 deletions(-) diff --git a/scripts/redshift-e2e.sh b/scripts/redshift-e2e.sh index d75f1dc..7b594f1 100755 --- a/scripts/redshift-e2e.sh +++ b/scripts/redshift-e2e.sh @@ -407,6 +407,44 @@ SQL 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" diff --git a/services/redshift/src/copy_backend.rs b/services/redshift/src/copy_backend.rs index 8ec7ed7..1b45710 100644 --- a/services/redshift/src/copy_backend.rs +++ b/services/redshift/src/copy_backend.rs @@ -27,8 +27,34 @@ const INSERT_BATCH_ROWS: usize = 500; /// Whether `statement` is a COPY (Redshift has only `COPY ... FROM`). pub(crate) fn is_copy(statement: &str) -> bool { - let trimmed = statement.trim_start(); - trimmed.len() > 5 && trimmed[..5].eq_ignore_ascii_case("copy ") + // 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)] @@ -101,21 +127,31 @@ impl Server { } }; let columns = target_columns(&mut exec, ©.table)?; - let mut count = 0usize; + // 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"))?; - let rows = match copy.options.format { + 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)?; - for batch in rows.chunks(INSERT_BATCH_ROWS) { - exec(&insert_statement(©.table, &columns, batch))?; - } - count += rows.len(); + }); + } + 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 {count}"))) + Ok(QueryResult::tag_only(&format!("COPY {}", rows.len()))) } /// The objects a COPY source names: every key under the prefix, in key @@ -181,11 +217,8 @@ impl Server { fn parse_copy(statement: &str) -> Result { let body = statement.trim().trim_end_matches(';').trim(); - let lower = body.to_ascii_lowercase(); - let from = lower - .find(" from ") - .ok_or_else(|| SqlError::new("COPY requires FROM"))?; - let table = body["copy ".len()..from].trim().to_string(); + 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")); } @@ -194,7 +227,7 @@ fn parse_copy(statement: &str) -> Result { "COPY with a column list is not supported by devcloud", )); } - let after_from = body[from + " from ".len()..].trim_start(); + 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 { @@ -708,7 +741,22 @@ mod tests { #[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"); } } From 2015c83dea0bb5044700ce61f4ab38f0755d6c27 Mon Sep 17 00:00:00 2001 From: "shingo.imota" Date: Tue, 3 Nov 2026 12:52:18 +0900 Subject: [PATCH 12/15] fix(scripts): keep Cloud Run project and region under cloudRun The ssm/sts blocks were inserted between cloudRun's enabled flag and its project/region, which then configured sts instead of Cloud Run. --- scripts/cloudrun-autoloop/verify.sh | 4 ++-- scripts/cloudrun-e2e.sh | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/scripts/cloudrun-autoloop/verify.sh b/scripts/cloudrun-autoloop/verify.sh index 2276cd9..7e0e1eb 100644 --- a/scripts/cloudrun-autoloop/verify.sh +++ b/scripts/cloudrun-autoloop/verify.sh @@ -174,12 +174,12 @@ services: enabled: false cloudRun: enabled: true + project: ${PROJECT} + region: ${REGION} ssm: enabled: false sts: enabled: false - project: ${PROJECT} - region: ${REGION} EOF run_check "devcloud binary builds" devcloud_build "${TMP_DIR}/devcloud" diff --git a/scripts/cloudrun-e2e.sh b/scripts/cloudrun-e2e.sh index d181178..3b838d6 100755 --- a/scripts/cloudrun-e2e.sh +++ b/scripts/cloudrun-e2e.sh @@ -213,12 +213,12 @@ services: enabled: false cloudRun: enabled: true + project: ${PROJECT} + region: ${REGION} ssm: enabled: false sts: enabled: false - project: ${PROJECT} - region: ${REGION} EOF } From 9fe2e1e9bd8e6018ab3b6dc7892637c263914086 Mon Sep 17 00:00:00 2001 From: "shingo.imota" Date: Tue, 3 Nov 2026 12:52:18 +0900 Subject: [PATCH 13/15] fix(ssm,sts): verify strict SigV4 against the body hash SDKs sign the body's SHA-256 for these services without sending x-amz-content-sha256; treating its absence as UNSIGNED-PAYLOAD rejected correctly signed requests in strict mode. --- services/ssm/src/sigv4.rs | 4 +++- services/sts/src/sigv4.rs | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/services/ssm/src/sigv4.rs b/services/ssm/src/sigv4.rs index d817e40..1684c68 100644 --- a/services/ssm/src/sigv4.rs +++ b/services/ssm/src/sigv4.rs @@ -88,8 +88,10 @@ fn verify_full(req: &SignedRequest, auth: &str, creds: &Credentials) -> Result<( 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() { - "UNSIGNED-PAYLOAD".to_string() + sha256_hex(req.body) } else { verify_payload_hash(req, req.content_sha256)?; req.content_sha256.to_string() diff --git a/services/sts/src/sigv4.rs b/services/sts/src/sigv4.rs index 6a64e79..0832c71 100644 --- a/services/sts/src/sigv4.rs +++ b/services/sts/src/sigv4.rs @@ -88,8 +88,10 @@ fn verify_full(req: &SignedRequest, auth: &str, creds: &Credentials) -> Result<( 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() { - "UNSIGNED-PAYLOAD".to_string() + sha256_hex(req.body) } else { verify_payload_hash(req, req.content_sha256)?; req.content_sha256.to_string() From 1ebdd756c63d48a81da08330db3e7fedbe4fd07b Mon Sep 17 00:00:00 2001 From: "shingo.imota" Date: Tue, 3 Nov 2026 12:52:18 +0900 Subject: [PATCH 14/15] fix(sts): keep malformed percent escapes in form decoding --- services/sts/src/http.rs | 37 ++++++++++++++++++++++++++++--------- 1 file changed, 28 insertions(+), 9 deletions(-) diff --git a/services/sts/src/http.rs b/services/sts/src/http.rs index ae8ae53..3e2fe17 100644 --- a/services/sts/src/http.rs +++ b/services/sts/src/http.rs @@ -248,15 +248,15 @@ fn form_decode(value: &str) -> String { while i < bytes.len() { match bytes[i] { b'+' => out.push(b' '), - b'%' if i + 2 < bytes.len() => { - let hex = std::str::from_utf8(&bytes[i + 1..i + 3]).ok(); - match hex.and_then(|h| u8::from_str_radix(h, 16).ok()) { - Some(b) => { - out.push(b); - i += 2; - } - None => 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), } @@ -382,3 +382,22 @@ mod request_framing_tests { 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"); + } +} From a12673f9570f3acd75d9b20c853cdc545cdb681c Mon Sep 17 00:00:00 2001 From: "shingo.imota" Date: Tue, 3 Nov 2026 12:52:18 +0900 Subject: [PATCH 15/15] fix(lambda): gate custom runtime spawning on Unix --- services/lambda/src/runtime.rs | 39 +++++++++++++++++++++++----------- 1 file changed, 27 insertions(+), 12 deletions(-) diff --git a/services/lambda/src/runtime.rs b/services/lambda/src/runtime.rs index a573dc8..1b12704 100644 --- a/services/lambda/src/runtime.rs +++ b/services/lambda/src/runtime.rs @@ -979,9 +979,22 @@ fn spawn( /// 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, @@ -1050,18 +1063,15 @@ async fn spawn_provided( .stdout(Stdio::from(child_out)) .stderr(Stdio::null()) .kill_on_drop(true); - #[cfg(unix)] - { - 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(()) - }); - } + 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. @@ -1121,6 +1131,7 @@ async fn spawn_provided( } /// 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( @@ -1141,6 +1152,7 @@ fn invalid_entrypoint(inv: &Invocation, reason: &str, log: &str, init: Duration) } /// 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(), @@ -1157,6 +1169,7 @@ fn os_error_text(e: &std::io::Error) -> String { /// 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; @@ -1179,6 +1192,7 @@ fn foreign_executable(path: &Path) -> Option<&'static str> { } /// `std::env::consts::OS` in Go's `GOOS` spelling. +#[cfg(unix)] fn go_os() -> &'static str { match std::env::consts::OS { "macos" => "darwin", @@ -1187,6 +1201,7 @@ fn go_os() -> &'static str { } /// `std::env::consts::ARCH` in Go's `GOARCH` spelling. +#[cfg(unix)] fn go_arch() -> &'static str { match std::env::consts::ARCH { "x86_64" => "amd64",