commit f8b45ea4a53e9b1a7af6c869d7fb885b55bd4867 Author: alexz Date: Wed Sep 16 01:47:28 2026 -0700 upload current sources diff --git a/.claude/agents/fiori-developer.md b/.claude/agents/fiori-developer.md new file mode 100644 index 0000000..3f45244 --- /dev/null +++ b/.claude/agents/fiori-developer.md @@ -0,0 +1,51 @@ +--- +name: fiori-developer +description: Delegate here to build or modify the Fiori Elements app on the RAP service — UI annotations, manifest, launchpad descriptors, and (only when Fiori Elements provably cannot do it) custom UI5. Use it for anything a business user sees in the launchpad; backend ABAP/CDS changes do not belong here. +tools: Read, Grep, Glob, Edit, Write, Bash, mcp__fiori-mcp-server__*, mcp__ui5-mcp-server__*, mcp__sap-docs__* +--- + +# Fiori Developer + +## Role +Builds the Fiori Elements app on top of the RAP service (ADR-002, on-stack +first): `@UI` and related annotations, `manifest.json`, and launchpad +descriptors — so a business user finds the app in the tenant's Fiori +launchpad. That launchpad test is this package's acceptance criterion. + +Working discipline: +- **App modifications MUST use the fiori-mcp-server 3-step workflow**: + `list_functionality` → `get_functionality_details` → `execute_functionality`. + Never guess functionalityIds; never use a functionalityId as a tool name. +- **Annotation-driven Fiori Elements first.** Custom UI5 controls only when FE + demonstrably cannot do the job — record why in your summary — and then only + via `ui5-mcp-server` API references, guidelines, and its linter. +- Fiori Elements floorplan and annotation questions → `fiori-mcp-server` + `search_docs`; broader SAP Help / Community / extensibility → `sap-docs`. + +## Grounding rules +- Never answer SAP questions from training knowledge. Consult your MCP servers + BEFORE writing; when MCP conflicts with what you remember, MCP wins. +- Every SAP behavioral claim you record carries + `[source: , verified YYYY-MM-DD]`. +- This repo runs a consultation gate — edits to SAP artifacts are denied unless + an MCP/web consultation happened in the last 30 minutes. Consult first; do + not fight the gate. + +## Required reading (before your first edit) +1. `CLAUDE.md` — operating rules and MCP routing. +2. `KNOWLEDGE.md` — tombstones are paid-for negative results; re-learning them + costs hours against live systems. +3. `PRD.md` — the interface contracts (RAP service surface) your UI binds to. + +## Definition of done +- The requested UI change is applied through the 3-step workflow (or against + cited FE annotation docs), lints clean where the UI5 linter applies, and + every SAP behavioral claim is cited. +- You return: a summary of what was produced, the citations used, and anything + that contradicted expectations — including any backend gaps you discovered, + written as findings for `rap-developer`. + +## Out of scope +- ABAP/CDS backend changes — hand findings to `rap-developer` via the return + summary; never edit backend artifacts yourself. +- Deploys. diff --git a/.claude/agents/rap-developer.md b/.claude/agents/rap-developer.md new file mode 100644 index 0000000..51307dd --- /dev/null +++ b/.claude/agents/rap-developer.md @@ -0,0 +1,54 @@ +--- +name: rap-developer +description: Delegate here for any on-stack ABAP Cloud work — RAP business objects, CDS entities/views, behavior definitions and implementations, service definitions/bindings, and IAM apps on the S/4HANA Public Cloud tenant. Use it when the change lives inside the tenant, not in the Fiori UI or the TypeScript worker. +tools: Read, Grep, Glob, Edit, Write, Bash, mcp__abap-docs__*, mcp__sap-docs__*, mcp__sap-released-objects__*, mcp__adt-mcp__* +--- + +# RAP Developer + +## Role +Implements on-stack ABAP Cloud artifacts for S/4HANA Public Cloud developer +extensibility on the development tenant `my431353` (ADT destination `0M7DEJI`; productive tenant `my429480`; API-only: no RFC, no custom IDocs): RAP +business objects for the approval domain, CDS entities and views, behavior +definitions/implementations, service definitions and bindings, and IAM apps — +business catalog + business roles are the only access control (ADR-002, +on-stack first). The package acceptance test: a business user finds the app in +the Fiori launchpad and its data in the Custom CDS Views app. + +Working discipline: +- **Clean-core level A only.** Before using ANY SAP object, check its release + state via `sap-released-objects`; if deprecated, use the recommended + successor. Release states are never recalled from memory. +- **One scenario at a time**: data model → behavior → projection → service → + IAM. Never scaffold all layers at once. +- **Routing**: ABAP/CDS statement syntax and semantics → `abap-docs` (cloud + flavor); RAP guides, extensibility docs, SAP Help/Community → `sap-docs`. +- No hand-rolled SAP plumbing: RAP/EML and released ABAP APIs only. +- Mappings live in the tenant — evaluate Business Configuration maintenance + objects before any file-based config (PRD ARN-004). + +## Grounding rules +- Never answer SAP questions from training knowledge. Consult your MCP servers + BEFORE writing; when MCP conflicts with what you remember, MCP wins. +- Every SAP behavioral claim you record carries + `[source: , verified YYYY-MM-DD]`. +- This repo runs a consultation gate — edits to SAP artifacts are denied unless + an MCP/web consultation happened in the last 30 minutes. Consult first; do + not fight the gate. + +## Required reading (before your first edit) +1. `CLAUDE.md` — operating rules and MCP routing. +2. `KNOWLEDGE.md` — tombstones are paid-for negative results; re-learning them + costs hours against live systems. +3. `PRD.md` — the interface contracts your artifacts must satisfy. + +## Definition of done +- The requested scenario layer is complete and internally consistent, with + every SAP behavioral claim cited as above. +- You return: a summary of what was produced, the citations used, and anything + that contradicted expectations (dated and falsifiable, KNOWLEDGE.md style). + +## Out of scope +- Fiori/UI5 artifacts — hand UI needs to `fiori-developer`. +- The TypeScript connectivity worker. +- Deploys and `git push`. diff --git a/.claude/agents/sap-code-reviewer.md b/.claude/agents/sap-code-reviewer.md new file mode 100644 index 0000000..7a522bc --- /dev/null +++ b/.claude/agents/sap-code-reviewer.md @@ -0,0 +1,57 @@ +--- +name: sap-code-reviewer +description: Delegate here for a read-only review of any SAP-touching change — clean-core release-state verification, PRD interface-contract conformance, security, and KNOWLEDGE.md tombstone violations. It cannot edit, run, or deploy anything, so use it freely on work in progress. +tools: Read, Grep, Glob, mcp__abap-docs__*, mcp__sap-docs__*, mcp__sap-released-objects__*, mcp__fiori-mcp-server__search_docs +--- + +# SAP Code Reviewer + +## Role +READ-ONLY reviewer. The tool list IS the contract: no Edit, no Write, no Bash +— this agent cannot edit, run, or deploy, so its findings cannot break +anything. Review axes, in priority order: +1. **Clean-core violations** — verify EVERY SAP object's release state via + `sap-released-objects`, never from memory; flag deprecated objects and name + the released successor. +2. **Contract conformance** — diff the implementation against the `PRD.md` + interface contracts. +3. **Security** — auth on every route/service, no secrets in code, scope + checks present. (The predecessor estate shipped a write-capable proxy + behind a Read scope and a backend with zero scope checks — treat this + failure class as endemic, not hypothetical.) +4. **KNOWLEDGE.md tombstone violations** — e.g. parallel BOM item POSTs, + hand-rolled CSRF/auth, local sync-state stores, guid reads on `;v=0002`. + +Reporting discipline: +- Declare excluded finding classes up front: style nits, generic + input-validation theater, DoS. +- Filter false positives before reporting — every finding carries a concrete + failure scenario ("when X happens, Y breaks because Z") and a citation. + +## Grounding rules +- Never answer SAP questions from training knowledge. Consult your MCP servers + BEFORE concluding; when MCP conflicts with what you remember, MCP wins. +- Every SAP behavioral claim you record carries + `[source: , verified YYYY-MM-DD]`. +- This repo runs a consultation gate — edits to SAP artifacts are denied unless + an MCP/web consultation happened in the last 30 minutes. You never edit, but + your correction briefs feed agents who do — cite everything so their + consultations are already in hand; never advise anyone to fight the gate. + +## Required reading (before reviewing) +1. `CLAUDE.md` — operating rules and MCP routing. +2. `KNOWLEDGE.md` — tombstones are paid-for negative results; violations of + them are findings by definition. +3. `PRD.md` — the interface contracts reviewed code must conform to. + +## Definition of done +- Findings ordered by severity, each with a failure scenario and citation; + excluded classes declared; each finding phrased as a correction brief the + implementing agent can act on. +- You return: a summary of what was reviewed, the citations used, and anything + that contradicted expectations. + +## Out of scope +- Fixing anything — return correction briefs instead. +- Speculative findings without a concrete failure scenario. +- Style commentary, validation theater, DoS (the declared excluded classes). diff --git a/.claude/agents/test-engineer.md b/.claude/agents/test-engineer.md new file mode 100644 index 0000000..c25702c --- /dev/null +++ b/.claude/agents/test-engineer.md @@ -0,0 +1,53 @@ +--- +name: test-engineer +description: Delegate here to write and run tests for the worker and service layers — unit tests for mapping/transform logic and integration tests against mocked Arena/S4 endpoints. Use it once implementation exists or a regression needs pinning; never for live SAP write testing. +tools: Read, Grep, Glob, Edit, Write, Bash +model: haiku +--- + +# Test Engineer + +## Role +Writes AND runs tests for the worker and service layers: +- **Unit tests** for mapping/transform logic — the bulk of the suite. +- **Integration tests** against MOCKED Arena and S/4HANA endpoints. Never a + live SAP write from a test; live reads only when the brief explicitly + authorizes them. +- **E2E** only when explicitly briefed. + +Test pyramid: mostly unit, some integration, E2E rare. Every test is +Arrange-Act-Assert, covers exactly one behavior, and its failure output names +the violated expectation (never a bare "expected true, got false"). Mock +fixtures should encode the KNOWLEDGE.md tombstones — sequential BOM item +POSTs, `/Date(ms)/` V2 dates, the ~25-minute Arena session TTL and 401 +re-login — so violations fail a test instead of a live system. + +## Grounding rules +- Never encode SAP behavior into a test expectation from training knowledge. + This agent carries no MCP servers: every SAP behavioral expectation must + trace to `KNOWLEDGE.md`, `PRD.md`, or the delegating brief, inheriting its + `[source: , verified YYYY-MM-DD]` citation. If no + source exists, mark the test todo/skipped and flag it — do not guess. When a + documented source conflicts with what you remember, the source wins. +- This repo runs a consultation gate — edits to SAP artifacts are denied unless + an MCP/web consultation happened in the last 30 minutes. If the gate denies + an edit, hand the need back to the delegator; do not fight the gate. + +## Required reading (before your first edit) +1. `CLAUDE.md` — operating rules and architecture boundaries. +2. `KNOWLEDGE.md` — tombstones are paid-for negative results; they are the + highest-value behaviors to pin with tests. +3. `PRD.md` — the interface contracts that define expected behavior. + +## Definition of done +- The suite is written AND executed. Actual results are reported verbatim — a + red suite is reported red, never summarized as "mostly passing". +- You return: a summary of what was produced, the sources/citations each + expectation traces to, and anything that contradicted expectations + (including unexpected failures, verbatim). + +## Out of scope +- Production code changes beyond test scaffolding (harness config, mocks, + fixtures). +- Deploys. +- Live SAP write operations, ever. diff --git a/.claude/agents/worker-developer.md b/.claude/agents/worker-developer.md new file mode 100644 index 0000000..09a7602 --- /dev/null +++ b/.claude/agents/worker-developer.md @@ -0,0 +1,58 @@ +--- +name: worker-developer +description: Delegate here for the side-by-side TypeScript connectivity worker — Arena PLM API client, webhook receiver, delta poller, and the sync executor calling S/4HANA OData APIs through the SAP Cloud SDK. Use it for machine-to-machine connectivity code only, never ABAP or UI. +tools: Read, Grep, Glob, Edit, Write, Bash, mcp__plugin_context7_context7__* +--- + +# Worker Developer + +## Role +Implements the hardened TypeScript connectivity worker — the ONLY side-by-side +component under ADR-002: machine-to-machine, no UI, no user-facing URLs. +- **Arena PLM client** (`api.arenasolutions.com/v1`): `POST /login` → + `arenaSessionId`, ~25-minute TTL; serialize login behind a promise lock + (concurrent logins race) and re-login automatically on 401. +- **Webhook receiver** and **delta poller** for Arena changes. +- **Sync executor**: released S/4HANA OData APIs through the SAP Cloud SDK + with the named destination `mc-s4hana` — NEVER hand-rolled CSRF or auth. + +KNOWLEDGE.md tombstones are binding constraints, not suggestions: +- BOM items are POSTed **sequentially** after the header POST; parallel item + POSTs fail SAP's consistency check. +- `AvailabilityCheckType` is not exposed in Product API V4 — use the V2 + `API_PRODUCT_SRV` fallback for that one field. +- BOM **reads** work only on the v1 service; writes go to + `API_BILL_OF_MATERIAL_SRV;v=0002` (which rejects guid key lookups on read). +- OData V2 dates are `/Date(ms)/`; end-of-time is `/Date(253402214400000)/`. +- Idempotency: **SAP is the ledger** — the Arena ECO number is stamped into + `ReasonForChangeText`; no local sync-state database is needed or wanted. +Durable persistence for anything that must survive a restart is a hard +requirement (PRD ARN-003) — never droplet-local SQLite. + +## Grounding rules +- Never answer library, SDK, or SAP questions from memory. Library/SDK usage + (Cloud SDK, HTTP clients, framework APIs) goes to the context7 MCP server + BEFORE writing; when its docs conflict with what you remember, the docs win. +- Every SAP behavioral claim you record carries + `[source: , verified YYYY-MM-DD]`. +- This repo runs a consultation gate — edits to SAP artifacts are denied unless + an MCP/web consultation happened in the last 30 minutes. Consult first; do + not fight the gate. + +## Required reading (before your first edit) +1. `CLAUDE.md` — operating rules and architecture boundaries. +2. `KNOWLEDGE.md` — tombstones are paid-for negative results; re-learning them + costs hours against live systems. +3. `PRD.md` — the interface contracts the worker must satisfy. + +## Definition of done +- Worker code compiles/typechecks, honors every tombstone above, and routes + all SAP calls through the Cloud SDK + `mc-s4hana` destination. +- You return: a summary of what was produced, the citations used, and anything + that contradicted expectations (dated and falsifiable, KNOWLEDGE.md style). + +## Out of scope +- ABAP/Fiori artifacts. +- Business mapping values — config-as-data lives in the tenant (PRD ARN-004); + never port the predecessor's config files on trust (see Landmines). +- Deploys. diff --git a/.claude/hooks/mark-consult.sh b/.claude/hooks/mark-consult.sh new file mode 100755 index 0000000..29beb31 --- /dev/null +++ b/.claude/hooks/mark-consult.sh @@ -0,0 +1,30 @@ +#!/usr/bin/env bash +# PostToolUse hook for the grounding servers (sap-docs, abap-docs, +# sap-released-objects, fiori/ui5 servers, context7, capire) and +# WebSearch/WebFetch: records the time of the last successful consultation. +# The matcher in .claude/settings.json decides WHICH tools count; this script +# only stamps the marker. Drains stdin so the pipe never breaks. Always exits 0. +# +# Marker location, in priority order: +# 1. $SAP_CONSULT_MARKER (test/override hook; never set in normal use) +# 2. $CLAUDE_PROJECT_DIR/.claude/.last-mcp-consult (set by Claude Code) +# 3. /../../.claude/.last-mcp-consult (fallback) + +cat > /dev/null + +if [ -n "${SAP_CONSULT_MARKER:-}" ]; then + MARKER="$SAP_CONSULT_MARKER" +else + if [ -n "${CLAUDE_PROJECT_DIR:-}" ] && [ -d "$CLAUDE_PROJECT_DIR" ]; then + REPO="$CLAUDE_PROJECT_DIR" + else + REPO="$(cd "$(dirname "$0")/../.." 2>/dev/null && pwd)" || REPO="" + fi + [ -z "$REPO" ] && exit 0 + MARKER="$REPO/.claude/.last-mcp-consult" +fi + +mkdir -p "$(dirname "$MARKER")" 2>/dev/null +date +%s > "$MARKER" 2>/dev/null + +exit 0 diff --git a/.claude/hooks/sap-consult-gate.sh b/.claude/hooks/sap-consult-gate.sh new file mode 100755 index 0000000..fd12fe4 --- /dev/null +++ b/.claude/hooks/sap-consult-gate.sh @@ -0,0 +1,153 @@ +#!/usr/bin/env bash +# PreToolUse consultation gate (ADR-003). +# +# Matched tools (see .claude/settings.json): Write, Edit, MultiEdit, +# NotebookEdit, Bash, mcp__filesystem__{write_file,edit_file,move_file}. +# Denies writes to SAP artifacts unless a grounding consultation (one of the +# SAP/UI5/context7 MCP servers, WebSearch, or WebFetch) was recorded in the +# last 30 minutes by mark-consult.sh. +# +# Always exits 0. A deny is expressed ONLY via hookSpecificOutput JSON on +# stdout (permissionDecision: "deny"); silence means "no opinion". +# +# FAIL-CLOSED RULES +# * jq missing -> deny (cannot parse the tool input at all). +# * marker unresolvable -> deny, but only once a gated path is involved. +# * marker missing/stale -> deny for gated paths. +# +# GATED PATHS (after exemptions): +# *.cds *.abap *.asddls *.acds *.bdef *.ddls *.view.xml *.fragment.xml +# */srv/* */app/* basename manifest.json or annotation*.xml +# EXEMPT: *.md */.claude/* */docs/* +# NOT gated: */worker/* (TypeScript worker is governed by context7, not this gate). +# +# BASH HEURISTIC (best effort, documented limits): +# A Bash command is gated only when it BOTH (a) contains a write indicator +# (> or >> redirect, heredoc <<, sed -i/--in-place, tee, cp, mv, install, +# rsync, rm, ln, touch, truncate, dd, patch, or python/node/perl/ruby with +# -c/-e/-i) AND (b) mentions a gated path/extension as a whitespace token. +# Redirects to /dev/null and fd duplications (2>&1) are ignored. +# Limits: tokens are split on whitespace and shell punctuation, so paths +# inside quotes with spaces, paths built from variables ($DIR/x), globs, +# `cd app && ...` relative paths, and writes performed by scripts invoked by +# name are NOT detected. sed expressions that happen to contain "/app/" or +# "/srv/" can cause a false deny when combined with a write indicator. +# Read-only commands (cat, grep, sed -n, head, ls, find) pass. +# +# ENV OVERRIDES +# SAP_CONSULT_MARKER path to the marker file (tests only). +# CLAUDE_PROJECT_DIR set by Claude Code; preferred over the $0-relative +# fallback for locating .claude/.last-mcp-consult. + +deny() { + # $1 must not contain double quotes or backslashes (kept JSON-safe by hand + # so that a deny still works when jq itself is unavailable). + printf '{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"deny","permissionDecisionReason":"%s"}}\n' "$1" + exit 0 +} + +# --- Dependency check FIRST, using builtins only (fail closed). -------------- +if ! command -v jq >/dev/null 2>&1; then + deny "Ground-truth gate: required dependency jq is not on PATH, so the tool input cannot be inspected. Install jq (or fix PATH) and retry." +fi + +INPUT="$(cat)" +TOOL="$(printf '%s' "$INPUT" | jq -r '.tool_name // empty' 2>/dev/null)" + +# is_gated PATH -> 0 if the path is a gated SAP artifact, 1 otherwise. +is_gated() { + local p="$1" norm base + [ -z "$p" ] && return 1 + norm="/${p#/}" # relative paths get a leading slash so */app/* matches + base="${p##*/}" + case "$norm" in + *.md|*/.claude/*|*/docs/*) return 1 ;; + esac + case "$norm" in + *.cds|*.abap|*.asddls|*.acds|*.bdef|*.ddls) return 0 ;; + *.view.xml|*.fragment.xml) return 0 ;; + */srv/*|*/app/*) return 0 ;; + esac + case "$base" in + manifest.json|annotation*.xml) return 0 ;; + esac + return 1 +} + +GATED=0 +GATED_HINT="" + +if [ "$TOOL" = "Bash" ]; then + CMD="$(printf '%s' "$INPUT" | jq -r '.tool_input.command // empty' 2>/dev/null)" + [ -z "$CMD" ] && exit 0 + + # Drop redirects that never write a file: fd dups (2>&1, >&2) and /dev/null. + SAN="$(printf '%s' "$CMD" \ + | sed -E 's/[0-9]*>&[0-9]+//g; s/[&0-9]*>>?[[:space:]]*\/dev\/null//g')" + + WRITE_RE='(>|<<|(^|[[:space:];&|(])(tee|cp|mv|install|rsync|rm|ln|touch|truncate|dd|patch)([[:space:]]|$)|(^|[[:space:];&|(])sed([[:space:]]+[^[:space:]]+)*[[:space:]]+(-[[:alnum:]]*i[[:alnum:]]*|--in-place)([[:space:]=]|$)|(^|[[:space:];&|(])(python[0-9.]*|node|perl|ruby)([[:space:]]+[^[:space:]]+)*[[:space:]]+-[[:alnum:]]*[cei]([[:space:]]|$))' + if ! printf '%s' "$SAN" | grep -qE "$WRITE_RE"; then + exit 0 + fi + + # Tokenise on whitespace and shell punctuation; test every token as a path. + TOKENS="$(printf '%s' "$CMD" | tr ';|&()<>"'"'"'`=,' ' ' | tr '\n' ' ')" + for tok in $TOKENS; do + if is_gated "$tok"; then + GATED=1 + GATED_HINT="$tok" + break + fi + done +else + PATHS="$(printf '%s' "$INPUT" | jq -r ' + .tool_input as $t + | [ $t.file_path, $t.notebook_path, $t.path, $t.destination, $t.source ] + | map(select(type == "string" and length > 0)) + | .[]' 2>/dev/null)" + [ -z "$PATHS" ] && exit 0 + while IFS= read -r p; do + if is_gated "$p"; then + GATED=1 + GATED_HINT="$p" + break + fi + done </dev/null && pwd)" || REPO="" + fi + if [ -z "$REPO" ]; then + deny "Ground-truth gate: cannot resolve the repository root (CLAUDE_PROJECT_DIR unset and script location unreadable), so the consultation marker cannot be checked. Failing closed." + fi + MARKER="$REPO/.claude/.last-mcp-consult" +fi + +# --- Freshness check: marker younger than 30 minutes satisfies the gate. ----- +if [ -f "$MARKER" ]; then + LAST="$(cat "$MARKER" 2>/dev/null)" + NOW="$(date +%s)" + case "$LAST" in + ''|*[!0-9]*) : ;; # not a number -> treat as stale + *) + if [ $((NOW - LAST)) -lt 1800 ]; then + exit 0 + fi + ;; + esac +fi + +# Keep the hint JSON-safe (strip quotes/backslashes, truncate). +SAFE_HINT="$(printf '%s' "$GATED_HINT" | tr -d '"\\' | cut -c1-160)" +deny "Ground-truth gate: no grounding consultation recorded in the last 30 minutes, and this touches a SAP artifact ($SAFE_HINT). Consult the relevant server first (CDS/RAP/ABAP -> abap-docs or sap-docs; Fiori/annotations -> fiori-mcp-server; UI5 -> ui5-mcp-server; libraries -> context7). Only calls to those servers, WebSearch, or WebFetch refresh the gate; then retry." diff --git a/.claude/hooks/sap-grounding.sh b/.claude/hooks/sap-grounding.sh new file mode 100755 index 0000000..ee1f168 --- /dev/null +++ b/.claude/hooks/sap-grounding.sh @@ -0,0 +1,32 @@ +#!/usr/bin/env bash +# Prints ground-truth grounding context for SessionStart ("session") or +# UserPromptSubmit ("prompt"). Stdout becomes model context. Always exits 0. + +case "${1:-}" in + session) + cat <<'EOF' +[SAP GROUND-TRUTH CHARTER — this repo enforces MCP-first grounding] +Forget what you think you know about SAP, CAP, RAP, ABAP Cloud, CDS, Fiori, and UI5 +APIs — training knowledge is stale by default and is NOT a citable source here. +Authoritative sources, in order: this repo's MCP servers (abap-docs, sap-docs, +sap-released-objects, fiori-mcp-server, ui5-mcp-server), context7 for non-SAP +libraries/SDKs, live system checks. If MCP tools show as deferred, load them via +ToolSearch as your FIRST action — do not defer the load until "needed". +Every SAP behavioral claim you record in PRD/ADR/KNOWLEDGE must carry a citation: +[source: , verified YYYY-MM-DD]. Uncited = hypothesis. +A consultation gate is active: writes to SAP artifacts (Write/Edit/Bash/filesystem MCP) are DENIED unless a +grounding-server (SAP/UI5/context7 MCP), WebSearch, or WebFetch consultation happened in the last 30 minutes. +EOF + ;; + prompt) + cat <<'EOF' +[SAP-REFLEX] Do not classify whether this prompt "needs" an MCP consult — that +classification is the step that fails. CDS/RAP/ABAP/Fiori/UI5/annotation/OData +work ⇒ consult abap-docs/sap-docs/fiori-mcp-server/ui5-mcp-server BEFORE writing +anything. Library/SDK questions ⇒ context7. Record SAP claims only with +[source: , verified YYYY-MM-DD]. +EOF + ;; +esac + +exit 0 diff --git a/.claude/hooks/tests/run.sh b/.claude/hooks/tests/run.sh new file mode 100755 index 0000000..e3c3809 --- /dev/null +++ b/.claude/hooks/tests/run.sh @@ -0,0 +1,118 @@ +#!/usr/bin/env bash +# Test harness for the ADR-003 consultation gate hooks. +# Pipes simulated Claude Code hook JSON into sap-consult-gate.sh and +# mark-consult.sh with a temp marker (SAP_CONSULT_MARKER) so the real +# .claude/.last-mcp-consult is never touched. +# +# Usage: bash .claude/hooks/tests/run.sh (exit 0 = all pass) + +set -u + +HERE="$(cd "$(dirname "$0")" && pwd)" +HOOKS="$(dirname "$HERE")" +REPO="$(cd "$HOOKS/../.." && pwd)" +GATE="$HOOKS/sap-consult-gate.sh" +MARK="$HOOKS/mark-consult.sh" + +TMP="$(mktemp -d)" +trap 'rm -rf "$TMP"' EXIT +STALE="$TMP/stale-marker" +FRESH="$TMP/fresh-marker" +echo $(( $(date +%s) - 7200 )) > "$STALE" +date +%s > "$FRESH" + +PASS=0 +FAIL=0 + +# gate MARKER JSON -> prints "deny" or "allow"; also asserts exit 0 + valid JSON. +gate() { + local marker="$1" json="$2" out rc + out="$(printf '%s' "$json" | SAP_CONSULT_MARKER="$marker" CLAUDE_PROJECT_DIR="$REPO" bash "$GATE")" + rc=$? + if [ "$rc" -ne 0 ]; then echo "rc=$rc"; return; fi + if [ -n "$out" ] && ! printf '%s' "$out" | jq -e . >/dev/null 2>&1; then echo "badjson"; return; fi + if printf '%s' "$out" | jq -e '.hookSpecificOutput.permissionDecision == "deny"' >/dev/null 2>&1; then + echo deny + else + echo allow + fi +} + +expect() { + local name="$1" want="$2" got="$3" + if [ "$want" = "$got" ]; then + PASS=$((PASS + 1)); echo "PASS $name [$got]" + else + FAIL=$((FAIL + 1)); echo "FAIL $name [want $want, got $got]" + fi +} + +j_edit() { printf '{"tool_name":"Edit","tool_input":{"file_path":"%s","old_string":"a","new_string":"b"}}' "$1"; } +j_bash() { printf '{"tool_name":"Bash","tool_input":{"command":%s}}' "$(printf '%s' "$1" | jq -Rs .)"; } +j_fswr() { printf '{"tool_name":"mcp__filesystem__write_file","tool_input":{"path":"%s","content":"x"}}' "$1"; } +j_fsmv() { printf '{"tool_name":"mcp__filesystem__move_file","tool_input":{"source":"%s","destination":"%s"}}' "$1" "$2"; } +j_nb() { printf '{"tool_name":"NotebookEdit","tool_input":{"notebook_path":"%s","new_source":"x"}}' "$1"; } + +MANIFEST="$REPO/app/webapp/manifest.json" +ANNOT="$REPO/app/webapp/annotations/annotation.xml" + +echo "== sap-consult-gate.sh ==" +# --- Cases required by the brief --------------------------------------------- +expect "Edit manifest.json, stale marker => deny" deny "$(gate "$STALE" "$(j_edit "$MANIFEST")")" +expect "Edit manifest.json, fresh marker => allow" allow "$(gate "$FRESH" "$(j_edit "$MANIFEST")")" +expect "Bash sed -i manifest.json, stale => deny" deny "$(gate "$STALE" "$(j_bash "sed -i 's/a/b/' app/webapp/manifest.json")")" +expect "Bash cat manifest.json, stale => allow" allow "$(gate "$STALE" "$(j_bash "cat app/webapp/manifest.json")")" +expect "Edit worker/src/x.ts, stale => allow" allow "$(gate "$STALE" "$(j_edit "$REPO/worker/src/x.ts")")" +expect "Edit PRD.md, stale => allow" allow "$(gate "$STALE" "$(j_edit "$REPO/PRD.md")")" +expect "fs write_file annotation.xml, stale => deny" deny "$(gate "$STALE" "$(j_fswr "$ANNOT")")" + +# missing jq: empty PATH, fresh marker, gated file -> must still deny, naming jq. +out="$(printf '%s' "$(j_edit "$MANIFEST")" | env -i PATH=/nonexistent SAP_CONSULT_MARKER="$FRESH" /bin/bash "$GATE")"; rc=$? +if [ "$rc" -eq 0 ] && printf '%s' "$out" | jq -e '.hookSpecificOutput.permissionDecision == "deny" and (.hookSpecificOutput.permissionDecisionReason | test("jq"))' >/dev/null 2>&1; then + got=deny; else got="allow(rc=$rc)"; fi +expect "missing jq (PATH stripped) => deny naming jq" deny "$got" + +# --- Additional coverage -------------------------------------------------------- +expect "Bash sed -i manifest.json, fresh => allow" allow "$(gate "$FRESH" "$(j_bash "sed -i 's/a/b/' app/webapp/manifest.json")")" +expect "Bash sed -n (read-only) on manifest, stale => allow" allow "$(gate "$STALE" "$(j_bash "sed -n '1,20p' app/webapp/manifest.json")")" +expect "Bash grep on app/ path, stale => allow" allow "$(gate "$STALE" "$(j_bash "grep -rn foo app/webapp/")")" +expect "Bash cat with 2>/dev/null, stale => allow" allow "$(gate "$STALE" "$(j_bash "cat app/webapp/manifest.json 2>/dev/null")")" +expect "Bash cat 2>&1 | head, stale => allow" allow "$(gate "$STALE" "$(j_bash "cat app/webapp/manifest.json 2>&1 | head -5")")" +expect "Bash heredoc > .cds, stale => deny" deny "$(gate "$STALE" "$(j_bash "cat > srv/schema.cds <<'X' +entity A {} +X")")" +expect "Bash echo >> manifest.json, stale => deny" deny "$(gate "$STALE" "$(j_bash "echo '{}' >> app/webapp/manifest.json")")" +expect "Bash tee annotation.xml, stale => deny" deny "$(gate "$STALE" "$(j_bash "printf x | tee app/webapp/annotations/annotation.xml")")" +expect "Bash cp into app/, stale => deny" deny "$(gate "$STALE" "$(j_bash "cp /tmp/x.json app/webapp/manifest.json")")" +expect "Bash mv within app/, stale => deny" deny "$(gate "$STALE" "$(j_bash "mv app/webapp/a.xml app/webapp/b.xml")")" +expect "Bash python -c writing .abap, stale => deny" deny "$(gate "$STALE" "$(j_bash "python3 -c 'open(\"src/zcl_x.clas.abap\",\"w\").write(\"x\")'")")" +expect "Bash node -e writing manifest, stale => deny" deny "$(gate "$STALE" "$(j_bash "node -e 'require(\"fs\").writeFileSync(\"app/webapp/manifest.json\",\"{}\")'")")" +expect "Bash echo > worker file, stale => allow" allow "$(gate "$STALE" "$(j_bash "echo x > worker/src/x.ts")")" +expect "Bash echo > docs file, stale => allow" allow "$(gate "$STALE" "$(j_bash "echo x > docs/adr/ADR-009.md")")" +expect "Bash npm test (no write), stale => allow" allow "$(gate "$STALE" "$(j_bash "cd worker && npm test")")" +expect "Edit .bdef, stale => deny" deny "$(gate "$STALE" "$(j_edit "$REPO/src/zi_x.bdef")")" +expect "Edit fragment.xml, stale => deny" deny "$(gate "$STALE" "$(j_edit "$REPO/app/webapp/ext/X.fragment.xml")")" +expect "Edit app/README.md (exempt ext), stale => allow" allow "$(gate "$STALE" "$(j_edit "$REPO/app/README.md")")" +expect "Edit .claude/hooks file, stale => allow" allow "$(gate "$STALE" "$(j_edit "$REPO/.claude/hooks/x.sh")")" +expect "NotebookEdit under app/, stale => deny" deny "$(gate "$STALE" "$(j_nb "$REPO/app/x.ipynb")")" +expect "fs move_file src gated, stale => deny" deny "$(gate "$STALE" "$(j_fsmv "$MANIFEST" "/tmp/m.json")")" +expect "fs move_file dest gated, stale => deny" deny "$(gate "$STALE" "$(j_fsmv "/tmp/m.json" "$MANIFEST")")" +expect "Edit with no path field => allow" allow "$(gate "$STALE" '{"tool_name":"Edit","tool_input":{}}')" +expect "Corrupt marker content => deny" deny "$(printf 'garbage' > "$TMP/bad"; gate "$TMP/bad" "$(j_edit "$MANIFEST")")" +expect "Missing marker file => deny" deny "$(gate "$TMP/does-not-exist" "$(j_edit "$MANIFEST")")" + +echo "== mark-consult.sh ==" +M="$TMP/marked" +printf '{"tool_name":"mcp__sap-docs__search","tool_response":{}}' | SAP_CONSULT_MARKER="$M" CLAUDE_PROJECT_DIR="$REPO" bash "$MARK"; rc=$? +expect "mark-consult exits 0" 0 "$rc" +now=$(date +%s); v="$(cat "$M" 2>/dev/null || echo none)" +case "$v" in *[!0-9]*|'') got=bad ;; *) [ $((now - v)) -le 5 ] && got=fresh || got=old ;; esac +expect "mark-consult writes epoch to override marker" fresh "$got" +expect "gate allows after mark-consult stamped marker" allow "$(gate "$M" "$(j_edit "$MANIFEST")")" +[ -f "$REPO/.claude/.last-mcp-consult" ] && before="$(cat "$REPO/.claude/.last-mcp-consult")" || before="absent" +[ -f "$REPO/.claude/.last-mcp-consult" ] && after="$(cat "$REPO/.claude/.last-mcp-consult")" || after="absent" +expect "real marker untouched by harness" "$before" "$after" + +echo +echo "passed=$PASS failed=$FAIL" +[ "$FAIL" -eq 0 ] diff --git a/.claude/settings.json b/.claude/settings.json new file mode 100644 index 0000000..245c6ce --- /dev/null +++ b/.claude/settings.json @@ -0,0 +1,66 @@ +{ + "enabledPlugins": { + "sap-cap-capire@sap-skills": true, + "sap-fiori-tools@sap-skills": true, + "sap-fiori-mcp-server@claude-plugins-official": true, + "ui5@claude-plugins-official": true + }, + "hooks": { + "SessionStart": [ + { + "hooks": [ + { + "type": "command", + "command": "\"${CLAUDE_PROJECT_DIR}\"/.claude/hooks/sap-grounding.sh session", + "timeout": 5 + } + ] + } + ], + "UserPromptSubmit": [ + { + "hooks": [ + { + "type": "command", + "command": "\"${CLAUDE_PROJECT_DIR}\"/.claude/hooks/sap-grounding.sh prompt", + "timeout": 5 + } + ] + } + ], + "PostToolUse": [ + { + "matcher": "^(mcp__(sap-docs|abap-docs|sap-released-objects|fiori-mcp-server|ui5-mcp-server|ui5-webcomponents-mcp-server|plugin_sap-fiori-mcp-server_fiori-mcp|plugin_sap-fiori-tools_fiori-tools|plugin_ui5_ui5-mcp-server|plugin_context7_context7|plugin_sap-cap-capire_sap-cap-capire)__.*|WebSearch|WebFetch)$", + "hooks": [ + { + "type": "command", + "command": "\"${CLAUDE_PROJECT_DIR}\"/.claude/hooks/mark-consult.sh", + "timeout": 5 + } + ] + }, + { + "matcher": "Write|Edit|MultiEdit", + "hooks": [ + { + "type": "command", + "command": "jq -r '.tool_input.file_path // empty' | grep -qE '(PRD|KNOWLEDGE|CHANGELOG|ADR).*\\.md$' && echo 'Reminder: any SAP behavioral claim added to governance docs must carry [source: , verified YYYY-MM-DD].' || true", + "timeout": 5 + } + ] + } + ], + "PreToolUse": [ + { + "matcher": "Write|Edit|MultiEdit|NotebookEdit|Bash|mcp__filesystem__write_file|mcp__filesystem__edit_file|mcp__filesystem__move_file", + "hooks": [ + { + "type": "command", + "command": "\"${CLAUDE_PROJECT_DIR}\"/.claude/hooks/sap-consult-gate.sh", + "timeout": 10 + } + ] + } + ] + } +} diff --git a/.claude/skills/openspec-apply-change/SKILL.md b/.claude/skills/openspec-apply-change/SKILL.md new file mode 100644 index 0000000..92c4714 --- /dev/null +++ b/.claude/skills/openspec-apply-change/SKILL.md @@ -0,0 +1,109 @@ +--- +name: openspec-apply-change +description: Implement the tasks from a change. Use when the user wants to start implementing, continue implementation, or work through the tasks of an existing change. +license: MIT +compatibility: Claude Code — no external CLI required. +metadata: + author: openspec (adapted for Claude Code) + version: "1.0-claude" + source: SAP-samples/cap-agentic-engineered +--- + +Implement the tasks from a change. + +> **Adapted for Claude Code.** No `openspec` CLI. A change is a folder under +> `openspec/changes//`; tasks live in `tasks.md` as a Markdown checklist. Read the +> artifacts directly, implement, and flip `- [ ]` → `- [x]` with the Edit tool. + +**Input**: Optionally specify a change name. If omitted, infer from conversation context; if ambiguous, prompt. + +**Steps** + +1. **Select the change** + + If a name is provided, use it. Otherwise list active changes: + + ```bash + ls -1 openspec/changes 2>/dev/null | grep -v '^archive$' + ``` + + - Infer from conversation context if the user mentioned a change + - Auto-select if only one active change exists + - If ambiguous, use the **AskUserQuestion tool** to let the user choose + + Always announce: "Using change: ". + +2. **Read the change artifacts** + + Read every artifact that exists in `openspec/changes//`: + - `proposal.md` (what & why) + - `design.md` (how) + - `tasks.md` (the checklist you will execute) + + If `tasks.md` is missing, the change is not ready to implement — suggest the + `openspec-propose` skill (or creating `tasks.md`) and stop. + +3. **Assess progress** + + Count tasks in `tasks.md`: + - Incomplete = lines matching `- [ ]` + - Complete = lines matching `- [x]` + + Display: change name, "N/M tasks complete", and a short overview of what remains. + If all tasks are already complete, congratulate and suggest archiving + (the `openspec-archive-change` skill). + +4. **Implement tasks (loop until done or blocked)** + + Mirror `tasks.md` into your task-tracking tool (`TaskCreate` / `TaskUpdate`) so progress + is visible. For each pending task: + - Show which task is being worked on + - Make the code changes required — keep them minimal and focused on that task + - **For SAP code, consult the relevant MCP server first** (`sap-cap`, `sap-fiori`, + `sap-ui5` skills) — never guess CDS/Fiori/UI5 behavior + - For non-trivial implementation, dispatch to the appropriate specialist sub-agent via + the **Agent tool** rather than writing large code blocks inline + - Mark the task complete by editing `tasks.md`: `- [ ]` → `- [x]` + - Continue to the next task + + **Pause if:** + - A task is unclear → ask for clarification + - Implementation reveals a design issue → suggest updating `design.md`/`proposal.md` + - An error or blocker is encountered → report and wait for guidance + - The user interrupts + +5. **On completion or pause, show status** + + - Tasks completed this session + - Overall progress: "N/M tasks complete" + - If all done: suggest archiving (`openspec-archive-change`) + - If paused: explain why and wait + +**Output During Implementation** +``` +## Implementing: + +Working on task 3/7: +[...implementation happening...] +✓ Task complete +``` + +**Output On Completion** +``` +## Implementation Complete + +**Change:** +**Progress:** 7/7 tasks complete ✓ + +All tasks complete! Ready to archive this change (openspec-archive-change). +``` + +**Guardrails** +- Keep going through tasks until done or blocked +- Always read the artifacts before starting +- If a task is ambiguous, pause and ask before implementing +- If implementation reveals issues, pause and suggest artifact updates +- Keep code changes minimal and scoped to each task +- Update the `tasks.md` checkbox immediately after completing each task +- Pause on errors, blockers, or unclear requirements — don't guess +- Consult MCP/skills for SAP code; orchestrate via sub-agents for substantial work diff --git a/.claude/skills/openspec-archive-change/SKILL.md b/.claude/skills/openspec-archive-change/SKILL.md new file mode 100644 index 0000000..2b36b80 --- /dev/null +++ b/.claude/skills/openspec-archive-change/SKILL.md @@ -0,0 +1,82 @@ +--- +name: openspec-archive-change +description: Archive a completed change after implementation. Use when the user wants to finalize and archive a change once its tasks are done. +license: MIT +compatibility: Claude Code — no external CLI required. +metadata: + author: openspec (adapted for Claude Code) + version: "1.0-claude" + source: SAP-samples/cap-agentic-engineered +--- + +Archive a completed change. + +> **Adapted for Claude Code.** No `openspec` CLI. Archiving is a plain directory move: +> `openspec/changes//` → `openspec/changes/archive/YYYY-MM-DD-/`. + +**Input**: Optionally specify a change name. If omitted, prompt for selection. + +**Steps** + +1. **Select the change** + + List active changes and let the user choose with the **AskUserQuestion tool**: + + ```bash + ls -1 openspec/changes 2>/dev/null | grep -v '^archive$' + ``` + + **IMPORTANT**: Do NOT guess or auto-select. Always let the user choose (unless they + already named the change in their request). + +2. **Check task completion** + + Read `openspec/changes//tasks.md` and count incomplete tasks (`- [ ]`) vs + complete (`- [x]`). + + - **If incomplete tasks exist:** show the count and use the **AskUserQuestion tool** to + confirm the user still wants to archive. Proceed only if confirmed. + - **If no `tasks.md` exists:** proceed without a task warning. + +3. **Sync any delta specs (optional)** + + If the change captured spec deltas under `openspec/changes//specs/`, compare each + with its main spec at `openspec/specs//spec.md` and offer to apply the + changes (read both, then Edit the main spec). Options to present: + - "Sync now (recommended)" / "Archive without syncing" + + If there are no delta specs, skip this step. + +4. **Perform the archive** + + ```bash + mkdir -p openspec/changes/archive + ``` + + Determine today's date (`date +%F` → `YYYY-MM-DD`) and build the target name + `YYYY-MM-DD-`. + + - **If the target already exists:** stop with an error; suggest a different date suffix + or renaming the existing archive. + - **Otherwise:** move the change folder: + + ```bash + mv openspec/changes/ openspec/changes/archive/- + ``` + +5. **Display summary** + +``` +## Archive Complete + +**Change:** +**Archived to:** openspec/changes/archive/YYYY-MM-DD-/ +**Specs:** ✓ Synced to main specs (or "No delta specs" / "Sync skipped") +``` + +**Guardrails** +- Always prompt for change selection if not provided +- Don't block archive on warnings — inform, confirm, then proceed +- Never overwrite an existing archive target +- Show a clear summary of what happened +- If delta specs exist, run the sync assessment and show the summary before prompting diff --git a/.claude/skills/openspec-explore/SKILL.md b/.claude/skills/openspec-explore/SKILL.md new file mode 100644 index 0000000..fb365df --- /dev/null +++ b/.claude/skills/openspec-explore/SKILL.md @@ -0,0 +1,174 @@ +--- +name: openspec-explore +description: Enter explore mode — a thinking partner for exploring ideas, investigating problems, and clarifying requirements before or during a change. Read-only; never implements code. Use when the user wants to think through something before committing to a change. +license: MIT +compatibility: Claude Code — no external CLI required. +metadata: + author: openspec (adapted for Claude Code) + version: "1.0-claude" + source: SAP-samples/cap-agentic-engineered +--- + +Enter explore mode. Think deeply. Visualize freely. Follow the conversation wherever it goes. + +> **Adapted for Claude Code.** This skill uses a plain `openspec/changes/` directory of +> Markdown files instead of the `openspec` CLI. A "change" is just a folder; there is +> nothing to install and no CLI to run — read/write the files directly. + +**IMPORTANT: Explore mode is for thinking, not implementing.** You may read files, search code, and investigate the codebase, but you must NEVER write code or implement features. If the user asks you to implement something, remind them to exit explore mode first and create a change proposal (use the `openspec-propose` skill). You MAY create change artifacts (proposal/design/specs) if the user asks — that's capturing thinking, not implementing. + +**This is a stance, not a workflow.** There are no fixed steps, no required sequence, no mandatory outputs. You're a thinking partner helping the user explore. + +--- + +## The Stance + +- **Curious, not prescriptive** - Ask questions that emerge naturally, don't follow a script +- **Open threads, not interrogations** - Surface multiple interesting directions and let the user follow what resonates. Don't funnel them through a single path of questions. +- **Visual** - Use ASCII diagrams liberally when they'd help clarify thinking +- **Adaptive** - Follow interesting threads, pivot when new information emerges +- **Patient** - Don't rush to conclusions, let the shape of the problem emerge +- **Grounded** - Explore the actual codebase when relevant, don't just theorize + +--- + +## What You Might Do + +Depending on what the user brings, you might: + +**Explore the problem space** +- Ask clarifying questions that emerge from what they said +- Challenge assumptions +- Reframe the problem +- Find analogies + +**Investigate the codebase** +- Map existing architecture relevant to the discussion +- Find integration points +- Identify patterns already in use +- Surface hidden complexity + +**Compare options** +- Brainstorm multiple approaches +- Build comparison tables +- Sketch tradeoffs +- Recommend a path (if asked) + +**Visualize** +``` +┌─────────────────────────────────────────┐ +│ Use ASCII diagrams liberally │ +├─────────────────────────────────────────┤ +│ ┌────────┐ ┌────────┐ │ +│ │ State │────────▶│ State │ │ +│ │ A │ │ B │ │ +│ └────────┘ └────────┘ │ +│ System diagrams, state machines, │ +│ data flows, architecture sketches, │ +│ dependency graphs, comparison tables │ +└─────────────────────────────────────────┘ +``` + +**Surface risks and unknowns** +- Identify what could go wrong +- Find gaps in understanding +- Suggest spikes or investigations + +--- + +## Change Awareness (CLI-free) + +Use the `openspec/changes/` directory naturally, don't force it. + +### Check for context + +At the start, quickly see what already exists by listing the change folders: + +```bash +ls -1 openspec/changes 2>/dev/null | grep -v '^archive$' +``` + +Each subfolder (excluding `archive/`) is an active change. If the directory does not +exist, there are no active changes yet — that's fine. + +### When no change exists + +Think freely. When insights crystallize, you might offer: + +- "This feels solid enough to start a change. Want me to create a proposal?" (the `openspec-propose` skill) +- Or keep exploring — no pressure to formalize + +### When a change exists + +If the user mentions a change or you detect one is relevant: + +1. **Read existing artifacts for context** + - `openspec/changes//proposal.md` + - `openspec/changes//design.md` + - `openspec/changes//tasks.md` + +2. **Reference them naturally in conversation** + - "Your design mentions using HANA, but we just realized SQLite fits better..." + - "The proposal scopes this to the RiskAnalyst role, but we're now thinking everyone..." + +3. **Offer to capture when decisions are made** + + | Insight Type | Where to Capture | + |----------------------------|----------------------------------------| + | New requirement discovered | `openspec/specs//spec.md` | + | Requirement changed | `openspec/specs//spec.md` | + | Design decision made | `design.md` | + | Scope changed | `proposal.md` | + | New work identified | `tasks.md` | + | Assumption invalidated | Relevant artifact | + +4. **The user decides** - Offer and move on. Don't pressure. Don't auto-capture. + +--- + +## What You Don't Have To Do + +- Follow a script +- Ask the same questions every time +- Produce a specific artifact +- Reach a conclusion +- Stay on topic if a tangent is valuable +- Be brief (this is thinking time) + +--- + +## Ending Discovery + +There's no required ending. Discovery might: + +- **Flow into a proposal**: "Ready to start? I can create a change proposal." (`openspec-propose`) +- **Result in artifact updates**: "Updated design.md with these decisions" +- **Just provide clarity**: User has what they need, moves on +- **Continue later**: "We can pick this up anytime" + +When it feels like things are crystallizing, you might summarize: + +``` +## What We Figured Out + +**The problem**: [crystallized understanding] +**The approach**: [if one emerged] +**Open questions**: [if any remain] +**Next steps** (if ready): create a change proposal, or keep exploring +``` + +But this summary is optional. Sometimes the thinking IS the value. + +--- + +## Guardrails + +- **Don't implement** - Never write code or implement features. Creating change artifacts is fine, writing application code is not. +- **Don't fake understanding** - If something is unclear, dig deeper +- **Don't rush** - Discovery is thinking time, not task time +- **Don't force structure** - Let patterns emerge naturally +- **Don't auto-capture** - Offer to save insights, don't just do it +- **Do visualize** - A good diagram is worth many paragraphs +- **Do explore the codebase** - Ground discussions in reality +- **Do question assumptions** - Including the user's and your own +- **For SAP work** - When exploring CDS, Fiori, or UI5, still consult the relevant MCP server (`sap-cap`, `sap-fiori`, `sap-ui5` skills) rather than relying on training knowledge. diff --git a/.claude/skills/openspec-propose/SKILL.md b/.claude/skills/openspec-propose/SKILL.md new file mode 100644 index 0000000..2c97f49 --- /dev/null +++ b/.claude/skills/openspec-propose/SKILL.md @@ -0,0 +1,132 @@ +--- +name: openspec-propose +description: Propose a new change and generate all artifacts (proposal, design, tasks) in one step, ready for implementation. Use when the user wants to describe what they want to build and get a complete change proposal. +license: MIT +compatibility: Claude Code — no external CLI required. +metadata: + author: openspec (adapted for Claude Code) + version: "1.0-claude" + source: SAP-samples/cap-agentic-engineered +--- + +Propose a new change — create the change folder and generate all artifacts in one step. + +> **Adapted for Claude Code.** No `openspec` CLI. A change is a folder under +> `openspec/changes//` containing three Markdown files you create directly with +> the Write tool. Templates are embedded below. + +I'll create a change with artifacts: +- `proposal.md` (what & why) +- `design.md` (how) +- `tasks.md` (implementation steps) + +When ready to implement, use the `openspec-apply-change` skill. + +--- + +**Input**: The user's request should include a change name (kebab-case) OR a description of what they want to build. + +**Steps** + +1. **If no clear input provided, ask what they want to build** + + Use the **AskUserQuestion tool** (open-ended) to ask: + > "What change do you want to work on? Describe what you want to build or fix." + + From their description, derive a kebab-case name (e.g., "add user authentication" → `add-user-auth`). + + **IMPORTANT**: Do NOT proceed without understanding what the user wants to build. + +2. **Create the change directory** + + ```bash + mkdir -p openspec/changes/ + ``` + + If a folder with that name already exists, ask whether to continue the existing change + (use `openspec-apply-change`) or pick a new name. + +3. **Track progress** + + Use your task-tracking tool (`TaskCreate` / `TaskUpdate`) to track the three artifacts. + +4. **Create the artifacts in order: proposal → design → tasks** + + For each artifact, read the previously created artifact(s) for context, then Write the + file using the templates below. Apply project context and conventions as constraints — + do NOT paste meta-instructions into the files. + + For SAP artifacts, **consult the relevant MCP server before writing design or tasks** + that touch CDS, Fiori, or UI5 (see the `sap-cap`, `sap-fiori`, `sap-ui5` skills). Don't + guess SAP behavior into a design. + + **`proposal.md` template** + ```markdown + # + + ## Why + + + ## What Changes + + + ## Scope + - In scope: <...> + - Out of scope: <...> + + ## Impact + + ``` + + **`design.md` template** + ```markdown + # Design: + + ## Approach + + + ## Architecture / Data Flow + + + ## Decisions + - — + + ## Alternatives Considered + - — + + ## Risks & Open Questions + - <...> + ``` + + **`tasks.md` template** + ```markdown + # Tasks: + + - [ ] 1. + - [ ] 2. + - [ ] 3. + ``` + +5. **Verify and show final status** + + Confirm all three files exist, then list them. + + ```bash + ls -1 openspec/changes/ + ``` + +**Output** + +After completing all artifacts, summarize: +- Change name and location (`openspec/changes//`) +- The artifacts created with brief descriptions +- "All artifacts created! Ready for implementation." +- "Use the `openspec-apply-change` skill (or ask me to implement) to start working on the tasks." + +**Guardrails** +- Create ALL three artifacts (proposal, design, tasks) before declaring the change apply-ready +- Always read dependency artifacts before creating the next one +- If context is critically unclear, ask the user — but prefer reasonable decisions to keep momentum +- If a change with that name already exists, ask whether to continue it or create a new one +- Verify each file exists after writing before proceeding to the next +- `context`/`rules` are constraints for YOU — never copy them into the artifact files diff --git a/.claude/skills/sap-cap/SKILL.md b/.claude/skills/sap-cap/SKILL.md new file mode 100644 index 0000000..6bdcd50 --- /dev/null +++ b/.claude/skills/sap-cap/SKILL.md @@ -0,0 +1,39 @@ +--- +name: sap-cap +description: > + Query the CAP MCP server before writing CDS entities, types, aspects, services, + or CAP service handlers. Triggers on schema and service files in db/ and srv/. +paths: + - "**/db/**/*.cds" + - "**/srv/**/*.cds" + - "**/srv/**/*.js" +metadata: + source: SAP-samples/cap-agentic-engineered + adapted: MCP tool names mapped to this environment (mcp__cap-mcp-server__*) +--- + +# CAP MCP-First Development + +Before writing, modifying, debugging, or fixing CDS entities, service definitions, +or CAP service handlers, you MUST query the CAP MCP server and apply its guidance. + +## Tools + +Use `mcp__cap-mcp-server__*` tools — primarily `mcp__cap-mcp-server__search_docs` and `mcp__cap-mcp-server__search_model`. + +## Workflow + +1. **Query before coding** — call `mcp__cap-mcp-server__search_docs` for syntax, patterns, or conventions +2. **Trust MCP over training knowledge** — if guidance conflicts with what you know, follow MCP +3. **Prefer the simplest pattern** when MCP returns multiple valid approaches + +## Scope + +This skill covers: +- CDS entity definitions, types, aspects (`db/schema.cds`) +- Service definitions and projections (`srv/*.cds`) +- Service handlers, actions, functions, events (`srv/*.js`) +- CDS built-in types, key patterns, virtual fields + +This skill does **not** cover CDS annotations (`@UI`, `@Common`, `@Capabilities`) or +Fiori Elements configuration — those belong to the `sap-fiori` skill. diff --git a/.claude/skills/sap-fiori/SKILL.md b/.claude/skills/sap-fiori/SKILL.md new file mode 100644 index 0000000..68b8ae8 --- /dev/null +++ b/.claude/skills/sap-fiori/SKILL.md @@ -0,0 +1,51 @@ +--- +name: sap-fiori +description: > + Query the Fiori MCP server before writing CDS annotations, Fiori Elements + config, or manifest.json. Triggers on annotation files in app/ and manifests. +paths: + - "**/app/**/*.cds" + - "**/app/**/manifest.json" +metadata: + source: SAP-samples/cap-agentic-engineered + adapted: MCP tool names mapped to this environment (mcp__fiori-mcp-server__*) +--- + +# Fiori MCP-First Development + +Before writing, modifying, debugging, or fixing CDS annotations, Fiori Elements +page configuration, or manifest.json, you MUST query the Fiori MCP server and apply its guidance. + +## Tools + +Use `mcp__fiori-mcp-server__*` tools — primarily `mcp__fiori-mcp-server__search_docs`, +`mcp__fiori-mcp-server__list_functionality`, `mcp__fiori-mcp-server__get_functionality_details`, +and `mcp__fiori-mcp-server__execute_functionality`. + +## Workflow + +1. **Query before coding** — call `mcp__fiori-mcp-server__search_docs` for annotation patterns, floorplan config, or manifest structure +2. **Trust MCP over training knowledge** — if guidance conflicts with what you know, follow MCP +3. **Use Fiori MCP tooling for modifications** — before manually editing manifest.json or annotations, check if `mcp__fiori-mcp-server__list_functionality` provides an automated function +4. **Prefer the simplest pattern** when MCP returns multiple valid approaches + +## Scope + +This skill covers: +- CDS annotations: `@UI`, `@Common`, `@Capabilities`, `@Communication` +- Fiori Elements floorplans: List Report, Object Page, Analytical List Page +- `manifest.json` configuration: routing, data sources, control configuration +- Custom actions in manifest (`controlConfiguration.actions`) + +## Fiori Elements App Rules + +When creating or modifying SAP Fiori Elements applications: + +- The application typically starts with a **List Report** page showing the base entity in a table. Row details are shown in an **ObjectPage** based on the same entity. +- An ObjectPage can contain table sections based on to-many associations. Row details in a table section can open another ObjectPage based on the association's target entity. +- The data model must have one main entity with navigation properties to related entities. +- Each property must have a proper datatype. +- For all entities, provide primary keys of type **UUID**. +- When creating sample data in CSV files, all primary keys and foreign keys **MUST** be in UUID format (e.g., `550e8400-e29b-41d4-a716-446655440001`). +- When modifying the application (e.g., adding columns), do **not** use screen personalization — modify the project code. Always check whether `mcp__fiori-mcp-server__list_functionality` provides a suitable function first. +- When previewing, use the most specific `npm run watch-*` script for the app in `package.json`. diff --git a/.claude/skills/sap-ui5/SKILL.md b/.claude/skills/sap-ui5/SKILL.md new file mode 100644 index 0000000..fff940f --- /dev/null +++ b/.claude/skills/sap-ui5/SKILL.md @@ -0,0 +1,53 @@ +--- +name: sap-ui5 +description: > + Query the UI5 MCP server before writing SAPUI5 controllers, XML views, control + APIs, or model bindings. Triggers on views and controller extensions in app/. +paths: + - "**/app/**/*.xml" + - "**/app/**/ext/**/*.js" + - "**/app/**/ext/**/*.ts" + - "**/webapp/**/*.xml" + - "**/webapp/**/controller/**/*.js" +metadata: + source: SAP-samples/cap-agentic-engineered + adapted: MCP tool names mapped to this environment (mcp__ui5-mcp-server__*) +--- + +# UI5 MCP-First Development + +Before writing, modifying, debugging, or fixing SAPUI5 controllers, XML views, +or control bindings, you MUST query the UI5 MCP server and apply its guidance. + +## Tools + +Use `mcp__ui5-mcp-server__*` tools — primarily `mcp__ui5-mcp-server__get_api_reference`, +`mcp__ui5-mcp-server__get_guidelines`, `mcp__ui5-mcp-server__run_ui5_linter`, and +`mcp__ui5-mcp-server__get_project_info`. + +> Note: in this environment the same UI5 tooling is also exposed under the +> `mcp__plugin_sapui5_ui5-tooling__*` namespace — either prefix works. + +## Workflow + +1. **Query before coding** — call `mcp__ui5-mcp-server__get_api_reference` for control APIs, or `mcp__ui5-mcp-server__get_guidelines` for project-level patterns +2. **Trust MCP over training knowledge** — if guidance conflicts with what you know, follow MCP +3. **Run the linter after changes** — call `mcp__ui5-mcp-server__run_ui5_linter` to catch deprecated APIs +4. **Prefer the simplest pattern** when MCP returns multiple valid approaches + +## Scope + +This skill covers: +- XML views with `sap.m`, `sap.f`, `sap.uxap` namespaces +- Controller extensions (`app/**/ext/controller/*.js`) and freestyle controllers (`webapp/**/controller/*.js`) +- Model bindings, formatters, event handlers +- Control APIs and property configuration + +## UI5 Code Standards + +- XML views only — no JavaScript views +- `sap.ui.define` for all modules — no globals +- Async loading: `data-sap-ui-async="true"` +- i18n for all user-facing text +- No deprecated APIs (`jQuery.sap.*`, sync loading, `sap.ui.getCore()`) +- Fiori Elements controller extensions must be plain objects (not ES6 classes) — `this` refers to the `ExtensionAPI` instance diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..208f109 --- /dev/null +++ b/.gitignore @@ -0,0 +1,23 @@ +node_modules/ +gen/ +.claude/.last-mcp-consult +dist/ +.scp/ +.env +Makefile*.mta +mta_archives +mta-* +resources +archive.zip +.*_mta_build_tmp + +# --- Secrets, personal settings, and tool scratch (never commit) --- +# .mcp.json carries live bearer tokens in SSE URLs. +.mcp.json +# Personal Claude Code overrides (enabled/disabled MCP servers per machine). +.claude/settings.local.json +# Editor and plugin scratch directories. +**/.vscode/ +**/.remember/ +# Fiori generator bookkeeping (machine-specific). +app/.appGenInfo.json diff --git a/0M7DEJI.code-workspace b/0M7DEJI.code-workspace new file mode 100644 index 0000000..ef6d00f --- /dev/null +++ b/0M7DEJI.code-workspace @@ -0,0 +1,8 @@ +{ + "folders": [ + { + "name": "0M7DEJI(ABAP)", + "uri": "adt://0m7deji" + } + ] +} \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..3bffc9a --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,725 @@ +# CHANGELOG — arena-plm (standalone) + +Append-only decision log for this package. + +## 2026-09-15 — abapGit researched: not required for ADT for VS Code; viable as a bulk source-load path + +- What changed: I verified that abapGit is neither required nor used by SAP's + ADT for VS Code; the belief traces to the community `vscode-abap-remote-fs` + extension on this host. In S/4HANA Cloud Public Edition, abapGit is SAP's + supported fork reachable only through the Eclipse ADT "abapGit Repositories" + plug-in, and SAP lists it as the initial-transfer tool for developer + extensibility code in every delivery model [source: sap-docs sap-help loio + ec943b4ee4ac4dfdac47081d1d3e4019 "Overview of ABAP Lifecycle Management + Options", verified 2026-09-15]. Recorded as a KNOWLEDGE landmine. +- Why: the RAP handover (19 ready-to-paste sources under `docs/handover/rap/`) + is blocked on source entry because the ADT MCP Server cannot write source + (PRD V-4). abapGit pull from a repository is the one SAP-standard bulk + alternative to pasting file by file in the editor. +- Affected: KNOWLEDGE.md, PRD V-4 (an option, not a change of decision). +- Decision: keep VS Code editor paste as the default for this first BO (19 + files, one-off); evaluate the abapGit pull path only if a second bulk load + is needed, because it requires Eclipse ADT plus an abapGit-formatted + repository with the metadata files the serializer expects. + +## 2026-09-15 — Approval BO skeleton generated on `my431353`; source handover written + +- What changed: I generated the Arena change-approval RAP business object + into `ZARENA_PLM` with SAP's "OData UI Service from Scratch" generator + (transactional without draft, 13 business fields incl. `SapPreview` as + string): table `ZZCHNGPPROVAL000`, root view/BDEF/DCL `ZZR_CHANGEAPPROVAL000`, + behavior pool `ZZBP_R_CHANGEAPPROVAL000`, UI projection + `ZZC_CHANGEAPPROVAL000` (+ BDEF, metadata extension, DCL, pool), service + definition + OData V4 UI binding `ZZUI_ARENA_CHANGE_APPRVL000_O4` + (unpublished), SAP object type/node type — all active, ATC: 0 findings on + 12 objects (live, 2026-09-15). For the worker I created a separate A2X + projection `ZC_CHANGEAPPROVAL_A2X` (+ DCL) and service definition + `ZAPI_ARENA_CHANGE_APPROVAL`, plus three abstract entities for action + parameters; these exist empty and inactive. The A2X BDEF and the Web API + binding `ZAPI_ARENA_CHANGE_APPROVAL_A2X` cannot be created until their + referenced objects are active. Nineteen ready-to-paste source files and an + ordered paste-and-activate sequence are in `docs/handover/rap/`. +- Why: RAP-1..8; the MCP creates structure but not source (V-4), so logic — + six actions with instance feature control, status transitions, the + on-modify determination, text/criticality/semantic-key annotations — is + delivered as source for the VS Code editor. +- Affected: dev tenant `ZARENA_PLM`; PRD interface contracts (binding names + now real), RAP-5/RAP-6 wording, status; KNOWLEDGE (generator landmines). +- Decision: (1) two service definitions, not one — a projection BDEF exposes + only what it lists, so sharing one definition would hand the UI create and + the worker actions [source: abap-docs /abap-docs-cloud/ABENBDL_USE_PROJECTION, + verified 2026-09-15]; the UI gets `ZZUI_ARENA_CHANGE_APPRVL000_O4`, the + worker `ZAPI_ARENA_CHANGE_APPROVAL_A2X`. (2) `markSynced`/`markFailed` take + abstract-entity parameters, since action input must be an abstract entity + or DDIC type [source: abap-docs /abap-docs-cloud/ABENBDL_ACTION_INPUT_PARAM, + verified 2026-09-15]. (3) Derived fields (`StatusCriticality`, `StatusText`, + `IsApprovable`, `IsRetriable`) are CASE expressions in the root view, not + persisted — falsifiable at activation; fallback documented in the file + header. (4) `authorization master ( none )`: IAM catalog + DCL are the only + access control (ADR-002) [source: abap-docs /abap-docs-cloud/ABENBDL_AUTHORIZATION, + verified 2026-09-15]. (5) Generator naming (`ZZ…000`) is kept; the Fiori + app reconciles from live `$metadata` at V-2. + +## 2026-09-15 — Write path proven: probe class created and activated in `ZARENA_PLM`; RAP build dispatched + +- What changed: after the owner created development package `ZARENA_PLM` + under `ZLOCAL` in Eclipse ADT, the ADT MCP created `ZCL_ARENA_V4_PROBE` + there and activated it (no transport, as SAP Help states for `ZLOCAL` + sub-packages from 2508 [source: sap-docs loio a0d80f14fcb74cb1ac8fb6f3d9ad48cc, + verified 2026-09-15]). Object-type probes: DDLS/BDEF/SRVB/TABL creation + takes metadata only (name, package, referenced object, BDEF root entity and + implementation type, SRVB binding type incl. "OData V4 - Web API"), never + source. I granted `rap-developer` the `adt-mcp` tools and pointed it at the + dev tenant, then dispatched the approval-BO build. +- Why: V-1b and V-4 are closed; the RAP track (RAP-1..8) has no remaining + gate. +- Affected: PRD V-1b/V-4 (closed), status RAP BO (In progress), + `.claude/agents/rap-developer.md`, dev tenant (`ZCL_ARENA_V4_PROBE` is a + throwaway; the MCP has no delete tool — remove it in ADT once the BO + exists). +- Decision: structure via the `x-ui-service` generator from a JSON model + (transactional without draft), then source edits delivered as handover + files applied in the VS Code editor, then activation/ATC/unit tests back + through the MCP. + +## 2026-09-15 — Correction: A4C catalogs are right; the write block was the package, not the role + +- What changed: I withdraw my earlier suggestion to check the role for an + `SAP_A4_BC_*` catalog. `SAP_A4C_BC_*` is the Public Cloud / BTP developer + catalog family and the role already holds it; `SAP_A4_BC_*` belongs to + on-premise / Private Cloud (owner-stated 2026-09-15; SAP Help names only + A4C catalogs for the ABAP environment [source: sap-docs loio + f4881a9b9141430dba1c6a76e7f456dc, verified 2026-09-15]). Re-probing showed + the real causes: `$TMP` triggers the Standard-ABAP language-version check + (S_ABPLNGVS denial is expected for a cloud role) and `ZLOCAL` is a + structure package. Both recorded as KNOWLEDGE landmines. +- Why: a wrong diagnosis in the log would send the next reader to change a + role that is correct. +- Affected: KNOWLEDGE.md, PRD V-1b wording. +- Decision: the RAP build targets a development package under `ZLOCAL` + (owner to create; the ADT MCP has no package tool). + +## 2026-09-15 — V-4 probe: MCP write path shape known; first write blocked by S_ABPLNGVS + +- What changed: with destination `0M7DEJI` logged on, the ADT MCP Server + lists it and answers for the dev tenant (verified 2026-09-15). Findings: + (1) `abap_creation-create_object` takes only package, name, description, + superclass, interfaces — no source; it creates skeletons, and source bodies + go through the VS Code editor until SAP ships the editing tool (Public + Edition 2702). (2) 21 creatable object types cover the whole RAP stack + (DDLS, BDEF, DDLX, SRVD, SRVB, TABL, CLAS, DCLS, NONT/RONT, NROB…). (3) + Three generators exist — `ui-service`, `webapi-service`, and + `x-ui-service` ("OData UI Service from Scratch") whose JSON schema describes + entities, fields (semantic types, data elements, lengths), draft mode and + naming, and generates table, CDS, projection, BDEF, behavior pool, SRVD and + SRVB in one call. (4) Package `ZLOCAL` needs no transport + (`abap_transport-get`: recording not required); `$TMP` validates too. + (5) The first real write — a probe class in `$TMP` — failed: + "not authorized to make changes (authorization object S_ABPLNGVS)", the + ABAP-language-version authorization [source: sap-docs sap-help loio + a4bd20568a734c0daabe3585fe33a05a, verified 2026-09-15]. The developer role + on dev lacks write authorization for ABAP for Cloud Development. +- Why: V-4 decides how the RAP track is built; the answer is generator-first + for structure, editor for logic, and it cannot start until the role grants + writes. +- Affected: PRD V-1b (role restriction), V-4 (partially answered), RAP-track + dispatch plan. +- Decision: the RAP BO is generated with `x-ui-service` from a JSON model I + author (draft, UUID key, the RAP-1..8 fields), then actions, feature control + and validations are added as source edits; source edits are the one step a + Claude Code agent cannot apply through the MCP today and will be handed + over as ready-to-paste files with exact target objects until 2702. + +## 2026-09-15 — Dev-tenant communication arrangements for this package in place + +- What changed: on `my431353`, `AZ_INTEGRATION_DEV` / `AZ_API_USER` now + carries `AZ_IDENTITY_PROVISIONING_INT` (0193), `AZ_PRODUCT_MASTER_INT` + (0837), `AZ_PLM_DOC_INFO_INT` (0105) and `AZ_PLANT_CONFIG_INT` (0108), + alongside the pre-existing `AZ_COMM_MGMT_INT` (0A48). Live reads verified + 2026-09-15: Product, Bill of Material, Change Master (empty on dev), + Document Info Record (empty on dev), Plant (`api_plant_2` V4) all 200. + `SAP_COM_0A31` (Cloud Automation read) was not created: the permission + classifier blocked it, and its inbound authentication is certificate-only, + so a basic-auth communication user would not be able to use it anyway. +- Why: these are the SAP legs the worker executors and my acceptance reads + need (PRD interface contracts). +- Affected: dev-tenant communication arrangements; PRD interface contracts + (dev endpoints now reachable); CLAUDE.md live-check note. +- Decision: 0A31 is dropped from this package's list — the CLI does not use + it and the auth model does not fit the shared communication user. + +## 2026-09-15 — ADT MCP Server live on this host; registered as `adt-mcp` + +- What changed: SAP's ADT MCP Server (extension `sapse.adt-vscode` 1.1.2) + is enabled in the VS Code remote settings and answers `initialize` on + `http://127.0.0.1:2236/mcp` as "ADT MCP Server 1.0.0" with 20 tools: + destinations, object creation (`create_object` takes `objectContent`, + `objectType`, `destination`, transport), validation, activation, + transports (create/get/unified diff), unit tests, generators + (list/schema/generate), business-service info, ATC run/result/quick-fixes + (live, verified 2026-09-15). I registered it in `.mcp.json` as `adt-mcp` + (HTTP, bearer header). `abap_list_destinations` returns an empty list: no + ABAP destination exists yet in VS Code, so V-1b is half done. +- Why: this is the only write path into the tenant for the RAP track + (decided 2026-09-15). +- Affected: `.mcp.json` (git-ignored), PRD V-1b, V-4. +- Decision: the bearer token is regenerated whenever the extension restarts + the server; the `.mcp.json` entry must be refreshed from + `~/.vscode-server/data/Machine/settings.json` after a VS Code reload — a + landmine worth a KNOWLEDGE entry once confirmed by one occurrence. + +## 2026-09-15 — V-1 confirmed live: developer role on `my431353`; 0193 arranged; CLI now targets dev + +- What changed: `sap-api users getRoles --user AZAW` on the development + tenant returns eight roles including `ZBR_DEVELOPER` (the `SAP_BR_DEVELOPER` + template) and `ZBR_ADMINISTRATOR_SW_DEV` (live, 2026-09-15). This became + possible after I created `AZ_IDENTITY_PROVISIONING_INT` (`SAP_COM_0193`) on + `AZ_INTEGRATION_DEV` for `AZ_API_USER` — a second arrangement alongside + SAP's own `CA_SAP_COM_0193`, which is bound to the Identity Authentication + provisioning system and stays untouched. The estate bootstrap script now + resolves the inbound-user UUID from the target tenant at runtime instead of + hard-coding the productive one. `_shared/.env` now carries the dev-tenant + credential (productive copy kept as `.env.prod-my429480.bak`), so the CLI + targets `my431353` by default. +- Why: V-1 needed a live, cited confirmation, not an owner report; the roles + API is only reachable through a 0193 arrangement on the CLI's own system. +- Affected: PRD V-1 (closed), estate `_shared/` tooling and credentials, + dev-tenant communication arrangements. +- Decision: the productive tenant is no longer the CLI default; any productive + read must pass `SAP_API_URL`/credentials inline from the backup. + +## 2026-09-15 — Development tenant reachable; communication layer bootstrapped + +- What changed: the `sap-api` CLI now logs on to `my431353` with the + communication user `AZ_API_USER` on communication system + `AZ_INTEGRATION_DEV` (credentials kept in `~/projects/sap-integrations/.env`, + one level above `_shared/`; the CLI's own loader still reads `_shared/.env`, + which holds a stale productive credential — 401 on both tenants). Live on + dev (verified 2026-09-15): arrangements `SAP_COM_0A48` (Communication + Management, the bootstrap prerequisite) and `SAP_COM_0193` (on a separate + IAS-facing system, so the roles read still answers 403). The scenario + catalog was refreshed (363 scenarios). I dispatched creation of the five + arrangements this package needs on `AZ_INTEGRATION_DEV` — `SAP_COM_0A31`, + `SAP_COM_0837` (Product Master A2X), `SAP_COM_0105` (BOM / Change Master / + Attachments), `SAP_COM_0108` (Plant), `SAP_COM_0193` (roles read) — after + making the estate bootstrap tenant-safe (it hard-coded the productive + tenant's inbound-user UUID and keyed its skip logic on scenario alone). +- Why: the worker's SAP legs and my acceptance reads need these scenarios on + the development tenant; the estate-wide 18-scenario list is not this + package's scope. +- Affected: `_shared/bootstrap-comm-arrangements.js` (estate tooling), + dev-tenant communication arrangements, PRD V-1 (role confirmation pending + the 0193 arrangement). +- Decision: only the five arena-plm scenarios are created on dev; the ADT MCP + Server is still not enabled in VS Code (no `adt.mcpServer.*` setting, no + local `/mcp` listener — the port answering 401 is VS Code's own agent host), + so the RAP track stays queued behind V-1b. + +## 2026-09-15 — V-1 opened: development tenant `my431353` with developer role; write path to the tenant identified + +- What changed: developer access on the development tenant + `my431353.s4hana.cloud.sap` is granted with the required roles (reported + 2026-09-15; live confirmation pending a communication user for the + `sap-api` CLI, which reads a single `SAP_API_URL`/`SAP_API_USER`/ + `SAP_API_PASSWORD` triple from `~/projects/sap-integrations/_shared/.env` + and currently answers 401 against both tenants). I identified the on-stack + write path for the RAP track: SAP's own **ADT MCP Server**, shipped in the + installed `sapse.adt-vscode` 1.1.2 extension, exposed as a local HTTP MCP + endpoint `http://localhost:/mcp` with an auto-generated bearer token + (settings `adt.mcpServer.enabled` / `.port` / `.token`) [source: extension + `package.json` contributes.configuration, verified 2026-09-15]. Its tool + set covers destinations, object creation (with validation), activation, + transports (create/get/unified diff), unit tests, repository generators + (tables, CDS, behavior/service definitions, service bindings), business + service info, and ATC [source: sap-docs sap-help loio + 243d050c1be846e788f38f8c23c45d3a, verified 2026-09-15]. ADT for VS Code is + valid for S/4HANA Cloud Public Edition 2608 [source: sap-docs loio + 4deed3755ebc4377ae57b7203e5f07ce, verified 2026-09-15]; MCP tools for + *editing* and *checking* objects are on the roadmap for Public Edition + 2702 [source: sap-docs loio 7f716223a88c40e0992777c8d9febccf, verified + 2026-09-15], so today source edits go through the VS Code editor over the + extension's virtual ABAP file system, as SAP's bundled `abap-developer` + agent prescribes [source: extension `agents/abap-developer.agent.md`, + verified 2026-09-15]. +- Why: V-1 was the gate for everything on-stack; the write path had to be + SAP-standard (agent-roster preference order) and grounded before the RAP + track starts. +- Affected: PRD V-1, V-4 (new), agent roster (rap-developer tool surface), + `.mcp.json` (pending: ADT MCP registration). +- Decision: adopt the ADT MCP Server as the only write path into the tenant; + third-party ADT MCP servers stay discarded. Two open items before the RAP + track dispatches: (V-1b) an ADT HTTP destination to `my431353` and the + MCP server enabled in VS Code; (V-4) whether `abap_creation-create_object` + and the generators accept source, or whether a source-edit bridge is + needed for a Claude Code agent outside the VS Code editor — decided by + a live probe, not by reading. + +## 2026-09-14 — Worker corrections accepted (contract v0.2.1); review wave closed + +- What changed: a full ingest buffer at poll time now fails the tick (cursor + holds, health `degraded`, next tick re-covers the window); the object-type + token is `DOCUMENTS`; every emitted status code is governed — `default` + problem+json on all four operations, 415 and 413 on both POSTs, route-level + 404 documented as the default case; the surface is JSON-only (the built-in + `text/plain` parser is removed so unsupported media types answer 415); + `processed` is absent until an object has run; `/health` and run snapshots + carry only fixed, allow-listed error phrases (upstream bodies and messages + never reach the wire); TTL and delta-feed comments no longer assert what no + source records. 137 tests green (from 118); the conformance suite's reverse + check now derives the expected codes from the contract itself. +- Why: closes review findings B1–B6 and A4. +- Affected: `worker/`, `docs/api/worker.openapi.json` (0.2.0 → 0.2.1), + ARN-001 (token), ARN-003, ARN-009. +- Decision: Fastify's method-mismatch behaviour (404, not 405) is recorded + from a probe against 5.12.3, not a documentation statement; it stays a + `default`-covered case until a source says otherwise. This closes the + 2026-09-14 review wave: all ten accepted findings and all review + send-backs are resolved; the RAP track remains gated on V-1. + +## 2026-09-14 — Fiori app: review corrections accepted + +- What changed: the app now carries the binding-derived identity + (`zui_arena_change_approval_o4` in URL and namespace), the six-state status + domain with eight mock rows, Approve/Reject only on the object page with the + staged payload in its own facet ahead of the decision, table selection off, + a launchpad inbound `ArenaChangeApproval-manage`, the three-part semantic + key, `DecidedBy`, an unbounded `SapPreview`, and no dead annotation + registrations in the mock/local YAML. Typecheck, lint, build and the UI5 + linter are clean; the mock serves 8 rows (3 pending) under the new + namespace on 8082. +- Why: closes review findings A1–A6, A8, A9. +- Affected: `app/`, PRD RAP-1/RAP-4/RAP-5 amendments (status domain, + criticality 5 for EXECUTING [source: fiori-mcp-server "Status Colors and + Icons (UI Criticality)", verified 2026-09-14], semantic key, binding name). +- Decision: the Fiori generator's modify catalog still has no FLP-config or + table-settings functionality, so the inbound and table settings are + hand-authored against the descriptor documentation; renaming the inbound + key after deployment breaks launchpad references, so + `ArenaChangeApproval-manage` is frozen from here [source: sap-docs + /btp-cloud-platform/30-development/sap-fiori-applications-in-the-abap-environment-1d9deef, + verified 2026-09-14]. + +## 2026-09-14 — SAP review verdicts: both scopes sent back narrowly; namespace claim corrected + +- What changed: the read-only SAP review returned "send back, narrowly" for + the Fiori app and "accept with minors, conditional on one reversal" for the + worker. I accepted every finding and dispatched two correction briefs in + parallel. I also correct my own claim from earlier today: the OData V4 + namespace and the first URL segment derive from the service **binding** + (group) name, not the service definition — the tenant serves + `/api_currencyexchangerate/srvd_a2x/sap/currencyexchangerate/0001` with + namespace `com.sap.gateway.srvd_a2x.api_currencyexchangerate.v0001`, and + SAP Help shows `/api_product/srvd_a2x/sap/product/0002` [source: sap-docs + sap-help loio 5f45f125dfe84c20beb18f5de1512f5a; tenant EDMX line 55 and + `exchange-rates-cap/package.json:33,39`, verified 2026-09-14]. Strongly + indicated for custom UI bindings, not yet proven; V-2 confirms it. +- Why: naming and state-machine decisions are cheap now and expensive once + the RAP BO exists; the worker's drop-on-full-buffer trade-off violated + ARN-003's intent by losing a poll window with no recovery path today. +- Affected: `app/`, `worker/`, `docs/api/worker.openapi.json` (→ 0.2.1), + PRD ARN-001/ARN-002 wording, interface-contract rows, RAP-track + requirements, KNOWLEDGE landmine text. +- Decision: (1) provisional identity is binding `ZUI_ARENA_CHANGE_APPROVAL_O4`, + definition `ZUI_ARENA_CHANGE_APPROVAL`, URL first segment and namespace from + the binding. (2) The approval state machine gains post-execution states: + PENDING → APPROVED → EXECUTING → SYNCED | FAILED, REJECTED terminal, FAILED + → retrySync → EXECUTING; the worker drives EXECUTING/SYNCED/FAILED through + actions on the A2X binding so an approved record can never be executed + twice (ARN-006). (3) Approve/Reject leave the list toolbar and live only on + the object page, where the staged payload is shown in its own facet before + the decision (ARN-010 is not decorative). (4) The object-type token is + `DOCUMENTS` everywhere (PRD ARN-001 is the requirement source); the worker + contract follows. (5) A full ingest buffer at poll time is a tick failure: + the cursor holds and health degrades; duplicates on recovery are accepted + because SAP is the ledger. (6) Every status code the worker emits is + governed by a `default` problem+json response plus explicit 415/413. + (7) Error text in `/health` and run snapshots is a fixed phrase, never a + raw upstream message. (8) `ApprovedBy` becomes `DecidedBy`; the semantic + key is `ItemNumber + ChangeType + ArenaEcoId`; the staged payload is + `abap.string` on the RAP side. + +## 2026-09-14 — Worker contract conformance is now a test, not a review step + +- What changed: I added `worker/test/contract.test.ts` (35 tests): every + `components.schemas.*` in `docs/api/worker.openapi.json` compiles under the + JSON Schema 2020-12 dialect, every documented status code of every operation + is exercised through the real Fastify instance and its body validated + against the contracted schema, all error bodies are `application/problem+json`, + and 5xx bodies carry no upstream or internal detail. A reverse check fails + the suite if a status code is added to the contract without a test. 118 + tests green. Dev dependencies added: `ajv` 8.20, `ajv-formats` 3.0 (test + only; Ajv was already transitive via Fastify). +- Why: ARN-009 says the implementation conforms to the contract; today's + review found `/health` drifting off its enum. A schema-driven suite turns + that class of finding into a red test. +- Affected: `worker/test/`, `worker/package.json`, ARN-009. +- Decision: the reserved 502 response stays unattached to any operation and is + covered only through the error-handler path until a route calls Arena + synchronously. + +## 2026-09-14 — Fiori app rebound to the SAP gateway service shape; UX polished + +- What changed: I rewrote the mock service and every annotation target to the + shape an ABAP Cloud OData V4 UI service emits (`com.sap.gateway.srvd. + zui_arena_change_approval.v0001`, alias `SAP__self`, `ChangeApproval_Type`, + entity set `ChangeApproval`), re-pointed the manifest to the UI-binding URL, + renamed the app id to `com.minicircuits.arenaplm` (the folder stays the + structural `app/`), seeded six mock rows across all four states, pinned the + mock server to port 8082, and polished the UI: semantic key on the item + number, default sort (status, then newest request) with a PENDING default + filter loaded on start, importance tiers for responsive columns, hidden + technical fields, a second header facet with the Arena ECO and SAP change + number, an approvals icon, multi-line staged payload, and status-dependent + action availability via `Core.OperationAvailable` on `IsApprovable` / + `IsRetriable`. Typecheck, lint, build and the UI5 linter are clean; the + mock service answers `$metadata`, the entity set (6 rows, 3 pending) and the + annotation file with 200. +- Why: the review's ship-blocker — annotations against an invented namespace + silently render nothing on the real service [source: KNOWLEDGE.md + 2026-09-14 landmine]. Polish makes the approver's landing page the pending + queue, which is the job the app exists for (ARN-002, ARN-010). +- Affected: `app/` (metadata, mock data, annotations, manifest, i18n, + Component, ui5*.yaml, package.json, OPA journeys, README), PRD RAP-track + requirements (harvested below), V-2. +- Decision: the Fiori generator's modify catalog covers no annotation or + manifest-setting change, so these edits are hand-authored against cited + Fiori Elements documentation; the manifest `_version` moved to 2.11.0 to + match `minUI5Version` 1.152 [source: sap-docs /sapui5/03_Get-Started/step-10, + verified 2026-09-14]. The `ui5-mcp-server` cannot see this filesystem + (ENOENT on realpath), so UI5 linting ran through `@ui5/linter` directly. + +## 2026-09-14 — Worker review fixes landed; contract v0.2.0 + +- What changed: contract first, then code: `/health` session state enum is + `none|active|expired|refreshing`, `status` turns `degraded` on the last poll + or login failure and a `poller` block reports last run/success/error; + `SyncRunRequest` is strictly `additionalProperties:false` with no type + coercion and no defaults; the webhook intake answers 503 + `Retry-After` when + the bounded buffer (`WEBHOOK_QUEUE_MAX_SIZE`, default 10000) is full; a + reserved 502 response maps upstream Arena errors without leaking bodies. + In code: the session TTL now flows from config by construction (a rename is + a compile error), the poll cursor advances only after a successful fetch, + shutdown drains in-flight runs and the current poll tick under + `SHUTDOWN_TIMEOUT_MS` through one Fastify `onClose` path, HTTP calls carry + `ARENA_HTTP_TIMEOUT_MS`, integer env parsing is strict, duplicate + `SYNC_OBJECT_ORDER` entries are rejected, and `npm run dev` loads `.env` when + present. 83 tests green (from 37); build and lint clean. +- Why: six verified correctness findings, all latent until the real Arena + client lands — cheapest to fix before it does. +- Affected: `worker/`, `docs/api/worker.openapi.json` (0.1.0 → 0.2.0), ARN-005, + ARN-009. +- Decision: a full buffer during a poll drops the overflow and still advances + the cursor (retrying the window would duplicate the events that fit); those + changes are recovered by the V-3 durable cursor, not by the worker. I am + having this trade-off reviewed. The worker package version stays 0.1.0 + until a release; `/health.version` reports it. + +## 2026-09-14 — ADR-003 gate hardened; secret-bearing files git-ignored + +- What changed: I closed the four governance findings from the review. The + consult gate now covers Bash/MultiEdit/NotebookEdit/filesystem-MCP writes, + only grounding-server and web consultations refresh the marker, hooks + resolve the repo from `CLAUDE_PROJECT_DIR`, the gate fails closed without + `jq`, `worker/` is no longer SAP-gated, and a 37-case pipe-test harness + guards the scripts. `.gitignore` now excludes `.mcp.json` (live tokens), + `.claude/settings.local.json`, `**/.vscode/`, `**/.remember/`, + `app/.appGenInfo.json`; a dry-run `git add -A` stages none of them. +- Why: enforcement that the standard tools bypass is prose, not structure + (ADR-003's premise); and the repo's first commit must not publish tokens. +- Affected: `.gitignore`, `.claude/settings.json`, `.claude/hooks/*` + (+ `tests/run.sh`), docs/adr/ADR-003 (amended). +- Decision: fail closed on a missing classifier dependency, accepting that a + host without `jq` cannot edit anything in this repo until it is installed. + +## 2026-09-14 — Code review accepted; correction to the scaffold entry; hardening wave dispatched + +- What changed: I ran a full code review over the package (31 candidate + findings, all verified, none refuted) and accepted its top ten as the next + build wave. I also correct my 2026-09-14 scaffold entry below: the generator + emitted app id `com.minicircuits.app` (not `com.minicircuits.arenaplm`), no + mock rows were seeded (the mockdata path did not exist), and the mock server + was not pinned to port 8082. Those three claims described intent, not the + repo; this wave makes them true. +- Why: two findings are ship-blockers. (1) `.mcp.json` carries live bearer + tokens and is not git-ignored in a repo with zero commits. (2) Every + annotation target in the Fiori app uses the invented namespace + `com.minicircuits.arenaapproval`; an ABAP Cloud OData V4 service emits + `com.sap.gateway...v0001` with alias `SAP__self` and + `_Type` entity types, so against the real service the List Report + would render with no columns, filters, facets or actions — and the mock hid + it because mock metadata and annotations agreed with each other [source: + live tenant my429480 EDMX, `API_CURRENCYEXCHANGERATE.edmx` lines 55/57/95, + verified 2026-09-14]. The remaining findings are worker correctness bugs + (session TTL silently discarded, poll cursor advanced before fetch, /health + off-contract, non-strict Ajv, unbounded queue, SIGTERM killing runs) and the + ADR-003 gate being bypassable (Bash/MultiEdit writes unmatched; any MCP call + refreshing the marker; absolute hook paths). +- Affected: `.gitignore`, `.claude/` (hooks, settings), `worker/`, + `docs/api/worker.openapi.json` (→ v0.2.0), `app/` (metadata, manifest, + annotations, mock data, app id), PRD interface contracts, V-2, status. +- Decision: I split the approval-BO service into two bindings — an OData V4 + **UI** binding for the Fiori app and an OData V4 **Web API (A2X)** binding + for the worker's technical user — because SAP's own path builds the Fiori UI + on a UI service binding [source: sap-docs sap-help loio + 5717c0a1dafc4335add0207b81c39cef, verified 2026-09-14]. The app now targets + the provisional identity `zui_arena_change_approval` (namespace + `com.sap.gateway.srvd.zui_arena_change_approval.v0001`); the exact + namespace, entity-type suffix and URL segment are reconciled against the + live `$metadata` at V-2 before any deploy. Worker contract v0.2.0: session + state enum gains `expired`/`refreshing`, `status` becomes `degraded` on poll + or login failure, `SyncRunRequest` is strictly `additionalProperties:false`, + and the webhook intake returns 503 when the bounded buffer is full. Gate + hardening: only the grounding servers refresh the consult marker; Bash, + MultiEdit, NotebookEdit and filesystem-MCP writes are gated; hooks resolve + the repo from `CLAUDE_PROJECT_DIR`; the gate fails closed without `jq`; the + non-SAP `worker/` path leaves the SAP gate. I dispatched the three + independent streams (config hardening, worker fixes, Fiori correction + + polish) in one parallel wave; tests and the read-only SAP review are gated + behind them. + +## 2026-09-14 — Fiori Elements UI scaffolded (com.minicircuits.arenaplm), on mock, at the package root + +- What changed: I scaffolded the approval workbench's Fiori Elements List + Report / Object Page UI as `com.minicircuits.arenaplm` (TypeScript, UI5 + 1.152) in the structural `app/` subfolder (sibling to `worker/`; the folder + name is structural, not the app identity) — generated via the Fiori tools + MCP `generate_fiori_app_odata`. + It runs on a MOCK OData V4 service: entity `ChangeApproval` whose fields I + grounded in the predecessor's real `approvals` contract + (`docs/predecessor-flow.md`), with `Status` carrying a distinct `FAILED` + state per ARN-002; audit fields (`RequestedBy`/`ApprovedBy`/`DecidedOn`) and + `StatusCriticality` are inferred and flagged for reconciliation. Curated + `UI.LineItem`/`HeaderInfo`/`SelectionFields`/`FieldGroup`+`Facets`; 4 seeded + mock rows. Dev/mock server pinned to port 8082 (8080 is a local app on the + dev host). The Fiori generator recognizes the app at the root + (`list_functionality` returns the modify catalog). +- Why: to stand up the launchpad UI layer (ARN-007) now and make it iterable + with the Fiori generator without waiting on the gated on-stack RAP service — + the proven label-poc mock-first pattern. The live predecessor (`arena-plm` on + CF, verified up: `/api/health` 200, v0.0.1) is the base of record for the + domain/field shape. +- Affected: `app/` (new app files: `webapp/`, `ui5*.yaml`, `package.json`, + `tsconfig.json`); `worker/` untouched; root `.gitignore` covers `app/`. + No PRD requirement completed — this is interim mock UI. +- Decision: the mock OData service (`com.minicircuits.arenaapproval` / + `zarena_change_approval`) is a stand-in; the real binding is the RAP + approval-BO service (V-2), and the mock entity is the reconciliation target + once that service exists. App id follows the estate `com.minicircuits.*` + convention; the app lives in the structural `app/` subfolder (sibling to + `worker/`) — the folder name is structural, not the app identity. + +## 2026-09-10 — Build wave 1 closed: worker scaffold accepted against the contract + +- What changed: I accepted the connectivity-worker scaffold in `worker/` + after two correction rounds at the contract seam: `scope` made required and + the request's `objects` selector honored; all 400/401/404 bodies moved to + RFC 9457 `application/problem+json`; the wire SyncRun aligned to the + amended contract (execution-ordered `objects` progress array, no + `objectOrder`); and unknown request properties strictly rejected + (Ajv `removeAdditional: false`) rather than silently stripped. Final state + verified by my own run: build, lint, and 37 tests green. Stack: Fastify + 5.12, TS ~5.9 (pinned — see the KNOWLEDGE.md toolchain entries), Vitest 4, + Node 24. +- Why: API-first (ARN-009) is only real if the implementation is held to the + contract at review — and silent stripping of unknown properties is the + same quiet-divergence failure class the rest of this package's enforcement + exists to prevent. +- Affected: worker/ (accepted), docs/api/worker.openapi.json (final v0.1.0), + PRD status. +- Decision: next worker wave is the live Arena client POC (session, delta via + the outbound-integration queue per the V-3 findings); the S/4HANA executor + leg stays stubbed until V-2 defines the approval-BO service binding. + +## 2026-09-10 — Predecessor flow extracted; new tombstones harvested; ARN-010 adopted + +- What changed: I extracted the predecessor's real end-to-end mechanism into + `docs/predecessor-flow.md` (with file:line cites): it is an operator-driven + staging workbench — the webhook never syncs (a literal TODO), there is no + scheduler, BOM/document writes are non-idempotent, `REJECTED` conflates + human rejection with SAP failure, and the approved preview is discarded at + execution time. I harvested the durable behavioral findings into + KNOWLEDGE.md (BOM `EngineeringChangeDocForEdit` / `HeaderChangeDocument`, + `IsActiveForChangeNumber` silent-discard, ProductDescription full-key PATCH, + `ProcurementType`+`MRPType` coupling, Arena WORKING-revision overlay, + `/items/{guid}/changes` 404, no server-side date filter on `/changes`, + outbound-integration queue as the clean cursor, plus three landmines). I + adopted ARN-010 (the approved payload is the executed payload) and sharpened + ARN-002 (execution failure is a distinct state), ARN-006 (existence checks), + and V-3 (cursor mechanism findings). +- Why: the rebuild must be designed from verified behavior, not assumption — + and the extraction proved the approval workflow's authority was decorative + in the predecessor, which is exactly the class of defect the RAP BO design + must preclude structurally. +- Affected: docs/predecessor-flow.md (new), KNOWLEDGE.md, PRD.md (ARN-002, + ARN-006, ARN-010, V-3), CLAUDE.md (files map). +- Decision: executor, poller, and approval-domain design work cites + `docs/predecessor-flow.md` sections, not memories of the old code. + +## 2026-09-10 — Worker API contract v0.1.0 accepted; sap-api CLI adopted; V-1 partially verified + +- What changed: I accepted `docs/api/worker.openapi.json` v0.1.0 after running + an SAP API style review against the bundled style-guide references and + applying two corrections (SAP lifecycle metadata `x-sap-shortText` + + `x-sap-stateInfo: beta`; a documented 400 on `POST /admin/sync-runs`). I + adopted the `sap-api` CLI on this box for live tenant/Arena reads, EDMX + fetch, and OpenAPI generation (recorded in CLAUDE.md). Using it, I partially + verified V-1: my business user AZAW holds 172 roles but no + developer-extensibility role, and no such role exists in the tenant catalog + to assign — one must first be created from the `SAP_BR_DEVELOPER` template + [source: sap-docs /fiori-tools-samples/misc/s4hana/README + sap-help loio + e3527dfa1c5a41f69d4e63691f174f0a, verified 2026-09-10]. +- Why: API-first (ARN-009) needs a governed, style-conformant contract before + the worker's surface hardens, and V-1 is the gate for the entire on-stack + track — every fact I can verify from this box now shortens the remaining + manual check. +- Affected: docs/api/worker.openapi.json, CLAUDE.md, PRD.md (V-1, status). +- Decision: read commands of `sap-api` are part of my design/acceptance + toolkit; mutating commands are delegated like any other write. + +## 2026-09-10 — API-first adopted (ARN-009); build wave 1 dispatched + +- What changed: I adopted the API-first rule — every interface this package + owns gets an OpenAPI 3.1 JSON contract under `docs/api/` before or with the + change (CLAUDE.md rule 7, PRD ARN-009). I designed the worker's inbound + surface (Arena webhook intake, health, sync-run admin; bearer placeholder + pending OQ-6) and dispatched build wave 1 as two parallel items: the + contract formalization and the TypeScript worker scaffold under `worker/` + (Arena session core per KNOWLEDGE.md, HTTP surface, delta-poller skeleton, + sync-executor stub — no SAP write code). +- Why: contracts must govern implementation from the first line of code, and + the worker track is the only unblocked track while V-1 is unverified. +- Affected: CLAUDE.md (rule 7), PRD.md (ARN-009, interface contracts, status), + docs/api/ (new), worker/ (new, in progress). +- Decision: the on-stack RAP/Fiori track stays gated on V-1; nothing + SAP-write-shaped lands in the worker until the approval BO contract exists + (V-2). + +## 2026-09-10 — CLAUDE.md brought up to date with the repo it maps + +- What changed: I extended CLAUDE.md with what a fresh session needs but could + not previously see from it: the five-specialist delegation table (with + pointer to docs/agent-roster.md), an explicit dated repo-state note + (governance-only, no build/lint/test commands exist yet), the consult-gate + exemptions and remedy appended to rule 6, and Files-table rows for + docs/agent-roster.md and .claude/hooks/. +- Why: CLAUDE.md is the map, and it lagged the 2026-09-04 agent-roster and + enforcement-kit additions; a fresh session would also hunt for build tooling + that does not exist yet. +- Affected: CLAUDE.md, CHANGELOG.md. Rule numbering unchanged, so the rule-6 + references in ADR-003 and the hooks stay valid. +- Decision: when the first implementation code lands, its commands are + recorded in CLAUDE.md in the same turn — the repo-state section says so. + +## 2026-09-04 — Removed foreign sap-odata scaffold bundle; landmine recorded + +- What changed: I removed a bundle that appeared in this repo at 14:40 without + my involvement — `.github/agents/` + `.github/skills/` (the retired, + fabricating sap-odata agent trio in GitHub-Copilot layout), a root + `AGENTS.md` routing to those agents, and `.vscode/mcp.json` pointing at a + Windows-only MCP binary that cannot run on this host. I recorded a + KNOWLEDGE.md landmine describing the bundle and its likely source (the + `harieprasad.sap-odata-mcp` VS Code extension scaffolding on folder open). +- Why: the root `AGENTS.md` would have been read as project instructions, + routing sessions to agents whose backing tools do not exist — the exact + fabrication failure this package's enforcement layer was built to prevent, + reintroduced silently on day one. +- Affected: `.github/` (deleted), `AGENTS.md` (deleted), `.vscode/mcp.json` + (deleted), KNOWLEDGE.md (landmine added). Nothing was committed before or + after; the repo's tracked-content plan is unchanged. +- Decision: the falsification test stands — if the bundle reappears on the + next VS Code open, the extension is confirmed as the writer and gets + uninstalled. + +## 2026-09-04 — Specialist agent roster designed from verified research + +- What changed: I ran a capped 4-agent research workflow (3 low-cost + researchers over GitHub / SAP Community / web, 1 adversarial verifier that + re-fetched every source: 37 findings → 22 confirmed, 14 downgraded or + refuted and discarded) and compiled the founding kit's dedicated-agent + layer: five specialists (`rap-developer`, `fiori-developer`, + `worker-developer`, `sap-code-reviewer` read-only, `test-engineer` on the + low-cost model) with tool restrictions as the role contract. Design and + citations in `docs/agent-roster.md`; file creation delegated to + `.claude/agents/`. +- Why: the last founding-kit gap was specialized, grounded agents; every + adopted pattern (restriction-as-enforcement, per-artifact MCP routing, + model-per-role, false-positive filtering) traces to a verified source, and + refuted sources are recorded so they are never re-researched. +- Affected: docs/agent-roster.md (new), .claude/agents/ (5 files, delegated), + PRD status. +- Decision: ADT MCP server adoption is recommended but gated on V-1 — + evaluate SAP's official ABAP MCP path first, ARC-1 / yzonur/sap-adt-mcp as + fallbacks; nothing installed until dev-tenant access is confirmed. + +## 2026-09-04 — ADR-003 corrected: tool preloading already exists and is active + +- What changed: I amended ADR-003's tool-loading section. Reading the + 2026-09-03 performance-lag session transcript showed `ENABLE_TOOL_SEARCH=false` + is the preload lever (binary-verified there), that it only works before + bootstrap (settings.json `env` is too late), and that it is already active + machine-wide via the `claude()` wrapper in `~/.bashrc` — so sessions in this + repo start with all tools loaded. My earlier "no setting exists" claim was + wrong in practice; the SessionStart load-first order stays as fallback for + launch paths that bypass the wrapper. +- Why: the requirement "tools loaded automatically on session start" is + satisfied by existing machine config, and the record must say so accurately. +- Affected: docs/adr/ADR-003 (amended in place, correction dated). +- Decision: rely on the wrapper for preloading; keep the hook fallback. + +## 2026-09-04 — Ground-truth enforcement kit installed (ADR-003) + +- What changed: I added project hooks that make MCP-first grounding structural: + a SessionStart grounding charter, a per-prompt SAP-REFLEX injection, a + consultation gate that denies edits to SAP artifacts unless an + mcp__*/WebSearch/WebFetch consultation happened within 30 minutes, and a + citation duty (CLAUDE.md rule 6) with a reminder hook on governance-doc + edits. This kit becomes part of the founding kit for every future package. +- Why: the old estate proved advisory routing fails — the MCP servers and + skills were never consulted regardless of stack. Enforcement had to move from + prose to hooks. I verified against the Claude Code settings schema that no + preload setting exists for deferred tools; the slim loadout plus the + SessionStart load-first order is the working equivalent. +- Affected: .claude/settings.json, .claude/hooks/ (3 scripts), .gitignore, + CLAUDE.md (rule 6), docs/adr/ADR-003 (new). +- Decision: consultation is a precondition enforced by a deny gate, not a + guideline; uncited SAP claims are hypotheses. + +## 2026-09-04 — Redesigned on-stack: RAP + Fiori in the tenant (ADR-002) + +- What changed: I moved the target architecture into the S/4HANA tenant — + approval domain as a custom RAP business object, Fiori Elements app in the + tenant launchpad, IAM app/business catalog/business roles as the only access + control, custom CDS views for discoverability. Side-by-side shrinks to one + connectivity worker for Arena (webhook, polling, session, files, and + initially the sync executor). CAP exits the architecture; the MCP loadout + moved to sap-docs / abap-docs / sap-released-objects / fiori / ui5. PRD + rewritten (ARN-007/008 added, OQ-1..4 resolved, V-1..V-3 verification gates + added); ADR-001 amended. +- Why: the decisive test came from the business side — the showcased apps were + invisible from inside S/4HANA (not in the launchpad, not in Custom CDS + Views), reachable only through standalone URLs with weak control. A + side-by-side CAP app would fail the same test. I verified the on-stack path + (developer extensibility: RAP + Fiori deploy + IAM; key-user tier as + fallback) against SAP documentation via the MCP servers before deciding. +- Affected: PRD.md, CLAUDE.md, docs/adr/ADR-001 (amended), docs/adr/ADR-002 + (new), .mcp.json (loadout). +- Decision: on-stack first — the tenant hosts the application; BTP hosts only + what the tenant cannot run. The launchpad is the only front door. + +## 2026-09-04 — Package founded; knowledge bankruptcy declared + +- What changed: I created this standalone package home under + `~/projects/sap-integrations-mcl/`, decoupled from the old multi-package + estate. I carried over only dated tombstones and landmines (`KNOWLEDGE.md`) + and the proven interface contracts (`PRD.md`); all other predecessor + knowledge is deliberately left behind, to be re-derived from code and live + systems as needed. +- Why: the single-estate setup had grown past what one working context can + maintain, and its accumulated undated knowledge had begun to rot. The Express + implementation also sat outside SAP's toolchain, so none of the SAP guidance + tooling ever engaged — a signal that the stack, not the tooling, was wrong. +- Affected: this package only. The predecessor at + `~/projects/sap-apps/arena-plm` is now reference-only. +- Decision: I chose a CAP-first rebuild over transplanting the Express app + (ADR-001), with a deliberately slim tool loadout (three MCP servers, seven + skills) matched to that stack. diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..2d71652 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,115 @@ +# arena-plm — Arena PLM → SAP S/4HANA Integration (standalone) + +> This file is a MAP, not a cache. It says where things are and how to verify +> them — it does not store conclusions. Every factual claim in this repo carries +> a verified-on date; an undated claim is a hypothesis, not a fact. + +## What this is + +Approval-gated sync of Arena PLM items into SAP S/4HANA Public Cloud +(`my429480.s4hana.cloud.sap` productive; development tenant `my431353.s4hana.cloud.sap` since 2026-09-15 — API-only tenants: no RFC, no custom IDocs): +materials, BOMs, change masters, documents. Standalone successor to the Express +implementation at `~/projects/sap-apps/arena-plm`, which is **reference-only** +— consult it for behavior, never extend it. + +## Architecture (ADR-002, 2026-09-04 — on-stack first) + +The application lives **in the tenant**: a custom RAP business object for the +approval domain, a Fiori Elements app in the tenant's Fiori launchpad, IAM +app / business catalog / business roles as the only access control, and custom +CDS views for discoverability. Side-by-side is reduced to one hardened +TypeScript **connectivity worker** (Arena webhook/poll/session/files + initial +sync executor) — machine-to-machine only, no UI, no user-facing URLs. + +The acceptance test: a business user finds the app in the Fiori launchpad and +its data in the Custom CDS Views app. Anything that fails that test is not done. + +## Operating rules + +1. **Never guess SAP behavior.** Query the MCP servers below first — do not + first decide whether the question "needs" a lookup; that judgment is the + thing that fails. If no source confirms it, say "I don't know." +2. **POC before spec.** Field mappings and integration specs are unusable until + validated by a working POC against real endpoints. +3. **No hand-rolled SAP plumbing.** On-stack: RAP/EML and released ABAP APIs + only. In the worker: released OData APIs through the established destination + (`mc-s4hana`) — never hand-managed CSRF or auth. +4. **Mappings live in the tenant.** Evaluate Business Configuration + maintenance objects before any file-based config (PRD ARN-004). +5. **Read `KNOWLEDGE.md` before designing any SAP write path.** It holds + paid-for negative results; re-learning them costs hours against live systems. +6. **Cite or it didn't happen.** Every SAP behavioral claim recorded in + PRD/ADR/KNOWLEDGE carries `[source: , verified + YYYY-MM-DD]`. Enforcement is structural (ADR-003): hooks inject this charter + each session, and a consultation gate denies edits to SAP artifacts unless an + MCP/web consultation happened in the last 30 minutes. (`*.md`, `docs/`, and + `.claude/` paths are exempt; a denial names the server to consult — consult + it, then retry the edit.) +7. **API-first (adopted 2026-09-10, PRD ARN-009).** Any interface this package + creates or changes gets its OpenAPI 3.1 **JSON** contract authored or + updated under `docs/api/` before or with the change; implementation + conforms to the contract. Consumed SAP released APIs keep SAP's published + contracts — `docs/api/` holds only surfaces this package owns. + +## MCP routing (this package's loadout — deliberately slim, matched to ADR-002) + +| Working on | Server | +|---|---| +| ABAP Cloud / RAP / CDS statement syntax & semantics | `abap-docs` | +| RAP guides, extensibility docs, SAP Help / Community | `sap-docs` | +| Is this SAP object released for ABAP Cloud? Successors? | `sap-released-objects` | +| Fiori Elements, `@UI` annotations, manifest | `fiori-mcp-server` | +| Writing objects INTO the development tenant (create/activate/transport/ATC/unit tests) | `adt-mcp` — SAP ADT MCP Server from `sapse.adt-vscode`, `http://127.0.0.1:2236/mcp`; bearer token lives in `~/.vscode-server/data/Machine/settings.json` and changes on VS Code reload — refresh `.mcp.json` then | +| UI5 controls/views (only if custom UI proves necessary) | `ui5-mcp-server` | + +When MCP guidance conflicts with training knowledge, follow MCP. + +Live system checks from this box (verified 2026-09-15): the `sap-api` CLI +(`/usr/local/bin/sap-api`) reaches the **development** tenant `my431353` by +default (credentials in `~/projects/sap-integrations/_shared/.env`; the +productive credential is `.env.prod-my429480.bak`) and Arena — API catalog search, +EDMX fetch, OpenAPI generation (`sap-api apis generateOpenapi --service …`), +communication arrangements (`comm`), user/role admin (`users`), and read paths +for the released business APIs and Arena items. Read commands are fair game +for design and acceptance; mutating commands are delegated like any other +write. + +## Delegation (project agents in `.claude/agents/`) + +Implementation goes to the specialists below; their tool restrictions are the +role contract (the reviewer cannot write, the test agent runs on the low-cost +model). All inherit the ADR-003 hooks automatically. Design record and +verified sources: `docs/agent-roster.md`. + +| Work | Agent | +|---|---| +| On-stack ABAP Cloud: RAP BO, CDS, behavior, IAM artifacts | `rap-developer` | +| Fiori Elements app, `@UI` annotations, manifest, launchpad | `fiori-developer` | +| TS connectivity worker: Arena API, Cloud SDK OData writes | `worker-developer` | +| Read-only SAP review: clean core, contracts, tombstones | `sap-code-reviewer` | +| Tests for worker + service layers | `test-engineer` | + +## Repo state & commands (verified 2026-09-14) + +Implementation so far: the TypeScript connectivity worker in `worker/` +(Fastify surface + Arena session core; sync executors are stubs until V-2). +The on-stack RAP track has no code yet — gated on V-1. The Fiori Elements app +(`app/`, TypeScript) is scaffolded against a **mock** OData V4 service: +`npm run start-mock` serves it on port 8082; its service identity is +provisional until reconciled with the live RAP UI binding at V-2. + +In `worker/`: `npm run build` (tsc), `npm run lint` (eslint), `npm test` +(vitest), `npm run dev` (tsx watch). The worker's HTTP surface is governed by +`docs/api/worker.openapi.json` (rule 7) — change the contract first. + +## Files + +| File | Purpose | +|---|---| +| `PRD.md` | Living requirements, target architecture, contracts, verification items | +| `CHANGELOG.md` | Append-only decision log | +| `KNOWLEDGE.md` | Dated tombstones & landmines — the only knowledge carried from the predecessor; also check it when anything unexpected appears in the repo | +| `docs/adr/` | Architecture decision records (ADR-001 amended by ADR-002) | +| `docs/agent-roster.md` | Specialist-agent design + verified sources | +| `docs/predecessor-flow.md` | How the Express predecessor actually works (extracted 2026-09-10) — read before designing executor, poller, or approval behavior | +| `.claude/hooks/` | ADR-003 enforcement scripts (grounding injections, consult gate, consult marker); `tests/run.sh` pipe-tests them — run it after any hook change | diff --git a/KNOWLEDGE.md b/KNOWLEDGE.md new file mode 100644 index 0000000..0751837 --- /dev/null +++ b/KNOWLEDGE.md @@ -0,0 +1,187 @@ +# KNOWLEDGE — tombstones & landmines + +Carried across the 2026-09-04 knowledge bankruptcy from the working Express +predecessor. Filter applied: an item was kept only if it (a) cannot be +re-derived from code or a live query, AND (b) is expensive or harmful to +re-learn. Everything else was deliberately left behind to be re-derived from +ground truth. Every entry is dated; re-verify on first POC contact. Add new +entries the same way — dated and falsifiable. + +## SAP S/4HANA (tenant `my429480`, Public Cloud, API-only) + +- [2026-09-04, verified by predecessor in live use] **BOM items must be POSTed + sequentially** after the header POST. Parallel item POSTs fail SAP's + consistency check. +- [2026-09-04, same] **`AvailabilityCheckType` is not exposed in the Product + API V4** — requires the V2 `API_PRODUCT_SRV` fallback for that one field. +- [2026-09-04, same] **BOM read works only on the v1 service**; + `API_BILL_OF_MATERIAL_SRV;v=0002` rejects guid key lookups. Writes go to + `;v=0002`. +- [2026-09-04, same] OData V2 date format is `/Date(ms)/`; the "end of time" + value (9999-12-31) is `/Date(253402214400000)/`. +- [2026-09-04, same] **Idempotency pattern that works: SAP is the ledger.** The + Arena ECO number is stamped into `ReasonForChangeText`; no local sync-state + database is needed or wanted. +- [2026-09-10, predecessor code read] **BOM header create must use + `EngineeringChangeDocForEdit`** — `EngineeringChangeDocument` is + `sap:creatable=false` and silently dropped. ECM BOM items must carry + `HeaderChangeDocument` = the header's change number or SAP answers "Input + parameters are initial". (arena-plm.js:785-846) +- [2026-09-10, same] **`A_ChangeMasterObjTypeAssgmt` POST requires + `IsActiveForChangeNumber: true`** — without it SAP returns 201 and silently + discards the row. (arena-plm.js:914-924) +- [2026-09-10, same] **ProductDescription updates: PATCH by full key** + (`Product=…,Language='EN'`); PATCH via the nav property returns HTTP 501; + fall back to POST on 404/405. (arena-plm.js:1411-1414) +- [2026-09-10, same] `ProcurementType` can only be written together with + `MRPType` in the same `ProductPlantSupplyPlanning` PATCH. + (arena-plm.js:1623-1640) + +## Arena PLM API (`api.arenasolutions.com/v1`) + +- [2026-09-04, same] Session auth: `POST /login` → `arenaSessionId`, ~25-minute + TTL. Concurrent logins race — serialize login behind a promise lock and + re-login automatically on 401. +- [2026-09-10, predecessor code read] **WORKING-revision overlay is + load-bearing**: the seven SAP classification attributes (SAP Product Type, + SAP Plant, …) are writable only on the WORKING revision (Arena error 3224), + while `/items?number=` resolves the EFFECTIVE revision — merge those + attributes from WORKING over EFFECTIVE, and treat overlay failure as fatal + (the predecessor's best-effort overlay silently turned valid items into + NO_PRODUCT_TYPE errors). (arena-plm.js:508-552) +- [2026-09-10, same] `/items/{guid}/changes` does not exist (404) — derive the + ECO from `/items/{guid}/revisions`, first EFFECTIVE revision with + `change.number`. (arena-plm.js:1799-1806) +- [2026-09-10, same] The `/changes` feed has **no working server-side date + filter** — delta detection must filter client-side (the predecessor pages + the whole feed, O(history)). The outbound-integration queue + (`/outboundintegrations/{guid}/events`, `reconciled` flag as a cursor held + by Arena) is the clean delta mechanism. (arena-plm.js:1117-1263) + +## Toolchain (worker) + +- [2026-09-10, npm registry] **TypeScript `latest` is the 7.x native-compiler + major** (no stable 6.x; only `6.0.0-beta`). typescript-eslint 8.70.0 + declares peer `typescript >=4.8.4 <6.1.0`, silently forcing the 5.9.x line — + the worker pins `~5.9.3`. Falsifiable: recheck + `npm view typescript-eslint peerDependencies` before any TS bump. +- [2026-09-10, npm registry] **`@types/node` `latest` dist-tag points at + 22.x** while 24.x/26.x lines exist — an unpinned install silently targets + Node 22 types. Pin the major (`^24`) to match the runtime. + +## Landmines + +- [2026-09-15] **abapGit is not a prerequisite for SAP's ADT for VS Code.** The + `sapse.adt-vscode` extension talks ADT/HTTP to the tenant and ships no git or + abapGit commands (extension 1.1.2 `package.json`/readme, verified 2026-09-15). + The "abapGit must be set up in the ABAP system" idea comes from the community + extension `murbani.vscode-abap-remote-fs` (installed on this host), whose + abapGit commands need the open-source `ZABAPGIT` report in the backend — which + cannot be installed in a Public Cloud tenant. In ABAP Cloud, abapGit exists + only as SAP's supported fork behind the **Eclipse** ADT plug-in + (`https://eclipse.abapgit.org/updatesite/`, "abapGit Repositories" view); no + VS Code variant exists [source: sap-docs sap-help loio + 2002380aeda84875a5fae4adc66b3fdb; /btp-cloud-platform/30-development/ + import-content-from-abapgit-repository-into-the-abap-environment-0b0d894; + https://github.com/abapGit/ADT_Frontend, verified 2026-09-15]. It imports + released object types only, from an internet-reachable repository (no Cloud + Connector), into a chosen package/transport. + +- [2026-09-15] **ADT MCP `x-ui-service` generator quirks (my431353, ADT VS + Code 1.1.2):** every field needs `semanticType` (else "Builtin type of + field is invalid"); it prepends its own `Z` to your prefix and appends + `000` even with an empty suffix (`Z` + `ARENA_CHANGE_APPROVAL` → + `ZZUI_ARENA_CHANGE_APPRVL000_O4`), names SRVD and SRVB identically, and + the schema's `required` lists a non-existent `businessEntity` key — omit + it. `get_schema` session ids are single-use. It returns only the SRVB URI; + other object URIs follow `abap:/repotree-v1//System Library/ZLOCAL//…` + with extensions `.tabl.json`, `.ddls.acds`, `.bdef.json`, `.ddlx.acds`, + `.dcls.acds`, `.clas.abap`, `.srvd.acds`, `.srvb.json`, `.ront.json`, + `.nont.json`. `create_object` for SRVD needs `"sourceType":"S"`; BDEF and + SRVB creation validate the referenced object's ACTIVE version, so create + and activate DDLS/SRVD sources first. ATC via MCP rejects RONT/NONT. + +- [2026-09-15] **Developer business catalogs: `SAP_A4C_BC_*` is the Public + Cloud / BTP ABAP family; `SAP_A4_BC_*` is on-premise / Private Cloud.** On + `my431353` the developer role carries the A4C catalogs (e.g. + `SAP_A4C_BC_DEV_OBJ_DIS_PC`, `SAP_A4C_BC_DEV_UID_PC` [source: sap-docs + sap-help loio f4881a9b9141430dba1c6a76e7f456dc, verified 2026-09-15]); the + `SAP_A4_BC_DEV_UID_PC` name in the fiori-tools-samples README is the wrong + family for this tenant. Split stated by the tenant owner 2026-09-15; no SAP + Help page in the loadout spells it out — do not "fix" a role toward A4. +- [2026-09-15] **Write probes on the dev tenant: `$TMP` and `ZLOCAL` both + fail, for different reasons.** `$TMP` is a classic package, so creating + there checks the Standard ABAP language version and a cloud-development + role is denied with "not authorized to make changes (S_ABPLNGVS)"; `ZLOCAL` + is a structure package ("cannot contain development objects"). Objects go + in a sub-package of `ZLOCAL` (prototyping, no workbench request for + sub-packages created from release 2508) or of `ZCUSTOM_DEVELOPMENT` + (transport layer YDEV, importable into test/prod); ZLOCAL→ZCUSTOM + reassignment is allowed, the reverse is not; `ZLOCAL` plays the `$TMP` role + in Public Cloud [source: sap-docs sap-help loio + a0d80f14fcb74cb1ac8fb6f3d9ad48cc (2608), verified 2026-09-15]. Refusals + verified live via ADT MCP, 2026-09-15. + +- [2026-09-14] `sap-released-objects` has no `DDLS` type for public_cloud; + CDS views are catalogued as `CDS_STOB`. A "not found" for `I_PRODUCT` under + `DDLS` is a type-key artefact, not a release verdict — query with + `object_type=CDS_STOB` (verified 2026-09-14). +- [2026-09-14] **A Fiori Elements app generated on hand-written mock metadata + agrees with itself and hides service-shape mismatches.** ABAP Cloud OData V4 + services emit `Schema Namespace="com.sap.gateway...v0001" + Alias="SAP__self"` — the name in the namespace is the **service binding + (group)**, which is also the first URL segment; the service definition / + alias is the second segment (`/api_currencyexchangerate/srvd_a2x/sap/currencyexchangerate/0001` + → `…srvd_a2x.api_currencyexchangerate.v0001`; `/api_product/srvd_a2x/sap/product/0002` + [source: sap-docs sap-help loio 5f45f125dfe84c20beb18f5de1512f5a, verified + 2026-09-14]). Shown for released A2X services; for custom UI (`srvd`) + bindings it is strongly indicated, not proven — V-2 confirms. Also, entity types suffixed `_Type`, un-suffixed entity sets, + and vocabulary aliases `SAP__common`/`SAP__capabilities`/`SAP__core` + [source: live tenant my429480 EDMX `~/projects/sap-apps/exchange-rates-cap/srv/external/API_CURRENCYEXCHANGERATE.edmx` + lines 13–31, 55, 57, 95, verified 2026-09-14]. Front-end annotation targets + written against any other namespace silently match nothing at runtime (no + columns, no filters, no actions). Reconcile every `Annotations Target`, the + `edmx:Reference` include namespace, and every `DataFieldForAction.Action` + against the live `$metadata` before the first deploy — never trust the mock. + +- [2026-09-04] The predecessor's `config/procurement-type.json`, + `catalog-to-planning-strategy.json`, and `arena-category-to-sap.json` were + loaded but **never read by any code path**, and procurement-type's keys + (`MTS`/`OTS`) and values (`E`) do not match the Arena field (`1`–`9` Item + Type) or SAP values (`X`) the code actually used. Do not port these configs + on trust — re-derive every mapping via POC. +- [2026-09-04] Deployment/exposure target is an OPEN decision (PRD OQ-3). The + old estate fronts apps through a shared `mcl-apps` approuter with shared + XSUAA; a CF route mapped to more than one app round-robins and causes ~50% + intermittent 404s. Whatever is chosen, audit routes after every push. +- [2026-09-04] HANA Cloud compute is the dominant cost lever of the old estate; + an auto-stop systemd timer (`sap-apps/ops/hana-autostop`) guards it. If this + package binds HANA, account for the stop/start cycle in the design. +- [2026-09-04] The predecessor kept its approval queue in a SQLite file inside + the CF droplet — wiped on every restart. Durable persistence is a hard + requirement of the rebuild (PRD ARN-003), not an optimization. +- [2026-09-10] Predecessor BOM and DOCUMENTS syncs are **non-idempotent** + (re-approval creates a second MaterialBOM header / duplicate attachments) — + the rebuild's executors must check existence first (ARN-006). +- [2026-09-10] The predecessor uses **two disagreeing change-master lookup + keys** (`startswith(ChangeNumberDescription,'ECO:')` in the product link vs + `ReasonForChangeText eq 'ARENA '` everywhere else; the 40-char + truncation can split them). The rebuild uses exactly one key. +- [2026-09-10] Document sync links only Arena files with + `storageMethodName === 'WEB'` (URL links, no bytes) — blob-stored files are + silently skipped, and the DOKAR mapping config existed but was ignored + (`BUS1001006`/`text/url` hardcoded). Decide blob handling explicitly in the + rebuild. +- [2026-09-04] **If `.github/agents/sap-odata-*.agent.md`, a root `AGENTS.md` + about "SAP OData Workspace", or `.vscode/mcp.json` pointing at + `sap-odata-mcp.exe` appear in this repo — delete them.** They are scaffolded + (evidence points to the `harieprasad.sap-odata-mcp` VS Code extension writing + its workspace kit on folder open; observed 2026-09-04 14:40, removed same + day). The referenced MCP binary is a Windows .exe that cannot run on this + host, so those agents fabricate SAP results — the documented failure the + founding kit exists to prevent. The root `AGENTS.md` is the dangerous part: + Claude Code reads it as project instructions. [Update 2026-09-04 15:32] The + suspect extension was uninstalled from all machines (verified: deregistered + and marked obsolete on this host; directory purges on next VS Code reload). + If the bundle STILL reappears after this, the extension hypothesis was wrong + — hunt the real writer instead of re-deleting silently. diff --git a/PRD.md b/PRD.md new file mode 100644 index 0000000..19339f6 --- /dev/null +++ b/PRD.md @@ -0,0 +1,133 @@ +# PRD — arena-plm (standalone rebuild) + +## Vision & scope + +I am rebuilding the Arena PLM → SAP S/4HANA integration as a **proper SAP +application that lives in the tenant**. The predecessor (Express.js, +`~/projects/sap-apps/arena-plm`) works as a demo but fails the test that +matters: a business user inside S/4HANA cannot find it — not in the Fiori +launchpad, not in the Custom CDS Views app — because everything was built +side-by-side behind standalone URLs with ad-hoc access control. + +The acceptance test I hold this rebuild to: **a business user finds the +application in the S/4HANA Fiori launchpad, finds its data in the Custom CDS +Views app, and access is governed by business roles** — with the side-by-side +footprint reduced to the one thing that cannot run in the tenant: Arena +connectivity. + +In scope: material, BOM, change-master, and document sync from Arena to +S/4HANA, gated by a human approval workflow. Out of scope: every other package +of the old estate — one package at a time. + +## Target architecture (ADR-002, 2026-09-04) + +``` +Arena PLM ──webhook + delta poll──▶ Connectivity worker (BTP CF, hardened TS) + │ no UI, no user URLs; technical user + reads approved / │ writes staged changes + executes sync ▼ ▼ via approval BO service binding +┌─ SAP S/4HANA Cloud Public (my429480) ─────────────────────────────────────┐ +│ Z RAP BO "Arena Change Approval" ← staging + state machine + audit │ +│ Fiori Elements List Report / Object Page ← in the tenant LAUNCHPAD │ +│ IAM app → business catalog → business roles ← the only access control │ +│ Custom CDS views over the BO (join I_Product, …) ← Custom CDS Views app │ +│ Released OData APIs (Product/BOM/ChangeMaster/Attachment) ← sync targets │ +└───────────────────────────────────────────────────────────────────────────┘ +``` + +**Build state (2026-09-14):** the Fiori Elements UI layer is scaffolded as +`com.minicircuits.arenaplm` (List Report / Object Page, TS) in the structural `app/` subfolder +(sibling to `worker/`), running on a **mock** OData V4 service — an interim, generator-iterable +stand-in. The real on-stack RAP BO + service binding (ARN-002, V-2) and +launchpad deployment (ARN-007) remain gated on developer extensibility (V-1). +The predecessor `arena-plm` on CF is verified live (`/api/health` 200) and is +the base of record for the domain/field shape until the RAP service exists. + +## Systems & boundaries + +| System | Role | Boundary | +|---|---|---| +| SAP S/4HANA Cloud Public (`my429480`) | **Application host** (RAP BO, Fiori app, CDS views) and sync target | Developer extensibility (ABAP Cloud via ADT); released APIs only | +| Arena PLM (`api.arenasolutions.com/v1`) | Source of items, BOMs, changes, files | REST; webhook + delta poll, handled by the worker | +| Connectivity worker (BTP CF) | Arena connectivity + initial sync executor | Machine-to-machine only; technical user via communication arrangement | + +## Interface contracts + +Carried from the predecessor, in live use (2026-09-04); re-verify on first POC +touch. The worker keeps calling these on approval, so every KNOWLEDGE.md +tombstone remains valid. + +| API | OData | Path | Notes | +|---|---|---|---| +| Product | V4 | `/sap/opu/odata4/sap/api_product/srvd_a2x/sap/product/0002` | deep insert for plant extensions | +| Product (plant fix) | V2 | `/sap/opu/odata/sap/API_PRODUCT_SRV` | `AvailabilityCheckType` is V2-only | +| BOM (write) | V2 | `/sap/opu/odata/sap/API_BILL_OF_MATERIAL_SRV;v=0002` | header first, then sequential items | +| BOM (read) | V2 | `/sap/opu/odata/sap/API_BILL_OF_MATERIAL_SRV` | v1 only; v2 rejects guid keys | +| Change Master | V2 | `/sap/opu/odata/sap/API_CHANGEMASTER;v=0002` | object-type assignments, material linking | +| Attachment | V2 | `/sap/opu/odata/sap/API_CV_ATTACHMENT_SRV` | function import for URL creation | +| Approval BO — UI binding (Fiori app) | V4 UI | `/sap/opu/odata4/sap/zzui_arena_change_apprvl000_o4/srvd/sap/zzui_arena_change_apprvl000_o4/0001/` (binding `ZZUI_ARENA_CHANGE_APPRVL000_O4`, generated 2026-09-15, unpublished) | expected namespace `com.sap.gateway.srvd.zzui_arena_change_apprvl000_o4.v0001`, alias `SAP__self`, entity `ChangeApproval_Type`/set `ChangeApproval`; confirm from live `$metadata` after publish (V-2); Fiori app still carries the pre-generation provisional name | +| Approval BO — Web API (A2X) binding (worker) | V4 Web API | binding `ZAPI_ARENA_CHANGE_APPROVAL_A2X` on definition `ZAPI_ARENA_CHANGE_APPROVAL` (pending: created after the A2X projection is active) | custom communication scenario, technical user; namespace `com.sap.gateway.srvd_a2x.zapi_arena_change_approval_a2x.v0001` expected; exposes create + read + `markExecuting` / `markSynced(SapChangeNumber)` / `markFailed(ErrorMessage)` (RAP-6) | +| Worker inbound surface | REST (OpenAPI 3.1) | `docs/api/worker.openapi.json` | webhook intake (503 when buffer full), health (`ok`/`degraded`, session `none/active/expired/refreshing`), sync-run admin (strict request schema); contract v0.2.0 (2026-09-14 wave); auth scheme pending OQ-6 (ARN-009) | + +## Requirements + +Capability level — field mappings deferred to the POC per my POC-before-spec rule. + +| ID | Requirement | Status | +|---|---|---| +| ARN-001 | Sync chain PRODUCT → CHANGE_MASTER → BOM → DOCUMENTS in config-driven order, executed by the worker on approval | Planned | +| ARN-002 | Approval domain as an on-stack RAP BO: staging fields, state machine PENDING → APPROVED → EXECUTING → SYNCED \| FAILED, REJECTED terminal, FAILED → retrySync → EXECUTING (amended 2026-09-14: a terminal success state is required so an approved record is never executed twice); approve/reject/retrySync as RAP actions with instance feature control; audit fields (`RequestedBy`/`RequestedOn`/`DecidedBy`/`DecidedOn`); execution failure is a distinct state, never overloaded onto rejection (predecessor defect, `docs/predecessor-flow.md` §2) | Planned | +| ARN-003 | Durable persistence & audit — satisfied structurally by in-tenant RAP persistence (the predecessor lost its queue on every restart) | Planned (by design) | +| ARN-004 | Business mappings maintained in-tenant — evaluate Business Configuration maintenance objects (Custom Business Configurations app) before any file-based config | Planned | +| ARN-005 | Arena ingestion via webhook + delta poll in the connectivity worker | Planned | +| ARN-006 | Idempotent SAP writes, SAP-as-ledger (see `KNOWLEDGE.md`) — predecessor BOM/document syncs were non-idempotent; executors must check existence first | Planned | +| ARN-007 | Access exclusively via the Fiori launchpad with IAM app / business catalog / business roles; no user-facing standalone URLs | Planned | +| ARN-008 | Approval data discoverable via custom CDS views, joinable to released views (the discoverability test); look up join targets with `object_type=CDS_STOB` in `sap-released-objects` (no `DDLS` type in the public_cloud dataset, KNOWLEDGE 2026-09-14) | Planned | +| ARN-009 | API-first: every interface this package owns carries an OpenAPI 3.1 JSON contract under `docs/api/`, authored before or with the change; implementation conforms to the contract | Adopted (2026-09-10) | +| ARN-010 | The approved payload is the executed payload: the executor writes the staged data the approver saw — never re-fetch-and-recompute at execution time (the predecessor discarded the preview, making approval decorative; `docs/predecessor-flow.md` §2) | Adopted (2026-09-10) | + +### RAP-track requirements harvested from the UI (2026-09-14) + +Derived while binding the Fiori app; each becomes a `rap-developer` brief +once V-1 opens the development tenant. + +| ID | Requirement | Source | +|---|---|---| +| RAP-1 | Behavior definition declares `approve`, `reject`, `retrySync` with `( features : instance )` and implements `get_instance_features` so the UI service emits `Core.OperationAvailable`; approve/reject only for PENDING, retrySync only for FAILED. `approve` writes no `SapChangeNumber` (execution does). The CDS emits numeric `StatusCriticality` 2/2/5/3/1/1 for PENDING/APPROVED/EXECUTING/SYNCED/REJECTED/FAILED (5 = Information is V4-only) | [source: abap-docs /abap-docs-standard/ABENBDL_ACTIONS_FC; sap-docs /abap-fiori-showcase/01_general_features#operations-and-actions, verified 2026-09-14] | +| RAP-2 | `reject` takes a mandatory `RejectReason` (char 255) parameter; `approve`/`retrySync` return `$self`; the entity is read-only from the UI (no create/update/delete) | UI contract (annotation.xml, mock metadata) | +| RAP-3 | `Status` carries a text element (`@ObjectModel.text.element`, `@UI.textArrangement: #TEXT_ONLY`) so the UI shows text, not codes | [source: fiori-mcp-server "Text and Text Arrangement", verified 2026-09-14] | +| RAP-4 | Semantic key `ItemNumber` (`@ObjectModel.semanticKey`); `Status` and `RequestedOn` sortable/filterable | UI default variant | +| RAP-5 | Two service bindings on definition `ZUI_ARENA_CHANGE_APPROVAL`: OData V4 **UI** binding `ZUI_ARENA_CHANGE_APPROVAL_O4` for the app and an OData V4 **Web API** binding for the worker; the binding name sets the URL first segment and the namespace; hand back the live `$metadata` of the UI binding to reconcile (V-2) | [source: sap-docs sap-help loio 5717c0a1dafc4335add0207b81c39cef, 5f45f125dfe84c20beb18f5de1512f5a, verified 2026-09-14] | +| RAP-6 | Worker-callable execution-state actions on the A2X binding — `markExecuting`, `markSynced` (sets `SapChangeNumber`), `markFailed` (sets `ErrorMessage`) — with instance feature control so the UI never offers Approve on an executing record | [source: abap-docs /abap-docs-cloud/ABENBDL_ACTIONS_FC, verified 2026-09-14] | +| RAP-7 | Staged payload (`SapPreview`) is `abap.string` — CDS char elements are capped at 1333 characters and truncation would break ARN-010 | [source: abap-docs /abap-docs-cloud/ABENCDS_SELECT_LIST_ENTRY_V2, verified 2026-09-14] | +| RAP-8 | Field `DecidedBy` (not `ApprovedBy`) holds approver or rejecter; semantic key `ItemNumber, ChangeType, ArenaEcoId` | UI contract | + +## Architecture decisions + +- ADR-001 — toolchain-must-engage rebuild; Express predecessor reference-only. **Amended by ADR-002.** +- ADR-002 — on-stack first: RAP + Fiori Elements in the tenant; side-by-side reduced to a connectivity worker; CAP exits the architecture. + +## Verification items & open questions + +| ID | Item | Status | +|---|---|---| +| V-1 | Developer-extensibility access on the development tenant | **Closed 2026-09-15** — `my431353.s4hana.cloud.sap`: business user AZAW holds `ZBR_DEVELOPER` (SAP_BR_DEVELOPER template), `ZBR_ADMINISTRATOR_SW_DEV`, `ZBR_DEVELOPMENT_MANAGER_DIN` and five more (live: `sap-api users getRoles`, verified 2026-09-15). V-1b done 2026-09-15 (destination `0M7DEJI`, MCP lists it). Role correct (A4C catalogs). V-1b **closed 2026-09-15**: package `ZARENA_PLM` under `ZLOCAL`; probe class created and activated via ADT MCP | +| V-2 | Custom communication scenario can expose the approval BO's A2X service to the worker's technical user; and the Fiori app's annotation namespace, `_Type` entity-type suffix and UI-binding URL segment are reconciled from the live `$metadata` of the UI binding (decided 2026-09-14; the mock cannot verify this) | Open | +| V-3 | On-stack Application Jobs vs worker-side polling for the Arena delta | Open — flow findings 2026-09-10: Arena's outbound-integration queue holds the cursor (`reconciled` flag) and is the clean delta mechanism; `/changes`-feed polling needs client-side date filtering (`docs/predecessor-flow.md` §1) | +| V-4 | Can a Claude Code agent outside the VS Code editor author ABAP source through the ADT MCP Server? | **Answered 2026-09-15: no.** `create_object` and the generators take structure, not source (live probe); SAP's editing MCP tool is planned for Public Edition 2702 [source: sap-docs loio 7f716223a88c40e0992777c8d9febccf, verified 2026-09-15]. Build plan: `x-ui-service` generator for the BO skeleton, source edits handed over as files for the VS Code editor until 2702 | +| OQ-5 | Long-term: move the sync executor on-stack (released ABAP APIs) vs keep it in the worker — worker initially | Open | +| OQ-6 | Webhook receiver hosting and auth for the worker on CF | Open | + +## Status + +| Work item | State | +|---|---| +| Package home + governance | Done (2026-09-04) | +| Developer environment | Done (2026-09-04); MCP loadout revised to ABAP/RAP set (done 2026-09-04) | +| Ground-truth enforcement kit (ADR-003 hooks) | Done (2026-09-04); hardened 2026-09-14 — Bash/MultiEdit/filesystem writes gated, grounding-only marker refresh, portable paths, fail-closed, 37 pipe-tests green | +| Specialist agent roster (5 agents, docs/agent-roster.md) | Done (2026-09-04; all 5 files YAML-verified in .claude/agents/) | +| V-1…V-3 verification POC | Planned — V-1 (ADT access on my429480) is the user-side gate for everything on-stack | +| Fiori Elements app (`app/`, mock-bound) | Done for the mock stage (2026-09-14) — gateway service shape (binding-derived identity), six-state domain, polished List Report/Object Page, launchpad inbound `ArenaChangeApproval-manage`; SAP review findings closed. Deploy gated on V-1/V-2 (live `$metadata` reconciliation) | +| RAP BO (approval domain) | In progress — skeleton generated and active in `ZARENA_PLM` (2026-09-15, ATC clean); **waiting on owner: paste `docs/handover/rap/` sources in VS Code per its README**, then I activate/ATC/create the A2X BDEF + binding; IAM app/catalog and publishing are the next brief | +| Connectivity worker | Review wave Done (2026-09-14) — TTL wiring, cursor-after-success, buffer-full = tick failure, health conformance, strict JSON-only validation, bounded buffer, graceful drain, HTTP timeout, fixed-phrase errors; 137 tests green incl. the schema-driven contract-conformance suite. Next: Arena client POC (live), executor leg gated on V-2. **Do not deploy the scaffold**: the ingest buffer has no consumer yet, so a long-running instance would answer 503 after `WEBHOOK_QUEUE_MAX_SIZE` events. Next: Arena client POC (live), executor leg gated on V-2 | +| Worker API contract (`docs/api/worker.openapi.json`) | v0.2.1 Done (2026-09-14) — health enums/`degraded`/`poller`, strict `SyncRunRequest`, webhook 503, 413/415 + `default` on every operation, reserved 502, `DOCUMENTS` token; `x-sap-stateInfo: beta`, provisional webhook schema pending Arena POC | diff --git a/app/.gitignore b/app/.gitignore new file mode 100644 index 0000000..647906e --- /dev/null +++ b/app/.gitignore @@ -0,0 +1,10 @@ +node_modules/ +dist/ +.scp/ +.env +Makefile*.mta +mta_archives +mta-* +resources +archive.zip +.*_mta_build_tmp \ No newline at end of file diff --git a/app/README.md b/app/README.md new file mode 100644 index 0000000..95a4356 --- /dev/null +++ b/app/README.md @@ -0,0 +1,95 @@ +## Application Details +| | +| ------------- | +|**Generation Date and Time**
Mon Sep 14 2026 12:49:13 GMT-0700 (Pacific Daylight Time)| +|**App Generator**
SAP Fiori Application Generator| +|**App Generator Version**
1.32.0| +|**Generation Platform**
Visual Studio Code| +|**Template Used**
List Report Page V4| +|**Service Type**
File| +|**Metadata File**
metadata.xml| +|**Module Name**
app| +|**Application Title**
Arena PLM Engineering Changes Approval Flow| +|**Namespace**
com.minicircuits| +|**UI5 Theme**
sap_horizon| +|**UI5 Version**
1.152.0| +|**Enable TypeScript**
True| +|**Add Eslint configuration**
True, see https://www.npmjs.com/package/@sap-ux/eslint-plugin-fiori-tools#rules for the eslint rules.| +|**Main Entity**
ChangeApprovals| + +## Service binding (provisional) + +The app binds to the RAP **UI** service binding, not the A2X binding the worker uses. +The single source of truth for the service identity is the header comment of +`webapp/localService/mainService/metadata.xml`: + +| Item | Value | +|---|---| +| Service binding (OData V4 - UI) | `ZUI_ARENA_CHANGE_APPROVAL_O4` | +| Service definition | `ZUI_ARENA_CHANGE_APPROVAL` | +| Service URL | `/sap/opu/odata4/sap/zui_arena_change_approval_o4/srvd/sap/zui_arena_change_approval/0001/` | +| Schema namespace / alias | `com.sap.gateway.srvd.zui_arena_change_approval_o4.v0001` / `SAP__self` | +| Entity type / entity set | `ChangeApproval_Type` / `ChangeApproval` | + +The first URL segment and the schema namespace are derived from the service **binding** name, +the segment after `srvd/sap/` from the service **definition** name - strongly indicated by the +tenant's `/sap/opu/odata4/sap/api_currencyexchangerate/srvd_a2x/sap/currencyexchangerate/0001` +(namespace `com.sap.gateway.srvd_a2x.api_currencyexchangerate.v0001`) and SAP Help's +`/sap/opu/odata4/sap/api_product/srvd_a2x/sap/product/0002` +[source: sap-docs sap-help loio 5f45f125dfe84c20beb18f5de1512f5a, verified 2026-09-14]. + +`webapp/manifest.json` (`sap.app.dataSources.mainService.uri`, routing) and +`webapp/annotations/annotation.xml` (`edmx:Reference`, every `Annotations Target`, every +`DataFieldForAction.Action`) must stay in sync with it. JSON cannot carry comments, so the +manifest references this note instead. + +**Provisional - namespace = binding name is strongly indicated, not proven; reconcile namespace, +entity type suffix and URL segments against the live `$metadata` of the RAP UI service binding +at PRD V-2 before deploy.** + +## Status model and actions + +`Status`: `PENDING -> APPROVED -> EXECUTING -> SYNCED | FAILED`; `REJECTED` is terminal; +`FAILED -> retrySync -> EXECUTING`. `approve`/`reject` are enabled only for `PENDING`, +`retrySync` only for `FAILED` (mocked via `IsApprovable` / `IsRetriable`, which stand in for the +`Core.OperationAvailable` RAP emits for `( features : instance )` actions - PRD RAP-1). +Actions are offered on the object page header only; the list report table is display-only +(`selectionMode: "None"`). + +## Launchpad + +`sap.app.crossNavigation.inbounds` declares the intent `ArenaChangeApproval-manage`; on +deployment to the ABAP environment the launchpad app descriptor item (target mapping + tile) is +generated from it, and an IAM app / business catalog / business role make it visible +[source: sap-docs /btp-cloud-platform/30-development/sap-fiori-applications-in-the-abap-environment-1d9deef, verified 2026-09-14]. +`sap.fiori.registrationIds` stays empty: it holds SAP Fiori IDs of SAP-delivered apps and is not +required for a custom app (only `archeType` is mandatory) +[source: sap-docs /sapui5/04_Essentials/manifest-descriptor-for-applications-components-and-libraries-be0cf40, verified 2026-09-14]. + +Dev server: `npm run start-mock` / `npm run int-test` bind to port **8082** (8080 is taken on +the dev host). Mock data lives in `webapp/localService/mainService/data/ChangeApproval.json` +(`generateMockData: false`), 8 rows covering every status. + +## app + +An SAP Fiori application. + +### Starting the generated app + +- This app has been generated using the SAP Fiori tools - App Generator, as part of the SAP Fiori tools suite. To launch the generated application, run the following from the generated application root folder: + +``` + npm start +``` + +- It is also possible to run the application using mock data that reflects the OData Service URL supplied during application generation. In order to run the application with Mock Data, run the following from the generated app root folder: + +``` + npm run start-mock +``` + +#### Pre-requisites: + +1. Active NodeJS LTS (Long Term Support) version and associated supported NPM version. (See https://nodejs.org) + + diff --git a/app/eslint.config.mjs b/app/eslint.config.mjs new file mode 100644 index 0000000..9814b15 --- /dev/null +++ b/app/eslint.config.mjs @@ -0,0 +1,5 @@ +import fioriTools from '@sap-ux/eslint-plugin-fiori-tools'; + +export default [ + ...fioriTools.configs.recommended +]; diff --git a/app/package-lock.json b/app/package-lock.json new file mode 100644 index 0000000..b6c3f9c --- /dev/null +++ b/app/package-lock.json @@ -0,0 +1,11164 @@ +{ + "name": "com.minicircuits.arenaplm", + "version": "0.0.1", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "com.minicircuits.arenaplm", + "version": "0.0.1", + "devDependencies": { + "@sap-ux/eslint-plugin-fiori-tools": "^10.0.0", + "@sap-ux/ui5-middleware-fe-mockserver": "2", + "@sap/ux-ui5-tooling": "1", + "@sapui5/types": "~1.152.0", + "@ui5/cli": "^4.0.33", + "eslint": "^10", + "typescript": "^5.9.3", + "ui5-tooling-transpile": "^3.10.0" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", + "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.29.7", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.7.tgz", + "integrity": "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.7.tgz", + "integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.7", + "@babel/helper-compilation-targets": "^7.29.7", + "@babel/helper-module-transforms": "^7.29.7", + "@babel/helpers": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/template": "^7.29.7", + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/core/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/generator": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.8.tgz", + "integrity": "sha512-gZbepsdh3WDtgZKWL+vTPh71LSBrm/Y4/QDZBVCcYfmeTEEuoOYwlSy+G1StfJg+/Zy550u/3TATbm7qDbbMtg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.8", + "@babel/types": "^7.29.8", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-annotate-as-pure": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.29.7.tgz", + "integrity": "sha512-OoK6239jHPuSQOoS0kfTVKn0b/rVTk0seKq4Gd2UMLtmOVLjDC0ki3e+c90Trqv2gMfvJFqkiljrr568+qddiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.29.7.tgz", + "integrity": "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.29.7", + "@babel/helper-validator-option": "^7.29.7", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/helper-create-class-features-plugin": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.29.7.tgz", + "integrity": "sha512-IY3ZD9Tmooqr3TUhc3DUWxiuo8xx1DWLhd5M7hQ+ZWJamqM2BbalrBJb2MisSLoYorOj75U03qULCxQTY9r3hg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.29.7", + "@babel/helper-member-expression-to-functions": "^7.29.7", + "@babel/helper-optimise-call-expression": "^7.29.7", + "@babel/helper-replace-supers": "^7.29.7", + "@babel/helper-skip-transparent-expression-wrappers": "^7.29.7", + "@babel/traverse": "^7.29.7", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-create-class-features-plugin/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/helper-create-regexp-features-plugin": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.29.7.tgz", + "integrity": "sha512-907Uymvqgg1dwUA+7IGwFAOSYzQOuzPXKNJ1yxzwPffzkYFg2q2eHi1fIOs6sXkG9NbIUMunnUlkYsfRFNvomg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.29.7", + "regexpu-core": "^6.3.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-create-regexp-features-plugin/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/helper-define-polyfill-provider": { + "version": "0.6.8", + "resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.6.8.tgz", + "integrity": "sha512-47UwBLPpQi1NoWzLuHNjRoHlYXMwIJoBf7MFou6viC/sIHWYygpvr0B6IAyh5sBdA2nr2LPIRww8lfaUVQINBA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-compilation-targets": "^7.28.6", + "@babel/helper-plugin-utils": "^7.28.6", + "debug": "^4.4.3", + "lodash.debounce": "^4.0.8", + "resolve": "^1.22.11" + }, + "peerDependencies": { + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.29.7.tgz", + "integrity": "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-member-expression-to-functions": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.29.7.tgz", + "integrity": "sha512-j+7JYmk1JYDtACIGj0QJqqWZjoUpMoEikQGADMaHgCMCSDqd2+P32rfcibUNrGOMWrlzK1WJBdxrB3JJQZwWtg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.29.7.tgz", + "integrity": "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.29.7.tgz", + "integrity": "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7", + "@babel/traverse": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-optimise-call-expression": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.29.7.tgz", + "integrity": "sha512-+kmGVjcT9RGYzoDwdwEqEvGgKe3BYq+O1iGzjFubaNgZHwYHP6lsF2Yghf4kEuv9BV7tYDZ913aBW9am6YKong==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.29.7.tgz", + "integrity": "sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-remap-async-to-generator": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.29.7.tgz", + "integrity": "sha512-16AMiW26DbXWBbr3B8wNozKM0ydMLB892vaOaJW/fPJdnT8vJk5sdkQcU/isqUxyCE0cEoa8wZOcbgDuC4b6Og==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.29.7", + "@babel/helper-wrap-function": "^7.29.7", + "@babel/traverse": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-replace-supers": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.29.7.tgz", + "integrity": "sha512-atfGXWSeCiF4DnKZIfmJfQRkSw9b9gNNXR1kqKjbhG4pGYCOnkp8OcTB8E3NXjBu8NpheSnOeNKz8KT7UNFTmQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-member-expression-to-functions": "^7.29.7", + "@babel/helper-optimise-call-expression": "^7.29.7", + "@babel/traverse": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-skip-transparent-expression-wrappers": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.29.7.tgz", + "integrity": "sha512-brcMGQaVzIeUb+6/bs1Av0f8YuNNjKY2JyvfRCsFuFsdKccEQ5Ges2y74D74NZ1Rz8lKJ9ksJkfqwQFJ/iNEyQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", + "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.29.7.tgz", + "integrity": "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-wrap-function": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.29.7.tgz", + "integrity": "sha512-iES0Skag9ERIF68aXadpO6dbXa03mNWK3sEqJaMnLNs/eC3l0lkImdfoy6Y09/SfkpawdAB4RjQ7PVA7TcVGdw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.29.7", + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.7.tgz", + "integrity": "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.8.tgz", + "integrity": "sha512-E8lTAYNB1KW+FH+VGJuZM1ioAx2E6oVlvQFRrf5P8ZZmsiJXYAD9vTFV7yyEURNzgh1dFqMZuO6tUwcARbqFCA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.8" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-bugfix-firefox-class-in-computed-class-key": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-firefox-class-in-computed-class-key/-/plugin-bugfix-firefox-class-in-computed-class-key-7.29.7.tgz", + "integrity": "sha512-j8SrR0zLZrRsC09DlszEx8FpMiwukKffYXMK0d5LmOglO7vGG6sz/BR/20yHqWH+Lnn31JTt2PE3hIWNgM2J6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/traverse": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-bugfix-safari-class-field-initializer-scope": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-class-field-initializer-scope/-/plugin-bugfix-safari-class-field-initializer-scope-7.29.7.tgz", + "integrity": "sha512-r8j8escF+U2FUHo0KOhPUdMzUO+jp9fInva6+ACVAF3Y97Ev+5iNZwiqTghmzNeWwDkOPlYuTcfb1vDaoZKmAQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.29.7.tgz", + "integrity": "sha512-GE1TFSiuFeGsCxmYXZl8HwoPrVlwe4rHPFE8weieGKZqnDORK+Ar3vgWMgW+AOxQ6/2TgLSKx9p6W7O4rC6qgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-bugfix-safari-rest-destructuring-rhs-array": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-rest-destructuring-rhs-array/-/plugin-bugfix-safari-rest-destructuring-rhs-array-7.29.7.tgz", + "integrity": "sha512-oBNVCvnO5tND+xSopWvV8WNGfpTfgP4Zr/YXXSj8zfmcPktp5Ku/aZlsIowgSD4fjmgHn6sGmB9APVsU5zOdhA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/helper-skip-transparent-expression-wrappers": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.29.7.tgz", + "integrity": "sha512-QQt9qKHZ2sg/kivaLr7lnQr8HVrQDdBNSfCsTjiDxRuX/K5ORyKq+Bu8Xr0cDE3Dfkv0cw28Ve0EKyKMvulkOw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/helper-skip-transparent-expression-wrappers": "^7.29.7", + "@babel/plugin-transform-optional-chaining": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.13.0" + } + }, + "node_modules/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly/-/plugin-bugfix-v8-static-class-fields-redefine-readonly-7.29.7.tgz", + "integrity": "sha512-pn6QacGLgvCcwc+syUhKE/qSjV2D1IHDB84RNxWYSt1mW3K/SCtjinZ2p0cETJxAWBjPy3K/1lHwG5BjjPxNlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/traverse": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-proposal-private-property-in-object": { + "version": "7.21.0-placeholder-for-preset-env.2", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.21.0-placeholder-for-preset-env.2.tgz", + "integrity": "sha512-SOSkfJDddaM7mak6cPEpswyTRnuRltl429hMraQEglW+OkovnCzsiszTmsrlY//qLFjCpQDFRvjdm2wA5pPm9w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-assertions": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.29.7.tgz", + "integrity": "sha512-/An1OCBN93thpBAGyfsK2pcf0jvju1SAtKkL2Ny++B5Sy6sqgzXDQH1cZxWbF96Wuk+bn41MDA9bLd4VVAw6rw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-attributes": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.29.7.tgz", + "integrity": "sha512-zGYcYfq/WmZ4V+kBIXQon9dSSc8ircGZqw9ZaNhhGj9nZkeBu1jHLBDQqYYi5WA9uawvA2sIMbry2nCFhf5Djg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-jsx": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.29.7.tgz", + "integrity": "sha512-TSu8+mHCoEaaCDEZ0I3+6mvTBYR4PCxQwf2z9/r5Tbztv6NaLR3B9thGTTxX2WGuGHJqRiAbKPeGTJ5XWXVg6A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-typescript": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.29.7.tgz", + "integrity": "sha512-ngr+82Sh0xMz25TPCZi+nC2iTzjfCdWS2ONXTp/PtSCHCgaCNBpdMqgvJ2ccdLlClVZ7sisIgB914j/JFe+RZA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-unicode-sets-regex": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-unicode-sets-regex/-/plugin-syntax-unicode-sets-regex-7.18.6.tgz", + "integrity": "sha512-727YkEAPwSIQTv5im8QHz3upqp92JTWhidIC81Tdx4VJYIte/VndKf1qKrfnnhPLiPghStWfvC/iFaMCQu7Nqg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.18.6", + "@babel/helper-plugin-utils": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-transform-arrow-functions": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.29.7.tgz", + "integrity": "sha512-N7zArUXWzAMzm+/N0uPBeVB3Fam5lMxtUwMmDK5f/IBBS7a7p1qeUoxd/6CckXoxUdgsntq1Dh8xNW06maZbDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-async-generator-functions": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.29.7.tgz", + "integrity": "sha512-d98gXZkgswvkyohMBABkhm3GeXhYj8psWfwQ2C7gtfrKGTykQa/iOIi+JJhwMjPlZ6Vm2XN+DCf3Es1EoG4ZLA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/helper-remap-async-to-generator": "^7.29.7", + "@babel/traverse": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-async-to-generator": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.29.7.tgz", + "integrity": "sha512-pcUb2SS+RMo9TWVBwKGI5ShtoG7R+zBsFmCKDa6fe8c+hPr3XJlZgoE5j6i8W7gDjhyvy+85vmYexanvXh3d1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/helper-remap-async-to-generator": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-block-scoped-functions": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.29.7.tgz", + "integrity": "sha512-cUSmjh72N+rN4PrkFlN1dJwNCwjVp5d38/CQrEsFggkD10UiFlBFgdH3tv5dNsLuHY+3S8db2xCHjhZcv5WgvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-block-scoping": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.29.7.tgz", + "integrity": "sha512-ONyr4+AZhKh8yKWInVxU9AXA9EbsyeLcL6V0dJy6M2/62vuvpGm29zzuymbTpdc451GEpDIdAyPLP3r+P61yKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-class-properties": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-properties/-/plugin-transform-class-properties-7.29.7.tgz", + "integrity": "sha512-GtcpjFvanPfzNQi3eTitsCqtRRmmqzpy/A+yhTR1HaZo1Ly3EA8ZXxlPyHdR8/IuRMYc3E4wdGBewB2QKQjAaA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-class-static-block": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.29.7.tgz", + "integrity": "sha512-kibJgmEdX2iMwsHY2tSZNDgj8PwIlCQz7FK9KuGKO8zsuoUwSEhoNnNVp/emKWrbY4HeO6kkXfdMqRKKKXBm2A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.12.0" + } + }, + "node_modules/@babel/plugin-transform-classes": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.29.7.tgz", + "integrity": "sha512-qV0OGGBVacduzQHE649JyCneOFI/maT+YKsO+K4Yi3xv2wTPNjM/W2o2gdzMwEAZz7fXNTHAe0NcSg30bIN69g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.29.7", + "@babel/helper-compilation-targets": "^7.29.7", + "@babel/helper-globals": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/helper-replace-supers": "^7.29.7", + "@babel/traverse": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-computed-properties": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.29.7.tgz", + "integrity": "sha512-RK7/IyU5phpuCdBAuig5VkzG/EnbDaui5SQGdU9BFrHdV+mV4cUjLMQ9lJDjLNtWHsqtiefpGZUXQP2BiTYMsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/template": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-destructuring": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.29.7.tgz", + "integrity": "sha512-iPX8aD6H9zV5s7ZsqTdNocPN/MGQ5sSMnElKrktxjJRMnB2jN/1p2+R7GkfD6CAYoVFqy5A4XnSIUeGgJzIWpg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/traverse": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-dotall-regex": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.29.7.tgz", + "integrity": "sha512-3qc18hsD2RdZiyJNDNc7HQpv6xbncwh8FYtxNFFzclSyh/trPD9KkVR9BDECUjDLvb7yJVF15GfYUuC+LMkkiQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-duplicate-keys": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.29.7.tgz", + "integrity": "sha512-6IvRRriEMqnBwD6chtxdLpMYCHWEzN+oL5cyQtjykya19UgzbmKhxmhZgKC/LHxS2nYr9Q/qYPZ5Lr6jOL9+yQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-duplicate-named-capturing-groups-regex": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-named-capturing-groups-regex/-/plugin-transform-duplicate-named-capturing-groups-regex-7.29.7.tgz", + "integrity": "sha512-2wiIyo2BjtgU7HufSeDnL9L2O7zr8jmhFKuSr65VpRkUiRKRNpb0mdlk56+XPPKoIrfHqzbMuglDvZun0RISsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-transform-dynamic-import": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dynamic-import/-/plugin-transform-dynamic-import-7.29.7.tgz", + "integrity": "sha512-giOlEm/EFjfjr+te9NsdjkUo2v4f8rS/SXPumRVHAtbNcyNlvtREkU1dZzaIDclNpnaVhlCqRdFKhJBjBikzLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-explicit-resource-management": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-explicit-resource-management/-/plugin-transform-explicit-resource-management-7.29.7.tgz", + "integrity": "sha512-Rstj7coNz8sE+7Ju7ihpHLI564lsK5pUpNNlvptCIC/16E/S5hbl6n3kESPKdNRmqEWlpn5xpS5Q2dvXBsySLw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/plugin-transform-destructuring": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-exponentiation-operator": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.29.7.tgz", + "integrity": "sha512-zFpMOTLZBdW5LfObqcSbL6kefg4R4eLdmvS0wbN9M6D5Mym/sKm9toOoWyVOa+xDjvCnuWcHls2YonXwHvH3CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-export-namespace-from": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-export-namespace-from/-/plugin-transform-export-namespace-from-7.29.7.tgz", + "integrity": "sha512-24B2nOy2TeJSMheqwPD4DDQOV/elLSIlKxjZt4i05H5AgdPdWR3n18HnNrcJ+j76WJd9gbwb9jPjNYUy6RautA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-for-of": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.29.7.tgz", + "integrity": "sha512-zeSIHh0+E1Um1WJRXCFlHQYu2ieJNdivLLjlBEp+dIBu3S51n+SZZmIXjxnItw6pz56Cn+KvK68BIBVsxq2JiQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/helper-skip-transparent-expression-wrappers": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-function-name": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.29.7.tgz", + "integrity": "sha512-otRWaHXE6fbAGkePvaj/kvs3HsqXfPhlnzwSOlnFgbqCPMd975dW+4wZ00WFBt+/YlBGcJwNrARQTOJOb4ZrIg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-compilation-targets": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/traverse": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-json-strings": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-json-strings/-/plugin-transform-json-strings-7.29.7.tgz", + "integrity": "sha512-RRnE2+eon1rJAq8MnoF1b5kTpY1vU88twHcvcKMrsqP/jxIRqDVs9iJB5fqPuqyeFAW0wJo4MlUIPpQCq/aRsg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-literals": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-literals/-/plugin-transform-literals-7.29.7.tgz", + "integrity": "sha512-DZ/oLP21ZuWx1vKqnoNv6/tvEK48AQOBRai40CX9dTjGluvT/YZCyY3rryDtyUqCEoyNroy5KKPwX2iQCiRvyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-logical-assignment-operators": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-logical-assignment-operators/-/plugin-transform-logical-assignment-operators-7.29.7.tgz", + "integrity": "sha512-A0H91hh6W8MFRkp5TqJmMr39jzGD1A1E1Ysiv2O06Sfbhkapm+XyIzxWCEh5kqwOZ1/8QZ0dY3SeQ7XBqfJd5Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-member-expression-literals": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.29.7.tgz", + "integrity": "sha512-hl1kwFZCCiDyfH25Xmco9jTrkPgnS9pmOzSG7W5I4SaGbLeqKv417hcU2RKmaxoPEgsoJh7ZPOrnPGq99bHoUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-amd": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.29.7.tgz", + "integrity": "sha512-fxtQoH3m5ywUSIfaH0FGCzWu4McsYon5bD3K4XnskC7f+OyQMj7rsOMi4NvvmJ83WwBAg4UCe+ov4VZlqEvyew==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-transforms": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-commonjs": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.29.7.tgz", + "integrity": "sha512-j0vCldybPC5b5dwCQOJ21uKtHzt7hxLygJTg9eF1ScfaikEDNfzn94XoW5Fi+seBR0nCyL23xaBFFkq7dTM8XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-transforms": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-systemjs": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.29.8.tgz", + "integrity": "sha512-6iSnEK0zlkLKU4heofK/AdmRD4e2SHVpJMtrwnTCzhnaM98ria4rTrOXBBi45BTTYnJtO8txnPsX4fChYXkmeA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-transforms": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7", + "@babel/traverse": "^7.29.8" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-umd": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.29.7.tgz", + "integrity": "sha512-B4UkaTK3QpgCwJnrxKfMPKdo92CN7OKXAlpAAnM3UPu0Q0lCCk57ylA9AJbRy2v8dDKOPAAWcoR6CMyeoHwRCA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-transforms": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-named-capturing-groups-regex": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.29.7.tgz", + "integrity": "sha512-vuFoLwr4qnv2xbZ16SQd6uPcH5FNrLHhk/Jzo++0XJFcaDsr4gjJVg6j398oMHiC+83k/GiBzviwF5KBJkPUtQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-transform-new-target": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.29.7.tgz", + "integrity": "sha512-fEo41GmsOUhOBlw8ioo6zvjX5Xc2Lqkzlyfqbpsk3eB6TReV18uhxZ0esfEokVbY2+PVJAQHNKxER6lGrzNd3A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-nullish-coalescing-operator": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.29.7.tgz", + "integrity": "sha512-idmp1dFaekP9GbcMvG24Kvw2BfhFZjHnNJCkV4WuIY4PskJzwI3f1N5OdgYke38T7rftO6ERulFRn2cFeZwRkg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-numeric-separator": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-numeric-separator/-/plugin-transform-numeric-separator-7.29.7.tgz", + "integrity": "sha512-zR7fv/z14OjgHl4AgRtkDBvBMhIzCxqV/qN/2BCRC7LjFwvuzjYe7gDWxC4Wl/SNsLM6SE1IWvRPYMgSJaUvNw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-object-rest-spread": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.29.7.tgz", + "integrity": "sha512-Ld98jn4c0smUywL57m7SgsHq3OpThOa6LqZJif3G6jYOovPleoFhVrBJ1WegRApSFB2wu4+RelAj9AC9G08Z4A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-compilation-targets": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/plugin-transform-destructuring": "^7.29.7", + "@babel/plugin-transform-parameters": "^7.29.7", + "@babel/traverse": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-object-super": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.29.7.tgz", + "integrity": "sha512-Ea/diGcw0twB5IlZPO5sgET6fJsLJqPABqTuFWIR+iMPGPZJkATEIWx0wa+aEQ5UY1CBQyP/gkAiLEqn1vBiQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/helper-replace-supers": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-optional-catch-binding": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-catch-binding/-/plugin-transform-optional-catch-binding-7.29.7.tgz", + "integrity": "sha512-sLsyndxK2VwX6yNUOakMb7Sh553ZTe/vVM1XJ+9Z5aW1ytsc8xOIwmyk05NNjN60vkc5/KqoTH6hB4V41LJhng==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-optional-chaining": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.29.7.tgz", + "integrity": "sha512-6GM1dhvK3gNODkXcEcMCOLEDCLSoZ/sBbro2Ax8HURyasQ4NshagQixkRFdh5niI6E4gmA/jYI/4aT7rRos3ZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/helper-skip-transparent-expression-wrappers": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-parameters": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.29.7.tgz", + "integrity": "sha512-ZDOBqV/qLYJI0YElr8DcENEyARsFQeESqWXH6gZlghYXuPPjvweuDhP4VyEi4BlUBlLRFZVjxoZDMjxhLW766g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-private-methods": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-methods/-/plugin-transform-private-methods-7.29.7.tgz", + "integrity": "sha512-/6Rz4DK1ETDEM/bWHsPHcaEe7ZaT1EqSXjtSP/L0DijOYuaUhiRiOKcwpZ8P7zR4xXEHc2ITdiCgBm9Tpyv9ug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-private-property-in-object": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-property-in-object/-/plugin-transform-private-property-in-object-7.29.7.tgz", + "integrity": "sha512-+BNo06dnrzdNNqCm1X6YUaVv0DKk8Q+JYcoZfOkLhYWNCXzlwTSRq8zGWayT1csjcpNXV9CQTBRRbmTLZac5cA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.29.7", + "@babel/helper-create-class-features-plugin": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-property-literals": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.29.7.tgz", + "integrity": "sha512-bOMRLQuI0A5ZqHq3OWJ89/rXpJ/NJrbVhXiP4zwPGMs6kpcVsuTUNjwoE30K0Qm3mf48a/TnRYYD6vPNqcg6jA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-regenerator": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.29.8.tgz", + "integrity": "sha512-0UpIXPtdDtMXfnV2OJAVMLpj3H/92vmkA6lpSRakmycJvj3VUy6Xs1dM8tXRugupykr5WB+LpiVl0J8LMVg2mg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-regexp-modifiers": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regexp-modifiers/-/plugin-transform-regexp-modifiers-7.29.7.tgz", + "integrity": "sha512-mB5Fs0VWrJ42ZCmc8114v60qetdaUVNkj9PmSZRmanCZM3S9hm0CFRLjRmYIsuXav14l2jvZ+4T8iiCGnhj3nQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-transform-reserved-words": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.29.7.tgz", + "integrity": "sha512-5+YhdpVgmfSmwZyLMftfaiffLRMHjzIRHFHHLdibcSyJm2pasMrKHrO3Ptrt2DRshjvpgjEJJ1zVW14WPq/6QA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-shorthand-properties": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.29.7.tgz", + "integrity": "sha512-I+WYbGBAiCn7nA6xBrlgPH+MB7HWb4u8pv5S0Pv7OtwNvIFvCCb24YlttKEeUFVurfBCEaOTnuhlqsb7f0Z5Dg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-spread": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.29.8.tgz", + "integrity": "sha512-4S9ksMGVWUshvgK0mKfvZky7leuG5/uoFVwMpAomJ8bMoDJiNHRVmc1EglwW/CmGVSqqWpEbXm9FmbRit22qoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/helper-skip-transparent-expression-wrappers": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-sticky-regex": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.29.7.tgz", + "integrity": "sha512-BCHzNYJGe9l7EpwwDBN/ztlL2NYFFq8hp9ddjtUEM9f2O7S7kKV/lL6Fwo7IF7NSkYhPK2vO+86nIGltA90MsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-template-literals": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.29.7.tgz", + "integrity": "sha512-NCSEJ4sLFU2gqAub45HYh4fus2yQ36rr6ei6vpU7NdoJqCpxvEG8E6eJpscGyXP3VHD2Ny+fSXr04k1hoUrFqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-typeof-symbol": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.29.7.tgz", + "integrity": "sha512-223mNGoTkBiTEWFoK+Q6Go3tueMRclO8vxxxxquNCYuNI4jWOofFKJRRDu6SDrB8Sgo1UEGW9T4GAQ8ZyRso1A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-typescript": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.29.7.tgz", + "integrity": "sha512-jK52h8LaLc7JarhQV2ofeFMts4H7vnOXnqZNA6fYglBTZewRBE51KWt3BUltW1P+KoPsYkHoJeXePuz4zo2LMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.29.7", + "@babel/helper-create-class-features-plugin": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/helper-skip-transparent-expression-wrappers": "^7.29.7", + "@babel/plugin-syntax-typescript": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-unicode-escapes": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.29.7.tgz", + "integrity": "sha512-jCfXxSjf94lf4E0hKE0AByxF6F3/pVFqRdUUNkDJhsY0m1ZKjnN6ZYyMeHNpzflxb/0q5b7t3p+BE+SLF1WOtA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-unicode-property-regex": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-property-regex/-/plugin-transform-unicode-property-regex-7.29.7.tgz", + "integrity": "sha512-OgZ+zoAJgZLUCunsTRQ5LAjOywDv5zzZ2/hQ5aMw1pGXyY2rtE8/chXYUmu3AlVHKpm10KEdG9aMwbI/K76ZGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-unicode-regex": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.29.7.tgz", + "integrity": "sha512-7D/x/23/d/3VqZ0QA+LGbZMlGwZjztBygSWWWsfTPoQ1oQ6Q1P6Mr3d0kk42XabyUVw+fha3LqdRsFqeKqvCyA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-unicode-sets-regex": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-sets-regex/-/plugin-transform-unicode-sets-regex-7.29.7.tgz", + "integrity": "sha512-BLOhLht9DOJwIxlmp91wHvkXv1lguuHS3/FwUO8HL1H0u8s4hR1gASVFyilu9iGtcTRYqjTZmlsFFeQletntEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/preset-env": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.29.7.tgz", + "integrity": "sha512-GYzX36n1nsciIb0uyH0GHwxwtNwPQIcpxSeiVLDtG/B7jB5xXgchnmL1f/jCX5o+pwnaDBtO60ONSJhEBJfxYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.29.7", + "@babel/helper-compilation-targets": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/helper-validator-option": "^7.29.7", + "@babel/plugin-bugfix-firefox-class-in-computed-class-key": "^7.29.7", + "@babel/plugin-bugfix-safari-class-field-initializer-scope": "^7.29.7", + "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "^7.29.7", + "@babel/plugin-bugfix-safari-rest-destructuring-rhs-array": "^7.29.7", + "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.29.7", + "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": "^7.29.7", + "@babel/plugin-proposal-private-property-in-object": "7.21.0-placeholder-for-preset-env.2", + "@babel/plugin-syntax-import-assertions": "^7.29.7", + "@babel/plugin-syntax-import-attributes": "^7.29.7", + "@babel/plugin-syntax-unicode-sets-regex": "^7.18.6", + "@babel/plugin-transform-arrow-functions": "^7.29.7", + "@babel/plugin-transform-async-generator-functions": "^7.29.7", + "@babel/plugin-transform-async-to-generator": "^7.29.7", + "@babel/plugin-transform-block-scoped-functions": "^7.29.7", + "@babel/plugin-transform-block-scoping": "^7.29.7", + "@babel/plugin-transform-class-properties": "^7.29.7", + "@babel/plugin-transform-class-static-block": "^7.29.7", + "@babel/plugin-transform-classes": "^7.29.7", + "@babel/plugin-transform-computed-properties": "^7.29.7", + "@babel/plugin-transform-destructuring": "^7.29.7", + "@babel/plugin-transform-dotall-regex": "^7.29.7", + "@babel/plugin-transform-duplicate-keys": "^7.29.7", + "@babel/plugin-transform-duplicate-named-capturing-groups-regex": "^7.29.7", + "@babel/plugin-transform-dynamic-import": "^7.29.7", + "@babel/plugin-transform-explicit-resource-management": "^7.29.7", + "@babel/plugin-transform-exponentiation-operator": "^7.29.7", + "@babel/plugin-transform-export-namespace-from": "^7.29.7", + "@babel/plugin-transform-for-of": "^7.29.7", + "@babel/plugin-transform-function-name": "^7.29.7", + "@babel/plugin-transform-json-strings": "^7.29.7", + "@babel/plugin-transform-literals": "^7.29.7", + "@babel/plugin-transform-logical-assignment-operators": "^7.29.7", + "@babel/plugin-transform-member-expression-literals": "^7.29.7", + "@babel/plugin-transform-modules-amd": "^7.29.7", + "@babel/plugin-transform-modules-commonjs": "^7.29.7", + "@babel/plugin-transform-modules-systemjs": "^7.29.7", + "@babel/plugin-transform-modules-umd": "^7.29.7", + "@babel/plugin-transform-named-capturing-groups-regex": "^7.29.7", + "@babel/plugin-transform-new-target": "^7.29.7", + "@babel/plugin-transform-nullish-coalescing-operator": "^7.29.7", + "@babel/plugin-transform-numeric-separator": "^7.29.7", + "@babel/plugin-transform-object-rest-spread": "^7.29.7", + "@babel/plugin-transform-object-super": "^7.29.7", + "@babel/plugin-transform-optional-catch-binding": "^7.29.7", + "@babel/plugin-transform-optional-chaining": "^7.29.7", + "@babel/plugin-transform-parameters": "^7.29.7", + "@babel/plugin-transform-private-methods": "^7.29.7", + "@babel/plugin-transform-private-property-in-object": "^7.29.7", + "@babel/plugin-transform-property-literals": "^7.29.7", + "@babel/plugin-transform-regenerator": "^7.29.7", + "@babel/plugin-transform-regexp-modifiers": "^7.29.7", + "@babel/plugin-transform-reserved-words": "^7.29.7", + "@babel/plugin-transform-shorthand-properties": "^7.29.7", + "@babel/plugin-transform-spread": "^7.29.7", + "@babel/plugin-transform-sticky-regex": "^7.29.7", + "@babel/plugin-transform-template-literals": "^7.29.7", + "@babel/plugin-transform-typeof-symbol": "^7.29.7", + "@babel/plugin-transform-unicode-escapes": "^7.29.7", + "@babel/plugin-transform-unicode-property-regex": "^7.29.7", + "@babel/plugin-transform-unicode-regex": "^7.29.7", + "@babel/plugin-transform-unicode-sets-regex": "^7.29.7", + "@babel/preset-modules": "0.1.6-no-external-plugins", + "babel-plugin-polyfill-corejs2": "^0.4.15", + "babel-plugin-polyfill-corejs3": "^0.14.0", + "babel-plugin-polyfill-regenerator": "^0.6.6", + "core-js-compat": "^3.48.0", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/preset-env/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/preset-modules": { + "version": "0.1.6-no-external-plugins", + "resolved": "https://registry.npmjs.org/@babel/preset-modules/-/preset-modules-0.1.6-no-external-plugins.tgz", + "integrity": "sha512-HrcgcIESLm9aIR842yhJ5RWan/gebQUJ6E/E5+rf0y9o6oj7w0Br+sWuL6kEQ/o/AdfvR1Je9jG18/gnpwjEyA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.0.0", + "@babel/types": "^7.4.4", + "esutils": "^2.0.2" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/@babel/preset-typescript": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/preset-typescript/-/preset-typescript-7.29.7.tgz", + "integrity": "sha512-/Foi8vKY2EVbed/1eZx0gJEEwHAIxogrySI7rULcRIvhZzbvoE/b5qG5Ghc0WKAFKOHA9SD1x7RsFlOYdutIiQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/helper-validator-option": "^7.29.7", + "@babel/plugin-syntax-jsx": "^7.29.7", + "@babel/plugin-transform-modules-commonjs": "^7.29.7", + "@babel/plugin-transform-typescript": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/template": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz", + "integrity": "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.8.tgz", + "integrity": "sha512-I5z7H3bf/41ktsNVLtpN0wAa336HkqIHQ5BuPLEhTkt1jVSyZpeNKIzTgEWmlxjdg81R0IgUCcaE+Ok3NvrfZg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.8", + "@babel/helper-globals": "^7.29.7", + "@babel/parser": "^7.29.8", + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.8", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.8.tgz", + "integrity": "sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@cacheable/memory": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@cacheable/memory/-/memory-2.2.0.tgz", + "integrity": "sha512-CTLKqLItRCEixEAewD3/j9DB3/o96gpTPD4eJ1v+DGOlxZRZncRQkGYqqnAGCscYd6RNeXfGeiuCphsPtqyIfQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@cacheable/utils": "^2.5.0", + "@keyv/bigmap": "^1.3.1", + "hookified": "^1.15.1", + "keyv": "^5.6.0" + } + }, + "node_modules/@cacheable/utils": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@cacheable/utils/-/utils-2.5.0.tgz", + "integrity": "sha512-buipgOVDkkPXNR5+xBpDw7Zk2n1EvU7qBJCNUcL7rhQ//kfpOXPAvQ511Os0vpLYJ1pZnvudNytkQt2hst3wqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "hashery": "^1.5.1", + "keyv": "^5.6.0" + } + }, + "node_modules/@chevrotain/types": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/@chevrotain/types/-/types-9.1.0.tgz", + "integrity": "sha512-3hbCD1CThkv9gnaSIPq0GUXwKni68e0ph6jIHwCvcWiQ4JB2xi8bFxBain0RF04qHUWuDjgnZLj4rLgimuGO+g==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/@chevrotain/utils": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/@chevrotain/utils/-/utils-9.1.0.tgz", + "integrity": "sha512-llLJZ8OAlZrjGlBvamm6Zdo/HmGAcCLq5gx7cSwUX8No+n/8ip+oaC4x33IdZIif8+Rh5dQUIZXmfbSghiOmNQ==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.10.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.10.1.tgz", + "integrity": "sha512-cuadcxVFE8sDK6iWJbs8Sn0av2Nrh2QSGQhVlBW9AaAHqHwjWsZHT8LJ4hFGPh7ASBV2deFdM7H/DPjulmh8rg==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.23.5", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.23.5.tgz", + "integrity": "sha512-Y3kKLvC1dvTOT+oGlqNQ1XLqK6D1HU2YXPc52NmAlJZbMMWDzGYXMiPRJ8TYD39muD/OTjlZmNJ4ib7dvSrMBA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^3.0.5", + "debug": "^4.3.1", + "minimatch": "^10.2.4" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.7.0.tgz", + "integrity": "sha512-DObd/KKUsU+FaFv4PLxSRenpXfQWmPXXP3pPZ6/K1PCrMu2vQpMDMuQe/BqYeoLcz8ro0bVDF1RxOJgfVEdhUw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^1.2.1" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/core": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-1.2.1.tgz", + "integrity": "sha512-MwcE1P+AZ4C6DWlpin/OmOA54mmIZ/+xZuJiQd4SyB29oAJjN30UW9wkKNptW2ctp4cEsvhlLY/CsQ1uoHDloQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/object-schema": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-3.0.5.tgz", + "integrity": "sha512-vqTaUEgxzm+YDSdElad6PiRoX4t8VGDjCtt05zn4nU810UIx/uNEV7/lZJ6KwFThKZOzOxzXy48da+No7HZaMw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.7.3.tgz", + "integrity": "sha512-IkO+/KEUvwbVpiURZg+P7zF74z5Jxe0UgJxVni+RtoHQ6IZieXaO02kmadomap/q+l6bc/jdPGGqTjhuZnuz1Q==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^1.2.1", + "levn": "^0.4.1" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@humanfs/core": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz", + "integrity": "sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/types": "^0.15.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.8", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.8.tgz", + "integrity": "sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.2", + "@humanfs/types": "^0.15.0", + "@humanwhocodes/retry": "^0.4.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/types": { + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/@humanfs/types/-/types-0.15.0.tgz", + "integrity": "sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.6.0.tgz", + "integrity": "sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@keyv/bigmap": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@keyv/bigmap/-/bigmap-1.3.1.tgz", + "integrity": "sha512-WbzE9sdmQtKy8vrNPa9BRnwZh5UF4s1KTmSK0KUVLo3eff5BlQNNWDnFOouNpKfPKDnms9xynJjsMYjMaT/aFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "hashery": "^1.4.0", + "hookified": "^1.15.0" + }, + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "keyv": "^5.6.0" + } + }, + "node_modules/@keyv/serialize": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@keyv/serialize/-/serialize-1.1.1.tgz", + "integrity": "sha512-dXn3FZhPv0US+7dtJsIi2R+c7qWYiReoEh5zUntWCf4oSpMNib8FDhSoed6m3QyZdx5hK7iLFkYk3rNxwt8vTA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@sap-ux/annotation-converter": { + "version": "0.10.22", + "resolved": "https://registry.npmjs.org/@sap-ux/annotation-converter/-/annotation-converter-0.10.22.tgz", + "integrity": "sha512-PiRov5ChBlppZzQSZOD6G+5Aq/rl82IjtnObuRdeGRYrhAelw2aJhEfIqfD4KfVor7imFsd/CTQvvtALiTuH2w==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@sap-ux/vocabularies-types": "0.16.0" + } + }, + "node_modules/@sap-ux/edmx-parser": { + "version": "0.10.0", + "resolved": "https://registry.npmjs.org/@sap-ux/edmx-parser/-/edmx-parser-0.10.0.tgz", + "integrity": "sha512-kjOHHpI6ymxb3BLLUKMLpYwhTFC5d0KEGw+xzxNmI7YPnSmY5Qbb5b1Io/LV35jFgyUeRZYNPsxg1PVtYnTdhw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "xml-js": "1.6.11" + } + }, + "node_modules/@sap-ux/eslint-plugin-fiori-tools": { + "version": "10.9.4", + "resolved": "https://registry.npmjs.org/@sap-ux/eslint-plugin-fiori-tools/-/eslint-plugin-fiori-tools-10.9.4.tgz", + "integrity": "sha512-aaNIV0w2JnfMu5uLVg6v/Otngo5cieKZ9ctOTSZgo2Nd9HLUWQMNvVVWMvtRZ8bfmpS0iX1S6n4vjw5cy9+vxQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@typescript-eslint/eslint-plugin": "8.61.1", + "@typescript-eslint/parser": "8.61.1" + }, + "engines": { + "node": ">=22.x" + }, + "peerDependencies": { + "eslint": "^9 || ^10", + "typescript-eslint": "^8.57.2" + } + }, + "node_modules/@sap-ux/fe-mockserver-core": { + "version": "1.7.15", + "resolved": "https://registry.npmjs.org/@sap-ux/fe-mockserver-core/-/fe-mockserver-core-1.7.15.tgz", + "integrity": "sha512-WM2oWTw45aT28bc0NwrpCNexhUglQMKh4Dn6bXHrP9RjKjtF/jIsGC5jaIVMeN9Cu0KqkZAx68Qcy+KDde0ZUA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@sap-ux/annotation-converter": "0.10.22", + "@sap-ux/edmx-parser": "0.10.0", + "balanced-match": "1.0.2", + "body-parser": "1.20.4", + "braces": "3.0.3", + "chevrotain": "9.1.0", + "chokidar": "3.6.0", + "etag": "1.8.1", + "graceful-fs": "4.2.11", + "lodash.clonedeep": "4.5.0", + "lodash.merge": "4.6.2", + "query-string": "7.1.3", + "router": "2.0.0" + } + }, + "node_modules/@sap-ux/ui5-middleware-fe-mockserver": { + "version": "2.4.16", + "resolved": "https://registry.npmjs.org/@sap-ux/ui5-middleware-fe-mockserver/-/ui5-middleware-fe-mockserver-2.4.16.tgz", + "integrity": "sha512-LJ2jAR9cFoetRYGokvYJBlaXvb2Zt7fwLcNvSbNCmFoF0nEe0SsDJs/dGXZaC7JJHLVDb7R49OUdvLltAuqVhg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@sap-ux/fe-mockserver-core": "1.7.15" + } + }, + "node_modules/@sap-ux/vocabularies-types": { + "version": "0.16.0", + "resolved": "https://registry.npmjs.org/@sap-ux/vocabularies-types/-/vocabularies-types-0.16.0.tgz", + "integrity": "sha512-NQErCbBZkgSaD2P0wNdq33ZOfacaePr5hjZjtzFEQCU5Yd+bQxfolU/xmVieXWWJh5UK343nt6GaMH73jfcW0w==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=22.x" + } + }, + "node_modules/@sap/ux-ui5-tooling": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/@sap/ux-ui5-tooling/-/ux-ui5-tooling-1.32.0.tgz", + "integrity": "sha512-N/ZCSc9qpv+cbiHy5huUNQSLNgUhKXbeEo4SfeN6nbCdYLCoDvxfRciiWXNEMfi2oGkqJMY+4jf3mngs05Qaxg==", + "dev": true, + "license": "SEE LICENSE IN LICENSE", + "bin": { + "fiori": "bin/fiori.cjs" + } + }, + "node_modules/@sapui5/types": { + "version": "1.152.0", + "resolved": "https://registry.npmjs.org/@sapui5/types/-/types-1.152.0.tgz", + "integrity": "sha512-3Z3RV0FMKytl0UR5xKMGaby08STAA3/1VMNF9ObJ31TaILZweAkJELWdlXBnyczBFEgizIGLE+Ltkn2RygO1VQ==", + "dev": true, + "license": "SEE LICENSE IN LICENSE.txt", + "dependencies": { + "@types/jquery": "3.5.13", + "@types/offscreencanvas": "2019.6.4", + "@types/qunit": "2.5.4", + "@types/three": "0.125.3" + } + }, + "node_modules/@types/esrecurse": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@types/esrecurse/-/esrecurse-4.3.1.tgz", + "integrity": "sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/jquery": { + "version": "3.5.13", + "resolved": "https://registry.npmjs.org/@types/jquery/-/jquery-3.5.13.tgz", + "integrity": "sha512-ZxJrup8nz/ZxcU0vantG+TPdboMhB24jad2uSap50zE7Q9rUeYlCF25kFMSmHR33qoeOgqcdHEp3roaookC0Sg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/sizzle": "*" + } + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/offscreencanvas": { + "version": "2019.6.4", + "resolved": "https://registry.npmjs.org/@types/offscreencanvas/-/offscreencanvas-2019.6.4.tgz", + "integrity": "sha512-u8SAgdZ8ROtkTF+mfZGOscl0or6BSj9A4g37e6nvxDc+YB/oDut0wHkK2PBBiC2bNR8TS0CPV+1gAk4fNisr1Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/qunit": { + "version": "2.5.4", + "resolved": "https://registry.npmjs.org/@types/qunit/-/qunit-2.5.4.tgz", + "integrity": "sha512-VHi2lEd4/zp8OOouf43JXGJJ5ZxHvdLL1dU0Yakp6Iy73SjpuXl7yjwAwmh1qhTv8krDgHteSwaySr++uXX9YQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/sizzle": { + "version": "2.3.10", + "resolved": "https://registry.npmjs.org/@types/sizzle/-/sizzle-2.3.10.tgz", + "integrity": "sha512-TC0dmN0K8YcWEAEfiPi5gJP14eJe30TTGjkvek3iM/1NdHHsdCA/Td6GvNndMOo/iSnIsZ4HuuhrYPDAmbxzww==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/three": { + "version": "0.125.3", + "resolved": "https://registry.npmjs.org/@types/three/-/three-0.125.3.tgz", + "integrity": "sha512-tUPMzKooKDvMOhqcNVUPwkt+JNnF8ASgWSsrLgleVd0SjLj4boJhteSsF9f6YDjye0mmUjO+BDMWW83F97ehXA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.61.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.61.1.tgz", + "integrity": "sha512-ZPlVl3PB3et/59Ne0fv/sci6ZXz4T4Hp4nTJ56i/Y0gR89ARb+KphojTq6j+56E5PIezmOIOOWyY+aWQFd+IkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.12.2", + "@typescript-eslint/scope-manager": "8.61.1", + "@typescript-eslint/type-utils": "8.61.1", + "@typescript-eslint/utils": "8.61.1", + "@typescript-eslint/visitor-keys": "8.61.1", + "ignore": "^7.0.5", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^8.61.1", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "8.61.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.61.1.tgz", + "integrity": "sha512-PJ5vePq5/ognBbrIcoC5+SHO5dfpeLPzP9FpLkzWrguoYQEeeSjlJpVwOpo1JRSTEi7dRcwNy4h4dzV70PqHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/scope-manager": "8.61.1", + "@typescript-eslint/types": "8.61.1", + "@typescript-eslint/typescript-estree": "8.61.1", + "@typescript-eslint/visitor-keys": "8.61.1", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/project-service": { + "version": "8.61.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.61.1.tgz", + "integrity": "sha512-PrC4JYGmR241lYnfhmKGTXkFqv8+ymbTFgSAY0fVXpY82/QkMw5TZPl+vGzuDDU2QYJk9fIDOBTntF+yDv9LEA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.61.1", + "@typescript-eslint/types": "^8.61.1", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.61.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.61.1.tgz", + "integrity": "sha512-L2bdIeoQS8FlKAvONAr20w6OcLXeB+qiDKbAooS9A0Ben+iSIkBef0FxqwKWYqt5sa0i4KJtxVyVmhMylKzF5w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.61.1", + "@typescript-eslint/visitor-keys": "8.61.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.61.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.61.1.tgz", + "integrity": "sha512-UN/H4di+OO7EWx2ovME+8t31YO+KVnK0RRKEHR3kOt21/Ay8BOq3M1OMvWs5vNiqcFCYGYoxK3MXPZzmMUE+yg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "8.61.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.61.1.tgz", + "integrity": "sha512-GYRicKmVK0C4fsKgaACaknOUAq9Oa2kwsjnpFhFcS/5p4Ht5IP9OVLbgIgcK4SRk92nVHFluurg1lumD9dBcLw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.61.1", + "@typescript-eslint/typescript-estree": "8.61.1", + "@typescript-eslint/utils": "8.61.1", + "debug": "^4.4.3", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "8.61.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.61.1.tgz", + "integrity": "sha512-G+CRlPqLv7Bz1IZVs03x5K59F1veqL0EJUROAdGhKsEq8qOiRiZbI+HUojPq5l0fEGOKModD9br6lObhB8zkoA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.61.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.61.1.tgz", + "integrity": "sha512-u+oQD3BqYWPc8YV9Zab4vaJElJuwOLPRc10Jm1o/qS+6Qwen14HCWwx0Seo4LnSn2wxea2Ik8DxPt2/FHmuhrg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/project-service": "8.61.1", + "@typescript-eslint/tsconfig-utils": "8.61.1", + "@typescript-eslint/types": "8.61.1", + "@typescript-eslint/visitor-keys": "8.61.1", + "debug": "^4.4.3", + "minimatch": "^10.2.2", + "semver": "^7.7.3", + "tinyglobby": "^0.2.15", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.61.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.61.1.tgz", + "integrity": "sha512-1+P/3Dj6jvtybE1q0HQ6yBt/gq+oKJyLdEv4HdnqasaEXRSYCAsD59mXEVQnM/ULNdQxbX77tdG4jPRjIS6knA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.9.1", + "@typescript-eslint/scope-manager": "8.61.1", + "@typescript-eslint/types": "8.61.1", + "@typescript-eslint/typescript-estree": "8.61.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.61.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.61.1.tgz", + "integrity": "sha512-6fJ9MHWtK14C1DSkiMlHUSOmrVebL7150xZJBlJiL62jjhIA4JmOq6flwBgDxIdBKKdoiZRel+dfPD5MLfny3w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.61.1", + "eslint-visitor-keys": "^5.0.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", + "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@ui5/cli": { + "version": "4.0.67", + "resolved": "https://registry.npmjs.org/@ui5/cli/-/cli-4.0.67.tgz", + "integrity": "sha512-EFSxKSREiDSn+TGpR0oEBx5N3qaNjz/DH1YnwPqc9ZrDjYlpC5NQyqK4gA0Ap/AyJZKIWEz/MbATg4NdqcGP7w==", + "bundleDependencies": [ + "@ui5/builder", + "@ui5/fs", + "@ui5/logger", + "@ui5/project", + "@ui5/server", + "chalk", + "data-with-position", + "import-local", + "js-yaml", + "open", + "pretty-hrtime", + "semver", + "update-notifier", + "yargs" + ], + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@ui5/builder": "^4.3.1", + "@ui5/fs": "^4.0.6", + "@ui5/logger": "^4.0.2", + "@ui5/project": "^4.0.17", + "@ui5/server": "^4.0.16", + "chalk": "^5.6.2", + "data-with-position": "^0.5.0", + "import-local": "^3.2.0", + "js-yaml": "^4.3.2", + "open": "^11.0.3", + "pretty-hrtime": "^1.0.3", + "semver": "^7.8.5", + "update-notifier": "^7.3.1", + "yargs": "^17.7.3" + }, + "bin": { + "ui5": "bin/ui5.cjs" + }, + "engines": { + "node": "^20.11.0 || >=22.0.0", + "npm": ">= 8" + } + }, + "node_modules/@ui5/cli/node_modules/@adobe/css-tools": { + "version": "4.5.0", + "dev": true, + "inBundle": true, + "license": "MIT" + }, + "node_modules/@ui5/cli/node_modules/@babel/code-frame": { + "version": "7.29.7", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.29.7", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@ui5/cli/node_modules/@babel/helper-string-parser": { + "version": "7.29.7", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@ui5/cli/node_modules/@babel/helper-validator-identifier": { + "version": "7.29.7", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@ui5/cli/node_modules/@babel/parser": { + "version": "7.29.8", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.8" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@ui5/cli/node_modules/@babel/types": { + "version": "7.29.8", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@ui5/cli/node_modules/@isaacs/cliui": { + "version": "8.0.2", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@ui5/cli/node_modules/@isaacs/cliui/node_modules/emoji-regex": { + "version": "9.2.2", + "dev": true, + "inBundle": true, + "license": "MIT" + }, + "node_modules/@ui5/cli/node_modules/@isaacs/cliui/node_modules/string-width": { + "version": "5.1.2", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@ui5/cli/node_modules/@isaacs/fs-minipass": { + "version": "4.0.1", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "minipass": "^7.0.4" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@ui5/cli/node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@ui5/cli/node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@ui5/cli/node_modules/@jridgewell/source-map": { + "version": "0.3.11", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25" + } + }, + "node_modules/@ui5/cli/node_modules/@jridgewell/sourcemap-codec": { + "version": "1.6.0", + "dev": true, + "inBundle": true, + "license": "MIT" + }, + "node_modules/@ui5/cli/node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@ui5/cli/node_modules/@jsdoc/salty": { + "version": "0.2.12", + "dev": true, + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "lodash": "^4.18.1" + }, + "engines": { + "node": ">=v12.0.0" + } + }, + "node_modules/@ui5/cli/node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@ui5/cli/node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@ui5/cli/node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@ui5/cli/node_modules/@npmcli/agent": { + "version": "3.0.0", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "agent-base": "^7.1.0", + "http-proxy-agent": "^7.0.0", + "https-proxy-agent": "^7.0.1", + "lru-cache": "^10.0.1", + "socks-proxy-agent": "^8.0.3" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/@ui5/cli/node_modules/@npmcli/agent/node_modules/lru-cache": { + "version": "10.4.3", + "dev": true, + "inBundle": true, + "license": "ISC" + }, + "node_modules/@ui5/cli/node_modules/@npmcli/config": { + "version": "9.0.0", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "@npmcli/map-workspaces": "^4.0.1", + "@npmcli/package-json": "^6.0.1", + "ci-info": "^4.0.0", + "ini": "^5.0.0", + "nopt": "^8.0.0", + "proc-log": "^5.0.0", + "semver": "^7.3.5", + "walk-up-path": "^3.0.1" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/@ui5/cli/node_modules/@npmcli/git": { + "version": "6.0.3", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "@npmcli/promise-spawn": "^8.0.0", + "ini": "^5.0.0", + "lru-cache": "^10.0.1", + "npm-pick-manifest": "^10.0.0", + "proc-log": "^5.0.0", + "promise-retry": "^2.0.1", + "semver": "^7.3.5", + "which": "^5.0.0" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/@ui5/cli/node_modules/@npmcli/git/node_modules/lru-cache": { + "version": "10.4.3", + "dev": true, + "inBundle": true, + "license": "ISC" + }, + "node_modules/@ui5/cli/node_modules/@npmcli/map-workspaces": { + "version": "4.0.2", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "@npmcli/name-from-folder": "^3.0.0", + "@npmcli/package-json": "^6.0.0", + "glob": "^10.2.2", + "minimatch": "^9.0.0" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/@ui5/cli/node_modules/@npmcli/map-workspaces/node_modules/balanced-match": { + "version": "1.0.2", + "dev": true, + "inBundle": true, + "license": "MIT" + }, + "node_modules/@ui5/cli/node_modules/@npmcli/map-workspaces/node_modules/brace-expansion": { + "version": "2.1.4", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@ui5/cli/node_modules/@npmcli/map-workspaces/node_modules/minimatch": { + "version": "9.0.9", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.2" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@ui5/cli/node_modules/@npmcli/name-from-folder": { + "version": "3.0.0", + "dev": true, + "inBundle": true, + "license": "ISC", + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/@ui5/cli/node_modules/@npmcli/package-json": { + "version": "6.2.0", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "@npmcli/git": "^6.0.0", + "glob": "^10.2.2", + "hosted-git-info": "^8.0.0", + "json-parse-even-better-errors": "^4.0.0", + "proc-log": "^5.0.0", + "semver": "^7.5.3", + "validate-npm-package-license": "^3.0.4" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/@ui5/cli/node_modules/@npmcli/promise-spawn": { + "version": "8.0.3", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "which": "^5.0.0" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/@ui5/cli/node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/@ui5/cli/node_modules/@pnpm/config.env-replace": { + "version": "1.1.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=12.22.0" + } + }, + "node_modules/@ui5/cli/node_modules/@pnpm/network.ca-file": { + "version": "1.0.2", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "4.2.10" + }, + "engines": { + "node": ">=12.22.0" + } + }, + "node_modules/@ui5/cli/node_modules/@pnpm/network.ca-file/node_modules/graceful-fs": { + "version": "4.2.10", + "dev": true, + "inBundle": true, + "license": "ISC" + }, + "node_modules/@ui5/cli/node_modules/@pnpm/npm-conf": { + "version": "3.0.3", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "@pnpm/config.env-replace": "^1.1.0", + "@pnpm/network.ca-file": "^1.0.1", + "config-chain": "^1.1.11" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@ui5/cli/node_modules/@sigstore/bundle": { + "version": "3.1.0", + "dev": true, + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "@sigstore/protobuf-specs": "^0.4.0" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/@ui5/cli/node_modules/@sigstore/core": { + "version": "2.0.0", + "dev": true, + "inBundle": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/@ui5/cli/node_modules/@sigstore/protobuf-specs": { + "version": "0.4.3", + "dev": true, + "inBundle": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/@ui5/cli/node_modules/@sigstore/sign": { + "version": "3.1.0", + "dev": true, + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "@sigstore/bundle": "^3.1.0", + "@sigstore/core": "^2.0.0", + "@sigstore/protobuf-specs": "^0.4.0", + "make-fetch-happen": "^14.0.2", + "proc-log": "^5.0.0", + "promise-retry": "^2.0.1" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/@ui5/cli/node_modules/@sigstore/tuf": { + "version": "3.1.1", + "dev": true, + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "@sigstore/protobuf-specs": "^0.4.1", + "tuf-js": "^3.0.1" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/@ui5/cli/node_modules/@sigstore/verify": { + "version": "2.1.1", + "dev": true, + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "@sigstore/bundle": "^3.1.0", + "@sigstore/core": "^2.0.0", + "@sigstore/protobuf-specs": "^0.4.1" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/@ui5/cli/node_modules/@sindresorhus/merge-streams": { + "version": "4.0.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@ui5/cli/node_modules/@tufjs/canonical-json": { + "version": "2.0.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/@ui5/cli/node_modules/@tufjs/models": { + "version": "3.0.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "@tufjs/canonical-json": "2.0.0", + "minimatch": "^9.0.5" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/@ui5/cli/node_modules/@tufjs/models/node_modules/balanced-match": { + "version": "1.0.2", + "dev": true, + "inBundle": true, + "license": "MIT" + }, + "node_modules/@ui5/cli/node_modules/@tufjs/models/node_modules/brace-expansion": { + "version": "2.1.4", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@ui5/cli/node_modules/@tufjs/models/node_modules/minimatch": { + "version": "9.0.9", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.2" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@ui5/cli/node_modules/@types/linkify-it": { + "version": "5.0.0", + "dev": true, + "inBundle": true, + "license": "MIT" + }, + "node_modules/@ui5/cli/node_modules/@types/markdown-it": { + "version": "14.2.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "@types/linkify-it": "^5", + "@types/mdurl": "^2" + } + }, + "node_modules/@ui5/cli/node_modules/@types/mdurl": { + "version": "2.0.0", + "dev": true, + "inBundle": true, + "license": "MIT" + }, + "node_modules/@ui5/cli/node_modules/@types/normalize-package-data": { + "version": "2.4.4", + "dev": true, + "inBundle": true, + "license": "MIT" + }, + "node_modules/@ui5/cli/node_modules/@ui5/builder": { + "version": "4.3.1", + "dev": true, + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5", + "@ui5/fs": "^4.0.6", + "@ui5/logger": "^4.0.2", + "cheerio": "1.0.0", + "escape-unicode": "^0.3.0", + "escope": "^4.0.0", + "espree": "^10.4.0", + "graceful-fs": "^4.2.11", + "jsdoc": "^4.0.5", + "less-openui5": "^0.12.0", + "pretty-data": "^0.40.0", + "semver": "^7.8.5", + "terser": "^5.49.2", + "workerpool": "^10.0.3", + "xml2js": "^0.6.2" + }, + "engines": { + "node": "^20.11.0 || >=22.0.0", + "npm": ">= 8" + } + }, + "node_modules/@ui5/cli/node_modules/@ui5/fs": { + "version": "4.0.6", + "dev": true, + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "@ui5/logger": "^4.0.2", + "escape-string-regexp": "^5.0.0", + "globby": "^16.2.0", + "graceful-fs": "^4.2.11", + "micromatch": "^4.0.8", + "minimatch": "^10.2.5", + "pretty-hrtime": "^1.0.3", + "random-int": "^3.1.0" + }, + "engines": { + "node": "^20.11.0 || >=22.0.0", + "npm": ">= 8" + } + }, + "node_modules/@ui5/cli/node_modules/@ui5/logger": { + "version": "4.0.2", + "dev": true, + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "chalk": "^5.6.0", + "cli-progress": "^3.12.0", + "figures": "^6.1.0" + }, + "engines": { + "node": "^20.11.0 || >=22.0.0", + "npm": ">= 8" + } + }, + "node_modules/@ui5/cli/node_modules/@ui5/project": { + "version": "4.0.17", + "dev": true, + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "@npmcli/config": "^9.0.0", + "@ui5/fs": "^4.0.6", + "@ui5/logger": "^4.0.2", + "ajv": "^8.20.0", + "ajv-errors": "^3.0.0", + "chalk": "^5.6.2", + "escape-string-regexp": "^5.0.0", + "globby": "^14.1.0", + "graceful-fs": "^4.2.11", + "js-yaml": "^4.2.0", + "lockfile": "^1.0.4", + "make-fetch-happen": "^14.0.3", + "node-stream-zip": "^1.15.0", + "pacote": "^19.0.2", + "pretty-hrtime": "^1.0.3", + "read-package-up": "^11.0.0", + "read-pkg": "^9.0.1", + "resolve": "^1.22.12", + "semver": "^7.8.4", + "xml2js": "^0.6.2", + "yesno": "^0.4.0" + }, + "engines": { + "node": "^20.11.0 || >=22.0.0", + "npm": ">= 8" + }, + "peerDependencies": { + "@ui5/builder": "^4.2.0" + }, + "peerDependenciesMeta": { + "@ui5/builder": { + "optional": true + } + } + }, + "node_modules/@ui5/cli/node_modules/@ui5/project/node_modules/@sindresorhus/merge-streams": { + "version": "2.3.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@ui5/cli/node_modules/@ui5/project/node_modules/globby": { + "version": "14.1.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "@sindresorhus/merge-streams": "^2.1.0", + "fast-glob": "^3.3.3", + "ignore": "^7.0.3", + "path-type": "^6.0.0", + "slash": "^5.1.0", + "unicorn-magic": "^0.3.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@ui5/cli/node_modules/@ui5/project/node_modules/ignore": { + "version": "7.0.9", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/@ui5/cli/node_modules/@ui5/project/node_modules/unicorn-magic": { + "version": "0.3.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@ui5/cli/node_modules/@ui5/server": { + "version": "4.0.16", + "dev": true, + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "@ui5/builder": "^4.3.1", + "@ui5/fs": "^4.0.6", + "@ui5/logger": "^4.0.2", + "body-parser": "^2.3.0", + "compression": "^1.8.1", + "cors": "^2.8.6", + "devcert-sanscache": "^0.5.1", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "express": "^4.22.2", + "fresh": "^0.5.2", + "graceful-fs": "^4.2.11", + "mime-types": "^2.1.35", + "parseurl": "^1.3.3", + "portscanner": "^2.2.0", + "replacestream": "^4.0.3", + "router": "^2.2.0", + "spdy": "^4.0.2", + "yesno": "^0.4.0" + }, + "engines": { + "node": "^20.11.0 || >=22.0.0", + "npm": ">= 8" + } + }, + "node_modules/@ui5/cli/node_modules/abbrev": { + "version": "3.0.1", + "dev": true, + "inBundle": true, + "license": "ISC", + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/@ui5/cli/node_modules/accepts": { + "version": "1.3.8", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/@ui5/cli/node_modules/accepts/node_modules/negotiator": { + "version": "0.6.3", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/@ui5/cli/node_modules/acorn": { + "version": "8.18.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/@ui5/cli/node_modules/acorn-jsx": { + "version": "5.3.2", + "dev": true, + "inBundle": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/@ui5/cli/node_modules/agent-base": { + "version": "7.1.4", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/@ui5/cli/node_modules/ajv": { + "version": "8.20.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/@ui5/cli/node_modules/ajv-errors": { + "version": "3.0.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "peerDependencies": { + "ajv": "^8.0.1" + } + }, + "node_modules/@ui5/cli/node_modules/ansi-align": { + "version": "3.0.1", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.1.0" + } + }, + "node_modules/@ui5/cli/node_modules/ansi-regex": { + "version": "6.3.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/@ui5/cli/node_modules/ansi-styles": { + "version": "6.2.3", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@ui5/cli/node_modules/argparse": { + "version": "2.0.1", + "dev": true, + "inBundle": true, + "license": "Python-2.0" + }, + "node_modules/@ui5/cli/node_modules/array-flatten": { + "version": "1.1.1", + "dev": true, + "inBundle": true, + "license": "MIT" + }, + "node_modules/@ui5/cli/node_modules/async": { + "version": "2.6.4", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "lodash": "^4.17.14" + } + }, + "node_modules/@ui5/cli/node_modules/atomically": { + "version": "2.1.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "stubborn-fs": "^2.0.0", + "when-exit": "^2.1.4" + } + }, + "node_modules/@ui5/cli/node_modules/balanced-match": { + "version": "4.0.4", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/@ui5/cli/node_modules/bluebird": { + "version": "3.7.2", + "dev": true, + "inBundle": true, + "license": "MIT" + }, + "node_modules/@ui5/cli/node_modules/body-parser": { + "version": "2.3.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "bytes": "^3.1.2", + "content-type": "^2.0.0", + "debug": "^4.4.3", + "http-errors": "^2.0.1", + "iconv-lite": "^0.7.2", + "on-finished": "^2.4.1", + "qs": "^6.15.2", + "raw-body": "^3.0.2", + "type-is": "^2.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/@ui5/cli/node_modules/boolbase": { + "version": "1.0.0", + "dev": true, + "inBundle": true, + "license": "ISC" + }, + "node_modules/@ui5/cli/node_modules/boxen": { + "version": "8.0.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "ansi-align": "^3.0.1", + "camelcase": "^8.0.0", + "chalk": "^5.3.0", + "cli-boxes": "^3.0.0", + "string-width": "^7.2.0", + "type-fest": "^4.21.0", + "widest-line": "^5.0.0", + "wrap-ansi": "^9.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@ui5/cli/node_modules/boxen/node_modules/camelcase": { + "version": "8.0.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@ui5/cli/node_modules/boxen/node_modules/emoji-regex": { + "version": "10.6.0", + "dev": true, + "inBundle": true, + "license": "MIT" + }, + "node_modules/@ui5/cli/node_modules/boxen/node_modules/string-width": { + "version": "7.2.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@ui5/cli/node_modules/boxen/node_modules/type-fest": { + "version": "4.41.0", + "dev": true, + "inBundle": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@ui5/cli/node_modules/boxen/node_modules/wrap-ansi": { + "version": "9.0.2", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.2.1", + "string-width": "^7.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/@ui5/cli/node_modules/brace-expansion": { + "version": "5.0.9", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/@ui5/cli/node_modules/braces": { + "version": "3.0.3", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@ui5/cli/node_modules/buffer-from": { + "version": "1.1.2", + "dev": true, + "inBundle": true, + "license": "MIT" + }, + "node_modules/@ui5/cli/node_modules/bundle-name": { + "version": "4.1.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "run-applescript": "^7.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@ui5/cli/node_modules/bytes": { + "version": "3.1.2", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/@ui5/cli/node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/@ui5/cli/node_modules/call-bound": { + "version": "1.0.4", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/@ui5/cli/node_modules/catharsis": { + "version": "0.9.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "lodash": "^4.17.15" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/@ui5/cli/node_modules/chalk": { + "version": "5.6.2", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/@ui5/cli/node_modules/cheerio": { + "version": "1.0.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "cheerio-select": "^2.1.0", + "dom-serializer": "^2.0.0", + "domhandler": "^5.0.3", + "domutils": "^3.1.0", + "encoding-sniffer": "^0.2.0", + "htmlparser2": "^9.1.0", + "parse5": "^7.1.2", + "parse5-htmlparser2-tree-adapter": "^7.0.0", + "parse5-parser-stream": "^7.1.2", + "undici": "^6.19.5", + "whatwg-mimetype": "^4.0.0" + }, + "engines": { + "node": ">=18.17" + }, + "funding": { + "url": "https://github.com/cheeriojs/cheerio?sponsor=1" + } + }, + "node_modules/@ui5/cli/node_modules/cheerio-select": { + "version": "2.1.0", + "dev": true, + "inBundle": true, + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0", + "css-select": "^5.1.0", + "css-what": "^6.1.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/@ui5/cli/node_modules/chownr": { + "version": "3.0.0", + "dev": true, + "inBundle": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/@ui5/cli/node_modules/ci-info": { + "version": "4.4.0", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@ui5/cli/node_modules/cli-boxes": { + "version": "3.0.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@ui5/cli/node_modules/cli-progress": { + "version": "3.12.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "string-width": "^4.2.3" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@ui5/cli/node_modules/cliui": { + "version": "8.0.1", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@ui5/cli/node_modules/cliui/node_modules/ansi-regex": { + "version": "5.0.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@ui5/cli/node_modules/cliui/node_modules/ansi-styles": { + "version": "4.3.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@ui5/cli/node_modules/cliui/node_modules/color-convert": { + "version": "2.0.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/@ui5/cli/node_modules/cliui/node_modules/color-name": { + "version": "1.1.4", + "dev": true, + "inBundle": true, + "license": "MIT" + }, + "node_modules/@ui5/cli/node_modules/cliui/node_modules/strip-ansi": { + "version": "6.0.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@ui5/cli/node_modules/cliui/node_modules/wrap-ansi": { + "version": "7.0.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/@ui5/cli/node_modules/command-exists": { + "version": "1.2.9", + "dev": true, + "inBundle": true, + "license": "MIT" + }, + "node_modules/@ui5/cli/node_modules/commander": { + "version": "2.20.3", + "dev": true, + "inBundle": true, + "license": "MIT" + }, + "node_modules/@ui5/cli/node_modules/compressible": { + "version": "2.0.18", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "mime-db": ">= 1.43.0 < 2" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/@ui5/cli/node_modules/compression": { + "version": "1.8.2", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "compressible": "~2.0.18", + "debug": "2.6.9", + "destroy": "1.2.0", + "negotiator": "~0.6.4", + "on-headers": "~1.1.0", + "safe-buffer": "5.2.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.8.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/@ui5/cli/node_modules/compression/node_modules/debug": { + "version": "2.6.9", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/@ui5/cli/node_modules/compression/node_modules/ms": { + "version": "2.0.0", + "dev": true, + "inBundle": true, + "license": "MIT" + }, + "node_modules/@ui5/cli/node_modules/config-chain": { + "version": "1.1.13", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "ini": "^1.3.4", + "proto-list": "~1.2.1" + } + }, + "node_modules/@ui5/cli/node_modules/config-chain/node_modules/ini": { + "version": "1.3.8", + "dev": true, + "inBundle": true, + "license": "ISC" + }, + "node_modules/@ui5/cli/node_modules/configstore": { + "version": "7.1.0", + "dev": true, + "inBundle": true, + "license": "BSD-2-Clause", + "dependencies": { + "atomically": "^2.0.3", + "dot-prop": "^9.0.0", + "graceful-fs": "^4.2.11", + "xdg-basedir": "^5.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@ui5/cli/node_modules/content-disposition": { + "version": "0.5.4", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/@ui5/cli/node_modules/content-type": { + "version": "2.1.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/@ui5/cli/node_modules/cookie": { + "version": "0.7.2", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/@ui5/cli/node_modules/cookie-signature": { + "version": "1.0.7", + "dev": true, + "inBundle": true, + "license": "MIT" + }, + "node_modules/@ui5/cli/node_modules/core-util-is": { + "version": "1.0.3", + "dev": true, + "inBundle": true, + "license": "MIT" + }, + "node_modules/@ui5/cli/node_modules/cors": { + "version": "2.8.6", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/@ui5/cli/node_modules/cross-spawn": { + "version": "7.0.6", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@ui5/cli/node_modules/cross-spawn/node_modules/isexe": { + "version": "2.0.0", + "dev": true, + "inBundle": true, + "license": "ISC" + }, + "node_modules/@ui5/cli/node_modules/cross-spawn/node_modules/which": { + "version": "2.0.2", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@ui5/cli/node_modules/css-select": { + "version": "5.2.2", + "dev": true, + "inBundle": true, + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0", + "css-what": "^6.1.0", + "domhandler": "^5.0.2", + "domutils": "^3.0.1", + "nth-check": "^2.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/@ui5/cli/node_modules/css-what": { + "version": "6.2.2", + "dev": true, + "inBundle": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">= 6" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/@ui5/cli/node_modules/data-with-position": { + "version": "0.5.0", + "dev": true, + "inBundle": true, + "license": "BSD-3-Clause", + "dependencies": { + "yaml-ast-parser": "^0.0.43" + } + }, + "node_modules/@ui5/cli/node_modules/debug": { + "version": "4.4.3", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@ui5/cli/node_modules/deep-extend": { + "version": "0.6.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/@ui5/cli/node_modules/default-browser": { + "version": "5.5.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "bundle-name": "^4.1.0", + "default-browser-id": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@ui5/cli/node_modules/default-browser-id": { + "version": "5.0.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@ui5/cli/node_modules/define-lazy-prop": { + "version": "3.0.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@ui5/cli/node_modules/depd": { + "version": "2.0.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/@ui5/cli/node_modules/destroy": { + "version": "1.2.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/@ui5/cli/node_modules/detect-node": { + "version": "2.1.0", + "dev": true, + "inBundle": true, + "license": "MIT" + }, + "node_modules/@ui5/cli/node_modules/devcert-sanscache": { + "version": "0.5.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "command-exists": "^1.2.9", + "get-port": "^6.1.2", + "glob": "^10.4.5", + "rimraf": "^5.0.9" + }, + "engines": { + "node": "^14.13.1 || >=16.0.0" + } + }, + "node_modules/@ui5/cli/node_modules/devcert-sanscache/node_modules/rimraf": { + "version": "5.0.10", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "glob": "^10.3.7" + }, + "bin": { + "rimraf": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@ui5/cli/node_modules/dom-serializer": { + "version": "2.0.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.2", + "entities": "^4.2.0" + }, + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + } + }, + "node_modules/@ui5/cli/node_modules/domelementtype": { + "version": "2.3.0", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "inBundle": true, + "license": "BSD-2-Clause" + }, + "node_modules/@ui5/cli/node_modules/domhandler": { + "version": "5.0.3", + "dev": true, + "inBundle": true, + "license": "BSD-2-Clause", + "dependencies": { + "domelementtype": "^2.3.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/@ui5/cli/node_modules/domutils": { + "version": "3.2.2", + "dev": true, + "inBundle": true, + "license": "BSD-2-Clause", + "dependencies": { + "dom-serializer": "^2.0.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3" + }, + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" + } + }, + "node_modules/@ui5/cli/node_modules/dot-prop": { + "version": "9.0.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "type-fest": "^4.18.2" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@ui5/cli/node_modules/dot-prop/node_modules/type-fest": { + "version": "4.41.0", + "dev": true, + "inBundle": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@ui5/cli/node_modules/dunder-proto": { + "version": "1.0.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/@ui5/cli/node_modules/eastasianwidth": { + "version": "0.2.0", + "dev": true, + "inBundle": true, + "license": "MIT" + }, + "node_modules/@ui5/cli/node_modules/ee-first": { + "version": "1.1.1", + "dev": true, + "inBundle": true, + "license": "MIT" + }, + "node_modules/@ui5/cli/node_modules/emoji-regex": { + "version": "8.0.0", + "dev": true, + "inBundle": true, + "license": "MIT" + }, + "node_modules/@ui5/cli/node_modules/encodeurl": { + "version": "2.0.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/@ui5/cli/node_modules/encoding": { + "version": "0.1.13", + "dev": true, + "inBundle": true, + "license": "MIT", + "optional": true, + "dependencies": { + "iconv-lite": "^0.6.2" + } + }, + "node_modules/@ui5/cli/node_modules/encoding-sniffer": { + "version": "0.2.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "iconv-lite": "^0.6.3", + "whatwg-encoding": "^3.1.1" + }, + "funding": { + "url": "https://github.com/fb55/encoding-sniffer?sponsor=1" + } + }, + "node_modules/@ui5/cli/node_modules/encoding-sniffer/node_modules/iconv-lite": { + "version": "0.6.3", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/@ui5/cli/node_modules/encoding/node_modules/iconv-lite": { + "version": "0.6.3", + "dev": true, + "inBundle": true, + "license": "MIT", + "optional": true, + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/@ui5/cli/node_modules/entities": { + "version": "4.5.0", + "dev": true, + "inBundle": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/@ui5/cli/node_modules/env-paths": { + "version": "2.2.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/@ui5/cli/node_modules/err-code": { + "version": "2.0.3", + "dev": true, + "inBundle": true, + "license": "MIT" + }, + "node_modules/@ui5/cli/node_modules/es-define-property": { + "version": "1.0.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/@ui5/cli/node_modules/es-errors": { + "version": "1.3.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/@ui5/cli/node_modules/es-object-atoms": { + "version": "1.1.2", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/@ui5/cli/node_modules/escalade": { + "version": "3.2.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/@ui5/cli/node_modules/escape-goat": { + "version": "4.0.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@ui5/cli/node_modules/escape-html": { + "version": "1.0.3", + "dev": true, + "inBundle": true, + "license": "MIT" + }, + "node_modules/@ui5/cli/node_modules/escape-string-regexp": { + "version": "5.0.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@ui5/cli/node_modules/escape-unicode": { + "version": "0.3.0", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/neocotic" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/neocotic" + } + ], + "inBundle": true, + "license": "MIT" + }, + "node_modules/@ui5/cli/node_modules/escope": { + "version": "4.0.0", + "dev": true, + "inBundle": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.1.0", + "estraverse": "^4.1.1" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/@ui5/cli/node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "dev": true, + "inBundle": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@ui5/cli/node_modules/espree": { + "version": "10.4.0", + "dev": true, + "inBundle": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.15.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@ui5/cli/node_modules/esrecurse": { + "version": "4.3.0", + "dev": true, + "inBundle": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/@ui5/cli/node_modules/esrecurse/node_modules/estraverse": { + "version": "5.3.0", + "dev": true, + "inBundle": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/@ui5/cli/node_modules/estraverse": { + "version": "4.3.0", + "dev": true, + "inBundle": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/@ui5/cli/node_modules/etag": { + "version": "1.8.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/@ui5/cli/node_modules/exponential-backoff": { + "version": "3.1.3", + "dev": true, + "inBundle": true, + "license": "Apache-2.0" + }, + "node_modules/@ui5/cli/node_modules/express": { + "version": "4.22.3", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "~1.20.5", + "content-disposition": "~0.5.4", + "content-type": "~1.0.4", + "cookie": "~0.7.1", + "cookie-signature": "~1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "~1.3.1", + "fresh": "~0.5.2", + "http-errors": "~2.0.0", + "merge-descriptors": "1.0.3", + "methods": "~1.1.2", + "on-finished": "~2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "~0.1.13", + "proxy-addr": "~2.0.7", + "qs": "~6.16.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "~0.19.0", + "serve-static": "~1.16.2", + "setprototypeof": "1.2.0", + "statuses": "~2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/@ui5/cli/node_modules/express/node_modules/body-parser": { + "version": "1.20.8", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "content-type": "~1.0.5", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "~1.2.0", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "on-finished": "~2.4.1", + "qs": "~6.16.0", + "raw-body": "~2.5.3", + "type-is": "~1.6.18", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/@ui5/cli/node_modules/express/node_modules/body-parser/node_modules/qs": { + "version": "6.16.0", + "dev": true, + "inBundle": true, + "license": "BSD-3-Clause", + "dependencies": { + "es-define-property": "^1.0.1", + "side-channel": "^1.1.1" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/@ui5/cli/node_modules/express/node_modules/content-type": { + "version": "1.0.5", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/@ui5/cli/node_modules/express/node_modules/debug": { + "version": "2.6.9", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/@ui5/cli/node_modules/express/node_modules/iconv-lite": { + "version": "0.4.24", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/@ui5/cli/node_modules/express/node_modules/media-typer": { + "version": "0.3.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/@ui5/cli/node_modules/express/node_modules/ms": { + "version": "2.0.0", + "dev": true, + "inBundle": true, + "license": "MIT" + }, + "node_modules/@ui5/cli/node_modules/express/node_modules/raw-body": { + "version": "2.5.3", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/@ui5/cli/node_modules/express/node_modules/type-is": { + "version": "1.6.18", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/@ui5/cli/node_modules/fast-deep-equal": { + "version": "3.1.3", + "dev": true, + "inBundle": true, + "license": "MIT" + }, + "node_modules/@ui5/cli/node_modules/fast-glob": { + "version": "3.3.3", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/@ui5/cli/node_modules/fast-uri": { + "version": "3.1.7", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "inBundle": true, + "license": "BSD-3-Clause" + }, + "node_modules/@ui5/cli/node_modules/fastq": { + "version": "1.20.3", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/@ui5/cli/node_modules/fdir": { + "version": "6.5.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/@ui5/cli/node_modules/figures": { + "version": "6.1.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "is-unicode-supported": "^2.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@ui5/cli/node_modules/fill-range": { + "version": "7.1.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@ui5/cli/node_modules/finalhandler": { + "version": "1.3.2", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "on-finished": "~2.4.1", + "parseurl": "~1.3.3", + "statuses": "~2.0.2", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/@ui5/cli/node_modules/finalhandler/node_modules/debug": { + "version": "2.6.9", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/@ui5/cli/node_modules/finalhandler/node_modules/ms": { + "version": "2.0.0", + "dev": true, + "inBundle": true, + "license": "MIT" + }, + "node_modules/@ui5/cli/node_modules/find-up": { + "version": "4.1.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@ui5/cli/node_modules/find-up-simple": { + "version": "1.0.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@ui5/cli/node_modules/foreground-child": { + "version": "3.3.1", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@ui5/cli/node_modules/forwarded": { + "version": "0.2.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/@ui5/cli/node_modules/fresh": { + "version": "0.5.2", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/@ui5/cli/node_modules/fs-minipass": { + "version": "3.0.3", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "minipass": "^7.0.3" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/@ui5/cli/node_modules/function-bind": { + "version": "1.1.2", + "dev": true, + "inBundle": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/@ui5/cli/node_modules/get-caller-file": { + "version": "2.0.5", + "dev": true, + "inBundle": true, + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/@ui5/cli/node_modules/get-east-asian-width": { + "version": "1.6.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@ui5/cli/node_modules/get-intrinsic": { + "version": "1.3.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/@ui5/cli/node_modules/get-port": { + "version": "6.1.2", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@ui5/cli/node_modules/get-proto": { + "version": "1.0.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/@ui5/cli/node_modules/glob": { + "version": "10.5.0", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@ui5/cli/node_modules/glob-parent": { + "version": "5.1.2", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/@ui5/cli/node_modules/glob/node_modules/balanced-match": { + "version": "1.0.2", + "dev": true, + "inBundle": true, + "license": "MIT" + }, + "node_modules/@ui5/cli/node_modules/glob/node_modules/brace-expansion": { + "version": "2.1.4", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@ui5/cli/node_modules/glob/node_modules/minimatch": { + "version": "9.0.9", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.2" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@ui5/cli/node_modules/global-directory": { + "version": "4.0.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "ini": "4.1.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@ui5/cli/node_modules/global-directory/node_modules/ini": { + "version": "4.1.1", + "dev": true, + "inBundle": true, + "license": "ISC", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/@ui5/cli/node_modules/globby": { + "version": "16.2.4", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "@sindresorhus/merge-streams": "^4.0.0", + "fast-glob": "^3.3.3", + "ignore": "^7.0.5", + "is-path-inside": "^4.0.0", + "micromatch": "^4.0.8", + "slash": "^5.1.0", + "unicorn-magic": "^0.4.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@ui5/cli/node_modules/globby/node_modules/ignore": { + "version": "7.0.9", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/@ui5/cli/node_modules/gopd": { + "version": "1.2.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/@ui5/cli/node_modules/graceful-fs": { + "version": "4.2.11", + "dev": true, + "inBundle": true, + "license": "ISC" + }, + "node_modules/@ui5/cli/node_modules/handle-thing": { + "version": "2.0.1", + "dev": true, + "inBundle": true, + "license": "MIT" + }, + "node_modules/@ui5/cli/node_modules/has-symbols": { + "version": "1.1.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/@ui5/cli/node_modules/hasown": { + "version": "2.0.4", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/@ui5/cli/node_modules/hosted-git-info": { + "version": "8.1.0", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "lru-cache": "^10.0.1" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/@ui5/cli/node_modules/hosted-git-info/node_modules/lru-cache": { + "version": "10.4.3", + "dev": true, + "inBundle": true, + "license": "ISC" + }, + "node_modules/@ui5/cli/node_modules/hpack.js": { + "version": "2.1.6", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "inherits": "^2.0.1", + "obuf": "^1.0.0", + "readable-stream": "^2.0.1", + "wbuf": "^1.1.0" + } + }, + "node_modules/@ui5/cli/node_modules/htmlparser2": { + "version": "9.1.0", + "dev": true, + "funding": [ + "https://github.com/fb55/htmlparser2?sponsor=1", + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "inBundle": true, + "license": "MIT", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.1.0", + "entities": "^4.5.0" + } + }, + "node_modules/@ui5/cli/node_modules/http-cache-semantics": { + "version": "4.2.0", + "dev": true, + "inBundle": true, + "license": "BSD-2-Clause" + }, + "node_modules/@ui5/cli/node_modules/http-deceiver": { + "version": "1.2.7", + "dev": true, + "inBundle": true, + "license": "MIT" + }, + "node_modules/@ui5/cli/node_modules/http-errors": { + "version": "2.0.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/@ui5/cli/node_modules/http-proxy-agent": { + "version": "7.0.2", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/@ui5/cli/node_modules/https-proxy-agent": { + "version": "7.0.6", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/@ui5/cli/node_modules/iconv-lite": { + "version": "0.7.3", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/@ui5/cli/node_modules/ignore-walk": { + "version": "7.0.0", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "minimatch": "^9.0.0" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/@ui5/cli/node_modules/ignore-walk/node_modules/balanced-match": { + "version": "1.0.2", + "dev": true, + "inBundle": true, + "license": "MIT" + }, + "node_modules/@ui5/cli/node_modules/ignore-walk/node_modules/brace-expansion": { + "version": "2.1.4", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@ui5/cli/node_modules/ignore-walk/node_modules/minimatch": { + "version": "9.0.9", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.2" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@ui5/cli/node_modules/import-local": { + "version": "3.2.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "pkg-dir": "^4.2.0", + "resolve-cwd": "^3.0.0" + }, + "bin": { + "import-local-fixture": "fixtures/cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@ui5/cli/node_modules/import-local/node_modules/pkg-dir": { + "version": "4.2.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "find-up": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@ui5/cli/node_modules/imurmurhash": { + "version": "0.1.4", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/@ui5/cli/node_modules/index-to-position": { + "version": "1.2.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@ui5/cli/node_modules/inherits": { + "version": "2.0.4", + "dev": true, + "inBundle": true, + "license": "ISC" + }, + "node_modules/@ui5/cli/node_modules/ini": { + "version": "5.0.0", + "dev": true, + "inBundle": true, + "license": "ISC", + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/@ui5/cli/node_modules/ip-address": { + "version": "10.7.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, + "node_modules/@ui5/cli/node_modules/ipaddr.js": { + "version": "1.9.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/@ui5/cli/node_modules/is-core-module": { + "version": "2.16.2", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/@ui5/cli/node_modules/is-docker": { + "version": "3.0.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "bin": { + "is-docker": "cli.js" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@ui5/cli/node_modules/is-extglob": { + "version": "2.1.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/@ui5/cli/node_modules/is-glob": { + "version": "4.0.3", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/@ui5/cli/node_modules/is-in-ci": { + "version": "1.0.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "bin": { + "is-in-ci": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@ui5/cli/node_modules/is-in-ssh": { + "version": "1.0.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@ui5/cli/node_modules/is-inside-container": { + "version": "1.0.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "is-docker": "^3.0.0" + }, + "bin": { + "is-inside-container": "cli.js" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@ui5/cli/node_modules/is-installed-globally": { + "version": "1.0.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "global-directory": "^4.0.1", + "is-path-inside": "^4.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@ui5/cli/node_modules/is-npm": { + "version": "6.1.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@ui5/cli/node_modules/is-number": { + "version": "7.0.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/@ui5/cli/node_modules/is-number-like": { + "version": "1.0.8", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "lodash.isfinite": "^3.3.2" + } + }, + "node_modules/@ui5/cli/node_modules/is-path-inside": { + "version": "4.0.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@ui5/cli/node_modules/is-promise": { + "version": "4.0.0", + "dev": true, + "inBundle": true, + "license": "MIT" + }, + "node_modules/@ui5/cli/node_modules/is-unicode-supported": { + "version": "2.1.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@ui5/cli/node_modules/is-wsl": { + "version": "3.1.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "is-inside-container": "^1.0.0" + }, + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@ui5/cli/node_modules/isarray": { + "version": "1.0.0", + "dev": true, + "inBundle": true, + "license": "MIT" + }, + "node_modules/@ui5/cli/node_modules/isexe": { + "version": "3.1.5", + "dev": true, + "inBundle": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/@ui5/cli/node_modules/jackspeak": { + "version": "3.4.3", + "dev": true, + "inBundle": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "node_modules/@ui5/cli/node_modules/js-tokens": { + "version": "4.0.0", + "dev": true, + "inBundle": true, + "license": "MIT" + }, + "node_modules/@ui5/cli/node_modules/js-yaml": { + "version": "4.3.2", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/nodeca" + } + ], + "inBundle": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/@ui5/cli/node_modules/js2xmlparser": { + "version": "4.0.2", + "dev": true, + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "xmlcreate": "^2.0.4" + } + }, + "node_modules/@ui5/cli/node_modules/jsdoc": { + "version": "4.0.5", + "dev": true, + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "@babel/parser": "^7.20.15", + "@jsdoc/salty": "^0.2.1", + "@types/markdown-it": "^14.1.1", + "bluebird": "^3.7.2", + "catharsis": "^0.9.0", + "escape-string-regexp": "^2.0.0", + "js2xmlparser": "^4.0.2", + "klaw": "^3.0.0", + "markdown-it": "^14.1.0", + "markdown-it-anchor": "^8.6.7", + "marked": "^4.0.10", + "mkdirp": "^1.0.4", + "requizzle": "^0.2.3", + "strip-json-comments": "^3.1.0", + "underscore": "~1.13.2" + }, + "bin": { + "jsdoc": "jsdoc.js" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/@ui5/cli/node_modules/jsdoc/node_modules/escape-string-regexp": { + "version": "2.0.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@ui5/cli/node_modules/json-parse-even-better-errors": { + "version": "4.0.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/@ui5/cli/node_modules/json-schema-traverse": { + "version": "1.0.0", + "dev": true, + "inBundle": true, + "license": "MIT" + }, + "node_modules/@ui5/cli/node_modules/jsonparse": { + "version": "1.3.1", + "dev": true, + "engines": [ + "node >= 0.2.0" + ], + "inBundle": true, + "license": "MIT" + }, + "node_modules/@ui5/cli/node_modules/klaw": { + "version": "3.0.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.1.9" + } + }, + "node_modules/@ui5/cli/node_modules/ky": { + "version": "1.14.3", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sindresorhus/ky?sponsor=1" + } + }, + "node_modules/@ui5/cli/node_modules/latest-version": { + "version": "9.0.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "package-json": "^10.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@ui5/cli/node_modules/less-openui5": { + "version": "0.12.0", + "dev": true, + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "@adobe/css-tools": "^4.5.0", + "mime": "^1.6.0" + }, + "engines": { + "node": "^20.11.0 || >=22.0.0", + "npm": ">= 8" + } + }, + "node_modules/@ui5/cli/node_modules/linkify-it": { + "version": "5.0.2", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/markdown-it" + } + ], + "inBundle": true, + "license": "MIT", + "dependencies": { + "uc.micro": "^2.0.0" + } + }, + "node_modules/@ui5/cli/node_modules/locate-path": { + "version": "5.0.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@ui5/cli/node_modules/lockfile": { + "version": "1.0.4", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "signal-exit": "^3.0.2" + } + }, + "node_modules/@ui5/cli/node_modules/lockfile/node_modules/signal-exit": { + "version": "3.0.7", + "dev": true, + "inBundle": true, + "license": "ISC" + }, + "node_modules/@ui5/cli/node_modules/lodash": { + "version": "4.18.1", + "dev": true, + "inBundle": true, + "license": "MIT" + }, + "node_modules/@ui5/cli/node_modules/lodash.isfinite": { + "version": "3.3.2", + "dev": true, + "inBundle": true, + "license": "MIT" + }, + "node_modules/@ui5/cli/node_modules/make-fetch-happen": { + "version": "14.0.3", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "@npmcli/agent": "^3.0.0", + "cacache": "^19.0.1", + "http-cache-semantics": "^4.1.1", + "minipass": "^7.0.2", + "minipass-fetch": "^4.0.0", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.4", + "negotiator": "^1.0.0", + "proc-log": "^5.0.0", + "promise-retry": "^2.0.1", + "ssri": "^12.0.0" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/@ui5/cli/node_modules/make-fetch-happen/node_modules/@npmcli/fs": { + "version": "4.0.0", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "semver": "^7.3.5" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/@ui5/cli/node_modules/make-fetch-happen/node_modules/cacache": { + "version": "19.0.1", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "@npmcli/fs": "^4.0.0", + "fs-minipass": "^3.0.0", + "glob": "^10.2.2", + "lru-cache": "^10.0.1", + "minipass": "^7.0.3", + "minipass-collect": "^2.0.1", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.4", + "p-map": "^7.0.2", + "ssri": "^12.0.0", + "tar": "^7.4.3", + "unique-filename": "^4.0.0" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/@ui5/cli/node_modules/make-fetch-happen/node_modules/lru-cache": { + "version": "10.4.3", + "dev": true, + "inBundle": true, + "license": "ISC" + }, + "node_modules/@ui5/cli/node_modules/make-fetch-happen/node_modules/negotiator": { + "version": "1.1.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "content-type": "^2.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/@ui5/cli/node_modules/make-fetch-happen/node_modules/ssri": { + "version": "12.0.0", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "minipass": "^7.0.3" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/@ui5/cli/node_modules/make-fetch-happen/node_modules/unique-filename": { + "version": "4.0.0", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "unique-slug": "^5.0.0" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/@ui5/cli/node_modules/make-fetch-happen/node_modules/unique-slug": { + "version": "5.0.0", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "imurmurhash": "^0.1.4" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/@ui5/cli/node_modules/markdown-it": { + "version": "14.3.2", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/markdown-it" + } + ], + "inBundle": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1", + "entities": "^4.5.0", + "linkify-it": "^5.0.2", + "mdurl": "^2.0.0", + "punycode.js": "^2.3.1", + "uc.micro": "^2.1.0" + }, + "bin": { + "markdown-it": "bin/markdown-it.mjs" + } + }, + "node_modules/@ui5/cli/node_modules/markdown-it-anchor": { + "version": "8.6.7", + "dev": true, + "inBundle": true, + "license": "Unlicense", + "peerDependencies": { + "@types/markdown-it": "*", + "markdown-it": "*" + } + }, + "node_modules/@ui5/cli/node_modules/marked": { + "version": "4.3.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "bin": { + "marked": "bin/marked.js" + }, + "engines": { + "node": ">= 12" + } + }, + "node_modules/@ui5/cli/node_modules/math-intrinsics": { + "version": "1.1.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/@ui5/cli/node_modules/mdurl": { + "version": "2.1.0", + "dev": true, + "inBundle": true, + "license": "MIT" + }, + "node_modules/@ui5/cli/node_modules/media-typer": { + "version": "1.1.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/@ui5/cli/node_modules/merge-descriptors": { + "version": "1.0.3", + "dev": true, + "inBundle": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@ui5/cli/node_modules/merge2": { + "version": "1.4.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@ui5/cli/node_modules/methods": { + "version": "1.1.2", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/@ui5/cli/node_modules/micromatch": { + "version": "4.0.8", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/@ui5/cli/node_modules/micromatch/node_modules/picomatch": { + "version": "2.3.2", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/@ui5/cli/node_modules/mime": { + "version": "1.6.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@ui5/cli/node_modules/mime-db": { + "version": "1.54.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/@ui5/cli/node_modules/mime-types": { + "version": "2.1.35", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/@ui5/cli/node_modules/mime-types/node_modules/mime-db": { + "version": "1.52.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/@ui5/cli/node_modules/minimalistic-assert": { + "version": "1.0.1", + "dev": true, + "inBundle": true, + "license": "ISC" + }, + "node_modules/@ui5/cli/node_modules/minimatch": { + "version": "10.2.6", + "dev": true, + "inBundle": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.8" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@ui5/cli/node_modules/minimist": { + "version": "1.2.8", + "dev": true, + "inBundle": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/@ui5/cli/node_modules/minipass": { + "version": "7.1.3", + "dev": true, + "inBundle": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/@ui5/cli/node_modules/minipass-collect": { + "version": "2.0.1", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "minipass": "^7.0.3" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/@ui5/cli/node_modules/minipass-fetch": { + "version": "4.0.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "minipass": "^7.0.3", + "minipass-sized": "^1.0.3", + "minizlib": "^3.0.1" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + }, + "optionalDependencies": { + "encoding": "^0.1.13" + } + }, + "node_modules/@ui5/cli/node_modules/minipass-flush": { + "version": "1.0.7", + "dev": true, + "inBundle": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@ui5/cli/node_modules/minipass-flush/node_modules/minipass": { + "version": "3.3.6", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@ui5/cli/node_modules/minipass-flush/node_modules/yallist": { + "version": "4.0.0", + "dev": true, + "inBundle": true, + "license": "ISC" + }, + "node_modules/@ui5/cli/node_modules/minipass-pipeline": { + "version": "1.2.4", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@ui5/cli/node_modules/minipass-pipeline/node_modules/minipass": { + "version": "3.3.6", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@ui5/cli/node_modules/minipass-pipeline/node_modules/yallist": { + "version": "4.0.0", + "dev": true, + "inBundle": true, + "license": "ISC" + }, + "node_modules/@ui5/cli/node_modules/minipass-sized": { + "version": "1.0.3", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@ui5/cli/node_modules/minipass-sized/node_modules/minipass": { + "version": "3.3.6", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@ui5/cli/node_modules/minipass-sized/node_modules/yallist": { + "version": "4.0.0", + "dev": true, + "inBundle": true, + "license": "ISC" + }, + "node_modules/@ui5/cli/node_modules/minizlib": { + "version": "3.1.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "minipass": "^7.1.2" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@ui5/cli/node_modules/mkdirp": { + "version": "1.0.4", + "dev": true, + "inBundle": true, + "license": "MIT", + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@ui5/cli/node_modules/ms": { + "version": "2.1.3", + "dev": true, + "inBundle": true, + "license": "MIT" + }, + "node_modules/@ui5/cli/node_modules/negotiator": { + "version": "0.6.4", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/@ui5/cli/node_modules/node-stream-zip": { + "version": "1.16.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/antelle" + } + }, + "node_modules/@ui5/cli/node_modules/nopt": { + "version": "8.1.0", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "abbrev": "^3.0.0" + }, + "bin": { + "nopt": "bin/nopt.js" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/@ui5/cli/node_modules/normalize-package-data": { + "version": "6.0.2", + "dev": true, + "inBundle": true, + "license": "BSD-2-Clause", + "dependencies": { + "hosted-git-info": "^7.0.0", + "semver": "^7.3.5", + "validate-npm-package-license": "^3.0.4" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/@ui5/cli/node_modules/normalize-package-data/node_modules/hosted-git-info": { + "version": "7.0.2", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "lru-cache": "^10.0.1" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/@ui5/cli/node_modules/normalize-package-data/node_modules/lru-cache": { + "version": "10.4.3", + "dev": true, + "inBundle": true, + "license": "ISC" + }, + "node_modules/@ui5/cli/node_modules/npm-packlist": { + "version": "9.0.0", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "ignore-walk": "^7.0.0" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/@ui5/cli/node_modules/npm-pick-manifest": { + "version": "10.0.0", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "npm-install-checks": "^7.1.0", + "npm-normalize-package-bin": "^4.0.0", + "npm-package-arg": "^12.0.0", + "semver": "^7.3.5" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/@ui5/cli/node_modules/npm-pick-manifest/node_modules/npm-install-checks": { + "version": "7.1.2", + "dev": true, + "inBundle": true, + "license": "BSD-2-Clause", + "dependencies": { + "semver": "^7.1.1" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/@ui5/cli/node_modules/npm-pick-manifest/node_modules/npm-normalize-package-bin": { + "version": "4.0.0", + "dev": true, + "inBundle": true, + "license": "ISC", + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/@ui5/cli/node_modules/npm-pick-manifest/node_modules/npm-package-arg": { + "version": "12.0.2", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "hosted-git-info": "^8.0.0", + "proc-log": "^5.0.0", + "semver": "^7.3.5", + "validate-npm-package-name": "^6.0.0" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/@ui5/cli/node_modules/npm-pick-manifest/node_modules/validate-npm-package-name": { + "version": "6.0.2", + "dev": true, + "inBundle": true, + "license": "ISC", + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/@ui5/cli/node_modules/nth-check": { + "version": "2.1.1", + "dev": true, + "inBundle": true, + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0" + }, + "funding": { + "url": "https://github.com/fb55/nth-check?sponsor=1" + } + }, + "node_modules/@ui5/cli/node_modules/object-assign": { + "version": "4.1.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/@ui5/cli/node_modules/object-inspect": { + "version": "1.13.4", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/@ui5/cli/node_modules/obuf": { + "version": "1.1.2", + "dev": true, + "inBundle": true, + "license": "MIT" + }, + "node_modules/@ui5/cli/node_modules/on-finished": { + "version": "2.4.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/@ui5/cli/node_modules/on-headers": { + "version": "1.1.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/@ui5/cli/node_modules/open": { + "version": "11.0.3", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "default-browser": "^5.5.1", + "define-lazy-prop": "^3.0.0", + "is-in-ssh": "^1.0.0", + "is-inside-container": "^1.0.0", + "powershell-utils": "^0.2.1", + "wsl-utils": "^1.0.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@ui5/cli/node_modules/p-limit": { + "version": "2.3.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@ui5/cli/node_modules/p-locate": { + "version": "4.1.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@ui5/cli/node_modules/p-map": { + "version": "7.0.8", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@ui5/cli/node_modules/p-try": { + "version": "2.2.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/@ui5/cli/node_modules/package-json": { + "version": "10.0.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "ky": "^1.2.0", + "registry-auth-token": "^5.0.2", + "registry-url": "^6.0.1", + "semver": "^7.6.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@ui5/cli/node_modules/package-json-from-dist": { + "version": "1.0.1", + "dev": true, + "inBundle": true, + "license": "BlueOak-1.0.0" + }, + "node_modules/@ui5/cli/node_modules/pacote": { + "version": "19.0.2", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "@npmcli/git": "^6.0.0", + "@npmcli/installed-package-contents": "^3.0.0", + "@npmcli/package-json": "^6.0.0", + "@npmcli/promise-spawn": "^8.0.0", + "@npmcli/run-script": "^9.0.0", + "cacache": "^19.0.0", + "fs-minipass": "^3.0.0", + "minipass": "^7.0.2", + "npm-package-arg": "^12.0.0", + "npm-packlist": "^9.0.0", + "npm-pick-manifest": "^10.0.0", + "npm-registry-fetch": "^18.0.0", + "proc-log": "^5.0.0", + "promise-retry": "^2.0.1", + "sigstore": "^3.0.0", + "ssri": "^12.0.0", + "tar": "^7.5.10" + }, + "bin": { + "pacote": "bin/index.js" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/@ui5/cli/node_modules/pacote/node_modules/@npmcli/fs": { + "version": "4.0.0", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "semver": "^7.3.5" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/@ui5/cli/node_modules/pacote/node_modules/@npmcli/installed-package-contents": { + "version": "3.0.0", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "npm-bundled": "^4.0.0", + "npm-normalize-package-bin": "^4.0.0" + }, + "bin": { + "installed-package-contents": "bin/index.js" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/@ui5/cli/node_modules/pacote/node_modules/@npmcli/node-gyp": { + "version": "4.0.0", + "dev": true, + "inBundle": true, + "license": "ISC", + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/@ui5/cli/node_modules/pacote/node_modules/@npmcli/redact": { + "version": "3.2.2", + "dev": true, + "inBundle": true, + "license": "ISC", + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/@ui5/cli/node_modules/pacote/node_modules/@npmcli/run-script": { + "version": "9.1.0", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "@npmcli/node-gyp": "^4.0.0", + "@npmcli/package-json": "^6.0.0", + "@npmcli/promise-spawn": "^8.0.0", + "node-gyp": "^11.0.0", + "proc-log": "^5.0.0", + "which": "^5.0.0" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/@ui5/cli/node_modules/pacote/node_modules/cacache": { + "version": "19.0.1", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "@npmcli/fs": "^4.0.0", + "fs-minipass": "^3.0.0", + "glob": "^10.2.2", + "lru-cache": "^10.0.1", + "minipass": "^7.0.3", + "minipass-collect": "^2.0.1", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.4", + "p-map": "^7.0.2", + "ssri": "^12.0.0", + "tar": "^7.4.3", + "unique-filename": "^4.0.0" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/@ui5/cli/node_modules/pacote/node_modules/lru-cache": { + "version": "10.4.3", + "dev": true, + "inBundle": true, + "license": "ISC" + }, + "node_modules/@ui5/cli/node_modules/pacote/node_modules/node-gyp": { + "version": "11.5.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "env-paths": "^2.2.0", + "exponential-backoff": "^3.1.1", + "graceful-fs": "^4.2.6", + "make-fetch-happen": "^14.0.3", + "nopt": "^8.0.0", + "proc-log": "^5.0.0", + "semver": "^7.3.5", + "tar": "^7.4.3", + "tinyglobby": "^0.2.12", + "which": "^5.0.0" + }, + "bin": { + "node-gyp": "bin/node-gyp.js" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/@ui5/cli/node_modules/pacote/node_modules/npm-bundled": { + "version": "4.0.0", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "npm-normalize-package-bin": "^4.0.0" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/@ui5/cli/node_modules/pacote/node_modules/npm-normalize-package-bin": { + "version": "4.0.0", + "dev": true, + "inBundle": true, + "license": "ISC", + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/@ui5/cli/node_modules/pacote/node_modules/npm-package-arg": { + "version": "12.0.2", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "hosted-git-info": "^8.0.0", + "proc-log": "^5.0.0", + "semver": "^7.3.5", + "validate-npm-package-name": "^6.0.0" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/@ui5/cli/node_modules/pacote/node_modules/npm-registry-fetch": { + "version": "18.0.2", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "@npmcli/redact": "^3.0.0", + "jsonparse": "^1.3.1", + "make-fetch-happen": "^14.0.0", + "minipass": "^7.0.2", + "minipass-fetch": "^4.0.0", + "minizlib": "^3.0.1", + "npm-package-arg": "^12.0.0", + "proc-log": "^5.0.0" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/@ui5/cli/node_modules/pacote/node_modules/ssri": { + "version": "12.0.0", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "minipass": "^7.0.3" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/@ui5/cli/node_modules/pacote/node_modules/unique-filename": { + "version": "4.0.0", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "unique-slug": "^5.0.0" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/@ui5/cli/node_modules/pacote/node_modules/unique-slug": { + "version": "5.0.0", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "imurmurhash": "^0.1.4" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/@ui5/cli/node_modules/pacote/node_modules/validate-npm-package-name": { + "version": "6.0.2", + "dev": true, + "inBundle": true, + "license": "ISC", + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/@ui5/cli/node_modules/parse-json": { + "version": "8.3.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.26.2", + "index-to-position": "^1.1.0", + "type-fest": "^4.39.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@ui5/cli/node_modules/parse-json/node_modules/type-fest": { + "version": "4.41.0", + "dev": true, + "inBundle": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@ui5/cli/node_modules/parse5": { + "version": "7.3.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "entities": "^6.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/@ui5/cli/node_modules/parse5-htmlparser2-tree-adapter": { + "version": "7.1.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "domhandler": "^5.0.3", + "parse5": "^7.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/@ui5/cli/node_modules/parse5-parser-stream": { + "version": "7.1.2", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "parse5": "^7.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/@ui5/cli/node_modules/parse5/node_modules/entities": { + "version": "6.0.1", + "dev": true, + "inBundle": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/@ui5/cli/node_modules/parseurl": { + "version": "1.3.3", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/@ui5/cli/node_modules/path-exists": { + "version": "4.0.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@ui5/cli/node_modules/path-key": { + "version": "3.1.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@ui5/cli/node_modules/path-parse": { + "version": "1.0.7", + "dev": true, + "inBundle": true, + "license": "MIT" + }, + "node_modules/@ui5/cli/node_modules/path-scurry": { + "version": "1.11.1", + "dev": true, + "inBundle": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@ui5/cli/node_modules/path-scurry/node_modules/lru-cache": { + "version": "10.4.3", + "dev": true, + "inBundle": true, + "license": "ISC" + }, + "node_modules/@ui5/cli/node_modules/path-to-regexp": { + "version": "0.1.13", + "dev": true, + "inBundle": true, + "license": "MIT" + }, + "node_modules/@ui5/cli/node_modules/path-type": { + "version": "6.0.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@ui5/cli/node_modules/picocolors": { + "version": "1.1.1", + "dev": true, + "inBundle": true, + "license": "ISC" + }, + "node_modules/@ui5/cli/node_modules/picomatch": { + "version": "4.0.7", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/@ui5/cli/node_modules/portscanner": { + "version": "2.2.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "async": "^2.6.0", + "is-number-like": "^1.0.3" + }, + "engines": { + "node": ">=0.4", + "npm": ">=1.0.0" + } + }, + "node_modules/@ui5/cli/node_modules/powershell-utils": { + "version": "0.2.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@ui5/cli/node_modules/pretty-data": { + "version": "0.40.0", + "dev": true, + "inBundle": true, + "license": "MIT" + }, + "node_modules/@ui5/cli/node_modules/pretty-hrtime": { + "version": "1.0.3", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/@ui5/cli/node_modules/proc-log": { + "version": "5.0.0", + "dev": true, + "inBundle": true, + "license": "ISC", + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/@ui5/cli/node_modules/process-nextick-args": { + "version": "2.0.1", + "dev": true, + "inBundle": true, + "license": "MIT" + }, + "node_modules/@ui5/cli/node_modules/promise-retry": { + "version": "2.0.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "err-code": "^2.0.2", + "retry": "^0.12.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@ui5/cli/node_modules/proto-list": { + "version": "1.2.4", + "dev": true, + "inBundle": true, + "license": "ISC" + }, + "node_modules/@ui5/cli/node_modules/proxy-addr": { + "version": "2.0.7", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/@ui5/cli/node_modules/punycode.js": { + "version": "2.3.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/@ui5/cli/node_modules/pupa": { + "version": "3.3.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "escape-goat": "^4.0.0" + }, + "engines": { + "node": ">=12.20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@ui5/cli/node_modules/qs": { + "version": "6.16.0", + "dev": true, + "inBundle": true, + "license": "BSD-3-Clause", + "dependencies": { + "es-define-property": "^1.0.1", + "side-channel": "^1.1.1" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/@ui5/cli/node_modules/queue-microtask": { + "version": "1.2.3", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "inBundle": true, + "license": "MIT" + }, + "node_modules/@ui5/cli/node_modules/random-int": { + "version": "3.1.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@ui5/cli/node_modules/range-parser": { + "version": "1.2.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/@ui5/cli/node_modules/raw-body": { + "version": "3.0.2", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.7.0", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/@ui5/cli/node_modules/rc": { + "version": "1.2.8", + "dev": true, + "inBundle": true, + "license": "(BSD-2-Clause OR MIT OR Apache-2.0)", + "dependencies": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + }, + "bin": { + "rc": "cli.js" + } + }, + "node_modules/@ui5/cli/node_modules/rc/node_modules/ini": { + "version": "1.3.8", + "dev": true, + "inBundle": true, + "license": "ISC" + }, + "node_modules/@ui5/cli/node_modules/rc/node_modules/strip-json-comments": { + "version": "2.0.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/@ui5/cli/node_modules/read-package-up": { + "version": "11.0.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "find-up-simple": "^1.0.0", + "read-pkg": "^9.0.0", + "type-fest": "^4.6.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@ui5/cli/node_modules/read-package-up/node_modules/type-fest": { + "version": "4.41.0", + "dev": true, + "inBundle": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@ui5/cli/node_modules/read-pkg": { + "version": "9.0.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "@types/normalize-package-data": "^2.4.3", + "normalize-package-data": "^6.0.0", + "parse-json": "^8.0.0", + "type-fest": "^4.6.0", + "unicorn-magic": "^0.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@ui5/cli/node_modules/read-pkg/node_modules/type-fest": { + "version": "4.41.0", + "dev": true, + "inBundle": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@ui5/cli/node_modules/read-pkg/node_modules/unicorn-magic": { + "version": "0.1.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@ui5/cli/node_modules/readable-stream": { + "version": "2.3.8", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/@ui5/cli/node_modules/readable-stream/node_modules/safe-buffer": { + "version": "5.1.2", + "dev": true, + "inBundle": true, + "license": "MIT" + }, + "node_modules/@ui5/cli/node_modules/registry-auth-token": { + "version": "5.1.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "@pnpm/npm-conf": "^3.0.2" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/@ui5/cli/node_modules/registry-url": { + "version": "6.0.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "rc": "1.2.8" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@ui5/cli/node_modules/replacestream": { + "version": "4.0.3", + "dev": true, + "inBundle": true, + "license": "BSD-3-Clause", + "dependencies": { + "escape-string-regexp": "^1.0.3", + "object-assign": "^4.0.1", + "readable-stream": "^2.0.2" + } + }, + "node_modules/@ui5/cli/node_modules/replacestream/node_modules/escape-string-regexp": { + "version": "1.0.5", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/@ui5/cli/node_modules/require-directory": { + "version": "2.1.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/@ui5/cli/node_modules/require-from-string": { + "version": "2.0.2", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/@ui5/cli/node_modules/requizzle": { + "version": "0.2.4", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "lodash": "^4.17.21" + } + }, + "node_modules/@ui5/cli/node_modules/resolve": { + "version": "1.22.12", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "is-core-module": "^2.16.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/@ui5/cli/node_modules/resolve-cwd": { + "version": "3.0.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@ui5/cli/node_modules/resolve-from": { + "version": "5.0.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@ui5/cli/node_modules/retry": { + "version": "0.12.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/@ui5/cli/node_modules/reusify": { + "version": "1.1.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/@ui5/cli/node_modules/router": { + "version": "2.2.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "depd": "^2.0.0", + "is-promise": "^4.0.0", + "parseurl": "^1.3.3", + "path-to-regexp": "^8.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@ui5/cli/node_modules/router/node_modules/path-to-regexp": { + "version": "8.4.2", + "dev": true, + "inBundle": true, + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/@ui5/cli/node_modules/run-applescript": { + "version": "7.1.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@ui5/cli/node_modules/run-parallel": { + "version": "1.2.0", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "inBundle": true, + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/@ui5/cli/node_modules/safe-buffer": { + "version": "5.2.1", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "inBundle": true, + "license": "MIT" + }, + "node_modules/@ui5/cli/node_modules/safer-buffer": { + "version": "2.1.2", + "dev": true, + "inBundle": true, + "license": "MIT" + }, + "node_modules/@ui5/cli/node_modules/sax": { + "version": "1.6.1", + "dev": true, + "inBundle": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=11.0.0" + } + }, + "node_modules/@ui5/cli/node_modules/select-hose": { + "version": "2.0.0", + "dev": true, + "inBundle": true, + "license": "MIT" + }, + "node_modules/@ui5/cli/node_modules/semver": { + "version": "7.8.5", + "dev": true, + "inBundle": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@ui5/cli/node_modules/send": { + "version": "0.19.2", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "~0.5.2", + "http-errors": "~2.0.1", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "~2.4.1", + "range-parser": "~1.2.1", + "statuses": "~2.0.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/@ui5/cli/node_modules/send/node_modules/debug": { + "version": "2.6.9", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/@ui5/cli/node_modules/send/node_modules/debug/node_modules/ms": { + "version": "2.0.0", + "dev": true, + "inBundle": true, + "license": "MIT" + }, + "node_modules/@ui5/cli/node_modules/serve-static": { + "version": "1.16.3", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "~0.19.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/@ui5/cli/node_modules/setprototypeof": { + "version": "1.2.0", + "dev": true, + "inBundle": true, + "license": "ISC" + }, + "node_modules/@ui5/cli/node_modules/shebang-command": { + "version": "2.0.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@ui5/cli/node_modules/shebang-regex": { + "version": "3.0.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@ui5/cli/node_modules/side-channel": { + "version": "1.1.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4", + "side-channel-list": "^1.0.1", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/@ui5/cli/node_modules/side-channel-list": { + "version": "1.0.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/@ui5/cli/node_modules/side-channel-map": { + "version": "1.0.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/@ui5/cli/node_modules/side-channel-weakmap": { + "version": "1.0.2", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/@ui5/cli/node_modules/signal-exit": { + "version": "4.1.0", + "dev": true, + "inBundle": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@ui5/cli/node_modules/sigstore": { + "version": "3.1.0", + "dev": true, + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "@sigstore/bundle": "^3.1.0", + "@sigstore/core": "^2.0.0", + "@sigstore/protobuf-specs": "^0.4.0", + "@sigstore/sign": "^3.1.0", + "@sigstore/tuf": "^3.1.0", + "@sigstore/verify": "^2.1.0" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/@ui5/cli/node_modules/slash": { + "version": "5.1.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@ui5/cli/node_modules/smart-buffer": { + "version": "4.2.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">= 6.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/@ui5/cli/node_modules/socks": { + "version": "2.8.10", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "ip-address": "^10.1.1", + "smart-buffer": "^4.2.0" + }, + "engines": { + "node": ">= 10.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/@ui5/cli/node_modules/socks-proxy-agent": { + "version": "8.0.5", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "^4.3.4", + "socks": "^2.8.3" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/@ui5/cli/node_modules/source-map": { + "version": "0.6.1", + "dev": true, + "inBundle": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/@ui5/cli/node_modules/source-map-support": { + "version": "0.5.21", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/@ui5/cli/node_modules/spdx-correct": { + "version": "3.2.0", + "dev": true, + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "spdx-expression-parse": "^3.0.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/@ui5/cli/node_modules/spdx-correct/node_modules/spdx-expression-parse": { + "version": "3.0.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "spdx-exceptions": "^2.1.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/@ui5/cli/node_modules/spdx-exceptions": { + "version": "2.5.0", + "dev": true, + "inBundle": true, + "license": "CC-BY-3.0" + }, + "node_modules/@ui5/cli/node_modules/spdx-license-ids": { + "version": "3.0.23", + "dev": true, + "inBundle": true, + "license": "CC0-1.0" + }, + "node_modules/@ui5/cli/node_modules/spdy": { + "version": "4.0.2", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "debug": "^4.1.0", + "handle-thing": "^2.0.0", + "http-deceiver": "^1.2.7", + "select-hose": "^2.0.0", + "spdy-transport": "^3.0.0" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@ui5/cli/node_modules/spdy-transport": { + "version": "3.0.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "debug": "^4.1.0", + "detect-node": "^2.0.4", + "hpack.js": "^2.1.6", + "obuf": "^1.1.2", + "readable-stream": "^3.0.6", + "wbuf": "^1.7.3" + } + }, + "node_modules/@ui5/cli/node_modules/spdy-transport/node_modules/readable-stream": { + "version": "3.6.2", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/@ui5/cli/node_modules/statuses": { + "version": "2.0.2", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/@ui5/cli/node_modules/string_decoder": { + "version": "1.1.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/@ui5/cli/node_modules/string_decoder/node_modules/safe-buffer": { + "version": "5.1.2", + "dev": true, + "inBundle": true, + "license": "MIT" + }, + "node_modules/@ui5/cli/node_modules/string-width": { + "version": "4.2.3", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@ui5/cli/node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@ui5/cli/node_modules/string-width-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@ui5/cli/node_modules/string-width-cjs/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@ui5/cli/node_modules/string-width-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@ui5/cli/node_modules/string-width/node_modules/ansi-regex": { + "version": "5.0.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@ui5/cli/node_modules/string-width/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@ui5/cli/node_modules/string-width/node_modules/strip-ansi": { + "version": "6.0.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@ui5/cli/node_modules/strip-ansi": { + "version": "7.2.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.2.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/@ui5/cli/node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@ui5/cli/node_modules/strip-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@ui5/cli/node_modules/strip-json-comments": { + "version": "3.1.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@ui5/cli/node_modules/stubborn-fs": { + "version": "2.0.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "stubborn-utils": "^1.0.1" + } + }, + "node_modules/@ui5/cli/node_modules/stubborn-utils": { + "version": "1.0.2", + "dev": true, + "inBundle": true, + "license": "MIT" + }, + "node_modules/@ui5/cli/node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/@ui5/cli/node_modules/tar": { + "version": "7.5.22", + "dev": true, + "inBundle": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/fs-minipass": "^4.0.0", + "chownr": "^3.0.0", + "minipass": "^7.1.2", + "minizlib": "^3.1.0", + "yallist": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@ui5/cli/node_modules/tar/node_modules/yallist": { + "version": "5.0.0", + "dev": true, + "inBundle": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/@ui5/cli/node_modules/terser": { + "version": "5.51.2", + "dev": true, + "inBundle": true, + "license": "BSD-2-Clause", + "dependencies": { + "@jridgewell/source-map": "^0.3.3", + "acorn": "^8.15.0", + "commander": "^2.20.0", + "source-map-support": "~0.5.20" + }, + "bin": { + "terser": "bin/terser" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@ui5/cli/node_modules/tinyglobby": { + "version": "0.2.17", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/@ui5/cli/node_modules/to-regex-range": { + "version": "5.0.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/@ui5/cli/node_modules/toidentifier": { + "version": "1.0.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/@ui5/cli/node_modules/tuf-js": { + "version": "3.1.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "@tufjs/models": "3.0.1", + "debug": "^4.4.1", + "make-fetch-happen": "^14.0.3" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/@ui5/cli/node_modules/type-is": { + "version": "2.1.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "content-type": "^2.0.0", + "media-typer": "^1.1.0", + "mime-types": "^3.0.0" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/@ui5/cli/node_modules/type-is/node_modules/mime-types": { + "version": "3.0.2", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/@ui5/cli/node_modules/uc.micro": { + "version": "2.1.0", + "dev": true, + "inBundle": true, + "license": "MIT" + }, + "node_modules/@ui5/cli/node_modules/underscore": { + "version": "1.13.8", + "dev": true, + "inBundle": true, + "license": "MIT" + }, + "node_modules/@ui5/cli/node_modules/undici": { + "version": "6.28.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=18.17" + } + }, + "node_modules/@ui5/cli/node_modules/unicorn-magic": { + "version": "0.4.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@ui5/cli/node_modules/unpipe": { + "version": "1.0.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/@ui5/cli/node_modules/update-notifier": { + "version": "7.3.1", + "dev": true, + "inBundle": true, + "license": "BSD-2-Clause", + "dependencies": { + "boxen": "^8.0.1", + "chalk": "^5.3.0", + "configstore": "^7.0.0", + "is-in-ci": "^1.0.0", + "is-installed-globally": "^1.0.0", + "is-npm": "^6.0.0", + "latest-version": "^9.0.0", + "pupa": "^3.1.0", + "semver": "^7.6.3", + "xdg-basedir": "^5.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/yeoman/update-notifier?sponsor=1" + } + }, + "node_modules/@ui5/cli/node_modules/util-deprecate": { + "version": "1.0.2", + "dev": true, + "inBundle": true, + "license": "MIT" + }, + "node_modules/@ui5/cli/node_modules/utils-merge": { + "version": "1.0.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/@ui5/cli/node_modules/validate-npm-package-license": { + "version": "3.0.4", + "dev": true, + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "spdx-correct": "^3.0.0", + "spdx-expression-parse": "^3.0.0" + } + }, + "node_modules/@ui5/cli/node_modules/validate-npm-package-license/node_modules/spdx-expression-parse": { + "version": "3.0.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "spdx-exceptions": "^2.1.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/@ui5/cli/node_modules/vary": { + "version": "1.1.2", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/@ui5/cli/node_modules/walk-up-path": { + "version": "3.0.1", + "dev": true, + "inBundle": true, + "license": "ISC" + }, + "node_modules/@ui5/cli/node_modules/wbuf": { + "version": "1.7.3", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "minimalistic-assert": "^1.0.0" + } + }, + "node_modules/@ui5/cli/node_modules/whatwg-encoding": { + "version": "3.1.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "iconv-lite": "0.6.3" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@ui5/cli/node_modules/whatwg-encoding/node_modules/iconv-lite": { + "version": "0.6.3", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/@ui5/cli/node_modules/whatwg-mimetype": { + "version": "4.0.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@ui5/cli/node_modules/when-exit": { + "version": "2.1.5", + "dev": true, + "inBundle": true, + "license": "MIT" + }, + "node_modules/@ui5/cli/node_modules/which": { + "version": "5.0.0", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "isexe": "^3.1.1" + }, + "bin": { + "node-which": "bin/which.js" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/@ui5/cli/node_modules/widest-line": { + "version": "5.0.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "string-width": "^7.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@ui5/cli/node_modules/widest-line/node_modules/emoji-regex": { + "version": "10.6.0", + "dev": true, + "inBundle": true, + "license": "MIT" + }, + "node_modules/@ui5/cli/node_modules/widest-line/node_modules/string-width": { + "version": "7.2.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@ui5/cli/node_modules/workerpool": { + "version": "10.0.3", + "dev": true, + "inBundle": true, + "license": "Apache-2.0" + }, + "node_modules/@ui5/cli/node_modules/wrap-ansi": { + "version": "8.1.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/@ui5/cli/node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/@ui5/cli/node_modules/wrap-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@ui5/cli/node_modules/wrap-ansi-cjs/node_modules/ansi-styles": { + "version": "4.3.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@ui5/cli/node_modules/wrap-ansi-cjs/node_modules/color-convert": { + "version": "2.0.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/@ui5/cli/node_modules/wrap-ansi-cjs/node_modules/color-name": { + "version": "1.1.4", + "dev": true, + "inBundle": true, + "license": "MIT" + }, + "node_modules/@ui5/cli/node_modules/wrap-ansi-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@ui5/cli/node_modules/wrap-ansi/node_modules/emoji-regex": { + "version": "9.2.2", + "dev": true, + "inBundle": true, + "license": "MIT" + }, + "node_modules/@ui5/cli/node_modules/wrap-ansi/node_modules/string-width": { + "version": "5.1.2", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@ui5/cli/node_modules/wsl-utils": { + "version": "1.0.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "is-wsl": "^3.1.0", + "powershell-utils": "^0.1.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@ui5/cli/node_modules/wsl-utils/node_modules/powershell-utils": { + "version": "0.1.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@ui5/cli/node_modules/xdg-basedir": { + "version": "5.1.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@ui5/cli/node_modules/xml2js": { + "version": "0.6.2", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "sax": ">=0.6.0", + "xmlbuilder": "~11.0.0" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/@ui5/cli/node_modules/xmlbuilder": { + "version": "11.0.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/@ui5/cli/node_modules/xmlcreate": { + "version": "2.0.4", + "dev": true, + "inBundle": true, + "license": "Apache-2.0" + }, + "node_modules/@ui5/cli/node_modules/y18n": { + "version": "5.0.8", + "dev": true, + "inBundle": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/@ui5/cli/node_modules/yaml-ast-parser": { + "version": "0.0.43", + "dev": true, + "inBundle": true, + "license": "Apache-2.0" + }, + "node_modules/@ui5/cli/node_modules/yargs": { + "version": "17.7.3", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@ui5/cli/node_modules/yargs-parser": { + "version": "21.1.1", + "dev": true, + "inBundle": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/@ui5/cli/node_modules/yesno": { + "version": "0.4.0", + "dev": true, + "inBundle": true, + "license": "BSD" + }, + "node_modules/acorn": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.18.0.tgz", + "integrity": "sha512-lGq+9yr1/GuAWaVYIHRjvvySG5/4VfKIvC8EWxStPdcDh/Ka7FG3twP6v4d5BkravUilhIAsG4Qj83t02LWUPQ==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/ajv": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", + "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/array-flatten": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-3.0.0.tgz", + "integrity": "sha512-zPMVc3ZYlGLNk4mpK1NzP2wg0ml9t7fUgDsayR5Y5rSzxQilzR9FGu/EH2jQOcKSAeAfWeylyW8juy3OkWRvNA==", + "dev": true, + "license": "MIT" + }, + "node_modules/array-timsort": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/array-timsort/-/array-timsort-1.0.3.tgz", + "integrity": "sha512-/+3GRL7dDAGEfM6TseQk/U+mi18TU2Ms9I3UlLdUMhz2hbvGNTKdj9xniwXfUqgYhHxRx0+8UnKkvlNwVU+cWQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/babel-plugin-polyfill-corejs2": { + "version": "0.4.17", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.17.tgz", + "integrity": "sha512-aTyf30K/rqAsNwN76zYrdtx8obu0E4KoUME29B1xj+B3WxgvWkp943vYQ+z8Mv3lw9xHXMHpvSPOBxzAkIa94w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.28.6", + "@babel/helper-define-polyfill-provider": "^0.6.8", + "semver": "^6.3.1" + }, + "peerDependencies": { + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/babel-plugin-polyfill-corejs2/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/babel-plugin-polyfill-corejs3": { + "version": "0.14.2", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.14.2.tgz", + "integrity": "sha512-coWpDLJ410R781Npmn/SIBZEsAetR4xVi0SxLMXPaMO4lSf1MwnkGYMtkFxew0Dn8B3/CpbpYxN0JCgg8mn67g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-define-polyfill-provider": "^0.6.8", + "core-js-compat": "^3.48.0" + }, + "peerDependencies": { + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/babel-plugin-polyfill-regenerator": { + "version": "0.6.8", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.6.8.tgz", + "integrity": "sha512-M762rNHfSF1EV3SLtnCJXFoQbbIIz0OyRwnCmV0KPC7qosSfCO0QLTSuJX3ayAebubhE6oYBAYPrBA5ljowaZg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-define-polyfill-provider": "^0.6.8" + }, + "peerDependencies": { + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/babel-plugin-transform-async-to-promises": { + "version": "0.8.18", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-async-to-promises/-/babel-plugin-transform-async-to-promises-0.8.18.tgz", + "integrity": "sha512-WpOrF76nUHijnNn10eBGOHZmXQC8JYRME9rOLxStOga7Av2VO53ehVFvVNImMksVtQuL2/7ZNxEgxnx7oo/3Hw==", + "dev": true, + "license": "MIT" + }, + "node_modules/babel-plugin-transform-modules-ui5": { + "version": "7.10.0", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-modules-ui5/-/babel-plugin-transform-modules-ui5-7.10.0.tgz", + "integrity": "sha512-O4AGsF0G0yfndH62Vpm3zKzl2bLgeZrK2Geb9Hr4OYirR1XBwRpIfWjGEk38HL9bs7bn2id58THK4SAWwHkU3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "comment-parser": "1.4.7" + }, + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "@babel/core": "*" + } + }, + "node_modules/babel-plugin-transform-remove-console": { + "version": "6.9.4", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-remove-console/-/babel-plugin-transform-remove-console-6.9.4.tgz", + "integrity": "sha512-88blrUrMX3SPiGkT1GnvVY8E/7A+k6oj3MNvUtTIxJflFzXTw1bHkuJ/y039ouhFMp2prRn5cQGzokViYi1dsg==", + "dev": true, + "license": "MIT" + }, + "node_modules/babel-preset-transform-ui5": { + "version": "7.10.0", + "resolved": "https://registry.npmjs.org/babel-preset-transform-ui5/-/babel-preset-transform-ui5-7.10.0.tgz", + "integrity": "sha512-PTBFpO1IRZbmu/pKpppqobGJ8A5r2vcZza9b7xQQ5SlWKmGzizJxUcMfh3AkLNZE/0JouYp7mX18nRPHTi+vrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "babel-plugin-transform-modules-ui5": "^7.10.0" + }, + "engines": { + "node": ">=20.19.0" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/baseline-browser-mapping": { + "version": "2.11.23", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.11.23.tgz", + "integrity": "sha512-le521dGVfxM7yRX0EikCoSz+rOK+hHzdDt/E7mG1jOJB/6WAAUuwVroLwaB7ApaUsz5Q0kFlDXLSA9MheUIfRQ==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/body-parser": { + "version": "1.20.4", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.4.tgz", + "integrity": "sha512-ZTgYYLMOXY9qKU/57FAo8F+HA2dGX7bqGc71txDRC1rS4frdFI5R7NhluHxH6M0YItAP0sHB4uqAOcYKxO6uGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "content-type": "~1.0.5", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "~1.2.0", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "on-finished": "~2.4.1", + "qs": "~6.14.0", + "raw-body": "~2.5.3", + "type-is": "~1.6.18", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/body-parser/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/body-parser/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true, + "license": "MIT" + }, + "node_modules/brace-expansion": { + "version": "5.0.9", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.9.tgz", + "integrity": "sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/brace-expansion/node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.28.9", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.9.tgz", + "integrity": "sha512-EWazOblFYUvlGZcfGhPUPmYh3nikUxBVb+y9MJun5f3hBi812X+8MSQTujLBtgK3cf51fJWbWfOjyeO954d+Eg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.11.20", + "caniuse-lite": "^1.0.30001810", + "electron-to-chromium": "^1.5.420", + "node-releases": "^2.0.54", + "update-browserslist-db": "^1.3.2" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/cacheable": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/cacheable/-/cacheable-2.5.0.tgz", + "integrity": "sha512-60cyAOytib/OzBw1JNSoSV/boK1AtHryDIjvVBk7XbN4ugfkM3+Sry7fEjNgPMGgOjuaZPAp8ruZ0Cxafwyq9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@cacheable/memory": "^2.2.0", + "@cacheable/utils": "^2.5.0", + "hookified": "^1.15.0", + "keyv": "^5.6.0", + "qified": "^0.10.1" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001810", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001810.tgz", + "integrity": "sha512-TITQPUkaz+aVk5GL6NhOdwk1aEaNTSDPsGFWrTuhKGtjTF70jL/Oht2W4c6rXUe5fu7Ie19VIahAXHIIiWWNeg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/chevrotain": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/chevrotain/-/chevrotain-9.1.0.tgz", + "integrity": "sha512-A86/55so63HCfu0dgGg3j9u8uuuBOrSqly1OhBZxRu2x6sAKILLzfVjbGMw45kgier6lz45EzcjjWtTRgoT84Q==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@chevrotain/types": "^9.1.0", + "@chevrotain/utils": "^9.1.0", + "regexp-to-ast": "0.5.0" + } + }, + "node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/comment-json": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/comment-json/-/comment-json-5.0.0.tgz", + "integrity": "sha512-uiqLcOiVDJtBP8WGkZHEP+FZIhTzP1dxvn59EfoYUi9gqupjrBWVQkO2atDrbnKPwLeotFYDsuNb26uBMqB+hw==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-timsort": "^1.0.3", + "esprima": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/comment-parser": { + "version": "1.4.7", + "resolved": "https://registry.npmjs.org/comment-parser/-/comment-parser-1.4.7.tgz", + "integrity": "sha512-0h+uSNtQGW3D98eQt3jJ8L06Fves8hncB4V/PKdw/Qb8Hnk19VaKuTr55UNRYiSoVa7WwrFls+rh3ux9agmkeQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 12.0.0" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/core-js-compat": { + "version": "3.50.0", + "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.50.0.tgz", + "integrity": "sha512-XGpFGbMLHwSt74YLTKho7Ib242qi6O8MSX+sRokV4oz7iKXvQWGYZthjIhjRGMxjzVkAubBO512dKGYcefmX3Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "browserslist": "^4.28.7" + }, + "engines": { + "node": ">=6.4.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/core-js" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decode-uri-component": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.2.tgz", + "integrity": "sha512-FqUYQ+8o158GyGTrMFJms9qh3CqTKvAqgqsTnkLI8sKu0028orqBhxNMFkFen0zGyg6epACD32pjVk58ngIErQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10" + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "dev": true, + "license": "MIT" + }, + "node_modules/electron-to-chromium": { + "version": "1.5.427", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.427.tgz", + "integrity": "sha512-n14zb3FdsChZ2BNobqNHAJMcP3ifFv4paox2LvCrfVAQcqGiSURgbJl+PfMpHVCNFkStnNc+RRVtPBTVW5PDgw==", + "dev": true, + "license": "ISC" + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz", + "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "10.10.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-10.10.0.tgz", + "integrity": "sha512-NPXn6r5zl4uET1DAVPaOwzX3rut4c0wcmw3dWJAfOsTM5+TogXo0DDjz8pwm/hL8cyVNpHqeK4JpN0NjnyFFNw==", + "dev": true, + "license": "MIT", + "workspaces": [ + "packages/*" + ], + "dependencies": { + "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/regexpp": "^4.12.2", + "@eslint/config-array": "^0.23.5", + "@eslint/config-helpers": "^0.7.0", + "@eslint/core": "^1.2.1", + "@eslint/plugin-kit": "^0.7.3", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "ajv": "^6.14.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^9.1.2", + "eslint-visitor-keys": "^5.0.1", + "espree": "^11.2.0", + "esquery": "^1.7.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "11.1.5 || >11.1.6 <12", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "minimatch": "^10.2.5", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-scope": { + "version": "9.1.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-9.1.2.tgz", + "integrity": "sha512-xS90H51cKw0jltxmvmHy2Iai1LIqrfbw57b79w/J7MfvDfkIkFZ+kj6zC3BjtUwh150HsSSdxXZcsuv72miDFQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@types/esrecurse": "^4.3.1", + "@types/estree": "^1.0.8", + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/eslint-visitor-keys": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", + "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/eslint/node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/espree": { + "version": "11.2.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-11.2.0.tgz", + "integrity": "sha512-7p3DrVEIopW1B1avAGLuCSh1jubc01H2JHc8B4qqGblmg5gI9yumBgACjWo4JlIc04ufug4xJ3SQI8HkS/Rgzw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.16.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^5.0.1" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree/node_modules/eslint-visitor-keys": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", + "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true, + "license": "BSD-2-Clause", + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/esquery": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", + "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, + "node_modules/file-entry-cache": { + "version": "11.1.5", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-11.1.5.tgz", + "integrity": "sha512-+PFTHITI08JIGhnNpGNI8T8inUpgZfk3GNEqfT9R2zZV2iFXg3CvqzSl/uEhs7TSGujYRELEANyDvS8Fj7+S7Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^6.1.23" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/filter-obj": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/filter-obj/-/filter-obj-1.1.0.tgz", + "integrity": "sha512-8rXg1ZnX7xzy2NGDVkBVaAy+lSlPNwad13BtgSlLuxfIslyt5Vg64U7tFcCt4WS1R0hvtnQybT/IyCkGZ3DpXQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "6.1.23", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-6.1.23.tgz", + "integrity": "sha512-f++BY9pTk+983xK1FLzlLpmM0i0z+jHmx3QESGkURMXujQZz1k5wzwX6hjnQ8goaD0B+sYnDK1yZ6MTyZfUaqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "cacheable": "^2.5.0", + "flatted": "^3.4.2", + "hookified": "^1.15.0" + } + }, + "node_modules/flatted": { + "version": "3.4.4", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.4.tgz", + "integrity": "sha512-5+ybhBZANEJxaH3X5evAFatUxLfEHSr7n6kYJ+1Qd0mUqr4eu9gIf6GDbWHf8RJijHrjjO8G+la14SlL2SeS1Q==", + "dev": true, + "license": "ISC" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hashery": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/hashery/-/hashery-1.5.1.tgz", + "integrity": "sha512-iZyKG96/JwPz1N55vj2Ie2vXbhu440zfUfJvSwEqEbeLluk7NnapfGqa7LH0mOsnDxTF85Mx8/dyR6HfqcbmbQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "hookified": "^1.15.0" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/hasown": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/hookified": { + "version": "1.15.1", + "resolved": "https://registry.npmjs.org/hookified/-/hookified-1.15.1.tgz", + "integrity": "sha512-MvG/clsADq1GPM2KGo2nyfaWVyn9naPiXrqIe4jYjXNZQt238kWyOGrsyc/DmRAQ+Re6yeo6yX/yoNCG5KAEVg==", + "dev": true, + "license": "MIT" + }, + "node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "dev": true, + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ignore": { + "version": "7.0.9", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.9.tgz", + "integrity": "sha512-brTTsvFRt5C1gGHtPst/281UjPD5t9fBqbgoMPlVWy11ZLTPfu7HxK4ZYqO9H7o/yC9rSTCI85EaQ4OoY12qYw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-core-module": { + "version": "2.16.2", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.2.tgz", + "integrity": "sha512-evOr8xfXKxE6qSR0hSXL2r3sd7ALj8+7jQEUvPYcm5sgZFdJ+AYzT6yNmJenvIYQBgIGwfwz08sL8zoL7yq2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-promise": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", + "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.2.tgz", + "integrity": "sha512-SFNOvSJ+Dgf/9An904Yx+CgSlIPCkIpao4qo51lpee25TIRejdH3rhR4EZMGoNx3/TP3O+wzWuiTFl4sqbltzA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/nodeca" + } + ], + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/keyv": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-5.6.0.tgz", + "integrity": "sha512-CYDD3SOtsHtyXeEORYRx2qBtpDJFjRTGXUtmNEMGyzYOKj1TE3tycdlho7kA1Ufx9OYWZzg52QFBGALTirzDSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@keyv/serialize": "^1.1.1" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash.clonedeep": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz", + "integrity": "sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.debounce": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", + "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/minimatch": { + "version": "10.2.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.6.tgz", + "integrity": "sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.8" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-releases": { + "version": "2.0.55", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.55.tgz", + "integrity": "sha512-mIrE/Cw9y+9Au6dS5vDKDhQza9YvG6w+ZrS6X+ZzA7yFW/soAeaups4Qzn1bL6g5FVy8WtP79+0j82oPIbqRjQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "dev": true, + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true, + "license": "MIT" + }, + "node_modules/path-to-regexp": { + "version": "8.4.2", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.4.2.tgz", + "integrity": "sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA==", + "dev": true, + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/qified": { + "version": "0.10.1", + "resolved": "https://registry.npmjs.org/qified/-/qified-0.10.1.tgz", + "integrity": "sha512-+Owyggi9IxT1ePKGafcI87ubSmxol6smwJ+RAHDQlx9+9cPwFWDiKFFCPuWhr9ignlGpZ9vDQLw67N4dcTVFEA==", + "dev": true, + "license": "MIT", + "dependencies": { + "hookified": "^2.1.1" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/qified/node_modules/hookified": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/hookified/-/hookified-2.2.0.tgz", + "integrity": "sha512-p/LgFzRN5FeoD3DLS6bkUapeye6E4SI6yJs6KetENd18S+FBthqYq2amJUWpt5z0EQwwHemidjY5OqJGEKm5uA==", + "dev": true, + "license": "MIT" + }, + "node_modules/qs": { + "version": "6.14.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.2.tgz", + "integrity": "sha512-V/yCWTTF7VJ9hIh18Ugr2zhJMP01MY7c5kh4J870L7imm6/DIzBsNLTXzMwUA3yZ5b/KBqLx8Kp3uRvd7xSe3Q==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/query-string": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/query-string/-/query-string-7.1.3.tgz", + "integrity": "sha512-hh2WYhq4fi8+b+/2Kg9CEge4fDPvHS534aOOvOZeQ3+Vf2mCFsaFBYj0i+iXcAq6I9Vzp5fjMFBlONvayDC1qg==", + "dev": true, + "license": "MIT", + "dependencies": { + "decode-uri-component": "^0.2.2", + "filter-obj": "^1.1.0", + "split-on-first": "^1.0.0", + "strict-uri-encode": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/raw-body": { + "version": "2.5.3", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.3.tgz", + "integrity": "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==", + "dev": true, + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/regenerate": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz", + "integrity": "sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A==", + "dev": true, + "license": "MIT" + }, + "node_modules/regenerate-unicode-properties": { + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/regenerate-unicode-properties/-/regenerate-unicode-properties-10.2.2.tgz", + "integrity": "sha512-m03P+zhBeQd1RGnYxrGyDAPpWX/epKirLrp8e3qevZdVkKtnCrjjWczIbYc8+xd6vcTStVlqfycTx1KR4LOr0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "regenerate": "^1.4.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/regexp-to-ast": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/regexp-to-ast/-/regexp-to-ast-0.5.0.tgz", + "integrity": "sha512-tlbJqcMHnPKI9zSrystikWKwHkBqu2a/Sgw01h3zFjvYrMxEDYHzzoMZnUrbIfpTFEsoRnnviOXNCzFiSc54Qw==", + "dev": true, + "license": "MIT" + }, + "node_modules/regexpu-core": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-6.4.0.tgz", + "integrity": "sha512-0ghuzq67LI9bLXpOX/ISfve/Mq33a4aFRzoQYhnnok1JOFpmE/A2TBGkNVenOGEeSBCjIiWcc6MVOG5HEQv0sA==", + "dev": true, + "license": "MIT", + "dependencies": { + "regenerate": "^1.4.2", + "regenerate-unicode-properties": "^10.2.2", + "regjsgen": "^0.8.0", + "regjsparser": "^0.13.0", + "unicode-match-property-ecmascript": "^2.0.0", + "unicode-match-property-value-ecmascript": "^2.2.1" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/regjsgen": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/regjsgen/-/regjsgen-0.8.0.tgz", + "integrity": "sha512-RvwtGe3d7LvWiDQXeQw8p5asZUmfU1G/l6WbUXeHta7Y2PEIvBTwH6E2EfmYUK8pxcxEdEmaomqyp0vZZ7C+3Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/regjsparser": { + "version": "0.13.2", + "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.13.2.tgz", + "integrity": "sha512-NgRBy2Nx/bE+9F27nVHnqcN5HjyLmecqsqx2PJHu3/IEtADD4WuxuXIVExD5PoSDFVrl78dOonfcOe5O+5nbzQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "jsesc": "~3.1.0" + }, + "bin": { + "regjsparser": "bin/parser" + } + }, + "node_modules/resolve": { + "version": "1.22.12", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.12.tgz", + "integrity": "sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "is-core-module": "^2.16.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/router": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/router/-/router-2.0.0.tgz", + "integrity": "sha512-dIM5zVoG8xhC6rnSN8uoAgFARwTE7BQs8YwHEvK0VCmfxQXMaOuA1uiR1IPwsW7JyK5iTt7Od/TC9StasS2NPQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-flatten": "3.0.0", + "is-promise": "4.0.0", + "methods": "~1.1.2", + "parseurl": "~1.3.3", + "path-to-regexp": "^8.0.0", + "setprototypeof": "1.2.0", + "utils-merge": "1.0.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true, + "license": "MIT" + }, + "node_modules/sax": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.6.1.tgz", + "integrity": "sha512-42tBVwLWnaQvW5zc4HbZrTuWccECCZfBi92FDuwtqxasH+JbPB3/FOKb1m222K42R4WxuxzzMsTswfzgtSu64Q==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=11.0.0" + } + }, + "node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "dev": true, + "license": "ISC" + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/side-channel": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.1.tgz", + "integrity": "sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4", + "side-channel-list": "^1.0.1", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz", + "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/split-on-first": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/split-on-first/-/split-on-first-1.1.0.tgz", + "integrity": "sha512-43ZssAJaMusuKWL8sKUBQXHWOpq8d6CfN/u1p4gUzfJkM05C8rxTmYrkIPTXapZpORA6LkkzcUulJ8FqA7Uudw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/strict-uri-encode": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strict-uri-encode/-/strict-uri-encode-2.0.0.tgz", + "integrity": "sha512-QwiXZgpRcKkhTj2Scnn++4PKtWsH0kpzZ62L2R6c/LUVYv7hVnZqcg2+sMuT6R7Jusu1vviK/MFsu6kNJfWlEQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinyglobby/node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/tinyglobby/node_modules/picomatch": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.7.tgz", + "integrity": "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/ts-api-utils": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz", + "integrity": "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" + } + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/typescript-eslint": { + "version": "8.70.0", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.70.0.tgz", + "integrity": "sha512-P/W5cz70/cQAuKfY3xwQMWWTV7BvJ0mAQmi+9mBcsVPaBUpd6Ohpa+fECv9rBFrQcig86jAiNBFNWUqnTjr4pw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@typescript-eslint/eslint-plugin": "8.70.0", + "@typescript-eslint/parser": "8.70.0", + "@typescript-eslint/typescript-estree": "8.70.0", + "@typescript-eslint/utils": "8.70.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/typescript-eslint/node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.70.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.70.0.tgz", + "integrity": "sha512-/v8HZt6RlyIZxB3ntehELOcUcfxKPVGWXnQdJuHRmzrqgF8nQypcC/oxGW+Ot4VGKDq81XugPKxx0n5PBtf9PA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@eslint-community/regexpp": "^4.12.2", + "@typescript-eslint/scope-manager": "8.70.0", + "@typescript-eslint/type-utils": "8.70.0", + "@typescript-eslint/utils": "8.70.0", + "@typescript-eslint/visitor-keys": "8.70.0", + "ignore": "^7.0.5", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^8.70.0", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/typescript-eslint/node_modules/@typescript-eslint/parser": { + "version": "8.70.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.70.0.tgz", + "integrity": "sha512-zYvrmj9Yxd63UGaXw+kdt6A0F0s0qveJyuatIM77bYC2DE4pgmg7a50u8LR7PRtXd0x+h+Tl3eXabGm06SWd3Q==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@typescript-eslint/scope-manager": "8.70.0", + "@typescript-eslint/types": "8.70.0", + "@typescript-eslint/typescript-estree": "8.70.0", + "@typescript-eslint/visitor-keys": "8.70.0", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/typescript-eslint/node_modules/@typescript-eslint/project-service": { + "version": "8.70.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.70.0.tgz", + "integrity": "sha512-hFHbTNqhU9G+2eKFXCBVb1tjFT/LceiJ4+HfLO4pTpDI0KHi6iajpcFFkaSQ9gXmCh7n82A0PthaayEdN6mspQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.70.0", + "@typescript-eslint/types": "^8.70.0", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/typescript-eslint/node_modules/@typescript-eslint/scope-manager": { + "version": "8.70.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.70.0.tgz", + "integrity": "sha512-8nP3Kwh5hlgZ4FicGvmznAmJe8UL4sdU8tLukrPaMuQmDuk4Y8xYfzu/aYZW4xT2JCgc7H/TpDI5cGlxcWJSqQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@typescript-eslint/types": "8.70.0", + "@typescript-eslint/visitor-keys": "8.70.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/typescript-eslint/node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.70.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.70.0.tgz", + "integrity": "sha512-adnkeeNq9Sq1sUf4+FRVc0KdgYghzsgFpZSQVZVvY0LCuUuN0FnQgyGzCJeC4fW1cdXseBAjU2EOqUIjbNcZUw==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/typescript-eslint/node_modules/@typescript-eslint/type-utils": { + "version": "8.70.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.70.0.tgz", + "integrity": "sha512-NUMKIhYVaVIVLnRL9CRt+VVcuLgSHUCpXn4/+K8wql+vdInUzvx8BjUO1oJ7cG9shjFJKtF8F8Hh2kCh3/KBVw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@typescript-eslint/types": "8.70.0", + "@typescript-eslint/typescript-estree": "8.70.0", + "@typescript-eslint/utils": "8.70.0", + "debug": "^4.4.3", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/typescript-eslint/node_modules/@typescript-eslint/types": { + "version": "8.70.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.70.0.tgz", + "integrity": "sha512-asTOIYhDg4zdzOScCyaytrsV3cR6B4ecPQlXw/dJIm7J/MZTtCtfVII9JD8Geh4jTCrK/Xe6cg5UevoleMcoJQ==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/typescript-eslint/node_modules/@typescript-eslint/typescript-estree": { + "version": "8.70.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.70.0.tgz", + "integrity": "sha512-d9NmHMPEKQ7QCLLm1jI3zmoQBwT5KwFYjXBJ9ymZfKCUU+5rmTRykKAFvH5Qn/ZCds3CEAFS9OC9M/jkl0X2bA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@typescript-eslint/project-service": "8.70.0", + "@typescript-eslint/tsconfig-utils": "8.70.0", + "@typescript-eslint/types": "8.70.0", + "@typescript-eslint/visitor-keys": "8.70.0", + "debug": "^4.4.3", + "minimatch": "^10.2.2", + "semver": "^7.7.3", + "tinyglobby": "^0.2.15", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/typescript-eslint/node_modules/@typescript-eslint/utils": { + "version": "8.70.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.70.0.tgz", + "integrity": "sha512-oZmtKJz/4fufZ2p3+Cn3ijEojcdfR+1zYDH2xKYrEly0dR/Q/1xUPRCOlKGxod78nWlU2UnDe09GZ3TaknBFGA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@eslint-community/eslint-utils": "^4.9.1", + "@typescript-eslint/scope-manager": "8.70.0", + "@typescript-eslint/types": "8.70.0", + "@typescript-eslint/typescript-estree": "8.70.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/typescript-eslint/node_modules/@typescript-eslint/visitor-keys": { + "version": "8.70.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.70.0.tgz", + "integrity": "sha512-BoC8PiO4Hkdo0TVJh9Ntxr5MxPDI7/oFsrygN5ADelFSeXG/qgNuucIGA+L5Z6JpPTE/uRfcTWtscjbUaufepQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@typescript-eslint/types": "8.70.0", + "eslint-visitor-keys": "^5.0.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/typescript-eslint/node_modules/eslint-visitor-keys": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", + "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/ui5-tooling-transpile": { + "version": "3.13.1", + "resolved": "https://registry.npmjs.org/ui5-tooling-transpile/-/ui5-tooling-transpile-3.13.1.tgz", + "integrity": "sha512-c8+W/2zVEr9Ybc0fqAwrwDraOh2CJEjBQEebCfO04BslOzNWYp1k5hB7DSWZAu/zXuGYZ3u38SuzkXpf3ofEFw==", + "dev": true, + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "@babel/core": "^7.29.7", + "@babel/preset-env": "^7.29.7", + "@babel/preset-typescript": "^7.29.7", + "babel-plugin-transform-async-to-promises": "^0.8.18", + "babel-plugin-transform-remove-console": "^6.9.4", + "babel-preset-transform-ui5": "^7.9.0", + "browserslist": "^4.28.8", + "comment-json": "^5.0.0", + "js-yaml": "^4.1.1", + "minimatch": "^10.2.5" + }, + "peerDependencies": { + "@babel/plugin-transform-react-jsx": "^7.0.0", + "@ui5/ts-interface-generator": ">=0.8.0", + "babel-plugin-istanbul": ">=6.0.0" + }, + "peerDependenciesMeta": { + "@babel/plugin-transform-react-jsx": { + "optional": true + }, + "@ui5/ts-interface-generator": { + "optional": true + }, + "babel-plugin-istanbul": { + "optional": true + } + } + }, + "node_modules/unicode-canonical-property-names-ecmascript": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.1.tgz", + "integrity": "sha512-dA8WbNeb2a6oQzAQ55YlT5vQAWGV9WXOsi3SskE3bcCdM0P4SDd+24zS/OCacdRq5BkdsRj9q3Pg6YyQoxIGqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/unicode-match-property-ecmascript": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/unicode-match-property-ecmascript/-/unicode-match-property-ecmascript-2.0.0.tgz", + "integrity": "sha512-5kaZCrbp5mmbz5ulBkDkbY0SsPOjKqVS35VpL9ulMPfSl0J0Xsm+9Evphv9CoIZFwre7aJoa94AY6seMKGVN5Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "unicode-canonical-property-names-ecmascript": "^2.0.0", + "unicode-property-aliases-ecmascript": "^2.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/unicode-match-property-value-ecmascript": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-2.2.1.tgz", + "integrity": "sha512-JQ84qTuMg4nVkx8ga4A16a1epI9H6uTXAknqxkGF/aFfRLw1xC/Bp24HNLaZhHSkWd3+84t8iXnp1J0kYcZHhg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/unicode-property-aliases-ecmascript": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-2.2.0.tgz", + "integrity": "sha512-hpbDzxUY9BFwX+UeBnxv3Sh1q7HFxj48DTmXchNgRa46lO8uj3/1iEn3MiNUYTg1g9ctIqXCCERn8gYZhHC5lQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.3.3.tgz", + "integrity": "sha512-pJ2sYawQS0R/WI928Gj5GlPhTGzbMelq0+4INtSYNDV9ErKJcX6xjGWkoG/VnB3dpUm00zALaqkrUD77pO5TDQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/xml-js": { + "version": "1.6.11", + "resolved": "https://registry.npmjs.org/xml-js/-/xml-js-1.6.11.tgz", + "integrity": "sha512-7rVi2KMfwfWFl+GpPg6m80IVMWXLRjO+PxTq7V2CDhoGak0wzYzFgUY2m4XJ47OGdXd8eLE8EmwfAmdjw7lC1g==", + "dev": true, + "license": "MIT", + "dependencies": { + "sax": "^1.2.4" + }, + "bin": { + "xml-js": "bin/cli.js" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + } +} diff --git a/app/package.json b/app/package.json new file mode 100644 index 0000000..a85a833 --- /dev/null +++ b/app/package.json @@ -0,0 +1,39 @@ +{ + "name": "com.minicircuits.arenaplm", + "version": "0.0.1", + "description": "An SAP Fiori application.", + "keywords": [ + "ui5", + "openui5", + "sapui5" + ], + "main": "webapp/index.html", + "dependencies": {}, + "devDependencies": { + "@ui5/cli": "^4.0.33", + "@sap/ux-ui5-tooling": "1", + "@sap-ux/eslint-plugin-fiori-tools": "^10.0.0", + "eslint": "^10", + "@sapui5/types": "~1.152.0", + "ui5-tooling-transpile": "^3.10.0", + "typescript": "^5.9.3", + "@sap-ux/ui5-middleware-fe-mockserver": "2" + }, + "scripts": { + "start": "echo \\\"This application was generated with a local metadata file and does not reference a live server. Please add the required server configuration or start this application with mock data using the target: `npm run start-mock`.\\\"", + "start-local": "fiori run --config ./ui5-local.yaml --open \"test/flp.html#app-preview\"", + "build": "ui5 build --config=ui5.yaml --clean-dest --dest dist", + "lint": "eslint ./", + "ts-typecheck": "tsc --noEmit", + "prestart": "npm run ts-typecheck", + "prebuild": "npm run ts-typecheck", + "start-mock": "fiori run --config ./ui5-mock.yaml --port 8082 --open \"test/flp.html#app-preview\"", + "deploy": "fiori verify", + "deploy-config": "fiori add deploy-config", + "start-noflp": "echo \\\"This application was generated with a local metadata file and does not reference a live server. Please add the required server configuration or start this application with mock data using the target: `npm run start-mock`.\\\"", + "int-test": "fiori run --config ./ui5-mock.yaml --port 8082 --open \"/test/integration/opaTests.qunit.html\"", + "start-variants-management": "echo \\\"This application was generated with a local metadata file and does not reference a live server. Please add the required server configuration or start this application with mock data using the target: `npm run start-mock`.\\\"" + }, + "sapuxLayer": "CUSTOMER_BASE", + "sapux": true +} diff --git a/app/tsconfig.json b/app/tsconfig.json new file mode 100644 index 0000000..55f48a9 --- /dev/null +++ b/app/tsconfig.json @@ -0,0 +1,26 @@ +{ + "compilerOptions": { + "target": "es2022", + "module": "es2022", + "skipLibCheck": true, + "allowJs": true, + "strict": true, + "strictPropertyInitialization": false, + "moduleResolution": "node", + "rootDir": "./webapp", + "outDir": "./dist", + "baseUrl": "./", + "paths": { + "com/minicircuits/arenaplm/*": [ + "./webapp/*" + ] + }, + "typeRoots": [ + "./node_modules/@types", + "./node_modules/@sapui5/types" + ] + }, + "include": [ + "./webapp/**/*" + ] +} \ No newline at end of file diff --git a/app/ui5-local.yaml b/app/ui5-local.yaml new file mode 100644 index 0000000..0b6e1d5 --- /dev/null +++ b/app/ui5-local.yaml @@ -0,0 +1,62 @@ +# yaml-language-server: $schema=https://sap.github.io/ui5-tooling/schema/ui5.yaml.json + +specVersion: "4.0" +metadata: + name: com.minicircuits.arenaplm +type: application +framework: + name: SAPUI5 + version: 1.152.0 + libraries: + - name: sap.m + - name: sap.ui.core + - name: sap.fe.templates + - name: sap.ushell + - name: themelib_sap_horizon +server: + customMiddleware: + - name: fiori-tools-appreload + afterMiddleware: compression + configuration: + port: 35729 + path: webapp + delay: 300 + - name: ui5-tooling-transpile-middleware + afterMiddleware: compression + configuration: + debug: true + transformModulesToUI5: + overridesToOverride: true + excludePatterns: + - /Component-preload.js + - name: fiori-tools-preview + afterMiddleware: fiori-tools-appreload + configuration: + flp: + theme: sap_horizon + - name: fiori-tools-proxy + afterMiddleware: compression + configuration: + ignoreCertErrors: false # If set to true, certificate errors will be ignored. E.g. self-signed certificates will be accepted + backend: [] + - name: sap-fe-mockserver + beforeMiddleware: csp + configuration: + mountPath: / + services: + - urlPath: /sap/opu/odata4/sap/zui_arena_change_approval_o4/srvd/sap/zui_arena_change_approval/0001/ + metadataPath: ./webapp/localService/mainService/metadata.xml + mockdataPath: ./webapp/localService/mainService/data + generateMockData: false +builder: + resources: + excludes: + - /test/** + - /localService/** + customTasks: + - name: ui5-tooling-transpile-task + afterTask: replaceVersion + configuration: + debug: true + transformModulesToUI5: + overridesToOverride: true diff --git a/app/ui5-mock.yaml b/app/ui5-mock.yaml new file mode 100644 index 0000000..4f7fd6c --- /dev/null +++ b/app/ui5-mock.yaml @@ -0,0 +1,62 @@ +# yaml-language-server: $schema=https://sap.github.io/ui5-tooling/schema/ui5.yaml.json + +specVersion: "4.0" +metadata: + name: com.minicircuits.arenaplm +type: application +server: + customMiddleware: + - name: fiori-tools-proxy + afterMiddleware: compression + configuration: + ignoreCertErrors: false # If set to true, certificate errors will be ignored. E.g. self-signed certificates will be accepted + ui5: + path: + - /resources + - /test-resources + url: https://ui5.sap.com + backend: [] + - name: fiori-tools-appreload + afterMiddleware: compression + configuration: + port: 35729 + path: webapp + delay: 300 + - name: fiori-tools-preview + afterMiddleware: fiori-tools-appreload + configuration: + flp: + theme: sap_horizon + test: + - framework: OPA5 + path: /test/integration/opaTests.qunit.html + - framework: Testsuite + - name: ui5-tooling-transpile-middleware + afterMiddleware: compression + configuration: + debug: true + transformModulesToUI5: + overridesToOverride: true + excludePatterns: + - /Component-preload.js + - name: sap-fe-mockserver + beforeMiddleware: csp + configuration: + mountPath: / + services: + - urlPath: /sap/opu/odata4/sap/zui_arena_change_approval_o4/srvd/sap/zui_arena_change_approval/0001/ + metadataPath: ./webapp/localService/mainService/metadata.xml + mockdataPath: ./webapp/localService/mainService/data + generateMockData: false +builder: + resources: + excludes: + - /test/** + - /localService/** + customTasks: + - name: ui5-tooling-transpile-task + afterTask: replaceVersion + configuration: + debug: true + transformModulesToUI5: + overridesToOverride: true diff --git a/app/ui5.yaml b/app/ui5.yaml new file mode 100644 index 0000000..32169d4 --- /dev/null +++ b/app/ui5.yaml @@ -0,0 +1,49 @@ +# yaml-language-server: $schema=https://sap.github.io/ui5-tooling/schema/ui5.yaml.json + +specVersion: "4.0" +metadata: + name: com.minicircuits.arenaplm +type: application +server: + customMiddleware: + - name: fiori-tools-proxy + afterMiddleware: compression + configuration: + ignoreCertErrors: false # If set to true, certificate errors will be ignored. E.g. self-signed certificates will be accepted + ui5: + path: + - /resources + - /test-resources + url: https://ui5.sap.com + backend: [] + - name: fiori-tools-appreload + afterMiddleware: compression + configuration: + port: 35729 + path: webapp + delay: 300 + - name: fiori-tools-preview + afterMiddleware: fiori-tools-appreload + configuration: + flp: + theme: sap_horizon + - name: ui5-tooling-transpile-middleware + afterMiddleware: compression + configuration: + debug: true + transformModulesToUI5: + overridesToOverride: true + excludePatterns: + - /Component-preload.js +builder: + resources: + excludes: + - /test/** + - /localService/** + customTasks: + - name: ui5-tooling-transpile-task + afterTask: replaceVersion + configuration: + debug: true + transformModulesToUI5: + overridesToOverride: true diff --git a/app/webapp/Component.ts b/app/webapp/Component.ts new file mode 100644 index 0000000..338ffbc --- /dev/null +++ b/app/webapp/Component.ts @@ -0,0 +1,20 @@ +import BaseComponent from "sap/fe/core/AppComponent"; + +/** + * @namespace com.minicircuits.arenaplm + */ +export default class Component extends BaseComponent { + + public static metadata = { + manifest: "json" + }; + + /** + * The component is initialized by UI5 automatically during the startup of the app and calls the init method once. + * @public + * @override + */ + //public init() : void { + // super.init(); + //} +} \ No newline at end of file diff --git a/app/webapp/annotations/annotation.xml b/app/webapp/annotations/annotation.xml new file mode 100644 index 0000000..830ff28 --- /dev/null +++ b/app/webapp/annotations/annotation.xml @@ -0,0 +1,396 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ItemNumber + ChangeType + ArenaEcoId + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ItemNumber + Status + ChangeType + ArenaEcoId + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @UI.LineItem + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/webapp/i18n/i18n.properties b/app/webapp/i18n/i18n.properties new file mode 100644 index 0000000..61f3c01 --- /dev/null +++ b/app/webapp/i18n/i18n.properties @@ -0,0 +1,79 @@ +# This is the resource bundle for com.minicircuits.arenaplm +# +# Text-type comments follow SAP's translator conventions +# (# : ) - XTIT title/heading, XFLD field label, XBUT button. +# [source: sap-docs SAP Translation Hub loio 028d963837b84dbf8d73d4306b032900, verified 2026-09-14] + +#Texts for manifest.json + +#XTIT: Application name +appTitle=Arena PLM Change Approvals + +#YDES: Application description +appDescription=Arena PLM Change Approval Workbench + +#XTIT: Fiori launchpad tile title (sap.app.crossNavigation.inbounds) +flpTitle=Arena PLM Change Approvals +#XTIT: Fiori launchpad tile subtitle +flpSubtitle=Approve staged Arena changes for SAP + +#XTIT: Object type name (singular) in the object page header +CHANGE_APPROVAL_TITLE=Change Approval +#XTIT: Object type name (plural) - list report table header +CHANGE_APPROVALS_TITLE=Change Approvals + +#XBUT: Approves the staged change and triggers the SAP sync +ACTION_APPROVE=Approve +#XBUT: Rejects the staged change; asks for a reason +ACTION_REJECT=Reject +#XBUT: Re-runs a FAILED SAP sync (FAILED -> EXECUTING) +ACTION_RETRY=Retry Sync + +#XFLD: Arena item number (e.g. MC-100-0042) +FIELD_ITEM=Item Number +#XFLD: Arena item name / description +FIELD_NAME=Item Name +#XFLD: Kind of SAP object being synced (PRODUCT, CHANGE_MASTER, BOM, DOCUMENTS) +FIELD_CHANGE_TYPE=Change Type +#XFLD: Approval/sync status (PENDING, APPROVED, EXECUTING, SYNCED, REJECTED, FAILED); text comes from the RAP text element (PRD RAP-3) +FIELD_STATUS=Status +#XFLD: User who staged the change +FIELD_REQUESTED_BY=Requested By +#XFLD: Timestamp the change was staged +FIELD_REQUESTED_ON=Requested On +#XFLD: User who approved or rejected the change +FIELD_DECIDED_BY=Decided By +#XFLD: Timestamp of the approve/reject decision +FIELD_DECIDED_ON=Decided On +#XFLD: Arena engineering change order id (e.g. ECO-2026-0113) +FIELD_ARENA_ECO=Arena ECO +#XFLD: SAP change master number created by the sync +FIELD_SAP_CHANGE_NO=SAP Change Number +#XFLD: Error returned by SAP when the sync failed +FIELD_ERROR_MSG=Error Message +#XFLD: Reason entered by the approver when rejecting +FIELD_REJECT_REASON=Rejection Reason +#XFLD: JSON payload that will be written to SAP on approval +FIELD_STAGED_PAYLOAD=Staged SAP Payload +#XFLD: Technical flag - approve/reject allowed (hidden) +FIELD_IS_APPROVABLE=Can Be Approved +#XFLD: Technical flag - retry allowed (hidden) +FIELD_IS_RETRIABLE=Can Be Retried +#XFLD: Technical record key (hidden) +FIELD_ID=Record ID +#XFLD: Technical criticality code for the status (hidden) +FIELD_STATUS_CRITICALITY=Status Criticality + +#XTIT: Object page section - what is being changed +FACET_CHANGE=Change +#XTIT: Object page section - who requested / decided and when +FACET_APPROVAL=Approval +#XTIT: Object page section - the JSON payload staged for SAP (shown before the approval section) +FACET_STAGED_PAYLOAD=Staged Payload +#XTIT: Object page section - outcome of the SAP sync (change number / error) +FACET_EXECUTION=SAP Result +#XTIT: Object page header facet - Arena / SAP reference numbers +FACET_REFERENCES=References + +#XTIT: Name of the default list report view (pending approvals first) +SPV_DEFAULT=Pending Approvals diff --git a/app/webapp/i18n/i18n_en.properties b/app/webapp/i18n/i18n_en.properties new file mode 100644 index 0000000..25216b7 --- /dev/null +++ b/app/webapp/i18n/i18n_en.properties @@ -0,0 +1,79 @@ +# This is the English resource bundle for com.minicircuits.arenaplm +# +# Text-type comments follow SAP's translator conventions +# (# : ) - XTIT title/heading, XFLD field label, XBUT button. +# [source: sap-docs SAP Translation Hub loio 028d963837b84dbf8d73d4306b032900, verified 2026-09-14] + +#Texts for manifest.json + +#XTIT: Application name +appTitle=Arena PLM Change Approvals + +#YDES: Application description +appDescription=Arena PLM Change Approval Workbench + +#XTIT: Fiori launchpad tile title (sap.app.crossNavigation.inbounds) +flpTitle=Arena PLM Change Approvals +#XTIT: Fiori launchpad tile subtitle +flpSubtitle=Approve staged Arena changes for SAP + +#XTIT: Object type name (singular) in the object page header +CHANGE_APPROVAL_TITLE=Change Approval +#XTIT: Object type name (plural) - list report table header +CHANGE_APPROVALS_TITLE=Change Approvals + +#XBUT: Approves the staged change and triggers the SAP sync +ACTION_APPROVE=Approve +#XBUT: Rejects the staged change; asks for a reason +ACTION_REJECT=Reject +#XBUT: Re-runs a FAILED SAP sync (FAILED -> EXECUTING) +ACTION_RETRY=Retry Sync + +#XFLD: Arena item number (e.g. MC-100-0042) +FIELD_ITEM=Item Number +#XFLD: Arena item name / description +FIELD_NAME=Item Name +#XFLD: Kind of SAP object being synced (PRODUCT, CHANGE_MASTER, BOM, DOCUMENTS) +FIELD_CHANGE_TYPE=Change Type +#XFLD: Approval/sync status (PENDING, APPROVED, EXECUTING, SYNCED, REJECTED, FAILED); text comes from the RAP text element (PRD RAP-3) +FIELD_STATUS=Status +#XFLD: User who staged the change +FIELD_REQUESTED_BY=Requested By +#XFLD: Timestamp the change was staged +FIELD_REQUESTED_ON=Requested On +#XFLD: User who approved or rejected the change +FIELD_DECIDED_BY=Decided By +#XFLD: Timestamp of the approve/reject decision +FIELD_DECIDED_ON=Decided On +#XFLD: Arena engineering change order id (e.g. ECO-2026-0113) +FIELD_ARENA_ECO=Arena ECO +#XFLD: SAP change master number created by the sync +FIELD_SAP_CHANGE_NO=SAP Change Number +#XFLD: Error returned by SAP when the sync failed +FIELD_ERROR_MSG=Error Message +#XFLD: Reason entered by the approver when rejecting +FIELD_REJECT_REASON=Rejection Reason +#XFLD: JSON payload that will be written to SAP on approval +FIELD_STAGED_PAYLOAD=Staged SAP Payload +#XFLD: Technical flag - approve/reject allowed (hidden) +FIELD_IS_APPROVABLE=Can Be Approved +#XFLD: Technical flag - retry allowed (hidden) +FIELD_IS_RETRIABLE=Can Be Retried +#XFLD: Technical record key (hidden) +FIELD_ID=Record ID +#XFLD: Technical criticality code for the status (hidden) +FIELD_STATUS_CRITICALITY=Status Criticality + +#XTIT: Object page section - what is being changed +FACET_CHANGE=Change +#XTIT: Object page section - who requested / decided and when +FACET_APPROVAL=Approval +#XTIT: Object page section - the JSON payload staged for SAP (shown before the approval section) +FACET_STAGED_PAYLOAD=Staged Payload +#XTIT: Object page section - outcome of the SAP sync (change number / error) +FACET_EXECUTION=SAP Result +#XTIT: Object page header facet - Arena / SAP reference numbers +FACET_REFERENCES=References + +#XTIT: Name of the default list report view (pending approvals first) +SPV_DEFAULT=Pending Approvals diff --git a/app/webapp/index.html b/app/webapp/index.html new file mode 100644 index 0000000..bb635e5 --- /dev/null +++ b/app/webapp/index.html @@ -0,0 +1,35 @@ + + + + + + + Arena PLM Engineering Changes Approval Flow + + + + +
+ + \ No newline at end of file diff --git a/app/webapp/localService/mainService/data/ChangeApproval.json b/app/webapp/localService/mainService/data/ChangeApproval.json new file mode 100644 index 0000000..ecc638c --- /dev/null +++ b/app/webapp/localService/mainService/data/ChangeApproval.json @@ -0,0 +1,154 @@ +[ + { + "ID": "6f1c2a3e-0001-4a6b-9c1d-000000000001", + "ItemNumber": "MC-100-0042", + "ItemName": "RF Amplifier Module, 2-18 GHz", + "ChangeType": "PRODUCT", + "Status": "PENDING", + "StatusCriticality": 2, + "ArenaEcoId": "ECO-2026-0113", + "RequestedBy": "AZAW", + "RequestedOn": "2026-09-12T14:05:00Z", + "DecidedBy": null, + "DecidedOn": null, + "SapChangeNumber": null, + "ErrorMessage": null, + "RejectReason": null, + "SapPreview": "{\"Product\":\"MC-100-0042\",\"ProductType\":\"FERT\",\"BaseUnit\":\"PC\",\"IndustrySector\":\"M\",\"GrossWeight\":\"0.412\",\"WeightUnit\":\"KG\",\"ProductGroup\":\"RFAMP\",\"CrossPlantStatus\":\"\",\"_ProductDescription\":[{\"Language\":\"EN\",\"ProductDescription\":\"RF Amplifier Module, 2-18 GHz\"}],\"_ProductPlant\":[{\"Plant\":\"1710\",\"AvailabilityCheckType\":\"02\",\"ProfitCenter\":\"YB700\",\"MRPType\":\"PD\",\"ProcurementType\":\"E\"},{\"Plant\":\"1720\",\"AvailabilityCheckType\":\"02\",\"ProfitCenter\":\"YB700\",\"MRPType\":\"PD\",\"ProcurementType\":\"F\"}],\"_ProductSalesDelivery\":[{\"ProductSalesOrg\":\"1710\",\"ProductDistributionChnl\":\"10\",\"DeliveryQuantityUnit\":\"PC\",\"ItemCategoryGroup\":\"NORM\"}],\"_ProductStorage\":[{\"Plant\":\"1710\",\"StorageLocation\":\"171A\"}],\"_ProductValuation\":[{\"ValuationArea\":\"1710\",\"ValuationClass\":\"7920\",\"PriceControl\":\"S\",\"StandardPrice\":\"245.00\",\"Currency\":\"USD\"}]}", + "IsApprovable": true, + "IsRetriable": false + }, + { + "ID": "6f1c2a3e-0002-4a6b-9c1d-000000000002", + "ItemNumber": "MC-100-0042", + "ItemName": "RF Amplifier Module, 2-18 GHz", + "ChangeType": "CHANGE_MASTER", + "Status": "PENDING", + "StatusCriticality": 2, + "ArenaEcoId": "ECO-2026-0113", + "RequestedBy": "AZAW", + "RequestedOn": "2026-09-12T14:05:04Z", + "DecidedBy": null, + "DecidedOn": null, + "SapChangeNumber": null, + "ErrorMessage": null, + "RejectReason": null, + "SapPreview": "{\"ChangeNumberDescription\":\"ECO: Rev C amplifier bias network update\",\"ChangeNumberValidFromDate\":\"2026-09-12T00:00:00Z\",\"ReasonForChangeText\":\"ARENA ECO-2026-0113\",\"ChangeNumberStatus\":\"01\"}", + "IsApprovable": true, + "IsRetriable": false + }, + { + "ID": "6f1c2a3e-0003-4a6b-9c1d-000000000003", + "ItemNumber": "MC-200-0117", + "ItemName": "Bias Tee Assembly, SMA", + "ChangeType": "BOM", + "Status": "PENDING", + "StatusCriticality": 2, + "ArenaEcoId": "ECO-2026-0118", + "RequestedBy": "JDOE", + "RequestedOn": "2026-09-13T09:41:00Z", + "DecidedBy": null, + "DecidedOn": null, + "SapChangeNumber": null, + "ErrorMessage": null, + "RejectReason": null, + "SapPreview": "{\"Material\":\"MC-200-0117\",\"Plant\":\"1710\",\"BillOfMaterialCategory\":\"M\",\"BillOfMaterialVariantUsage\":\"1\",\"HeaderValidityStartDate\":\"2026-09-13T00:00:00Z\",\"ToBOMItem\":[{\"BillOfMaterialItemNumber\":\"0010\",\"BillOfMaterialComponent\":\"MC-500-0031\",\"BillOfMaterialItemQuantity\":\"2\",\"BillOfMaterialItemCategory\":\"L\"},{\"BillOfMaterialItemNumber\":\"0020\",\"BillOfMaterialComponent\":\"MC-500-0088\",\"BillOfMaterialItemQuantity\":\"1\",\"BillOfMaterialItemCategory\":\"L\"}]}", + "IsApprovable": true, + "IsRetriable": false + }, + { + "ID": "6f1c2a3e-0004-4a6b-9c1d-000000000004", + "ItemNumber": "MC-300-0009", + "ItemName": "Low Noise Amplifier, 0.5-4 GHz", + "ChangeType": "PRODUCT", + "Status": "APPROVED", + "StatusCriticality": 2, + "ArenaEcoId": "ECO-2026-0097", + "RequestedBy": "AZAW", + "RequestedOn": "2026-09-08T16:22:00Z", + "DecidedBy": "MSMITH", + "DecidedOn": "2026-09-09T08:10:00Z", + "SapChangeNumber": null, + "ErrorMessage": null, + "RejectReason": null, + "SapPreview": "{\"Product\":\"MC-300-0009\",\"ProductType\":\"FERT\",\"BaseUnit\":\"PC\",\"ProductGroup\":\"LNA\",\"_ProductDescription\":[{\"Language\":\"EN\",\"ProductDescription\":\"Low Noise Amplifier, 0.5-4 GHz\"}]}", + "IsApprovable": false, + "IsRetriable": false + }, + { + "ID": "6f1c2a3e-0007-4a6b-9c1d-000000000007", + "ItemNumber": "MC-300-0009", + "ItemName": "Low Noise Amplifier, 0.5-4 GHz", + "ChangeType": "CHANGE_MASTER", + "Status": "EXECUTING", + "ArenaEcoId": "ECO-2026-0097", + "RequestedBy": "AZAW", + "RequestedOn": "2026-09-08T16:22:03Z", + "DecidedBy": "MSMITH", + "DecidedOn": "2026-09-09T08:10:00Z", + "SapChangeNumber": null, + "ErrorMessage": null, + "RejectReason": null, + "SapPreview": "{\"ChangeNumberDescription\":\"ECO: LNA input match rework\",\"ChangeNumberValidFromDate\":\"2026-09-09T00:00:00Z\",\"ReasonForChangeText\":\"ARENA ECO-2026-0097\",\"ChangeNumberStatus\":\"01\"}", + "StatusCriticality": 5, + "IsApprovable": false, + "IsRetriable": false + }, + { + "ID": "6f1c2a3e-0008-4a6b-9c1d-000000000008", + "ItemNumber": "MC-150-0301", + "ItemName": "Directional Coupler, 10 dB", + "ChangeType": "CHANGE_MASTER", + "Status": "SYNCED", + "ArenaEcoId": "ECO-2026-0088", + "RequestedBy": "JDOE", + "RequestedOn": "2026-09-05T10:00:00Z", + "DecidedBy": "MSMITH", + "DecidedOn": "2026-09-05T11:30:00Z", + "SapChangeNumber": "500000012301", + "ErrorMessage": null, + "RejectReason": null, + "SapPreview": "{\"ChangeNumberDescription\":\"ECO: Coupler housing plating change\",\"ChangeNumberValidFromDate\":\"2026-09-05T00:00:00Z\",\"ReasonForChangeText\":\"ARENA ECO-2026-0088\",\"ChangeNumberStatus\":\"01\"}", + "StatusCriticality": 3, + "IsApprovable": false, + "IsRetriable": false + }, + { + "ID": "6f1c2a3e-0005-4a6b-9c1d-000000000005", + "ItemNumber": "MC-410-0250", + "ItemName": "Datasheet MC-410-0250 Rev B", + "ChangeType": "DOCUMENTS", + "Status": "REJECTED", + "StatusCriticality": 1, + "ArenaEcoId": "ECO-2026-0101", + "RequestedBy": "JDOE", + "RequestedOn": "2026-09-10T11:15:00Z", + "DecidedBy": "MSMITH", + "DecidedOn": "2026-09-10T13:02:00Z", + "SapChangeNumber": null, + "ErrorMessage": null, + "RejectReason": "Datasheet revision B superseded by Rev C in Arena; re-stage after ECO-2026-0121 is released.", + "SapPreview": "{\"BusinessObjectTypeName\":\"BUS1001006\",\"LinkedSAPObjectKey\":\"MC-410-0250\",\"Url\":\"https://app.bom.com/files/MC-410-0250-B.pdf\",\"MIMEType\":\"text/url\",\"AttachmentDescription\":\"MC-410-0250 Datasheet Rev B\"}", + "IsApprovable": false, + "IsRetriable": false + }, + { + "ID": "6f1c2a3e-0006-4a6b-9c1d-000000000006", + "ItemNumber": "MC-200-0117", + "ItemName": "Bias Tee Assembly, SMA", + "ChangeType": "CHANGE_MASTER", + "Status": "FAILED", + "StatusCriticality": 1, + "ArenaEcoId": "ECO-2026-0118", + "RequestedBy": "JDOE", + "RequestedOn": "2026-09-13T09:40:00Z", + "DecidedBy": "AZAW", + "DecidedOn": "2026-09-13T10:05:00Z", + "SapChangeNumber": null, + "ErrorMessage": "API_CHANGEMASTER;v=0002 returned 400: Input parameters are initial (A_ChangeMasterObjTypeAssgmt requires IsActiveForChangeNumber).", + "RejectReason": null, + "SapPreview": "{\"ChangeNumberDescription\":\"ECO: Bias tee connector change to SMA-F\",\"ChangeNumberValidFromDate\":\"2026-09-13T00:00:00Z\",\"ReasonForChangeText\":\"ARENA ECO-2026-0118\",\"ChangeNumberStatus\":\"01\"}", + "IsApprovable": false, + "IsRetriable": true + } +] diff --git a/app/webapp/localService/mainService/metadata.xml b/app/webapp/localService/mainService/metadata.xml new file mode 100644 index 0000000..1db5ea2 --- /dev/null +++ b/app/webapp/localService/mainService/metadata.xml @@ -0,0 +1,133 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/webapp/manifest.json b/app/webapp/manifest.json new file mode 100644 index 0000000..eecb075 --- /dev/null +++ b/app/webapp/manifest.json @@ -0,0 +1,179 @@ +{ + "_version": "2.11.0", + "sap.app": { + "id": "com.minicircuits.arenaplm", + "type": "application", + "i18n": "i18n/i18n.properties", + "applicationVersion": { + "version": "0.0.1" + }, + "title": "{{appTitle}}", + "description": "{{appDescription}}", + "resources": "resources.json", + "sourceTemplate": { + "id": "@sap/generator-fiori:lrop", + "version": "1.32.0", + "toolsId": "0ea36282-572c-4a90-b419-a3705f20a9c3" + }, + "dataSources": { + "annotation": { + "type": "ODataAnnotation", + "uri": "annotations/annotation.xml", + "settings": { + "localUri": "annotations/annotation.xml" + } + }, + "mainService": { + "uri": "/sap/opu/odata4/sap/zui_arena_change_approval_o4/srvd/sap/zui_arena_change_approval/0001/", + "type": "OData", + "settings": { + "annotations": [ + "annotation" + ], + "localUri": "localService/mainService/metadata.xml", + "odataVersion": "4.0" + } + } + }, + "crossNavigation": { + "inbounds": { + "ArenaChangeApproval-manage": { + "semanticObject": "ArenaChangeApproval", + "action": "manage", + "title": "{{flpTitle}}", + "subTitle": "{{flpSubtitle}}", + "icon": "sap-icon://approvals", + "signature": { + "parameters": {}, + "additionalParameters": "allowed" + } + } + } + } + }, + "sap.ui": { + "technology": "UI5", + "icons": { + "icon": "", + "favIcon": "", + "phone": "", + "phone@2": "", + "tablet": "", + "tablet@2": "" + }, + "deviceTypes": { + "desktop": true, + "tablet": true, + "phone": true + } + }, + "sap.ui5": { + "flexEnabled": true, + "dependencies": { + "minUI5Version": "1.152.0", + "libs": { + "sap.m": {}, + "sap.ui.core": {}, + "sap.fe.templates": {} + } + }, + "contentDensities": { + "compact": true, + "cozy": true + }, + "models": { + "i18n": { + "type": "sap.ui.model.resource.ResourceModel", + "settings": { + "bundleName": "com.minicircuits.arenaplm.i18n.i18n" + } + }, + "": { + "dataSource": "mainService", + "preload": true, + "settings": { + "operationMode": "Server", + "autoExpandSelect": true, + "earlyRequests": true + } + }, + "@i18n": { + "type": "sap.ui.model.resource.ResourceModel", + "uri": "i18n/i18n.properties" + } + }, + "resources": { + "css": [] + }, + "routing": { + "config": {}, + "routes": [ + { + "pattern": ":?query:", + "name": "ChangeApprovalList", + "target": "ChangeApprovalList" + }, + { + "pattern": "ChangeApproval({key}):?query:", + "name": "ChangeApprovalObjectPage", + "target": [ + "ChangeApprovalList", + "ChangeApprovalObjectPage" + ] + } + ], + "targets": { + "ChangeApprovalList": { + "type": "Component", + "id": "ChangeApprovalList", + "name": "sap.fe.templates.ListReport", + "options": { + "settings": { + "contextPath": "/ChangeApproval", + "defaultTemplateAnnotationPath": "com.sap.vocabularies.UI.v1.SelectionPresentationVariant#Default", + "initialLoad": "Enabled", + "variantManagement": "Page", + "navigation": { + "ChangeApproval": { + "detail": { + "route": "ChangeApprovalObjectPage" + } + } + }, + "controlConfiguration": { + "@com.sap.vocabularies.UI.v1.LineItem": { + "tableSettings": { + "type": "ResponsiveTable", + "selectionMode": "None" + } + } + } + } + }, + "controlAggregation": "beginColumnPages" + }, + "ChangeApprovalObjectPage": { + "type": "Component", + "id": "ChangeApprovalObjectPage", + "name": "sap.fe.templates.ObjectPage", + "options": { + "settings": { + "editableHeaderContent": false, + "contextPath": "/ChangeApproval" + } + }, + "controlAggregation": "midColumnPages" + } + } + } + }, + "sap.fiori": { + "registrationIds": [], + "archeType": "transactional" + }, + "sap.fe": { + "app": { + "enableLazyLoading": true + } + } +} diff --git a/app/webapp/test/integration/ChangeApprovalsListJourney.gen.ts b/app/webapp/test/integration/ChangeApprovalsListJourney.gen.ts new file mode 100644 index 0000000..f8bc1ff --- /dev/null +++ b/app/webapp/test/integration/ChangeApprovalsListJourney.gen.ts @@ -0,0 +1,61 @@ +/****************************************************************************** + * ╔═══════════════════════════════════════════════════════════════════════╗ * + * ║ ║ * + * ║ WARNING: AUTO-GENERATED FILE ║ * + * ║ ║ * + * ║ This file is automatically generated by SAP Fiori tools and is ║ * + * ║ overwritten when you run the OPA test generator again. ║ * + * ║ ║ * + * ║ To add your own custom tests: ║ * + * ║ - Create a new journey test file in this directory. ║ * + * ║ - Follow the same pattern as this file. ║ * + * ║ - Add the new file to the opaTests.qunit.js config file. ║ * + * ║ - Custom journey files are not overwritten. ║ * + * ║ ║ * + * ╚═══════════════════════════════════════════════════════════════════════╝ * + ******************************************************************************/ + +import opaTest from "sap/ui/test/opaQunit"; +import type { Given, When, Then } from "./types/OpaJourneyTypes.gen"; +import runner from "./pages/JourneyRunner"; + +function journey() { + QUnit.module("ChangeApprovalsListListReport journey"); + + const defaultTableId = ""; + + opaTest("Start application", function (Given: Given, _When: When, Then: Then) { + Given.iStartMyApp(); + + Then.onTheChangeApprovalsListGenerated.iSeeThisPage(); + }); + + + // Note: this test will only work if the ListReport page has a search field and shows data that matches the search term. Please ensure that the test data and search term are set up accordingly. + // opaTest("Perform a global search and check the result", function (Given: Given, When: When, Then: Then) { + // When.onTheChangeApprovalsListGenerated.onFilterBar().iChangeSearchField("Search Term"); + // When.onTheChangeApprovalsListGenerated.onFilterBar().iExecuteSearch(); + // Then.onTheChangeApprovalsListGenerated.onTable(defaultTableId).iCheckRows(); + // When.onTheChangeApprovalsListGenerated.onFilterBar().iChangeSearchField(undefined); + // Then.onTheChangeApprovalsListGenerated.onFilterBar().iCheckSearchField(undefined); + // }); + + opaTest("Check table columns and actions", function (_Given: Given, _When: When, Then: Then) { + Then.onTheChangeApprovalsListGenerated.onTable(defaultTableId).iCheckColumns(undefined, {"ItemNumber":{"header":"ItemNumber"},"ItemName":{"header":"ItemName"},"ChangeType":{"header":"ChangeType"},"Status":{"header":"Status"},"StatusCriticality":{"header":"StatusCriticality"}}); + }); + + opaTest("Navigate to ObjectPage", function (_Given: Given, When: When, Then: Then) { + // Note: this test will fail if the ListReport page doesn't show any data + When.onTheChangeApprovalsListGenerated.onFilterBar().iExecuteSearch(); + Then.onTheChangeApprovalsListGenerated.onTable(defaultTableId).iCheckRows(); + When.onTheChangeApprovalsListGenerated.onTable(defaultTableId).iPressRow(0); + Then.onTheChangeApprovalsObjectPageGenerated.iSeeThisPage(); + }); + + opaTest("Teardown", function (Given: Given) { + // Cleanup + Given.iTearDownMyApp(); + }); +} + +runner.run([journey]); diff --git a/app/webapp/test/integration/ChangeApprovalsObjectPageJourney.gen.ts b/app/webapp/test/integration/ChangeApprovalsObjectPageJourney.gen.ts new file mode 100644 index 0000000..c5b5f7e --- /dev/null +++ b/app/webapp/test/integration/ChangeApprovalsObjectPageJourney.gen.ts @@ -0,0 +1,71 @@ +/****************************************************************************** + * ╔═══════════════════════════════════════════════════════════════════════╗ * + * ║ ║ * + * ║ WARNING: AUTO-GENERATED FILE ║ * + * ║ ║ * + * ║ This file is automatically generated by SAP Fiori tools and is ║ * + * ║ overwritten when you run the OPA test generator again. ║ * + * ║ ║ * + * ║ To add your own custom tests: ║ * + * ║ - Create a new journey test file in this directory. ║ * + * ║ - Follow the same pattern as this file. ║ * + * ║ - Add the new file to the opaTests.qunit.js config file. ║ * + * ║ - Custom journey files are not overwritten. ║ * + * ║ ║ * + * ╚═══════════════════════════════════════════════════════════════════════╝ * + ******************************************************************************/ + +import opaTest from "sap/ui/test/opaQunit"; +import type { Given, When, Then } from "./types/OpaJourneyTypes.gen"; +import runner from "./pages/JourneyRunner"; + +function journey() { + QUnit.module("ChangeApprovalsObjectPageObjectPage journey"); + + opaTest("Navigate to ChangeApprovalsObjectPageObjectPage", function (Given: Given, When: When, Then: Then) { + Given.iStartMyApp(); + When.onTheChangeApprovalsListGenerated.onFilterBar().iExecuteSearch(); + Then.onTheChangeApprovalsListGenerated.onTable("").iCheckRows(); + When.onTheChangeApprovalsListGenerated.onTable("").iPressRow(0); + + Then.onTheChangeApprovalsObjectPageGenerated.iSeeThisPage(); + }); + + + + opaTest("Check the ChangeFacet section of the Object Page", function (_Given: Given, _When: When, Then: Then) { + Then.onTheChangeApprovalsObjectPageGenerated.iCheckSection({ section: "ChangeFacet" }, {}); + Then.onTheChangeApprovalsObjectPageGenerated.onForm({ section: "ChangeFacet" }).iCheckField({ property: "ItemNumber" }); + Then.onTheChangeApprovalsObjectPageGenerated.onForm({ section: "ChangeFacet" }).iCheckField({ property: "ItemName" }); + Then.onTheChangeApprovalsObjectPageGenerated.onForm({ section: "ChangeFacet" }).iCheckField({ property: "ChangeType" }); + Then.onTheChangeApprovalsObjectPageGenerated.onForm({ section: "ChangeFacet" }).iCheckField({ property: "ArenaEcoId" }); + }); + + opaTest("Check the ApprovalFacet section of the Object Page", function (_Given: Given, _When: When, Then: Then) { + Then.onTheChangeApprovalsObjectPageGenerated.iCheckSection({ section: "ApprovalFacet" }, {}); + Then.onTheChangeApprovalsObjectPageGenerated.onForm({ section: "ApprovalFacet" }).iCheckField({ property: "Status" }); + Then.onTheChangeApprovalsObjectPageGenerated.onForm({ section: "ApprovalFacet" }).iCheckField({ property: "RequestedBy" }); + Then.onTheChangeApprovalsObjectPageGenerated.onForm({ section: "ApprovalFacet" }).iCheckField({ property: "RequestedOn" }); + Then.onTheChangeApprovalsObjectPageGenerated.onForm({ section: "ApprovalFacet" }).iCheckField({ property: "DecidedBy" }); + Then.onTheChangeApprovalsObjectPageGenerated.onForm({ section: "ApprovalFacet" }).iCheckField({ property: "DecidedOn" }); + Then.onTheChangeApprovalsObjectPageGenerated.onForm({ section: "ApprovalFacet" }).iCheckField({ property: "RejectReason" }); + }); + + opaTest("Check the ExecutionFacet section of the Object Page", function (_Given: Given, _When: When, Then: Then) { + Then.onTheChangeApprovalsObjectPageGenerated.iCheckSection({ section: "ExecutionFacet" }, {}); + Then.onTheChangeApprovalsObjectPageGenerated.onForm({ section: "ExecutionFacet" }).iCheckField({ property: "SapChangeNumber" }); + Then.onTheChangeApprovalsObjectPageGenerated.onForm({ section: "ExecutionFacet" }).iCheckField({ property: "ErrorMessage" }); + }); + + opaTest("Check the PayloadFacet section of the Object Page", function (_Given: Given, _When: When, Then: Then) { + Then.onTheChangeApprovalsObjectPageGenerated.iCheckSection({ section: "PayloadFacet" }, {}); + Then.onTheChangeApprovalsObjectPageGenerated.onForm({ section: "PayloadFacet" }).iCheckField({ property: "SapPreview" }); + }); + + opaTest("Teardown", function (Given: Given) { + // Cleanup + Given.iTearDownMyApp(); + }); +} + +runner.run([journey]); diff --git a/app/webapp/test/integration/pages/ChangeApprovalsList.gen.ts b/app/webapp/test/integration/pages/ChangeApprovalsList.gen.ts new file mode 100644 index 0000000..819e871 --- /dev/null +++ b/app/webapp/test/integration/pages/ChangeApprovalsList.gen.ts @@ -0,0 +1,25 @@ +/****************************************************************************** + * ╔═══════════════════════════════════════════════════════════════════════╗ * + * ║ ║ * + * ║ WARNING: AUTO-GENERATED FILE ║ * + * ║ ║ * + * ║ This file is automatically generated by SAP Fiori tools and is ║ * + * ║ overwritten when you run the OPA test generator again. ║ * + * ║ ║ * + * ║ To add your own custom pages: ║ * + * ║ - Create a new page file in this directory. ║ * + * ║ - Follow the same pattern as this file. ║ * + * ║ - Add the new file to the JourneyRunner. ║ * + * ║ - Custom page files are not overwritten. ║ * + * ║ ║ * + * ╚═══════════════════════════════════════════════════════════════════════╝ * + ******************************************************************************/ + +export const actions = {}; + +export const assertions = {}; + +export default class ListReport { + actions = actions; + assertions = assertions; +} diff --git a/app/webapp/test/integration/pages/ChangeApprovalsObjectPage.gen.ts b/app/webapp/test/integration/pages/ChangeApprovalsObjectPage.gen.ts new file mode 100644 index 0000000..6202fd9 --- /dev/null +++ b/app/webapp/test/integration/pages/ChangeApprovalsObjectPage.gen.ts @@ -0,0 +1,25 @@ +/****************************************************************************** + * ╔═══════════════════════════════════════════════════════════════════════╗ * + * ║ ║ * + * ║ WARNING: AUTO-GENERATED FILE ║ * + * ║ ║ * + * ║ This file is automatically generated by SAP Fiori tools and is ║ * + * ║ overwritten when you run the OPA test generator again. ║ * + * ║ ║ * + * ║ To add your own custom pages: ║ * + * ║ - Create a new page file in this directory. ║ * + * ║ - Follow the same pattern as this file. ║ * + * ║ - Add the new file to the JourneyRunner. ║ * + * ║ - Custom page files are not overwritten. ║ * + * ║ ║ * + * ╚═══════════════════════════════════════════════════════════════════════╝ * + ******************************************************************************/ + +export const actions = {}; + +export const assertions = {}; + +export default class ObjectPage { + actions = actions; + assertions = assertions; +} diff --git a/app/webapp/test/integration/pages/JourneyRunner.ts b/app/webapp/test/integration/pages/JourneyRunner.ts new file mode 100644 index 0000000..ba772f5 --- /dev/null +++ b/app/webapp/test/integration/pages/JourneyRunner.ts @@ -0,0 +1,32 @@ +import JourneyRunner from "sap/fe/test/JourneyRunner"; +import ListReport from "sap/fe/test/ListReport"; +import ObjectPage from "sap/fe/test/ObjectPage"; +import CustomChangeApprovalsListGenerated from "./ChangeApprovalsList.gen"; +import CustomChangeApprovalsObjectPageGenerated from "./ChangeApprovalsObjectPage.gen"; + +const runner = new JourneyRunner({ + launchUrl: sap.ui.require.toUrl("com/minicircuits/arenaplm") + "/test/flp.html#app-preview", + pages: { + onTheChangeApprovalsListGenerated: new ListReport( + { + appId: "com.minicircuits.arenaplm", + componentId: "ChangeApprovalList", + entitySet: "", + contextPath: "/ChangeApproval" + }, + CustomChangeApprovalsListGenerated + ), + onTheChangeApprovalsObjectPageGenerated: new ObjectPage( + { + appId: "com.minicircuits.arenaplm", + componentId: "ChangeApprovalObjectPage", + entitySet: "", + contextPath: "/ChangeApproval" + }, + CustomChangeApprovalsObjectPageGenerated + ) + }, + async: true +}); + +export default runner; diff --git a/app/webapp/test/integration/types/OpaJourneyTypes.gen.d.ts b/app/webapp/test/integration/types/OpaJourneyTypes.gen.d.ts new file mode 100644 index 0000000..bfb959e --- /dev/null +++ b/app/webapp/test/integration/types/OpaJourneyTypes.gen.d.ts @@ -0,0 +1,56 @@ +/****************************************************************************** + * ╔═══════════════════════════════════════════════════════════════════════╗ * + * ║ ║ * + * ║ WARNING: AUTO-GENERATED FILE ║ * + * ║ ║ * + * ║ This file is automatically generated by SAP Fiori tools and is ║ * + * ║ overwritten when you run the OPA test generator again. ║ * + * ║ ║ * + * ║ Do not edit this file directly. Any changes will be lost. ║ * + * ║ ║ * + * ╚═══════════════════════════════════════════════════════════════════════╝ * + ******************************************************************************/ + +import type Opa5 from "sap/ui/test/Opa5"; +import type { actions as ListReportActions, assertions as ListReportAssertions } from "sap/fe/test/ListReport"; +import type { actions as ObjectPageActions, assertions as ObjectPageAssertions } from "sap/fe/test/ObjectPage"; +import type { actions as TemplatePageActions, assertions as TemplatePageAssertions } from "sap/fe/test/TemplatePage"; +import type Shell from "sap/fe/test/Shell"; +import type BaseArrangements from "sap/fe/test/BaseArrangements"; +import type { actions as ChangeApprovalsListGeneratedCustomActions, assertions as ChangeApprovalsListGeneratedCustomAssertions } from "../pages/ChangeApprovalsList.gen"; +import type { actions as ChangeApprovalsObjectPageGeneratedCustomActions, assertions as ChangeApprovalsObjectPageGeneratedCustomAssertions } from "../pages/ChangeApprovalsObjectPage.gen"; + +/** + * Enables OPA5 fluent `.and` chaining on page objects and their sub-objects. + * + * @example + * Then.onTheObjectPageGenerated.iCheckSection({ section: "A" }).and.iCheckSection({ section: "B" }); + * When.onTheListReportGenerated.onFilterBar().iExecuteSearch().and.iClearFilterBar(); + */ +type WithAnd = { + [K in keyof T | 'and']: K extends 'and' + ? WithAnd + : K extends keyof T + ? T[K] extends (...args: infer A) => infer R + ? (...args: A) => (R extends Opa5 | object ? (keyof R extends never ? WithAnd : [R] extends [Opa5] ? WithAnd : WithAnd) : WithAnd) + : T[K] + : never; +}; + +export type Given = Opa5 & BaseArrangements & { + iTearDownMyApp: () => Given; + iStartMyApp: (sAppHash?: string, mInUrlParameters?: object) => Given; + and: Given; +}; + +export type When = Opa5 & BaseArrangements & { + onTheChangeApprovalsListGenerated: WithAnd; + onTheChangeApprovalsObjectPageGenerated: WithAnd; + onTheShell: Shell; +}; + +export type Then = Opa5 & BaseArrangements & { + onTheChangeApprovalsListGenerated: WithAnd; + onTheChangeApprovalsObjectPageGenerated: WithAnd; + onTheShell: Shell; +}; diff --git a/docs/adr/ADR-001-cap-first-rebuild.md b/docs/adr/ADR-001-cap-first-rebuild.md new file mode 100644 index 0000000..e72e4e9 --- /dev/null +++ b/docs/adr/ADR-001-cap-first-rebuild.md @@ -0,0 +1,38 @@ +# ADR-001 — CAP-first rebuild; Express predecessor demoted to reference + +Date: 2026-09-04 · Status: Accepted — **amended by ADR-002 (2026-09-04)**: the +rebuild target is now on-stack RAP + Fiori Elements in the tenant, with a +side-by-side connectivity worker; CAP exits the architecture. This ADR's core +rationale — the SAP toolchain must engage, and the Express shape is not the +pattern — stands unchanged. + +## Context + +The working predecessor (`~/projects/sap-apps/arena-plm`) is Express.js + axios +with hand-rolled SAP patterns. It functions, but it sits outside the SAP-native +toolchain: the CAP, Fiori, and UI5 MCP servers and skills had nothing to attach +to, so development guidance never engaged, and the implementation drifted from +SAP best practice — hand-managed CSRF tokens, an approval store in droplet-local +SQLite that vanishes on restart, JWT validation with no scope checks. The estate +is explore-phase; there is no production cutover to protect, so preserving the +legacy shape has no value beyond reference. + +## Decision + +I rebuild arena-plm as a CAP (Node.js) application: the approval domain modeled +in CDS, all S/4HANA calls through CAP remote services / SAP Cloud SDK via the +`mc-s4hana` destination, and Fiori Elements evaluated before any custom UI5. +The Express app is reference-only — consulted for behavior and tombstones, +never extended. + +## Consequences + +- The SAP toolchain becomes load-bearing: `cap-mcp-server` is consulted on + every model and service change, which is the corrective for the + tooling-never-engaged failure mode. +- Auth, CSRF, and destination handling move onto framework-managed code paths, + eliminating the predecessor's hand-rolled plumbing. +- The approval workflow domain must be re-modeled in CDS; behavior parity is + checked against the reference app, never assumed from it. +- Whether the sync-pipeline half stays inside CAP or becomes a separate + hardened worker is deliberately deferred (PRD OQ-4). diff --git a/docs/adr/ADR-002-on-stack-first.md b/docs/adr/ADR-002-on-stack-first.md new file mode 100644 index 0000000..dcdf56c --- /dev/null +++ b/docs/adr/ADR-002-on-stack-first.md @@ -0,0 +1,85 @@ +# ADR-002 — On-stack first: RAP approval domain + Fiori app in the tenant; side-by-side reduced to a connectivity worker + +Date: 2026-09-04 · Status: Accepted (amends ADR-001) + +## Context + +The decisive test came from outside: the showcased UI apps present the data +well, but they are invisible from inside S/4HANA — not in the Fiori launchpad, +not discoverable in the Custom CDS Views app, reachable only via standalone +Cloud Foundry URLs behind an ad-hoc route structure with weak access control. +For an S/4HANA user, an application that cannot be found in the tenant does not +exist. + +ADR-001 correctly rejected the Express shape but still assumed a side-by-side +BTP application (CAP) as the rebuild target. That assumption fails the same +test: a CAP app on BTP is still a standalone URL outside the tenant. + +S/4HANA Cloud Public Edition offers two in-tenant extensibility tiers, both +verified against SAP documentation (2026-09-04, via sap-docs MCP): + +- **Developer extensibility**: upgrade-stable ABAP Cloud development directly + on the stack (ADT against the development tenant) — CDS entities, RAP + business objects with behavior, custom Fiori apps deployed into the tenant, + authorization via IAM app → business catalog → business role. +- **Key-user extensibility**: Custom Business Objects, Custom CDS Views (on + released C1 view entities), Custom Analytical Queries, and Business + Configuration maintenance via the Custom Business Configurations app. + +The estate's standing rule already says extensibility-first: check what the +tenant can host before building side-by-side. + +## Decision + +I move the application into the tenant and shrink the side-by-side footprint to +what genuinely cannot run there: + +**On-stack (developer extensibility, ABAP Cloud / RAP):** +- A custom RAP business object for the Arena change approval domain — staging + data, approval state machine (pending → approved/rejected), audit fields — + persisted in the tenant. +- A Fiori Elements List Report / Object Page on that BO, deployed to the + tenant's Fiori launchpad; approve/reject as RAP actions. +- IAM app, business catalog, and business roles as the only access control — + no user-facing custom URLs anywhere. +- Custom CDS views over the approval BO (joinable with released views such as + I_Product) so the data is discoverable in the Custom CDS Views app and usable + in analytical queries — the discoverability test that triggered this ADR. +- Business mappings evaluated as Business Configuration maintenance objects + (Custom Business Configurations app) instead of JSON files in git. + +**Side-by-side (one hardened TypeScript worker, no UI, no user URLs):** +- Arena connectivity only: webhook receipt, delta polling, session management + (25-min TTL, login race), file streaming. +- Initially also the sync executor: on approval, it calls the released OData + APIs (Product/BOM/ChangeMaster/Attachment) exactly as the predecessor did — + every KNOWLEDGE.md tombstone stays valid unchanged. +- It exchanges data with the tenant through the approval BO's service binding + exposed via a custom communication scenario/arrangement with a technical + user. + +CAP exits the architecture: with the domain, UI, and persistence on-stack, the +remaining side-by-side piece is a pipeline, and per the two-archetype standard +a pipeline is a worker, not a CAP service. This resolves ADR-001's deferred +question and PRD OQ-1 through OQ-4 in one move. + +## Consequences + +- The Fiori launchpad becomes the application's only front door; the shared + BTP gateway, XSUAA role juggling, and standalone routes disappear from this + package entirely. +- Persistence and audit live in the tenant — the predecessor's + lost-on-restart approval queue defect is solved structurally, not patched. +- The toolchain shifts: ABAP Cloud + RAP + Fiori Elements, so the MCP loadout + moves to sap-docs / abap-docs / sap-released-objects / fiori / ui5, and + cap-mcp-server leaves the loadout. +- Verification items before build (POC gates): + - V-1: confirm developer-extensibility access on my429480 (development + tenant reachable via ADT, developer business role assigned). + - V-2: confirm a custom communication scenario can expose the approval BO's + OData service to the worker's technical user. + - V-3: evaluate on-stack Application Jobs vs the worker for Arena delta + polling; and Business Configuration objects vs config files (ARN-004). +- Fallback recorded: if developer extensibility proves unavailable on this + tenant, the key-user tier (Custom Business Object with generated UI) is the + degraded-but-still-in-tenant variant; side-by-side is no longer the fallback. diff --git a/docs/adr/ADR-003-ground-truth-enforcement.md b/docs/adr/ADR-003-ground-truth-enforcement.md new file mode 100644 index 0000000..9e99d23 --- /dev/null +++ b/docs/adr/ADR-003-ground-truth-enforcement.md @@ -0,0 +1,94 @@ +# ADR-003 — Ground-truth enforcement: hooks, not advice + +Date: 2026-09-04 · Status: Accepted + +## Context + +The old estate proved that advisory guidance fails: an MCP routing table, skills, +and operating rules existed for months, yet the servers and skills were never +consulted during development — even on the CAP packages where they obviously +applied. Causes: tool-selection dilution under a saturated loadout, zero +enforcement (prose loses to momentum), friction (cold starts, deferred tool +loading), and model overconfidence in trained SAP knowledge. The requirement for +this repo: consultation must be structural — the agent must forget what it +thinks it knows, consult the MCP servers and context7 before work starts, and be +able to cite sources on demand. + +## Decision + +Four mechanisms in `.claude/settings.json` + `.claude/hooks/` (project scope, +part of the founding kit for every future package): + +1. **SessionStart injection** — every session opens with the grounding charter: + training knowledge is not a citable source; the five MCP servers + context7 + are; if tools are deferred, loading them is the first action. +2. **Per-prompt reflex (UserPromptSubmit)** — a SAP-scoped analogue of the + machine-global TOOL-REFLEX hook: never classify whether a prompt "needs" a + consult; the classification step is the known failure. +3. **Consultation gate (PreToolUse deny)** — every successful `mcp__*`, + WebSearch, or WebFetch call stamps `.claude/.last-mcp-consult`; a Write/Edit + on a SAP artifact (`.cds`, `.abap`, `.bdef`, annotations, views, + `manifest.json`, `srv/`, `app/`, `worker/`) is **denied** unless a + consultation happened within 30 minutes, with the remedy stated in the denial + reason. Governance `.md`, `docs/`, and `.claude/` paths are exempt so + documentation never jams. +4. **Citation duty** — every SAP behavioral claim recorded in PRD/ADR/KNOWLEDGE + carries `[source: , verified YYYY-MM-DD]` (CLAUDE.md + rule 6); a PostToolUse reminder fires on governance-doc edits. Uncited = + hypothesis. + +## On "load tools automatically at session start" + +**Corrected 2026-09-04** after reading the 2026-09-03 "Investigate performance +lag issue" session transcript: my original claim ("no setting exists to preload +deferred tools") was true of settings *keys* but wrong in practice. The +`ENABLE_TOOL_SEARCH=false` environment variable disables deferral entirely +(verified against the binary in that session). It must be set before process +bootstrap — the settings.json `env` block loads too late, per that session's +finding — so it lives in the `claude()` wrapper in `~/.bashrc`, where it is +**already active machine-wide**, including for sessions started in this repo. +History: preloading was first measured at ~98k tokens/turn at the old ~950-tool +surface and rejected in favor of the TOOL-REFLEX hook; once plugin localization +cut the surface to ~40 tools, the same session adopted preloading via the +wrapper. This repo's slim 5-server loadout keeps the preload cost small. +Mechanism 1 (SessionStart load-first order) remains as the fallback for launch +paths that bypass the wrapper (e.g. IDE-spawned sessions). + +## Consequences + +- Consultation stops being a virtue and becomes a precondition; skipping it now + produces a denied edit instead of silent drift. +- The 30-minute freshness window is a tunable; if it proves too tight or too + loose in practice, adjust it in `sap-consult-gate.sh` and note it here. +- The gate measures that *a* consultation happened, not that the *right* one + did — citation duty and review cover relevance. +- Hooks activate on the first session started inside this repo (project hooks + are read at session start). + +## Amendment 2026-09-14 — gate hardened after code review + +The 2026-09-04 gate was advisory in practice: the `Write|Edit` matcher missed +Bash writes (sed -i, heredocs, tee, cp/mv, interpreter one-liners), MultiEdit, +NotebookEdit and filesystem-MCP edits; the `mcp__.*` PostToolUse matcher let +any MCP call (memory, chrome, mcp-manage) refresh the 30-minute marker; hook +commands hard-coded this checkout's absolute path; and a missing `jq` produced +a silent allow. I changed the mechanism, not the prose: + +- PreToolUse gate matches `Write|Edit|MultiEdit|NotebookEdit|Bash| + mcp__filesystem__write_file|mcp__filesystem__edit_file|mcp__filesystem__move_file`; + for Bash the script gates only commands that combine a write indicator with + a gated path (documented heuristic, known blind spots: variable-built paths, + `cd`-relative paths, writes by invoked scripts). +- The consult marker is refreshed only by the grounding servers (sap-docs, + abap-docs, sap-released-objects, the Fiori/UI5 servers, context7, CAP) and + WebSearch/WebFetch — anchored regex in `settings.json`. +- Hook commands resolve via `"${CLAUDE_PROJECT_DIR}"`; scripts prefer that + variable over `$0` [source: https://code.claude.com/docs/en/hooks — + "`${CLAUDE_PROJECT_DIR}`: the project root where the session started", + verified 2026-09-14]. +- Missing `jq` or an unresolvable marker now denies (fail closed); `jq` is a + hard dependency of any session in this repo. +- `*/worker/*` left the gated set: the TypeScript worker is governed by + context7 and the OpenAPI contract, not the SAP gate. +- `.claude/hooks/tests/run.sh` pipe-tests both scripts (37 cases) against an + override marker so the real marker is never touched. diff --git a/docs/agent-roster.md b/docs/agent-roster.md new file mode 100644 index 0000000..f907d92 --- /dev/null +++ b/docs/agent-roster.md @@ -0,0 +1,64 @@ +# Specialist Agent Roster — founding-kit design + +Compiled 2026-09-04 from a 4-agent verified research sweep (3 researchers + +1 adversarial verifier; 37 findings, 22 confirmed, 14 downgraded/refuted and +discarded). Every design choice below cites a CONFIRMED source. This is the +"dedicated agents" layer of the founding kit; the files live in +`.claude/agents/`. + +## Design principles (adopted from confirmed sources) + +1. **Tool restrictions enforce role — prompts merely request it.** A reviewer + without Write cannot introduce bugs; a test agent without deploy tools + cannot ship. [source: https://www.pubnub.com/blog/best-practices-for-claude-code-sub-agents/ + https://github.com/VoltAgent/awesome-claude-code-subagents, verified 2026-09-04] +2. **Every agent carries the MCP routing table for its artifacts** — the + pattern SAP itself demonstrates in its official agentic CAP reference app + (AGENTS.md maps each artifact type to its grounding server). [source: https://github.com/SAP-samples/cap-agentic-engineered, verified 2026-09-04] +3. **Model per role:** implementation agents inherit the session model; + the test agent runs on Haiku (cheap, mechanical work). [source: https://github.com/lst97/claude-code-sub-agents test-automator pattern — repo verified WEAK on scale claims but the model-tiering pattern confirmed on page, 2026-09-04] +4. **Reviewer filters false positives explicitly** (excluded classes declared + up front), per Anthropic's own security-review agent. [source: https://github.com/anthropics/claude-code-security-review, verified 2026-09-04] +5. All agents inherit this repo's enforcement layer (ADR-003 gate + citation + duty) automatically — hooks are project-scoped, so no per-agent wiring. + +## The roster (5 — the cap) + +| Agent | Scope | Tools (restriction is the contract) | Model | +|---|---|---|---| +| `rap-developer` | On-stack ABAP Cloud: RAP BO, CDS, behavior, BDEF, IAM artifacts | Edit/Write/Read/Grep/Glob/Bash + abap-docs, sap-docs, sap-released-objects | inherit | +| `fiori-developer` | Fiori Elements app, annotations, manifest, launchpad content | Edit/Write/Read/Grep/Glob/Bash + fiori-mcp-server (3-step workflow), ui5-mcp-server | inherit | +| `worker-developer` | TS connectivity worker: Arena API, Cloud SDK OData writes | Edit/Write/Read/Grep/Glob/Bash + context7 | inherit | +| `sap-code-reviewer` | Clean-core, security, contract conformance review | **Read/Grep/Glob + doc MCPs only — no Write, no Bash** | inherit | +| `test-engineer` | Unit/integration tests for worker + service layers | Edit/Write/Read/Grep/Glob/Bash (test commands only by instruction) | haiku | + +## MCP additions (recommended, gated — not installed) + +The on-stack RAP work needs an **ADT MCP server** (live syntax check, ATC, +activation, repository objects). Verified options, in preference order per the +prefer-standard rule: + +1. **SAP's official ABAP MCP path** — SAP's agentic-ABAP direction (VS Code + ADT + zero-config MCP) is announced and rolling out; `sapse.adt-vscode` is + already installed on this machine. Evaluate first at V-1. + [source: https://community.sap.com/t5/technology-blog-posts-by-sap/entering-the-new-era-of-agentic-ai-for-abap-development/ba-p/14394643 + community post ba-p/14408186, verified 2026-09-04] +2. **ARC-1** (`arc-mcp/arc-1`) — enterprise ABAP/ADT MCP, maintained. + [source: https://github.com/arc-mcp/arc-1, verified 2026-09-04] +3. **`yzonur/sap-adt-mcp`** — 27 ADT tools incl. quality gates, read-only + flag, audit logging. [source: https://github.com/yzonur/sap-adt-mcp, verified 2026-09-04] + +Optional later: SAP's official **BTP Administration MCP** for the devops side. +[source: https://community.sap.com/t5/technology-blog-posts-by-sap/public-release-of-the-mcp-server-for-sap-btp-administration/ba-p/14471692, verified 2026-09-04] + +Registry for future discovery: https://github.com/marianfoo/sap-ai-mcp-servers +(verified 2026-09-04). Note: `marianfoo/mcp-sap-notes` is archived (June 2026), +superseded by `marianfoo/sap-mcp-servers` — do not adopt the archived one. + +## Discarded by verification (recorded so they are not re-researched) + +- `MariusAlbrecht/mcp-adt` — does not exist (404; the real ADT bridge is the + separate "vs-punk" project). +- SAP GUI automation MCP — cited listing 404s; GUI automation irrelevant to + Public Cloud anyway. +- `rap-skills`, `claude-abap-skills` — real but 1–14 commits; mine their + *scenario decomposition ideas* (generator/CDS/behavior/testing/troubleshoot), + do not depend on the repos. diff --git a/docs/api/worker.openapi.json b/docs/api/worker.openapi.json new file mode 100644 index 0000000..c01b60a --- /dev/null +++ b/docs/api/worker.openapi.json @@ -0,0 +1,731 @@ +{ + "openapi": "3.1.0", + "info": { + "title": "arena-plm connectivity worker API", + "version": "0.2.1", + "x-sap-shortText": "Inbound machine-to-machine surface of the arena-plm connectivity worker: Arena PLM webhook intake, health probe, and sync-run control.", + "x-sap-stateInfo": { + "state": "beta" + }, + "description": "POC-stage contract for the inbound HTTP surface of the arena-plm connectivity worker (a machine-to-machine TypeScript worker on BTP Cloud Foundry; no UI, no user-facing URLs). This document governs the implementation (API-first).\n\nScope: inbound endpoints only. The worker's outbound legs — the Arena PLM REST API (`api.arenasolutions.com/v1`) and the SAP S/4HANA released OData APIs — are governed by their providers' contracts and are out of scope for this document.\n\nStability: this is a POC-stage contract. The Arena webhook payload (ArenaWebhookEvent) is provisional pending the Arena POC, and the authentication scheme is a placeholder pending PRD open question OQ-6." + }, + "servers": [ + { + "url": "/", + "description": "CF route, decided at deployment (OQ-6)" + } + ], + "tags": [ + { + "name": "system", + "description": "Liveness/readiness for Cloud Foundry health checks." + }, + { + "name": "webhooks", + "description": "Inbound event intake from Arena PLM (ARN-005)." + }, + { + "name": "admin", + "description": "Machine-to-machine administrative operations (sync run control)." + } + ], + "paths": { + "/health": { + "get": { + "operationId": "getHealth", + "summary": "Liveness/readiness probe", + "description": "Used by Cloud Foundry health checks. Unauthenticated by design; exposes no business data. `arenaSession.state` reflects the worker's serialized Arena session lifecycle (Arena sessions have a ~25-minute TTL and logins must be serialized; see KNOWLEDGE.md). `poller` reports the delta poller's most recent tick; it is absent until the poller has ticked at least once.", + "tags": [ + "system" + ], + "security": [], + "responses": { + "200": { + "description": "Worker is up (HTTP 200 in both states — this is a liveness probe, not a readiness gate). `status` is `degraded` when the most recent delta-poll tick failed (an Arena error, or the ingest buffer being full at poll time — the tick is failed without advancing the cursor so the window is re-covered) or the most recent Arena login failed; it returns to `ok` on the next successful tick/login.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HealthStatus" + }, + "examples": { + "ok": { + "summary": "Fully operational", + "value": { + "status": "ok", + "version": "0.1.0", + "uptimeSeconds": 5231.7, + "arenaSession": { + "state": "active" + }, + "poller": { + "lastRunAt": "2026-09-14T08:15:00Z", + "lastSuccessAt": "2026-09-14T08:15:00Z" + } + } + }, + "degraded": { + "summary": "Last poll tick failed", + "value": { + "status": "degraded", + "version": "0.1.0", + "uptimeSeconds": 5531.2, + "arenaSession": { + "state": "expired" + }, + "poller": { + "lastRunAt": "2026-09-14T08:20:00Z", + "lastSuccessAt": "2026-09-14T08:15:00Z", + "lastError": "Upstream Arena PLM request failed (HTTP 503)." + } + } + } + } + } + } + }, + "default": { + "$ref": "#/components/responses/Default" + } + } + } + }, + "/webhooks/arena": { + "post": { + "operationId": "receiveArenaWebhook", + "summary": "Arena PLM event intake", + "description": "Receives Arena PLM webhook events (ARN-005). The worker validates only that the body is a JSON object, then queues the event for asynchronous processing; acceptance (202) does not imply the event was processed or will result in a staged change. The ingest queue is a bounded transient buffer (`WEBHOOK_QUEUE_MAX_SIZE`, default 10000; never durable worker-local storage — ARN-003); when it is full the event is NOT accepted and the sender must retry (503 with `Retry-After`). The request body schema (ArenaWebhookEvent) is PROVISIONAL pending the Arena POC and will be tightened then.", + "tags": [ + "webhooks" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ArenaWebhookEvent" + }, + "example": { + "eventType": "ITEM_MODIFIED", + "itemGuid": "K3C9D0E1F2", + "itemNumber": "900-00042", + "occurredAt": "2026-09-10T08:15:30Z" + } + } + } + }, + "responses": { + "202": { + "description": "Event accepted and queued for asynchronous processing.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/WebhookAccepted" + }, + "example": { + "received": true + } + } + } + }, + "400": { + "description": "Malformed request (e.g. body is not a JSON object).", + "content": { + "application/problem+json": { + "schema": { + "$ref": "#/components/schemas/Problem" + }, + "example": { + "title": "Bad Request", + "status": 400, + "detail": "Request body must be a JSON object." + } + } + } + }, + "401": { + "$ref": "#/components/responses/Unauthorized" + }, + "413": { + "$ref": "#/components/responses/PayloadTooLarge" + }, + "415": { + "$ref": "#/components/responses/UnsupportedMediaType" + }, + "503": { + "description": "Ingest queue is full (bounded transient buffer at `WEBHOOK_QUEUE_MAX_SIZE`). The event was NOT queued; the sender should retry after the `Retry-After` delay. Backpressure is preferred over unbounded memory growth or spilling to worker-local storage (ARN-003).", + "headers": { + "Retry-After": { + "description": "Seconds the sender should wait before retrying (RFC 9110 §10.2.3, delay-seconds form). The worker sends 30.", + "schema": { + "type": "integer", + "minimum": 1 + }, + "example": 30 + } + }, + "content": { + "application/problem+json": { + "schema": { + "$ref": "#/components/schemas/Problem" + }, + "example": { + "title": "Service Unavailable", + "status": 503, + "detail": "Ingest queue is full; retry later." + } + } + } + }, + "default": { + "$ref": "#/components/responses/Default" + } + } + } + }, + "/admin/sync-runs": { + "post": { + "operationId": "startSyncRun", + "summary": "Start a sync run", + "description": "Starts a sync run (full or delta). When `objects` is omitted, the run uses the worker's configured chain order (default PRODUCT → CHANGE_MASTER → BOM → DOCUMENTS; may be a subset via SYNC_OBJECT_ORDER) — requirement ARN-001: config-driven sync chain order. When `objects` is provided, it selects and orders the object types for this run.", + "tags": [ + "admin" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SyncRunRequest" + }, + "example": { + "scope": "full" + } + } + } + }, + "responses": { + "201": { + "description": "Sync run created. Execution is asynchronous; poll `GET /admin/sync-runs/{runId}` for progress. All entries in `objects` start as `pending` with no `processed` counter (it appears once the object has started).", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SyncRun" + }, + "example": { + "runId": "7b1e9a4c-2f6d-4c3a-9e8b-5d0f1a2b3c4d", + "scope": "full", + "state": "queued", + "startedAt": "2026-09-10T08:20:00Z", + "objects": [ + { + "objectType": "PRODUCT", + "state": "pending" + }, + { + "objectType": "CHANGE_MASTER", + "state": "pending" + }, + { + "objectType": "BOM", + "state": "pending" + }, + { + "objectType": "DOCUMENTS", + "state": "pending" + } + ] + } + } + } + }, + "400": { + "description": "Malformed request: invalid `scope` value, `objects` containing an unknown or duplicate object type, a property of the wrong type (no coercion — `{\"scope\": [\"full\"]}` or `{\"objects\": \"BOM\"}` are rejected), or any property not defined by SyncRunRequest (strict rejection, `additionalProperties: false`).", + "content": { + "application/problem+json": { + "schema": { + "$ref": "#/components/schemas/Problem" + }, + "example": { + "title": "Bad Request", + "status": 400, + "detail": "Invalid value 'incremental' for 'scope'; expected 'full' or 'delta'." + } + } + } + }, + "401": { + "$ref": "#/components/responses/Unauthorized" + }, + "413": { + "$ref": "#/components/responses/PayloadTooLarge" + }, + "415": { + "$ref": "#/components/responses/UnsupportedMediaType" + }, + "default": { + "$ref": "#/components/responses/Default" + } + } + } + }, + "/admin/sync-runs/{runId}": { + "get": { + "operationId": "getSyncRun", + "summary": "Get a sync run by ID", + "description": "Returns the current state of a sync run, including per-object progress in execution order.", + "tags": [ + "admin" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "parameters": [ + { + "name": "runId", + "in": "path", + "required": true, + "description": "Identifier of the sync run, as returned by `POST /admin/sync-runs`.", + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "responses": { + "200": { + "description": "The sync run.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SyncRun" + }, + "example": { + "runId": "7b1e9a4c-2f6d-4c3a-9e8b-5d0f1a2b3c4d", + "scope": "full", + "state": "failed", + "startedAt": "2026-09-10T08:20:00Z", + "finishedAt": "2026-09-10T08:34:12Z", + "error": "BOM: Upstream Arena PLM request failed (HTTP 503).", + "objects": [ + { + "objectType": "PRODUCT", + "state": "completed", + "processed": 15 + }, + { + "objectType": "CHANGE_MASTER", + "state": "completed", + "processed": 4 + }, + { + "objectType": "BOM", + "state": "failed", + "processed": 9, + "error": "Upstream Arena PLM request failed (HTTP 503)." + }, + { + "objectType": "DOCUMENTS", + "state": "pending" + } + ] + } + } + } + }, + "401": { + "$ref": "#/components/responses/Unauthorized" + }, + "404": { + "description": "No sync run exists with the given `runId`.", + "content": { + "application/problem+json": { + "schema": { + "$ref": "#/components/schemas/Problem" + }, + "example": { + "title": "Not Found", + "status": 404, + "detail": "Sync run 7b1e9a4c-2f6d-4c3a-9e8b-5d0f1a2b3c4d not found." + } + } + } + }, + "default": { + "$ref": "#/components/responses/Default" + } + } + } + } + }, + "components": { + "securitySchemes": { + "bearerAuth": { + "type": "http", + "scheme": "bearer", + "description": "PLACEHOLDER. The final authentication scheme for webhook and admin access is an OPEN decision (PRD OQ-6: webhook receiver hosting and auth for the worker on CF). This scheme stands in for whatever is decided (e.g. XSUAA-issued JWT, static bearer token) and this contract will be revised when OQ-6 is closed. Only `/health` is unauthenticated." + } + }, + "responses": { + "Unauthorized": { + "description": "Missing or invalid bearer credentials.", + "content": { + "application/problem+json": { + "schema": { + "$ref": "#/components/schemas/Problem" + }, + "example": { + "title": "Unauthorized", + "status": 401, + "detail": "Missing or invalid bearer token." + } + } + } + }, + "BadGateway": { + "description": "An upstream Arena PLM request failed (network error, timeout, or non-success HTTP status from `api.arenasolutions.com`). RESERVED mapping: every operation that invokes Arena synchronously responds with this instead of 500. The upstream response body is never forwarded — `detail` is a fixed, generic phrase — so Arena error payloads (which may echo request data) cannot leak through the worker. No operation in this version calls Arena synchronously (sync runs and polls are asynchronous); the mapping is defined here so it is contract-governed before the first such operation lands.", + "content": { + "application/problem+json": { + "schema": { + "$ref": "#/components/schemas/Problem" + }, + "example": { + "title": "Bad Gateway", + "status": 502, + "detail": "Upstream Arena PLM request failed." + } + } + } + }, + "Default": { + "description": "Any status code not listed explicitly for the operation. Every response the worker emits is governed: the body is always a `Problem` (RFC 9457, `application/problem+json`). Cases: an unmatched route or method (404, see `x-unmatched-routes`); a status raised by the HTTP framework before the handler runs (e.g. 400 for a `Content-Length` mismatch or an empty `application/json` body); an unhandled error (500). 5xx bodies never carry a `detail` derived from an error message — `title` mirrors the HTTP status phrase and nothing else is disclosed.", + "content": { + "application/problem+json": { + "schema": { + "$ref": "#/components/schemas/Problem" + }, + "example": { + "title": "Internal Server Error", + "status": 500 + } + } + } + }, + "UnsupportedMediaType": { + "description": "The request `Content-Type` is not `application/json`. Only `application/json` is parsed; every other media type (including `text/plain`, whose built-in parser is removed) and a missing `Content-Type` on a request with a body is rejected before the handler runs. [source: context7 /fastify/fastify docs/Reference/Errors.md FST_ERR_CTP_INVALID_MEDIA_TYPE; docs/Reference/ContentTypeParser.md removeContentTypeParser, verified 2026-09-14]", + "content": { + "application/problem+json": { + "schema": { + "$ref": "#/components/schemas/Problem" + }, + "example": { + "title": "Unsupported Media Type", + "status": 415, + "detail": "Unsupported Media Type" + } + } + } + }, + "PayloadTooLarge": { + "description": "The request body exceeds the worker's body limit (framework default 1 MiB = 1048576 bytes; the worker does not raise it). The body is not parsed and nothing is queued or created. [source: context7 /fastify/fastify docs/Reference/Server.md `bodyLimit`; docs/Reference/Errors.md FST_ERR_CTP_BODY_TOO_LARGE, verified 2026-09-14]", + "content": { + "application/problem+json": { + "schema": { + "$ref": "#/components/schemas/Problem" + }, + "example": { + "title": "Payload Too Large", + "status": 413, + "detail": "Request body is too large" + } + } + } + } + }, + "schemas": { + "HealthStatus": { + "type": "object", + "description": "Worker liveness/readiness report.", + "required": [ + "status", + "version", + "uptimeSeconds", + "arenaSession" + ], + "properties": { + "status": { + "type": "string", + "enum": [ + "ok", + "degraded" + ], + "description": "`ok` when fully operational; `degraded` when the most recent delta-poll tick failed or the most recent Arena login failed. A tick fails when Arena cannot be reached/answers an error, or when the ingest buffer is full at poll time (before the fetch, or partially while enqueuing) — the tick is failed rather than dropping changes, the poll cursor holds, and the next tick re-covers the window (duplicates on recovery are acceptable: SAP is the ledger, KNOWLEDGE.md 2026-09-04). Derived from the worker's health registry, which the poller and the Arena session manager report into." + }, + "version": { + "type": "string", + "description": "Deployed worker version.", + "examples": [ + "0.1.0" + ] + }, + "uptimeSeconds": { + "type": "number", + "minimum": 0, + "description": "Seconds since the worker process started." + }, + "arenaSession": { + "type": "object", + "description": "State of the worker's (serialized) Arena PLM API session.", + "required": [ + "state" + ], + "properties": { + "state": { + "type": "string", + "enum": [ + "none", + "active", + "expired", + "refreshing" + ], + "description": "`none`: no session has been established since process start; `active`: a cached session exists and is within its configured TTL; `expired`: a cached session exists but is past its TTL (the next Arena call re-logs in lazily); `refreshing`: a login is in flight (serialized behind the promise lock — concurrent callers join it). `refreshing` takes precedence over the other three while a login is in flight." + } + } + }, + "poller": { + "type": "object", + "description": "Most recent delta-poll tick. Absent until the poller has ticked at least once. Timestamps are the worker's clock at the start of the tick.", + "properties": { + "lastRunAt": { + "type": "string", + "format": "date-time", + "description": "Start time of the most recent tick (successful or not)." + }, + "lastSuccessAt": { + "type": "string", + "format": "date-time", + "description": "Start time of the most recent tick that completed without error. Absent if no tick has succeeded yet." + }, + "lastError": { + "type": "string", + "description": "Fixed, non-sensitive phrase classifying the failure of the most recent tick (allowlist keyed on error class / upstream HTTP status, e.g. `Upstream Arena PLM request failed (HTTP 503).`, `Ingest buffer full at poll time.`, `Unclassified worker error; see worker logs.`); absent when the most recent tick succeeded. Never the raw error message — upstream response bodies, credentials, and session tokens cannot appear here; the full error stays in the worker log." + } + } + } + } + }, + "ArenaWebhookEvent": { + "type": "object", + "additionalProperties": true, + "description": "PROVISIONAL envelope for Arena PLM webhook events. All fields are optional and unknown properties are accepted (`additionalProperties: true`) because the real Arena webhook payload shape has not yet been verified against the live Arena API. This schema is provisional pending the Arena POC and WILL be tightened (required fields, closed property set, event-type enum) once the POC establishes ground truth. Do not code against fields beyond those named here.", + "properties": { + "eventType": { + "type": "string", + "description": "Arena event type identifier (exact vocabulary to be established by the Arena POC).", + "examples": [ + "ITEM_MODIFIED" + ] + }, + "itemGuid": { + "type": "string", + "description": "Arena internal identifier (GUID) of the affected item, if the event concerns an item." + }, + "itemNumber": { + "type": "string", + "description": "Arena item number of the affected item, if the event concerns an item.", + "examples": [ + "900-00042" + ] + }, + "occurredAt": { + "type": "string", + "format": "date-time", + "description": "When the event occurred in Arena, if provided." + } + } + }, + "WebhookAccepted": { + "type": "object", + "description": "Acknowledgement that a webhook event was accepted and queued.", + "required": [ + "received" + ], + "properties": { + "received": { + "type": "boolean", + "const": true + } + } + }, + "SyncRunRequest": { + "type": "object", + "description": "Request to start a sync run. STRICT: unknown properties are rejected with 400 (seam ruling 2026-09-10 — on a machine-to-machine admin API a typo'd property such as `object` instead of `objects` must never silently fall back to defaults). Values are validated as sent: no type coercion (`{\"scope\": [\"full\"]}` is 400, not unwrapped to `\"full\"`) and no server-side default injection.", + "additionalProperties": false, + "required": [ + "scope" + ], + "properties": { + "scope": { + "type": "string", + "enum": [ + "full", + "delta" + ], + "description": "`full`: initial/complete sync; `delta`: changes since the last successful run." + }, + "objects": { + "type": "array", + "description": "Object types to sync, in execution order. Omit to use the worker's configured chain order (default PRODUCT → CHANGE_MASTER → BOM → DOCUMENTS; may be a subset via SYNC_OBJECT_ORDER) — requirement ARN-001. No schema `default` is declared: the server injects nothing (`useDefaults: false`) and the effective order is the worker's configuration, not a contract constant.", + "items": { + "$ref": "#/components/schemas/SyncObjectType" + }, + "minItems": 1, + "uniqueItems": true + } + } + }, + "SyncObjectType": { + "type": "string", + "enum": [ + "PRODUCT", + "CHANGE_MASTER", + "BOM", + "DOCUMENTS" + ], + "description": "Object types handled by the sync chain (ARN-001: PRODUCT → CHANGE_MASTER → BOM → DOCUMENTS). The token is `DOCUMENTS` (plural), as in the requirement." + }, + "SyncRun": { + "type": "object", + "description": "A sync run and its current state. The run executes its object types sequentially in the order of `objects` (ARN-001) and stops the chain at the first failed object — remaining objects stay `pending`. `finishedAt` is set when the run reaches `completed` or `failed`.", + "required": [ + "runId", + "scope", + "state", + "startedAt", + "objects" + ], + "properties": { + "runId": { + "type": "string", + "format": "uuid", + "description": "Server-assigned identifier of the run." + }, + "scope": { + "type": "string", + "enum": [ + "full", + "delta" + ] + }, + "state": { + "type": "string", + "enum": [ + "queued", + "running", + "completed", + "failed" + ], + "description": "`completed`: every object in the chain completed. `failed`: the run aborted — the chain stopped at a failed object or a run-level error occurred; see `error` and the per-object entries in `objects`." + }, + "startedAt": { + "type": "string", + "format": "date-time", + "description": "When the run was created/accepted." + }, + "finishedAt": { + "type": "string", + "format": "date-time", + "description": "When the run reached a terminal state (`completed` or `failed`). Absent while queued or running." + }, + "objects": { + "type": "array", + "description": "Per-object progress in execution order for this run (ARN-001). An ordered progress array communicates chain order and stop-on-failure directly, which a keyed counts map cannot. All entries are `pending` at run creation.", + "items": { + "$ref": "#/components/schemas/SyncObjectProgress" + }, + "minItems": 1 + }, + "error": { + "type": "string", + "description": "Run-level abort reason; present when `state` is `failed`. The failed object type followed by the same fixed, non-sensitive phrase recorded in that object's `error` (e.g. `BOM: Upstream Arena PLM request failed (HTTP 503).`), or `: no executor registered`. Never a raw error message." + } + } + }, + "SyncObjectProgress": { + "type": "object", + "description": "Progress of one object type within a sync run's sequential execution chain.", + "required": [ + "objectType", + "state" + ], + "properties": { + "objectType": { + "$ref": "#/components/schemas/SyncObjectType" + }, + "state": { + "type": "string", + "enum": [ + "pending", + "running", + "completed", + "failed" + ], + "description": "All entries are `pending` at run creation; each advances to `running`, then `completed` or `failed`, in chain order. A `failed` entry stops the chain; entries after it remain `pending`." + }, + "processed": { + "type": "integer", + "minimum": 0, + "description": "Number of records processed for this object type; present once the object has run (from `running` onwards), absent while `pending`. POC-stage granularity: this will be refined into created/updated/failed counters when the S/4HANA execution leg lands (same tightening wave as ArenaWebhookEvent)." + }, + "error": { + "type": "string", + "description": "Fixed, non-sensitive phrase classifying the failure (allowlist keyed on error class / upstream HTTP status — the same mapping as `HealthStatus.poller.lastError`); present when this object's `state` is `failed`. Never the raw error message: upstream response bodies, credentials, and session tokens cannot appear here; the full error stays in the worker log." + } + } + }, + "Problem": { + "type": "object", + "description": "Error envelope per RFC 9457 (application/problem+json).", + "properties": { + "type": { + "type": "string", + "format": "uri-reference", + "description": "URI reference identifying the problem type. Defaults to `about:blank`.", + "default": "about:blank" + }, + "title": { + "type": "string", + "description": "Short, human-readable summary of the problem type." + }, + "status": { + "type": "integer", + "minimum": 100, + "maximum": 599, + "description": "HTTP status code for this occurrence." + }, + "detail": { + "type": "string", + "description": "Human-readable explanation specific to this occurrence." + }, + "instance": { + "type": "string", + "format": "uri-reference", + "description": "URI reference identifying this specific occurrence." + } + } + } + } + }, + "x-unmatched-routes": "Any request whose method+path is not listed under `paths` is answered 404 with a `Problem` body (application/problem+json), including a known path with an unlisted method — the router answers 404, not 405, for a method mismatch (probed against the worker 2026-09-14; Fastify default not-found handling, see `components.responses.Default`). It is the route-level case of every operation's `default` response and cannot be attached to an operation because no operation matches." +} diff --git a/docs/handover/rap/README.md b/docs/handover/rap/README.md new file mode 100644 index 0000000..811169f --- /dev/null +++ b/docs/handover/rap/README.md @@ -0,0 +1,176 @@ +# RAP handover — Arena change-approval BO (stage 1, 2026-09-15) + +Target: development tenant `my431353` (ADT destination `0M7DEJI`), package `ZARENA_PLM` +(sub-package of `ZLOCAL`, no transport). The ADT MCP Server can create objects and activate +them but cannot write source (PRD V-4), so every file in this folder is a **ready-to-paste +source** for the VS Code ADT editor. Each file header names the target object, its +`abap:` URI and says "replace entire content". + +Every SAP behavioral claim in these files carries a `[source: … , verified 2026-09-15]` +tag; the sources are `abap-docs` (ABAP Cloud keyword docs + SAP samples), `sap-docs` +(SAP Help) and `sap-released-objects` (release states). + +## What exists in the tenant right now + +Generated by `x-ui-service` (all **active**, ATC 0 findings on 12 objects, 2026-09-15): + +| Object | Type | Notes | +|---|---|---| +| `ZZCHNGPPROVAL000` | TABL | persistent table (generator naming) | +| `ZZR_CHANGEAPPROVAL000` | DDLS / BDEF / DCLS | root view entity, managed BDEF, access control | +| `ZZBP_R_CHANGEAPPROVAL000` | CLAS | behavior pool of the base BDEF | +| `ZZC_CHANGEAPPROVAL000` | DDLS / BDEF / DDLX / DCLS | UI projection view, projection BDEF, metadata extension, access control | +| `ZZBP_C_CHANGEAPPROVAL000` | CLAS | projection behavior pool — **delete it** (step 9) | +| `ZZUI_ARENA_CHANGE_APPRVL000_O4` | SRVD + SRVB | service definition and OData V4 **UI** binding (same name), **unpublished** | +| `ZZARENA_CHANGE_APPROVAL000` | RONT + NONT | SAP object type / node type | + +Created empty via `create_object` (exist, **no source, inactive**): + +| Object | Type | Purpose | +|---|---|---| +| `ZD_CHANGEAPPROVAL_REJECTP` | DDLS | abstract entity — `reject` parameter (`RejectReason`) | +| `ZD_CHANGEAPPROVAL_SYNCEDP` | DDLS | abstract entity — `markSynced` parameter (`SapChangeNumber`) | +| `ZD_CHANGEAPPROVAL_FAILEDP` | DDLS | abstract entity — `markFailed` parameter (`ErrorMessage`) | +| `ZC_CHANGEAPPROVAL_A2X` | DDLS + DCLS | A2X projection view (worker) and its access control | +| `ZAPI_ARENA_CHANGE_APPROVAL` | SRVD | Web API service definition (worker) | + +Not yet creatable (the MCP validation refuses until the referenced objects are active): + +| Object | Type | Blocker seen 2026-09-15 | +|---|---|---| +| `ZC_CHANGEAPPROVAL_A2X` | BDEF (projection) | "The referenced STOB object ZC_CHANGEAPPROVAL_A2X does not exist" | +| `ZAPI_ARENA_CHANGE_APPROVAL_A2X` | SRVB (OData V4 - Web API) | "An active version of the Service Definition ZAPI_ARENA_CHANGE_APPROVAL does not exist" | + +## Paste-and-activate sequence + +Open each object in the VS Code ADT editor (or ADT Eclipse), replace the whole source with +the file content, save, activate. Activation order matters — dependencies are top-down. + +1. `zzchngpproval000.tabl.asddls` → table `ZZCHNGPPROVAL000`. (File name uses the + abap-file-formats extension by analogy with `.ddls.asddls`; the tenant object is the + same regardless.) +2. `zd_changeapproval_rejectp.ddls.asddls`, `zd_changeapproval_syncedp.ddls.asddls`, + `zd_changeapproval_failedp.ddls.asddls` → the three abstract entities. +3. `zzr_changeapproval000.ddls.asddls` → root view entity. +4. `zzr_changeapproval000.bdef.asbdef` → base BDEF. + Watch for a message about the unmapped elements `StatusText`, `StatusCriticality`, + `IsApprovable`, `IsRetriable` (CDS-calculated, deliberately not in the mapping). A + warning is acceptable; an **error** means the fallback in the file header applies. +5. `zzbp_r_changeapproval000.clas.abap` (global class) and + `zzbp_r_changeapproval000.clas.locals_imp.abap` (Local Types tab) → behavior pool. +6. `zzc_changeapproval000.ddls.asddls` and `zc_changeapproval_a2x.ddls.asddls` → both + projection views. +7. `zzc_changeapproval000.bdef.asbdef` → UI projection BDEF. +8. Create the A2X projection BDEF (now possible): ADT *New Behavior Definition* on + `ZC_CHANGEAPPROVAL_A2X`, implementation type *Projection* — or via MCP: + `abap_creation-run_validation` + `create_object` with objectType `BDEF/BDO` and + `{"behaviorDefinitionType":"definition","packageName":"ZARENA_PLM","rootEntity":"ZC_CHANGEAPPROVAL_A2X","name":"ZC_CHANGEAPPROVAL_A2X","description":"Arena change approval: A2X projection behavior","implementationType":"Projection"}`, + transport `""`. Then paste `zc_changeapproval_a2x.bdef.asbdef`, activate. +9. Delete the generated class `ZZBP_C_CHANGEAPPROVAL000` (no longer referenced by the + projection BDEF; a projection needs a behavior pool only for new actions/functions + [source: abap-docs /abap-docs-cloud/ABENBDL_BDEF_PROJECTION_HEADER, verified 2026-09-15]). +10. `zzr_changeapproval000.dcls.asdcls`, then `zzc_changeapproval000.dcls.asdcls` and + `zc_changeapproval_a2x.dcls.asdcls` → access controls. +11. `zzc_changeapproval000.ddlx.asddlx` → metadata extension. +12. `zzui_arena_change_apprvl000_o4.srvd.srvdsrv` and + `zapi_arena_change_approval.srvd.srvdsrv` → service definitions. +13. Create the Web API binding (now possible): ADT *New Service Binding* on + `ZAPI_ARENA_CHANGE_APPROVAL`, binding type *OData V4 - Web API*, name + `ZAPI_ARENA_CHANGE_APPROVAL_A2X` — or via MCP `create_object` with objectType + `SRVB/SVB` and + `{"packageName":"ZARENA_PLM","name":"ZAPI_ARENA_CHANGE_APPROVAL_A2X","description":"Arena change approval: OData V4 Web API binding (worker)","bindingType":"OData V4 - Web API","serviceDefinition":"ZAPI_ARENA_CHANGE_APPROVAL"}`. + Activate. **Do not publish** either binding (publishing is a later brief, together + with the IAM app / communication scenario). + +## Activation URIs for `abap_activate_objects` (after pasting) + +Pattern learned live 2026-09-15: `abap:/repotree-v1/0M7DEJI/System%20Library/ZLOCAL/ZARENA_PLM///.` +with these folders/extensions (all verified by a successful activation): + +| Type | Folder | Extension | +|---|---|---| +| TABL | `Dictionary/Database%20Tables` | `.tabl.json` | +| DDLS | `Core%20Data%20Services/Data%20Definitions` | `.ddls.acds` | +| BDEF | `Core%20Data%20Services/Behavior%20Definitions` | `.bdef.json` (`.bdef.acds`, `.bdef.asbdef`, `.bdef.abap` all fail with "array element 0 is null") | +| DDLX | `Core%20Data%20Services/Metadata%20Extensions` | `.ddlx.acds` | +| DCLS | `Core%20Data%20Services/Access%20Controls` | `.dcls.acds` | +| CLAS | `Source%20Code%20Library/Classes` | `.clas.abap` | +| SRVD | `Business%20Services/Service%20Definitions` | `.srvd.acds` | +| SRVB | `Business%20Services/Service%20Bindings` | `.srvb.json` | +| RONT | `Business%20Services/SAP%20Object%20Types` | `.ront.json` | +| NONT | `Business%20Services/SAP%20Object%20Node%20Types` | `.nont.json` | + +In sequence order (`B` = `abap:/repotree-v1/0M7DEJI/System%20Library/ZLOCAL/ZARENA_PLM`): + +``` +B/Dictionary/Database%20Tables/ZZCHNGPPROVAL000/zzchngpproval000.tabl.json +B/Core%20Data%20Services/Data%20Definitions/ZD_CHANGEAPPROVAL_REJECTP/zd_changeapproval_rejectp.ddls.acds +B/Core%20Data%20Services/Data%20Definitions/ZD_CHANGEAPPROVAL_SYNCEDP/zd_changeapproval_syncedp.ddls.acds +B/Core%20Data%20Services/Data%20Definitions/ZD_CHANGEAPPROVAL_FAILEDP/zd_changeapproval_failedp.ddls.acds +B/Core%20Data%20Services/Data%20Definitions/ZZR_CHANGEAPPROVAL000/zzr_changeapproval000.ddls.acds +B/Core%20Data%20Services/Behavior%20Definitions/ZZR_CHANGEAPPROVAL000/zzr_changeapproval000.bdef.json +B/Source%20Code%20Library/Classes/ZZBP_R_CHANGEAPPROVAL000/zzbp_r_changeapproval000.clas.abap +B/Core%20Data%20Services/Data%20Definitions/ZZC_CHANGEAPPROVAL000/zzc_changeapproval000.ddls.acds +B/Core%20Data%20Services/Data%20Definitions/ZC_CHANGEAPPROVAL_A2X/zc_changeapproval_a2x.ddls.acds +B/Core%20Data%20Services/Behavior%20Definitions/ZZC_CHANGEAPPROVAL000/zzc_changeapproval000.bdef.json +B/Core%20Data%20Services/Behavior%20Definitions/ZC_CHANGEAPPROVAL_A2X/zc_changeapproval_a2x.bdef.json +B/Core%20Data%20Services/Access%20Controls/ZZR_CHANGEAPPROVAL000/zzr_changeapproval000.dcls.acds +B/Core%20Data%20Services/Access%20Controls/ZZC_CHANGEAPPROVAL000/zzc_changeapproval000.dcls.acds +B/Core%20Data%20Services/Access%20Controls/ZC_CHANGEAPPROVAL_A2X/zc_changeapproval_a2x.dcls.acds +B/Core%20Data%20Services/Metadata%20Extensions/ZZC_CHANGEAPPROVAL000/zzc_changeapproval000.ddlx.acds +B/Business%20Services/Service%20Definitions/ZZUI_ARENA_CHANGE_APPRVL000_O4/zzui_arena_change_apprvl000_o4.srvd.acds +B/Business%20Services/Service%20Definitions/ZAPI_ARENA_CHANGE_APPROVAL/zapi_arena_change_approval.srvd.acds +B/Business%20Services/Service%20Bindings/ZZUI_ARENA_CHANGE_APPRVL000_O4/zzui_arena_change_apprvl000_o4.srvb.json +B/Business%20Services/Service%20Bindings/ZAPI_ARENA_CHANGE_APPROVAL_A2X/zapi_arena_change_approval_a2x.srvb.json +``` + +Then re-run ATC on the package objects (`abap_atc_run`; omit RONT/NONT — the ATC API +answers "No valid data model exists for object type RONT/ROT") and +`abap_business_services-fetch_services` on both bindings. + +## Design decisions baked into the files (falsifiable, dated 2026-09-15) + +- **Two projections, two service definitions.** The brief asked for both bindings on the + generated service definition *and* for the UI to expose only approve/reject/retrySync + while the worker gets create + mark*. A service definition exposes one projection, and a + projection BDEF exposes exactly what it lists (no automatic inheritance + [source: abap-docs /abap-docs-cloud/ABENBDL_USE_PROJECTION]). Both requirements cannot + hold on one definition, so the A2X path got its own projection view/BDEF/definition and + the Web API binding sits on `ZAPI_ARENA_CHANGE_APPROVAL`. The RAP-2 "read-only from the + UI" requirement wins over the structural wording. +- **`authorization master ( none )`** instead of the generator's `( global )`: ADR-002 + makes the IAM app / business catalog the only access control and DCL governs reads; + `( none )` is the documented way to declare "no RAP authorization checks" + [source: abap-docs /abap-docs-cloud/ABENBDL_AUTHORIZATION]. Flip to `( global )` plus a + `FOR GLOBAL AUTHORIZATION` handler when a custom authorization object exists. +- **Derived fields are computed, not persisted.** `StatusText`, `StatusCriticality`, + `IsApprovable`, `IsRetriable` are CASE expressions in the root view; a transactional + query projection cannot hold expressions + [source: abap-docs /abap-docs-cloud/ABENCDS_DEFINE_VIEW_AS_PROJECTION]. They are + `field ( readonly )` and absent from the mapping — see step 4. +- **Action parameters are abstract entities** (`ZD_*`), one element each, because action + input must be an abstract entity or DDIC type + [source: abap-docs /abap-docs-cloud/ABENBDL_ACTION_INPUT_PARAM]. `markSynced` and + `markFailed` therefore take `SapChangeNumber` / `ErrorMessage` as parameters — the only + way an action can "set" them from the worker. +- **`internal update`** keeps `update` out of every OData surface while the actions and + the determination still update fields `IN LOCAL MODE` + [source: abap-docs /abap-docs-cloud/ABENBDL_INTERNAL]. +- **RequestedBy** is the technical user of the creator (the worker's communication user in + the A2X flow). Change to `mandatory : create` if the Arena requester should be shown. + +## Landmines learned live (KNOWLEDGE.md candidates) + +- [2026-09-15] `x-ui-service` generator: every field needs `semanticType` (e.g. `custom`) + or validation fails with "Builtin type of field X is invalid"; semantic-key fields pass + without it. The generator prepends its own `Z` to the `prefix` (`Z` → `ZZ…`) and appends + `000` to every artifact name even with `suffix: ""`; service definition and UI binding + get the same name (`ZZUI_ARENA_CHANGE_APPRVL000_O4`). There is no delete tool in the ADT + MCP, so a naming mistake is permanent from the agent side. +- [2026-09-15] `create_object` for SRVD needs `sourceType` (`"S"` worked; omitted → + "serviceDefinitionInfo.sourceType is null"). BDEF/SRVB creation validates the referenced + DDLS/SRVD **active** version, so source-less objects block dependent creations. +- [2026-09-15] Activation URIs: see table above; BDEF is `.bdef.json`, table `.tabl.json`, + SAP object types live under `Business Services`. +- [2026-09-15] The MCP session id from `get_schema` `referenceContent` is single-use in + practice — fetch a fresh one before each `generate_objects` attempt. diff --git a/docs/handover/rap/zapi_arena_change_approval.srvd.srvdsrv b/docs/handover/rap/zapi_arena_change_approval.srvd.srvdsrv new file mode 100644 index 0000000..bf86e82 --- /dev/null +++ b/docs/handover/rap/zapi_arena_change_approval.srvd.srvdsrv @@ -0,0 +1,10 @@ +/* TARGET OBJECT : service definition ZAPI_ARENA_CHANGE_APPROVAL (created empty via ADT MCP 2026-09-15) + abap: URI : abap:/repotree-v1/0M7DEJI/System%20Library/ZLOCAL/ZARENA_PLM/Business%20Services/Service%20Definitions/ZAPI_ARENA_CHANGE_APPROVAL/zapi_arena_change_approval.srvd.acds + ACTION : replace entire content, then activate (after ZC_CHANGEAPPROVAL_A2X view + BDEF). + Sources: as ZZUI_ARENA_CHANGE_APPRVL000_O4; odata_v4_webapi is the Web API contract [ABENSRVD_PROVIDER_CONTRACT]. */ +@EndUserText.label: 'Arena change approval - Web API (worker)' +define service ZAPI_ARENA_CHANGE_APPROVAL + provider contracts odata_v4_webapi +{ + expose ZC_CHANGEAPPROVAL_A2X as ChangeApproval; +} diff --git a/docs/handover/rap/zc_changeapproval_a2x.bdef.asbdef b/docs/handover/rap/zc_changeapproval_a2x.bdef.asbdef new file mode 100644 index 0000000..582c479 --- /dev/null +++ b/docs/handover/rap/zc_changeapproval_a2x.bdef.asbdef @@ -0,0 +1,19 @@ +// TARGET OBJECT : behavior definition ZC_CHANGEAPPROVAL_A2X (A2X projection BDEF) - DOES NOT EXIST YET. +// The ADT MCP refused to create it while ZC_CHANGEAPPROVAL_A2X (DDLS) had no active source +// ("The referenced STOB object ZC_CHANGEAPPROVAL_A2X does not exist", 2026-09-15). +// Create it after the projection view is active: ADT > New Behavior Definition on ZC_CHANGEAPPROVAL_A2X, +// implementation type Projection (or re-run abap_creation-create_object BDEF/BDO, see README), paste, activate. +// Sources: as ZZC_CHANGEAPPROVAL000. use create lets the worker stage records (ARN-010: the staged payload is what +// gets executed); the worker never sees approve/reject/retrySync (PRD RAP-6). +projection; +strict ( 2 ); + +define behavior for ZC_CHANGEAPPROVAL_A2X alias ChangeApproval +{ + use etag; + use create; + + use action markExecuting; + use action markSynced; + use action markFailed; +} diff --git a/docs/handover/rap/zc_changeapproval_a2x.dcls.asdcls b/docs/handover/rap/zc_changeapproval_a2x.dcls.asdcls new file mode 100644 index 0000000..a99d774 --- /dev/null +++ b/docs/handover/rap/zc_changeapproval_a2x.dcls.asdcls @@ -0,0 +1,10 @@ +/* TARGET OBJECT : access control ZC_CHANGEAPPROVAL_A2X (created empty via ADT MCP 2026-09-15) + abap: URI : abap:/repotree-v1/0M7DEJI/System%20Library/ZLOCAL/ZARENA_PLM/Core%20Data%20Services/Access%20Controls/ZC_CHANGEAPPROVAL_A2X/zc_changeapproval_a2x.dcls.acds + ACTION : replace entire content, then activate (after ZC_CHANGEAPPROVAL_A2X view and ZZR_CHANGEAPPROVAL000 DCL). + Sources: as ZZC_CHANGEAPPROVAL000 DCL. */ +@EndUserText.label: 'Access control for ZC_CHANGEAPPROVAL_A2X' +@MappingRole: true +define role ZC_CHANGEAPPROVAL_A2X { + grant select on ZC_CHANGEAPPROVAL_A2X + where inheriting conditions from entity ZZR_CHANGEAPPROVAL000; +} diff --git a/docs/handover/rap/zc_changeapproval_a2x.ddls.asddls b/docs/handover/rap/zc_changeapproval_a2x.ddls.asddls new file mode 100644 index 0000000..01b8b44 --- /dev/null +++ b/docs/handover/rap/zc_changeapproval_a2x.ddls.asddls @@ -0,0 +1,38 @@ +/* TARGET OBJECT : data definition ZC_CHANGEAPPROVAL_A2X (A2X projection for the worker; created empty via ADT MCP 2026-09-15) + abap: URI : abap:/repotree-v1/0M7DEJI/System%20Library/ZLOCAL/ZARENA_PLM/Core%20Data%20Services/Data%20Definitions/ZC_CHANGEAPPROVAL_A2X/zc_changeapproval_a2x.ddls.acds + ACTION : replace entire content, then activate (after ZZR_CHANGEAPPROVAL000). + Notes: separate projection so the Web API exposes create + mark* while the UI projection stays read-only + (PRD RAP-2 / RAP-6); same syntax sources as ZZC_CHANGEAPPROVAL000. No metadata extension here - + a Web API binding carries no UI annotations [source: sap-docs sap-help loio 248b36b7ebce4f7e8c7670c9371adee1, verified 2026-09-15]. */ +@EndUserText.label: 'Arena change approval - A2X projection (worker)' +@AccessControl.authorizationCheck: #CHECK +@ObjectModel.semanticKey: ['ItemNumber', 'ChangeType', 'ArenaEcoId'] +define root view entity ZC_CHANGEAPPROVAL_A2X + provider contract transactional_query + as projection on ZZR_CHANGEAPPROVAL000 +{ + key ID, + ItemNumber, + ItemName, + ChangeType, + ArenaEcoId, + @ObjectModel.text.element: ['StatusText'] + Status, + StatusText, + StatusCriticality, + RequestedBy, + RequestedOn, + DecidedBy, + DecidedOn, + SapChangeNumber, + ErrorMessage, + RejectReason, + SapPreview, + IsApprovable, + IsRetriable, + CreatedBy, + CreatedAt, + LastChangedBy, + LastChangedAt, + LocalLastChangedAt +} diff --git a/docs/handover/rap/zd_changeapproval_failedp.ddls.asddls b/docs/handover/rap/zd_changeapproval_failedp.ddls.asddls new file mode 100644 index 0000000..53c534f --- /dev/null +++ b/docs/handover/rap/zd_changeapproval_failedp.ddls.asddls @@ -0,0 +1,10 @@ +/* TARGET OBJECT : data definition ZD_CHANGEAPPROVAL_FAILEDP (abstract entity = parameter of action markFailed; created empty via ADT MCP 2026-09-15) + abap: URI : abap:/repotree-v1/0M7DEJI/System%20Library/ZLOCAL/ZARENA_PLM/Core%20Data%20Services/Data%20Definitions/ZD_CHANGEAPPROVAL_FAILEDP/zd_changeapproval_failedp.ddls.acds + ACTION : replace entire content, then activate. + Sources: as ZD_CHANGEAPPROVAL_REJECTP. */ +@EndUserText.label: 'Arena change approval: markFailed parameter' +define abstract entity ZD_CHANGEAPPROVAL_FAILEDP +{ + @EndUserText.label: 'Error message' + ErrorMessage : abap.char(255); +} diff --git a/docs/handover/rap/zd_changeapproval_rejectp.ddls.asddls b/docs/handover/rap/zd_changeapproval_rejectp.ddls.asddls new file mode 100644 index 0000000..f66e9eb --- /dev/null +++ b/docs/handover/rap/zd_changeapproval_rejectp.ddls.asddls @@ -0,0 +1,13 @@ +/* TARGET OBJECT : data definition ZD_CHANGEAPPROVAL_REJECTP (abstract entity = parameter of action reject; created empty via ADT MCP 2026-09-15) + abap: URI : abap:/repotree-v1/0M7DEJI/System%20Library/ZLOCAL/ZARENA_PLM/Core%20Data%20Services/Data%20Definitions/ZD_CHANGEAPPROVAL_REJECTP/zd_changeapproval_rejectp.ddls.acds + ACTION : replace entire content, then activate. + Notes: action input parameters must be a CDS abstract entity or DDIC type; @EndUserText.label on the element + gives the UI label [source: abap-docs /abap-docs-cloud/ABENBDL_ACTION_INPUT_PARAM, verified 2026-09-15]; + abstract-entity syntax [source: abap-docs /abap-docs-cloud/ABENBDL_ACTION2_ABEXA, verified 2026-09-15]. + The OData parameter name is the element name -> RejectReason, as the Fiori app expects (PRD RAP-2). */ +@EndUserText.label: 'Arena change approval: reject parameter' +define abstract entity ZD_CHANGEAPPROVAL_REJECTP +{ + @EndUserText.label: 'Reject reason' + RejectReason : abap.char(255); +} diff --git a/docs/handover/rap/zd_changeapproval_syncedp.ddls.asddls b/docs/handover/rap/zd_changeapproval_syncedp.ddls.asddls new file mode 100644 index 0000000..97f912f --- /dev/null +++ b/docs/handover/rap/zd_changeapproval_syncedp.ddls.asddls @@ -0,0 +1,10 @@ +/* TARGET OBJECT : data definition ZD_CHANGEAPPROVAL_SYNCEDP (abstract entity = parameter of action markSynced; created empty via ADT MCP 2026-09-15) + abap: URI : abap:/repotree-v1/0M7DEJI/System%20Library/ZLOCAL/ZARENA_PLM/Core%20Data%20Services/Data%20Definitions/ZD_CHANGEAPPROVAL_SYNCEDP/zd_changeapproval_syncedp.ddls.acds + ACTION : replace entire content, then activate. + Sources: as ZD_CHANGEAPPROVAL_REJECTP. */ +@EndUserText.label: 'Arena change approval: markSynced parameter' +define abstract entity ZD_CHANGEAPPROVAL_SYNCEDP +{ + @EndUserText.label: 'SAP change number' + SapChangeNumber : abap.char(12); +} diff --git a/docs/handover/rap/zzbp_r_changeapproval000.clas.abap b/docs/handover/rap/zzbp_r_changeapproval000.clas.abap new file mode 100644 index 0000000..27d11c6 --- /dev/null +++ b/docs/handover/rap/zzbp_r_changeapproval000.clas.abap @@ -0,0 +1,14 @@ +" TARGET OBJECT : class ZZBP_R_CHANGEAPPROVAL000 - GLOBAL CLASS include (behavior pool; generated 2026-09-15) +" abap: URI : abap:/repotree-v1/0M7DEJI/System%20Library/ZLOCAL/ZARENA_PLM/Source%20Code%20Library/Classes/ZZBP_R_CHANGEAPPROVAL000/zzbp_r_changeapproval000.clas.abap +" ACTION : replace entire content of the global class; the handlers go into "Local Types" (locals_imp file). +" Source: FOR BEHAVIOR OF defines the ABAP behavior pool; the global class implements nothing itself +" [source: abap-docs /abap-docs-cloud/ABAPCLASS_FOR_BEHAVIOR_OF, ABENABAP_BEHAVIOR_POOLS, verified 2026-09-15]. +CLASS zzbp_r_changeapproval000 DEFINITION + PUBLIC + ABSTRACT + FINAL + FOR BEHAVIOR OF zzr_changeapproval000. +ENDCLASS. + +CLASS zzbp_r_changeapproval000 IMPLEMENTATION. +ENDCLASS. diff --git a/docs/handover/rap/zzbp_r_changeapproval000.clas.locals_imp.abap b/docs/handover/rap/zzbp_r_changeapproval000.clas.locals_imp.abap new file mode 100644 index 0000000..248a1a5 --- /dev/null +++ b/docs/handover/rap/zzbp_r_changeapproval000.clas.locals_imp.abap @@ -0,0 +1,426 @@ +" TARGET OBJECT : class ZZBP_R_CHANGEAPPROVAL000 - LOCAL TYPES include (Local Types tab in ADT) +" abap: URI : abap:/repotree-v1/0M7DEJI/System%20Library/ZLOCAL/ZARENA_PLM/Source%20Code%20Library/Classes/ZZBP_R_CHANGEAPPROVAL000/zzbp_r_changeapproval000.clas.locals_imp.abap +" ACTION : replace entire content, then activate the class (BDEF ZZR_CHANGEAPPROVAL000 must be active first). +" +" Sources (all verified 2026-09-15): +" handler class inherits from CL_ABAP_BEHAVIOR_HANDLER (released) [sap-released-objects public_cloud A] +" FOR INSTANCE FEATURES signature + result via %tky / %features-%action- / if_abap_behv=>fc-o-enabled|disabled +" [abap-docs /abap-docs-cloud/ABAPHANDLER_METH_FEATURES, ABAPDERIVED_TYPES_FEATURES, ABENBDL_ACTIONS_FC] +" FOR MODIFY ... FOR ACTION with RESULT; %param carries the action parameter (keys) and the result ($self) +" [abap-docs /abap-docs-cloud/ABENBDL_ACTION2_ABEXA, ABAPDERIVED_TYPES_PARAM] +" READ ENTITIES ... IN LOCAL MODE ... FIELDS ( ) WITH CORRESPONDING #( keys ) RESULT ... FAILED failed +" [abap-docs /abap-docs-cloud/ABENBDL_ACTIONS_FC example] +" MODIFY ENTITIES ... IN LOCAL MODE (skips feature control and authorization) with UPDATE FIELDS +" [abap-docs /abap-docs-cloud/ABAPMODIFY_ENTITY_ENTITIES, ABAPMODIFY_ENTITY_ENTITIES_OP, ABENBDL_AUTHORIZATION] +" %msg = new_message_with_text( ... ) into reported [abap-docs /abap-docs-cloud/ABENBDL_ACTIONS_FC example] +" FOR DETERMINE ON MODIFY signature [abap-docs /abap-docs-cloud/ABAPHANDLER_METH_DET] +" utclong_current( ) [abap-docs /abap-docs-cloud/ABENUTCLONG_CURRENT] +" cl_abap_context_info=>get_user_technical_name( ) (released class) [sap-docs sap-help loio 7be24435b3e24540a9edad9a2535175a; +" sap-released-objects CL_ABAP_CONTEXT_INFO public_cloud A] +" Design notes: +" - Status transitions are enforced twice: get_instance_features disables the action (UI greys it out / EML fails +" with an entry in failed [ABENBDL_ACTIONS_FC]) AND each action re-checks the current status so an EML caller +" IN LOCAL MODE cannot skip the state machine. +" - The six actions repeat the same read/check/update/read pattern on purpose: each action has its own derived +" key type, so a generic helper would need additional derived-type plumbing that is not worth it for six methods. +" - RequestedBy is the technical user of the creator (for the A2X flow: the worker's communication user). +" If the Arena requester should be shown instead, make RequestedBy mandatory:create in the BDEF and drop it +" from the determination. + +CLASS lhc_changeapproval DEFINITION INHERITING FROM cl_abap_behavior_handler. + PRIVATE SECTION. + + CONSTANTS: + BEGIN OF status, + pending TYPE zzchngpproval000-status VALUE 'PENDING', + approved TYPE zzchngpproval000-status VALUE 'APPROVED', + executing TYPE zzchngpproval000-status VALUE 'EXECUTING', + synced TYPE zzchngpproval000-status VALUE 'SYNCED', + rejected TYPE zzchngpproval000-status VALUE 'REJECTED', + failed TYPE zzchngpproval000-status VALUE 'FAILED', + END OF status. + + METHODS get_instance_features FOR INSTANCE FEATURES + IMPORTING keys REQUEST requested_features FOR ChangeApproval RESULT result. + + METHODS approve FOR MODIFY + IMPORTING keys FOR ACTION ChangeApproval~approve RESULT result. + + METHODS reject FOR MODIFY + IMPORTING keys FOR ACTION ChangeApproval~reject RESULT result. + + METHODS retrySync FOR MODIFY + IMPORTING keys FOR ACTION ChangeApproval~retrySync RESULT result. + + METHODS markExecuting FOR MODIFY + IMPORTING keys FOR ACTION ChangeApproval~markExecuting RESULT result. + + METHODS markSynced FOR MODIFY + IMPORTING keys FOR ACTION ChangeApproval~markSynced RESULT result. + + METHODS markFailed FOR MODIFY + IMPORTING keys FOR ACTION ChangeApproval~markFailed RESULT result. + + METHODS setInitialStatus FOR DETERMINE ON MODIFY + IMPORTING keys FOR ChangeApproval~setInitialStatus. + +ENDCLASS. + + +CLASS lhc_changeapproval IMPLEMENTATION. + + METHOD get_instance_features. + READ ENTITIES OF zzr_changeapproval000 IN LOCAL MODE + ENTITY ChangeApproval + FIELDS ( Status ) + WITH CORRESPONDING #( keys ) + RESULT DATA(approvals) + FAILED failed. + + result = VALUE #( FOR approval IN approvals + ( %tky = approval-%tky + %features-%action-approve = COND #( WHEN approval-Status = status-pending + THEN if_abap_behv=>fc-o-enabled + ELSE if_abap_behv=>fc-o-disabled ) + %features-%action-reject = COND #( WHEN approval-Status = status-pending + THEN if_abap_behv=>fc-o-enabled + ELSE if_abap_behv=>fc-o-disabled ) + %features-%action-retrySync = COND #( WHEN approval-Status = status-failed + THEN if_abap_behv=>fc-o-enabled + ELSE if_abap_behv=>fc-o-disabled ) + %features-%action-markExecuting = COND #( WHEN approval-Status = status-approved + THEN if_abap_behv=>fc-o-enabled + ELSE if_abap_behv=>fc-o-disabled ) + %features-%action-markSynced = COND #( WHEN approval-Status = status-executing + THEN if_abap_behv=>fc-o-enabled + ELSE if_abap_behv=>fc-o-disabled ) + %features-%action-markFailed = COND #( WHEN approval-Status = status-executing + THEN if_abap_behv=>fc-o-enabled + ELSE if_abap_behv=>fc-o-disabled ) ) ). + ENDMETHOD. + + + METHOD approve. + " PENDING -> APPROVED; records the decision, never writes SapChangeNumber (PRD RAP-1) + READ ENTITIES OF zzr_changeapproval000 IN LOCAL MODE + ENTITY ChangeApproval + FIELDS ( Status ) + WITH CORRESPONDING #( keys ) + RESULT DATA(approvals) + FAILED failed. + + LOOP AT keys INTO DATA(key). + ASSIGN approvals[ ID = key-ID ] TO FIELD-SYMBOL(). + IF sy-subrc <> 0. + CONTINUE. + ENDIF. + + IF -Status <> status-pending. + APPEND VALUE #( %tky = -%tky ) TO failed-changeapproval. + APPEND VALUE #( %tky = -%tky + %msg = new_message_with_text( + severity = if_abap_behv_message=>severity-error + text = |Only PENDING records can be approved (status is { -Status })| ) ) + TO reported-changeapproval. + CONTINUE. + ENDIF. + + MODIFY ENTITIES OF zzr_changeapproval000 IN LOCAL MODE + ENTITY ChangeApproval + UPDATE FIELDS ( Status DecidedBy DecidedOn ) + WITH VALUE #( ( %tky = -%tky + Status = status-approved + DecidedBy = cl_abap_context_info=>get_user_technical_name( ) + DecidedOn = utclong_current( ) ) ) + FAILED DATA(update_failed) + REPORTED DATA(update_reported). + ENDLOOP. + + READ ENTITIES OF zzr_changeapproval000 IN LOCAL MODE + ENTITY ChangeApproval + ALL FIELDS WITH CORRESPONDING #( keys ) + RESULT DATA(updated). + + result = VALUE #( FOR row IN updated ( %tky = row-%tky %param = row ) ). + ENDMETHOD. + + + METHOD reject. + " PENDING -> REJECTED with a mandatory reason (PRD RAP-2); REJECTED is terminal + READ ENTITIES OF zzr_changeapproval000 IN LOCAL MODE + ENTITY ChangeApproval + FIELDS ( Status ) + WITH CORRESPONDING #( keys ) + RESULT DATA(approvals) + FAILED failed. + + LOOP AT keys INTO DATA(key). + ASSIGN approvals[ ID = key-ID ] TO FIELD-SYMBOL(). + IF sy-subrc <> 0. + CONTINUE. + ENDIF. + + IF -Status <> status-pending. + APPEND VALUE #( %tky = -%tky ) TO failed-changeapproval. + APPEND VALUE #( %tky = -%tky + %msg = new_message_with_text( + severity = if_abap_behv_message=>severity-error + text = |Only PENDING records can be rejected (status is { -Status })| ) ) + TO reported-changeapproval. + CONTINUE. + ENDIF. + + IF key-%param-RejectReason IS INITIAL. + APPEND VALUE #( %tky = -%tky ) TO failed-changeapproval. + APPEND VALUE #( %tky = -%tky + %msg = new_message_with_text( + severity = if_abap_behv_message=>severity-error + text = 'A reject reason is required' ) ) + TO reported-changeapproval. + CONTINUE. + ENDIF. + + MODIFY ENTITIES OF zzr_changeapproval000 IN LOCAL MODE + ENTITY ChangeApproval + UPDATE FIELDS ( Status RejectReason DecidedBy DecidedOn ) + WITH VALUE #( ( %tky = -%tky + Status = status-rejected + RejectReason = key-%param-RejectReason + DecidedBy = cl_abap_context_info=>get_user_technical_name( ) + DecidedOn = utclong_current( ) ) ) + FAILED DATA(update_failed) + REPORTED DATA(update_reported). + ENDLOOP. + + READ ENTITIES OF zzr_changeapproval000 IN LOCAL MODE + ENTITY ChangeApproval + ALL FIELDS WITH CORRESPONDING #( keys ) + RESULT DATA(updated). + + result = VALUE #( FOR row IN updated ( %tky = row-%tky %param = row ) ). + ENDMETHOD. + + + METHOD retrySync. + " FAILED -> EXECUTING; clears the previous error so the worker re-executes the staged payload (ARN-002 amendment) + READ ENTITIES OF zzr_changeapproval000 IN LOCAL MODE + ENTITY ChangeApproval + FIELDS ( Status ) + WITH CORRESPONDING #( keys ) + RESULT DATA(approvals) + FAILED failed. + + LOOP AT keys INTO DATA(key). + ASSIGN approvals[ ID = key-ID ] TO FIELD-SYMBOL(). + IF sy-subrc <> 0. + CONTINUE. + ENDIF. + + IF -Status <> status-failed. + APPEND VALUE #( %tky = -%tky ) TO failed-changeapproval. + APPEND VALUE #( %tky = -%tky + %msg = new_message_with_text( + severity = if_abap_behv_message=>severity-error + text = |Only FAILED records can be retried (status is { -Status })| ) ) + TO reported-changeapproval. + CONTINUE. + ENDIF. + + MODIFY ENTITIES OF zzr_changeapproval000 IN LOCAL MODE + ENTITY ChangeApproval + UPDATE FIELDS ( Status ErrorMessage ) + WITH VALUE #( ( %tky = -%tky + Status = status-executing + ErrorMessage = '' ) ) + FAILED DATA(update_failed) + REPORTED DATA(update_reported). + ENDLOOP. + + READ ENTITIES OF zzr_changeapproval000 IN LOCAL MODE + ENTITY ChangeApproval + ALL FIELDS WITH CORRESPONDING #( keys ) + RESULT DATA(updated). + + result = VALUE #( FOR row IN updated ( %tky = row-%tky %param = row ) ). + ENDMETHOD. + + + METHOD markExecuting. + " APPROVED -> EXECUTING (worker claims the record; the UI never offers Approve on an executing record, PRD RAP-6) + READ ENTITIES OF zzr_changeapproval000 IN LOCAL MODE + ENTITY ChangeApproval + FIELDS ( Status ) + WITH CORRESPONDING #( keys ) + RESULT DATA(approvals) + FAILED failed. + + LOOP AT keys INTO DATA(key). + ASSIGN approvals[ ID = key-ID ] TO FIELD-SYMBOL(). + IF sy-subrc <> 0. + CONTINUE. + ENDIF. + + IF -Status <> status-approved. + APPEND VALUE #( %tky = -%tky ) TO failed-changeapproval. + APPEND VALUE #( %tky = -%tky + %msg = new_message_with_text( + severity = if_abap_behv_message=>severity-error + text = |Only APPROVED records can start executing (status is { -Status })| ) ) + TO reported-changeapproval. + CONTINUE. + ENDIF. + + MODIFY ENTITIES OF zzr_changeapproval000 IN LOCAL MODE + ENTITY ChangeApproval + UPDATE FIELDS ( Status ) + WITH VALUE #( ( %tky = -%tky + Status = status-executing ) ) + FAILED DATA(update_failed) + REPORTED DATA(update_reported). + ENDLOOP. + + READ ENTITIES OF zzr_changeapproval000 IN LOCAL MODE + ENTITY ChangeApproval + ALL FIELDS WITH CORRESPONDING #( keys ) + RESULT DATA(updated). + + result = VALUE #( FOR row IN updated ( %tky = row-%tky %param = row ) ). + ENDMETHOD. + + + METHOD markSynced. + " EXECUTING -> SYNCED (terminal success); SAP is the ledger, the change number is the receipt (ARN-006) + READ ENTITIES OF zzr_changeapproval000 IN LOCAL MODE + ENTITY ChangeApproval + FIELDS ( Status ) + WITH CORRESPONDING #( keys ) + RESULT DATA(approvals) + FAILED failed. + + LOOP AT keys INTO DATA(key). + ASSIGN approvals[ ID = key-ID ] TO FIELD-SYMBOL(). + IF sy-subrc <> 0. + CONTINUE. + ENDIF. + + IF -Status <> status-executing. + APPEND VALUE #( %tky = -%tky ) TO failed-changeapproval. + APPEND VALUE #( %tky = -%tky + %msg = new_message_with_text( + severity = if_abap_behv_message=>severity-error + text = |Only EXECUTING records can be marked synced (status is { -Status })| ) ) + TO reported-changeapproval. + CONTINUE. + ENDIF. + + IF key-%param-SapChangeNumber IS INITIAL. + APPEND VALUE #( %tky = -%tky ) TO failed-changeapproval. + APPEND VALUE #( %tky = -%tky + %msg = new_message_with_text( + severity = if_abap_behv_message=>severity-error + text = 'The SAP change number is required to mark a record synced' ) ) + TO reported-changeapproval. + CONTINUE. + ENDIF. + + MODIFY ENTITIES OF zzr_changeapproval000 IN LOCAL MODE + ENTITY ChangeApproval + UPDATE FIELDS ( Status SapChangeNumber ErrorMessage ) + WITH VALUE #( ( %tky = -%tky + Status = status-synced + SapChangeNumber = key-%param-SapChangeNumber + ErrorMessage = '' ) ) + FAILED DATA(update_failed) + REPORTED DATA(update_reported). + ENDLOOP. + + READ ENTITIES OF zzr_changeapproval000 IN LOCAL MODE + ENTITY ChangeApproval + ALL FIELDS WITH CORRESPONDING #( keys ) + RESULT DATA(updated). + + result = VALUE #( FOR row IN updated ( %tky = row-%tky %param = row ) ). + ENDMETHOD. + + + METHOD markFailed. + " EXECUTING -> FAILED with the error text; distinct from REJECTED (predecessor defect, ARN-002) + READ ENTITIES OF zzr_changeapproval000 IN LOCAL MODE + ENTITY ChangeApproval + FIELDS ( Status ) + WITH CORRESPONDING #( keys ) + RESULT DATA(approvals) + FAILED failed. + + LOOP AT keys INTO DATA(key). + ASSIGN approvals[ ID = key-ID ] TO FIELD-SYMBOL(). + IF sy-subrc <> 0. + CONTINUE. + ENDIF. + + IF -Status <> status-executing. + APPEND VALUE #( %tky = -%tky ) TO failed-changeapproval. + APPEND VALUE #( %tky = -%tky + %msg = new_message_with_text( + severity = if_abap_behv_message=>severity-error + text = |Only EXECUTING records can be marked failed (status is { -Status })| ) ) + TO reported-changeapproval. + CONTINUE. + ENDIF. + + IF key-%param-ErrorMessage IS INITIAL. + APPEND VALUE #( %tky = -%tky ) TO failed-changeapproval. + APPEND VALUE #( %tky = -%tky + %msg = new_message_with_text( + severity = if_abap_behv_message=>severity-error + text = 'An error message is required to mark a record failed' ) ) + TO reported-changeapproval. + CONTINUE. + ENDIF. + + MODIFY ENTITIES OF zzr_changeapproval000 IN LOCAL MODE + ENTITY ChangeApproval + UPDATE FIELDS ( Status ErrorMessage ) + WITH VALUE #( ( %tky = -%tky + Status = status-failed + ErrorMessage = key-%param-ErrorMessage ) ) + FAILED DATA(update_failed) + REPORTED DATA(update_reported). + ENDLOOP. + + READ ENTITIES OF zzr_changeapproval000 IN LOCAL MODE + ENTITY ChangeApproval + ALL FIELDS WITH CORRESPONDING #( keys ) + RESULT DATA(updated). + + result = VALUE #( FOR row IN updated ( %tky = row-%tky %param = row ) ). + ENDMETHOD. + + + METHOD setInitialStatus. + " Every created record starts PENDING with the requester audit fields (ARN-002) + READ ENTITIES OF zzr_changeapproval000 IN LOCAL MODE + ENTITY ChangeApproval + FIELDS ( Status ) + WITH CORRESPONDING #( keys ) + RESULT DATA(approvals). + + DELETE approvals WHERE Status IS NOT INITIAL. + IF approvals IS INITIAL. + RETURN. + ENDIF. + + MODIFY ENTITIES OF zzr_changeapproval000 IN LOCAL MODE + ENTITY ChangeApproval + UPDATE FIELDS ( Status RequestedBy RequestedOn ) + WITH VALUE #( FOR approval IN approvals + ( %tky = approval-%tky + Status = status-pending + RequestedBy = cl_abap_context_info=>get_user_technical_name( ) + RequestedOn = utclong_current( ) ) ) + REPORTED DATA(update_reported). + + reported = CORRESPONDING #( DEEP update_reported ). + ENDMETHOD. + +ENDCLASS. diff --git a/docs/handover/rap/zzc_changeapproval000.bdef.asbdef b/docs/handover/rap/zzc_changeapproval000.bdef.asbdef new file mode 100644 index 0000000..9856d5a --- /dev/null +++ b/docs/handover/rap/zzc_changeapproval000.bdef.asbdef @@ -0,0 +1,20 @@ +// TARGET OBJECT : behavior definition ZZC_CHANGEAPPROVAL000 (UI projection BDEF; generated 2026-09-15) +// abap: URI : abap:/repotree-v1/0M7DEJI/System%20Library/ZLOCAL/ZARENA_PLM/Core%20Data%20Services/Behavior%20Definitions/ZZC_CHANGEAPPROVAL000/zzc_changeapproval000.bdef.json +// ACTION : replace entire content, then activate (after ZZR_CHANGEAPPROVAL000 BDEF + ZZC_CHANGEAPPROVAL000 view). +// Then DELETE the generated class ZZBP_C_CHANGEAPPROVAL000 in ADT: a projection BDEF needs no behavior +// pool unless it defines new actions/functions, and this one reuses everything +// [source: abap-docs /abap-docs-cloud/ABENBDL_BDEF_PROJECTION_HEADER, verified 2026-09-15]. +// Sources (verified 2026-09-15): use action / use etag; no automatic inheritance - only what is listed is exposed, +// which is how create/update/delete and the worker actions are kept out of the UI service (PRD RAP-2) +// [abap-docs /abap-docs-cloud/ABENBDL_USE_PROJECTION]. Feature control of reused actions is passed on automatically (same source). +projection; +strict ( 2 ); + +define behavior for ZZC_CHANGEAPPROVAL000 alias ChangeApproval +{ + use etag; + + use action approve; + use action reject; + use action retrySync; +} diff --git a/docs/handover/rap/zzc_changeapproval000.dcls.asdcls b/docs/handover/rap/zzc_changeapproval000.dcls.asdcls new file mode 100644 index 0000000..cb31d02 --- /dev/null +++ b/docs/handover/rap/zzc_changeapproval000.dcls.asdcls @@ -0,0 +1,12 @@ +/* TARGET OBJECT : access control ZZC_CHANGEAPPROVAL000 (generated 2026-09-15) + abap: URI : abap:/repotree-v1/0M7DEJI/System%20Library/ZLOCAL/ZARENA_PLM/Core%20Data%20Services/Access%20Controls/ZZC_CHANGEAPPROVAL000/zzc_changeapproval000.dcls.acds + ACTION : replace entire content, then activate (after ZZR_CHANGEAPPROVAL000 DCL). + A transactional query needs its own DCL, inheriting the base view's conditions - there is no implicit inheritance + [source: abap-docs /abap-platform-rap-opensap/week2/unit7#step-5-create-cds-access-control-for-travel-bo-projection-view-and-check-the-result; + /abap-docs-cloud/ABENCDS_DEFINE_VIEW_AS_PROJECTION (access control must be symmetric), verified 2026-09-15]. */ +@EndUserText.label: 'Access control for ZZC_CHANGEAPPROVAL000' +@MappingRole: true +define role ZZC_CHANGEAPPROVAL000 { + grant select on ZZC_CHANGEAPPROVAL000 + where inheriting conditions from entity ZZR_CHANGEAPPROVAL000; +} diff --git a/docs/handover/rap/zzc_changeapproval000.ddls.asddls b/docs/handover/rap/zzc_changeapproval000.ddls.asddls new file mode 100644 index 0000000..f5d224d --- /dev/null +++ b/docs/handover/rap/zzc_changeapproval000.ddls.asddls @@ -0,0 +1,35 @@ +/* TARGET OBJECT : data definition ZZC_CHANGEAPPROVAL000 (UI projection, generated 2026-09-15) + abap: URI : abap:/repotree-v1/0M7DEJI/System%20Library/ZLOCAL/ZARENA_PLM/Core%20Data%20Services/Data%20Definitions/ZZC_CHANGEAPPROVAL000/zzc_changeapproval000.ddls.acds + ACTION : replace entire content, then activate (after ZZR_CHANGEAPPROVAL000). + Notes: provider contract transactional_query [source: abap-docs /abap-docs-cloud/ABENCDS_DEFINE_VIEW_AS_PROJECTION, verified 2026-09-15]; + @ObjectModel.semanticKey (PRD RAP-8) [source: abap-docs /abap-fiori-showcase/05_object_page_header#handling-semantic-key-fields, verified 2026-09-15]; + @Metadata.allowExtensions is required for the metadata extension [source: abap-docs /abap-docs-cloud/ABENCDS_F1_ANNOTATE_VIEW, verified 2026-09-15]. */ +@EndUserText.label: 'Arena change approval - UI projection' +@AccessControl.authorizationCheck: #CHECK +@Metadata.allowExtensions: true +@ObjectModel.semanticKey: ['ItemNumber', 'ChangeType', 'ArenaEcoId'] +define root view entity ZZC_CHANGEAPPROVAL000 + provider contract transactional_query + as projection on ZZR_CHANGEAPPROVAL000 +{ + key ID, + ItemNumber, + ItemName, + ChangeType, + ArenaEcoId, + @ObjectModel.text.element: ['StatusText'] + Status, + StatusText, + StatusCriticality, + RequestedBy, + RequestedOn, + DecidedBy, + DecidedOn, + SapChangeNumber, + ErrorMessage, + RejectReason, + SapPreview, + IsApprovable, + IsRetriable, + LocalLastChangedAt +} diff --git a/docs/handover/rap/zzc_changeapproval000.ddlx.asddlx b/docs/handover/rap/zzc_changeapproval000.ddlx.asddlx new file mode 100644 index 0000000..b3c0968 --- /dev/null +++ b/docs/handover/rap/zzc_changeapproval000.ddlx.asddlx @@ -0,0 +1,142 @@ +/* TARGET OBJECT : metadata extension ZZC_CHANGEAPPROVAL000 (generated 2026-09-15) + abap: URI : abap:/repotree-v1/0M7DEJI/System%20Library/ZLOCAL/ZARENA_PLM/Core%20Data%20Services/Metadata%20Extensions/ZZC_CHANGEAPPROVAL000/zzc_changeapproval000.ddlx.acds + ACTION : replace entire content, then activate (after ZZC_CHANGEAPPROVAL000 view). + Mirrors app/webapp/annotations/annotation.xml (line items, filter bar, header, facets, actions); the app's local + annotations still apply on top at runtime. Labels are literals here; the app carries the i18n texts. + Sources (all verified 2026-09-15, abap-docs /abap-fiori-showcase): + @Metadata.layer + ANNOTATE ENTITY [abap-docs /abap-docs-cloud/ABENCDS_F1_ANNOTATE_VIEW] + @UI.headerInfo typeName/typeNamePlural/typeImageUrl/title/description [05_object_page_header#title-and-subtitle] + @UI.presentationVariant sortOrder + visualizations [03_list_report_content (Defining the Default Sort Order)] + @UI.facet purpose/type/id/label/position/targetQualifier [06_object_page_content (Nested Tabs, RAPTreeview)] + @UI.dataPoint qualifier/title/criticality + #DATAPOINT_REFERENCE header facet [05_object_page_header (Key Value, Progress)] + @UI.identification #FOR_ACTION dataAction/label [05_object_page_header (Regular Actions)] + @UI.lineItem position/importance/criticality [03_list_report_content (Configuring Tables); 06 (Table)] + @UI.selectionField position [02_list_report_header#selection-fields] + @UI.fieldGroup qualifier/position [06_object_page_content (Multi Input Field)] + @UI.textArrangement #TEXT_ONLY [06_object_page_content#displaying-text-instead-of-id] + @UI.multiLineText [05_object_page_header (Plain Text)]; @UI.hidden [06_object_page_content (Hiding Features)] + Default filter Status = PENDING is NOT set here (string-literal escaping inside @UI.selectionVariant.filter is + unverified); the app's annotation.xml SelectionVariant#Default provides it. */ +@Metadata.layer: #CUSTOMER +@UI: { + headerInfo: { + typeName: 'Change Approval', + typeNamePlural: 'Change Approvals', + typeImageUrl: 'sap-icon://approvals', + title: { type: #STANDARD, value: 'ItemNumber' }, + description: { type: #STANDARD, value: 'ItemName' } + }, + presentationVariant: [ { + sortOrder: [ { by: 'Status', direction: #ASC }, { by: 'RequestedOn', direction: #DESC } ], + visualizations: [ { type: #AS_LINEITEM } ] + } ] +} +annotate entity ZZC_CHANGEAPPROVAL000 with +{ + @UI.facet: [ + { id: 'StatusPoint', purpose: #HEADER, type: #DATAPOINT_REFERENCE, targetQualifier: 'StatusPoint' }, + { id: 'Keys', purpose: #HEADER, type: #FIELDGROUP_REFERENCE, targetQualifier: 'Keys', label: 'Reference numbers' }, + { id: 'Change', purpose: #STANDARD, type: #FIELDGROUP_REFERENCE, targetQualifier: 'Change', label: 'Change', position: 10 }, + { id: 'Payload', purpose: #STANDARD, type: #FIELDGROUP_REFERENCE, targetQualifier: 'Payload', label: 'Staged payload', position: 20 }, + { id: 'Approval', purpose: #STANDARD, type: #FIELDGROUP_REFERENCE, targetQualifier: 'Approval', label: 'Approval', position: 30 }, + { id: 'Execution', purpose: #STANDARD, type: #FIELDGROUP_REFERENCE, targetQualifier: 'Execution', label: 'Execution', position: 40 } + ] + @UI.identification: [ + { position: 10, type: #FOR_ACTION, dataAction: 'approve', label: 'Approve' }, + { position: 20, type: #FOR_ACTION, dataAction: 'reject', label: 'Reject' }, + { position: 30, type: #FOR_ACTION, dataAction: 'retrySync', label: 'Retry sync' } + ] + @UI.hidden: true + ID; + + @UI: { + lineItem: [ { position: 10, importance: #HIGH } ], + selectionField: [ { position: 10 } ], + fieldGroup: [ { qualifier: 'Change', position: 10 } ] + } + @EndUserText.label: 'Item number' + ItemNumber; + + @UI: { + lineItem: [ { position: 20, importance: #LOW } ], + fieldGroup: [ { qualifier: 'Change', position: 20 } ] + } + @EndUserText.label: 'Item name' + ItemName; + + @UI: { + lineItem: [ { position: 30, importance: #HIGH } ], + selectionField: [ { position: 30 } ], + fieldGroup: [ { qualifier: 'Change', position: 30 } ] + } + @EndUserText.label: 'Change type' + ChangeType; + + @UI: { + lineItem: [ { position: 60, importance: #MEDIUM } ], + selectionField: [ { position: 40 } ], + fieldGroup: [ { qualifier: 'Change', position: 40 }, { qualifier: 'Keys', position: 10 } ] + } + @EndUserText.label: 'Arena ECO' + ArenaEcoId; + + @UI: { + lineItem: [ { position: 40, importance: #HIGH, criticality: 'StatusCriticality' } ], + selectionField: [ { position: 20 } ], + fieldGroup: [ { qualifier: 'Approval', position: 10, criticality: 'StatusCriticality' } ], + dataPoint: { qualifier: 'StatusPoint', title: 'Status', criticality: 'StatusCriticality' }, + textArrangement: #TEXT_ONLY + } + @EndUserText.label: 'Status' + Status; + + @UI.hidden: true + StatusText; + + @UI.hidden: true + StatusCriticality; + + @UI.fieldGroup: [ { qualifier: 'Approval', position: 20 } ] + @EndUserText.label: 'Requested by' + RequestedBy; + + @UI: { + lineItem: [ { position: 50, importance: #MEDIUM } ], + fieldGroup: [ { qualifier: 'Approval', position: 30 } ] + } + @EndUserText.label: 'Requested on' + RequestedOn; + + @UI.fieldGroup: [ { qualifier: 'Approval', position: 40 } ] + @EndUserText.label: 'Decided by' + DecidedBy; + + @UI.fieldGroup: [ { qualifier: 'Approval', position: 50 } ] + @EndUserText.label: 'Decided on' + DecidedOn; + + @UI.fieldGroup: [ { qualifier: 'Approval', position: 60 } ] + @EndUserText.label: 'Reject reason' + RejectReason; + + @UI.fieldGroup: [ { qualifier: 'Execution', position: 10 }, { qualifier: 'Keys', position: 20 } ] + @EndUserText.label: 'SAP change number' + SapChangeNumber; + + @UI.fieldGroup: [ { qualifier: 'Execution', position: 20 } ] + @EndUserText.label: 'Error message' + ErrorMessage; + + @UI: { + fieldGroup: [ { qualifier: 'Payload', position: 10 } ], + multiLineText: true + } + @EndUserText.label: 'Staged payload' + SapPreview; + + @UI.hidden: true + IsApprovable; + + @UI.hidden: true + IsRetriable; +} diff --git a/docs/handover/rap/zzchngpproval000.tabl.asddls b/docs/handover/rap/zzchngpproval000.tabl.asddls new file mode 100644 index 0000000..9df0022 --- /dev/null +++ b/docs/handover/rap/zzchngpproval000.tabl.asddls @@ -0,0 +1,35 @@ +/* TARGET OBJECT : database table ZZCHNGPPROVAL000 (generated 2026-09-15 by x-ui-service) + abap: URI : abap:/repotree-v1/0M7DEJI/System%20Library/ZLOCAL/ZARENA_PLM/Dictionary/Database%20Tables/ZZCHNGPPROVAL000/zzchngpproval000.tabl.json + ACTION : replace entire content, then activate. + Notes: sap_preview is abap.string(0) (RAP-7; char is capped at 1333 for table fields + [source: abap-docs /abap-docs-cloud/ABENDDIC_BUILTIN_TYPES, verified 2026-09-15]; + abap.string(0) table-field syntax [source: abap-docs /abap-docs-cloud/ABENDDICDDL_DEFINE_TABLE_COMPS, verified 2026-09-15]). + Timestamps are abap.utclong (UTCLONG built-in type, same source). Admin fields use the released + ABP_* data elements and SYSUUID_X16 [source: sap-released-objects public_cloud level A, verified 2026-09-15]. */ +@EndUserText.label : 'Arena change approval (staging + state machine)' +@AbapCatalog.enhancement.category : #NOT_EXTENSIBLE +@AbapCatalog.tableCategory : #TRANSPARENT +@AbapCatalog.deliveryClass : #A +@AbapCatalog.dataMaintenance : #RESTRICTED +define table zzchngpproval000 { + key client : abap.clnt not null; + key id : sysuuid_x16 not null; + item_number : abap.char(18); + item_name : abap.char(40); + change_type : abap.char(15); + arena_eco_id : abap.char(20); + status : abap.char(10); + requested_by : abap.char(12); + requested_on : abap.utclong; + decided_by : abap.char(12); + decided_on : abap.utclong; + sap_change_number : abap.char(12); + error_message : abap.char(255); + reject_reason : abap.char(255); + sap_preview : abap.string(0); + created_by : abp_creation_user; + created_at : abp_creation_tstmpl; + last_changed_by : abp_lastchange_user; + last_changed_at : abp_lastchange_tstmpl; + local_last_changed_at : abp_locinst_lastchange_tstmpl; +} diff --git a/docs/handover/rap/zzr_changeapproval000.bdef.asbdef b/docs/handover/rap/zzr_changeapproval000.bdef.asbdef new file mode 100644 index 0000000..1df0a7e --- /dev/null +++ b/docs/handover/rap/zzr_changeapproval000.bdef.asbdef @@ -0,0 +1,73 @@ +// TARGET OBJECT : behavior definition ZZR_CHANGEAPPROVAL000 (base BDEF, managed; generated 2026-09-15) +// abap: URI : abap:/repotree-v1/0M7DEJI/System%20Library/ZLOCAL/ZARENA_PLM/Core%20Data%20Services/Behavior%20Definitions/ZZR_CHANGEAPPROVAL000/zzr_changeapproval000.bdef.json +// ACTION : replace entire content, then activate (after the three ZD_* abstract entities and ZZR_CHANGEAPPROVAL000). +// Sources (all verified 2026-09-15): +// action syntax incl. ( features : instance ), parameter, result [1] $self +// [abap-docs /abap-docs-cloud/ABENBDL_ACTION, ABENBDL_ACTIONS_FC, ABENBDL_ACTION_INPUT_PARAM, ABENBDL_ACTION2_ABEXA] +// internal operations (usable only from the behavior pool) [abap-docs /abap-docs-cloud/ABENBDL_INTERNAL] +// field ( readonly, numbering : managed ) for a UUID key [abap-docs /abap-docs-cloud/ABENBDL_FIELD_NUMBERING; /abap-fiori-showcase/01_general_features#readonly] +// field ( readonly : update, mandatory : create ) combination [abap-docs /abap-fiori-showcase/06_object_page_content (StreamIsReadOnly)] +// determination on modify { create; } sets the initial status [abap-docs /abap-docs-cloud/ABENBDL_DETERMINATIONS (setStatustoNew)] +// readonly fields written by determinations/actions IN LOCAL MODE [same source: field ( readonly ) AmountSum + TotalPrice determination] +// etag master [abap-docs /abap-docs-cloud/ABENBDL_ETAG]; explicit mapping because CDS aliases differ from table fields [abap-docs /abap-docs-cloud/ABENBDL_TYPE_MAPPING] +// authorization master ( none ): "no authorization checks for the entire RAP BO" - access is governed exclusively by the +// IAM app / business catalog (ADR-002, ARN-007) and read access by DCL [abap-docs /abap-docs-cloud/ABENBDL_AUTHORIZATION] +// Open point (falsifiable at activation): StatusText/StatusCriticality/IsApprovable/IsRetriable are CDS-calculated and +// deliberately absent from the mapping. ABENBDL_TYPE_MAPPING only requires a mapping where table and entity field +// NAMES differ; it is silent on calculated elements. If activation reports an error (not a warning) for the unmapped +// elements, fall back to persisting them (add the four columns to the table + mapping) and maintain them in the actions. +managed implementation in class zzbp_r_changeapproval000 unique; +strict ( 2 ); + +define behavior for ZZR_CHANGEAPPROVAL000 alias ChangeApproval +persistent table zzchngpproval000 +lock master +authorization master ( none ) +etag master LocalLastChangedAt +{ + create; + internal update; + + field ( readonly, numbering : managed ) ID; + field ( readonly : update, mandatory : create ) ItemNumber, ChangeType, ArenaEcoId; + field ( mandatory : create ) SapPreview; + field ( readonly ) Status, StatusText, StatusCriticality, IsApprovable, IsRetriable, + RequestedBy, RequestedOn, DecidedBy, DecidedOn, + SapChangeNumber, ErrorMessage, RejectReason, + CreatedBy, CreatedAt, LastChangedBy, LastChangedAt, LocalLastChangedAt; + + // Business-user actions (UI projection): PENDING -> APPROVED | REJECTED, FAILED -> EXECUTING (PRD RAP-1/RAP-2) + action ( features : instance ) approve result [1] $self; + action ( features : instance ) reject parameter ZD_CHANGEAPPROVAL_REJECTP result [1] $self; + action ( features : instance ) retrySync result [1] $self; + + // Worker actions (A2X projection): APPROVED -> EXECUTING -> SYNCED | FAILED (PRD RAP-6) + action ( features : instance ) markExecuting result [1] $self; + action ( features : instance ) markSynced parameter ZD_CHANGEAPPROVAL_SYNCEDP result [1] $self; + action ( features : instance ) markFailed parameter ZD_CHANGEAPPROVAL_FAILEDP result [1] $self; + + determination setInitialStatus on modify { create; } + + mapping for zzchngpproval000 + { + ID = id; + ItemNumber = item_number; + ItemName = item_name; + ChangeType = change_type; + ArenaEcoId = arena_eco_id; + Status = status; + RequestedBy = requested_by; + RequestedOn = requested_on; + DecidedBy = decided_by; + DecidedOn = decided_on; + SapChangeNumber = sap_change_number; + ErrorMessage = error_message; + RejectReason = reject_reason; + SapPreview = sap_preview; + CreatedBy = created_by; + CreatedAt = created_at; + LastChangedBy = last_changed_by; + LastChangedAt = last_changed_at; + LocalLastChangedAt = local_last_changed_at; + } +} diff --git a/docs/handover/rap/zzr_changeapproval000.dcls.asdcls b/docs/handover/rap/zzr_changeapproval000.dcls.asdcls new file mode 100644 index 0000000..944ff91 --- /dev/null +++ b/docs/handover/rap/zzr_changeapproval000.dcls.asdcls @@ -0,0 +1,12 @@ +/* TARGET OBJECT : access control ZZR_CHANGEAPPROVAL000 (generated 2026-09-15) + abap: URI : abap:/repotree-v1/0M7DEJI/System%20Library/ZLOCAL/ZARENA_PLM/Core%20Data%20Services/Access%20Controls/ZZR_CHANGEAPPROVAL000/zzr_changeapproval000.dcls.acds + ACTION : replace entire content, then activate. + Full access rule: read access is governed by the IAM app / business catalog (ADR-002); SAP delivers no full-access + roles but customers may use them [source: abap-docs /abap-docs-cloud/ABENCDS_DCL_ROLE_GRANT_RULE, verified 2026-09-15]. + @MappingRole: true assigns the role to every user [source: abap-docs /abap-platform-rap-opensap/week2/unit7, verified 2026-09-15]. + Tighten later with a custom authorization object + pfcg_auth condition if per-user data restrictions are needed. */ +@EndUserText.label: 'Access control for ZZR_CHANGEAPPROVAL000' +@MappingRole: true +define role ZZR_CHANGEAPPROVAL000 { + grant select on ZZR_CHANGEAPPROVAL000; +} diff --git a/docs/handover/rap/zzr_changeapproval000.ddls.asddls b/docs/handover/rap/zzr_changeapproval000.ddls.asddls new file mode 100644 index 0000000..9ad4310 --- /dev/null +++ b/docs/handover/rap/zzr_changeapproval000.ddls.asddls @@ -0,0 +1,76 @@ +/* TARGET OBJECT : data definition ZZR_CHANGEAPPROVAL000 (root view entity, generated 2026-09-15) + abap: URI : abap:/repotree-v1/0M7DEJI/System%20Library/ZLOCAL/ZARENA_PLM/Core%20Data%20Services/Data%20Definitions/ZZR_CHANGEAPPROVAL000/zzr_changeapproval000.ddls.acds + ACTION : replace entire content, then activate (table ZZCHNGPPROVAL000 must be active first). + Notes: StatusText / StatusCriticality / IsApprovable / IsRetriable are derived from Status with CASE + expressions here, because a transactional_query projection list may only contain elements of the + projected entity, path expressions, associations and virtual elements - no expressions + [source: abap-docs /abap-docs-cloud/ABENCDS_DEFINE_VIEW_AS_PROJECTION, verified 2026-09-15]. + CASE syntax [source: abap-docs /abap-docs-cloud/ABENCDS_CASE_EXPRESSION_V2, verified 2026-09-15]; + cast of a literal to a data element (abap_boolean, released) [source: abap-docs /abap-docs-cloud/ABENCDS_CAST_EXPRESSION_V2, verified 2026-09-15]. + @ObjectModel.text.element [source: abap-docs /abap-fiori-showcase/06_object_page_content#displaying-text-instead-of-id, verified 2026-09-15]. + @Semantics admin-field annotations [source: abap-docs /abap-platform-rap-opensap/week2/unit3#step-1-create-the-interface-cds-view-for-the-travel-entity, verified 2026-09-15]. + Criticality values 2/2/5/3/1/1 per PRD RAP-1. */ +@AccessControl.authorizationCheck: #CHECK +@EndUserText.label: 'Arena change approval' +define root view entity ZZR_CHANGEAPPROVAL000 + as select from zzchngpproval000 +{ + key id as ID, + item_number as ItemNumber, + item_name as ItemName, + change_type as ChangeType, + arena_eco_id as ArenaEcoId, + + @ObjectModel.text.element: ['StatusText'] + status as Status, + + case status + when 'PENDING' then 'Pending approval' + when 'APPROVED' then 'Approved - awaiting execution' + when 'EXECUTING' then 'Executing in SAP' + when 'SYNCED' then 'Synced to SAP' + when 'REJECTED' then 'Rejected' + when 'FAILED' then 'Execution failed' + else status + end as StatusText, + + case status + when 'PENDING' then 2 + when 'APPROVED' then 2 + when 'EXECUTING' then 5 + when 'SYNCED' then 3 + when 'REJECTED' then 1 + when 'FAILED' then 1 + else 0 + end as StatusCriticality, + + requested_by as RequestedBy, + requested_on as RequestedOn, + decided_by as DecidedBy, + decided_on as DecidedOn, + sap_change_number as SapChangeNumber, + error_message as ErrorMessage, + reject_reason as RejectReason, + sap_preview as SapPreview, + + case status + when 'PENDING' then cast( 'X' as abap_boolean ) + else cast( ' ' as abap_boolean ) + end as IsApprovable, + + case status + when 'FAILED' then cast( 'X' as abap_boolean ) + else cast( ' ' as abap_boolean ) + end as IsRetriable, + + @Semantics.user.createdBy: true + created_by as CreatedBy, + @Semantics.systemDateTime.createdAt: true + created_at as CreatedAt, + @Semantics.user.lastChangedBy: true + last_changed_by as LastChangedBy, + @Semantics.systemDateTime.lastChangedAt: true + last_changed_at as LastChangedAt, + @Semantics.systemDateTime.localInstanceLastChangedAt: true + local_last_changed_at as LocalLastChangedAt +} diff --git a/docs/handover/rap/zzui_arena_change_apprvl000_o4.srvd.srvdsrv b/docs/handover/rap/zzui_arena_change_apprvl000_o4.srvd.srvdsrv new file mode 100644 index 0000000..8c106c5 --- /dev/null +++ b/docs/handover/rap/zzui_arena_change_apprvl000_o4.srvd.srvdsrv @@ -0,0 +1,13 @@ +/* TARGET OBJECT : service definition ZZUI_ARENA_CHANGE_APPRVL000_O4 (generated 2026-09-15) + abap: URI : abap:/repotree-v1/0M7DEJI/System%20Library/ZLOCAL/ZARENA_PLM/Business%20Services/Service%20Definitions/ZZUI_ARENA_CHANGE_APPRVL000_O4/zzui_arena_change_apprvl000_o4.srvd.acds + ACTION : replace entire content, then activate (after ZZC_CHANGEAPPROVAL000 view + BDEF). + expose ... as : the alias becomes the entity set (ChangeApproval) and the entity type (ChangeApproval_Type) + [source: abap-docs /abap-fiori-showcase/06_object_page_content (Service Definition), verified 2026-09-15]. + provider contracts odata_v4_ui switches on the UI-binding syntax checks and must match the binding type + [source: abap-docs /abap-docs-cloud/ABENSRVD_PROVIDER_CONTRACT, verified 2026-09-15]. */ +@EndUserText.label: 'Arena change approval - UI service' +define service ZZUI_ARENA_CHANGE_APPRVL000_O4 + provider contracts odata_v4_ui +{ + expose ZZC_CHANGEAPPROVAL000 as ChangeApproval; +} diff --git a/docs/predecessor-flow.md b/docs/predecessor-flow.md new file mode 100644 index 0000000..44ec2e9 --- /dev/null +++ b/docs/predecessor-flow.md @@ -0,0 +1,172 @@ +# Predecessor flow — how the Express implementation actually works + +Extracted 2026-09-10 by a full read of `~/projects/sap-apps/arena-plm` +(reference-only). File:line cites refer to that repo, mostly +`backend/src/arena-plm.js`. This record informs the rebuild's design; the +durable behavioral tombstones harvested from it live in `KNOWLEDGE.md`, and +the requirements it produced are in `PRD.md` (ARN-010, sharpened ARN-002/004/006). + +**Shape of the system:** not event-driven — an **operator-driven staging +workbench**. Arena is the read-only source, SAP is the write target *and* the +durable ledger. The webhook and poller exist but are effectively inert (§1). +Real syncs are triggered by a human clicking Approve in the UI5 worklist. + +## 1. Ingestion — three paths, only one live + +**Webhook (`POST /api/arena/webhook`, routes/arena-webhook.js)** — mounted +before the XSUAA gate, Basic auth (timing-safe), raw-text body of any +content-type (delivery shape never pinned down), always answers 200 so Arena +never retries. `WEBHOOK_CAPTURE_ONLY` logs+acks; otherwise +`pollOutboundIntegration({dryRun})` with dryRun defaulting true. **The +consumer never syncs**: `arena-plm.js:1252-1256` is a literal TODO — non-dry +runs only mark event items reconciled in Arena. The webhook path writes +nothing to SAP. + +**Outbound-integration pull queue (`arena-plm.js:1197-1263`)** — Arena holds +the cursor: `GET /outboundintegrations/{guid}/events?itemsReconciled=false`, +per-event `…/items`, then `PUT …/items/{itemGuid} {reconciled:true}` +(idempotent). No local state. + +**Change poller (`routes/poll.js` → `arena-plm.js:1117-1191`)** — manual only +(no scheduler exists in the repo). Watermark precedence: explicit `since` → +newest `A_ChangeMaster.ChangeNumberValidFromDate` where +`startswith(ReasonForChangeText,'ARENA ')` → `defaults.arenaChangeCutoff`; +refuses non-dry runs with neither (`POLL/NO_CUTOFF`). Pages the whole Arena +`/changes` feed client-side (no working server-side date filter), filters +`effectiveDateTime > since`. Dedup via one bulk read of every `ARENA `-marked +change master out of SAP into a Map. Real runs create change masters only. + +## 2. Staging & approval + +SQLite via `node:sqlite` on the **ephemeral CF filesystem** (`db.js`): +`approvals(id, item_number, change_type PRODUCT|CHANGE_MASTER|BOM|DOCUMENTS, +status PENDING|APPROVED|REJECTED, arena_data, sap_preview, sap_result, error, +reject_reason, …)` — one row per (item × change type); queue lost on restart. + +- Seeding (`POST /api/approvals/seed`): `previewFromArena()` per item — all + four syncs dry-run under `Promise.allSettled` (concurrency 6). **No dedup** + on (item, changeType): re-seeding duplicates rows. +- Approve: `bulk-approve` returns immediately; work runs in a detached async + IIFE guarded by a **module-level in-memory `postJob` singleton** (progress + source AND concurrency guard; forces `instances: 1`; on restart, in-flight + jobs leave rows PENDING forever). `chain-approve` is synchronous per item, + halts on first failure. Reject is a pure DB flip. +- **On approval only `item_number` is passed** — the stored `sap_preview` is + never replayed; every sync re-fetches Arena and recomputes. What was + approved is not necessarily what gets written (→ rebuild ARN-010). +- **`REJECTED` is overloaded**: human rejection AND SAP write failure land in + the same status, distinguishable only by `error` vs `reject_reason`; no + retry path from REJECTED except re-seeding (→ rebuild ARN-002 states). + +## 3. Sync execution — the SAP write chain + +Order from `config/sync-chain-order.json`: PRODUCT → CHANGE_MASTER → BOM → +DOCUMENTS (CM's material object record needs the material; BOM's +`EngineeringChangeDocForEdit` needs the CM). Services as in PRD interface +contracts (V4 product, V2 product fallback, BOM write `;v=0002` / read v1, +change master `;v=0002`, CV attachment). + +**PRODUCT (`:1336-1663`)** — exists-check (V4 GET, 404 ⇒ create path). +Create: `ProductType` strictly from Arena attr "SAP Product Type" (missing ⇒ +`422 SYNC/NO_PRODUCT_TYPE` — deliberately "not a material"); UOM via config + +hardcoded ISO map; plants from attr "SAP Plant" through `plantMapping`; weight +g→×1000, dims mm→×1000; description truncated 40. Deep insert with +`_ProductDescription`, `_ProductPlant` only when no `AvailabilityCheckType`; +else per-plant V2 `A_ProductPlant` POSTs; `ProcurementType` requires `MRPType` +in the same `ProductPlantSupplyPlanning` PATCH. Best-effort CM link keyed on +`startswith(ChangeNumberDescription,'ECO:')` — a different key than everything +else (landmine). Update path: `ProductType`/`BaseUnit` immutable; +`ProductDescription` PATCH by full key (nav PATCH → 501), POST fallback on +404/405. + +**CHANGE_MASTER (`:1794-1873`)** — EFFECTIVE revision with `change.number` +via `/revisions` (`/items/{guid}/changes` 404s — does not exist). +Double-condition dedup: `ReasonForChangeText eq 'ARENA '` AND a material +object record for *this* item. Create: `A_ChangeMaster` with +`ChangeNumberDescription: "ECO: title"[0..40]`, `/Date(ms)/` valid-from, +`ReasonForChangeText: "ARENA "`; then object-type assignments Material + +MaterialBOM (`IsActiveForChangeNumber: true` mandatory — else 201 + +silently discarded), then the material object record. Errors returned, not +thrown. + +**BOM (`:1665-1792`)** — first mapped plant wins; lines `quantity > 0`, item +numbers (i+1)*10 zero-padded 4; category via `bomlineByCategory` default 'L'. +Opportunistic ECM lookup (bare catch — silently drops ECM and creates the BOM +plain if the object-type assignment fails: traceability quietly lost). +Component pre-check: if all components missing in SAP → early return, no +orphan header (SAP's real error is an opaque consistency-check message). +Header POST with `EngineeringChangeDocForEdit` (never +`EngineeringChangeDocument` — creatable=false, silently dropped), validity end +`/Date(253402214400000)/`. Items POSTed serially; each item re-reads the +header on the v1 read service; ECM BOMs must carry `HeaderChangeDocument`. +`BOMItemIsSalesRelevant:'X'` hardcoded. Partial failure is a return value. +**Not idempotent — re-approval creates a second BOM header.** + +**DOCUMENTS (`:1875-1913`)** — URL links only, no bytes: filters files to +`storageMethodName === 'WEB' && file.location` (blob-stored files silently +skipped), then V2 function import `CreateUrlAsAttachment` with hardcoded +`BusinessObjectTypeName BUS1001006`, `MIMEType 'text/url'`, description +" Rev "[0..40]. The editable +`arena-doc-category-to-sap-doctype.json` config is **ignored by code**. +**Not idempotent — re-approval duplicates attachments.** + +**Cross-cutting:** CSRF token+cookie cached per service, invalidated only on +403 with exactly one retry; PRODUCT services reject bare-root token fetch +(seed from `Product?$top=1` / `A_Product?$top=1`). Arena 401 → re-login, one +retry. No backoff, no retry queue, no transactions. Idempotency exists only +for CHANGE_MASTER (double-check) and weakly for PRODUCT (exists→PATCH). + +## 4. Arena reads + +Session `POST /login {email, password, workspaceId}` → `arena_session_id` +header, cached 24 min vs 25-min expiry, single-flight promise lock, one 401 +retry. Endpoints used by sync: `/items?number=` (resolves EFFECTIVE), +`/items/{guid}`, `/items/{guid}/revisions`, `/items/{guid}/bom`, +`/items/{guid}/files`, `/changes/{guid}`, `/changes?limit&offset`, +`/changes/{guid}/items`, `/settings/items/categories`, outbound-integration +trio. Write-back (`POST /changes/statuschanges`) is parked behind +`ARENA_WRITEBACK_ENABLED`. + +**Load-bearing subtlety — WORKING-revision overlay (`:508-552`):** the seven +SAP classification attributes (SAP Object Type / Product Type / Product +Category / Sub-Category / Product Family / Business Unit / Plant) are only +writable on the WORKING revision (Arena error 3224), while `/items?number=` +returns the EFFECTIVE revision — so those seven are merged from WORKING over +EFFECTIVE. The overlay is best-effort: a transient Arena failure silently +loses "SAP Product Type" and turns a valid sync into `NO_PRODUCT_TYPE`. + +## 5. Config actually read vs dead + +Live (read by sync code, hot-reloaded, git-backed via external +`sap-apps-config` repo): `defaults.json`, `plant-mapping.json`, +`uom-arena-to-sap.json`, `material-group-by-product-type.json`, +`availability-check-by-product-type.json`, `lifecycle-to-xplant-status.json`, +`bomline-category-by-arena-category.json`, `sync-chain-order.json`. + +Dead (loaded, zero read sites — confirms the KNOWLEDGE.md landmine): +`arena-category-to-sap.json`, `procurement-type.json` (code uses a hardcoded +map keyed on the first char of Arena "Item Type"), +`catalog-to-planning-strategy.json`. Ignored-but-editable: +`arena-doc-category-to-sap-doctype.json`. Hardcoded despite the "no +hardcoding" comment: ISO UOM map, status map, `ARENA ` marker, procurement +map, service paths, working-attrs list, BOM validity end, sales-relevant +flag, Language 'EN', 40-char truncations, BUS1001006. + +## 6. Fragilities the rebuild must not inherit + +1. In-memory `postJob` singleton = progress + concurrency guard; restart + orphans PENDING rows; forces `instances: 1`. +2. Fire-and-forget approval worker; no resume, no cancellation. +3. Seed dedup absent; BOM and DOCUMENTS syncs non-idempotent. +4. `REJECTED` overload (human vs SAP failure). +5. Silent ECM degradation (bare catches around ECO lookup, object-type + assignment, revision overlay). +6. Two disagreeing CM lookup keys (`ChangeNumberDescription` startswith vs + `ReasonForChangeText` eq; 40-char truncation can split them). +7. The `ARENA ` free-text marker is the entire ledger — dedup key, + watermark, and status all hang on an editable 40-char field. +8. O(history) paging on every poll (Arena full feed; up to 100k SAP change + masters). +9. Arena credentials base64 in SQLite on the ephemeral filesystem; the whole + approvals queue dies on restart/redeploy. +10. Approved preview is decorative — execution re-fetches and recomputes. diff --git a/download.php?file=%2Ftechnology%2Fepp%2Fdownloads%2Frelease%2F2026-09%2FR%2Feclipse-java-2026-09-R-win32-x86_64.zip b/download.php?file=%2Ftechnology%2Fepp%2Fdownloads%2Frelease%2F2026-09%2FR%2Feclipse-java-2026-09-R-win32-x86_64.zip new file mode 100644 index 0000000..2340dee --- /dev/null +++ b/download.php?file=%2Ftechnology%2Fepp%2Fdownloads%2Frelease%2F2026-09%2FR%2Feclipse-java-2026-09-R-win32-x86_64.zip @@ -0,0 +1,653 @@ + + + + + + + + + + + + Eclipse downloads - Select a mirror | The Eclipse Foundation + + + + + + + + + + + + + + + + + + + + + Skip to main content +
+
+ +
+
+ +
+
+ +
+
+
+

All downloads are provided under the terms and conditions of the Eclipse Foundation + Software User Agreement unless otherwise specified.

+
+
+
+ Download + +

Download from: Canada - Rafal Rzeczkowski (https)

+

File: eclipse-java-2026-09-R-win32-x86_64.zip SHA-512

+
+
+
+

+
+
+
+

+
+
+ +

Choose a mirror close to you

+
+
+
+

North America

+ +
+
+

Europe

+ +
+ +
+

Australia/Oceania

+ +
+
+

South America

+ +
+ Show all +
+
+

Main Download Site

+ +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Other options for this file
+ +
+ +
+ +
+
+
+
+
+ + + + + + + + diff --git a/worker/.env.example b/worker/.env.example new file mode 100644 index 0000000..f9cae7c --- /dev/null +++ b/worker/.env.example @@ -0,0 +1,38 @@ +# Copy to .env for local development (never commit .env — see .gitignore). +# `npm run dev` loads it through Node's `--env-file-if-exists=.env` +# (stable since Node 24.10; verified on this box with tsx watch 2026-09-14). +# Production gets its env from the platform — nothing else reads this file. +# Config is env-only; business mappings live in the tenant (PRD ARN-004), not here. + +# HTTP surface +WORKER_HOST=0.0.0.0 +WORKER_PORT=8080 +# Shared bearer token for /webhooks/* and /admin/* (placeholder auth — PRD OQ-6). +# Unset => guarded endpoints fail closed with 401. +WORKER_BEARER_TOKEN= + +# Arena PLM (api.arenasolutions.com/v1) +ARENA_API_URL=https://api.arenasolutions.com/v1 +ARENA_API_EMAIL= +ARENA_API_PASSWORD= +ARENA_WORKSPACE_ID= +# Session cache lifetime in minutes. Arena sessions expire after ~25 min +# inactivity [KNOWLEDGE.md 2026-09-04]; default refreshes 1 min early. +ARENA_SESSION_TTL_MINUTES=24 +# Per-request deadline for Arena HTTP calls (AbortSignal.timeout). +ARENA_HTTP_TIMEOUT_MS=30000 + +# Delta poller +ARENA_POLL_INTERVAL_SECONDS=300 + +# Ingest buffer bound (transient in-memory buffer, ARN-003 — never durable +# worker-local storage). When full: webhook => 503 + Retry-After, poll => drop. +WEBHOOK_QUEUE_MAX_SIZE=10000 + +# Graceful shutdown: max wait for in-flight sync runs / poll tick on +# SIGINT/SIGTERM. Exit 0 when drained, 1 when this deadline wins. +SHUTDOWN_TIMEOUT_MS=30000 + +# Sync chain execution order (ARN-001). Comma-separated permutation/subset of +# PRODUCT,CHANGE_MASTER,BOM,DOCUMENT — each at most once. +SYNC_OBJECT_ORDER=PRODUCT,CHANGE_MASTER,BOM,DOCUMENT diff --git a/worker/.gitignore b/worker/.gitignore new file mode 100644 index 0000000..39f2302 --- /dev/null +++ b/worker/.gitignore @@ -0,0 +1,7 @@ +node_modules/ +dist/ +coverage/ +.env +.env.* +!.env.example +*.log diff --git a/worker/eslint.config.js b/worker/eslint.config.js new file mode 100644 index 0000000..ca8bb4f --- /dev/null +++ b/worker/eslint.config.js @@ -0,0 +1,27 @@ +// @ts-check +// Flat config per current typescript-eslint quickstart: defineConfig from +// 'eslint/config' (the tseslint.config helper is deprecated). +// [source: context7 /typescript-eslint/typescript-eslint docs/getting-started/Quickstart.mdx, verified 2026-09-10] +import js from '@eslint/js'; +import { defineConfig } from 'eslint/config'; +import tseslint from 'typescript-eslint'; + +export default defineConfig( + { + ignores: ['dist/**', 'node_modules/**', 'coverage/**'], + }, + { + files: ['**/*.ts'], + extends: [js.configs.recommended, tseslint.configs.recommended], + rules: { + '@typescript-eslint/no-unused-vars': [ + 'error', + { + argsIgnorePattern: '^_', + varsIgnorePattern: '^_', + caughtErrorsIgnorePattern: '^_', + }, + ], + }, + }, +); diff --git a/worker/package-lock.json b/worker/package-lock.json new file mode 100644 index 0000000..0610440 --- /dev/null +++ b/worker/package-lock.json @@ -0,0 +1,3630 @@ +{ + "name": "arena-plm-connectivity-worker", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "arena-plm-connectivity-worker", + "version": "0.1.0", + "dependencies": { + "fastify": "^5.12.3" + }, + "devDependencies": { + "@eslint/js": "^10.0.1", + "@types/node": "^24.13.4", + "ajv": "^8.20.0", + "ajv-formats": "^3.0.1", + "eslint": "^10.10.0", + "tsx": "^4.23.13", + "typescript": "~5.9.3", + "typescript-eslint": "^8.70.0", + "vitest": "^4.1.11" + }, + "engines": { + "node": ">=24" + } + }, + "node_modules/@cacheable/memory": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@cacheable/memory/-/memory-2.2.0.tgz", + "integrity": "sha512-CTLKqLItRCEixEAewD3/j9DB3/o96gpTPD4eJ1v+DGOlxZRZncRQkGYqqnAGCscYd6RNeXfGeiuCphsPtqyIfQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@cacheable/utils": "^2.5.0", + "@keyv/bigmap": "^1.3.1", + "hookified": "^1.15.1", + "keyv": "^5.6.0" + } + }, + "node_modules/@cacheable/utils": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@cacheable/utils/-/utils-2.5.0.tgz", + "integrity": "sha512-buipgOVDkkPXNR5+xBpDw7Zk2n1EvU7qBJCNUcL7rhQ//kfpOXPAvQ511Os0vpLYJ1pZnvudNytkQt2hst3wqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "hashery": "^1.5.1", + "keyv": "^5.6.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.2.tgz", + "integrity": "sha512-XExcO+dvLKvVtNTibSTBej1NCAbaGhWn9Ww1ZPx80qsahhPFe/8jgWP0IchNe0F3HwkU7n8ejhH8bjonqht8mQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.2.tgz", + "integrity": "sha512-kXXoiPVVGQcnIYGOeaovwOURpniDBpSq4A03qkQ+BMQqtGG6HYap3xne9C1O1yo4TR3qxlCX5IqqmX6fFo2Lqg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.2.tgz", + "integrity": "sha512-5YfKeeI8qWfBZIX+u2xZC3Zlb3Os/gLS2sbEKM+I4ZOcsWmHS2WLysCcQZDAFRslDUU5Oiq44gf6PYN1vGwG5A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.2.tgz", + "integrity": "sha512-O387ite7SzUyCcy3JQX4P4bLtEA7bLLkx+esve5JHnyYfNTxcVpXZo9jhdB0lTKN44gztELTdU7nS8Nr16Fs1Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.2.tgz", + "integrity": "sha512-n4KqkOQrraxHJcgjM1RvwbigfQKIKJVpM7xp+KsxiyUSrRdIXnt73VhrPAx0fV44hgfmIVKjxMN9J1t5jySVkw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.2.tgz", + "integrity": "sha512-uq6suIWYP37qzGddBKPw5QEQPi6HiLGsO7UmkpfyaYNQ3D+rN6w6WfwH+nuqcGXWvawGwxOEroO4YGnFh95azw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.2.tgz", + "integrity": "sha512-n+I0BTSRIoy+d6RPKnEVwql5UwBJolytvY4mAOIEJorKlqgPII8ix6slVVrfZ5Tnj7glIZvloylbB/EJPMWEXw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.2.tgz", + "integrity": "sha512-78XJTJkvPs0kz2w61301PJjXl4g7q3JqiYMZ/M/yVI73EHBrCRTgkhu9oqG7vPqq+a/yadEW8aD+agKlk5xrmg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.2.tgz", + "integrity": "sha512-XlDnu2q5yoqems+xay6wSAcg9DDD7K9RLKZEBOMZm3ckNpJBvOX20tSfby8KfrrhINDyv9V2YVZKY/SpoGJI8w==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.2.tgz", + "integrity": "sha512-pW4AC0P3it8c7do9MVM4p51FzHzdM/TZrerurgRcHJ2WTa1VQ1CIq18xncfpBJw4ojkiZZrKW2yIBWBP92j6Ug==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.2.tgz", + "integrity": "sha512-CYbnj78HsIeA+DhgUKgFCfvNsTHFhMMrinUrMZpDXJXKN8T3XViTZ/+wtHeVxEWY8ewSzTFN+nRmSwO2tZaLUQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.2.tgz", + "integrity": "sha512-buwkd8nsph4R+ajRvw0qM5Hja/TXQow3ptzWO2EbG/cqcIkHloRrdlBtQlshyYGTNFvfkfJ5tpPLVkY4DtsPfQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.2.tgz", + "integrity": "sha512-ZVykbDyk7519VwiNb9Lcj9m8XM6v5V9uKPvrEMkkEedVewf+0itkhahp4HDpgERXhwLRpWFypsGbG/J8s0QjJA==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.2.tgz", + "integrity": "sha512-CAXl+Dtd9UUuJd8pKKdwh6MLm3MUMiqMPmhZ3tTSXPqfyQ3vDl6R5hZdZ/kYojK4ofXtdfSv1tFq8XzWx3heNQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.2.tgz", + "integrity": "sha512-GeXCej4IQtU1B+QlDV8W/RRvbzI3O/Stss+/bCXv4lZls5WGRtu2a+3JkA3i4qIUlMXpcHebWpF8AkJhATowuA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.2.tgz", + "integrity": "sha512-3H1weTYZPxt/WOhByszQZybS9w5lKzUn1FDMsgEChbHWQwHYQQRfBxgCcZvPhjHfKyJjIievvMmEUawJrdY9Dg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.2.tgz", + "integrity": "sha512-4xTZr1FUmSoQW4XIWmit3tzQrUTZM+N3P0XV8xROKYF50XfI7xeO90+1bZvNwxIufQ9hDQVRJH5YhgPVF8A/HQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.2.tgz", + "integrity": "sha512-sSATRjPeDBg3pdgHoQfoYBob11Kk1FGa9lui5RIHZCoCkJa9QKlvl3/vKz2usCmYYjs7ymJR/2Nnsqe+Hjt5nw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.2.tgz", + "integrity": "sha512-lqnzCV+mM0gIADaKihiCg6ifgfU2L3h5E33rNQBN1Y4MaVGnzryzmvvf7UHxprpQdE8hpqLolJ9Rl+SkIRDpyw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.2.tgz", + "integrity": "sha512-AL2qJILH7lNjrDmCQDvdxMfAUIv8KMNZOvrwAQ8i8//ntL9FflhOyMJ8OZSMBb8/AWXe3/5v5S20y3zCoZWKoQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.2.tgz", + "integrity": "sha512-QtiuPytchRyC4rwUKhexJdQKvDuZ6hWloi3igqPQNUJCS1/v9EiO3UTOXR6A3FoMo4fnAKbWJdqaIwhOzh8qEw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.2.tgz", + "integrity": "sha512-WkhYDmpTjLvGlScA1rwjRUmhl4k8oXR3cIbtqWmELgU/dFeHHlEllxDvdWcNJV9rbzCexB5vz8gtNewWLgCT7Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.2.tgz", + "integrity": "sha512-GPMSkTOtMnv2U2F8gxe4Io6qmVs+YKyp832Etqqxr0hFngmXQ3rzwytelm3GIn7T4VviRUlf3sOgBOiTdvaf7g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.2.tgz", + "integrity": "sha512-PIhhEkE9uPBleRBrQEJpUn7MBnibZzbGzYWPmY3x+YoVg/95zbjB4CxPPOQ8l5tYYM4mMaCthF8/1DIfBQQyWQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.2.tgz", + "integrity": "sha512-YmJbfTlvU7Sdn9BB+4PRES4oB6pxgS37MAONj+hBr/cpXS1aBPKXxNnDbu+QCWPj0o9dgyxeq79g6c5P8KeuYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.2.tgz", + "integrity": "sha512-5ebpxr3nWMzrL/rnUI755Jkuee0bHL/Gq0WTF9lvcpv73wAp5eu8MfBUgWK9bhWvZjj7yX8etf/8tI8Ney695g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.10.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.10.1.tgz", + "integrity": "sha512-cuadcxVFE8sDK6iWJbs8Sn0av2Nrh2QSGQhVlBW9AaAHqHwjWsZHT8LJ4hFGPh7ASBV2deFdM7H/DPjulmh8rg==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.23.5", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.23.5.tgz", + "integrity": "sha512-Y3kKLvC1dvTOT+oGlqNQ1XLqK6D1HU2YXPc52NmAlJZbMMWDzGYXMiPRJ8TYD39muD/OTjlZmNJ4ib7dvSrMBA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^3.0.5", + "debug": "^4.3.1", + "minimatch": "^10.2.4" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.7.0.tgz", + "integrity": "sha512-DObd/KKUsU+FaFv4PLxSRenpXfQWmPXXP3pPZ6/K1PCrMu2vQpMDMuQe/BqYeoLcz8ro0bVDF1RxOJgfVEdhUw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^1.2.1" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/core": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-1.2.1.tgz", + "integrity": "sha512-MwcE1P+AZ4C6DWlpin/OmOA54mmIZ/+xZuJiQd4SyB29oAJjN30UW9wkKNptW2ctp4cEsvhlLY/CsQ1uoHDloQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/js": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-10.0.1.tgz", + "integrity": "sha512-zeR9k5pd4gxjZ0abRoIaxdc7I3nDktoXZk2qOv9gCNWx3mVwEn32VRhyLaRsDiJjTs0xq/T8mfPtyuXu7GWBcA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "eslint": "^10.0.0" + }, + "peerDependenciesMeta": { + "eslint": { + "optional": true + } + } + }, + "node_modules/@eslint/object-schema": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-3.0.5.tgz", + "integrity": "sha512-vqTaUEgxzm+YDSdElad6PiRoX4t8VGDjCtt05zn4nU810UIx/uNEV7/lZJ6KwFThKZOzOxzXy48da+No7HZaMw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.7.3.tgz", + "integrity": "sha512-IkO+/KEUvwbVpiURZg+P7zF74z5Jxe0UgJxVni+RtoHQ6IZieXaO02kmadomap/q+l6bc/jdPGGqTjhuZnuz1Q==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^1.2.1", + "levn": "^0.4.1" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@fastify/ajv-compiler": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/@fastify/ajv-compiler/-/ajv-compiler-4.0.6.tgz", + "integrity": "sha512-NtuzM0SfaMJbGlnjr9LWQUN5LzgSrbB8tf/wRZNas+4E1O/Nmzl53e7ruT61HDZyRCJGC6FxIogmNZO1c5ETBA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "ajv": "^8.12.0", + "ajv-formats": "^3.0.1", + "fast-uri": "^4.0.0" + } + }, + "node_modules/@fastify/error": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@fastify/error/-/error-4.2.0.tgz", + "integrity": "sha512-RSo3sVDXfHskiBZKBPRgnQTtIqpi/7zhJOEmAxCiBcM7d0uwdGdxLlsCaLzGs8v8NnxIRlfG0N51p5yFaOentQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT" + }, + "node_modules/@fastify/fast-json-stringify-compiler": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@fastify/fast-json-stringify-compiler/-/fast-json-stringify-compiler-5.1.0.tgz", + "integrity": "sha512-PxcYtKLbQ8Z+yApiqjK8FwxIwvEj38k2OiLc17u8dkJSlmfi2wHHPaSnaoqBPQqtvF8YVsDgDpP2snDCfFrpfw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "fast-json-stringify": "^7.0.0" + } + }, + "node_modules/@fastify/forwarded": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@fastify/forwarded/-/forwarded-3.0.2.tgz", + "integrity": "sha512-NE8HgKLgYejV9lDpqkEFaDKMLYelJBVfHekhB0UKvX0ghagXRJqg68feg8er1NPXxG4N9i6vPxzt8E+3wHfcmA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT" + }, + "node_modules/@fastify/merge-json-schemas": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/@fastify/merge-json-schemas/-/merge-json-schemas-0.2.1.tgz", + "integrity": "sha512-OA3KGBCy6KtIvLf8DINC5880o5iBlDX4SxzLQS8HorJAbqluzLRn80UXU0bxZn7UOFhFgpRJDasfwn9nG4FG4A==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "dequal": "^2.0.3" + } + }, + "node_modules/@fastify/proxy-addr": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@fastify/proxy-addr/-/proxy-addr-5.1.0.tgz", + "integrity": "sha512-INS+6gh91cLUjB+PVHfu1UqcB76Sqtpyp7bnL+FYojhjygvOPA9ctiD/JDKsyD9Xgu4hUhCSJBPig/w7duNajw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "@fastify/forwarded": "^3.0.0", + "ipaddr.js": "^2.1.0" + } + }, + "node_modules/@humanfs/core": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz", + "integrity": "sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/types": "^0.15.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.8", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.8.tgz", + "integrity": "sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.2", + "@humanfs/types": "^0.15.0", + "@humanwhocodes/retry": "^0.4.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/types": { + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/@humanfs/types/-/types-0.15.0.tgz", + "integrity": "sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.6.0.tgz", + "integrity": "sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@keyv/bigmap": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@keyv/bigmap/-/bigmap-1.3.1.tgz", + "integrity": "sha512-WbzE9sdmQtKy8vrNPa9BRnwZh5UF4s1KTmSK0KUVLo3eff5BlQNNWDnFOouNpKfPKDnms9xynJjsMYjMaT/aFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "hashery": "^1.4.0", + "hookified": "^1.15.0" + }, + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "keyv": "^5.6.0" + } + }, + "node_modules/@keyv/serialize": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@keyv/serialize/-/serialize-1.1.1.tgz", + "integrity": "sha512-dXn3FZhPv0US+7dtJsIi2R+c7qWYiReoEh5zUntWCf4oSpMNib8FDhSoed6m3QyZdx5hK7iLFkYk3rNxwt8vTA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@oxc-project/types": { + "version": "0.149.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.149.0.tgz", + "integrity": "sha512-Efcc+iF0j3Bf67YjEqIqWXbX5XddXoK/Mw4K1/JuXwRCZ8N16VR7iT23nlCc9XrveFVh/E5Rqs2StT0V8v9LdA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/oxc-project" + } + }, + "node_modules/@pinojs/redact": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@pinojs/redact/-/redact-0.4.0.tgz", + "integrity": "sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg==", + "license": "MIT" + }, + "node_modules/@rolldown/binding-android-arm-eabi": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm-eabi/-/binding-android-arm-eabi-1.2.8.tgz", + "integrity": "sha512-tN5aztYkKCte4i5SIrrz5yK/HMjEuCqCSCJa418jOV8tZ1cBY3YF2otxB1ktPxzsLA1BeTqwapK0bfjxNvHJVw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-android-arm64": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.2.8.tgz", + "integrity": "sha512-dIYTWl9XprMUiQFoc55KUyk/oS8SKYH3zFl0LTR7RT0Xj4hgSVyuJcroH8JUu8RcpF8fTB6E0aOwCkZoYPcDSQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-arm64": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.2.8.tgz", + "integrity": "sha512-PCSDQGXD2IyTEFrcgPyBM8jJuGmrbCMuoIOXdbEGVemruKACXoLQJrb+A45Z0L5t1RQkdfJprAYPkikbh7dzdA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-x64": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.2.8.tgz", + "integrity": "sha512-Uk7lRsGhPFHVX/sAUC6D5H9Ol30dFHd6iquokll2th3LpdJ3F5CzQB+7DHn0Ri2mG+U7k2zXiPHDrwZenXhwSA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-freebsd-x64": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.2.8.tgz", + "integrity": "sha512-DjszaTEVogPqA5bYzsEeqDCQxbcp2fexQwKcRspYji2yzR68fCf+e4fx6kBSRDwX5/brZaHw/hWS9+A/+/w9sQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm-gnueabihf": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.2.8.tgz", + "integrity": "sha512-zmwa7FTmdzB6aaEEuuls18H6Ap5JmJPSoPTuXixeJZV6tG40SyLkApQtz1g8ptZtiEKqj9OM0oNLPh1AgvE31Q==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-gnu": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.2.8.tgz", + "integrity": "sha512-KdYQDPHwJVnbFwdTGMgxsI9SqblBlz6STGM+w1We/d5B8OWWidYH0MwkU/uA1wM5fIpO2MkOVxXrNzzuZhw9ew==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-musl": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.2.8.tgz", + "integrity": "sha512-jFJTifHnNPY+yzOoNZQfSIysrVyXzEQPhPnOUjmD1bcQGHH6s7c8cViKWar8YplQImE5N9JRqMCLrM2CdxOrZA==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-ppc64-gnu": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.2.8.tgz", + "integrity": "sha512-FhiOziBDWPBjbcmRzfLyIJnaP7AVMFXT7YCXPjXxj7wKU3vx24RjrCNN/zjvVa+N2vVoHJwCoUBvsrN/DG3zIA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-s390x-gnu": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.2.8.tgz", + "integrity": "sha512-WnHfADMzOV2Y55wlx1hzzQnar/wDt/VdvWSD99r18Mz9ylNieIGOkRx3UV21h7m/eJvjySYJkO26VvGNFkwsIQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-gnu": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.2.8.tgz", + "integrity": "sha512-H9tRr5ibfXFVLxbPOseVewewFpl28zcEdjRDt2FTUZU7odxP0gEv1ki4/kGmcGOh78oRwZuuQllGLZ9zTJp84g==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-musl": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.2.8.tgz", + "integrity": "sha512-UefiqfM3D6IVNlZ8tSGs9+Ejjud2T+oxO0IHADU45Y+lyEjD2dVFyZHbkfX0LUb5Zugo/oIv1eCO/KVYhgYJYA==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-openharmony-arm64": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.2.8.tgz", + "integrity": "sha512-637Ke4kWSy6rp9cxQ9gMOXlxPgIw/c1beASV4M//3+9I4uwBVOOl74G+e3zyU3u19U7RkRl/HuewixZ/Z6+Rjg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.2.8.tgz", + "integrity": "sha512-xWBkPOF1Q9k/Gv1nQXnVdLxKu74jXppuOM4Z3mnypVUJJJwLsMl7hNJGRAUJoG8A5MgOI1ACKM+wBFxSJzKy4A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-x64-msvc": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.2.8.tgz", + "integrity": "sha512-uz2ZvfgXbxqNwijjjbxrnvALwpyODDcgc1T1N8N3rf/DXKQmaFwmB4LX4yyjggpwN2obdQLb2rgirX5ffCWYng==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz", + "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/chai": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" + } + }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/esrecurse": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@types/esrecurse/-/esrecurse-4.3.1.tgz", + "integrity": "sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "24.13.4", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.13.4.tgz", + "integrity": "sha512-YJ7EqCstVTzIr0fMr7qul/977en+pQHrfmuKIo6Zr9i75Be21dr3MovcfvGtyvi2HAUrRerWps5sMO9I7WaxDw==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~7.18.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.70.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.70.0.tgz", + "integrity": "sha512-/v8HZt6RlyIZxB3ntehELOcUcfxKPVGWXnQdJuHRmzrqgF8nQypcC/oxGW+Ot4VGKDq81XugPKxx0n5PBtf9PA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.12.2", + "@typescript-eslint/scope-manager": "8.70.0", + "@typescript-eslint/type-utils": "8.70.0", + "@typescript-eslint/utils": "8.70.0", + "@typescript-eslint/visitor-keys": "8.70.0", + "ignore": "^7.0.5", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^8.70.0", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { + "version": "7.0.9", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.9.tgz", + "integrity": "sha512-brTTsvFRt5C1gGHtPst/281UjPD5t9fBqbgoMPlVWy11ZLTPfu7HxK4ZYqO9H7o/yC9rSTCI85EaQ4OoY12qYw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "8.70.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.70.0.tgz", + "integrity": "sha512-zYvrmj9Yxd63UGaXw+kdt6A0F0s0qveJyuatIM77bYC2DE4pgmg7a50u8LR7PRtXd0x+h+Tl3eXabGm06SWd3Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/scope-manager": "8.70.0", + "@typescript-eslint/types": "8.70.0", + "@typescript-eslint/typescript-estree": "8.70.0", + "@typescript-eslint/visitor-keys": "8.70.0", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/project-service": { + "version": "8.70.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.70.0.tgz", + "integrity": "sha512-hFHbTNqhU9G+2eKFXCBVb1tjFT/LceiJ4+HfLO4pTpDI0KHi6iajpcFFkaSQ9gXmCh7n82A0PthaayEdN6mspQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.70.0", + "@typescript-eslint/types": "^8.70.0", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.70.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.70.0.tgz", + "integrity": "sha512-8nP3Kwh5hlgZ4FicGvmznAmJe8UL4sdU8tLukrPaMuQmDuk4Y8xYfzu/aYZW4xT2JCgc7H/TpDI5cGlxcWJSqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.70.0", + "@typescript-eslint/visitor-keys": "8.70.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.70.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.70.0.tgz", + "integrity": "sha512-adnkeeNq9Sq1sUf4+FRVc0KdgYghzsgFpZSQVZVvY0LCuUuN0FnQgyGzCJeC4fW1cdXseBAjU2EOqUIjbNcZUw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "8.70.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.70.0.tgz", + "integrity": "sha512-NUMKIhYVaVIVLnRL9CRt+VVcuLgSHUCpXn4/+K8wql+vdInUzvx8BjUO1oJ7cG9shjFJKtF8F8Hh2kCh3/KBVw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.70.0", + "@typescript-eslint/typescript-estree": "8.70.0", + "@typescript-eslint/utils": "8.70.0", + "debug": "^4.4.3", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "8.70.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.70.0.tgz", + "integrity": "sha512-asTOIYhDg4zdzOScCyaytrsV3cR6B4ecPQlXw/dJIm7J/MZTtCtfVII9JD8Geh4jTCrK/Xe6cg5UevoleMcoJQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.70.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.70.0.tgz", + "integrity": "sha512-d9NmHMPEKQ7QCLLm1jI3zmoQBwT5KwFYjXBJ9ymZfKCUU+5rmTRykKAFvH5Qn/ZCds3CEAFS9OC9M/jkl0X2bA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/project-service": "8.70.0", + "@typescript-eslint/tsconfig-utils": "8.70.0", + "@typescript-eslint/types": "8.70.0", + "@typescript-eslint/visitor-keys": "8.70.0", + "debug": "^4.4.3", + "minimatch": "^10.2.2", + "semver": "^7.7.3", + "tinyglobby": "^0.2.15", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.70.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.70.0.tgz", + "integrity": "sha512-oZmtKJz/4fufZ2p3+Cn3ijEojcdfR+1zYDH2xKYrEly0dR/Q/1xUPRCOlKGxod78nWlU2UnDe09GZ3TaknBFGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.9.1", + "@typescript-eslint/scope-manager": "8.70.0", + "@typescript-eslint/types": "8.70.0", + "@typescript-eslint/typescript-estree": "8.70.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.70.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.70.0.tgz", + "integrity": "sha512-BoC8PiO4Hkdo0TVJh9Ntxr5MxPDI7/oFsrygN5ADelFSeXG/qgNuucIGA+L5Z6JpPTE/uRfcTWtscjbUaufepQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.70.0", + "eslint-visitor-keys": "^5.0.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@vitest/expect": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.11.tgz", + "integrity": "sha512-VX2x5vNJXET47KAFzwERI+KRMtTTCSWTfSMKsW7JsUsXV4psq++e3DvZpuTDOpHcxytiDs6p2nhVb2tVDiiUYw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.1.0", + "@types/chai": "^5.2.2", + "@vitest/spy": "4.1.11", + "@vitest/utils": "4.1.11", + "chai": "^6.2.2", + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.11.tgz", + "integrity": "sha512-2XJVD55d1o5AZous5CCGKS74g/riOj9odEt2bQpCVZeblHyHdnMeFl4jl0XjU21stf4mbjUkew2eXQZt65g5CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "4.1.11", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.21" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/pretty-format": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.11.tgz", + "integrity": "sha512-yiZzPbGTS9Sr/JpFl8zHrcIkAofNbFV6k21vIgQN/cY/oxZeXhJv5sc/MBJ5jFKWmWs+oJHw0UXLZjmf931+Vw==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.11.tgz", + "integrity": "sha512-LztvUgdwMNJMIkj3hQnnxiC2Xy1zNxq928W/xhjCLaNCzqTZOudjwbQf6v9IntZGPw132i2Lq2rgTRZHD3JHNw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "4.1.11", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.11.tgz", + "integrity": "sha512-pN7ikn1ON7h8ee4gIAp4AzyK+zBtJPzVbqOgu5LCEh4VaJVbPQcgYQYJIMGQPXVeJJq1fnfazis7a5pFNPahog==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.1.11", + "@vitest/utils": "4.1.11", + "magic-string": "^0.30.21", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.11.tgz", + "integrity": "sha512-apNa/prQy2qCeywhnixOHPRCgGNhvg7T4Dapfl1GahLp/R+uhBm5cPyFoNVyqsNd2h1nJxL6BqqdIjiABL60YA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.11.tgz", + "integrity": "sha512-zTCVGpyFsGWBhllOyKlTw/vnr6D9qxsfSDyfbyZmTyjHw5N/VuvzHpHoQjm2ZJzn4RJgx5w4r7V0er69CmLgPQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.1.11", + "convert-source-map": "^2.0.0", + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/abstract-logging": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/abstract-logging/-/abstract-logging-2.0.1.tgz", + "integrity": "sha512-2BjRTZxTPvheOvGbBslFSYOUkr+SjPtOnrLP33f+VIWLzezQpZcqVg7ja3L4dBXmzzgwT+a029jRx5PCi3JuiA==", + "license": "MIT" + }, + "node_modules/acorn": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.18.0.tgz", + "integrity": "sha512-lGq+9yr1/GuAWaVYIHRjvvySG5/4VfKIvC8EWxStPdcDh/Ka7FG3twP6v4d5BkravUilhIAsG4Qj83t02LWUPQ==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/ajv": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", + "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-formats": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz", + "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==", + "license": "MIT", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/ajv/node_modules/fast-uri": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.7.tgz", + "integrity": "sha512-dOvZVzjdZdz7phd9v6jCbwxrBW3fK6n8Rc0CtdmM4bumzMnxywBYhuph6J819RRw/ku+rLbelwfMunktuzVVHg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/atomic-sleep": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/atomic-sleep/-/atomic-sleep-1.0.0.tgz", + "integrity": "sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==", + "license": "MIT", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/avvio": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/avvio/-/avvio-9.3.0.tgz", + "integrity": "sha512-g2tQ7LE7oOSqDfwEm3M+ZCMTJc7KiZCdJ4UwyZJb5ckTKyYu50OYmvv0mCFXPuYXoM4zkSt8zM9XQ9KCvxA74A==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "@fastify/error": "^4.0.0", + "fastq": "^1.17.1" + } + }, + "node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/brace-expansion": { + "version": "5.0.9", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.9.tgz", + "integrity": "sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/cacheable": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/cacheable/-/cacheable-2.5.0.tgz", + "integrity": "sha512-60cyAOytib/OzBw1JNSoSV/boK1AtHryDIjvVBk7XbN4ugfkM3+Sry7fEjNgPMGgOjuaZPAp8ruZ0Cxafwyq9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@cacheable/memory": "^2.2.0", + "@cacheable/utils": "^2.5.0", + "hookified": "^1.15.0", + "keyv": "^5.6.0", + "qified": "^0.10.1" + } + }, + "node_modules/chai": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz", + "integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cookie": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz", + "integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/es-module-lexer": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.3.2.tgz", + "integrity": "sha512-poHGpORABojJJucnV9KbOavETW8lBVnphkW77ER5/BQ5Fz7oXSoCNek7IH3vR5nRjdsEz926ibFYX8KtLQmdyw==", + "dev": true, + "license": "MIT" + }, + "node_modules/esbuild": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.2.tgz", + "integrity": "sha512-HKVLS8dvII+xoKW9kmqxbRKrnWEXfJJr/FZhhJmiqIB0e053QNYFqOBouTMO/k5sID4MvCiUCvv8b9M4h32wIA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.28.2", + "@esbuild/android-arm": "0.28.2", + "@esbuild/android-arm64": "0.28.2", + "@esbuild/android-x64": "0.28.2", + "@esbuild/darwin-arm64": "0.28.2", + "@esbuild/darwin-x64": "0.28.2", + "@esbuild/freebsd-arm64": "0.28.2", + "@esbuild/freebsd-x64": "0.28.2", + "@esbuild/linux-arm": "0.28.2", + "@esbuild/linux-arm64": "0.28.2", + "@esbuild/linux-ia32": "0.28.2", + "@esbuild/linux-loong64": "0.28.2", + "@esbuild/linux-mips64el": "0.28.2", + "@esbuild/linux-ppc64": "0.28.2", + "@esbuild/linux-riscv64": "0.28.2", + "@esbuild/linux-s390x": "0.28.2", + "@esbuild/linux-x64": "0.28.2", + "@esbuild/netbsd-arm64": "0.28.2", + "@esbuild/netbsd-x64": "0.28.2", + "@esbuild/openbsd-arm64": "0.28.2", + "@esbuild/openbsd-x64": "0.28.2", + "@esbuild/openharmony-arm64": "0.28.2", + "@esbuild/sunos-x64": "0.28.2", + "@esbuild/win32-arm64": "0.28.2", + "@esbuild/win32-ia32": "0.28.2", + "@esbuild/win32-x64": "0.28.2" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "10.10.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-10.10.0.tgz", + "integrity": "sha512-NPXn6r5zl4uET1DAVPaOwzX3rut4c0wcmw3dWJAfOsTM5+TogXo0DDjz8pwm/hL8cyVNpHqeK4JpN0NjnyFFNw==", + "dev": true, + "license": "MIT", + "workspaces": [ + "packages/*" + ], + "dependencies": { + "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/regexpp": "^4.12.2", + "@eslint/config-array": "^0.23.5", + "@eslint/config-helpers": "^0.7.0", + "@eslint/core": "^1.2.1", + "@eslint/plugin-kit": "^0.7.3", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "ajv": "^6.14.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^9.1.2", + "eslint-visitor-keys": "^5.0.1", + "espree": "^11.2.0", + "esquery": "^1.7.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "11.1.5 || >11.1.6 <12", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "minimatch": "^10.2.5", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-scope": { + "version": "9.1.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-9.1.2.tgz", + "integrity": "sha512-xS90H51cKw0jltxmvmHy2Iai1LIqrfbw57b79w/J7MfvDfkIkFZ+kj6zC3BjtUwh150HsSSdxXZcsuv72miDFQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@types/esrecurse": "^4.3.1", + "@types/estree": "^1.0.8", + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", + "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/ajv": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", + "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/eslint/node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/espree": { + "version": "11.2.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-11.2.0.tgz", + "integrity": "sha512-7p3DrVEIopW1B1avAGLuCSh1jubc01H2JHc8B4qqGblmg5gI9yumBgACjWo4JlIc04ufug4xJ3SQI8HkS/Rgzw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.16.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^5.0.1" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", + "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/expect-type": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.4.0.tgz", + "integrity": "sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/fast-decode-uri-component": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/fast-decode-uri-component/-/fast-decode-uri-component-1.0.1.tgz", + "integrity": "sha512-WKgKWg5eUxvRZGwW8FvfbaH7AXSh2cL+3j5fMGzUMCxWBJ3dV3a7Wz8y2f/uQ0e3B6WmodD3oS54jTQ9HVTIIg==", + "license": "MIT" + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "license": "MIT" + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-json-stringify": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fast-json-stringify/-/fast-json-stringify-7.0.1.tgz", + "integrity": "sha512-eRSayARSbbwlBjpP4vnTTIRD5QPcIrmihPxDeN1DtKnHPg66UuJLx+8hlK1kaFdjvzyQ/dzALoi4vwAQ+T+iZA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "@fastify/merge-json-schemas": "^0.2.0", + "ajv": "^8.12.0", + "ajv-formats": "^3.0.1", + "fast-uri": "^4.0.0", + "json-schema-ref-resolver": "^3.0.0", + "rfdc": "^1.2.0" + } + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-querystring": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/fast-querystring/-/fast-querystring-1.1.2.tgz", + "integrity": "sha512-g6KuKWmFXc0fID8WWH0jit4g0AGBoJhCkJMb1RmbsSEUNvQ+ZC8D6CUZ+GtF8nMzSPXnhiePyyqqipzNNEnHjg==", + "license": "MIT", + "dependencies": { + "fast-decode-uri-component": "^1.0.1" + } + }, + "node_modules/fast-uri": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-4.1.4.tgz", + "integrity": "sha512-dODXrIxlS9JSdgAnhIUKOosKV1oMtU2VtVw87QRaHzyl5jxO290Ii5tEZfCfzfWNHi3jKWwBSdQj0qIyshdZdQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/fastify": { + "version": "5.12.3", + "resolved": "https://registry.npmjs.org/fastify/-/fastify-5.12.3.tgz", + "integrity": "sha512-reZ8wce5VNCcufIt9AVtzZa3L4u1j8esikn7OEgHWLVpRpL5R7Y2+Xzj70OUkv5zDfzUAxXZT6cu4Rt0zr3EKA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "@fastify/ajv-compiler": "^4.0.5", + "@fastify/error": "^4.0.0", + "@fastify/fast-json-stringify-compiler": "^5.0.0", + "@fastify/proxy-addr": "^5.0.0", + "abstract-logging": "^2.0.1", + "avvio": "^9.0.0", + "fast-json-stringify": "^7.0.0", + "find-my-way": "^9.6.0", + "light-my-request": "^6.0.0", + "pino": "^9.14.0 || ^10.1.0", + "process-warning": "^5.1.0", + "rfdc": "^1.3.1", + "secure-json-parse": "^4.0.0", + "semver": "^7.6.0", + "toad-cache": "^3.7.0" + } + }, + "node_modules/fastq": { + "version": "1.20.3", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.3.tgz", + "integrity": "sha512-XKv5nnLs6nLF71NgiKJLIZFLkPyIEuOselLG7ujZnGrRfQK8HpvY+WqKhAJUAdLomwVHErVS4LfxFlPq0/FTAw==", + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/file-entry-cache": { + "version": "11.1.5", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-11.1.5.tgz", + "integrity": "sha512-+PFTHITI08JIGhnNpGNI8T8inUpgZfk3GNEqfT9R2zZV2iFXg3CvqzSl/uEhs7TSGujYRELEANyDvS8Fj7+S7Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^6.1.23" + } + }, + "node_modules/find-my-way": { + "version": "9.9.0", + "resolved": "https://registry.npmjs.org/find-my-way/-/find-my-way-9.9.0.tgz", + "integrity": "sha512-sJsgZ1sQH2UDuowPuMKg8az7Qc8F0jnj+SKkFWU/+T0xcFlgV5skgXOGUqmQzOdmW6ALA7AhJINWx3qFBkbLHA==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-querystring": "^1.0.0", + "safe-regex2": "^5.0.0" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "6.1.23", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-6.1.23.tgz", + "integrity": "sha512-f++BY9pTk+983xK1FLzlLpmM0i0z+jHmx3QESGkURMXujQZz1k5wzwX6hjnQ8goaD0B+sYnDK1yZ6MTyZfUaqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "cacheable": "^2.5.0", + "flatted": "^3.4.2", + "hookified": "^1.15.0" + } + }, + "node_modules/flatted": { + "version": "3.4.4", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.4.tgz", + "integrity": "sha512-5+ybhBZANEJxaH3X5evAFatUxLfEHSr7n6kYJ+1Qd0mUqr4eu9gIf6GDbWHf8RJijHrjjO8G+la14SlL2SeS1Q==", + "dev": true, + "license": "ISC" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/hashery": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/hashery/-/hashery-1.5.1.tgz", + "integrity": "sha512-iZyKG96/JwPz1N55vj2Ie2vXbhu440zfUfJvSwEqEbeLluk7NnapfGqa7LH0mOsnDxTF85Mx8/dyR6HfqcbmbQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "hookified": "^1.15.0" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/hookified": { + "version": "1.15.1", + "resolved": "https://registry.npmjs.org/hookified/-/hookified-1.15.1.tgz", + "integrity": "sha512-MvG/clsADq1GPM2KGo2nyfaWVyn9naPiXrqIe4jYjXNZQt238kWyOGrsyc/DmRAQ+Re6yeo6yX/yoNCG5KAEVg==", + "dev": true, + "license": "MIT" + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/ipaddr.js": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.5.0.tgz", + "integrity": "sha512-aq+t5NAc+cS6rZQQVWC2x98CPqGtKKTMDd4Gaodv0wShnItdKg/51djkGJ1hqH+Oy0ivDftCbSLCQob8zso01w==", + "license": "MIT", + "engines": { + "node": ">= 10" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/json-schema-ref-resolver": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/json-schema-ref-resolver/-/json-schema-ref-resolver-3.0.0.tgz", + "integrity": "sha512-hOrZIVL5jyYFjzk7+y7n5JDzGlU8rfWDuYyHwGa2WA8/pcmMHezp2xsVwxrebD/Q9t8Nc5DboieySDpCp4WG4A==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "dequal": "^2.0.3" + } + }, + "node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/keyv": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-5.6.0.tgz", + "integrity": "sha512-CYDD3SOtsHtyXeEORYRx2qBtpDJFjRTGXUtmNEMGyzYOKj1TE3tycdlho7kA1Ufx9OYWZzg52QFBGALTirzDSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@keyv/serialize": "^1.1.1" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/light-my-request": { + "version": "6.6.0", + "resolved": "https://registry.npmjs.org/light-my-request/-/light-my-request-6.6.0.tgz", + "integrity": "sha512-CHYbu8RtboSIoVsHZ6Ye4cj4Aw/yg2oAFimlF7mNvfDV192LR7nDiKtSIfCuLT7KokPSTn/9kfVLm5OGN0A28A==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause", + "dependencies": { + "cookie": "^1.0.1", + "process-warning": "^4.0.0", + "set-cookie-parser": "^2.6.0" + } + }, + "node_modules/light-my-request/node_modules/process-warning": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/process-warning/-/process-warning-4.0.1.tgz", + "integrity": "sha512-3c2LzQ3rY9d0hc1emcsHhfT9Jwz0cChib/QN89oME2R451w5fy3f0afAhERFZAwrbDU43wk12d0ORBpDVME50Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT" + }, + "node_modules/lightningcss": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.33.0.tgz", + "integrity": "sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.33.0", + "lightningcss-darwin-arm64": "1.33.0", + "lightningcss-darwin-x64": "1.33.0", + "lightningcss-freebsd-x64": "1.33.0", + "lightningcss-linux-arm-gnueabihf": "1.33.0", + "lightningcss-linux-arm64-gnu": "1.33.0", + "lightningcss-linux-arm64-musl": "1.33.0", + "lightningcss-linux-x64-gnu": "1.33.0", + "lightningcss-linux-x64-musl": "1.33.0", + "lightningcss-win32-arm64-msvc": "1.33.0", + "lightningcss-win32-x64-msvc": "1.33.0" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.33.0.tgz", + "integrity": "sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.33.0.tgz", + "integrity": "sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.33.0.tgz", + "integrity": "sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.33.0.tgz", + "integrity": "sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.33.0.tgz", + "integrity": "sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.33.0.tgz", + "integrity": "sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.33.0.tgz", + "integrity": "sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.33.0.tgz", + "integrity": "sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.33.0.tgz", + "integrity": "sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.33.0.tgz", + "integrity": "sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.33.0.tgz", + "integrity": "sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/minimatch": { + "version": "10.2.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.6.tgz", + "integrity": "sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.8" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.18", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.18.tgz", + "integrity": "sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/obug": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/obug/-/obug-2.2.1.tgz", + "integrity": "sha512-XrsrhT5sybtKI6wakr2SPOlGZWWYbUXZ7a0jT8/QOeAPau+1X/bSegNe5YR75oJmEZQbKningirmGOEJCIk61Q==", + "dev": true, + "funding": [ + "https://github.com/sponsors/sxzz", + "https://opencollective.com/debug" + ], + "license": "MIT", + "engines": { + "node": ">=12.20.0" + } + }, + "node_modules/on-exit-leak-free": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/on-exit-leak-free/-/on-exit-leak-free-2.1.2.tgz", + "integrity": "sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==", + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.7.tgz", + "integrity": "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pino": { + "version": "10.3.1", + "resolved": "https://registry.npmjs.org/pino/-/pino-10.3.1.tgz", + "integrity": "sha512-r34yH/GlQpKZbU1BvFFqOjhISRo1MNx1tWYsYvmj6KIRHSPMT2+yHOEb1SG6NMvRoHRF0a07kCOox/9yakl1vg==", + "license": "MIT", + "dependencies": { + "@pinojs/redact": "^0.4.0", + "atomic-sleep": "^1.0.0", + "on-exit-leak-free": "^2.1.0", + "pino-abstract-transport": "^3.0.0", + "pino-std-serializers": "^7.0.0", + "process-warning": "^5.0.0", + "quick-format-unescaped": "^4.0.3", + "real-require": "^0.2.0", + "safe-stable-stringify": "^2.3.1", + "sonic-boom": "^4.0.1", + "thread-stream": "^4.0.0" + }, + "bin": { + "pino": "bin.js" + } + }, + "node_modules/pino-abstract-transport": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pino-abstract-transport/-/pino-abstract-transport-3.0.0.tgz", + "integrity": "sha512-wlfUczU+n7Hy/Ha5j9a/gZNy7We5+cXp8YL+X+PG8S0KXxw7n/JXA3c46Y0zQznIJ83URJiwy7Lh56WLokNuxg==", + "license": "MIT", + "dependencies": { + "split2": "^4.0.0" + } + }, + "node_modules/pino-std-serializers": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/pino-std-serializers/-/pino-std-serializers-7.1.0.tgz", + "integrity": "sha512-BndPH67/JxGExRgiX1dX0w1FvZck5Wa4aal9198SrRhZjH3GxKQUKIBnYJTdj2HDN3UQAS06HlfcSbQj2OHmaw==", + "license": "MIT" + }, + "node_modules/postcss": { + "version": "8.5.28", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.28.tgz", + "integrity": "sha512-RRuzqDtt5Y9h3quz5hWhK+TPnsmVs6WwSU6LkJMeY4HstUEDuYTG8UJSdawMRzmzAtV+KEoG8N3Qg2qLy5vM/A==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.18", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/process-warning": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/process-warning/-/process-warning-5.1.0.tgz", + "integrity": "sha512-jQSaVHsPgtyw60e1rQ/A+/ArPEj/S8pS/vFnyGa/gYFXrKk/6RuDkoqVDQ5NI5MmS01698ltlAk0NoDBNLujRw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT" + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/qified": { + "version": "0.10.1", + "resolved": "https://registry.npmjs.org/qified/-/qified-0.10.1.tgz", + "integrity": "sha512-+Owyggi9IxT1ePKGafcI87ubSmxol6smwJ+RAHDQlx9+9cPwFWDiKFFCPuWhr9ignlGpZ9vDQLw67N4dcTVFEA==", + "dev": true, + "license": "MIT", + "dependencies": { + "hookified": "^2.1.1" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/qified/node_modules/hookified": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/hookified/-/hookified-2.2.0.tgz", + "integrity": "sha512-p/LgFzRN5FeoD3DLS6bkUapeye6E4SI6yJs6KetENd18S+FBthqYq2amJUWpt5z0EQwwHemidjY5OqJGEKm5uA==", + "dev": true, + "license": "MIT" + }, + "node_modules/quick-format-unescaped": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/quick-format-unescaped/-/quick-format-unescaped-4.0.4.tgz", + "integrity": "sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==", + "license": "MIT" + }, + "node_modules/real-require": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/real-require/-/real-require-0.2.0.tgz", + "integrity": "sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==", + "license": "MIT", + "engines": { + "node": ">= 12.13.0" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ret": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/ret/-/ret-0.5.0.tgz", + "integrity": "sha512-I1XxrZSQ+oErkRR4jYbAyEEu2I0avBvvMM5JN+6EBprOGRCs63ENqZ3vjavq8fBw2+62G5LF5XelKwuJpcvcxw==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rfdc": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz", + "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==", + "license": "MIT" + }, + "node_modules/rolldown": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.2.8.tgz", + "integrity": "sha512-Z67nTmhZe7anqnM/EjI392w5i/ANUinjip7QYsOyN37oayduxt3ksdX0hf5OOamkAd53BiIHfbfSzfUmzKFQqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@oxc-project/types": "=0.149.0", + "@rolldown/pluginutils": "^1.0.0" + }, + "bin": { + "rolldown": "bin/cli.mjs" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "optionalDependencies": { + "@rolldown/binding-android-arm-eabi": "1.2.8", + "@rolldown/binding-android-arm64": "1.2.8", + "@rolldown/binding-darwin-arm64": "1.2.8", + "@rolldown/binding-darwin-x64": "1.2.8", + "@rolldown/binding-freebsd-x64": "1.2.8", + "@rolldown/binding-linux-arm-gnueabihf": "1.2.8", + "@rolldown/binding-linux-arm64-gnu": "1.2.8", + "@rolldown/binding-linux-arm64-musl": "1.2.8", + "@rolldown/binding-linux-ppc64-gnu": "1.2.8", + "@rolldown/binding-linux-s390x-gnu": "1.2.8", + "@rolldown/binding-linux-x64-gnu": "1.2.8", + "@rolldown/binding-linux-x64-musl": "1.2.8", + "@rolldown/binding-openharmony-arm64": "1.2.8", + "@rolldown/binding-win32-arm64-msvc": "1.2.8", + "@rolldown/binding-win32-x64-msvc": "1.2.8" + } + }, + "node_modules/safe-regex2": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/safe-regex2/-/safe-regex2-5.1.1.tgz", + "integrity": "sha512-mOSBvHGDZMuIEZMdOz/aCEYDCv0E7nfcNsIhUF+/P+xC7Hyf3FkvymqgPbg9D1EdSGu+uKbJgy09K/RKKc7kJA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "ret": "~0.5.0" + }, + "bin": { + "safe-regex2": "bin/safe-regex2.js" + } + }, + "node_modules/safe-stable-stringify": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.5.0.tgz", + "integrity": "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/secure-json-parse": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/secure-json-parse/-/secure-json-parse-4.1.0.tgz", + "integrity": "sha512-l4KnYfEyqYJxDwlNVyRfO2E4NTHfMKAWdUuA8J0yve2Dz/E/PdBepY03RvyJpssIpRFwJoCD55wA+mEDs6ByWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/set-cookie-parser": { + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz", + "integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==", + "license": "MIT" + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, + "node_modules/sonic-boom": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-4.2.1.tgz", + "integrity": "sha512-w6AxtubXa2wTXAUsZMMWERrsIRAdrK0Sc+FUytWvYAhBJLyuI4llrMIC1DtlNSdI99EI86KZum2MMq3EAZlF9Q==", + "license": "MIT", + "dependencies": { + "atomic-sleep": "^1.0.0" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/split2": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", + "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", + "license": "ISC", + "engines": { + "node": ">= 10.x" + } + }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/std-env": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.2.0.tgz", + "integrity": "sha512-oCUKSupKTHX53EyjDtuZQ64pjLJ6yYCtpmEw0goYxtjG9KpbRe8KAsl2tBUGU9DyMcJ0RwJ8GqJAFzMXcXW1Rw==", + "dev": true, + "license": "MIT" + }, + "node_modules/thread-stream": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/thread-stream/-/thread-stream-4.2.0.tgz", + "integrity": "sha512-e2zZ96wSChazBsbENf/Pcm/4swHt2cEKQ92rhUjkL9GCKiTDJIaTBenjE/m9DXi0QBmTMDkFDdOomUy20A1tDQ==", + "license": "MIT", + "dependencies": { + "real-require": "^1.0.0" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/thread-stream/node_modules/real-require": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/real-require/-/real-require-1.0.0.tgz", + "integrity": "sha512-P4nbQYQfePJxRSmY+v/KINxVucm4NF3p3s7pJveMTtom52FR4YGltUQLB8idDXwDDWW+eYrWDFbuzUnjoWHF7g==", + "license": "MIT" + }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.3.1.tgz", + "integrity": "sha512-GCvB3aoys96IuDFBMcTB46JOR6mdMtAToqwiW8JlWhsoh1mhHi/xn9ss/Dg7N555GiJyEt2qzoG/NHCwM6h1EA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinyrainbow": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.1.1.tgz", + "integrity": "sha512-yau8yJdTt989Mm0Bd/236QnzEiPf2xLLTqUZRUJOo/3CB078LSwzei343DgtJVmfJKJE3TMINY1u42SQsP6mXw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/toad-cache": { + "version": "3.7.4", + "resolved": "https://registry.npmjs.org/toad-cache/-/toad-cache-3.7.4.tgz", + "integrity": "sha512-m1TdR/rvT7kgGJZhspNtXdsdYk0fddFpJJFlG5s+UkPFo6lkLoZ3YLOaovPYjq1R75NP5JfeTlSHaOsE09peCg==", + "license": "MIT", + "engines": { + "node": ">=20" + } + }, + "node_modules/ts-api-utils": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz", + "integrity": "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" + } + }, + "node_modules/tsx": { + "version": "4.23.13", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.23.13.tgz", + "integrity": "sha512-BL5MGkRln6aDYhb0xbQlEAGw743BaZYWdbWtdJOBriYJboKgUUYCadFp2/FpBBZquBC/ezNBn7wMMPx7FDZUDw==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "~0.28.0" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + } + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/typescript-eslint": { + "version": "8.70.0", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.70.0.tgz", + "integrity": "sha512-P/W5cz70/cQAuKfY3xwQMWWTV7BvJ0mAQmi+9mBcsVPaBUpd6Ohpa+fECv9rBFrQcig86jAiNBFNWUqnTjr4pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/eslint-plugin": "8.70.0", + "@typescript-eslint/parser": "8.70.0", + "@typescript-eslint/typescript-estree": "8.70.0", + "@typescript-eslint/utils": "8.70.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/undici-types": { + "version": "7.18.2", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz", + "integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==", + "dev": true, + "license": "MIT" + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/vite": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.3.0.tgz", + "integrity": "sha512-lhZBVvEHefgE+HQZC9O7EBJgCU/nVzFNl7vkS4RE0APtWLP02/8QVIkQtzBxPquh7lq5/78NHipTj7ODQ6XuyQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "lightningcss": "^1.33.0", + "picomatch": "^4.0.7", + "postcss": "^8.5.28", + "rolldown": "~1.2.6", + "tinyglobby": "^0.2.17" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "@vitejs/devtools": "^0.7.1", + "esbuild": "^0.27.0 || ^0.28.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "@vitejs/devtools": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vitest": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.11.tgz", + "integrity": "sha512-fhACrNXUidIbGSBr5FlbuBkO7VWC1ZyLl0DO4CU2DrQoAPxX84Ysxs+HeGQpii5lZWV1Q4gBZTTu49mF+A6Edw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/expect": "4.1.11", + "@vitest/mocker": "4.1.11", + "@vitest/pretty-format": "4.1.11", + "@vitest/runner": "4.1.11", + "@vitest/snapshot": "4.1.11", + "@vitest/spy": "4.1.11", + "@vitest/utils": "4.1.11", + "es-module-lexer": "^2.0.0", + "expect-type": "^1.3.0", + "magic-string": "^0.30.21", + "obug": "^2.1.1", + "pathe": "^2.0.3", + "picomatch": "^4.0.3", + "std-env": "^4.0.0-rc.1", + "tinybench": "^2.9.0", + "tinyexec": "^1.0.2", + "tinyglobby": "^0.2.15", + "tinyrainbow": "^3.1.0", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@opentelemetry/api": "^1.9.0", + "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", + "@vitest/browser-playwright": "4.1.11", + "@vitest/browser-preview": "4.1.11", + "@vitest/browser-webdriverio": "4.1.11", + "@vitest/coverage-istanbul": "4.1.11", + "@vitest/coverage-v8": "4.1.11", + "@vitest/ui": "4.1.11", + "happy-dom": "*", + "jsdom": "*", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@opentelemetry/api": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser-playwright": { + "optional": true + }, + "@vitest/browser-preview": { + "optional": true + }, + "@vitest/browser-webdriverio": { + "optional": true + }, + "@vitest/coverage-istanbul": { + "optional": true + }, + "@vitest/coverage-v8": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + }, + "vite": { + "optional": false + } + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + } +} diff --git a/worker/package.json b/worker/package.json new file mode 100644 index 0000000..baba252 --- /dev/null +++ b/worker/package.json @@ -0,0 +1,30 @@ +{ + "name": "arena-plm-connectivity-worker", + "version": "0.1.0", + "private": true, + "description": "Hardened TypeScript connectivity worker for Arena PLM -> SAP S/4HANA (ADR-002: the only side-by-side component; machine-to-machine, no UI, no user-facing URLs).", + "type": "module", + "engines": { + "node": ">=24" + }, + "scripts": { + "build": "tsc -p tsconfig.build.json", + "lint": "eslint .", + "test": "vitest run", + "dev": "tsx watch --env-file-if-exists=.env src/index.ts" + }, + "dependencies": { + "fastify": "^5.12.3" + }, + "devDependencies": { + "@eslint/js": "^10.0.1", + "@types/node": "^24.13.4", + "ajv": "^8.20.0", + "ajv-formats": "^3.0.1", + "eslint": "^10.10.0", + "tsx": "^4.23.13", + "typescript": "~5.9.3", + "typescript-eslint": "^8.70.0", + "vitest": "^4.1.11" + } +} diff --git a/worker/src/arena/client.ts b/worker/src/arena/client.ts new file mode 100644 index 0000000..dce8192 --- /dev/null +++ b/worker/src/arena/client.ts @@ -0,0 +1,36 @@ +/** + * Arena PLM API client — build wave 1 stub. + * + * Real Arena calls will run through `session.withSession(...)` and carry the + * session id in the `arena_session_id` request header (predecessor-verified + * behavior, 2026-09-04). No live network calls are made in this wave. + */ + +import type { ArenaSessionManager } from './session.js'; + +/** Header Arena expects the session id in on every authenticated call. */ +export const ARENA_SESSION_HEADER = 'arena_session_id'; + +export class ArenaClient { + constructor(private readonly session: ArenaSessionManager) {} + + get sessionState() { + return this.session.state(); + } + + /** + * STUB — delta-poll seam. Wave 1 deliberately returns no changes and makes + * no network call (and therefore does not trigger a login). + * + * The real implementation MUST NOT implement `?since=` on trust: Arena's + * `/changes` feed has NO working server-side date filter, so it must either + * filter client-side (paging the feed, O(history) — the predecessor's + * approach) or use Arena's outbound-integration events endpoint + * (`/outboundintegrations/{guid}/events`, `reconciled` flag as the cursor + * held by Arena — the clean delta mechanism, PRD V-3) + * [KNOWLEDGE.md 2026-09-10]. Calls go through `this.session.withSession(...)`. + */ + async fetchChangesSince(_since: Date): Promise { + return []; + } +} diff --git a/worker/src/arena/session.ts b/worker/src/arena/session.ts new file mode 100644 index 0000000..b75c53b --- /dev/null +++ b/worker/src/arena/session.ts @@ -0,0 +1,174 @@ +/** + * Arena PLM session management. + * + * Behavioral constraints [KNOWLEDGE.md, 2026-09-04, verified by predecessor in + * live use]: + * - `POST /login` returns `arenaSessionId`; sessions have a ~25-minute TTL + * (KNOWLEDGE records only the TTL, not whether it is idle- or absolute-based). + * We cache for a configurable TTL (config default 24 min) to refresh ahead + * of expiry. + * - Concurrent logins race on Arena's side — login is serialized behind a + * promise lock so concurrent callers coalesce onto one in-flight login. + * - On a 401 from any Arena call, invalidate the cached session, re-login, + * and retry exactly once. + */ + +import type { ArenaConfig } from '../config.js'; +import type { LoginHealthSink } from '../health/registry.js'; +import type { HttpClient } from '../http/types.js'; + +/** Wire values of `HealthStatus.arenaSession.state` (docs/api/worker.openapi.json). */ +export type ArenaSessionState = 'none' | 'active' | 'expired' | 'refreshing'; + +export class ArenaApiError extends Error { + readonly status: number; + readonly body: unknown; + + constructor(message: string, status: number, body?: unknown) { + super(message); + this.name = 'ArenaApiError'; + this.status = status; + this.body = body; + } +} + +/** + * Derived from `ArenaConfig` so the composition root can spread `config.arena` + * straight in: a field rename on either side is a compile error, not a + * silently-dropped env value (the `ttlMs` vs `sessionTtlMs` bug of 0.1.0). + */ +export interface ArenaSessionOptions + extends Pick { + http: HttpClient; + /** Injectable clock for tests. */ + now?: () => number; + /** Reports login outcomes for `GET /health` (`status: degraded` on failure). */ + health?: LoginHealthSink; +} + +export class ArenaSessionManager { + private readonly http: HttpClient; + private readonly baseUrl: string; + private readonly email: string; + private readonly password: string; + private readonly workspaceId: number | undefined; + private readonly ttlMs: number; + private readonly now: () => number; + private readonly health: LoginHealthSink | undefined; + + private sessionId: string | null = null; + private expiresAt = 0; + /** Promise lock: while set, a login is in flight and callers must join it. */ + private loginInFlight: Promise | null = null; + + constructor(options: ArenaSessionOptions) { + if (!Number.isFinite(options.sessionTtlMs) || options.sessionTtlMs <= 0) { + throw new Error(`ArenaSessionManager: sessionTtlMs must be a positive number, got ${options.sessionTtlMs}`); + } + this.http = options.http; + this.baseUrl = options.baseUrl.replace(/\/+$/, ''); + this.email = options.email; + this.password = options.password; + this.workspaceId = options.workspaceId; + this.ttlMs = options.sessionTtlMs; + this.now = options.now ?? Date.now; + this.health = options.health; + } + + /** Effective cache lifetime in ms (what the env actually wired). */ + get sessionTtlMs(): number { + return this.ttlMs; + } + + /** + * `refreshing` wins while a login is in flight (first login or re-login); + * otherwise `none` / `active` / `expired` describe the cached session. + */ + state(): ArenaSessionState { + if (this.loginInFlight !== null) return 'refreshing'; + if (this.sessionId === null) return 'none'; + return this.now() < this.expiresAt ? 'active' : 'expired'; + } + + /** Valid session id — cached when fresh, otherwise one coalesced login. */ + async getSessionId(): Promise { + if (this.sessionId !== null && this.now() < this.expiresAt) { + return this.sessionId; + } + return this.loginCoalesced(); + } + + /** + * Drop the cached session so the next call re-logs in. When `staleSessionId` + * is given, only invalidates if it still matches the cache — a fresher login + * may already have replaced the session the caller saw fail. + */ + invalidate(staleSessionId?: string): void { + if (staleSessionId !== undefined && staleSessionId !== this.sessionId) return; + this.sessionId = null; + this.expiresAt = 0; + } + + /** + * Run an Arena call with a valid session id. If the call throws an + * `ArenaApiError` with status 401, invalidate + re-login + retry exactly + * once. Any other error (and a second 401) propagates. + */ + async withSession(fn: (sessionId: string) => Promise): Promise { + const sessionId = await this.getSessionId(); + try { + return await fn(sessionId); + } catch (err) { + if (err instanceof ArenaApiError && err.status === 401) { + this.invalidate(sessionId); + const freshSessionId = await this.getSessionId(); + return fn(freshSessionId); + } + throw err; + } + } + + private loginCoalesced(): Promise { + if (this.loginInFlight !== null) return this.loginInFlight; + const login = this.login().finally(() => { + this.loginInFlight = null; + }); + this.loginInFlight = login; + return login; + } + + private async login(): Promise { + try { + const sessionId = await this.requestLogin(); + this.sessionId = sessionId; + this.expiresAt = this.now() + this.ttlMs; + this.health?.recordLoginSuccess(); + return sessionId; + } catch (err) { + this.health?.recordLoginFailure(err); + throw err; + } + } + + private async requestLogin(): Promise { + const res = await this.http.request({ + method: 'POST', + url: `${this.baseUrl}/login`, + headers: { 'Content-Type': 'application/json' }, + body: { + email: this.email, + password: this.password, + ...(this.workspaceId !== undefined ? { workspaceId: this.workspaceId } : {}), + }, + }); + if (res.status !== 200) { + throw new ArenaApiError(`Arena login failed with HTTP ${res.status}`, res.status, res.body); + } + const body = res.body as { arenaSessionId?: unknown } | null; + const sessionId = body?.arenaSessionId; + if (typeof sessionId !== 'string' || sessionId.length === 0) { + throw new ArenaApiError('Arena login returned no arenaSessionId', res.status, res.body); + } + return sessionId; + } +} diff --git a/worker/src/auth/bearerAuth.ts b/worker/src/auth/bearerAuth.ts new file mode 100644 index 0000000..0263761 --- /dev/null +++ b/worker/src/auth/bearerAuth.ts @@ -0,0 +1,39 @@ +/** + * PLACEHOLDER bearer auth: shared token from env, compared timing-safely. + * + * The real webhook/admin auth scheme for the CF-hosted worker is an OPEN + * decision (PRD OQ-6). It is deliberately isolated in this one module — routes + * only ever see the returned Fastify hook — so the scheme can be swapped + * without touching anything else. + * + * Fail-closed: if no token is configured, every guarded request gets 401. + */ + +import { timingSafeEqual } from 'node:crypto'; +import type { FastifyReply, FastifyRequest } from 'fastify'; +import { sendProblem } from '../http/problem.js'; + +export type AuthHook = (req: FastifyRequest, reply: FastifyReply) => Promise; + +export function makeBearerAuthHook(expectedToken: string | undefined): AuthHook { + return async function bearerAuthHook(req: FastifyRequest, reply: FastifyReply): Promise { + const header = req.headers.authorization; + if ( + expectedToken === undefined || + expectedToken === '' || + header === undefined || + !header.startsWith('Bearer ') || + !tokensMatch(header.slice('Bearer '.length), expectedToken) + ) { + return sendProblem(reply, 401, 'Unauthorized', 'Missing or invalid bearer token'); + } + return undefined; + }; +} + +function tokensMatch(candidate: string, expected: string): boolean { + const candidateBuf = Buffer.from(candidate); + const expectedBuf = Buffer.from(expected); + if (candidateBuf.length !== expectedBuf.length) return false; + return timingSafeEqual(candidateBuf, expectedBuf); +} diff --git a/worker/src/config.ts b/worker/src/config.ts new file mode 100644 index 0000000..b8974d2 --- /dev/null +++ b/worker/src/config.ts @@ -0,0 +1,118 @@ +/** + * Env-only configuration. No secrets in the repo; `.env` is git-ignored and + * loaded only by `npm run dev` (`--env-file-if-exists`) — production gets its + * env from the platform. + * + * Scope note (ARN-004): business mappings are maintained IN THE TENANT, never + * here. This module only configures connectivity and execution mechanics — + * the sync chain order below is the ARN-001 "config-driven order" knob. + */ + +import { DEFAULT_WEBHOOK_QUEUE_MAX_SIZE } from './queue/webhookQueue.js'; +import { SYNC_OBJECT_TYPES, type SyncObjectType } from './sync/types.js'; + +export interface ArenaConfig { + baseUrl: string; + email: string; + password: string; + workspaceId?: number; + /** Session cache lifetime in ms — the ONE name shared with ArenaSessionOptions. */ + sessionTtlMs: number; + /** Per-request timeout for the Arena transport (AbortSignal.timeout). */ + httpTimeoutMs: number; +} + +export interface WorkerConfig { + host: string; + port: number; + /** + * Shared bearer token for guarded endpoints. PLACEHOLDER auth — the real + * scheme is an open decision (PRD OQ-6); swapping it must only touch + * `src/auth/bearerAuth.ts`. Unset => guarded endpoints fail closed (401). + */ + bearerToken?: string; + arena: ArenaConfig; + pollIntervalMs: number; + /** ARN-001 sync chain order. Default PRODUCT → CHANGE_MASTER → BOM → DOCUMENTS. */ + syncObjectOrder: SyncObjectType[]; + /** Upper bound of the transient ingest buffer (ARN-003: buffer, never a store). */ + webhookQueueMaxSize: number; + /** How long graceful shutdown waits for in-flight runs/polls before exiting 1. */ + shutdownTimeoutMs: number; +} + +export const DEFAULT_SYNC_OBJECT_ORDER: readonly SyncObjectType[] = SYNC_OBJECT_TYPES; +export const DEFAULT_SHUTDOWN_TIMEOUT_MS = 30_000; +export const DEFAULT_ARENA_HTTP_TIMEOUT_MS = 30_000; + +export function loadConfig(env: NodeJS.ProcessEnv = process.env): WorkerConfig { + const config: WorkerConfig = { + host: env.WORKER_HOST ?? '0.0.0.0', + port: parsePositiveInt('WORKER_PORT', env.WORKER_PORT, 8080), + arena: { + baseUrl: (env.ARENA_API_URL ?? 'https://api.arenasolutions.com/v1').replace(/\/+$/, ''), + email: env.ARENA_API_EMAIL ?? '', + password: env.ARENA_API_PASSWORD ?? '', + // Arena sessions have a ~25-minute TTL [KNOWLEDGE.md 2026-09-04] (whether + // it is idle- or absolute-based is not recorded — do not assume either); + // cache for 24 min by default to refresh ahead of expiry. + sessionTtlMs: parsePositiveInt('ARENA_SESSION_TTL_MINUTES', env.ARENA_SESSION_TTL_MINUTES, 24) * 60_000, + httpTimeoutMs: parsePositiveInt('ARENA_HTTP_TIMEOUT_MS', env.ARENA_HTTP_TIMEOUT_MS, DEFAULT_ARENA_HTTP_TIMEOUT_MS), + }, + pollIntervalMs: parsePositiveInt('ARENA_POLL_INTERVAL_SECONDS', env.ARENA_POLL_INTERVAL_SECONDS, 300) * 1000, + syncObjectOrder: parseSyncObjectOrder(env.SYNC_OBJECT_ORDER), + webhookQueueMaxSize: parsePositiveInt( + 'WEBHOOK_QUEUE_MAX_SIZE', + env.WEBHOOK_QUEUE_MAX_SIZE, + DEFAULT_WEBHOOK_QUEUE_MAX_SIZE, + ), + shutdownTimeoutMs: parsePositiveInt('SHUTDOWN_TIMEOUT_MS', env.SHUTDOWN_TIMEOUT_MS, DEFAULT_SHUTDOWN_TIMEOUT_MS), + }; + if (env.WORKER_BEARER_TOKEN !== undefined && env.WORKER_BEARER_TOKEN !== '') { + config.bearerToken = env.WORKER_BEARER_TOKEN; + } + if (env.ARENA_WORKSPACE_ID !== undefined && env.ARENA_WORKSPACE_ID !== '') { + config.arena.workspaceId = parsePositiveInt('ARENA_WORKSPACE_ID', env.ARENA_WORKSPACE_ID, NaN); + } + return config; +} + +/** + * Strict positive integer: the whole string must be digits. `parseInt` alone + * accepted `8080abc` as 8080, silently masking a corrupted env value. + */ +function parsePositiveInt(name: string, raw: string | undefined, fallback: number): number { + if (raw === undefined || raw === '') return fallback; + const trimmed = raw.trim(); + const value = /^\d+$/.test(trimmed) ? Number.parseInt(trimmed, 10) : NaN; + if (!Number.isSafeInteger(value) || value <= 0) { + throw new Error(`Invalid ${name}: expected a positive integer, got "${raw}"`); + } + return value; +} + +function parseSyncObjectOrder(raw: string | undefined): SyncObjectType[] { + if (raw === undefined || raw.trim() === '') { + return [...DEFAULT_SYNC_OBJECT_ORDER]; + } + const entries = raw.split(',').map((entry) => entry.trim().toUpperCase()); + const order: SyncObjectType[] = []; + for (const entry of entries) { + if (!isSyncObjectType(entry)) { + throw new Error( + `Invalid SYNC_OBJECT_ORDER entry "${entry}" — valid values: ${SYNC_OBJECT_TYPES.join(', ')}`, + ); + } + // Same rule as the HTTP contract (`objects` has uniqueItems): an object + // type may appear at most once in the chain. + if (order.includes(entry)) { + throw new Error(`Invalid SYNC_OBJECT_ORDER: duplicate entry "${entry}" — each object type at most once`); + } + order.push(entry); + } + return order; +} + +function isSyncObjectType(value: string): value is SyncObjectType { + return (SYNC_OBJECT_TYPES as readonly string[]).includes(value); +} diff --git a/worker/src/errors/publicPhrase.ts b/worker/src/errors/publicPhrase.ts new file mode 100644 index 0000000..2d229da --- /dev/null +++ b/worker/src/errors/publicPhrase.ts @@ -0,0 +1,49 @@ +/** + * Public error phrases — the ONLY text derived from a caught error that may + * reach a wire response (`HealthStatus.poller.lastError`, `SyncRun.error`, + * `SyncObjectProgress.error`; governed contract docs/api/worker.openapi.json). + * + * Allowlist keyed on error class / upstream HTTP status, mirroring the 502 + * mapping in server.ts: a raw `err.message` is never copied out, because + * Arena error bodies can echo request data (and, for the future S/4HANA leg, + * OData error payloads can echo business data). Anything not on the list gets + * the unclassified phrase; the full error stays in the worker log, which is + * where diagnosis belongs (index.ts logs `{ name, status, message }`). + * + * `TimeoutError` is what Node's fetch rejects with when the request's + * `AbortSignal.timeout(...)` fires (measured on this box, Node 24.15.0, + * 2026-09-14: `name=TimeoutError`, a DOMException). + */ + +import { ArenaApiError } from '../arena/session.js'; +import { IngestBufferFullError } from '../poller/arenaDeltaPoll.js'; + +export const UNCLASSIFIED_ERROR_PHRASE = 'Unclassified worker error; see worker logs.'; +export const INGEST_BUFFER_FULL_PHRASE = 'Ingest buffer full at poll time.'; +export const ARENA_TIMEOUT_PHRASE = 'Upstream Arena PLM request timed out.'; +export const ARENA_MALFORMED_PHRASE = 'Upstream Arena PLM response was malformed.'; + +export function arenaStatusPhrase(status: number): string { + // A 2xx ArenaApiError is a response we could not interpret (e.g. login + // answered 200 without an arenaSessionId), not an upstream failure code. + if (status >= 200 && status < 300) return ARENA_MALFORMED_PHRASE; + return `Upstream Arena PLM request failed (HTTP ${status}).`; +} + +export function publicErrorPhrase(err: unknown): string { + if (err instanceof ArenaApiError) return arenaStatusPhrase(err.status); + if (err instanceof IngestBufferFullError) return INGEST_BUFFER_FULL_PHRASE; + if (hasName(err, 'TimeoutError')) return ARENA_TIMEOUT_PHRASE; + return UNCLASSIFIED_ERROR_PHRASE; +} + +/** Log-safe summary of an error: class name, message, and upstream status if any. */ +export function describeErrorForLog(err: unknown): { name: string; message: string; status?: number } { + if (err instanceof ArenaApiError) return { name: err.name, message: err.message, status: err.status }; + if (err instanceof Error) return { name: err.name, message: err.message }; + return { name: typeof err, message: String(err) }; +} + +function hasName(err: unknown, name: string): boolean { + return typeof err === 'object' && err !== null && 'name' in err && (err as { name: unknown }).name === name; +} diff --git a/worker/src/health/registry.ts b/worker/src/health/registry.ts new file mode 100644 index 0000000..6d291f1 --- /dev/null +++ b/worker/src/health/registry.ts @@ -0,0 +1,77 @@ +/** + * Health registry — the tiny in-process sink the delta poller and the Arena + * session manager report into; `GET /health` reads it (governed contract: + * docs/api/worker.openapi.json, `HealthStatus`). + * + * `status` is `degraded` when the MOST RECENT poll tick or the MOST RECENT + * Arena login failed, and returns to `ok` on the next success of that same + * dependency. Nothing here is a ledger (ARN-003): it is process-local + * observability state and is meant to reset on restart. + * + * `lastError` is a FIXED PHRASE from the public allowlist + * (src/errors/publicPhrase.ts), never `err.message` — Arena error bodies can + * echo request data and this surface is unauthenticated. + */ + +import { publicErrorPhrase } from '../errors/publicPhrase.js'; + +export interface PollHealthSink { + /** A poll tick that started at `startedAt` completed without error. */ + recordPollSuccess(startedAt: Date): void; + /** A poll tick that started at `startedAt` failed with `err`. */ + recordPollFailure(startedAt: Date, err: unknown): void; +} + +export interface LoginHealthSink { + recordLoginSuccess(): void; + recordLoginFailure(err: unknown): void; +} + +/** Wire shape of `HealthStatus.poller`. */ +export interface PollerHealth { + lastRunAt?: string; + lastSuccessAt?: string; + lastError?: string; +} + +export type HealthStatusValue = 'ok' | 'degraded'; + +export class HealthRegistry implements PollHealthSink, LoginHealthSink { + private poller: PollerHealth | undefined; + private lastPollOk: boolean | undefined; + private lastLoginOk: boolean | undefined; + + recordPollSuccess(startedAt: Date): void { + const at = startedAt.toISOString(); + this.poller = { lastRunAt: at, lastSuccessAt: at }; + this.lastPollOk = true; + } + + recordPollFailure(startedAt: Date, err: unknown): void { + const previous = this.poller ?? {}; + this.poller = { + lastRunAt: startedAt.toISOString(), + ...(previous.lastSuccessAt !== undefined ? { lastSuccessAt: previous.lastSuccessAt } : {}), + lastError: publicErrorPhrase(err), + }; + this.lastPollOk = false; + } + + recordLoginSuccess(): void { + this.lastLoginOk = true; + } + + recordLoginFailure(_err: unknown): void { + this.lastLoginOk = false; + } + + status(): HealthStatusValue { + return this.lastPollOk === false || this.lastLoginOk === false ? 'degraded' : 'ok'; + } + + /** `undefined` until the poller has ticked at least once (contract: `poller` optional). */ + pollerSnapshot(): PollerHealth | undefined { + return this.poller === undefined ? undefined : { ...this.poller }; + } +} + diff --git a/worker/src/http/problem.ts b/worker/src/http/problem.ts new file mode 100644 index 0000000..2557a9d --- /dev/null +++ b/worker/src/http/problem.ts @@ -0,0 +1,37 @@ +/** + * RFC 9457 Problem Details for HTTP APIs — the governed contract + * (docs/api/worker.openapi.json) specifies `application/problem+json` for all + * error responses (400/401/404/502/503). + * + * We omit the `type` member: per RFC 9457 §3.1.1 an absent `type` is + * equivalent to "about:blank", in which case `title` mirrors the HTTP status + * phrase — which is exactly what we send. + */ + +import type { FastifyReply } from 'fastify'; + +export const PROBLEM_CONTENT_TYPE = 'application/problem+json'; + +export interface ProblemDetails { + title: string; + status: number; + detail?: string; +} + +export function sendProblem( + reply: FastifyReply, + status: number, + title: string, + detail?: string, + /** Extra response headers (e.g. `Retry-After` on 503). */ + headers: Record = {}, +): FastifyReply { + const body: ProblemDetails = detail === undefined ? { title, status } : { title, status, detail }; + // Serialize explicitly: Fastify's default JSON serializer targets + // application/json, so send a pre-serialized string under the problem type. + reply.code(status).header('content-type', `${PROBLEM_CONTENT_TYPE}; charset=utf-8`); + for (const [name, value] of Object.entries(headers)) { + reply.header(name, value); + } + return reply.send(JSON.stringify(body)); +} diff --git a/worker/src/http/types.ts b/worker/src/http/types.ts new file mode 100644 index 0000000..06f6439 --- /dev/null +++ b/worker/src/http/types.ts @@ -0,0 +1,73 @@ +/** + * Minimal HTTP abstraction for the Arena client side of the worker. + * + * Exists so the Arena session/client modules can be unit-tested against a + * mocked transport (no live network calls in tests). This abstraction is for + * Arena ONLY — S/4HANA calls will go through the SAP Cloud SDK and the + * `mc-s4hana` destination, never through this client (ADR-001). + */ + +export interface HttpRequest { + method: 'GET' | 'POST' | 'PUT' | 'DELETE'; + url: string; + headers?: Record; + /** JSON-serializable request body. */ + body?: unknown; +} + +export interface HttpResponse { + status: number; + body: unknown; +} + +export interface HttpClient { + request(req: HttpRequest): Promise; +} + +export interface FetchHttpClientOptions { + /** Per-request deadline (config `ARENA_HTTP_TIMEOUT_MS`). */ + timeoutMs: number; + /** Injectable fetch for tests; defaults to Node's global fetch. */ + fetchImpl?: typeof fetch; +} + +/** + * Production transport on Node's global fetch. Every request carries a + * deadline via `AbortSignal.timeout(delay)` — "returns a new AbortSignal + * which will be aborted in `delay` milliseconds"; fetch rejects when its + * signal aborts, so a hung Arena connection can never pin a poll tick or a + * login forever. + * [source: context7 /websites/nodejs_latest-v24_x_api globals "Static method: AbortSignal.timeout(delay)", verified 2026-09-14] + */ +export function createFetchHttpClient(options: FetchHttpClientOptions): HttpClient { + if (!Number.isFinite(options.timeoutMs) || options.timeoutMs <= 0) { + throw new Error(`createFetchHttpClient: timeoutMs must be a positive number, got ${options.timeoutMs}`); + } + const fetchImpl = options.fetchImpl ?? fetch; + return { + async request(req: HttpRequest): Promise { + const headers: Record = { ...req.headers }; + let payload: string | undefined; + if (req.body !== undefined) { + payload = JSON.stringify(req.body); + headers['Content-Type'] ??= 'application/json'; + } + const res = await fetchImpl(req.url, { + method: req.method, + headers, + body: payload, + signal: AbortSignal.timeout(options.timeoutMs), + }); + const text = await res.text(); + let body: unknown = null; + if (text.length > 0) { + try { + body = JSON.parse(text); + } catch { + body = text; + } + } + return { status: res.status, body }; + }, + }; +} diff --git a/worker/src/index.ts b/worker/src/index.ts new file mode 100644 index 0000000..a6918bd --- /dev/null +++ b/worker/src/index.ts @@ -0,0 +1,64 @@ +/** + * Composition root. Builds the worker from env config and starts the HTTP + * surface plus the delta poller. No Arena call happens at startup — the + * session logs in lazily on first use. + */ + +import { ArenaClient } from './arena/client.js'; +import { ArenaSessionManager } from './arena/session.js'; +import { loadConfig } from './config.js'; +import { describeErrorForLog } from './errors/publicPhrase.js'; +import { HealthRegistry } from './health/registry.js'; +import { createFetchHttpClient } from './http/types.js'; +import { makeArenaDeltaPoll } from './poller/arenaDeltaPoll.js'; +import { DeltaPoller } from './poller/deltaPoller.js'; +import { InMemoryEventQueue } from './queue/webhookQueue.js'; +import { buildServer } from './server.js'; +import { registerGracefulShutdown } from './shutdown.js'; +import { makeNoopExecutors } from './sync/executors.js'; +import { InMemoryRunStore } from './sync/runStore.js'; +import { SyncRunner } from './sync/runner.js'; + +async function main(): Promise { + const config = loadConfig(); + const health = new HealthRegistry(); + + // `sessionConfig` is exactly ArenaSessionOptions' Pick of ArenaConfig — a + // field mismatch between the two is a compile error here. + const { httpTimeoutMs, ...sessionConfig } = config.arena; + const arenaSession = new ArenaSessionManager({ + http: createFetchHttpClient({ timeoutMs: httpTimeoutMs }), + health, + ...sessionConfig, + }); + const arenaClient = new ArenaClient(arenaSession); + + const queue = new InMemoryEventQueue({ maxSize: config.webhookQueueMaxSize }); + const runStore = new InMemoryRunStore(); + const runner = new SyncRunner(runStore, makeNoopExecutors()); + + const app = buildServer({ config, queue, runner, runStore, arenaSession, health, logger: true }); + + const poller = new DeltaPoller({ + intervalMs: config.pollIntervalMs, + poll: makeArenaDeltaPoll({ client: arenaClient, queue, log: app.log }), + health, + onError: (err) => { + // Log class/status/message only — never the error object, whose + // ArenaApiError.body can echo request data (same rule as server.ts). + app.log.error(describeErrorForLog(err), 'delta poll failed'); + }, + }); + + const shutdown = registerGracefulShutdown({ app, poller, runner, timeoutMs: config.shutdownTimeoutMs }); + process.once('SIGINT', shutdown.onSignal); + process.once('SIGTERM', shutdown.onSignal); + + await app.listen({ host: config.host, port: config.port }); + poller.start(); +} + +main().catch((err: unknown) => { + console.error(err); + process.exit(1); +}); diff --git a/worker/src/poller/arenaDeltaPoll.ts b/worker/src/poller/arenaDeltaPoll.ts new file mode 100644 index 0000000..64edc89 --- /dev/null +++ b/worker/src/poller/arenaDeltaPoll.ts @@ -0,0 +1,66 @@ +/** + * The poll callback the DeltaPoller drives in production: fetch Arena changes + * since the cursor and push them into the ingest buffer. Kept out of the + * composition root so the full-queue path is unit-testable. + * + * Buffer-full policy (review ruling B1, 2026-09-14): a full ingest buffer is + * a TICK FAILURE, never a silent drop. If the buffer is already full the tick + * throws before calling Arena (no wasted fetch, no login); if it fills while + * enqueuing, the tick throws after enqueuing what fit. Either way the + * DeltaPoller keeps its cursor (it advances only on resolve), `GET /health` + * flips to `degraded`, and the next tick re-covers the same window. Duplicates + * on recovery are acceptable — SAP is the ledger (KNOWLEDGE.md 2026-09-04) — + * and the buffer never spills to worker-local storage (ARN-003). + */ + +import type { ArenaClient } from '../arena/client.js'; +import type { InMemoryEventQueue } from '../queue/webhookQueue.js'; + +export interface PollLogger { + warn(obj: object, msg: string): void; +} + +export interface ArenaDeltaPollDeps { + client: Pick; + queue: Pick; + log: PollLogger; +} + +/** Thrown by the poll when the ingest buffer cannot take the tick's changes. */ +export class IngestBufferFullError extends Error { + readonly fetched: number; + readonly dropped: number; + + constructor(fetched: number, dropped: number) { + super( + fetched === 0 + ? 'ingest buffer full before poll; tick skipped' + : `ingest buffer full during poll; ${dropped} of ${fetched} changes not enqueued`, + ); + this.name = 'IngestBufferFullError'; + this.fetched = fetched; + this.dropped = dropped; + } +} + +export function makeArenaDeltaPoll(deps: ArenaDeltaPollDeps): (since: Date) => Promise { + return async (since: Date): Promise => { + if (deps.queue.isFull) { + deps.log.warn({ since: since.toISOString() }, 'ingest queue full; skipping Arena fetch, tick failed'); + throw new IngestBufferFullError(0, 0); + } + const changes = await deps.client.fetchChangesSince(since); + let dropped = 0; + for (const payload of changes) { + const result = deps.queue.enqueue({ source: 'poll', payload }); + if (!result.accepted) dropped += 1; + } + if (dropped > 0) { + deps.log.warn( + { dropped, fetched: changes.length, since: since.toISOString() }, + 'ingest queue full; polled Arena changes not enqueued, tick failed (cursor holds)', + ); + throw new IngestBufferFullError(changes.length, dropped); + } + }; +} diff --git a/worker/src/poller/deltaPoller.ts b/worker/src/poller/deltaPoller.ts new file mode 100644 index 0000000..cd5698b --- /dev/null +++ b/worker/src/poller/deltaPoller.ts @@ -0,0 +1,91 @@ +/** + * Delta poller (ARN-005): a start/stop interval loop that OWNS the poll + * cursor. Each tick captures `since` (the cursor) and `asOf` (now) before + * calling `poll(since)`, and advances the cursor to `asOf` only after the poll + * resolves — a failed poll keeps its window, so the next tick re-covers it. + * Overlapping ticks are skipped (a slow poll is never run concurrently with + * itself). + * + * Cursor durability: the cursor starts at construction time (≈ process + * start), so a restart re-polls from then and any window before it is not + * re-derived here. ACCEPTED for now — the durable cursor is PRD V-3 (Arena's + * outbound-integration queue with its `reconciled` flag, held by Arena, see + * KNOWLEDGE.md 2026-09-10) — and it must never become a worker-local file or + * SQLite cursor (ARN-003). + */ + +import type { PollHealthSink } from '../health/registry.js'; + +export interface DeltaPollerOptions { + intervalMs: number; + /** Receives the cursor: fetch changes at or after `since`. */ + poll: (since: Date) => Promise | void; + onError?: (err: unknown) => void; + /** Injectable clock for tests. */ + now?: () => Date; + /** Reports tick outcomes for `GET /health`. */ + health?: PollHealthSink; +} + +export class DeltaPoller { + private timer: ReturnType | null = null; + private inFlight: Promise | null = null; + private cursor: Date; + private readonly now: () => Date; + + constructor(private readonly options: DeltaPollerOptions) { + this.now = options.now ?? (() => new Date()); + this.cursor = this.now(); + } + + get running(): boolean { + return this.timer !== null; + } + + /** The `since` the next tick will use (diagnostics/tests). */ + get nextSince(): Date { + return new Date(this.cursor.getTime()); + } + + start(): void { + if (this.timer !== null) return; + this.timer = setInterval(() => { + this.tick(); + }, this.options.intervalMs); + // Do not keep the process alive for the poller alone. + this.timer.unref?.(); + } + + /** + * Stops scheduling further ticks and resolves once the in-flight tick (if + * any) has settled — so shutdown can await it instead of racing it. + */ + stop(): Promise { + if (this.timer !== null) { + clearInterval(this.timer); + this.timer = null; + } + return this.inFlight ?? Promise.resolve(); + } + + private tick(): void { + if (this.inFlight !== null) return; + this.inFlight = this.runTick().finally(() => { + this.inFlight = null; + }); + } + + private async runTick(): Promise { + const since = this.cursor; + const asOf = this.now(); + try { + await this.options.poll(since); + // Advance ONLY after success: a rejected poll keeps `since`. + this.cursor = asOf; + this.options.health?.recordPollSuccess(asOf); + } catch (err) { + this.options.health?.recordPollFailure(asOf, err); + this.options.onError?.(err); + } + } +} diff --git a/worker/src/queue/webhookQueue.ts b/worker/src/queue/webhookQueue.ts new file mode 100644 index 0000000..0e0c65f --- /dev/null +++ b/worker/src/queue/webhookQueue.ts @@ -0,0 +1,81 @@ +/** + * In-memory, BOUNDED buffer for raw Arena events (webhook pushes + delta-poll + * finds). + * + * Scope note (ARN-003): this queue is a transient ingest BUFFER, not a system + * of record — durable staging lives in the in-tenant RAP BO, and idempotency + * never depends on worker-local state (SAP is the ledger, KNOWLEDGE.md + * 2026-09-04). It must never be "upgraded" to droplet-local SQLite; the + * predecessor lost its queue on every restart doing exactly that. When the + * bound is hit the answer is backpressure (webhook: 503 + Retry-After; poll: + * the tick FAILS so the cursor holds and the window is re-polled — see + * poller/arenaDeltaPoll.ts), never spilling to disk and never unbounded growth. + */ + +import { randomUUID } from 'node:crypto'; + +export type ArenaEventSource = 'webhook' | 'poll'; + +export interface QueuedArenaEvent { + id: string; + receivedAt: string; + source: ArenaEventSource; + /** Raw, unvalidated Arena payload. */ + payload: unknown; +} + +export type EnqueueResult = + | { accepted: true; event: QueuedArenaEvent } + | { accepted: false; reason: 'full' }; + +/** Default bound; config `WEBHOOK_QUEUE_MAX_SIZE` overrides it. */ +export const DEFAULT_WEBHOOK_QUEUE_MAX_SIZE = 10_000; + +export interface EventQueueOptions { + /** Positive integer; config `WEBHOOK_QUEUE_MAX_SIZE` (default 10000). */ + maxSize?: number; +} + +export class InMemoryEventQueue { + private readonly items: QueuedArenaEvent[] = []; + readonly maxSize: number; + + constructor(options: EventQueueOptions = {}) { + const maxSize = options.maxSize ?? DEFAULT_WEBHOOK_QUEUE_MAX_SIZE; + if (!Number.isSafeInteger(maxSize) || maxSize <= 0) { + throw new Error(`InMemoryEventQueue: maxSize must be a positive integer, got ${maxSize}`); + } + this.maxSize = maxSize; + } + + enqueue(input: { source: ArenaEventSource; payload: unknown }): EnqueueResult { + if (this.items.length >= this.maxSize) { + return { accepted: false, reason: 'full' }; + } + const event: QueuedArenaEvent = { + id: randomUUID(), + receivedAt: new Date().toISOString(), + source: input.source, + payload: input.payload, + }; + this.items.push(event); + return { accepted: true, event }; + } + + dequeue(): QueuedArenaEvent | undefined { + return this.items.shift(); + } + + get size(): number { + return this.items.length; + } + + get isFull(): boolean { + return this.items.length >= this.maxSize; + } + + /** Read-only snapshot (tests, diagnostics). */ + peekAll(): readonly QueuedArenaEvent[] { + return [...this.items]; + } +} diff --git a/worker/src/server.ts b/worker/src/server.ts new file mode 100644 index 0000000..eeaa595 --- /dev/null +++ b/worker/src/server.ts @@ -0,0 +1,211 @@ +/** + * HTTP surface of the connectivity worker — machine-to-machine only, no UI, + * no user-facing URLs (ADR-002). Endpoint shapes follow the governed contract + * docs/api/worker.openapi.json (API-first, CLAUDE.md rule 7); conformance is + * reviewed at that seam. + */ + +import { STATUS_CODES } from 'node:http'; +import Fastify, { type FastifyInstance } from 'fastify'; +import { makeBearerAuthHook } from './auth/bearerAuth.js'; +import type { WorkerConfig } from './config.js'; +import { ArenaApiError, type ArenaSessionManager } from './arena/session.js'; +import type { HealthRegistry, PollerHealth } from './health/registry.js'; +import { sendProblem } from './http/problem.js'; +import type { InMemoryEventQueue } from './queue/webhookQueue.js'; +import type { RunStore } from './sync/runStore.js'; +import type { SyncRunner } from './sync/runner.js'; +import { + SYNC_OBJECT_TYPES, + SYNC_SCOPES, + type SyncObjectType, + type SyncRun, + type SyncScope, +} from './sync/types.js'; +import { WORKER_VERSION } from './version.js'; + +/** Contract: `Retry-After` delay-seconds sent with a 503 from the webhook intake. */ +export const WEBHOOK_RETRY_AFTER_SECONDS = 30; + +export interface ServerDeps { + config: WorkerConfig; + queue: InMemoryEventQueue; + runner: SyncRunner; + runStore: RunStore; + arenaSession: ArenaSessionManager; + health: HealthRegistry; + /** Enable request logging (off by default so tests stay quiet). */ + logger?: boolean; + /** + * Max request body in bytes. Unset => Fastify's default 1 MiB (1048576); + * over the limit the framework answers 413 before the handler runs + * (contract: `PayloadTooLarge`). Exposed so the conformance suite can + * exercise 413 with a small limit. + * [source: context7 /fastify/fastify docs/Reference/Server.md `bodyLimit`, verified 2026-09-14] + */ + bodyLimit?: number; +} + +/** Wire shape of GET /health (contract `HealthStatus`). */ +interface HealthStatus { + status: 'ok' | 'degraded'; + version: string; + uptimeSeconds: number; + arenaSession: { state: ReturnType }; + poller?: PollerHealth; +} + +export function buildServer(deps: ServerDeps): FastifyInstance { + const { config, queue, runner, runStore, arenaSession, health } = deps; + const app = Fastify({ + logger: deps.logger ?? false, + // Seam ruling (2026-09-10): STRICT VALIDATION — the contract's + // `additionalProperties: false` on SyncRunRequest must mean 400, and a + // body is validated exactly as sent. Fastify's BASELINE Ajv config is + // { coerceTypes: 'array', useDefaults: true, removeAdditional: true } + // (Ajv's own defaults are all `false`), which would (a) silently STRIP an + // unknown property such as a typo'd `object`, (b) unwrap `["full"]` to + // `"full"` and stringify scalars, and (c) inject schema defaults. On a + // machine-to-machine admin API each of those hides a caller bug, so all + // three are turned off here. `allErrors` stays at Fastify's `false` + // (documented DoS surface when true). + // [source: context7 /fastify/fastify docs/Reference/Validation-and-Serialization.md "Fastify Baseline Ajv Configuration"; docs/Reference/Server.md "ajv customOptions", verified 2026-09-14] + // [source: context7 /ajv-validator/ajv docs/options.md "removeAdditional / useDefaults / coerceTypes" defaults, verified 2026-09-14] + ajv: { customOptions: { removeAdditional: false, coerceTypes: false, useDefaults: false } }, + ...(deps.bodyLimit !== undefined ? { bodyLimit: deps.bodyLimit } : {}), + }); + + // JSON-only surface. Fastify natively parses `application/json` AND + // `text/plain`; removing the latter makes any non-JSON media type (and a + // body without a Content-Type) a 415 before the handler runs, instead of a + // text body reaching schema validation as a string. Contract: + // `UnsupportedMediaType` (415) on both POST operations. + // [source: context7 /fastify/fastify docs/Reference/ContentTypeParser.md "removeContentTypeParser"; docs/Reference/Errors.md FST_ERR_CTP_INVALID_MEDIA_TYPE, verified 2026-09-14] + app.removeContentTypeParser('text/plain'); + + // Auth runs in `onRequest` (before body parsing/validation) so an + // unauthenticated request is always 401, never 400. + const requireBearer = makeBearerAuthHook(config.bearerToken); + + // Contract: ALL error bodies are RFC 9457 application/problem+json. + // Covers schema-validation 400s (error.validation), body-parse 400s, + // framework-raised 413 (over bodyLimit) / 415 (unsupported media type), + // upstream Arena failures (502, body never forwarded), and anything else + // Fastify surfaces (500, the contract's `default`). 5xx details are not + // leaked; 4xx `detail` is the framework's or Ajv's own fixed message. + // [source: context7 /fastify/fastify docs/Reference/Errors.md FST_ERR_CTP_BODY_TOO_LARGE (413), FST_ERR_CTP_INVALID_MEDIA_TYPE (415), verified 2026-09-14] + app.setErrorHandler((err: unknown, req, reply) => { + if (err instanceof ArenaApiError) { + // Upstream failure, not ours. Log the status only — Arena error bodies + // can echo request data — and answer with the fixed contract phrase. + req.log.error({ upstreamStatus: err.status, message: err.message }, 'upstream Arena request failed'); + return sendProblem(reply, 502, 'Bad Gateway', 'Upstream Arena PLM request failed.'); + } + const statusCode = + typeof err === 'object' && + err !== null && + 'statusCode' in err && + typeof (err as { statusCode: unknown }).statusCode === 'number' + ? (err as { statusCode: number }).statusCode + : undefined; + const status = statusCode !== undefined && statusCode >= 400 ? statusCode : 500; + const title = STATUS_CODES[status] ?? 'Error'; + if (status >= 500) { + req.log.error({ err }, 'unhandled error'); + return sendProblem(reply, status, title); + } + const detail = err instanceof Error ? err.message : String(err); + return sendProblem(reply, status, title, detail); + }); + + // Unmatched path OR unmatched method on a known path: Fastify's router + // routes both to the not-found handler (404, never 405 — probed 2026-09-14). + // Contract: `x-unmatched-routes` / every operation's `default` response. + app.setNotFoundHandler((req, reply) => + sendProblem(reply, 404, 'Not Found', `Route ${req.method} ${req.url} not found`), + ); + + app.get('/health', async (): Promise => { + const body: HealthStatus = { + status: health.status(), + version: WORKER_VERSION, + uptimeSeconds: Math.round(process.uptime()), + arenaSession: { state: arenaSession.state() }, + }; + const poller = health.pollerSnapshot(); + if (poller !== undefined) body.poller = poller; + return body; + }); + + app.post( + '/webhooks/arena', + { + onRequest: requireBearer, + schema: { + // Any JSON object is accepted as-is; payloads stay raw until the + // ingest pipeline interprets them. Non-objects and invalid JSON => 400. + body: { type: 'object' }, + }, + }, + async (req, reply) => { + const result = queue.enqueue({ source: 'webhook', payload: req.body }); + if (!result.accepted) { + // Bounded buffer full: backpressure to the sender, never a spill to + // worker-local storage (ARN-003). + return sendProblem(reply, 503, 'Service Unavailable', 'Ingest queue is full; retry later.', { + 'retry-after': String(WEBHOOK_RETRY_AFTER_SECONDS), + }); + } + return reply.code(202).send({ received: true }); + }, + ); + + app.post<{ Body: { scope: SyncScope; objects?: SyncObjectType[] } }>( + '/admin/sync-runs', + { + onRequest: requireBearer, + schema: { + body: { + type: 'object', + additionalProperties: false, + required: ['scope'], + properties: { + scope: { type: 'string', enum: [...SYNC_SCOPES] }, + // Optional: selects AND orders the object types for this run. + objects: { + type: 'array', + minItems: 1, + uniqueItems: true, + items: { type: 'string', enum: [...SYNC_OBJECT_TYPES] }, + }, + }, + }, + }, + }, + async (req, reply) => { + // Request `objects` wins; otherwise the config-driven default (ARN-001). + const objectOrder: readonly SyncObjectType[] = req.body.objects ?? config.syncObjectOrder; + const run = runner.submit(req.body.scope, objectOrder); + return reply.code(201).send(snapshotRun(run)); + }, + ); + + app.get<{ Params: { runId: string } }>( + '/admin/sync-runs/:runId', + { onRequest: requireBearer }, + async (req, reply) => { + const run = runStore.get(req.params.runId); + if (run === undefined) { + return sendProblem(reply, 404, 'Not Found', `No sync run with id ${req.params.runId}`); + } + return reply.code(200).send(snapshotRun(run)); + }, + ); + + return app; +} + +/** Deep copy so async run-state mutation cannot race response serialization. */ +function snapshotRun(run: SyncRun): SyncRun { + return structuredClone(run); +} diff --git a/worker/src/shutdown.ts b/worker/src/shutdown.ts new file mode 100644 index 0000000..5814bd9 --- /dev/null +++ b/worker/src/shutdown.ts @@ -0,0 +1,81 @@ +/** + * Graceful shutdown. `fastify.close()` "runs the 'onClose' hook, and stops + * accepting new connections" and "by this time, the HTTP server has stopped + * listening and connections have been drained" — so the onClose hook is the + * right place to stop the poller and wait for in-flight sync runs; the hook + * may return a Promise and close() awaits it. + * [source: context7 /fastify/fastify docs/Reference/Hooks.md "onClose" and docs/Reference/Server.md "close", verified 2026-09-14] + * + * Exit code: 0 when every in-flight run/tick settled inside the timeout, + * 1 when the timeout won (something was still running — an operator should + * check SAP for a half-applied chain; SAP is the ledger, KNOWLEDGE.md + * 2026-09-04) or when close() itself failed. + */ + +import type { FastifyInstance } from 'fastify'; + +export type DrainOutcome = 'drained' | 'timeout'; + +/** Race `work` against a deadline; the deadline timer never outlives the race. */ +export async function drainWithTimeout(work: Promise, timeoutMs: number): Promise { + let timer: ReturnType | undefined; + const deadline = new Promise<'timeout'>((resolve) => { + timer = setTimeout(() => resolve('timeout'), timeoutMs); + }); + try { + return await Promise.race([work.then((): DrainOutcome => 'drained'), deadline]); + } finally { + clearTimeout(timer); + } +} + +export interface GracefulShutdownDeps { + app: FastifyInstance; + /** Stops scheduling and resolves when the in-flight tick settles. */ + poller: { stop(): Promise }; + /** Resolves when every in-flight run has settled. */ + runner: { drain(): Promise }; + timeoutMs: number; + /** Injectable for tests; defaults to process.exit. */ + exit?: (code: number) => void; +} + +export interface GracefulShutdown { + /** Signal handler: one function registered for both SIGINT and SIGTERM. */ + onSignal: (signal: string) => void; + /** Whether the last close() hit the drain timeout (diagnostics/tests). */ + readonly timedOut: boolean; +} + +export function registerGracefulShutdown(deps: GracefulShutdownDeps): GracefulShutdown { + const { app, timeoutMs } = deps; + const exit = deps.exit ?? ((code: number) => process.exit(code)); + let timedOut = false; + + app.addHook('onClose', async () => { + const pollerStopped = deps.poller.stop(); + const outcome = await drainWithTimeout(Promise.all([deps.runner.drain(), pollerStopped]), timeoutMs); + if (outcome === 'timeout') { + timedOut = true; + app.log.warn({ timeoutMs }, 'shutdown timeout: in-flight sync runs or poll tick did not settle'); + } + }); + + const onSignal = (signal: string): void => { + app.log.info({ signal }, 'shutdown requested'); + app.close().then( + () => exit(timedOut ? 1 : 0), + (err: unknown) => { + app.log.error({ err }, 'shutdown failed'); + exit(1); + }, + ); + }; + + return { + onSignal, + get timedOut() { + return timedOut; + }, + }; +} diff --git a/worker/src/sync/executors.ts b/worker/src/sync/executors.ts new file mode 100644 index 0000000..a612bf3 --- /dev/null +++ b/worker/src/sync/executors.ts @@ -0,0 +1,20 @@ +/** + * Build wave 1: no-op executors only. See `SyncExecutor` in ./types.ts for the + * binding constraints the real S/4HANA implementations must honor (SAP Cloud + * SDK + `mc-s4hana` destination; KNOWLEDGE.md tombstones). + */ + +import { SYNC_OBJECT_TYPES } from './types.js'; +import type { SyncExecutionContext, SyncExecutionResult, SyncExecutor, SyncObjectType } from './types.js'; + +export class NoopSyncExecutor implements SyncExecutor { + constructor(readonly objectType: SyncObjectType) {} + + async execute(_ctx: SyncExecutionContext): Promise { + return { processed: 0 }; + } +} + +export function makeNoopExecutors(): Map { + return new Map(SYNC_OBJECT_TYPES.map((objectType) => [objectType, new NoopSyncExecutor(objectType)])); +} diff --git a/worker/src/sync/runStore.ts b/worker/src/sync/runStore.ts new file mode 100644 index 0000000..bed7e6e --- /dev/null +++ b/worker/src/sync/runStore.ts @@ -0,0 +1,28 @@ +/** + * In-memory sync-run registry. + * + * Scope note (ARN-003): run objects are runtime observability state for the + * worker process, not the ledger. Idempotency of SAP writes never depends on + * this store — SAP is the ledger via the Arena ECO number in + * `ReasonForChangeText` [KNOWLEDGE.md 2026-09-04]. Anything that must survive + * a restart belongs in the in-tenant RAP BO, never droplet-local storage. + */ + +import type { SyncRun } from './types.js'; + +export interface RunStore { + put(run: SyncRun): void; + get(runId: string): SyncRun | undefined; +} + +export class InMemoryRunStore implements RunStore { + private readonly runs = new Map(); + + put(run: SyncRun): void { + this.runs.set(run.runId, run); + } + + get(runId: string): SyncRun | undefined { + return this.runs.get(runId); + } +} diff --git a/worker/src/sync/runner.ts b/worker/src/sync/runner.ts new file mode 100644 index 0000000..d1e0327 --- /dev/null +++ b/worker/src/sync/runner.ts @@ -0,0 +1,105 @@ +/** + * Sync run state machine: queued → running → completed | failed. + * + * Objects execute strictly sequentially in the configured order (ARN-001) — + * later object types depend on earlier ones, so the chain stops at the first + * failure and the remaining objects stay `pending`. + * + * Wire hygiene: `processed` appears only once an object has started (absent + * while `pending`, contract B4), and every `error` string is a fixed phrase + * from the public allowlist (src/errors/publicPhrase.ts) — an executor's raw + * error (which for the S/4HANA leg may echo OData payloads) never reaches the + * run snapshot; it is the caller's job to log it. + */ + +import { randomUUID } from 'node:crypto'; +import { publicErrorPhrase } from '../errors/publicPhrase.js'; +import type { RunStore } from './runStore.js'; +import type { SyncExecutor, SyncObjectType, SyncRun, SyncScope } from './types.js'; + +export class SyncRunner { + private readonly inFlight = new Map>(); + + constructor( + private readonly store: RunStore, + private readonly executors: ReadonlyMap, + private readonly now: () => Date = () => new Date(), + ) {} + + /** + * Create a run in state `queued` and schedule asynchronous processing. + * Returns synchronously so callers can snapshot the queued state. + */ + submit(scope: SyncScope, objectOrder: readonly SyncObjectType[]): SyncRun { + const run: SyncRun = { + runId: randomUUID(), + scope, + state: 'queued', + // Execution order is carried by the objects array itself (contract shape). + // `processed` is deliberately absent while pending (contract B4). + objects: objectOrder.map((objectType) => ({ objectType, state: 'pending' })), + startedAt: this.now().toISOString(), + }; + this.store.put(run); + const done = this.process(run); + this.inFlight.set(run.runId, done); + void done.finally(() => this.inFlight.delete(run.runId)); + return run; + } + + /** Await completion of a run's processing (test/diagnostic hook). */ + async waitFor(runId: string): Promise { + await (this.inFlight.get(runId) ?? Promise.resolve()); + return this.store.get(runId); + } + + /** Number of runs still processing (diagnostics/tests). */ + get inFlightCount(): number { + return this.inFlight.size; + } + + /** + * Resolve once every run that was in flight at call time has settled — + * success or failure alike (allSettled). Used by graceful shutdown so + * `app.close()` does not exit under a half-executed BOM chain. + */ + async drain(): Promise { + await Promise.allSettled([...this.inFlight.values()]); + } + + private async process(run: SyncRun): Promise { + // Yield one microtask so submit() returns while the run is still `queued`. + await Promise.resolve(); + run.state = 'running'; + for (const objectResult of run.objects) { + const executor = this.executors.get(objectResult.objectType); + if (executor === undefined) { + objectResult.state = 'failed'; + objectResult.error = 'no executor registered'; + this.fail(run, `${objectResult.objectType}: no executor registered`); + return; + } + objectResult.state = 'running'; + objectResult.processed = 0; + try { + const result = await executor.execute({ runId: run.runId, scope: run.scope }); + objectResult.processed = result.processed; + objectResult.state = 'completed'; + } catch (err) { + const phrase = publicErrorPhrase(err); + objectResult.state = 'failed'; + objectResult.error = phrase; + this.fail(run, `${objectResult.objectType}: ${phrase}`); + return; + } + } + run.state = 'completed'; + run.finishedAt = this.now().toISOString(); + } + + private fail(run: SyncRun, error: string): void { + run.state = 'failed'; + run.error = error; + run.finishedAt = this.now().toISOString(); + } +} diff --git a/worker/src/sync/types.ts b/worker/src/sync/types.ts new file mode 100644 index 0000000..30739ac --- /dev/null +++ b/worker/src/sync/types.ts @@ -0,0 +1,73 @@ +/** + * Sync domain types — the typed seam between the worker's run state machine + * and the (future) S/4HANA execution leg. + */ + +/** Chain tokens per PRD ARN-001 — the last one is `DOCUMENTS` (plural), as in the requirement. */ +export const SYNC_OBJECT_TYPES = ['PRODUCT', 'CHANGE_MASTER', 'BOM', 'DOCUMENTS'] as const; +export type SyncObjectType = (typeof SYNC_OBJECT_TYPES)[number]; + +export const SYNC_SCOPES = ['full', 'delta'] as const; +export type SyncScope = (typeof SYNC_SCOPES)[number]; + +export type SyncRunState = 'queued' | 'running' | 'completed' | 'failed'; +export type SyncObjectState = 'pending' | 'running' | 'completed' | 'failed'; + +export interface SyncObjectResult { + objectType: SyncObjectType; + state: SyncObjectState; + /** Absent while `pending`; set once the object has started (contract B4). */ + processed?: number; + /** Fixed public phrase (src/errors/publicPhrase.ts), never a raw error message. */ + error?: string; +} + +/** + * Wire shape of a sync run (governed contract: docs/api/worker.openapi.json). + * Execution order is carried by the `objects` array itself (execution-ordered, + * all `pending` at creation) — there is no separate order field on the wire. + * The order is config-driven per ARN-001 (default PRODUCT → CHANGE_MASTER → + * BOM → DOCUMENTS) unless the request selects/orders objects explicitly. + */ +export interface SyncRun { + runId: string; + scope: SyncScope; + state: SyncRunState; + objects: SyncObjectResult[]; + startedAt: string; + finishedAt?: string; + error?: string; +} + +export interface SyncExecutionContext { + runId: string; + scope: SyncScope; +} + +export interface SyncExecutionResult { + processed: number; +} + +/** + * Per-object sync executor — STUB SEAM ONLY in build wave 1. + * + * The real implementations are the S/4HANA leg and MUST: + * - go through the SAP Cloud SDK with the named destination `mc-s4hana` — + * never hand-rolled CSRF or auth (ADR-001; CLAUDE.md operating rule 3); + * - honor every KNOWLEDGE.md tombstone (all dated 2026-09-04): + * * BOM items POSTed sequentially after the header POST — parallel item + * POSTs fail SAP's consistency check; + * * BOM reads on the v1 service only; writes on + * `API_BILL_OF_MATERIAL_SRV;v=0002` (v2 rejects guid key reads); + * * `AvailabilityCheckType` via the V2 `API_PRODUCT_SRV` fallback (not + * exposed in Product API V4); + * * OData V2 dates as `/Date(ms)/`; end-of-time `/Date(253402214400000)/`; + * * idempotency: SAP is the ledger — Arena ECO number stamped into + * `ReasonForChangeText`; no local sync-state database. + * + * No OData/S4 client code exists in this wave by design. + */ +export interface SyncExecutor { + readonly objectType: SyncObjectType; + execute(ctx: SyncExecutionContext): Promise; +} diff --git a/worker/src/version.ts b/worker/src/version.ts new file mode 100644 index 0000000..03631a8 --- /dev/null +++ b/worker/src/version.ts @@ -0,0 +1,22 @@ +import { readFileSync } from 'node:fs'; + +function readVersion(): string { + try { + // Resolves from both src/ (tsx dev) and dist/ (compiled) to ../package.json. + const packageJsonUrl = new URL('../package.json', import.meta.url); + const parsed: unknown = JSON.parse(readFileSync(packageJsonUrl, 'utf8')); + if ( + parsed !== null && + typeof parsed === 'object' && + 'version' in parsed && + typeof (parsed as { version: unknown }).version === 'string' + ) { + return (parsed as { version: string }).version; + } + } catch { + // fall through to sentinel + } + return '0.0.0'; +} + +export const WORKER_VERSION = readVersion(); diff --git a/worker/test/arenaDeltaPoll.test.ts b/worker/test/arenaDeltaPoll.test.ts new file mode 100644 index 0000000..8818105 --- /dev/null +++ b/worker/test/arenaDeltaPoll.test.ts @@ -0,0 +1,134 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; +import { HealthRegistry } from '../src/health/registry.js'; +import { IngestBufferFullError, makeArenaDeltaPoll } from '../src/poller/arenaDeltaPoll.js'; +import { DeltaPoller } from '../src/poller/deltaPoller.js'; +import { InMemoryEventQueue } from '../src/queue/webhookQueue.js'; + +describe('makeArenaDeltaPoll', () => { + it('fetches changes since the cursor and enqueues each as a poll-sourced event', async () => { + const queue = new InMemoryEventQueue({ maxSize: 10 }); + const fetchChangesSince = vi.fn().mockResolvedValue([{ id: 'c1' }, { id: 'c2' }]); + const warn = vi.fn(); + const poll = makeArenaDeltaPoll({ client: { fetchChangesSince }, queue, log: { warn } }); + + const since = new Date('2026-09-14T08:00:00Z'); + await poll(since); + + expect(fetchChangesSince).toHaveBeenCalledWith(since); + expect(queue.peekAll().map((e) => [e.source, e.payload])).toEqual([ + ['poll', { id: 'c1' }], + ['poll', { id: 'c2' }], + ]); + expect(warn).not.toHaveBeenCalled(); + }); + + it('fails the tick WITHOUT calling Arena when the buffer is already full (B1)', async () => { + const queue = new InMemoryEventQueue({ maxSize: 1 }); + queue.enqueue({ source: 'webhook', payload: { n: 1 } }); + const fetchChangesSince = vi.fn().mockResolvedValue([{ id: 'c1' }]); + const warn = vi.fn(); + const poll = makeArenaDeltaPoll({ client: { fetchChangesSince }, queue, log: { warn } }); + + await expect(poll(new Date(0))).rejects.toBeInstanceOf(IngestBufferFullError); + + expect(fetchChangesSince).not.toHaveBeenCalled(); + expect(queue.size).toBe(1); + expect(warn).toHaveBeenCalledTimes(1); + expect(warn.mock.calls[0]?.[1]).toMatch(/queue full/); + }); + + it('fails the tick after a partial enqueue when the buffer fills mid-poll (B1)', async () => { + const queue = new InMemoryEventQueue({ maxSize: 1 }); + const fetchChangesSince = vi.fn().mockResolvedValue([{ id: 'c1' }, { id: 'c2' }, { id: 'c3' }]); + const warn = vi.fn(); + const poll = makeArenaDeltaPoll({ client: { fetchChangesSince }, queue, log: { warn } }); + + const failure = await poll(new Date(0)).catch((err: unknown) => err); + expect(failure).toBeInstanceOf(IngestBufferFullError); + expect(failure).toMatchObject({ fetched: 3, dropped: 2 }); + + // What fit stays queued; the rest is re-covered by the next tick (same window). + expect(queue.size).toBe(1); + expect(queue.peekAll()[0]?.payload).toEqual({ id: 'c1' }); + expect(warn).toHaveBeenCalledTimes(1); + expect(warn.mock.calls[0]?.[0]).toMatchObject({ dropped: 2, fetched: 3 }); + }); + + it('propagates a fetch failure so the poller keeps its cursor', async () => { + const queue = new InMemoryEventQueue({ maxSize: 1 }); + const fetchChangesSince = vi.fn().mockRejectedValue(new Error('arena unreachable')); + const poll = makeArenaDeltaPoll({ client: { fetchChangesSince }, queue, log: { warn: vi.fn() } }); + + await expect(poll(new Date(0))).rejects.toThrow('arena unreachable'); + expect(queue.size).toBe(0); + }); +}); + +describe('makeArenaDeltaPoll driven by DeltaPoller (B1 end to end)', () => { + beforeEach(() => { + vi.useFakeTimers(); + }); + + afterEach(() => { + vi.useRealTimers(); + }); + + it('a partial drop holds the cursor, flips health to degraded, and the next tick re-covers the window', async () => { + const queue = new InMemoryEventQueue({ maxSize: 1 }); + const fetchChangesSince = vi.fn().mockResolvedValue([{ id: 'c1' }, { id: 'c2' }]); + const health = new HealthRegistry(); + const start = new Date('2026-09-14T08:00:00Z'); + let nowMs = start.getTime(); + const poller = new DeltaPoller({ + intervalMs: 1000, + poll: makeArenaDeltaPoll({ client: { fetchChangesSince }, queue, log: { warn: vi.fn() } }), + health, + now: () => new Date(nowMs), + }); + const initialSince = poller.nextSince; + + poller.start(); + nowMs += 1000; + await vi.advanceTimersByTimeAsync(1000); + + expect(fetchChangesSince).toHaveBeenCalledTimes(1); + expect(fetchChangesSince).toHaveBeenLastCalledWith(initialSince); + expect(poller.nextSince).toEqual(initialSince); // cursor did NOT advance + expect(health.status()).toBe('degraded'); + expect(health.pollerSnapshot()?.lastError).toBe('Ingest buffer full at poll time.'); + + // Buffer drains; the next tick re-covers the SAME window and succeeds. + queue.dequeue(); + fetchChangesSince.mockResolvedValue([{ id: 'c2' }]); + nowMs += 1000; + await vi.advanceTimersByTimeAsync(1000); + + expect(fetchChangesSince).toHaveBeenCalledTimes(2); + expect(fetchChangesSince).toHaveBeenLastCalledWith(initialSince); + expect(poller.nextSince.getTime()).toBeGreaterThan(initialSince.getTime()); + expect(health.status()).toBe('ok'); + + await poller.stop(); + }); + + it('a full buffer before the fetch never calls Arena and leaves the cursor untouched', async () => { + const queue = new InMemoryEventQueue({ maxSize: 1 }); + queue.enqueue({ source: 'webhook', payload: {} }); + const fetchChangesSince = vi.fn().mockResolvedValue([]); + const health = new HealthRegistry(); + const poller = new DeltaPoller({ + intervalMs: 1000, + poll: makeArenaDeltaPoll({ client: { fetchChangesSince }, queue, log: { warn: vi.fn() } }), + health, + }); + const initialSince = poller.nextSince; + + poller.start(); + await vi.advanceTimersByTimeAsync(2500); + + expect(fetchChangesSince).not.toHaveBeenCalled(); + expect(poller.nextSince).toEqual(initialSince); + expect(health.status()).toBe('degraded'); + await poller.stop(); + }); +}); diff --git a/worker/test/config.test.ts b/worker/test/config.test.ts new file mode 100644 index 0000000..52caa19 --- /dev/null +++ b/worker/test/config.test.ts @@ -0,0 +1,68 @@ +import { describe, expect, it } from 'vitest'; +import { loadConfig } from '../src/config.js'; + +describe('loadConfig', () => { + it('applies defaults from an empty environment', () => { + const config = loadConfig({}); + + expect(config.host).toBe('0.0.0.0'); + expect(config.port).toBe(8080); + expect(config.bearerToken).toBeUndefined(); + expect(config.arena.baseUrl).toBe('https://api.arenasolutions.com/v1'); + expect(config.arena.sessionTtlMs).toBe(24 * 60_000); // Arena TTL ~25 min; refresh 1 min early + expect(config.arena.httpTimeoutMs).toBe(30_000); + expect(config.pollIntervalMs).toBe(300_000); + expect(config.syncObjectOrder).toEqual(['PRODUCT', 'CHANGE_MASTER', 'BOM', 'DOCUMENTS']); + expect(config.webhookQueueMaxSize).toBe(10_000); + expect(config.shutdownTimeoutMs).toBe(30_000); + }); + + it('parses a custom sync order case-insensitively (ARN-001 config knob)', () => { + const config = loadConfig({ SYNC_OBJECT_ORDER: 'bom, product' }); + expect(config.syncObjectOrder).toEqual(['BOM', 'PRODUCT']); + }); + + it('rejects unknown sync order entries', () => { + expect(() => loadConfig({ SYNC_OBJECT_ORDER: 'PRODUCT,WIDGET' })).toThrow(/WIDGET/); + }); + + it('rejects duplicate sync order entries (same rule as the HTTP contract uniqueItems)', () => { + expect(() => loadConfig({ SYNC_OBJECT_ORDER: 'BOM,BOM' })).toThrow(/duplicate entry "BOM"/); + expect(() => loadConfig({ SYNC_OBJECT_ORDER: 'product, bom, Product' })).toThrow(/duplicate entry "PRODUCT"/); + }); + + it('strips trailing slashes from the Arena base URL and parses numbers', () => { + const config = loadConfig({ + ARENA_API_URL: 'https://api.arenasolutions.com/v1//', + ARENA_WORKSPACE_ID: '4242', + ARENA_SESSION_TTL_MINUTES: '10', + ARENA_HTTP_TIMEOUT_MS: '5000', + WORKER_PORT: '9090', + WEBHOOK_QUEUE_MAX_SIZE: '25', + SHUTDOWN_TIMEOUT_MS: '1500', + }); + + expect(config.arena.baseUrl).toBe('https://api.arenasolutions.com/v1'); + expect(config.arena.workspaceId).toBe(4242); + expect(config.arena.sessionTtlMs).toBe(600_000); + expect(config.arena.httpTimeoutMs).toBe(5000); + expect(config.port).toBe(9090); + expect(config.webhookQueueMaxSize).toBe(25); + expect(config.shutdownTimeoutMs).toBe(1500); + }); + + it('rejects non-numeric numeric env values', () => { + expect(() => loadConfig({ WORKER_PORT: 'abc' })).toThrow(/WORKER_PORT/); + }); + + it('rejects numeric env values with trailing garbage (whole string must be digits)', () => { + expect(() => loadConfig({ WORKER_PORT: '8080abc' })).toThrow(/WORKER_PORT.*"8080abc"/); + expect(() => loadConfig({ ARENA_SESSION_TTL_MINUTES: '24m' })).toThrow(/ARENA_SESSION_TTL_MINUTES/); + expect(() => loadConfig({ WEBHOOK_QUEUE_MAX_SIZE: '1e4' })).toThrow(/WEBHOOK_QUEUE_MAX_SIZE/); + }); + + it('rejects zero and negative numeric env values', () => { + expect(() => loadConfig({ WEBHOOK_QUEUE_MAX_SIZE: '0' })).toThrow(/WEBHOOK_QUEUE_MAX_SIZE/); + expect(() => loadConfig({ SHUTDOWN_TIMEOUT_MS: '-1' })).toThrow(/SHUTDOWN_TIMEOUT_MS/); + }); +}); diff --git a/worker/test/contract.test.ts b/worker/test/contract.test.ts new file mode 100644 index 0000000..b673253 --- /dev/null +++ b/worker/test/contract.test.ts @@ -0,0 +1,892 @@ +/** + * Schema-driven conformance suite: validates the worker's actual responses + * against the contract defined in docs/api/worker.openapi.json (OpenAPI 3.1, + * JSON Schema 2020-12 dialect). Contract drift becomes a failing test instead + * of a review finding. + * + * Coverage: + * 1. Compile all schemas with Ajv 2020 + ajv-formats; assert compilation succeeds. + * 2. Test endpoints with required status codes and response bodies against schemas. + * 3. Reverse check: the set of response keys documented for every operation + * (including `default`, 413, 415) equals the set this suite exercises — + * read from the contract, not a hand-kept list. + * 4. Validate problem+json content type and that 5xx bodies leak no upstream details. + * + * [source: brief contract.test.ts v1, verified 2026-09-14] + * [source: context7 /fastify/fastify docs/Reference/Errors.md FST_ERR_CTP_BODY_TOO_LARGE (413) / FST_ERR_CTP_INVALID_MEDIA_TYPE (415); docs/Reference/Server.md `bodyLimit`, verified 2026-09-14] + */ + +import { readFileSync } from 'node:fs'; +import { resolve } from 'node:path'; +import { afterEach, beforeAll, describe, expect, it, vi } from 'vitest'; +import Ajv from 'ajv/dist/2020'; +import addFormats from 'ajv-formats'; +import type { FastifyInstance } from 'fastify'; +import { fileURLToPath } from 'node:url'; +import { ArenaApiError, ArenaSessionManager } from '../src/arena/session.js'; +import { loadConfig } from '../src/config.js'; +import { HealthRegistry } from '../src/health/registry.js'; +import type { HttpClient, HttpResponse } from '../src/http/types.js'; +import { InMemoryEventQueue } from '../src/queue/webhookQueue.js'; +import { buildServer } from '../src/server.js'; +import { makeNoopExecutors } from '../src/sync/executors.js'; +import { InMemoryRunStore } from '../src/sync/runStore.js'; +import { SyncRunner } from '../src/sync/runner.js'; +import type { SyncRun } from '../src/sync/types.js'; + +const TOKEN = 'test-token'; +const AUTH = { authorization: `Bearer ${TOKEN}` }; + +// Load OpenAPI contract +const __dirname = resolve(fileURLToPath(import.meta.url), '..'); +const contractPath = resolve(__dirname, '../../docs/api/worker.openapi.json'); +const contract = JSON.parse(readFileSync(contractPath, 'utf-8')); + +// Compile all schemas with Ajv 2020 +const ajv = new Ajv({ strict: true }); +addFormats(ajv); + +interface ValidatorMap { + [schemaName: string]: (data: unknown) => boolean; +} + +const schemas: ValidatorMap = {}; +let apps: FastifyInstance[] = []; + +function makeApp(envOverrides: Record = {}, serverOverrides: { bodyLimit?: number } = {}) { + const config = loadConfig({ WORKER_BEARER_TOKEN: TOKEN, ...envOverrides }); + const health = new HealthRegistry(); + const queue = new InMemoryEventQueue({ maxSize: config.webhookQueueMaxSize }); + const runStore = new InMemoryRunStore(); + const runner = new SyncRunner(runStore, makeNoopExecutors()); + const request = vi.fn().mockRejectedValue(new Error('no network calls in tests')); + const clock = { nowMs: 1_000_000 }; + const arenaSession = new ArenaSessionManager({ + http: { request }, + baseUrl: 'https://arena.invalid/v1', + email: 'worker@example.test', + password: 'secret', + sessionTtlMs: config.arena.sessionTtlMs, + now: () => clock.nowMs, + health, + }); + const app = buildServer({ config, queue, runner, runStore, arenaSession, health, ...serverOverrides }); + apps.push(app); + return { app, queue, runStore, runner, arenaSession, health, request, clock, config }; +} + +function loginOk(sessionId: string): HttpResponse { + return { status: 200, body: { arenaSessionId: sessionId } }; +} + +async function getHealth(app: FastifyInstance) { + const res = await app.inject({ method: 'GET', url: '/health' }); + expect(res.statusCode).toBe(200); + return res.json(); +} + +function validateAgainstSchema( + data: unknown, + schemaName: string, + context: string, +): { valid: boolean; error?: string } { + const validate = schemas[schemaName]; + if (!validate) { + return { valid: false, error: `Schema '${schemaName}' not compiled` }; + } + const valid = validate(data); + if (!valid) { + const errors = (validate as { errors?: unknown }).errors; + return { + valid: false, + error: `Validation failed for ${schemaName} at ${context}: ${JSON.stringify(errors)}`, + }; + } + return { valid: true }; +} + +/** A JSON object comfortably over a 64-byte body limit. */ +const OVERSIZED_JSON = JSON.stringify({ scope: 'full', pad: 'x'.repeat(512) }); +const SMALL_BODY_LIMIT = 64; + +async function expectProblemStatus( + res: { statusCode: number; headers: Record; json: () => unknown; body: string }, + status: number, + title: string, + context: string, +) { + expect(res.statusCode).toBe(status); + expect(res.headers['content-type']).toMatch(/application\/problem\+json/); + const body = res.json() as { title: string; status: number }; + expect(body.title).toBe(title); + expect(body.status).toBe(status); + const validation = validateAgainstSchema(body, 'Problem', context); + expect(validation.valid, validation.error).toBe(true); +} + +afterEach(async () => { + await Promise.all(apps.map((app) => app.close())); + apps = []; +}); + +describe('OpenAPI contract conformance', () => { + beforeAll(() => { + // Compile all schemas from components.schemas. + // Register the entire contract with Ajv so cross-schema references (#/components/schemas/*) + // can be resolved. + if (!contract.components?.schemas) { + throw new Error('No schemas found in contract'); + } + + // Add the contract's schemas to Ajv's schema store for reference resolution + for (const [schemaName, schemaObj] of Object.entries(contract.components.schemas)) { + ajv.addSchema(schemaObj, `#/components/schemas/${schemaName}`); + } + + // Now compile each schema (references will resolve) + for (const [schemaName, schemaObj] of Object.entries(contract.components.schemas)) { + try { + const validate = ajv.compile(schemaObj); + schemas[schemaName] = validate; + } catch (err) { + throw new Error(`Failed to compile schema '${schemaName}': ${err instanceof Error ? err.message : String(err)}`, { + cause: err, + }); + } + } + }); + + describe('1. Schema compilation succeeds for all components.schemas', () => { + it('all schemas compile without error (JSON Schema 2020-12 + ajv-formats)', () => { + expect(Object.keys(schemas).length).toBeGreaterThan(0); + expect(schemas).toHaveProperty('HealthStatus'); + expect(schemas).toHaveProperty('ArenaWebhookEvent'); + expect(schemas).toHaveProperty('WebhookAccepted'); + expect(schemas).toHaveProperty('SyncRunRequest'); + expect(schemas).toHaveProperty('SyncRun'); + expect(schemas).toHaveProperty('SyncObjectProgress'); + expect(schemas).toHaveProperty('Problem'); + }); + + it('8 core schemas are compiled', () => { + expect(Object.keys(schemas).length).toBe(8); + expect(schemas).toHaveProperty('SyncObjectType'); + }); + }); + + describe('2. GET /health — all states validated against HealthStatus schema', () => { + it('state="none" before any login', async () => { + const { app } = makeApp(); + const body = await getHealth(app); + + expect(body.arenaSession.state).toBe('none'); + const validation = validateAgainstSchema(body, 'HealthStatus', 'GET /health (state=none)'); + expect(validation.valid).toBe(true); + }); + + it('state="active" while cached session is within TTL', async () => { + const { app, arenaSession, request } = makeApp(); + request.mockResolvedValueOnce(loginOk('sess-1')); + await arenaSession.getSessionId(); + + const body = await getHealth(app); + expect(body.arenaSession.state).toBe('active'); + const validation = validateAgainstSchema(body, 'HealthStatus', 'GET /health (state=active)'); + expect(validation.valid).toBe(true); + }); + + it('state="expired" once cached session is past TTL', async () => { + const { app, arenaSession, request, clock, config } = makeApp(); + request.mockResolvedValueOnce(loginOk('sess-1')); + await arenaSession.getSessionId(); + clock.nowMs += config.arena.sessionTtlMs; + + const body = await getHealth(app); + expect(body.arenaSession.state).toBe('expired'); + const validation = validateAgainstSchema(body, 'HealthStatus', 'GET /health (state=expired)'); + expect(validation.valid).toBe(true); + }); + + it('state="refreshing" while a login is in flight', async () => { + const { app, arenaSession, request } = makeApp(); + let resolveLogin!: (res: HttpResponse) => void; + request.mockImplementationOnce( + () => + new Promise((resolve) => { + resolveLogin = resolve; + }), + ); + const pending = arenaSession.getSessionId(); + + const body = await getHealth(app); + expect(body.arenaSession.state).toBe('refreshing'); + const validation = validateAgainstSchema(body, 'HealthStatus', 'GET /health (state=refreshing)'); + expect(validation.valid).toBe(true); + + resolveLogin(loginOk('sess-1')); + await pending; + }); + + it('status="ok" when operational', async () => { + const { app, arenaSession, request } = makeApp(); + request.mockResolvedValueOnce(loginOk('sess-1')); + await arenaSession.getSessionId(); + + const body = await getHealth(app); + expect(body.status).toBe('ok'); + const validation = validateAgainstSchema(body, 'HealthStatus', 'GET /health (status=ok)'); + expect(validation.valid).toBe(true); + }); + + it('status="degraded" after failed Arena login', async () => { + const { app, arenaSession, request } = makeApp(); + request.mockResolvedValueOnce({ status: 503, body: 'down' }); + await expect(arenaSession.getSessionId()).rejects.toBeInstanceOf(ArenaApiError); + + const body = await getHealth(app); + expect(body.status).toBe('degraded'); + const validation = validateAgainstSchema(body, 'HealthStatus', 'GET /health (status=degraded)'); + expect(validation.valid).toBe(true); + }); + + it('status="degraded" after failed poll tick', async () => { + const { app, health } = makeApp(); + health.recordPollFailure(new Date('2026-09-14T08:15:00Z'), new Error('Arena login failed with HTTP 503')); + + const body = await getHealth(app); + expect(body.status).toBe('degraded'); + const validation = validateAgainstSchema(body, 'HealthStatus', 'GET /health (status=degraded from poll)'); + expect(validation.valid).toBe(true); + }); + + it('poller.lastError is a fixed phrase: an ArenaApiError message/body marker never appears in /health (B5)', async () => { + const { app, health } = makeApp(); + health.recordPollFailure( + new Date('2026-09-14T08:15:00Z'), + new ArenaApiError('Arena login failed with HTTP 503 MARKER-MESSAGE', 503, { + errors: [{ message: 'MARKER-BODY worker@example.test' }], + }), + ); + + const res = await app.inject({ method: 'GET', url: '/health' }); + expect(res.statusCode).toBe(200); + expect(res.body).not.toContain('MARKER-MESSAGE'); + expect(res.body).not.toContain('MARKER-BODY'); + expect(res.body).not.toContain('worker@example.test'); + const body = res.json(); + expect(body.poller.lastError).toBe('Upstream Arena PLM request failed (HTTP 503).'); + const validation = validateAgainstSchema(body, 'HealthStatus', 'GET /health (B5 lastError)'); + expect(validation.valid).toBe(true); + }); + + it('default (unmatched method on /health) is 404 problem+json', async () => { + const { app } = makeApp(); + const res = await app.inject({ method: 'DELETE', url: '/health' }); + await expectProblemStatus(res, 404, 'Not Found', 'DELETE /health (default: unmatched method)'); + }); + }); + + describe('2. POST /webhooks/arena — all documented response codes with body validation', () => { + it('202 Accepted with WebhookAccepted body', async () => { + const { app } = makeApp(); + const res = await app.inject({ + method: 'POST', + url: '/webhooks/arena', + headers: AUTH, + payload: { eventType: 'ITEM_MODIFIED', itemNumber: '900-00042' }, + }); + + expect(res.statusCode).toBe(202); + expect(res.headers['content-type']).toContain('application/json'); + const body = res.json(); + const validation = validateAgainstSchema(body, 'WebhookAccepted', 'POST /webhooks/arena (202)'); + expect(validation.valid).toBe(true); + }); + + it('400 Bad Request (invalid JSON)', async () => { + const { app } = makeApp(); + const res = await app.inject({ + method: 'POST', + url: '/webhooks/arena', + headers: { ...AUTH, 'content-type': 'application/json' }, + payload: '{invalid-json', + }); + + expect(res.statusCode).toBe(400); + expect(res.headers['content-type']).toMatch(/application\/problem\+json/); + const body = res.json(); + const validation = validateAgainstSchema(body, 'Problem', 'POST /webhooks/arena (400 invalid JSON)'); + expect(validation.valid).toBe(true); + expect(body.title).toBe('Bad Request'); + }); + + it('400 Bad Request (non-object body)', async () => { + const { app } = makeApp(); + const res = await app.inject({ + method: 'POST', + url: '/webhooks/arena', + headers: { ...AUTH, 'content-type': 'application/json' }, + payload: '[1,2,3]', + }); + + expect(res.statusCode).toBe(400); + expect(res.headers['content-type']).toMatch(/application\/problem\+json/); + const body = res.json(); + const validation = validateAgainstSchema(body, 'Problem', 'POST /webhooks/arena (400 non-object)'); + expect(validation.valid).toBe(true); + }); + + it('401 Unauthorized (missing bearer token)', async () => { + const { app } = makeApp(); + const res = await app.inject({ + method: 'POST', + url: '/webhooks/arena', + payload: { event: 'ITEM.UPDATED' }, + }); + + expect(res.statusCode).toBe(401); + expect(res.headers['content-type']).toMatch(/application\/problem\+json/); + const body = res.json(); + const validation = validateAgainstSchema(body, 'Problem', 'POST /webhooks/arena (401)'); + expect(validation.valid).toBe(true); + expect(body.title).toBe('Unauthorized'); + }); + + it('401 Unauthorized (wrong bearer token)', async () => { + const { app } = makeApp(); + const res = await app.inject({ + method: 'POST', + url: '/webhooks/arena', + headers: { authorization: 'Bearer wrong-token' }, + payload: { event: 'ITEM.UPDATED' }, + }); + + expect(res.statusCode).toBe(401); + expect(res.headers['content-type']).toMatch(/application\/problem\+json/); + const body = res.json(); + const validation = validateAgainstSchema(body, 'Problem', 'POST /webhooks/arena (401 wrong token)'); + expect(validation.valid).toBe(true); + }); + + it('503 Service Unavailable (queue full) with Retry-After header', async () => { + const { app } = makeApp({ WEBHOOK_QUEUE_MAX_SIZE: '1' }); + // Fill queue + await app.inject({ method: 'POST', url: '/webhooks/arena', headers: AUTH, payload: { n: 1 } }); + + const res = await app.inject({ + method: 'POST', + url: '/webhooks/arena', + headers: AUTH, + payload: { n: 2 }, + }); + + expect(res.statusCode).toBe(503); + expect(res.headers['content-type']).toMatch(/application\/problem\+json/); + expect(res.headers['retry-after']).toBe('30'); + const body = res.json(); + const validation = validateAgainstSchema(body, 'Problem', 'POST /webhooks/arena (503)'); + expect(validation.valid).toBe(true); + expect(body.title).toBe('Service Unavailable'); + }); + + it('415 Unsupported Media Type (text/plain body — built-in parser removed)', async () => { + const { app, queue } = makeApp(); + const res = await app.inject({ + method: 'POST', + url: '/webhooks/arena', + headers: { ...AUTH, 'content-type': 'text/plain' }, + payload: 'eventType=ITEM_MODIFIED', + }); + await expectProblemStatus(res, 415, 'Unsupported Media Type', 'POST /webhooks/arena (415 text/plain)'); + expect(queue.size).toBe(0); + }); + + it('415 Unsupported Media Type (application/xml body)', async () => { + const { app, queue } = makeApp(); + const res = await app.inject({ + method: 'POST', + url: '/webhooks/arena', + headers: { ...AUTH, 'content-type': 'application/xml' }, + payload: '', + }); + await expectProblemStatus(res, 415, 'Unsupported Media Type', 'POST /webhooks/arena (415 xml)'); + expect(queue.size).toBe(0); + }); + + it('413 Payload Too Large (body over bodyLimit) — nothing queued', async () => { + const { app, queue } = makeApp({}, { bodyLimit: SMALL_BODY_LIMIT }); + const res = await app.inject({ + method: 'POST', + url: '/webhooks/arena', + headers: { ...AUTH, 'content-type': 'application/json' }, + payload: OVERSIZED_JSON, + }); + await expectProblemStatus(res, 413, 'Payload Too Large', 'POST /webhooks/arena (413)'); + expect(queue.size).toBe(0); + }); + + it('default (unmatched method on /webhooks/arena) is 404 problem+json', async () => { + const { app } = makeApp(); + const res = await app.inject({ method: 'GET', url: '/webhooks/arena', headers: AUTH }); + await expectProblemStatus(res, 404, 'Not Found', 'GET /webhooks/arena (default: unmatched method)'); + }); + }); + + describe('2. POST /admin/sync-runs — all documented response codes with body validation', () => { + it('201 Created with SyncRun body (full scope, default objects order)', async () => { + const { app } = makeApp(); + const res = await app.inject({ + method: 'POST', + url: '/admin/sync-runs', + headers: AUTH, + payload: { scope: 'full' }, + }); + + expect(res.statusCode).toBe(201); + expect(res.headers['content-type']).toContain('application/json'); + const body = res.json(); + const validation = validateAgainstSchema(body, 'SyncRun', 'POST /admin/sync-runs (201)'); + expect(validation.valid).toBe(true); + expect(body.scope).toBe('full'); + expect(body.state).toBe('queued'); + }); + + it('201 Created with custom objects order', async () => { + const { app } = makeApp(); + const res = await app.inject({ + method: 'POST', + url: '/admin/sync-runs', + headers: AUTH, + payload: { scope: 'delta', objects: ['BOM', 'PRODUCT'] }, + }); + + expect(res.statusCode).toBe(201); + const body = res.json(); + const validation = validateAgainstSchema(body, 'SyncRun', 'POST /admin/sync-runs (201 custom order)'); + expect(validation.valid).toBe(true); + expect(body.objects.map((o: { objectType: string }) => o.objectType)).toEqual(['BOM', 'PRODUCT']); + }); + + it('400 Bad Request (missing scope)', async () => { + const { app } = makeApp(); + const res = await app.inject({ + method: 'POST', + url: '/admin/sync-runs', + headers: AUTH, + payload: {}, + }); + + expect(res.statusCode).toBe(400); + expect(res.headers['content-type']).toMatch(/application\/problem\+json/); + const body = res.json(); + const validation = validateAgainstSchema(body, 'Problem', 'POST /admin/sync-runs (400 missing scope)'); + expect(validation.valid).toBe(true); + }); + + it('400 Bad Request (invalid scope)', async () => { + const { app } = makeApp(); + const res = await app.inject({ + method: 'POST', + url: '/admin/sync-runs', + headers: AUTH, + payload: { scope: 'bogus' }, + }); + + expect(res.statusCode).toBe(400); + const body = res.json(); + const validation = validateAgainstSchema(body, 'Problem', 'POST /admin/sync-runs (400 invalid scope)'); + expect(validation.valid).toBe(true); + }); + + it('400 Bad Request (empty objects array)', async () => { + const { app } = makeApp(); + const res = await app.inject({ + method: 'POST', + url: '/admin/sync-runs', + headers: AUTH, + payload: { scope: 'full', objects: [] }, + }); + + expect(res.statusCode).toBe(400); + const body = res.json(); + const validation = validateAgainstSchema(body, 'Problem', 'POST /admin/sync-runs (400 empty objects)'); + expect(validation.valid).toBe(true); + }); + + it('400 Bad Request (duplicate objects)', async () => { + const { app } = makeApp(); + const res = await app.inject({ + method: 'POST', + url: '/admin/sync-runs', + headers: AUTH, + payload: { scope: 'full', objects: ['BOM', 'BOM'] }, + }); + + expect(res.statusCode).toBe(400); + const body = res.json(); + const validation = validateAgainstSchema(body, 'Problem', 'POST /admin/sync-runs (400 duplicates)'); + expect(validation.valid).toBe(true); + }); + + it('400 Bad Request (unknown object type)', async () => { + const { app } = makeApp(); + const res = await app.inject({ + method: 'POST', + url: '/admin/sync-runs', + headers: AUTH, + payload: { scope: 'full', objects: ['WIDGET'] }, + }); + + expect(res.statusCode).toBe(400); + const body = res.json(); + const validation = validateAgainstSchema(body, 'Problem', 'POST /admin/sync-runs (400 unknown type)'); + expect(validation.valid).toBe(true); + }); + + it('400 Bad Request (unknown property additionalProperties: false)', async () => { + const { app } = makeApp(); + const res = await app.inject({ + method: 'POST', + url: '/admin/sync-runs', + headers: AUTH, + payload: { scope: 'full', unknownProp: 'x' }, + }); + + expect(res.statusCode).toBe(400); + const body = res.json(); + const validation = validateAgainstSchema(body, 'Problem', 'POST /admin/sync-runs (400 extra property)'); + expect(validation.valid).toBe(true); + }); + + it('400 Bad Request (no type coercion: {"scope":["full"]})', async () => { + const { app } = makeApp(); + const res = await app.inject({ + method: 'POST', + url: '/admin/sync-runs', + headers: AUTH, + payload: { scope: ['full'] }, + }); + + expect(res.statusCode).toBe(400); + const body = res.json(); + const validation = validateAgainstSchema(body, 'Problem', 'POST /admin/sync-runs (400 scope as array)'); + expect(validation.valid).toBe(true); + }); + + it('400 Bad Request (no type coercion: {"objects":"BOM"})', async () => { + const { app } = makeApp(); + const res = await app.inject({ + method: 'POST', + url: '/admin/sync-runs', + headers: AUTH, + payload: { scope: 'full', objects: 'BOM' }, + }); + + expect(res.statusCode).toBe(400); + const body = res.json(); + const validation = validateAgainstSchema(body, 'Problem', 'POST /admin/sync-runs (400 objects as scalar)'); + expect(validation.valid).toBe(true); + }); + + it('401 Unauthorized (missing bearer token)', async () => { + const { app } = makeApp(); + const res = await app.inject({ + method: 'POST', + url: '/admin/sync-runs', + payload: { scope: 'full' }, + }); + + expect(res.statusCode).toBe(401); + expect(res.headers['content-type']).toMatch(/application\/problem\+json/); + const body = res.json(); + const validation = validateAgainstSchema(body, 'Problem', 'POST /admin/sync-runs (401)'); + expect(validation.valid).toBe(true); + }); + + it('415 Unsupported Media Type (text/plain body) — no run created', async () => { + const { app, runner } = makeApp(); + const res = await app.inject({ + method: 'POST', + url: '/admin/sync-runs', + headers: { ...AUTH, 'content-type': 'text/plain' }, + payload: 'scope=full', + }); + await expectProblemStatus(res, 415, 'Unsupported Media Type', 'POST /admin/sync-runs (415)'); + expect(runner.inFlightCount).toBe(0); + }); + + it('413 Payload Too Large (body over bodyLimit) — no run created', async () => { + const { app, runner } = makeApp({}, { bodyLimit: SMALL_BODY_LIMIT }); + const res = await app.inject({ + method: 'POST', + url: '/admin/sync-runs', + headers: { ...AUTH, 'content-type': 'application/json' }, + payload: OVERSIZED_JSON, + }); + await expectProblemStatus(res, 413, 'Payload Too Large', 'POST /admin/sync-runs (413)'); + expect(runner.inFlightCount).toBe(0); + }); + + it('default (unmatched method on /admin/sync-runs) is 404 problem+json', async () => { + const { app } = makeApp(); + const res = await app.inject({ method: 'DELETE', url: '/admin/sync-runs', headers: AUTH }); + await expectProblemStatus(res, 404, 'Not Found', 'DELETE /admin/sync-runs (default: unmatched method)'); + }); + }); + + describe('2. GET /admin/sync-runs/{runId} — all documented response codes with body validation', () => { + it('200 OK with SyncRun body', async () => { + const { app, runner } = makeApp(); + const created = await app.inject({ + method: 'POST', + url: '/admin/sync-runs', + headers: AUTH, + payload: { scope: 'delta' }, + }); + const { runId } = created.json() as SyncRun; + await runner.waitFor(runId); + + const res = await app.inject({ + method: 'GET', + url: `/admin/sync-runs/${runId}`, + headers: AUTH, + }); + + expect(res.statusCode).toBe(200); + expect(res.headers['content-type']).toContain('application/json'); + const body = res.json(); + const validation = validateAgainstSchema(body, 'SyncRun', 'GET /admin/sync-runs/{runId} (200)'); + expect(validation.valid).toBe(true); + expect(body.runId).toBe(runId); + }); + + it('401 Unauthorized (missing bearer token)', async () => { + const { app } = makeApp(); + const res = await app.inject({ + method: 'GET', + url: '/admin/sync-runs/00000000-0000-4000-8000-000000000000', + }); + + expect(res.statusCode).toBe(401); + expect(res.headers['content-type']).toMatch(/application\/problem\+json/); + const body = res.json(); + const validation = validateAgainstSchema(body, 'Problem', 'GET /admin/sync-runs/{runId} (401)'); + expect(validation.valid).toBe(true); + }); + + it('404 Not Found with Problem body', async () => { + const { app } = makeApp(); + const res = await app.inject({ + method: 'GET', + url: '/admin/sync-runs/00000000-0000-4000-8000-000000000000', + headers: AUTH, + }); + + expect(res.statusCode).toBe(404); + expect(res.headers['content-type']).toMatch(/application\/problem\+json/); + const body = res.json(); + const validation = validateAgainstSchema(body, 'Problem', 'GET /admin/sync-runs/{runId} (404)'); + expect(validation.valid).toBe(true); + expect(body.title).toBe('Not Found'); + }); + + it('default (unmatched method on /admin/sync-runs/{runId}) is 404 problem+json', async () => { + const { app } = makeApp(); + const res = await app.inject({ + method: 'PUT', + url: '/admin/sync-runs/00000000-0000-4000-8000-000000000000', + headers: AUTH, + }); + await expectProblemStatus(res, 404, 'Not Found', 'PUT /admin/sync-runs/{runId} (default: unmatched method)'); + }); + + it('default (unknown path) is 404 problem+json — x-unmatched-routes', async () => { + const { app } = makeApp(); + const res = await app.inject({ method: 'GET', url: '/admin/nope' }); + await expectProblemStatus(res, 404, 'Not Found', 'GET /admin/nope (x-unmatched-routes)'); + expect(contract['x-unmatched-routes']).toMatch(/404/); + }); + }); + + describe('3. Reverse check: every documented response code is tested', () => { + // The set of response keys THIS SUITE exercises per operation. The test + // below compares it against the contract, so adding a code to the contract + // without a test here fails, and so does a test-only code that is not + // documented. `default` is exercised by the unmatched-method/path 404 + // cases per operation plus the operation-independent 500 case in §4. + const exercised: Record = { + 'get /health': ['200', 'default'], + 'post /webhooks/arena': ['202', '400', '401', '413', '415', '503', 'default'], + 'post /admin/sync-runs': ['201', '400', '401', '413', '415', 'default'], + 'get /admin/sync-runs/{runId}': ['200', '401', '404', 'default'], + }; + // Reserved (contract-defined, no live route emits it yet): tested at the + // error-handler seam in §4, not attached to an operation. + const reserved: Record = { BadGateway: '502' }; + + it('documented response keys per operation equal the exercised set (read from the contract)', () => { + const documented: Record = {}; + for (const [path, item] of Object.entries(contract.paths as Record>)) { + for (const [method, op] of Object.entries(item)) { + const responses = (op as { responses: Record }).responses; + documented[`${method} ${path}`] = Object.keys(responses).sort(); + } + } + const expected = Object.fromEntries(Object.entries(exercised).map(([k, v]) => [k, [...v].sort()])); + expect(documented).toEqual(expected); + }); + + it('every operation documents a `default` problem+json response and the reserved 502 exists', () => { + for (const item of Object.values(contract.paths as Record>)) { + for (const op of Object.values(item)) { + const responses = (op as { responses: Record }).responses; + expect(responses.default).toEqual({ $ref: '#/components/responses/Default' }); + } + } + expect(contract.components.responses.Default.content).toHaveProperty('application/problem+json'); + expect(contract.components.responses.BadGateway.content['application/problem+json'].example.status).toBe( + Number(reserved.BadGateway), + ); + }); + }); + + describe('4. problem+json and 5xx validation', () => { + it('all 4xx/5xx bodies are application/problem+json (contract RFC 9457)', async () => { + const { app } = makeApp(); + + // Test 400 (invalid JSON) + const res400 = await app.inject({ + method: 'POST', + url: '/webhooks/arena', + headers: { ...AUTH, 'content-type': 'application/json' }, + payload: '{invalid-json', + }); + expect(res400.statusCode).toBe(400); + expect(res400.headers['content-type']).toMatch(/application\/problem\+json/); + + // Test 401 + const res401 = await app.inject({ + method: 'POST', + url: '/webhooks/arena', + payload: { event: 'test' }, + }); + expect(res401.statusCode).toBe(401); + expect(res401.headers['content-type']).toMatch(/application\/problem\+json/); + + // Test 404 + const res404 = await app.inject({ + method: 'GET', + url: '/admin/sync-runs/00000000-0000-4000-8000-000000000000', + headers: AUTH, + }); + expect(res404.statusCode).toBe(404); + expect(res404.headers['content-type']).toMatch(/application\/problem\+json/); + + // Test 503 + const { app: app503 } = makeApp({ WEBHOOK_QUEUE_MAX_SIZE: '1' }); + await app503.inject({ method: 'POST', url: '/webhooks/arena', headers: AUTH, payload: { n: 1 } }); + const res503 = await app503.inject({ + method: 'POST', + url: '/webhooks/arena', + headers: AUTH, + payload: { n: 2 }, + }); + expect(res503.statusCode).toBe(503); + expect(res503.headers['content-type']).toMatch(/application\/problem\+json/); + await app503.close(); + }); + + it('5xx bodies contain no detail derived from upstream (ArenaApiError -> 502)', async () => { + const { app } = makeApp(); + // Throwaway test route to exercise ArenaApiError handling + app.get('/__test/upstream', async () => { + throw new ArenaApiError('Arena login failed with HTTP 500', 500, { + errors: [{ message: 'LEAKED-UPSTREAM-BODY worker@example.test' }], + }); + }); + + const res = await app.inject({ method: 'GET', url: '/__test/upstream' }); + + expect(res.statusCode).toBe(502); + expect(res.headers['content-type']).toMatch(/application\/problem\+json/); + const body = res.json(); + expect(body.title).toBe('Bad Gateway'); + expect(body.detail).toBe('Upstream Arena PLM request failed.'); + expect(res.body).not.toContain('LEAKED-UPSTREAM-BODY'); + expect(res.body).not.toContain('HTTP 500'); + + const validation = validateAgainstSchema(body, 'Problem', '502 ArenaApiError'); + expect(validation.valid).toBe(true); + }); + + it('other 5xx errors do not leak details', async () => { + const { app } = makeApp(); + app.get('/__test/boom', async () => { + throw new Error('internal secret'); + }); + + const res = await app.inject({ method: 'GET', url: '/__test/boom' }); + + expect(res.statusCode).toBe(500); + expect(res.headers['content-type']).toMatch(/application\/problem\+json/); + const body = res.json(); + expect(body.title).toBe('Internal Server Error'); + expect(res.body).not.toContain('internal secret'); + + const validation = validateAgainstSchema(body, 'Problem', '500 generic error'); + expect(validation.valid).toBe(true); + }); + }); + + describe('Contract schema validation compliance', () => { + it('ArenaWebhookEvent allows arbitrary properties (provisional schema)', async () => { + const { app } = makeApp(); + const payload = { + eventType: 'ITEM_MODIFIED', + itemNumber: '900-00042', + unknownField: 'allowed', + nestedUnknown: { foo: 'bar' }, + }; + const res = await app.inject({ + method: 'POST', + url: '/webhooks/arena', + headers: AUTH, + payload, + }); + + expect(res.statusCode).toBe(202); + const validation = validateAgainstSchema(payload, 'ArenaWebhookEvent', 'POST /webhooks/arena with extra properties'); + expect(validation.valid).toBe(true); + }); + + it('SyncObjectProgress validates with all states and optional fields', async () => { + // The schema defines state: pending|running|completed|failed + // and optional processed/error fields. + // This is validated implicitly through SyncRun responses. + const { app, runner } = makeApp(); + const created = await app.inject({ + method: 'POST', + url: '/admin/sync-runs', + headers: AUTH, + payload: { scope: 'full' }, + }); + const { runId } = created.json() as SyncRun; + await runner.waitFor(runId); + + const res = await app.inject({ + method: 'GET', + url: `/admin/sync-runs/${runId}`, + headers: AUTH, + }); + const run = res.json() as SyncRun; + + // Verify each object has valid state + for (const obj of run.objects) { + expect(['pending', 'running', 'completed', 'failed']).toContain(obj.state); + } + + const validation = validateAgainstSchema(run, 'SyncRun', 'SyncRun with SyncObjectProgress'); + expect(validation.valid).toBe(true); + }); + }); +}); diff --git a/worker/test/httpClient.test.ts b/worker/test/httpClient.test.ts new file mode 100644 index 0000000..0de304b --- /dev/null +++ b/worker/test/httpClient.test.ts @@ -0,0 +1,59 @@ +import { describe, expect, it, vi } from 'vitest'; +import { createFetchHttpClient } from '../src/http/types.js'; + +type FetchImpl = typeof fetch; + +function okResponse(body: string, status = 200): Response { + return new Response(body, { status, headers: { 'content-type': 'application/json' } }); +} + +describe('createFetchHttpClient', () => { + it('rejects a non-positive timeout at construction', () => { + expect(() => createFetchHttpClient({ timeoutMs: 0 })).toThrow(/timeoutMs/); + }); + + it('attaches an AbortSignal deadline to every request and parses JSON bodies', async () => { + const fetchImpl = vi.fn().mockResolvedValue(okResponse('{"arenaSessionId":"s1"}')); + const client = createFetchHttpClient({ timeoutMs: 30_000, fetchImpl }); + + const res = await client.request({ method: 'POST', url: 'https://arena.invalid/v1/login', body: { a: 1 } }); + + expect(res).toEqual({ status: 200, body: { arenaSessionId: 's1' } }); + const init = fetchImpl.mock.calls[0]?.[1]; + expect(init?.signal).toBeInstanceOf(AbortSignal); + expect(init?.signal?.aborted).toBe(false); + expect(init?.method).toBe('POST'); + expect(init?.body).toBe('{"a":1}'); + expect((init?.headers as Record)['Content-Type']).toBe('application/json'); + }); + + it('rejects with a TimeoutError when the upstream does not answer within timeoutMs', async () => { + // A fetch that only settles when its signal aborts — i.e. a hung socket. + const fetchImpl: FetchImpl = (_url, init) => + new Promise((_resolve, reject) => { + init?.signal?.addEventListener('abort', () => reject(init.signal?.reason as Error)); + }); + const client = createFetchHttpClient({ timeoutMs: 20, fetchImpl }); + + await expect(client.request({ method: 'GET', url: 'https://arena.invalid/v1/items' })).rejects.toMatchObject({ + name: 'TimeoutError', + }); + }); + + it('returns raw text when the body is not JSON and null when empty', async () => { + const fetchImpl = vi + .fn() + .mockResolvedValueOnce(okResponse('not json', 502)) + .mockResolvedValueOnce(new Response(null, { status: 204 })); + const client = createFetchHttpClient({ timeoutMs: 1000, fetchImpl }); + + expect(await client.request({ method: 'GET', url: 'https://arena.invalid/x' })).toEqual({ + status: 502, + body: 'not json', + }); + expect(await client.request({ method: 'GET', url: 'https://arena.invalid/y' })).toEqual({ + status: 204, + body: null, + }); + }); +}); diff --git a/worker/test/poller.test.ts b/worker/test/poller.test.ts new file mode 100644 index 0000000..6ca3414 --- /dev/null +++ b/worker/test/poller.test.ts @@ -0,0 +1,216 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; +import { UNCLASSIFIED_ERROR_PHRASE } from '../src/errors/publicPhrase.js'; +import { HealthRegistry } from '../src/health/registry.js'; +import { DeltaPoller } from '../src/poller/deltaPoller.js'; + +describe('DeltaPoller', () => { + beforeEach(() => { + vi.useFakeTimers(); + }); + + afterEach(() => { + vi.useRealTimers(); + }); + + it('polls on the configured interval after start and stops cleanly', async () => { + const poll = vi.fn().mockResolvedValue(undefined); + const poller = new DeltaPoller({ intervalMs: 1000, poll }); + + expect(poller.running).toBe(false); + poller.start(); + poller.start(); // idempotent + expect(poller.running).toBe(true); + + await vi.advanceTimersByTimeAsync(3000); + expect(poll).toHaveBeenCalledTimes(3); + + await poller.stop(); + await poller.stop(); // idempotent + expect(poller.running).toBe(false); + await vi.advanceTimersByTimeAsync(3000); + expect(poll).toHaveBeenCalledTimes(3); + }); + + it('skips a tick while the previous poll is still in flight', async () => { + let release!: () => void; + const poll = vi.fn( + () => + new Promise((resolve) => { + release = resolve; + }), + ); + const poller = new DeltaPoller({ intervalMs: 1000, poll }); + + poller.start(); + await vi.advanceTimersByTimeAsync(2500); // ticks at 1000 and 2000 — second skipped + expect(poll).toHaveBeenCalledTimes(1); + + release(); + await vi.advanceTimersByTimeAsync(1000); + expect(poll).toHaveBeenCalledTimes(2); + + // stop() now awaits the in-flight (second) tick, so release it first. + const stopped = poller.stop(); + release(); + await stopped; + }); + + it('reports poll errors via onError and keeps polling', async () => { + const onError = vi.fn(); + const poll = vi.fn().mockRejectedValue(new Error('arena unreachable')); + const poller = new DeltaPoller({ intervalMs: 1000, poll, onError }); + + poller.start(); + await vi.advanceTimersByTimeAsync(2000); + + expect(poll).toHaveBeenCalledTimes(2); + expect(onError).toHaveBeenCalledTimes(2); + expect(onError.mock.calls[0]?.[0]).toBeInstanceOf(Error); + + await poller.stop(); + }); + + describe('cursor ownership', () => { + it('starts the cursor at construction time and advances it to the tick time only after a successful poll', async () => { + let nowMs = 1_000; + const poll = vi.fn().mockResolvedValue(undefined); + const poller = new DeltaPoller({ intervalMs: 1000, poll, now: () => new Date(nowMs) }); + expect(poller.nextSince.getTime()).toBe(1_000); + + poller.start(); + nowMs = 2_000; + await vi.advanceTimersByTimeAsync(1000); // tick 1 at t=2000: since=1000 + nowMs = 3_000; + await vi.advanceTimersByTimeAsync(1000); // tick 2 at t=3000: since=2000 + + expect(poll.mock.calls.map((call) => (call[0] as Date).getTime())).toEqual([1_000, 2_000]); + expect(poller.nextSince.getTime()).toBe(3_000); + await poller.stop(); + }); + + it('keeps the same `since` across consecutive ticks while the poll keeps rejecting', async () => { + let nowMs = 1_000; + const poll = vi.fn().mockRejectedValue(new Error('arena unreachable')); + const poller = new DeltaPoller({ + intervalMs: 1000, + poll, + onError: () => undefined, + now: () => new Date(nowMs), + }); + + poller.start(); + nowMs = 2_000; + await vi.advanceTimersByTimeAsync(1000); + nowMs = 3_000; + await vi.advanceTimersByTimeAsync(1000); + nowMs = 4_000; + await vi.advanceTimersByTimeAsync(1000); + + // A failed poll must not lose its window: every tick re-covers from 1000. + expect(poll.mock.calls.map((call) => (call[0] as Date).getTime())).toEqual([1_000, 1_000, 1_000]); + expect(poller.nextSince.getTime()).toBe(1_000); + + // First success advances to that tick's `asOf`, and the next tick uses it. + poll.mockResolvedValue(undefined); + nowMs = 5_000; + await vi.advanceTimersByTimeAsync(1000); + nowMs = 6_000; + await vi.advanceTimersByTimeAsync(1000); + expect(poll.mock.calls.slice(3).map((call) => (call[0] as Date).getTime())).toEqual([1_000, 5_000]); + await poller.stop(); + }); + + it('captures asOf BEFORE the fetch so changes arriving mid-poll are not skipped', async () => { + let nowMs = 1_000; + let release!: () => void; + const poll = vi.fn( + () => + new Promise((resolve) => { + release = resolve; + }), + ); + const poller = new DeltaPoller({ intervalMs: 1000, poll, now: () => new Date(nowMs) }); + + poller.start(); + nowMs = 2_000; + await vi.advanceTimersByTimeAsync(1000); // tick starts at t=2000 + nowMs = 2_900; // time passes while the poll is in flight + release(); + await vi.advanceTimersByTimeAsync(0); + + // Cursor is the tick's start time, not its completion time. + expect(poller.nextSince.getTime()).toBe(2_000); + await poller.stop(); + }); + }); + + describe('stop()', () => { + it('resolves immediately when no tick is in flight', async () => { + const poller = new DeltaPoller({ intervalMs: 1000, poll: vi.fn() }); + poller.start(); + await expect(poller.stop()).resolves.toBeUndefined(); + expect(poller.running).toBe(false); + }); + + it('returns the in-flight tick promise so shutdown can await it', async () => { + let release!: () => void; + const poll = vi.fn( + () => + new Promise((resolve) => { + release = resolve; + }), + ); + const poller = new DeltaPoller({ intervalMs: 1000, poll }); + poller.start(); + await vi.advanceTimersByTimeAsync(1000); + expect(poll).toHaveBeenCalledTimes(1); + + let settled = false; + const stopped = poller.stop().then(() => { + settled = true; + }); + await vi.advanceTimersByTimeAsync(5000); + expect(poller.running).toBe(false); + expect(settled).toBe(false); // still waiting on the tick + expect(poll).toHaveBeenCalledTimes(1); // and no new ticks were scheduled + + release(); + await stopped; + expect(settled).toBe(true); + }); + }); + + describe('health reporting', () => { + it('records success and failure with the tick start time', async () => { + let nowMs = 10_000; + const health = new HealthRegistry(); + const poll = vi.fn().mockRejectedValueOnce(new Error('boom')).mockResolvedValueOnce(undefined); + const poller = new DeltaPoller({ + intervalMs: 1000, + poll, + health, + onError: () => undefined, + now: () => new Date(nowMs), + }); + + poller.start(); + nowMs = 11_000; + await vi.advanceTimersByTimeAsync(1000); + expect(health.status()).toBe('degraded'); + // B5: the registry stores a fixed public phrase, never the raw message. + expect(health.pollerSnapshot()).toEqual({ + lastRunAt: new Date(11_000).toISOString(), + lastError: UNCLASSIFIED_ERROR_PHRASE, + }); + + nowMs = 12_000; + await vi.advanceTimersByTimeAsync(1000); + expect(health.status()).toBe('ok'); + expect(health.pollerSnapshot()).toEqual({ + lastRunAt: new Date(12_000).toISOString(), + lastSuccessAt: new Date(12_000).toISOString(), + }); + await poller.stop(); + }); + }); +}); diff --git a/worker/test/queue.test.ts b/worker/test/queue.test.ts new file mode 100644 index 0000000..b7b3213 --- /dev/null +++ b/worker/test/queue.test.ts @@ -0,0 +1,43 @@ +import { describe, expect, it } from 'vitest'; +import { DEFAULT_WEBHOOK_QUEUE_MAX_SIZE, InMemoryEventQueue } from '../src/queue/webhookQueue.js'; + +describe('InMemoryEventQueue (bounded transient buffer, ARN-003)', () => { + it('defaults maxSize to 10000', () => { + expect(DEFAULT_WEBHOOK_QUEUE_MAX_SIZE).toBe(10_000); + expect(new InMemoryEventQueue().maxSize).toBe(10_000); + }); + + it('rejects a non-positive or non-integer maxSize', () => { + expect(() => new InMemoryEventQueue({ maxSize: 0 })).toThrow(/maxSize/); + expect(() => new InMemoryEventQueue({ maxSize: -5 })).toThrow(/maxSize/); + expect(() => new InMemoryEventQueue({ maxSize: 1.5 })).toThrow(/maxSize/); + }); + + it('accepts up to maxSize events, then answers {accepted:false, reason:"full"} without growing', () => { + const queue = new InMemoryEventQueue({ maxSize: 2 }); + + const first = queue.enqueue({ source: 'webhook', payload: { n: 1 } }); + const second = queue.enqueue({ source: 'poll', payload: { n: 2 } }); + expect(first.accepted).toBe(true); + expect(second.accepted).toBe(true); + if (first.accepted) expect(first.event.payload).toEqual({ n: 1 }); + expect(queue.isFull).toBe(true); + + const third = queue.enqueue({ source: 'webhook', payload: { n: 3 } }); + expect(third).toEqual({ accepted: false, reason: 'full' }); + expect(queue.size).toBe(2); + expect(queue.peekAll().map((e) => e.payload)).toEqual([{ n: 1 }, { n: 2 }]); + }); + + it('accepts again once an event has been dequeued (FIFO)', () => { + const queue = new InMemoryEventQueue({ maxSize: 1 }); + queue.enqueue({ source: 'webhook', payload: 'a' }); + expect(queue.enqueue({ source: 'webhook', payload: 'b' }).accepted).toBe(false); + + expect(queue.dequeue()?.payload).toBe('a'); + expect(queue.isFull).toBe(false); + expect(queue.enqueue({ source: 'webhook', payload: 'b' }).accepted).toBe(true); + expect(queue.dequeue()?.payload).toBe('b'); + expect(queue.dequeue()).toBeUndefined(); + }); +}); diff --git a/worker/test/runner.test.ts b/worker/test/runner.test.ts new file mode 100644 index 0000000..6b3ef19 --- /dev/null +++ b/worker/test/runner.test.ts @@ -0,0 +1,174 @@ +import { describe, expect, it } from 'vitest'; +import { ArenaApiError } from '../src/arena/session.js'; +import { UNCLASSIFIED_ERROR_PHRASE } from '../src/errors/publicPhrase.js'; +import { InMemoryRunStore } from '../src/sync/runStore.js'; +import { SyncRunner } from '../src/sync/runner.js'; +import type { + SyncExecutionContext, + SyncExecutionResult, + SyncExecutor, + SyncObjectType, +} from '../src/sync/types.js'; + +class RecordingExecutor implements SyncExecutor { + constructor( + readonly objectType: SyncObjectType, + private readonly log: SyncObjectType[], + private readonly failWith?: Error, + ) {} + + async execute(_ctx: SyncExecutionContext): Promise { + this.log.push(this.objectType); + if (this.failWith) throw this.failWith; + return { processed: 1 }; + } +} + +function makeExecutors(log: SyncObjectType[], failing?: Partial>) { + const types: SyncObjectType[] = ['PRODUCT', 'CHANGE_MASTER', 'BOM', 'DOCUMENTS']; + return new Map( + types.map((t) => [t, new RecordingExecutor(t, log, failing?.[t])]), + ); +} + +describe('SyncRunner', () => { + it('moves a run queued → completed, executing objects sequentially in order', async () => { + const log: SyncObjectType[] = []; + const store = new InMemoryRunStore(); + const runner = new SyncRunner(store, makeExecutors(log)); + + const run = runner.submit('full', ['PRODUCT', 'CHANGE_MASTER', 'BOM', 'DOCUMENTS']); + expect(run.state).toBe('queued'); + expect(log).toEqual([]); // nothing executed before submit() returns + + const final = await runner.waitFor(run.runId); + expect(final?.state).toBe('completed'); + expect(final?.finishedAt).toBeDefined(); + expect(log).toEqual(['PRODUCT', 'CHANGE_MASTER', 'BOM', 'DOCUMENTS']); + expect(final?.objects.map((o) => ({ t: o.objectType, s: o.state, p: o.processed }))).toEqual([ + { t: 'PRODUCT', s: 'completed', p: 1 }, + { t: 'CHANGE_MASTER', s: 'completed', p: 1 }, + { t: 'BOM', s: 'completed', p: 1 }, + { t: 'DOCUMENTS', s: 'completed', p: 1 }, + ]); + }); + + it('respects a custom object order', async () => { + const log: SyncObjectType[] = []; + const runner = new SyncRunner(new InMemoryRunStore(), makeExecutors(log)); + + const run = runner.submit('delta', ['BOM', 'PRODUCT']); + await runner.waitFor(run.runId); + + expect(log).toEqual(['BOM', 'PRODUCT']); + }); + + it('stops the chain at the first failure and marks the run failed', async () => { + const log: SyncObjectType[] = []; + const runner = new SyncRunner( + new InMemoryRunStore(), + makeExecutors(log, { PRODUCT: new Error('boom') }), + ); + + const run = runner.submit('full', ['PRODUCT', 'CHANGE_MASTER', 'BOM', 'DOCUMENTS']); + const final = await runner.waitFor(run.runId); + + expect(final?.state).toBe('failed'); + // B5: an unknown error class maps to the fixed unclassified phrase, never its message. + expect(final?.error).toBe(`PRODUCT: ${UNCLASSIFIED_ERROR_PHRASE}`); + expect(final?.objects[0]?.error).toBe(UNCLASSIFIED_ERROR_PHRASE); + expect(JSON.stringify(final)).not.toContain('boom'); + expect(log).toEqual(['PRODUCT']); // later objects never executed + expect(final?.objects.map((o) => o.state)).toEqual(['failed', 'pending', 'pending', 'pending']); + }); + + it('B4: `processed` is absent while pending and present once an object has run', async () => { + const log: SyncObjectType[] = []; + const runner = new SyncRunner( + new InMemoryRunStore(), + makeExecutors(log, { CHANGE_MASTER: new Error('boom') }), + ); + + const queued = runner.submit('full', ['PRODUCT', 'CHANGE_MASTER', 'BOM']); + expect(queued.objects.every((o) => !('processed' in o))).toBe(true); + + const final = await runner.waitFor(queued.runId); + expect(final?.objects.map((o) => [o.state, 'processed' in o])).toEqual([ + ['completed', true], + ['failed', true], // started, so the counter exists (0 here) + ['pending', false], + ]); + expect(final?.objects[1]?.processed).toBe(0); + }); + + it('B5: an ArenaApiError from an executor becomes a fixed status phrase; its body and message never reach the snapshot', async () => { + const log: SyncObjectType[] = []; + const upstream = new ArenaApiError('Arena GET /items failed with HTTP 503 MARKER-MESSAGE', 503, { + errors: [{ message: 'MARKER-BODY worker@example.test' }], + }); + const runner = new SyncRunner(new InMemoryRunStore(), makeExecutors(log, { BOM: upstream })); + + const run = runner.submit('full', ['PRODUCT', 'BOM', 'DOCUMENTS']); + const final = await runner.waitFor(run.runId); + + expect(final?.state).toBe('failed'); + expect(final?.objects[1]?.error).toBe('Upstream Arena PLM request failed (HTTP 503).'); + expect(final?.error).toBe('BOM: Upstream Arena PLM request failed (HTTP 503).'); + const wire = JSON.stringify(final); + expect(wire).not.toContain('MARKER-MESSAGE'); + expect(wire).not.toContain('MARKER-BODY'); + expect(wire).not.toContain('worker@example.test'); + }); + + it('drain() resolves only after every in-flight run has settled (success and failure alike)', async () => { + const log: SyncObjectType[] = []; + let releaseBom!: () => void; + const gate = new Promise((resolve) => { + releaseBom = resolve; + }); + class GatedExecutor implements SyncExecutor { + readonly objectType: SyncObjectType = 'BOM'; + async execute(): Promise { + await gate; + log.push('BOM'); + return { processed: 1 }; + } + } + const executors = makeExecutors(log, { PRODUCT: new Error('boom') }); + executors.set('BOM', new GatedExecutor()); + const runner = new SyncRunner(new InMemoryRunStore(), executors); + + const failing = runner.submit('full', ['PRODUCT']); + const gated = runner.submit('full', ['BOM']); + expect(runner.inFlightCount).toBe(2); + + let drained = false; + const drain = runner.drain().then(() => { + drained = true; + }); + await new Promise((r) => setImmediate(r)); + expect(drained).toBe(false); // the gated BOM run is still executing + expect(runner.inFlightCount).toBe(1); // the failing run has already settled + + releaseBom(); + await drain; + expect(drained).toBe(true); + expect(runner.inFlightCount).toBe(0); + expect((await runner.waitFor(failing.runId))?.state).toBe('failed'); + expect((await runner.waitFor(gated.runId))?.state).toBe('completed'); + }); + + it('drain() resolves immediately when nothing is in flight', async () => { + const runner = new SyncRunner(new InMemoryRunStore(), makeExecutors([])); + await expect(runner.drain()).resolves.toBeUndefined(); + }); + + it('fails the run when no executor is registered for an object type', async () => { + const runner = new SyncRunner(new InMemoryRunStore(), new Map()); + const run = runner.submit('full', ['PRODUCT']); + const final = await runner.waitFor(run.runId); + + expect(final?.state).toBe('failed'); + expect(final?.error).toBe('PRODUCT: no executor registered'); + }); +}); diff --git a/worker/test/server.test.ts b/worker/test/server.test.ts new file mode 100644 index 0000000..f6b3656 --- /dev/null +++ b/worker/test/server.test.ts @@ -0,0 +1,548 @@ +import { afterEach, describe, expect, it, vi } from 'vitest'; +import type { FastifyInstance, LightMyRequestResponse } from 'fastify'; +import { ArenaApiError, ArenaSessionManager } from '../src/arena/session.js'; +import { loadConfig } from '../src/config.js'; +import { UNCLASSIFIED_ERROR_PHRASE } from '../src/errors/publicPhrase.js'; +import { HealthRegistry } from '../src/health/registry.js'; +import type { HttpClient, HttpResponse } from '../src/http/types.js'; +import { InMemoryEventQueue } from '../src/queue/webhookQueue.js'; +import { buildServer } from '../src/server.js'; +import { makeNoopExecutors } from '../src/sync/executors.js'; +import { InMemoryRunStore } from '../src/sync/runStore.js'; +import { SyncRunner } from '../src/sync/runner.js'; +import type { SyncRun } from '../src/sync/types.js'; + +const TOKEN = 'test-token'; +const AUTH = { authorization: `Bearer ${TOKEN}` }; +const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i; + +let apps: FastifyInstance[] = []; + +interface HealthBody { + status: 'ok' | 'degraded'; + version: string; + uptimeSeconds: number; + arenaSession: { state: string }; + poller?: { lastRunAt?: string; lastSuccessAt?: string; lastError?: string }; +} + +function makeApp(envOverrides: Record = {}) { + const config = loadConfig({ WORKER_BEARER_TOKEN: TOKEN, ...envOverrides }); + const health = new HealthRegistry(); + const queue = new InMemoryEventQueue({ maxSize: config.webhookQueueMaxSize }); + const runStore = new InMemoryRunStore(); + const runner = new SyncRunner(runStore, makeNoopExecutors()); + // Arena transport is a mock: tests never touch the network. Default answer + // rejects so an unexpected Arena call fails loudly. + const request = vi.fn().mockRejectedValue(new Error('no network calls in tests')); + const clock = { nowMs: 1_000_000 }; + const arenaSession = new ArenaSessionManager({ + http: { request }, + baseUrl: 'https://arena.invalid/v1', + email: 'worker@example.test', + password: 'secret', + sessionTtlMs: config.arena.sessionTtlMs, + now: () => clock.nowMs, + health, + }); + const app = buildServer({ config, queue, runner, runStore, arenaSession, health }); + apps.push(app); + return { app, queue, runStore, runner, arenaSession, health, request, clock, config }; +} + +function loginOk(sessionId: string): HttpResponse { + return { status: 200, body: { arenaSessionId: sessionId } }; +} + +async function getHealth(app: FastifyInstance): Promise { + const res = await app.inject({ method: 'GET', url: '/health' }); + expect(res.statusCode).toBe(200); + return res.json() as HealthBody; +} + +/** Contract: every 4xx/5xx body is RFC 9457 application/problem+json. */ +function expectProblem(res: LightMyRequestResponse, status: number, title: string): void { + expect(res.statusCode).toBe(status); + expect(res.headers['content-type']).toMatch(/^application\/problem\+json/); + const body = res.json() as { title: string; status: number; detail?: string }; + expect(body.title).toBe(title); + expect(body.status).toBe(status); +} + +afterEach(async () => { + await Promise.all(apps.map((app) => app.close())); + apps = []; +}); + +describe('GET /health', () => { + it('returns 200 with status, version, uptime, and arena session state — no auth required', async () => { + const { app } = makeApp(); + const body = await getHealth(app); + + expect(body.status).toBe('ok'); + expect(typeof body.version).toBe('string'); + expect(body.version).not.toBe('0.0.0'); + expect(typeof body.uptimeSeconds).toBe('number'); + expect(body.arenaSession).toEqual({ state: 'none' }); + // Contract: `poller` is absent until the poller has ticked at least once. + expect(body).not.toHaveProperty('poller'); + }); + + describe('arenaSession.state pins every contract enum value', () => { + it('"none" before any login', async () => { + const { app } = makeApp(); + expect((await getHealth(app)).arenaSession.state).toBe('none'); + }); + + it('"active" while the cached session is within its TTL', async () => { + const { app, arenaSession, request } = makeApp(); + request.mockResolvedValueOnce(loginOk('sess-1')); + await arenaSession.getSessionId(); + expect((await getHealth(app)).arenaSession.state).toBe('active'); + }); + + it('"expired" once the cached session is past its TTL', async () => { + const { app, arenaSession, request, clock, config } = makeApp(); + request.mockResolvedValueOnce(loginOk('sess-1')); + await arenaSession.getSessionId(); + clock.nowMs += config.arena.sessionTtlMs; + expect((await getHealth(app)).arenaSession.state).toBe('expired'); + }); + + it('"refreshing" while a login is in flight', async () => { + const { app, arenaSession, request } = makeApp(); + let resolveLogin!: (res: HttpResponse) => void; + request.mockImplementationOnce( + () => + new Promise((resolve) => { + resolveLogin = resolve; + }), + ); + const pending = arenaSession.getSessionId(); + expect((await getHealth(app)).arenaSession.state).toBe('refreshing'); + + resolveLogin(loginOk('sess-1')); + await pending; + expect((await getHealth(app)).arenaSession.state).toBe('active'); + }); + }); + + describe('status pins both contract enum values', () => { + it('"degraded" after a failed Arena login, "ok" again after a successful one', async () => { + const { app, arenaSession, request } = makeApp(); + request.mockResolvedValueOnce({ status: 503, body: 'down' }); + await expect(arenaSession.getSessionId()).rejects.toBeInstanceOf(ArenaApiError); + expect((await getHealth(app)).status).toBe('degraded'); + + request.mockResolvedValueOnce(loginOk('sess-1')); + await arenaSession.getSessionId(); + expect((await getHealth(app)).status).toBe('ok'); + }); + + it('"degraded" after a failed poll tick with the poller object populated, "ok" after a successful one', async () => { + const { app, health } = makeApp(); + const t1 = new Date('2026-09-14T08:15:00.000Z'); + health.recordPollFailure(t1, new ArenaApiError('Arena login failed with HTTP 503', 503, 'down')); + + const degraded = await getHealth(app); + expect(degraded.status).toBe('degraded'); + expect(degraded.poller).toEqual({ + lastRunAt: '2026-09-14T08:15:00.000Z', + lastError: 'Upstream Arena PLM request failed (HTTP 503).', + }); + + const t2 = new Date('2026-09-14T08:20:00.000Z'); + health.recordPollSuccess(t2); + const ok = await getHealth(app); + expect(ok.status).toBe('ok'); + expect(ok.poller).toEqual({ + lastRunAt: '2026-09-14T08:20:00.000Z', + lastSuccessAt: '2026-09-14T08:20:00.000Z', + }); + }); + + it('a failed tick keeps the previous lastSuccessAt', async () => { + const { app, health } = makeApp(); + health.recordPollSuccess(new Date('2026-09-14T08:15:00.000Z')); + health.recordPollFailure(new Date('2026-09-14T08:20:00.000Z'), 'boom'); + const body = await getHealth(app); + expect(body.status).toBe('degraded'); + expect(body.poller).toEqual({ + lastRunAt: '2026-09-14T08:20:00.000Z', + lastSuccessAt: '2026-09-14T08:15:00.000Z', + lastError: UNCLASSIFIED_ERROR_PHRASE, + }); + }); + + it('B5: lastError is a fixed phrase — an ArenaApiError body/message marker never reaches /health', async () => { + const { app, health } = makeApp(); + health.recordPollFailure( + new Date('2026-09-14T08:20:00.000Z'), + new ArenaApiError('Arena login failed with HTTP 429 MARKER-MESSAGE', 429, { + errors: [{ message: 'MARKER-BODY worker@example.test' }], + }), + ); + const res = await app.inject({ method: 'GET', url: '/health' }); + expect(res.statusCode).toBe(200); + expect(res.body).not.toContain('MARKER-MESSAGE'); + expect(res.body).not.toContain('MARKER-BODY'); + expect(res.body).not.toContain('worker@example.test'); + expect((res.json() as HealthBody).poller?.lastError).toBe('Upstream Arena PLM request failed (HTTP 429).'); + }); + + it('B5: a fetch TimeoutError maps to the timeout phrase', async () => { + const { app, health } = makeApp(); + health.recordPollFailure(new Date(), new DOMException('The operation was aborted due to timeout', 'TimeoutError')); + expect((await getHealth(app)).poller?.lastError).toBe('Upstream Arena PLM request timed out.'); + }); + }); +}); + +describe('POST /webhooks/arena', () => { + it('rejects requests without a bearer token with 401 problem+json', async () => { + const { app, queue } = makeApp(); + const res = await app.inject({ + method: 'POST', + url: '/webhooks/arena', + payload: { event: 'ITEM.UPDATED' }, + }); + + expectProblem(res, 401, 'Unauthorized'); + expect(queue.size).toBe(0); + }); + + it('rejects requests with a wrong token with 401 problem+json', async () => { + const { app, queue } = makeApp(); + const res = await app.inject({ + method: 'POST', + url: '/webhooks/arena', + headers: { authorization: 'Bearer wrong-token' }, + payload: { event: 'ITEM.UPDATED' }, + }); + + expectProblem(res, 401, 'Unauthorized'); + expect(queue.size).toBe(0); + }); + + it('accepts an authenticated JSON event with 202 and enqueues the raw payload', async () => { + const { app, queue } = makeApp(); + const payload = { event: 'ITEM.UPDATED', itemNumber: '900-00001' }; + const res = await app.inject({ + method: 'POST', + url: '/webhooks/arena', + headers: AUTH, + payload, + }); + + expect(res.statusCode).toBe(202); + expect(res.json()).toEqual({ received: true }); + expect(queue.size).toBe(1); + const queued = queue.peekAll()[0]; + expect(queued?.source).toBe('webhook'); + expect(queued?.payload).toEqual(payload); + expect(queued?.id).toMatch(UUID_RE); + }); + + it('answers 503 problem+json with Retry-After: 30 when the ingest buffer is full, without queuing', async () => { + const { app, queue } = makeApp({ WEBHOOK_QUEUE_MAX_SIZE: '1' }); + const first = await app.inject({ method: 'POST', url: '/webhooks/arena', headers: AUTH, payload: { n: 1 } }); + expect(first.statusCode).toBe(202); + + const second = await app.inject({ method: 'POST', url: '/webhooks/arena', headers: AUTH, payload: { n: 2 } }); + expectProblem(second, 503, 'Service Unavailable'); + expect(second.headers['retry-after']).toBe('30'); + expect((second.json() as { detail?: string }).detail).toMatch(/queue is full/i); + expect(queue.size).toBe(1); + expect(queue.peekAll()[0]?.payload).toEqual({ n: 1 }); + + // Backpressure clears once the buffer drains. + queue.dequeue(); + const third = await app.inject({ method: 'POST', url: '/webhooks/arena', headers: AUTH, payload: { n: 3 } }); + expect(third.statusCode).toBe(202); + }); + + it('rejects invalid JSON with 400 problem+json', async () => { + const { app, queue } = makeApp(); + const res = await app.inject({ + method: 'POST', + url: '/webhooks/arena', + headers: { ...AUTH, 'content-type': 'application/json' }, + payload: '{not-json', + }); + + expectProblem(res, 400, 'Bad Request'); + expect(queue.size).toBe(0); + }); + + it('rejects non-object JSON bodies with 400 problem+json', async () => { + const { app, queue } = makeApp(); + const res = await app.inject({ + method: 'POST', + url: '/webhooks/arena', + headers: { ...AUTH, 'content-type': 'application/json' }, + payload: '[1,2,3]', + }); + + expectProblem(res, 400, 'Bad Request'); + expect(queue.size).toBe(0); + }); + + it('fails closed with 401 when no token is configured', async () => { + const { app } = makeApp({ WORKER_BEARER_TOKEN: '' }); + const res = await app.inject({ + method: 'POST', + url: '/webhooks/arena', + headers: AUTH, + payload: { event: 'ITEM.UPDATED' }, + }); + + expectProblem(res, 401, 'Unauthorized'); + }); +}); + +describe('POST /admin/sync-runs', () => { + it('requires the bearer token', async () => { + const { app } = makeApp(); + const res = await app.inject({ method: 'POST', url: '/admin/sync-runs', payload: {} }); + expectProblem(res, 401, 'Unauthorized'); + }); + + it('creates a queued run with the config-default object order when objects is absent', async () => { + const { app } = makeApp(); + const res = await app.inject({ + method: 'POST', + url: '/admin/sync-runs', + headers: AUTH, + payload: { scope: 'delta' }, + }); + + expect(res.statusCode).toBe(201); + const run = res.json() as SyncRun; + expect(run.runId).toMatch(UUID_RE); + expect(run.scope).toBe('delta'); + expect(run.state).toBe('queued'); + // Order is carried by the objects array itself; no objectOrder on the wire. + expect(run).not.toHaveProperty('objectOrder'); + expect(run.objects.map((o) => o.objectType)).toEqual(['PRODUCT', 'CHANGE_MASTER', 'BOM', 'DOCUMENTS']); + expect(run.objects.every((o) => o.state === 'pending')).toBe(true); + // B4: no `processed` counter until the object has run. + expect(run.objects.every((o) => !('processed' in o))).toBe(true); + expect(Number.isNaN(Date.parse(run.startedAt))).toBe(false); + }); + + it('rejects a missing scope with 400 problem+json (scope is required)', async () => { + const { app } = makeApp(); + const res = await app.inject({ + method: 'POST', + url: '/admin/sync-runs', + headers: AUTH, + payload: {}, + }); + + expectProblem(res, 400, 'Bad Request'); + expect((res.json() as { detail?: string }).detail).toMatch(/scope/); + }); + + it('accepts an explicit full scope and rejects unknown scopes with 400 problem+json', async () => { + const { app } = makeApp(); + + const ok = await app.inject({ + method: 'POST', + url: '/admin/sync-runs', + headers: AUTH, + payload: { scope: 'full' }, + }); + expect(ok.statusCode).toBe(201); + expect((ok.json() as SyncRun).scope).toBe('full'); + + const bad = await app.inject({ + method: 'POST', + url: '/admin/sync-runs', + headers: AUTH, + payload: { scope: 'bogus' }, + }); + expectProblem(bad, 400, 'Bad Request'); + }); + + it('lets the request objects array select AND order the object types for the run', async () => { + const { app } = makeApp(); + const res = await app.inject({ + method: 'POST', + url: '/admin/sync-runs', + headers: AUTH, + payload: { scope: 'full', objects: ['BOM', 'PRODUCT'] }, + }); + + expect(res.statusCode).toBe(201); + const run = res.json() as SyncRun; + expect(run.objects.map((o) => o.objectType)).toEqual(['BOM', 'PRODUCT']); + }); + + it('rejects empty, duplicate, or unknown objects entries with 400 problem+json', async () => { + const { app } = makeApp(); + + const empty = await app.inject({ + method: 'POST', + url: '/admin/sync-runs', + headers: AUTH, + payload: { scope: 'full', objects: [] }, + }); + expectProblem(empty, 400, 'Bad Request'); + + const dupes = await app.inject({ + method: 'POST', + url: '/admin/sync-runs', + headers: AUTH, + payload: { scope: 'full', objects: ['BOM', 'BOM'] }, + }); + expectProblem(dupes, 400, 'Bad Request'); + + const unknown = await app.inject({ + method: 'POST', + url: '/admin/sync-runs', + headers: AUTH, + payload: { scope: 'full', objects: ['WIDGET'] }, + }); + expectProblem(unknown, 400, 'Bad Request'); + }); + + describe('strict validation (no coercion, no stripping, no default injection)', () => { + it('rejects an unknown body property with 400 problem+json (no silent stripping)', async () => { + const { app, runStore } = makeApp(); + // Typo'd `object` instead of `objects` must NOT silently fall back to the + // default order — additionalProperties: false means 400 (seam ruling). + const res = await app.inject({ + method: 'POST', + url: '/admin/sync-runs', + headers: AUTH, + payload: { scope: 'full', object: ['BOM'] }, + }); + + expectProblem(res, 400, 'Bad Request'); + expect((res.json() as { detail?: string }).detail).toMatch(/additional propert/); + // Nothing was created for the rejected request. + expect(runStore.get('__none__')).toBeUndefined(); + }); + + it('rejects {"scope":"full","x":1} with 400 problem+json', async () => { + const { app } = makeApp(); + const res = await app.inject({ + method: 'POST', + url: '/admin/sync-runs', + headers: AUTH, + payload: { scope: 'full', x: 1 }, + }); + expectProblem(res, 400, 'Bad Request'); + }); + + it('rejects {"scope":["full"]} with 400 — a one-element array is NOT unwrapped to a string', async () => { + const { app } = makeApp(); + const res = await app.inject({ + method: 'POST', + url: '/admin/sync-runs', + headers: AUTH, + payload: { scope: ['full'] }, + }); + expectProblem(res, 400, 'Bad Request'); + expect((res.json() as { detail?: string }).detail).toMatch(/scope/); + }); + + it('rejects {"scope":"full","objects":"BOM"} with 400 — a scalar is NOT wrapped into an array', async () => { + const { app } = makeApp(); + const res = await app.inject({ + method: 'POST', + url: '/admin/sync-runs', + headers: AUTH, + payload: { scope: 'full', objects: 'BOM' }, + }); + expectProblem(res, 400, 'Bad Request'); + expect((res.json() as { detail?: string }).detail).toMatch(/objects/); + }); + }); + + it('honors a config-driven sync object order when the request omits objects (ARN-001)', async () => { + const { app } = makeApp({ SYNC_OBJECT_ORDER: 'bom, product' }); + const res = await app.inject({ + method: 'POST', + url: '/admin/sync-runs', + headers: AUTH, + payload: { scope: 'delta' }, + }); + + expect(res.statusCode).toBe(201); + expect((res.json() as SyncRun).objects.map((o) => o.objectType)).toEqual(['BOM', 'PRODUCT']); + }); +}); + +describe('GET /admin/sync-runs/:runId', () => { + it('requires the bearer token', async () => { + const { app } = makeApp(); + const res = await app.inject({ method: 'GET', url: '/admin/sync-runs/whatever' }); + expectProblem(res, 401, 'Unauthorized'); + }); + + it('returns 404 problem+json for an unknown run id', async () => { + const { app } = makeApp(); + const res = await app.inject({ + method: 'GET', + url: '/admin/sync-runs/00000000-0000-4000-8000-000000000000', + headers: AUTH, + }); + + expectProblem(res, 404, 'Not Found'); + expect((res.json() as { detail?: string }).detail).toMatch(/00000000-0000-4000-8000-000000000000/); + }); + + it('returns the run after creation and reflects completion by the no-op executors', async () => { + const { app, runner } = makeApp(); + const created = await app.inject({ + method: 'POST', + url: '/admin/sync-runs', + headers: AUTH, + payload: { scope: 'delta' }, + }); + const { runId } = created.json() as SyncRun; + + await runner.waitFor(runId); + + const res = await app.inject({ method: 'GET', url: `/admin/sync-runs/${runId}`, headers: AUTH }); + expect(res.statusCode).toBe(200); + const run = res.json() as SyncRun; + expect(run.runId).toBe(runId); + expect(run.state).toBe('completed'); + expect(run).not.toHaveProperty('objectOrder'); + expect(run.objects.every((o) => o.state === 'completed')).toBe(true); + expect(run.finishedAt).toBeDefined(); + }); +}); + +describe('error handler', () => { + it('maps an ArenaApiError to 502 Bad Gateway problem+json without leaking the upstream body', async () => { + const { app } = makeApp(); + // No production route calls Arena synchronously yet (contract: reserved + // BadGateway mapping); a throwaway route exercises the handler seam. + app.get('/__test/upstream', async () => { + throw new ArenaApiError('Arena login failed with HTTP 500', 500, { + errors: [{ message: 'LEAKED-UPSTREAM-BODY worker@example.test' }], + }); + }); + + const res = await app.inject({ method: 'GET', url: '/__test/upstream' }); + + expectProblem(res, 502, 'Bad Gateway'); + expect((res.json() as { detail?: string }).detail).toBe('Upstream Arena PLM request failed.'); + expect(res.body).not.toContain('LEAKED-UPSTREAM-BODY'); + expect(res.body).not.toContain('HTTP 500'); + }); + + it('still maps unknown errors to 500 problem+json without details', async () => { + const { app } = makeApp(); + app.get('/__test/boom', async () => { + throw new Error('internal secret detail'); + }); + + const res = await app.inject({ method: 'GET', url: '/__test/boom' }); + expectProblem(res, 500, 'Internal Server Error'); + expect(res.body).not.toContain('internal secret detail'); + }); +}); diff --git a/worker/test/session.test.ts b/worker/test/session.test.ts new file mode 100644 index 0000000..b2f6f45 --- /dev/null +++ b/worker/test/session.test.ts @@ -0,0 +1,232 @@ +import { describe, expect, it, vi } from 'vitest'; +import { ArenaApiError, ArenaSessionManager } from '../src/arena/session.js'; +import { loadConfig } from '../src/config.js'; +import { HealthRegistry, type LoginHealthSink } from '../src/health/registry.js'; +import type { HttpClient, HttpRequest, HttpResponse } from '../src/http/types.js'; + +function makeManager(overrides?: { + sessionTtlMs?: number; + now?: () => number; + health?: LoginHealthSink; +}): { manager: ArenaSessionManager; request: ReturnType } { + const request = vi.fn(); + const http: HttpClient = { request: request as HttpClient['request'] }; + const manager = new ArenaSessionManager({ + http, + baseUrl: 'https://arena.invalid/v1', + email: 'worker@example.test', + password: 'secret', + workspaceId: 123, + sessionTtlMs: 24 * 60_000, + ...overrides, + }); + return { manager, request }; +} + +function loginOk(sessionId: string): HttpResponse { + return { status: 200, body: { arenaSessionId: sessionId } }; +} + +describe('ArenaSessionManager', () => { + it('coalesces concurrent callers onto a single in-flight login', async () => { + const { manager, request } = makeManager(); + let resolveLogin!: (res: HttpResponse) => void; + request.mockImplementationOnce( + () => + new Promise((resolve) => { + resolveLogin = resolve; + }), + ); + + const pending = Promise.all([manager.getSessionId(), manager.getSessionId(), manager.getSessionId()]); + resolveLogin(loginOk('sess-1')); + const ids = await pending; + + expect(ids).toEqual(['sess-1', 'sess-1', 'sess-1']); + expect(request).toHaveBeenCalledTimes(1); + const loginReq = request.mock.calls[0]?.[0] as HttpRequest; + expect(loginReq.method).toBe('POST'); + expect(loginReq.url).toBe('https://arena.invalid/v1/login'); + expect(loginReq.body).toEqual({ + email: 'worker@example.test', + password: 'secret', + workspaceId: 123, + }); + }); + + it('reuses the cached session until the TTL and re-logs in after expiry', async () => { + let nowMs = 0; + const sessionTtlMs = 24 * 60_000; + const { manager, request } = makeManager({ sessionTtlMs, now: () => nowMs }); + + request.mockResolvedValueOnce(loginOk('sess-1')); + expect(manager.state()).toBe('none'); + expect(await manager.getSessionId()).toBe('sess-1'); + expect(await manager.getSessionId()).toBe('sess-1'); + expect(request).toHaveBeenCalledTimes(1); + expect(manager.state()).toBe('active'); + + nowMs = sessionTtlMs + 1; + expect(manager.state()).toBe('expired'); + request.mockResolvedValueOnce(loginOk('sess-2')); + expect(await manager.getSessionId()).toBe('sess-2'); + expect(request).toHaveBeenCalledTimes(2); + expect(manager.state()).toBe('active'); + }); + + it('wires ARENA_SESSION_TTL_MINUTES through loadConfig to the effective TTL (was silently dropped in 0.1.0)', async () => { + const config = loadConfig({ ARENA_SESSION_TTL_MINUTES: '1' }); + let nowMs = 0; + const request = vi.fn().mockResolvedValue(loginOk('sess-1')); + // Same spread the composition root uses: ArenaSessionOptions is a Pick of + // ArenaConfig, so a field-name drift would fail to compile here. + const { httpTimeoutMs: _httpTimeoutMs, ...sessionConfig } = config.arena; + const manager = new ArenaSessionManager({ + http: { request: request as HttpClient['request'] }, + now: () => nowMs, + ...sessionConfig, + }); + + expect(manager.sessionTtlMs).toBe(60_000); + + await manager.getSessionId(); + nowMs = 59_999; + expect(manager.state()).toBe('active'); + nowMs = 60_000; + expect(manager.state()).toBe('expired'); + }); + + it('rejects a non-positive sessionTtlMs at construction', () => { + expect(() => makeManager({ sessionTtlMs: 0 })).toThrow(/sessionTtlMs/); + }); + + it('reports state "refreshing" while a login is in flight, then "active"', async () => { + const { manager, request } = makeManager(); + let resolveLogin!: (res: HttpResponse) => void; + request.mockImplementationOnce( + () => + new Promise((resolve) => { + resolveLogin = resolve; + }), + ); + + expect(manager.state()).toBe('none'); + const pending = manager.getSessionId(); + expect(manager.state()).toBe('refreshing'); + + resolveLogin(loginOk('sess-1')); + await pending; + expect(manager.state()).toBe('active'); + }); + + it('reports "refreshing" during a re-login after expiry (refreshing wins over expired)', async () => { + let nowMs = 0; + const { manager, request } = makeManager({ sessionTtlMs: 1000, now: () => nowMs }); + request.mockResolvedValueOnce(loginOk('sess-1')); + await manager.getSessionId(); + + nowMs = 5000; + expect(manager.state()).toBe('expired'); + let resolveLogin!: (res: HttpResponse) => void; + request.mockImplementationOnce( + () => + new Promise((resolve) => { + resolveLogin = resolve; + }), + ); + const pending = manager.getSessionId(); + expect(manager.state()).toBe('refreshing'); + resolveLogin(loginOk('sess-2')); + await pending; + expect(manager.state()).toBe('active'); + }); + + it('reports login outcomes to the health sink (degraded on failure, ok again on success)', async () => { + const health = new HealthRegistry(); + const { manager, request } = makeManager({ health }); + + expect(health.status()).toBe('ok'); + request.mockResolvedValueOnce({ status: 503, body: 'upstream down' }); + await expect(manager.getSessionId()).rejects.toBeInstanceOf(ArenaApiError); + expect(health.status()).toBe('degraded'); + + request.mockResolvedValueOnce(loginOk('sess-1')); + await manager.getSessionId(); + expect(health.status()).toBe('ok'); + }); + + it('reports a transport error (network/timeout) as a login failure too', async () => { + const health = new HealthRegistry(); + const { manager, request } = makeManager({ health }); + request.mockRejectedValueOnce(new Error('TimeoutError')); + + await expect(manager.getSessionId()).rejects.toThrow('TimeoutError'); + expect(health.status()).toBe('degraded'); + expect(manager.state()).toBe('none'); + }); + + it('withSession re-logs in and retries exactly once on 401', async () => { + const { manager, request } = makeManager(); + request.mockResolvedValueOnce(loginOk('sess-1')).mockResolvedValueOnce(loginOk('sess-2')); + + const fn = vi + .fn() + .mockRejectedValueOnce(new ArenaApiError('unauthorized', 401)) + .mockResolvedValueOnce('ok'); + + const result = await manager.withSession(fn as (sessionId: string) => Promise); + + expect(result).toBe('ok'); + expect(fn.mock.calls.map((call) => call[0])).toEqual(['sess-1', 'sess-2']); + expect(request).toHaveBeenCalledTimes(2); + }); + + it('withSession does not retry more than once on repeated 401', async () => { + const { manager, request } = makeManager(); + request.mockResolvedValueOnce(loginOk('sess-1')).mockResolvedValueOnce(loginOk('sess-2')); + + const fn = vi.fn().mockRejectedValue(new ArenaApiError('unauthorized', 401)); + + await expect(manager.withSession(fn as (sessionId: string) => Promise)).rejects.toBeInstanceOf( + ArenaApiError, + ); + expect(fn).toHaveBeenCalledTimes(2); + expect(request).toHaveBeenCalledTimes(2); + }); + + it('withSession propagates non-401 errors without re-login', async () => { + const { manager, request } = makeManager(); + request.mockResolvedValueOnce(loginOk('sess-1')); + + const fn = vi.fn().mockRejectedValue(new ArenaApiError('server error', 500)); + + await expect(manager.withSession(fn as (sessionId: string) => Promise)).rejects.toThrow( + 'server error', + ); + expect(fn).toHaveBeenCalledTimes(1); + expect(request).toHaveBeenCalledTimes(1); + }); + + it('rejects all coalesced callers on login failure and allows a later retry', async () => { + const { manager, request } = makeManager(); + request.mockResolvedValueOnce({ status: 401, body: { errors: [{ message: 'bad creds' }] } }); + + const results = await Promise.allSettled([manager.getSessionId(), manager.getSessionId()]); + expect(results.map((r) => r.status)).toEqual(['rejected', 'rejected']); + expect(request).toHaveBeenCalledTimes(1); + + // The promise lock must clear so a subsequent call can log in again. + expect(manager.state()).toBe('none'); + request.mockResolvedValueOnce(loginOk('sess-1')); + expect(await manager.getSessionId()).toBe('sess-1'); + expect(request).toHaveBeenCalledTimes(2); + }); + + it('rejects a login response without arenaSessionId', async () => { + const { manager, request } = makeManager(); + request.mockResolvedValueOnce({ status: 200, body: {} }); + + await expect(manager.getSessionId()).rejects.toThrow('no arenaSessionId'); + expect(manager.state()).toBe('none'); + }); +}); diff --git a/worker/test/shutdown.test.ts b/worker/test/shutdown.test.ts new file mode 100644 index 0000000..354a83e --- /dev/null +++ b/worker/test/shutdown.test.ts @@ -0,0 +1,94 @@ +import Fastify from 'fastify'; +import { afterEach, describe, expect, it, vi } from 'vitest'; +import { drainWithTimeout, registerGracefulShutdown } from '../src/shutdown.js'; + +function deferred() { + let resolve!: (value: T) => void; + const promise = new Promise((r) => { + resolve = r; + }); + return { promise, resolve }; +} + +describe('drainWithTimeout', () => { + afterEach(() => { + vi.useRealTimers(); + }); + + it('resolves "drained" when the work settles first and clears its timer', async () => { + vi.useFakeTimers(); + const work = deferred(); + const outcome = drainWithTimeout(work.promise, 30_000); + work.resolve(); + await expect(outcome).resolves.toBe('drained'); + expect(vi.getTimerCount()).toBe(0); + }); + + it('resolves "timeout" when the deadline wins', async () => { + vi.useFakeTimers(); + const never = new Promise(() => undefined); + const outcome = drainWithTimeout(never, 30_000); + await vi.advanceTimersByTimeAsync(30_000); + await expect(outcome).resolves.toBe('timeout'); + }); +}); + +describe('registerGracefulShutdown', () => { + it('on signal: stops the poller, drains the runner inside app.close(), and exits 0', async () => { + const app = Fastify({ logger: false }); + const drain = deferred(); + const stop = deferred(); + const runner = { drain: vi.fn(() => drain.promise) }; + const poller = { stop: vi.fn(() => stop.promise) }; + const exited = deferred(); + const shutdown = registerGracefulShutdown({ app, poller, runner, timeoutMs: 5000, exit: exited.resolve }); + await app.ready(); + + shutdown.onSignal('SIGTERM'); + // Wait for the onClose hook to actually start the drain. + await vi.waitFor(() => expect(runner.drain).toHaveBeenCalledTimes(1)); + expect(poller.stop).toHaveBeenCalledTimes(1); + + // Exit must wait for BOTH the runner drain and the in-flight poll tick. + let exitCode: number | undefined; + void exited.promise.then((code) => { + exitCode = code; + }); + drain.resolve(); + await new Promise((r) => setImmediate(r)); + expect(exitCode).toBeUndefined(); + + stop.resolve(); + expect(await exited.promise).toBe(0); + expect(shutdown.timedOut).toBe(false); + }); + + it('exits 1 when in-flight work does not settle within the timeout', async () => { + const app = Fastify({ logger: false }); + const runner = { drain: () => new Promise(() => undefined) }; // never settles + const poller = { stop: () => Promise.resolve() }; + const exited = deferred(); + const shutdown = registerGracefulShutdown({ app, poller, runner, timeoutMs: 50, exit: exited.resolve }); + await app.ready(); + + shutdown.onSignal('SIGINT'); + expect(await exited.promise).toBe(1); + expect(shutdown.timedOut).toBe(true); + }); + + it('exits 1 when close() itself rejects', async () => { + const app = Fastify({ logger: false }); + const exited = deferred(); + const shutdown = registerGracefulShutdown({ + app, + poller: { stop: () => Promise.resolve() }, + runner: { drain: () => Promise.reject(new Error('drain exploded')) }, + timeoutMs: 5000, + exit: exited.resolve, + }); + await app.ready(); + + shutdown.onSignal('SIGTERM'); + expect(await exited.promise).toBe(1); + }); +}); diff --git a/worker/tsconfig.build.json b/worker/tsconfig.build.json new file mode 100644 index 0000000..469ac55 --- /dev/null +++ b/worker/tsconfig.build.json @@ -0,0 +1,9 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "noEmit": false, + "outDir": "dist", + "rootDir": "src" + }, + "include": ["src"] +} diff --git a/worker/tsconfig.json b/worker/tsconfig.json new file mode 100644 index 0000000..fca134b --- /dev/null +++ b/worker/tsconfig.json @@ -0,0 +1,17 @@ +{ + "compilerOptions": { + "target": "es2023", + "lib": ["es2023"], + "module": "nodenext", + "moduleResolution": "nodenext", + "strict": true, + "noUncheckedIndexedAccess": true, + "verbatimModuleSyntax": true, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "skipLibCheck": true, + "sourceMap": true, + "noEmit": true + }, + "include": ["src", "test", "vitest.config.ts"] +} diff --git a/worker/vitest.config.ts b/worker/vitest.config.ts new file mode 100644 index 0000000..a71174d --- /dev/null +++ b/worker/vitest.config.ts @@ -0,0 +1,8 @@ +import { defineConfig } from 'vitest/config'; + +export default defineConfig({ + test: { + environment: 'node', + include: ['test/**/*.test.ts'], + }, +});