diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..469feb3 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,14 @@ +.git +.github +.vscode +node_modules +dist +npm-debug.log* +*.tar +.env +.env.* +local.settings.json +web-integrations-template-docker-image.tar +README.md +AGENTS.md +deploy diff --git a/.editorconfig b/.editorconfig index 8919159..68a969e 100644 --- a/.editorconfig +++ b/.editorconfig @@ -1,14 +1,12 @@ -# .editorconfig root = true [*] -indent_style = space -indent_size = 4 -end_of_line = lf charset = utf-8 -trim_trailing_whitespace = true +end_of_line = lf +indent_size = 4 +indent_style = space insert_final_newline = true -max_line_length = 200 +trim_trailing_whitespace = true -[*.js] -quote_type = double +[*.{yml,yaml}] +indent_size = 2 diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..850830b --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,13 @@ +version: 2 +updates: + - package-ecosystem: npm + directory: / + schedule: + interval: weekly + open-pull-requests-limit: 10 + + - package-ecosystem: github-actions + directory: / + schedule: + interval: weekly + open-pull-requests-limit: 10 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..51c7f9b --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,25 @@ +name: CI + +on: + pull_request: + push: + branches: [main] + +jobs: + typecheck: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v5 + + - name: Setup Node.js + uses: actions/setup-node@v6 + with: + node-version: '24' + cache: 'npm' + + - name: Install dependencies + run: npm ci + + - name: Typecheck + run: npm run typecheck diff --git a/.npmrc b/.npmrc new file mode 100644 index 0000000..a30514d --- /dev/null +++ b/.npmrc @@ -0,0 +1,3 @@ +audit=true +fund=false +package-lock=true diff --git a/.nvmrc b/.nvmrc deleted file mode 100644 index b714151..0000000 --- a/.nvmrc +++ /dev/null @@ -1 +0,0 @@ -v18.18.0 \ No newline at end of file diff --git a/.vscode/launch.json b/.vscode/launch.json index 8f07250..e4f038d 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -5,13 +5,10 @@ "type": "node", "request": "launch", "name": "Launch Program", - "runtimeArgs": [ - "-r", - "ts-node/register" - ], + "runtimeArgs": [], "args": [ "${workspaceFolder}/src/server.ts" ], } ] -} \ No newline at end of file +} diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..d5720c8 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,81 @@ +# AGENTS.md + +## Runtime +- Self-hosted, Azure-Functions-style integrations runtime. Small Node + Express service, runs directly via native Node TypeScript stripping; no compile step. +- Main entrypoint is `src/server.ts`; `npm start` runs `node ./src/server.ts`, and `npm run dev` runs `node --watch ./src/server.ts` for local restarts. +- The source tree separates runtime from user code by folder name: + - `src/runtime/` - framework: registry, scheduler, openapi, auth, log, settings. Don't normally touch. + - `src/actions/` - your action handlers. `sample-*.ts` are starters; `index.ts` is the registration sink. + - `src/helpers/` - your shared helpers. `sample-upload-blob.ts` is the starter example. + - `src/config/windows-service.ts` - Windows service name + node-windows definition. + - `src/views/` - EJS templates. `src/public/` - static assets. + - `src/server.ts` - entrypoint, rarely needs editing. +- Imports across the boundary are visible: action files do `import { defineAction } from "../runtime/registry.ts"`. +- Frontend uses hand-rolled `src/public/styles.css` with Clear Path Research brand tokens (warm grays, coral accent, OKLCH palette, dark/light via `prefers-color-scheme` + `.light` class). Poppins + JetBrains Mono are loaded from Google Fonts. No CSS framework; no JS framework. Each action card has an inline output panel - clicking Run reveals it and streams in place. No modal/dialog. +- A short README lives at `src/actions/README.md` for users opening a freshly cloned template. + +## Actions +- Integration handlers live under `src/actions/` and register themselves with `defineAction(...)` from `src/actions/registry.ts`. Routes (`/action/:name`) and the index page are auto-driven by the registry. Add a new action by dropping a file in `src/actions/` and importing it from `src/actions/index.ts`. +- Positioning: this is a **bridge-product** for the messy middle of cloud migration. The action signature is deliberately close to Azure Functions v4 so handlers port with cosmetic changes once on-prem constraints clear. README's intro and "When you'd / when you'd *not*" sections carry this framing. Don't reposition it as a destination runtime. +- An action takes one of three forms, all supporting optional `timeoutMs`, `onSuccess(result, ctx)`, `schedule` (cron), `method` (`"GET"` default or `"POST"`), and `auth` level: + - **handler**: `handler: (ctx) => result` - one-shot request/response. Result is JSON-wrapped as `{ action, result }`. + - **steps**: `steps: ["a", "b"]` - sequence of other registered actions, run in order. Streamed over HTTP as NDJSON, one line per step transition (`{ step, status: "started" | "done" | "failed" | "progress", result?, error?, at }`). On first failure the sequence stops. Sequences can contain stream actions; their yields are wrapped as `progress` events. + - **stream**: `stream: async function*(ctx) { yield ... }` - async generator. Each yield becomes one NDJSON line. `timeoutMs` here means "max idle between yields". The generator's `return` value is passed to `onSuccess`. +- `ctx` is a discriminated union of `HttpContext` (`trigger: "http"`, has `req`/`res`) and `ScheduleContext` (`trigger: "schedule"`, no `req`/`res`). Use `httpCtx(ctx)` from `runtime/registry.ts` to narrow when you need `req`/`res` (e.g. POST actions reading the body) - it throws if invoked from the scheduler. +- Migration-ease fields on both contexts: `ctx.log("...")` (with `.info`/`.warn`/`.error` methods) mirrors Azure Functions `context.log`. `ctx.invocationId` is an alias for `ctx.requestId` matching Functions naming. Handlers using these are syntactically closer to a Functions handler body. +- Return-shape compatibility: a handler may return `httpResponse({ status, jsonBody, headers, body })` (or a duck-typed object with those fields). The route emits the response init directly instead of wrapping. Plain return values still work and get wrapped as `{ action, result }`. `httpResponse` is identity at runtime; it's just a typed helper. +- Cron schedules accept both 5-field cron and 6-field NCRONTAB (`{second minute hour day month weekday}`). Functions-style schedules like `"0 */15 * * * *"` work and stay portable to Azure Functions Timer triggers. +- POST actions: set `method: "POST"`. The runtime returns 405 if the wrong verb is used. JSON bodies are parsed up to 1MB via the global `express.json` middleware; access via `httpCtx(ctx).req.body`. +- Example recipes live in `src/actions/examples/` (inert by default - typechecked, not auto-registered). Cover SQL Server → blob, inbound webhook with HMAC verify, and SharePoint list sync via Graph + msal-node. Users copy / import to activate. +- The bundled `src/public/app.js` content-type-sniffs the response: handler results render as JSON, sequences and streams render line-by-line into the card's inline output panel as they arrive. +- Handlers should be **idempotent**: running the same action twice should leave the system in the same end state. Schedulers retry on failure and humans double-click buttons. +- An observability-init comment block at the top of `src/server.ts` marks where to import a tracer (New Relic, OTel, Datadog) so it can patch modules before they load. + +## API docs +- `GET /openapi.json` - auto-generated OpenAPI 3.0 spec built from `getActions()`. Each action becomes a path entry tagged by kind (`handler` / `sequence` / `stream` / `scheduled`), with per-action security mapped from `action.auth`. +- `GET /docs` - Swagger UI page loading the spec. Helmet's CSP is widened just enough to allow Swagger UI assets from `cdnjs.cloudflare.com`. +- Both routes are gated by the global auth middleware. Update `src/openapi.ts` to add response schemas per action when callers need richer docs. + +## Triggering actions +Two ways, use either or both: +- **External (recommended for visibility)**: Windows Task Scheduler / cron / Azure App Proxy webhooks hit `/action/:name` over HTTP. An on-prem admin sees scheduled jobs where they expect to find them, in Task Scheduler. +- **Internal**: set `schedule: "*/15 * * * *"` on the action definition. `src/scheduler.ts` registers cron jobs at boot via `node-cron`. Stream actions can't be scheduled internally (they need a Response to write to); use HTTP for those. +The same action is callable both ways simultaneously - define once, trigger from either. + +## Auth +- Global default: header `token` matching `AZURE_CUSTOM_HEADER_TOKEN`, or query `apiKey` matching `WEB_INTEGRATIONS_API_KEY`. Both compared with `crypto.timingSafeEqual`. Loopback (`127.0.0.1`/`::1`) bypass applies only when `SERVICE_TYPE=dev`. +- Per-action override via `auth: "anonymous" | "key" | "proxy"`: + - `anonymous` - no auth at all. Use sparingly (e.g. public webhook receivers that bring their own HMAC). + - `key` (default) - global rule above. + - `proxy` - only the header `token` is accepted; query `apiKey` is rejected. Use for actions meant to be human-driven through Azure App Proxy. +- Auth credentials are read once at module load. Rotating `AZURE_CUSTOM_HEADER_TOKEN` or `WEB_INTEGRATIONS_API_KEY` requires a process restart to take effect. + +## Config and secrets +- `local.settings.json` is gitignored and must not be committed; start it with `{ "$schema": "./local.settings.schema.json" }` and use the schema for required keys. +- Runtime settings are read from `local.settings.json` first, then environment variables. `local.settings.json` uses a top-level `Values` object. +- Required settings: `AZURE_CUSTOM_HEADER_TOKEN`, `SERVICE_TYPE`, `WEB_INTEGRATIONS_API_KEY`, `WEBSITE_BLOB_SAS`. Optional: `PORT` (default `3000`), `CORS_ORIGINS` (comma-separated allowed origins; CORS disabled if blank). + +## Commands +- Use `npm install` for local setup. The project ships `package-lock.json`. Stick with npm; pnpm/yarn add justification an enterprise admin will ask about and offer no benefit for this audience. +- `npm run dev` starts the local watched service; `npm start` starts without a watcher. +- `npm run check` / `npm run typecheck` for TypeScript checking. No `test`, `lint`, or `build` scripts. +- Windows service: `npm run windows-install` / `npm run windows-uninstall`; both execute TypeScript files under `deploy/windows` via `node`. +- `npm run configure` runs `scripts/configure.ts` to write `local.settings.json` interactively (random UUIDs for the two secrets, `SERVICE_TYPE=dev` default). The file is written with mode 0600. +- `npm run docker-package` builds a Docker image and saves `az-functions-onprem-docker-image.tar`. + +## Common on-prem integration packages +Not bundled; install on demand when you need them. +- **`mssql`** - Dynamics, internal data warehouses, anything talking to SQL Server. Use a connection pool; close on shutdown. +- **`@azure/msal-node`** - Azure AD app/user auth for Microsoft Graph, SharePoint, Dataverse, Power Platform APIs. +- **`@azure/storage-blob`** - already bundled. Used by `src/helpers/sample-upload-blob.ts`. +- **`ldapjs`** or **`activedirectory2`** - Active Directory lookups (groups, users) for legacy on-prem identity. +- **`nodemailer`** - SMTP relay for sending operational alerts via Exchange / O365 SMTP. +- **`ssh2-sftp-client`** - SFTP file drops; common with legacy ERP/EDI partners. +- **`undici`** or native `fetch` - outbound HTTP/OData calls; `undici` lets you set per-host agents (mTLS client certs). +- **`pino`** - structured JSON logging once `console.log` is no longer enough. Slot it into `src/runtime/log.ts`. + +## Deployment notes +- The Dockerfile uses `node:slim`, copies `package*.json`, runs `npm ci --omit=dev`, sets `NODE_ENV=production`, and exposes `/healthz`. Keep `package-lock.json` current or Docker/CI installs will fail. +- Docker deployments should provide environment variables matching `local.settings.schema.json`; `local.settings.json` is intentionally not copied into the image. +- CI typechecking is active at `.github/workflows/ci.yml`; the Docker publishing workflow remains an optional template at `deploy/docker/github-workflows-docker.yml` until moved to `.github/workflows/docker.yml`. +- The Windows service definition runs `src/server.ts` directly through Node. diff --git a/Dockerfile b/Dockerfile index dd67008..eee790d 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,23 +1,33 @@ # Use the official Node.js image as the base image -FROM node:18.18-alpine +FROM node:24-slim + +LABEL org.opencontainers.image.title="az-functions-onprem" +LABEL org.opencontainers.image.description="Self-hosted, Azure-Functions-style integrations runtime for Windows Server or Docker." +LABEL org.opencontainers.image.licenses="MIT" +LABEL org.opencontainers.image.source="https://github.com/alirobe/az-functions-onprem" + +ENV NODE_ENV=production # Set the working directory inside the image WORKDIR /app -# Copy package.json and package-lock.json into the image +# Copy lockfile + manifest into the image COPY package*.json ./ -COPY tsconfig.json ./ -COPY nodemon.json ./ # DO NOT COPY .env OR local.settings.json INTO THE IMAGE -# Install dependencies -RUN npm ci +# Install runtime dependencies +RUN npm ci --omit=dev # Copy the remaining application files into the image COPY src ./src/ +USER node + # Expose the port your application will run on EXPOSE 3000 +HEALTHCHECK --interval=30s --timeout=3s --start-period=10s --retries=3 \ + CMD node -e "fetch('http://127.0.0.1:' + (process.env.PORT || 3000) + '/healthz').then(r => process.exit(r.ok ? 0 : 1)).catch(() => process.exit(1))" + # Start the application -CMD ["npm", "start"] \ No newline at end of file +CMD ["npm", "start"] diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..8ea8ee2 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 Clear Path Research (clearpath.cloud) - Ali Robertson + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md index 509b148..d6ee920 100644 --- a/README.md +++ b/README.md @@ -1,49 +1,271 @@ -# Web Integrations Template +# az-functions-onprem -- This project contains a Windows Web Service or Docker Container designed for internal rapid development using node + typescript. -- It is designed for small, simple, low-risk projects. -- It relies on rotatable keys for authentication. -- It relies on Azure App Proxy for limited internal publishing. -- It was originally designed to facilitate an [Azure Integration Services](https://azure.microsoft.com/en-us/products/category/integration) pattern. - -## Good to know: +[![CI](https://github.com/alirobe/az-functions-onprem/actions/workflows/ci.yml/badge.svg)](https://github.com/alirobe/az-functions-onprem/actions/workflows/ci.yml) +[![License: MIT](https://img.shields.io/badge/license-MIT-blue)](LICENSE) +[![Node](https://img.shields.io/badge/node-%E2%89%A5%2023.6-339933)](https://nodejs.org) -- Some Git + DevSecOps. -- Some TypeScript (it's quite similar to C#). -- REST, OData, SQL and JSON Schema will help. -- Basic Windows Server experience, OR experience deploying docker containers. +Azure-Functions-style integrations runtime for Windows Server or Docker, behind your firewall. -## How it works +Necessary today. Temporary by design. Handlers port to Azure Functions v4 with cosmetic changes. -All actions must be called with some sort of fixed key, either in a header, or the query string. -All secrets and keys are defined `local.settings.json` or in ENV variables, for now - but you should move them somewhere more secure. +- Windows Service or Docker. +- TypeScript handlers via `defineAction({ name, description, handler })`. +- External triggers (Task Scheduler / cron / webhooks) or internal cron. +- Header token + rotatable API key. Per-action auth (anonymous / key / proxy). +- Streaming NDJSON for long-running syncs. +- Native TypeScript stripping. No build step. +- MIT. -- Windows Task Scheduler + Powershell can make requests to the URLs, so they're run regularly. Those can use the query string like so: `Invoke-WebRequest http://localhost:3000/action?apiKey=` -- If you set up an Azure App Proxy Application, it can be configured to add headers. It should add two: `token` should match the setting `AZURE_CUSTOM_HEADER_TOKEN` (make a note in a shared maintenance calendar to rotate this key regularly), and the `upn`, with the (unverified) UPN of the user for logging purposes. +## Hello action -These are not best practices, but they're a practical starting point. +```ts +// src/actions/hello.ts +import { defineAction } from "../runtime/registry.ts"; -It's up to you to implement further security and secret management however you see fit. +defineAction({ + name: "hello", + description: "Smallest possible action.", + handler: () => ({ greeting: "hi" }), +}); +``` -For OpSec reasons I won't give any further guidance. +```ts +// src/actions/index.ts +import "./hello.ts"; +``` -#### Warning: The `local.settings.json` or environment variables should not be committed to the repo or stored in the container. #### +```bash +curl http://localhost:3000/action/hello +# {"action":"hello","result":{"greeting":"hi"}} +``` -## Local development: getting started +## When you'd reach for this -- Ensure node version manager (`nvm`) is installed on your machine. -- cd to this directory -- `nvm install ` (check `.nvmrc` for version number) -- `nvm use lts` -- `npm install` -- Use VS Code to create `local.settings.json` file in root, containing `{"$schema": "./local.settings.schema.json"}`. -- Close and re-open the file. intellisense will pick up the schema and provide autocomplete and validation. Provide required settings and save. -- Run `npm start` -- No compile step exists. nodemon will restart the service every time you make a change. +- **ERP → CRM sync.** SQL/OData read, push summary to HubSpot/Salesforce. +- **DB → website cache.** Internal DB → JSON blob → cache-clear endpoint. +- **Inbound webhook.** Receive SaaS webhook, verify HMAC, project into SharePoint / DB. +- **AD → SaaS provisioning.** Read groups via LDAP, mirror to Okta/Entra. +- **SFTP file drop.** Poll, transform, upload to Blob. +- **Internal notification fan-out.** App alert → Teams/Slack/email/PagerDuty. +Recipes for the first three live in `src/actions/examples/`. + +## When you'd not reach for this + +- You can use real Azure Functions (cloud data, Entra identity, no firewall blockers). +- You need autoscaling beyond a single Windows box / container. +- You're sustained above ~10 req/s. + +## Comparison + +| | az-functions-onprem | Azure Functions | Logic Apps / Power Automate | Express + cron | +| ------------------------------ | ------------------- | ------------------------ | --------------------------- | -------------- | +| Runs behind firewall | yes | Premium + VNet only | no | yes | +| Code-first authoring | TypeScript | TypeScript / C# / Python | designer / low-code | any | +| Built-in scheduler | yes | yes | yes | no | +| Webhook receivers | yes | yes | yes | yes | +| OpenAPI auto-generated | yes | no | no | no | +| Operates fully offline | yes | no | no | yes | +| Per-invocation cost | $0 | metered | metered | $0 | +| Migration to Azure Functions | trivial | n/a | rewrite | rewrite | + +## Migrating to Azure Functions + +```ts +// Here: +defineAction({ + name: "syncCustomers", + method: "POST", + auth: "key", + handler: async (ctx) => { + ctx.log("starting sync"); + return httpResponse({ status: 201, jsonBody: { count: 42 } }); + }, +}); + +// There (Azure Functions v4): +app.http("syncCustomers", { + methods: ["POST"], + authLevel: "function", + handler: async (request, context) => { + context.log("starting sync"); + return { status: 201, jsonBody: { count: 42 } }; + }, +}); +``` + +`ctx.log` (with `.info`/`.warn`/`.error`), `ctx.invocationId`, and `httpResponse({...})` mirror the Functions v4 context. Cron schedules use 6-field NCRONTAB and port to Timer triggers as-is. The scheduler, OpenAPI generator, auth middleware, Windows-service install, and UI are not used in Functions; the handler bodies are. + +## Requirements + +- Node.js 23.6 or newer (native TypeScript stripping). +- npm. +- Optional: Docker, Windows Server. + +## Quickstart + +```bash +npm install +npm run configure # writes local.settings.json with random tokens, SERVICE_TYPE=dev +npm run dev # node --watch on src/server.ts +``` + +Open `http://localhost:3000/`. Loopback auth is bypassed in `dev` mode. API docs at `/docs` (Swagger UI over `/openapi.json`). + +`npm run check` before committing. + +## Source layout + +``` +src/ +├── runtime/ framework. Don't touch. +├── actions/ your actions (see src/actions/README.md) +│ └── examples/ copy-paste recipes, inert by default +├── helpers/ your shared helpers +├── config/ Windows service definition +├── views/ EJS UI +├── public/ static assets +└── server.ts entrypoint +``` + +## Authoring actions + +```ts +// src/actions/sync-customers.ts +import { defineAction } from "../runtime/registry.ts"; + +defineAction({ + name: "syncCustomers", + description: "Pull customers from CRM, write to blob.", + schedule: "0 */15 * * * *", // optional internal cron (6-field NCRONTAB) + method: "GET", // GET (default) or POST + timeoutMs: 60_000, + auth: "key", // anonymous | key (default) | proxy + handler: async (ctx) => { + ctx.log("starting"); + return { count: 42 }; + }, + onSuccess: async (result, ctx) => { + // optional post-success hook, e.g. ping a cache-clear endpoint + }, +}); +``` + +An action is one of: + +- `handler` — one-shot async function returning JSON. +- `steps: ["a", "b"]` — sequence of other registered actions, streamed as NDJSON step events. +- `stream` — async generator yielding NDJSON chunks. + +Drop the file in `src/actions/`, add `import "./sync-customers.ts";` to `src/actions/index.ts`. The HTTP route, scheduler, OpenAPI entry, and index-page card are wired automatically. + +POST actions reading the body: + +```ts +import { defineAction, httpCtx } from "../runtime/registry.ts"; + +defineAction({ + name: "receive", + description: "Accept a JSON POST.", + method: "POST", + handler: async (ctx) => { + const { req } = httpCtx(ctx); + const payload = req.body; + return { received: true }; + }, +}); +``` + +## Triggering + +**External**: Windows Task Scheduler / cron / Azure App Proxy webhooks hit `/action/:name`. + +```pwsh +Invoke-WebRequest -Headers @{token=''} ` + http://localhost:3000/action/syncCustomers +``` + +Prefer header auth so the key doesn't end up in URL history or access logs. Query-string `?apiKey=<...>` works for quick local testing. + +**Internal**: set `schedule` on the action; `node-cron` registers it at boot. Stream actions can't be scheduled internally. Both 5-field standard cron and 6-field NCRONTAB are accepted; the 6-field form ports straight to Azure Functions Timer triggers. + +Azure App Proxy: configure it to add the `token` header on every proxied request. The auth middleware validates the header and logs the `upn` claim when present. + +## Common on-prem integration packages + +Not bundled. Install on demand: + +- **`mssql`** — SQL Server (Dynamics GP, Business Central via TDS, internal warehouses). See `src/actions/examples/mssql-to-blob.ts`. +- **`@azure/msal-node`** — Entra app auth for Microsoft Graph, SharePoint, Dataverse, Power Platform. See `src/actions/examples/sharepoint-list-sync.ts`. +- **`@azure/storage-blob`** — already included; used by `src/helpers/sample-upload-blob.ts`. +- **`ldapjs`** / **`activedirectory2`** — direct Active Directory queries. +- **`nodemailer`** — SMTP relay via Exchange / O365. +- **`ssh2-sftp-client`** — SFTP drops for legacy partners. +- **`pino`** — structured JSON logging. + +## Configuration + +`local.settings.json` (gitignored): + +```json +{ + "$schema": "./local.settings.schema.json", + "Values": { + "AZURE_CUSTOM_HEADER_TOKEN": "replace-with-rotatable-header-token", + "SERVICE_TYPE": "dev", + "WEB_INTEGRATIONS_API_KEY": "replace-with-rotatable-api-key", + "WEBSITE_BLOB_SAS": "https://example.blob.core.windows.net/container?sv=placeholder", + "PORT": 3000, + "CORS_ORIGINS": "" + } +} +``` + +`CORS_ORIGINS` is a comma-separated allowlist; blank disables CORS. + +For Docker / hosted deployments, provide the same keys as environment variables. + +**Do not commit** `local.settings.json` or bake secrets into the image. ## Deployment -Partial instructions in the relevant `deploy` folder. +### Docker + +```bash +docker build -t az-functions-onprem . +docker run --env-file .env -p 3000:3000 az-functions-onprem +``` + +- `NODE_ENV=production`, non-root `node` user. +- `npm ci --omit=dev`. +- `/healthz` for container health checks. +- `local.settings.json` and `.env` are not copied in. + +### Windows Service + +`npm run windows-install` / `npm run windows-uninstall` register the service via `node-windows`. Configure the service account in `services.msc`; logs go to the Application event log. + +## About + +A [Clear Path Research](https://clearpath.cloud) template. + +## Version History + +### v2 (2026-05) + +- Renamed `az-functions-onprem`. Positioned as a migration-bridge runtime. +- `defineAction` registry with handler, sequence, and streaming (NDJSON) forms. +- Per-action `schedule` (cron), `timeoutMs`, `onSuccess`, `method` (GET/POST), `auth` level. +- `ctx.log` / `ctx.invocationId` / `httpResponse(...)` match Azure Functions v4 surface. +- `src/actions/examples/` recipes for SQL-to-blob, inbound webhook (HMAC), SharePoint via Graph. +- Source tree split: `src/runtime/` (framework) vs `src/actions/` / `src/helpers/` (user code). +- Internal `node-cron` scheduler; external Task Scheduler / cron still works. +- `/openapi.json` and `/docs` (Swagger UI) auto-generated from the registry. +- `CORS_ORIGINS` allowlist. +- UI: per-card inline output panels stream progress in place; no modal. +- Native Node TypeScript stripping replaces `nodemon` / `ts-node`. +- Hardened security defaults, refreshed dependencies. + +### v1 (2023-11) -#### Security note: This template is a quick start template only. It uses nodemon in 'production'. Fix this before going live. #### +- Initial template: Express service, Windows-service install path, Docker packaging, shared header / API-key gating. diff --git a/deploy/docker/README.md b/deploy/docker/README.md index 4dd003c..da1de37 100644 --- a/deploy/docker/README.md +++ b/deploy/docker/README.md @@ -1,11 +1,11 @@ # Docker + Github Deployment Instructions -1. Move the github-workflows-docker file to `/.github/workflows/docker.yml`, or change for your CI/CD env. +1. Move the optional `github-workflows-docker.yml` file to `/.github/workflows/docker.yml`, or change for your CI/CD env. The default active CI workflow only typechecks the project. 2. Ensure whatever container service you're using has access to your package registry. -3. Deploy using an .env file with variables that match the `local.settings.json.schema` (the `local.settings.json` package is not included in the image). +3. Deploy using an .env file with variables that match the `local.settings.schema.json` (the `local.settings.json` package is not included in the image). -You may also wish to use the `npm run docker-package` function to create an image that is portable without a container registry. +You may also wish to use the `npm run docker-package` function to create `az-functions-onprem-docker-image.tar`, which is portable without a container registry. -In testing, you can just use standard docker commands with the existing docker file, to build and run the project. +In testing, you can just use standard docker commands with the existing Dockerfile, to build and run the project. The image uses `node:slim` for broader compatibility, runs as the non-root `node` user, sets `NODE_ENV=production`, installs only production dependencies, and exposes `/healthz` for container health checks. -Ensure that files are secured, and consider using better secrets management. This template is meant as a starting point / POC for a project. I would also caution against the long term use of nodemon in prod. As this tool grows, consider adding a build step, expanding shared utils, and adding more security measures. Be careful to always use a least privileges approach when working on integrations. \ No newline at end of file +Ensure that files are secured, and consider using better secrets management. This template is meant as a starting point / POC for a project. As this tool grows, consider adding a build step, expanding shared utils, and adding more security measures. Be careful to always use a least privileges approach when working on integrations. diff --git a/deploy/docker/github-workflows-docker.yml b/deploy/docker/github-workflows-docker.yml index 6ab7a76..b6ca1c8 100644 --- a/deploy/docker/github-workflows-docker.yml +++ b/deploy/docker/github-workflows-docker.yml @@ -10,24 +10,25 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3.5.2 + - uses: actions/checkout@v5 - name: Setup Node.js environment - uses: actions/setup-node@v3 + uses: actions/setup-node@v5 with: - node-version: 18.18 + node-version: '24' + cache: 'npm' - name: Install dependencies run: npm ci - # - # - name: Build TypeScript - # run: npm run build + + - name: Typecheck + run: npm run typecheck - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v2.5.0 + uses: docker/setup-buildx-action@v3 - name: Login to GitHub Container Registry - uses: docker/login-action@v2.1.0 + uses: docker/login-action@v3 with: registry: ghcr.io username: ${{ github.repository_owner }} @@ -38,10 +39,10 @@ jobs: run: echo "DATETIME=$(date +%Y%m%d%H%M%S)" >> $GITHUB_ENV - name: Build and push Docker image - uses: docker/build-push-action@v4.0.0 + uses: docker/build-push-action@v6 with: context: . push: true tags: | - ghcr.io/${{ github.repository_owner }}/web-integrations-template:latest - ghcr.io/${{ github.repository_owner }}/web-integrations-template:${{ env.DATETIME }} + ghcr.io/${{ github.repository_owner }}/az-functions-onprem:latest + ghcr.io/${{ github.repository_owner }}/az-functions-onprem:${{ env.DATETIME }} diff --git a/deploy/windows/README.md b/deploy/windows/README.md index c85b037..9dbbf48 100644 --- a/deploy/windows/README.md +++ b/deploy/windows/README.md @@ -1,11 +1,11 @@ # Windows Installation Instructions 1. Place these files in a folder for execution -2. Ensure node is installed on the server, to the same version defined in the nvmrc file. +2. Ensure a current Node.js release is installed on the server. 3. Run `npm install`. This will download dependencies. 4. Ensure a file called `local.settings.json` exists and contains at least `{"$schema": "./local.settings.schema.json"}`. You can use VS Code (a relatively small text editor) to autocomplete and verify the file. Ensure this file is correctly set up before continuing. 5. Run `npm run windows-install` to install the windows service. 6. Configure the service in `services.msc`, setting the user. 7. Inspect `eventvwr.exe` Application Log, where you'll see events logged from the service. You may need to restart the service if changes are made. -Ensure that files are secured, and consider using better secrets management. This template is meant as a starting point / POC for a project. I would also caution against the long term use of nodemon in prod. As this tool grows, consider adding a build step, expanding the utils, and adding more security measures. Be careful to use a least privileges approach always when working on integrations. \ No newline at end of file +Ensure that files are secured, and consider using better secrets management. This template is meant as a starting point / POC for a project. As this tool grows, consider adding a build step, expanding the utils, and adding more security measures. Be careful to use a least privileges approach always when working on integrations. diff --git a/deploy/windows/install.ts b/deploy/windows/install.ts index d28e5d9..52c3685 100644 --- a/deploy/windows/install.ts +++ b/deploy/windows/install.ts @@ -1,5 +1,4 @@ -// @ts-ignore -import { service, serviceDefinition } from "../../src/config/windows-service"; +import { service, serviceDefinition } from "../../src/config/windows-service.ts"; service.on("install", () => { service.start(); diff --git a/deploy/windows/uninstall.ts b/deploy/windows/uninstall.ts index 7648957..c5fad54 100644 --- a/deploy/windows/uninstall.ts +++ b/deploy/windows/uninstall.ts @@ -1,9 +1,12 @@ -// @ts-ignore -import { service, serviceDefinition } from "../../src/config/windows-service"; +import { service, serviceDefinition } from "../../src/config/windows-service.ts"; service.on("uninstall", () => { console.log("Uninstall complete."); console.log(`The service '${serviceDefinition.name}' exists ${service.exists}`); }); +service.on("alreadyuninstalled", () => { + console.log(`The service '${serviceDefinition.name}' is already uninstalled.`); +}); + service.uninstall(); diff --git a/local.settings.schema.json b/local.settings.schema.json index d05bcac..b2d6a66 100644 --- a/local.settings.schema.json +++ b/local.settings.schema.json @@ -43,6 +43,13 @@ ], "maximum": 65535, "minimum": 1 + }, + "CORS_ORIGINS": { + "type": "string", + "description": "Comma-separated list of allowed CORS origins. Leave blank to disable CORS entirely.", + "examples": [ + "https://internal.example.com,https://admin.example.com" + ] } }, "required": [ diff --git a/nodemon.json b/nodemon.json deleted file mode 100644 index 48ba221..0000000 --- a/nodemon.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "watch": [ - "src", - "./local.settings.json" - ], - "ext": "ts", - "ignore": [ - "src/**/*.spec.ts" - ], - "exec": "ts-node ./src/server.ts" -} \ No newline at end of file diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..9717396 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,1589 @@ +{ + "name": "az-functions-onprem", + "version": "2.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "az-functions-onprem", + "version": "2.0.0", + "license": "MIT", + "dependencies": { + "@azure/storage-blob": "^12.0.0", + "cors": "^2.8.6", + "ejs": "^5.0.0", + "express": "^5.0.0", + "helmet": "^8.0.0", + "node-cron": "^4.2.1", + "node-windows": "^1.0.0-beta.8" + }, + "devDependencies": { + "@types/cors": "^2.8.19", + "@types/express": "^5.0.0", + "@types/node": "^25.0.0", + "@types/node-windows": "^0.1.0", + "typescript": "^6.0.0" + }, + "engines": { + "node": ">=23.6.0" + } + }, + "node_modules/@azure/abort-controller": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@azure/abort-controller/-/abort-controller-2.1.2.tgz", + "integrity": "sha512-nBrLsEWm4J2u5LpAPjxADTlq3trDgVZZXHNKabeXZtpq3d3AbN/KGO82R87rdDz5/lYB024rtEf10/q0urNgsA==", + "license": "MIT", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@azure/core-auth": { + "version": "1.10.1", + "resolved": "https://registry.npmjs.org/@azure/core-auth/-/core-auth-1.10.1.tgz", + "integrity": "sha512-ykRMW8PjVAn+RS6ww5cmK9U2CyH9p4Q88YJwvUslfuMmN98w/2rdGRLPqJYObapBCdzBVeDgYWdJnFPFb7qzpg==", + "license": "MIT", + "dependencies": { + "@azure/abort-controller": "^2.1.2", + "@azure/core-util": "^1.13.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@azure/core-client": { + "version": "1.10.1", + "resolved": "https://registry.npmjs.org/@azure/core-client/-/core-client-1.10.1.tgz", + "integrity": "sha512-Nh5PhEOeY6PrnxNPsEHRr9eimxLwgLlpmguQaHKBinFYA/RU9+kOYVOQqOrTsCL+KSxrLLl1gD8Dk5BFW/7l/w==", + "license": "MIT", + "dependencies": { + "@azure/abort-controller": "^2.1.2", + "@azure/core-auth": "^1.10.0", + "@azure/core-rest-pipeline": "^1.22.0", + "@azure/core-tracing": "^1.3.0", + "@azure/core-util": "^1.13.0", + "@azure/logger": "^1.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@azure/core-http-compat": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/@azure/core-http-compat/-/core-http-compat-2.4.0.tgz", + "integrity": "sha512-f1P96IB399YiN2ARYHP7EpZi3Bf3wH4SN2lGzrw7JVwm7bbsVYtf2iKSBwTywD2P62NOPZGHFSZi+6jjb75JuA==", + "license": "MIT", + "dependencies": { + "@azure/abort-controller": "^2.1.2" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "@azure/core-client": "^1.10.0", + "@azure/core-rest-pipeline": "^1.22.0" + } + }, + "node_modules/@azure/core-lro": { + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/@azure/core-lro/-/core-lro-2.7.2.tgz", + "integrity": "sha512-0YIpccoX8m/k00O7mDDMdJpbr6mf1yWo2dfmxt5A8XVZVVMz2SSKaEbMCeJRvgQ0IaSlqhjT47p4hVIRRy90xw==", + "license": "MIT", + "dependencies": { + "@azure/abort-controller": "^2.0.0", + "@azure/core-util": "^1.2.0", + "@azure/logger": "^1.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@azure/core-paging": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/@azure/core-paging/-/core-paging-1.6.2.tgz", + "integrity": "sha512-YKWi9YuCU04B55h25cnOYZHxXYtEvQEbKST5vqRga7hWY9ydd3FZHdeQF8pyh+acWZvppw13M/LMGx0LABUVMA==", + "license": "MIT", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@azure/core-rest-pipeline": { + "version": "1.23.0", + "resolved": "https://registry.npmjs.org/@azure/core-rest-pipeline/-/core-rest-pipeline-1.23.0.tgz", + "integrity": "sha512-Evs1INHo+jUjwHi1T6SG6Ua/LHOQBCLuKEEE6efIpt4ZOoNonaT1kP32GoOcdNDbfqsD2445CPri3MubBy5DEQ==", + "license": "MIT", + "dependencies": { + "@azure/abort-controller": "^2.1.2", + "@azure/core-auth": "^1.10.0", + "@azure/core-tracing": "^1.3.0", + "@azure/core-util": "^1.13.0", + "@azure/logger": "^1.3.0", + "@typespec/ts-http-runtime": "^0.3.4", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@azure/core-tracing": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@azure/core-tracing/-/core-tracing-1.3.1.tgz", + "integrity": "sha512-9MWKevR7Hz8kNzzPLfX4EAtGM2b8mr50HPDBvio96bURP/9C+HjdH3sBlLSNNrvRAr5/k/svoH457gB5IKpmwQ==", + "license": "MIT", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@azure/core-util": { + "version": "1.13.1", + "resolved": "https://registry.npmjs.org/@azure/core-util/-/core-util-1.13.1.tgz", + "integrity": "sha512-XPArKLzsvl0Hf0CaGyKHUyVgF7oDnhKoP85Xv6M4StF/1AhfORhZudHtOyf2s+FcbuQ9dPRAjB8J2KvRRMUK2A==", + "license": "MIT", + "dependencies": { + "@azure/abort-controller": "^2.1.2", + "@typespec/ts-http-runtime": "^0.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@azure/core-xml": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/@azure/core-xml/-/core-xml-1.5.1.tgz", + "integrity": "sha512-xcNRHqCoSp4AunOALEae6A8f3qATb83gSrm31Iqb01OzblvC3/W/bfXozcq78EzIdzZzuH1bZ2NvRR0TdX709w==", + "license": "MIT", + "dependencies": { + "fast-xml-parser": "^5.5.9", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@azure/logger": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@azure/logger/-/logger-1.3.0.tgz", + "integrity": "sha512-fCqPIfOcLE+CGqGPd66c8bZpwAji98tZ4JI9i/mlTNTlsIWslCfpg48s/ypyLxZTump5sypjrKn2/kY7q8oAbA==", + "license": "MIT", + "dependencies": { + "@typespec/ts-http-runtime": "^0.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@azure/storage-blob": { + "version": "12.31.0", + "resolved": "https://registry.npmjs.org/@azure/storage-blob/-/storage-blob-12.31.0.tgz", + "integrity": "sha512-DBgNv10aCSxopt92DkTDD0o9xScXeBqPKGmR50FPZQaEcH4JLQ+GEOGEDv19V5BMkB7kxr+m4h6il/cCDPvmHg==", + "license": "MIT", + "dependencies": { + "@azure/abort-controller": "^2.1.2", + "@azure/core-auth": "^1.9.0", + "@azure/core-client": "^1.9.3", + "@azure/core-http-compat": "^2.2.0", + "@azure/core-lro": "^2.2.0", + "@azure/core-paging": "^1.6.2", + "@azure/core-rest-pipeline": "^1.19.1", + "@azure/core-tracing": "^1.2.0", + "@azure/core-util": "^1.11.0", + "@azure/core-xml": "^1.4.5", + "@azure/logger": "^1.1.4", + "@azure/storage-common": "^12.3.0", + "events": "^3.0.0", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@azure/storage-common": { + "version": "12.3.0", + "resolved": "https://registry.npmjs.org/@azure/storage-common/-/storage-common-12.3.0.tgz", + "integrity": "sha512-/OFHhy86aG5Pe8dP5tsp+BuJ25JOAl9yaMU3WZbkeoiFMHFtJ7tu5ili7qEdBXNW9G5lDB19trwyI6V49F/8iQ==", + "license": "MIT", + "dependencies": { + "@azure/abort-controller": "^2.1.2", + "@azure/core-auth": "^1.9.0", + "@azure/core-http-compat": "^2.2.0", + "@azure/core-rest-pipeline": "^1.19.1", + "@azure/core-tracing": "^1.2.0", + "@azure/core-util": "^1.11.0", + "@azure/logger": "^1.1.4", + "events": "^3.3.0", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@nodable/entities": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@nodable/entities/-/entities-2.1.0.tgz", + "integrity": "sha512-nyT7T3nbMyBI/lvr6L5TyWbFJAI9FTgVRakNoBqCD+PmID8DzFrrNdLLtHMwMszOtqZa8PAOV24ZqDnQrhQINA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/nodable" + } + ], + "license": "MIT" + }, + "node_modules/@types/body-parser": { + "version": "1.19.6", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz", + "integrity": "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/connect": "*", + "@types/node": "*" + } + }, + "node_modules/@types/connect": { + "version": "3.4.38", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", + "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/cors": { + "version": "2.8.19", + "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.19.tgz", + "integrity": "sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/express": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/@types/express/-/express-5.0.6.tgz", + "integrity": "sha512-sKYVuV7Sv9fbPIt/442koC7+IIwK5olP1KWeD88e/idgoJqDm3JV/YUiPwkoKK92ylff2MGxSz1CSjsXelx0YA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^5.0.0", + "@types/serve-static": "^2" + } + }, + "node_modules/@types/express-serve-static-core": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.1.1.tgz", + "integrity": "sha512-v4zIMr/cX7/d2BpAEX3KNKL/JrT1s43s96lLvvdTmza1oEvDudCqK9aF/djc/SWgy8Yh0h30TZx5VpzqFCxk5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*", + "@types/send": "*" + } + }, + "node_modules/@types/http-errors": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.5.tgz", + "integrity": "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "25.7.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.7.0.tgz", + "integrity": "sha512-z+pdZyxE+RTQE9AcboAZCb4otwcrvgHD+GlBpPgn0emDVt0ohrTMhAwlr2Wd9nZ+nihhYFxO2pThz3C5qSu2Eg==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~7.21.0" + } + }, + "node_modules/@types/node-windows": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/@types/node-windows/-/node-windows-0.1.6.tgz", + "integrity": "sha512-vbjaBtsF23Nprkw8rQFwskWzv5l6grWbUerYBHHjY/hqp/vIBAPGVQnr3dqsd75SwCQIAoZeBr/b+AbFFV/LfQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/qs": { + "version": "6.15.1", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.15.1.tgz", + "integrity": "sha512-GZHUBZR9hckSUhrxmp1nG6NwdpM9fCunJwyThLW1X3AyHgd9IlHb6VANpQQqDr2o/qQp6McZ3y/IA2rVzKzSbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/range-parser": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", + "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/send": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@types/send/-/send-1.2.1.tgz", + "integrity": "sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/serve-static": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-2.2.0.tgz", + "integrity": "sha512-8mam4H1NHLtu7nmtalF7eyBH14QyOASmcxHhSfEoRyr0nP/YdoesEtU+uSRvMe96TW/HPTtkoKqQLl53N7UXMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/http-errors": "*", + "@types/node": "*" + } + }, + "node_modules/@typespec/ts-http-runtime": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/@typespec/ts-http-runtime/-/ts-http-runtime-0.3.5.tgz", + "integrity": "sha512-yURCknZhvywvQItHMMmFSo+fq5arCUIyz/CVk7jD89MSai7dkaX8ufjCWp3NttLojoTVbcE72ri+be/TnEbMHw==", + "license": "MIT", + "dependencies": { + "http-proxy-agent": "^7.0.0", + "https-proxy-agent": "^7.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/accepts": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", + "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", + "license": "MIT", + "dependencies": { + "mime-types": "^3.0.0", + "negotiator": "^1.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/body-parser": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.2.tgz", + "integrity": "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==", + "license": "MIT", + "dependencies": { + "bytes": "^3.1.2", + "content-type": "^1.0.5", + "debug": "^4.4.3", + "http-errors": "^2.0.0", + "iconv-lite": "^0.7.0", + "on-finished": "^2.4.1", + "qs": "^6.14.1", + "raw-body": "^3.0.1", + "type-is": "^2.0.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "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==", + "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==", + "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/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "license": "MIT" + }, + "node_modules/content-disposition": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.1.0.tgz", + "integrity": "sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "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==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", + "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", + "license": "MIT", + "engines": { + "node": ">=6.6.0" + } + }, + "node_modules/cors": { + "version": "2.8.6", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz", + "integrity": "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==", + "license": "MIT", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "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==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "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==", + "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==", + "license": "MIT" + }, + "node_modules/ejs": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/ejs/-/ejs-5.0.2.tgz", + "integrity": "sha512-IpbUaI/CAW86l3f+T8zN0iggSc0LmMZLcIW5eRVStLVNCoTXkE0YlncbbH50fp8Cl6zHIky0sW2uUbhBqGw0Jw==", + "license": "Apache-2.0", + "bin": { + "ejs": "bin/cli.js" + }, + "engines": { + "node": ">=0.12.18" + } + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "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==", + "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==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "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==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "license": "MIT", + "engines": { + "node": ">=0.8.x" + } + }, + "node_modules/express": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", + "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", + "license": "MIT", + "dependencies": { + "accepts": "^2.0.0", + "body-parser": "^2.2.1", + "content-disposition": "^1.0.0", + "content-type": "^1.0.5", + "cookie": "^0.7.1", + "cookie-signature": "^1.2.1", + "debug": "^4.4.0", + "depd": "^2.0.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "finalhandler": "^2.1.0", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "merge-descriptors": "^2.0.0", + "mime-types": "^3.0.0", + "on-finished": "^2.4.1", + "once": "^1.4.0", + "parseurl": "^1.3.3", + "proxy-addr": "^2.0.7", + "qs": "^6.14.0", + "range-parser": "^1.2.1", + "router": "^2.2.0", + "send": "^1.1.0", + "serve-static": "^2.2.0", + "statuses": "^2.0.1", + "type-is": "^2.0.1", + "vary": "^1.1.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/fast-xml-builder": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/fast-xml-builder/-/fast-xml-builder-1.2.0.tgz", + "integrity": "sha512-00aAWieqff+ZJhsXA4g1g7M8k+7AYoMUUHF+/zFb5U6Uv/P0Vl4QZo84/IcufzYalLuEj9928bXN9PbbFzMF0Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "dependencies": { + "path-expression-matcher": "^1.5.0", + "xml-naming": "^0.1.0" + } + }, + "node_modules/fast-xml-parser": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.8.0.tgz", + "integrity": "sha512-6bIM7fsJxeo3uXv7OncQYsBAMPJ7V16Slahl/6M98C/i2q+vB1+4a0MtrvYwDFEUrwDSbAmeLDRXsOBwrL7yAg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "dependencies": { + "@nodable/entities": "^2.1.0", + "fast-xml-builder": "^1.2.0", + "path-expression-matcher": "^1.5.0", + "strnum": "^2.3.0", + "xml-naming": "^0.1.0" + }, + "bin": { + "fxparser": "src/cli/cli.js" + } + }, + "node_modules/finalhandler": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz", + "integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "on-finished": "^2.4.1", + "parseurl": "^1.3.3", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", + "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "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==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "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==", + "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==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.3.tgz", + "integrity": "sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/helmet": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/helmet/-/helmet-8.1.0.tgz", + "integrity": "sha512-jOiHyAZsmnr8LqoPGmCjYAaiuWwjAPLgY8ZX2XrmHawt99/u1y6RgrZMTeoPfpUbV96HOalYgz1qzkRbw54Pmg==", + "license": "MIT", + "engines": { + "node": ">=18.0.0" + } + }, + "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==", + "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/http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/iconv-lite": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz", + "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==", + "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/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "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==", + "license": "MIT" + }, + "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==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", + "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/merge-descriptors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", + "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", + "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "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==", + "license": "MIT" + }, + "node_modules/negotiator": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", + "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/node-cron": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/node-cron/-/node-cron-4.2.1.tgz", + "integrity": "sha512-lgimEHPE/QDgFlywTd8yTR61ptugX3Qer29efeyWw2rv259HtGBNn1vZVmp8lB9uo9wC0t/AT4iGqXxia+CJFg==", + "license": "ISC", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/node-windows": { + "version": "1.0.0-beta.8", + "resolved": "https://registry.npmjs.org/node-windows/-/node-windows-1.0.0-beta.8.tgz", + "integrity": "sha512-uLekXnSeem3nW5escID224Fd0U/1VtvE796JpSpOY+c73Cslz/Qn2WUHRJyPQJEMrNGAy/FMRFjjhh4z1alZTA==", + "license": "MIT", + "dependencies": { + "xml": "1.0.1", + "yargs": "^17.5.1" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "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==", + "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==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-expression-matcher": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/path-expression-matcher/-/path-expression-matcher-1.5.0.tgz", + "integrity": "sha512-cbrerZV+6rvdQrrD+iGMcZFEiiSrbv9Tfdkvnusy6y0x0GKBXREFg/Y65GhIfm0tnLntThhzCnfKwp1WRjeCyQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "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==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/qs": { + "version": "6.15.1", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.1.tgz", + "integrity": "sha512-6YHEFRL9mfgcAvql/XhwTvf5jKcOiiupt2FiJxHkiX1z4j7WL8J/jRHYLluORvc1XxB5rV20KoeK00gVJamspg==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz", + "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==", + "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/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/router": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", + "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", + "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/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/send": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz", + "integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.3", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "fresh": "^2.0.0", + "http-errors": "^2.0.1", + "mime-types": "^3.0.2", + "ms": "^2.1.3", + "on-finished": "^2.4.1", + "range-parser": "^1.2.1", + "statuses": "^2.0.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/serve-static": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.1.tgz", + "integrity": "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==", + "license": "MIT", + "dependencies": { + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "parseurl": "^1.3.3", + "send": "^1.2.0" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "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==", + "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==", + "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==", + "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/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "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/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strnum": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/strnum/-/strnum-2.3.0.tgz", + "integrity": "sha512-ums3KNd42PGyx5xaoVTO1mjU1bH3NpY4vsrVlnv9PNGqQj8wd7rJ6nEypLrJ7z5vxK5RP0yMLo6J/Gsm62DI5Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT" + }, + "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==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/type-is": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", + "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==", + "license": "MIT", + "dependencies": { + "content-type": "^1.0.5", + "media-typer": "^1.1.0", + "mime-types": "^3.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/typescript": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz", + "integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "7.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.21.0.tgz", + "integrity": "sha512-w9IMgQrz4O0YN1LtB7K5P63vhlIOvC7opSmouCJ+ZywlPAlO9gIkJ+otk6LvGpAs2wg4econaCz3TvQ9xPoyuQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "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/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" + }, + "node_modules/xml": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/xml/-/xml-1.0.1.tgz", + "integrity": "sha512-huCv9IH9Tcf95zuYCsQraZtWnJvBtLVE0QHMOs8bWyZAFZNDcYjsPq1nEx8jKA9y+Beo9v+7OBPRisQTjinQMw==", + "license": "MIT" + }, + "node_modules/xml-naming": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/xml-naming/-/xml-naming-0.1.0.tgz", + "integrity": "sha512-k8KO9hrMyNk6tUWqUfkTEZbezRRpONVOzUTnc97VnCvyj6Tf9lyUR9EDAIeiVLv56jsMcoXEwjW8Kv5yPY52lw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "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/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "license": "ISC", + "engines": { + "node": ">=12" + } + } + } +} diff --git a/package.json b/package.json index dbcece6..68fbe62 100644 --- a/package.json +++ b/package.json @@ -1,25 +1,42 @@ { - "name": "web-integrations-template", - "version": "1.0.0", - "description": "A sample web integrations project that can be run in a Windows Environment", + "name": "az-functions-onprem", + "version": "2.0.0", + "description": "Self-hosted, Azure-Functions-style integrations runtime for Windows Server or Docker behind the firewall.", + "type": "module", "main": "index.js", "scripts": { - "start": "nodemon", - "windows-install": "ts-node ./deploy/windows/install.ts", - "windows-uninstall": "ts-node ./deploy/windows/uninstall.ts", - "docker-package": "docker build -t \"web-integrations-template\" . && docker save -o .\\web-integrations-template-docker-image.tar web-integrations-template" + "dev": "node --watch ./src/server.ts", + "start": "node ./src/server.ts", + "configure": "node ./scripts/configure.ts", + "check": "npm run typecheck", + "typecheck": "tsc --noEmit", + "windows-install": "node ./deploy/windows/install.ts", + "windows-uninstall": "node ./deploy/windows/uninstall.ts", + "docker-package": "docker build -t az-functions-onprem . && docker save -o az-functions-onprem-docker-image.tar az-functions-onprem" }, "author": "https://github.com/alirobe", - "license": "ISC", + "license": "MIT", + "engines": { + "node": ">=23.6.0" + }, "dependencies": { - "@azure/storage-blob": "^12", - "@types/express": "^4", - "@types/node-windows": "^0", - "ejs": "^3", - "express": "^4", - "node-windows": "^1.0.0-beta.8", - "nodemon": "^3", - "ts-node": "^10", - "typescript": "^5" + "@azure/storage-blob": "^12.0.0", + "cors": "^2.8.6", + "ejs": "^5.0.0", + "express": "^5.0.0", + "helmet": "^8.0.0", + "node-cron": "^4.2.1", + "node-windows": "^1.0.0-beta.8" + }, + "devDependencies": { + "@types/cors": "^2.8.19", + "@types/express": "^5.0.0", + "@types/node": "^25.0.0", + "@types/node-windows": "^0.1.0", + "typescript": "^6.0.0" + }, + "overrides": { + "fast-xml-parser": "^5.8.0", + "fast-xml-builder": "^1.2.0" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml deleted file mode 100644 index 90c6d0c..0000000 --- a/pnpm-lock.yaml +++ /dev/null @@ -1,1495 +0,0 @@ -lockfileVersion: '9.0' - -settings: - autoInstallPeers: true - excludeLinksFromLockfile: false - -importers: - - .: - dependencies: - '@azure/storage-blob': - specifier: ^12 - version: 12.31.0 - '@types/express': - specifier: ^4 - version: 4.17.25 - '@types/node-windows': - specifier: ^0 - version: 0.1.6 - ejs: - specifier: ^3 - version: 3.1.10 - express: - specifier: ^4 - version: 4.22.1 - node-windows: - specifier: ^1.0.0-beta.8 - version: 1.0.0-beta.8 - nodemon: - specifier: ^3 - version: 3.1.14 - ts-node: - specifier: ^10 - version: 10.9.2(@types/node@25.5.2)(typescript@5.9.3) - typescript: - specifier: ^5 - version: 5.9.3 - -packages: - - '@azure/abort-controller@2.1.2': - resolution: {integrity: sha512-nBrLsEWm4J2u5LpAPjxADTlq3trDgVZZXHNKabeXZtpq3d3AbN/KGO82R87rdDz5/lYB024rtEf10/q0urNgsA==} - engines: {node: '>=18.0.0'} - - '@azure/core-auth@1.10.1': - resolution: {integrity: sha512-ykRMW8PjVAn+RS6ww5cmK9U2CyH9p4Q88YJwvUslfuMmN98w/2rdGRLPqJYObapBCdzBVeDgYWdJnFPFb7qzpg==} - engines: {node: '>=20.0.0'} - - '@azure/core-client@1.10.1': - resolution: {integrity: sha512-Nh5PhEOeY6PrnxNPsEHRr9eimxLwgLlpmguQaHKBinFYA/RU9+kOYVOQqOrTsCL+KSxrLLl1gD8Dk5BFW/7l/w==} - engines: {node: '>=20.0.0'} - - '@azure/core-http-compat@2.3.2': - resolution: {integrity: sha512-Tf6ltdKzOJEgxZeWLCjMxrxbodB/ZeCbzzA1A2qHbhzAjzjHoBVSUeSl/baT/oHAxhc4qdqVaDKnc2+iE932gw==} - engines: {node: '>=20.0.0'} - peerDependencies: - '@azure/core-client': ^1.10.0 - '@azure/core-rest-pipeline': ^1.22.0 - - '@azure/core-lro@2.7.2': - resolution: {integrity: sha512-0YIpccoX8m/k00O7mDDMdJpbr6mf1yWo2dfmxt5A8XVZVVMz2SSKaEbMCeJRvgQ0IaSlqhjT47p4hVIRRy90xw==} - engines: {node: '>=18.0.0'} - - '@azure/core-paging@1.6.2': - resolution: {integrity: sha512-YKWi9YuCU04B55h25cnOYZHxXYtEvQEbKST5vqRga7hWY9ydd3FZHdeQF8pyh+acWZvppw13M/LMGx0LABUVMA==} - engines: {node: '>=18.0.0'} - - '@azure/core-rest-pipeline@1.23.0': - resolution: {integrity: sha512-Evs1INHo+jUjwHi1T6SG6Ua/LHOQBCLuKEEE6efIpt4ZOoNonaT1kP32GoOcdNDbfqsD2445CPri3MubBy5DEQ==} - engines: {node: '>=20.0.0'} - - '@azure/core-tracing@1.3.1': - resolution: {integrity: sha512-9MWKevR7Hz8kNzzPLfX4EAtGM2b8mr50HPDBvio96bURP/9C+HjdH3sBlLSNNrvRAr5/k/svoH457gB5IKpmwQ==} - engines: {node: '>=20.0.0'} - - '@azure/core-util@1.13.1': - resolution: {integrity: sha512-XPArKLzsvl0Hf0CaGyKHUyVgF7oDnhKoP85Xv6M4StF/1AhfORhZudHtOyf2s+FcbuQ9dPRAjB8J2KvRRMUK2A==} - engines: {node: '>=20.0.0'} - - '@azure/core-xml@1.5.0': - resolution: {integrity: sha512-D/sdlJBMJfx7gqoj66PKVmhDDaU6TKA49ptcolxdas29X7AfvLTmfAGLjAcIMBK7UZ2o4lygHIqVckOlQU3xWw==} - engines: {node: '>=20.0.0'} - - '@azure/logger@1.3.0': - resolution: {integrity: sha512-fCqPIfOcLE+CGqGPd66c8bZpwAji98tZ4JI9i/mlTNTlsIWslCfpg48s/ypyLxZTump5sypjrKn2/kY7q8oAbA==} - engines: {node: '>=20.0.0'} - - '@azure/storage-blob@12.31.0': - resolution: {integrity: sha512-DBgNv10aCSxopt92DkTDD0o9xScXeBqPKGmR50FPZQaEcH4JLQ+GEOGEDv19V5BMkB7kxr+m4h6il/cCDPvmHg==} - engines: {node: '>=20.0.0'} - - '@azure/storage-common@12.3.0': - resolution: {integrity: sha512-/OFHhy86aG5Pe8dP5tsp+BuJ25JOAl9yaMU3WZbkeoiFMHFtJ7tu5ili7qEdBXNW9G5lDB19trwyI6V49F/8iQ==} - engines: {node: '>=20.0.0'} - - '@cspotcode/source-map-support@0.8.1': - resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==} - engines: {node: '>=12'} - - '@jridgewell/resolve-uri@3.1.2': - resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} - engines: {node: '>=6.0.0'} - - '@jridgewell/sourcemap-codec@1.5.5': - resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} - - '@jridgewell/trace-mapping@0.3.9': - resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==} - - '@tsconfig/node10@1.0.12': - resolution: {integrity: sha512-UCYBaeFvM11aU2y3YPZ//O5Rhj+xKyzy7mvcIoAjASbigy8mHMryP5cK7dgjlz2hWxh1g5pLw084E0a/wlUSFQ==} - - '@tsconfig/node12@1.0.11': - resolution: {integrity: sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==} - - '@tsconfig/node14@1.0.3': - resolution: {integrity: sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==} - - '@tsconfig/node16@1.0.4': - resolution: {integrity: sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==} - - '@types/body-parser@1.19.6': - resolution: {integrity: sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==} - - '@types/connect@3.4.38': - resolution: {integrity: sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==} - - '@types/express-serve-static-core@4.19.8': - resolution: {integrity: sha512-02S5fmqeoKzVZCHPZid4b8JH2eM5HzQLZWN2FohQEy/0eXTq8VXZfSN6Pcr3F6N9R/vNrj7cpgbhjie6m/1tCA==} - - '@types/express@4.17.25': - resolution: {integrity: sha512-dVd04UKsfpINUnK0yBoYHDF3xu7xVH4BuDotC/xGuycx4CgbP48X/KF/586bcObxT0HENHXEU8Nqtu6NR+eKhw==} - - '@types/http-errors@2.0.5': - resolution: {integrity: sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==} - - '@types/mime@1.3.5': - resolution: {integrity: sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==} - - '@types/node-windows@0.1.6': - resolution: {integrity: sha512-vbjaBtsF23Nprkw8rQFwskWzv5l6grWbUerYBHHjY/hqp/vIBAPGVQnr3dqsd75SwCQIAoZeBr/b+AbFFV/LfQ==} - - '@types/node@25.5.2': - resolution: {integrity: sha512-tO4ZIRKNC+MDWV4qKVZe3Ql/woTnmHDr5JD8UI5hn2pwBrHEwOEMZK7WlNb5RKB6EoJ02gwmQS9OrjuFnZYdpg==} - - '@types/qs@6.15.0': - resolution: {integrity: sha512-JawvT8iBVWpzTrz3EGw9BTQFg3BQNmwERdKE22vlTxawwtbyUSlMppvZYKLZzB5zgACXdXxbD3m1bXaMqP/9ow==} - - '@types/range-parser@1.2.7': - resolution: {integrity: sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==} - - '@types/send@0.17.6': - resolution: {integrity: sha512-Uqt8rPBE8SY0RK8JB1EzVOIZ32uqy8HwdxCnoCOsYrvnswqmFZ/k+9Ikidlk/ImhsdvBsloHbAlewb2IEBV/Og==} - - '@types/send@1.2.1': - resolution: {integrity: sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ==} - - '@types/serve-static@1.15.10': - resolution: {integrity: sha512-tRs1dB+g8Itk72rlSI2ZrW6vZg0YrLI81iQSTkMmOqnqCaNr/8Ek4VwWcN5vZgCYWbg/JJSGBlUaYGAOP73qBw==} - - '@typespec/ts-http-runtime@0.3.4': - resolution: {integrity: sha512-CI0NhTrz4EBaa0U+HaaUZrJhPoso8sG7ZFya8uQoBA57fjzrjRSv87ekCjLZOFExN+gXE/z0xuN2QfH4H2HrLQ==} - engines: {node: '>=20.0.0'} - - accepts@1.3.8: - resolution: {integrity: sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==} - engines: {node: '>= 0.6'} - - acorn-walk@8.3.5: - resolution: {integrity: sha512-HEHNfbars9v4pgpW6SO1KSPkfoS0xVOM/9UzkJltjlsHZmJasxg8aXkuZa7SMf8vKGIBhpUsPluQSqhJFCqebw==} - engines: {node: '>=0.4.0'} - - acorn@8.16.0: - resolution: {integrity: sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==} - engines: {node: '>=0.4.0'} - hasBin: true - - agent-base@7.1.4: - resolution: {integrity: sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==} - engines: {node: '>= 14'} - - ansi-regex@5.0.1: - resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} - engines: {node: '>=8'} - - ansi-styles@4.3.0: - resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} - engines: {node: '>=8'} - - anymatch@3.1.3: - resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==} - engines: {node: '>= 8'} - - arg@4.1.3: - resolution: {integrity: sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==} - - array-flatten@1.1.1: - resolution: {integrity: sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==} - - async@3.2.6: - resolution: {integrity: sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==} - - balanced-match@1.0.2: - resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} - - balanced-match@4.0.4: - resolution: {integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==} - engines: {node: 18 || 20 || >=22} - - binary-extensions@2.3.0: - resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==} - engines: {node: '>=8'} - - body-parser@1.20.4: - resolution: {integrity: sha512-ZTgYYLMOXY9qKU/57FAo8F+HA2dGX7bqGc71txDRC1rS4frdFI5R7NhluHxH6M0YItAP0sHB4uqAOcYKxO6uGA==} - engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} - - brace-expansion@2.0.3: - resolution: {integrity: sha512-MCV/fYJEbqx68aE58kv2cA/kiky1G8vux3OR6/jbS+jIMe/6fJWa0DTzJU7dqijOWYwHi1t29FlfYI9uytqlpA==} - - brace-expansion@5.0.5: - resolution: {integrity: sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==} - engines: {node: 18 || 20 || >=22} - - braces@3.0.3: - resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} - engines: {node: '>=8'} - - bytes@3.1.2: - resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==} - engines: {node: '>= 0.8'} - - call-bind-apply-helpers@1.0.2: - resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} - engines: {node: '>= 0.4'} - - call-bound@1.0.4: - resolution: {integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==} - engines: {node: '>= 0.4'} - - chokidar@3.6.0: - resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==} - engines: {node: '>= 8.10.0'} - - cliui@8.0.1: - resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==} - engines: {node: '>=12'} - - color-convert@2.0.1: - resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} - engines: {node: '>=7.0.0'} - - color-name@1.1.4: - resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} - - content-disposition@0.5.4: - resolution: {integrity: sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==} - engines: {node: '>= 0.6'} - - content-type@1.0.5: - resolution: {integrity: sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==} - engines: {node: '>= 0.6'} - - cookie-signature@1.0.7: - resolution: {integrity: sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==} - - cookie@0.7.2: - resolution: {integrity: sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==} - engines: {node: '>= 0.6'} - - create-require@1.1.1: - resolution: {integrity: sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==} - - debug@2.6.9: - resolution: {integrity: sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==} - peerDependencies: - supports-color: '*' - peerDependenciesMeta: - supports-color: - optional: true - - debug@4.4.3: - resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} - engines: {node: '>=6.0'} - peerDependencies: - supports-color: '*' - peerDependenciesMeta: - supports-color: - optional: true - - depd@2.0.0: - resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==} - engines: {node: '>= 0.8'} - - destroy@1.2.0: - resolution: {integrity: sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==} - engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} - - diff@4.0.4: - resolution: {integrity: sha512-X07nttJQkwkfKfvTPG/KSnE2OMdcUCao6+eXF3wmnIQRn2aPAHH3VxDbDOdegkd6JbPsXqShpvEOHfAT+nCNwQ==} - engines: {node: '>=0.3.1'} - - dunder-proto@1.0.1: - resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} - engines: {node: '>= 0.4'} - - ee-first@1.1.1: - resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} - - ejs@3.1.10: - resolution: {integrity: sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==} - engines: {node: '>=0.10.0'} - hasBin: true - - emoji-regex@8.0.0: - resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} - - encodeurl@2.0.0: - resolution: {integrity: sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==} - engines: {node: '>= 0.8'} - - es-define-property@1.0.1: - resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} - engines: {node: '>= 0.4'} - - es-errors@1.3.0: - resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} - engines: {node: '>= 0.4'} - - es-object-atoms@1.1.1: - resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==} - engines: {node: '>= 0.4'} - - escalade@3.2.0: - resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} - engines: {node: '>=6'} - - escape-html@1.0.3: - resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==} - - etag@1.8.1: - resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==} - engines: {node: '>= 0.6'} - - events@3.3.0: - resolution: {integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==} - engines: {node: '>=0.8.x'} - - express@4.22.1: - resolution: {integrity: sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g==} - engines: {node: '>= 0.10.0'} - - fast-xml-builder@1.1.4: - resolution: {integrity: sha512-f2jhpN4Eccy0/Uz9csxh3Nu6q4ErKxf0XIsasomfOihuSUa3/xw6w8dnOtCDgEItQFJG8KyXPzQXzcODDrrbOg==} - - fast-xml-parser@5.5.10: - resolution: {integrity: sha512-go2J2xODMc32hT+4Xr/bBGXMaIoiCwrwp2mMtAvKyvEFW6S/v5Gn2pBmE4nvbwNjGhpcAiOwEv7R6/GZ6XRa9w==} - hasBin: true - - filelist@1.0.6: - resolution: {integrity: sha512-5giy2PkLYY1cP39p17Ech+2xlpTRL9HLspOfEgm0L6CwBXBTgsK5ou0JtzYuepxkaQ/tvhCFIJ5uXo0OrM2DxA==} - - fill-range@7.1.1: - resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} - engines: {node: '>=8'} - - finalhandler@1.3.2: - resolution: {integrity: sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==} - engines: {node: '>= 0.8'} - - forwarded@0.2.0: - resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==} - engines: {node: '>= 0.6'} - - fresh@0.5.2: - resolution: {integrity: sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==} - engines: {node: '>= 0.6'} - - fsevents@2.3.3: - resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} - engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} - os: [darwin] - - function-bind@1.1.2: - resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} - - get-caller-file@2.0.5: - resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} - engines: {node: 6.* || 8.* || >= 10.*} - - get-intrinsic@1.3.0: - resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} - engines: {node: '>= 0.4'} - - get-proto@1.0.1: - resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} - engines: {node: '>= 0.4'} - - glob-parent@5.1.2: - resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} - engines: {node: '>= 6'} - - gopd@1.2.0: - resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} - engines: {node: '>= 0.4'} - - has-flag@3.0.0: - resolution: {integrity: sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==} - engines: {node: '>=4'} - - has-symbols@1.1.0: - resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} - engines: {node: '>= 0.4'} - - hasown@2.0.2: - resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} - engines: {node: '>= 0.4'} - - http-errors@2.0.1: - resolution: {integrity: sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==} - engines: {node: '>= 0.8'} - - http-proxy-agent@7.0.2: - resolution: {integrity: sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==} - engines: {node: '>= 14'} - - https-proxy-agent@7.0.6: - resolution: {integrity: sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==} - engines: {node: '>= 14'} - - iconv-lite@0.4.24: - resolution: {integrity: sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==} - engines: {node: '>=0.10.0'} - - ignore-by-default@1.0.1: - resolution: {integrity: sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==} - - inherits@2.0.4: - resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} - - ipaddr.js@1.9.1: - resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==} - engines: {node: '>= 0.10'} - - is-binary-path@2.1.0: - resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==} - engines: {node: '>=8'} - - is-extglob@2.1.1: - resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} - engines: {node: '>=0.10.0'} - - is-fullwidth-code-point@3.0.0: - resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} - engines: {node: '>=8'} - - is-glob@4.0.3: - resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} - engines: {node: '>=0.10.0'} - - is-number@7.0.0: - resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} - engines: {node: '>=0.12.0'} - - jake@10.9.4: - resolution: {integrity: sha512-wpHYzhxiVQL+IV05BLE2Xn34zW1S223hvjtqk0+gsPrwd/8JNLXJgZZM/iPFsYc1xyphF+6M6EvdE5E9MBGkDA==} - engines: {node: '>=10'} - hasBin: true - - make-error@1.3.6: - resolution: {integrity: sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==} - - math-intrinsics@1.1.0: - resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} - engines: {node: '>= 0.4'} - - media-typer@0.3.0: - resolution: {integrity: sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==} - engines: {node: '>= 0.6'} - - merge-descriptors@1.0.3: - resolution: {integrity: sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==} - - methods@1.1.2: - resolution: {integrity: sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==} - engines: {node: '>= 0.6'} - - mime-db@1.52.0: - resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} - engines: {node: '>= 0.6'} - - mime-types@2.1.35: - resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} - engines: {node: '>= 0.6'} - - mime@1.6.0: - resolution: {integrity: sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==} - engines: {node: '>=4'} - hasBin: true - - minimatch@10.2.5: - resolution: {integrity: sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==} - engines: {node: 18 || 20 || >=22} - - minimatch@5.1.9: - resolution: {integrity: sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw==} - engines: {node: '>=10'} - - ms@2.0.0: - resolution: {integrity: sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==} - - ms@2.1.3: - resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} - - negotiator@0.6.3: - resolution: {integrity: sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==} - engines: {node: '>= 0.6'} - - node-windows@1.0.0-beta.8: - resolution: {integrity: sha512-uLekXnSeem3nW5escID224Fd0U/1VtvE796JpSpOY+c73Cslz/Qn2WUHRJyPQJEMrNGAy/FMRFjjhh4z1alZTA==} - - nodemon@3.1.14: - resolution: {integrity: sha512-jakjZi93UtB3jHMWsXL68FXSAosbLfY0In5gtKq3niLSkrWznrVBzXFNOEMJUfc9+Ke7SHWoAZsiMkNP3vq6Jw==} - engines: {node: '>=10'} - hasBin: true - - normalize-path@3.0.0: - resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} - engines: {node: '>=0.10.0'} - - object-inspect@1.13.4: - resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==} - engines: {node: '>= 0.4'} - - on-finished@2.4.1: - resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==} - engines: {node: '>= 0.8'} - - parseurl@1.3.3: - resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==} - engines: {node: '>= 0.8'} - - path-expression-matcher@1.3.0: - resolution: {integrity: sha512-tkolHg8cWjEFA8+TqYGk0w6aNEZVcb7pVxW8KXZpU+ebaBr3s1ogbLssjK1cL74TtEuKl/qy6cb90RnwTFt3kw==} - engines: {node: '>=14.0.0'} - - path-to-regexp@0.1.13: - resolution: {integrity: sha512-A/AGNMFN3c8bOlvV9RreMdrv7jsmF9XIfDeCd87+I8RNg6s78BhJxMu69NEMHBSJFxKidViTEdruRwEk/WIKqA==} - - picocolors@1.1.1: - resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} - - picomatch@2.3.2: - resolution: {integrity: sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==} - engines: {node: '>=8.6'} - - proxy-addr@2.0.7: - resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==} - engines: {node: '>= 0.10'} - - pstree.remy@1.1.8: - resolution: {integrity: sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==} - - qs@6.14.2: - resolution: {integrity: sha512-V/yCWTTF7VJ9hIh18Ugr2zhJMP01MY7c5kh4J870L7imm6/DIzBsNLTXzMwUA3yZ5b/KBqLx8Kp3uRvd7xSe3Q==} - engines: {node: '>=0.6'} - - range-parser@1.2.1: - resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==} - engines: {node: '>= 0.6'} - - raw-body@2.5.3: - resolution: {integrity: sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==} - engines: {node: '>= 0.8'} - - readdirp@3.6.0: - resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} - engines: {node: '>=8.10.0'} - - require-directory@2.1.1: - resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} - engines: {node: '>=0.10.0'} - - safe-buffer@5.2.1: - resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} - - safer-buffer@2.1.2: - resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} - - semver@7.7.4: - resolution: {integrity: sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==} - engines: {node: '>=10'} - hasBin: true - - send@0.19.2: - resolution: {integrity: sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==} - engines: {node: '>= 0.8.0'} - - serve-static@1.16.3: - resolution: {integrity: sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==} - engines: {node: '>= 0.8.0'} - - setprototypeof@1.2.0: - resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==} - - side-channel-list@1.0.0: - resolution: {integrity: sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==} - engines: {node: '>= 0.4'} - - side-channel-map@1.0.1: - resolution: {integrity: sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==} - engines: {node: '>= 0.4'} - - side-channel-weakmap@1.0.2: - resolution: {integrity: sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==} - engines: {node: '>= 0.4'} - - side-channel@1.1.0: - resolution: {integrity: sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==} - engines: {node: '>= 0.4'} - - simple-update-notifier@2.0.0: - resolution: {integrity: sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==} - engines: {node: '>=10'} - - statuses@2.0.2: - resolution: {integrity: sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==} - engines: {node: '>= 0.8'} - - string-width@4.2.3: - resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} - engines: {node: '>=8'} - - strip-ansi@6.0.1: - resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} - engines: {node: '>=8'} - - strnum@2.2.3: - resolution: {integrity: sha512-oKx6RUCuHfT3oyVjtnrmn19H1SiCqgJSg+54XqURKp5aCMbrXrhLjRN9TjuwMjiYstZ0MzDrHqkGZ5dFTKd+zg==} - - supports-color@5.5.0: - resolution: {integrity: sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==} - engines: {node: '>=4'} - - to-regex-range@5.0.1: - resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} - engines: {node: '>=8.0'} - - toidentifier@1.0.1: - resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==} - engines: {node: '>=0.6'} - - touch@3.1.1: - resolution: {integrity: sha512-r0eojU4bI8MnHr8c5bNo7lJDdI2qXlWWJk6a9EAFG7vbhTjElYhBVS3/miuE0uOuoLdb8Mc/rVfsmm6eo5o9GA==} - hasBin: true - - ts-node@10.9.2: - resolution: {integrity: sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==} - hasBin: true - peerDependencies: - '@swc/core': '>=1.2.50' - '@swc/wasm': '>=1.2.50' - '@types/node': '*' - typescript: '>=2.7' - peerDependenciesMeta: - '@swc/core': - optional: true - '@swc/wasm': - optional: true - - tslib@2.8.1: - resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} - - type-is@1.6.18: - resolution: {integrity: sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==} - engines: {node: '>= 0.6'} - - typescript@5.9.3: - resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} - engines: {node: '>=14.17'} - hasBin: true - - undefsafe@2.0.5: - resolution: {integrity: sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==} - - undici-types@7.18.2: - resolution: {integrity: sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==} - - unpipe@1.0.0: - resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==} - engines: {node: '>= 0.8'} - - utils-merge@1.0.1: - resolution: {integrity: sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==} - engines: {node: '>= 0.4.0'} - - v8-compile-cache-lib@3.0.1: - resolution: {integrity: sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==} - - vary@1.1.2: - resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} - engines: {node: '>= 0.8'} - - wrap-ansi@7.0.0: - resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} - engines: {node: '>=10'} - - xml@1.0.1: - resolution: {integrity: sha512-huCv9IH9Tcf95zuYCsQraZtWnJvBtLVE0QHMOs8bWyZAFZNDcYjsPq1nEx8jKA9y+Beo9v+7OBPRisQTjinQMw==} - - y18n@5.0.8: - resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} - engines: {node: '>=10'} - - yargs-parser@21.1.1: - resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==} - engines: {node: '>=12'} - - yargs@17.7.2: - resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==} - engines: {node: '>=12'} - - yn@3.1.1: - resolution: {integrity: sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==} - engines: {node: '>=6'} - -snapshots: - - '@azure/abort-controller@2.1.2': - dependencies: - tslib: 2.8.1 - - '@azure/core-auth@1.10.1': - dependencies: - '@azure/abort-controller': 2.1.2 - '@azure/core-util': 1.13.1 - tslib: 2.8.1 - transitivePeerDependencies: - - supports-color - - '@azure/core-client@1.10.1': - dependencies: - '@azure/abort-controller': 2.1.2 - '@azure/core-auth': 1.10.1 - '@azure/core-rest-pipeline': 1.23.0 - '@azure/core-tracing': 1.3.1 - '@azure/core-util': 1.13.1 - '@azure/logger': 1.3.0 - tslib: 2.8.1 - transitivePeerDependencies: - - supports-color - - '@azure/core-http-compat@2.3.2(@azure/core-client@1.10.1)(@azure/core-rest-pipeline@1.23.0)': - dependencies: - '@azure/abort-controller': 2.1.2 - '@azure/core-client': 1.10.1 - '@azure/core-rest-pipeline': 1.23.0 - - '@azure/core-lro@2.7.2': - dependencies: - '@azure/abort-controller': 2.1.2 - '@azure/core-util': 1.13.1 - '@azure/logger': 1.3.0 - tslib: 2.8.1 - transitivePeerDependencies: - - supports-color - - '@azure/core-paging@1.6.2': - dependencies: - tslib: 2.8.1 - - '@azure/core-rest-pipeline@1.23.0': - dependencies: - '@azure/abort-controller': 2.1.2 - '@azure/core-auth': 1.10.1 - '@azure/core-tracing': 1.3.1 - '@azure/core-util': 1.13.1 - '@azure/logger': 1.3.0 - '@typespec/ts-http-runtime': 0.3.4 - tslib: 2.8.1 - transitivePeerDependencies: - - supports-color - - '@azure/core-tracing@1.3.1': - dependencies: - tslib: 2.8.1 - - '@azure/core-util@1.13.1': - dependencies: - '@azure/abort-controller': 2.1.2 - '@typespec/ts-http-runtime': 0.3.4 - tslib: 2.8.1 - transitivePeerDependencies: - - supports-color - - '@azure/core-xml@1.5.0': - dependencies: - fast-xml-parser: 5.5.10 - tslib: 2.8.1 - - '@azure/logger@1.3.0': - dependencies: - '@typespec/ts-http-runtime': 0.3.4 - tslib: 2.8.1 - transitivePeerDependencies: - - supports-color - - '@azure/storage-blob@12.31.0': - dependencies: - '@azure/abort-controller': 2.1.2 - '@azure/core-auth': 1.10.1 - '@azure/core-client': 1.10.1 - '@azure/core-http-compat': 2.3.2(@azure/core-client@1.10.1)(@azure/core-rest-pipeline@1.23.0) - '@azure/core-lro': 2.7.2 - '@azure/core-paging': 1.6.2 - '@azure/core-rest-pipeline': 1.23.0 - '@azure/core-tracing': 1.3.1 - '@azure/core-util': 1.13.1 - '@azure/core-xml': 1.5.0 - '@azure/logger': 1.3.0 - '@azure/storage-common': 12.3.0(@azure/core-client@1.10.1) - events: 3.3.0 - tslib: 2.8.1 - transitivePeerDependencies: - - supports-color - - '@azure/storage-common@12.3.0(@azure/core-client@1.10.1)': - dependencies: - '@azure/abort-controller': 2.1.2 - '@azure/core-auth': 1.10.1 - '@azure/core-http-compat': 2.3.2(@azure/core-client@1.10.1)(@azure/core-rest-pipeline@1.23.0) - '@azure/core-rest-pipeline': 1.23.0 - '@azure/core-tracing': 1.3.1 - '@azure/core-util': 1.13.1 - '@azure/logger': 1.3.0 - events: 3.3.0 - tslib: 2.8.1 - transitivePeerDependencies: - - '@azure/core-client' - - supports-color - - '@cspotcode/source-map-support@0.8.1': - dependencies: - '@jridgewell/trace-mapping': 0.3.9 - - '@jridgewell/resolve-uri@3.1.2': {} - - '@jridgewell/sourcemap-codec@1.5.5': {} - - '@jridgewell/trace-mapping@0.3.9': - dependencies: - '@jridgewell/resolve-uri': 3.1.2 - '@jridgewell/sourcemap-codec': 1.5.5 - - '@tsconfig/node10@1.0.12': {} - - '@tsconfig/node12@1.0.11': {} - - '@tsconfig/node14@1.0.3': {} - - '@tsconfig/node16@1.0.4': {} - - '@types/body-parser@1.19.6': - dependencies: - '@types/connect': 3.4.38 - '@types/node': 25.5.2 - - '@types/connect@3.4.38': - dependencies: - '@types/node': 25.5.2 - - '@types/express-serve-static-core@4.19.8': - dependencies: - '@types/node': 25.5.2 - '@types/qs': 6.15.0 - '@types/range-parser': 1.2.7 - '@types/send': 1.2.1 - - '@types/express@4.17.25': - dependencies: - '@types/body-parser': 1.19.6 - '@types/express-serve-static-core': 4.19.8 - '@types/qs': 6.15.0 - '@types/serve-static': 1.15.10 - - '@types/http-errors@2.0.5': {} - - '@types/mime@1.3.5': {} - - '@types/node-windows@0.1.6': - dependencies: - '@types/node': 25.5.2 - - '@types/node@25.5.2': - dependencies: - undici-types: 7.18.2 - - '@types/qs@6.15.0': {} - - '@types/range-parser@1.2.7': {} - - '@types/send@0.17.6': - dependencies: - '@types/mime': 1.3.5 - '@types/node': 25.5.2 - - '@types/send@1.2.1': - dependencies: - '@types/node': 25.5.2 - - '@types/serve-static@1.15.10': - dependencies: - '@types/http-errors': 2.0.5 - '@types/node': 25.5.2 - '@types/send': 0.17.6 - - '@typespec/ts-http-runtime@0.3.4': - dependencies: - http-proxy-agent: 7.0.2 - https-proxy-agent: 7.0.6 - tslib: 2.8.1 - transitivePeerDependencies: - - supports-color - - accepts@1.3.8: - dependencies: - mime-types: 2.1.35 - negotiator: 0.6.3 - - acorn-walk@8.3.5: - dependencies: - acorn: 8.16.0 - - acorn@8.16.0: {} - - agent-base@7.1.4: {} - - ansi-regex@5.0.1: {} - - ansi-styles@4.3.0: - dependencies: - color-convert: 2.0.1 - - anymatch@3.1.3: - dependencies: - normalize-path: 3.0.0 - picomatch: 2.3.2 - - arg@4.1.3: {} - - array-flatten@1.1.1: {} - - async@3.2.6: {} - - balanced-match@1.0.2: {} - - balanced-match@4.0.4: {} - - binary-extensions@2.3.0: {} - - body-parser@1.20.4: - 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.2 - raw-body: 2.5.3 - type-is: 1.6.18 - unpipe: 1.0.0 - transitivePeerDependencies: - - supports-color - - brace-expansion@2.0.3: - dependencies: - balanced-match: 1.0.2 - - brace-expansion@5.0.5: - dependencies: - balanced-match: 4.0.4 - - braces@3.0.3: - dependencies: - fill-range: 7.1.1 - - bytes@3.1.2: {} - - call-bind-apply-helpers@1.0.2: - dependencies: - es-errors: 1.3.0 - function-bind: 1.1.2 - - call-bound@1.0.4: - dependencies: - call-bind-apply-helpers: 1.0.2 - get-intrinsic: 1.3.0 - - chokidar@3.6.0: - dependencies: - anymatch: 3.1.3 - braces: 3.0.3 - glob-parent: 5.1.2 - is-binary-path: 2.1.0 - is-glob: 4.0.3 - normalize-path: 3.0.0 - readdirp: 3.6.0 - optionalDependencies: - fsevents: 2.3.3 - - cliui@8.0.1: - dependencies: - string-width: 4.2.3 - strip-ansi: 6.0.1 - wrap-ansi: 7.0.0 - - color-convert@2.0.1: - dependencies: - color-name: 1.1.4 - - color-name@1.1.4: {} - - content-disposition@0.5.4: - dependencies: - safe-buffer: 5.2.1 - - content-type@1.0.5: {} - - cookie-signature@1.0.7: {} - - cookie@0.7.2: {} - - create-require@1.1.1: {} - - debug@2.6.9: - dependencies: - ms: 2.0.0 - - debug@4.4.3(supports-color@5.5.0): - dependencies: - ms: 2.1.3 - optionalDependencies: - supports-color: 5.5.0 - - depd@2.0.0: {} - - destroy@1.2.0: {} - - diff@4.0.4: {} - - dunder-proto@1.0.1: - dependencies: - call-bind-apply-helpers: 1.0.2 - es-errors: 1.3.0 - gopd: 1.2.0 - - ee-first@1.1.1: {} - - ejs@3.1.10: - dependencies: - jake: 10.9.4 - - emoji-regex@8.0.0: {} - - encodeurl@2.0.0: {} - - es-define-property@1.0.1: {} - - es-errors@1.3.0: {} - - es-object-atoms@1.1.1: - dependencies: - es-errors: 1.3.0 - - escalade@3.2.0: {} - - escape-html@1.0.3: {} - - etag@1.8.1: {} - - events@3.3.0: {} - - express@4.22.1: - dependencies: - accepts: 1.3.8 - array-flatten: 1.1.1 - body-parser: 1.20.4 - content-disposition: 0.5.4 - content-type: 1.0.5 - cookie: 0.7.2 - cookie-signature: 1.0.7 - debug: 2.6.9 - depd: 2.0.0 - encodeurl: 2.0.0 - escape-html: 1.0.3 - etag: 1.8.1 - finalhandler: 1.3.2 - fresh: 0.5.2 - http-errors: 2.0.1 - 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.14.2 - range-parser: 1.2.1 - safe-buffer: 5.2.1 - send: 0.19.2 - serve-static: 1.16.3 - setprototypeof: 1.2.0 - statuses: 2.0.2 - type-is: 1.6.18 - utils-merge: 1.0.1 - vary: 1.1.2 - transitivePeerDependencies: - - supports-color - - fast-xml-builder@1.1.4: - dependencies: - path-expression-matcher: 1.3.0 - - fast-xml-parser@5.5.10: - dependencies: - fast-xml-builder: 1.1.4 - path-expression-matcher: 1.3.0 - strnum: 2.2.3 - - filelist@1.0.6: - dependencies: - minimatch: 5.1.9 - - fill-range@7.1.1: - dependencies: - to-regex-range: 5.0.1 - - finalhandler@1.3.2: - 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 - transitivePeerDependencies: - - supports-color - - forwarded@0.2.0: {} - - fresh@0.5.2: {} - - fsevents@2.3.3: - optional: true - - function-bind@1.1.2: {} - - get-caller-file@2.0.5: {} - - get-intrinsic@1.3.0: - dependencies: - call-bind-apply-helpers: 1.0.2 - es-define-property: 1.0.1 - es-errors: 1.3.0 - es-object-atoms: 1.1.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 - - get-proto@1.0.1: - dependencies: - dunder-proto: 1.0.1 - es-object-atoms: 1.1.1 - - glob-parent@5.1.2: - dependencies: - is-glob: 4.0.3 - - gopd@1.2.0: {} - - has-flag@3.0.0: {} - - has-symbols@1.1.0: {} - - hasown@2.0.2: - dependencies: - function-bind: 1.1.2 - - http-errors@2.0.1: - dependencies: - depd: 2.0.0 - inherits: 2.0.4 - setprototypeof: 1.2.0 - statuses: 2.0.2 - toidentifier: 1.0.1 - - http-proxy-agent@7.0.2: - dependencies: - agent-base: 7.1.4 - debug: 4.4.3(supports-color@5.5.0) - transitivePeerDependencies: - - supports-color - - https-proxy-agent@7.0.6: - dependencies: - agent-base: 7.1.4 - debug: 4.4.3(supports-color@5.5.0) - transitivePeerDependencies: - - supports-color - - iconv-lite@0.4.24: - dependencies: - safer-buffer: 2.1.2 - - ignore-by-default@1.0.1: {} - - inherits@2.0.4: {} - - ipaddr.js@1.9.1: {} - - is-binary-path@2.1.0: - dependencies: - binary-extensions: 2.3.0 - - is-extglob@2.1.1: {} - - is-fullwidth-code-point@3.0.0: {} - - is-glob@4.0.3: - dependencies: - is-extglob: 2.1.1 - - is-number@7.0.0: {} - - jake@10.9.4: - dependencies: - async: 3.2.6 - filelist: 1.0.6 - picocolors: 1.1.1 - - make-error@1.3.6: {} - - math-intrinsics@1.1.0: {} - - media-typer@0.3.0: {} - - merge-descriptors@1.0.3: {} - - methods@1.1.2: {} - - mime-db@1.52.0: {} - - mime-types@2.1.35: - dependencies: - mime-db: 1.52.0 - - mime@1.6.0: {} - - minimatch@10.2.5: - dependencies: - brace-expansion: 5.0.5 - - minimatch@5.1.9: - dependencies: - brace-expansion: 2.0.3 - - ms@2.0.0: {} - - ms@2.1.3: {} - - negotiator@0.6.3: {} - - node-windows@1.0.0-beta.8: - dependencies: - xml: 1.0.1 - yargs: 17.7.2 - - nodemon@3.1.14: - dependencies: - chokidar: 3.6.0 - debug: 4.4.3(supports-color@5.5.0) - ignore-by-default: 1.0.1 - minimatch: 10.2.5 - pstree.remy: 1.1.8 - semver: 7.7.4 - simple-update-notifier: 2.0.0 - supports-color: 5.5.0 - touch: 3.1.1 - undefsafe: 2.0.5 - - normalize-path@3.0.0: {} - - object-inspect@1.13.4: {} - - on-finished@2.4.1: - dependencies: - ee-first: 1.1.1 - - parseurl@1.3.3: {} - - path-expression-matcher@1.3.0: {} - - path-to-regexp@0.1.13: {} - - picocolors@1.1.1: {} - - picomatch@2.3.2: {} - - proxy-addr@2.0.7: - dependencies: - forwarded: 0.2.0 - ipaddr.js: 1.9.1 - - pstree.remy@1.1.8: {} - - qs@6.14.2: - dependencies: - side-channel: 1.1.0 - - range-parser@1.2.1: {} - - raw-body@2.5.3: - dependencies: - bytes: 3.1.2 - http-errors: 2.0.1 - iconv-lite: 0.4.24 - unpipe: 1.0.0 - - readdirp@3.6.0: - dependencies: - picomatch: 2.3.2 - - require-directory@2.1.1: {} - - safe-buffer@5.2.1: {} - - safer-buffer@2.1.2: {} - - semver@7.7.4: {} - - send@0.19.2: - 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 - transitivePeerDependencies: - - supports-color - - serve-static@1.16.3: - dependencies: - encodeurl: 2.0.0 - escape-html: 1.0.3 - parseurl: 1.3.3 - send: 0.19.2 - transitivePeerDependencies: - - supports-color - - setprototypeof@1.2.0: {} - - side-channel-list@1.0.0: - dependencies: - es-errors: 1.3.0 - object-inspect: 1.13.4 - - side-channel-map@1.0.1: - dependencies: - call-bound: 1.0.4 - es-errors: 1.3.0 - get-intrinsic: 1.3.0 - object-inspect: 1.13.4 - - side-channel-weakmap@1.0.2: - dependencies: - call-bound: 1.0.4 - es-errors: 1.3.0 - get-intrinsic: 1.3.0 - object-inspect: 1.13.4 - side-channel-map: 1.0.1 - - side-channel@1.1.0: - dependencies: - es-errors: 1.3.0 - object-inspect: 1.13.4 - side-channel-list: 1.0.0 - side-channel-map: 1.0.1 - side-channel-weakmap: 1.0.2 - - simple-update-notifier@2.0.0: - dependencies: - semver: 7.7.4 - - statuses@2.0.2: {} - - string-width@4.2.3: - dependencies: - emoji-regex: 8.0.0 - is-fullwidth-code-point: 3.0.0 - strip-ansi: 6.0.1 - - strip-ansi@6.0.1: - dependencies: - ansi-regex: 5.0.1 - - strnum@2.2.3: {} - - supports-color@5.5.0: - dependencies: - has-flag: 3.0.0 - - to-regex-range@5.0.1: - dependencies: - is-number: 7.0.0 - - toidentifier@1.0.1: {} - - touch@3.1.1: {} - - ts-node@10.9.2(@types/node@25.5.2)(typescript@5.9.3): - dependencies: - '@cspotcode/source-map-support': 0.8.1 - '@tsconfig/node10': 1.0.12 - '@tsconfig/node12': 1.0.11 - '@tsconfig/node14': 1.0.3 - '@tsconfig/node16': 1.0.4 - '@types/node': 25.5.2 - acorn: 8.16.0 - acorn-walk: 8.3.5 - arg: 4.1.3 - create-require: 1.1.1 - diff: 4.0.4 - make-error: 1.3.6 - typescript: 5.9.3 - v8-compile-cache-lib: 3.0.1 - yn: 3.1.1 - - tslib@2.8.1: {} - - type-is@1.6.18: - dependencies: - media-typer: 0.3.0 - mime-types: 2.1.35 - - typescript@5.9.3: {} - - undefsafe@2.0.5: {} - - undici-types@7.18.2: {} - - unpipe@1.0.0: {} - - utils-merge@1.0.1: {} - - v8-compile-cache-lib@3.0.1: {} - - vary@1.1.2: {} - - wrap-ansi@7.0.0: - dependencies: - ansi-styles: 4.3.0 - string-width: 4.2.3 - strip-ansi: 6.0.1 - - xml@1.0.1: {} - - y18n@5.0.8: {} - - yargs-parser@21.1.1: {} - - yargs@17.7.2: - dependencies: - cliui: 8.0.1 - escalade: 3.2.0 - get-caller-file: 2.0.5 - require-directory: 2.1.1 - string-width: 4.2.3 - y18n: 5.0.8 - yargs-parser: 21.1.1 - - yn@3.1.1: {} diff --git a/scripts/configure.ts b/scripts/configure.ts new file mode 100644 index 0000000..db136f8 --- /dev/null +++ b/scripts/configure.ts @@ -0,0 +1,82 @@ +import { access, writeFile } from "node:fs/promises"; +import { randomUUID } from "node:crypto"; +import { createInterface } from "node:readline/promises"; +import { stdin, stdout, exit } from "node:process"; + +const TARGET = "./local.settings.json"; +const SERVICE_TYPES = ["docker", "windows", "dev"] as const; +type ServiceType = (typeof SERVICE_TYPES)[number]; + +async function exists(p: string): Promise { + try { + await access(p); + return true; + } catch { + return false; + } +} + +async function main() { + const rl = createInterface({ input: stdin, output: stdout }); + const ask = async (label: string, fallback?: string): Promise => { + const prompt = fallback ? `${label} [${fallback}]: ` : `${label}: `; + const answer = (await rl.question(prompt)).trim(); + return answer || fallback || ""; + }; + + if (await exists(TARGET)) { + const overwrite = await ask("local.settings.json already exists. Overwrite? (y/N)", "N"); + if (overwrite.toLowerCase() !== "y") { + console.log("Aborted. Existing file left in place."); + rl.close(); + return; + } + } + + console.log("\nConfiguring local.settings.json. Press Enter to accept the default in brackets.\n"); + + let serviceType: ServiceType | undefined; + while (!serviceType) { + const input = (await ask(`SERVICE_TYPE (${SERVICE_TYPES.join("|")})`)).toLowerCase(); + if (SERVICE_TYPES.includes(input as ServiceType)) { + serviceType = input as ServiceType; + } else { + console.log(` must be one of: ${SERVICE_TYPES.join(", ")}`); + } + } + + const azureToken = await ask("AZURE_CUSTOM_HEADER_TOKEN (proxy header secret)", randomUUID()); + const apiKey = await ask("WEB_INTEGRATIONS_API_KEY (query-string fallback secret)", randomUUID()); + const blobSas = await ask("WEBSITE_BLOB_SAS (paste full SAS URL or leave blank)", ""); + const portInput = await ask("PORT", "3000"); + const port = Number.parseInt(portInput, 10); + if (!Number.isInteger(port) || port < 1 || port > 65535) { + console.error(`PORT must be an integer between 1 and 65535 (got "${portInput}")`); + rl.close(); + exit(1); + } + + const payload = { + $schema: "./local.settings.schema.json", + Values: { + AZURE_CUSTOM_HEADER_TOKEN: azureToken, + SERVICE_TYPE: serviceType, + WEB_INTEGRATIONS_API_KEY: apiKey, + WEBSITE_BLOB_SAS: blobSas, + PORT: port, + }, + }; + + await writeFile(TARGET, JSON.stringify(payload, null, 4) + "\n", { encoding: "utf8", mode: 0o600 }); + rl.close(); + + console.log(`\nWrote ${TARGET} (mode 0600).`); + console.log(` SERVICE_TYPE: ${serviceType}`); + console.log(` PORT: ${port}`); + if (!blobSas) { + console.log(" WEBSITE_BLOB_SAS is blank; uploadBlob calls will fail until you set it."); + } + console.log("\nNext: npm run dev"); +} + +main(); diff --git a/src/actions/README.md b/src/actions/README.md new file mode 100644 index 0000000..d37232c --- /dev/null +++ b/src/actions/README.md @@ -0,0 +1,47 @@ +# actions + +This folder is yours. Define integration actions here. + +```ts +// src/actions/sync-customers.ts +import { defineAction } from "../runtime/registry.ts"; + +defineAction({ + name: "syncCustomers", + description: "Pull customers from CRM, write to blob.", + handler: async (ctx) => { + // ... your work + return { count: 42 }; + }, +}); +``` + +Then wire it up in `src/actions/index.ts`: + +```ts +import "./sync-customers.ts"; +``` + +The runtime auto-registers the route (`GET /action/syncCustomers`), the index-page card, the OpenAPI entry, and, if you set a `schedule`, the cron job. + +## Action forms + +An action takes one of three forms, picked by which field you supply: + +- `handler: (ctx) => result` - one-shot, JSON wrapped as `{ action, result }`. +- `steps: ["a", "b"]` - sequence of other registered actions; streamed as NDJSON step events. +- `stream: async function*(ctx) { yield ... }` - async generator, NDJSON one line per yield. + +All three accept optional `timeoutMs`, `onSuccess(result, ctx)`, `schedule` (cron), and `auth` (`anonymous` / `key` / `proxy`). + +## Sample files + +`sample-handler.ts`, `sample-stream.ts`, `sample-sequence.ts` are starter examples. Delete them when you're writing your own. + +## Where things live + +- Runtime (framework, don't touch normally): `src/runtime/` +- Your actions: this folder +- Your helpers: `src/helpers/` (e.g. `sample-upload-blob.ts`) +- Service config: `src/config/` +- UI: `src/views/`, `src/public/` diff --git a/src/actions/examples/inbound-webhook.ts b/src/actions/examples/inbound-webhook.ts new file mode 100644 index 0000000..dad2922 --- /dev/null +++ b/src/actions/examples/inbound-webhook.ts @@ -0,0 +1,41 @@ +// Recipe: receive a POST webhook, verify HMAC, project into internal storage. +// +// Common use case: a SaaS (Stripe, GitHub, Slack, custom partner) sends events +// your on-prem systems need to react to but can't expose to the public internet. +// Run this template behind Azure App Proxy, take the webhook at /action/inboundWebhook, +// verify the provider's signature, then write into your DB / SharePoint / event bus. +// +// Replace WEBHOOK_SECRET with whatever your provider gave you. +// +// Note: real providers usually require the **raw** request body for HMAC. +// express.json discards that by default - capture it via +// `express.json({ verify: (req, res, buf) => { (req as any).rawBody = buf; } })` +// and read `(ctx.req as any).rawBody` here. The example below verifies against +// the parsed body, which works only if the provider signed the canonical JSON +// form you'll re-serialise. Read your provider's signature docs. + +import { defineAction, httpCtx } from "../../runtime/registry.ts"; +import { verifyHmac } from "../../helpers/sample-verify-hmac.ts"; + +defineAction({ + name: "inboundWebhook", + description: "Receive a partner webhook, verify HMAC, hand off to internal systems.", + method: "POST", + auth: "anonymous", // The HMAC is the auth; our key/proxy gates do not apply here. + handler: async (ctx) => { + const { req } = httpCtx(ctx); + const secret = process.env.WEBHOOK_SECRET ?? ""; + const signature = String(req.get("x-signature") ?? ""); + const raw = JSON.stringify(req.body); + + if (!verifyHmac(secret, raw, signature)) { + throw new Error("Invalid signature"); + } + + // Do something with the verified payload - write to a queue, file, DB, etc. + // const payload = req.body; + // await yourInternalSink(payload); + + return { received: true }; + }, +}); diff --git a/src/actions/examples/mssql-to-blob.ts b/src/actions/examples/mssql-to-blob.ts new file mode 100644 index 0000000..ff7f478 --- /dev/null +++ b/src/actions/examples/mssql-to-blob.ts @@ -0,0 +1,38 @@ +// Recipe: SQL Server table → JSON blob upload. +// +// Prerequisite: `npm install mssql` +// +// Common use case: nightly snapshot of an on-prem ERP table (Dynamics GP, +// Business Central, internal warehouse) written to Azure Blob so a downstream +// website / CRM / analytics layer can pick it up. Once your data leaves the +// firewall this action moves to cloud-native; until then, this is the bridge. +// +// To activate: install mssql, drop this file in src/actions/ (out of examples/), +// add `import "./mssql-to-blob.ts";` to src/actions/index.ts, and uncomment the +// implementation block below. + +import { defineAction } from "../../runtime/registry.ts"; +import getSettings from "../../runtime/settings.ts"; +import uploadBlob from "../../helpers/sample-upload-blob.ts"; + +defineAction({ + name: "mssqlToBlob", + description: "Snapshot a SQL Server table to JSON blob.", + schedule: "0 2 * * *", // 02:00 daily + timeoutMs: 5 * 60_000, + handler: async () => { + // Uncomment after `npm install mssql`: + // + // const sql = await import("mssql"); + // const pool = await new sql.default.ConnectionPool(getSettings("GP_CONNECTIONSTRING")).connect(); + // const result = await pool.request().query("SELECT * FROM dbo.Customers"); + // await pool.close(); + // const payload = JSON.stringify(result.recordset); + // await uploadBlob("customers.json", payload); + // return { rows: result.recordset.length }; + + void getSettings; + void uploadBlob; + return { todo: "install mssql and uncomment the handler body" }; + }, +}); diff --git a/src/actions/examples/sharepoint-list-sync.ts b/src/actions/examples/sharepoint-list-sync.ts new file mode 100644 index 0000000..522294d --- /dev/null +++ b/src/actions/examples/sharepoint-list-sync.ts @@ -0,0 +1,59 @@ +// Recipe: pull a SharePoint list via Microsoft Graph, write to blob. +// +// Prerequisites: `npm install @azure/msal-node` +// Plus an Azure AD app registration with `Sites.Read.All` (or per-site) application permission. +// +// Common use case: surface a SharePoint list (vendor register, project roster, +// approved-supplier list) to a public website or analytics tool while keeping +// the authoritative copy in SharePoint. Eventually you'd move this to a Logic +// App or Power Automate flow; this template covers the gap while access is +// gated to on-prem identity. +// +// To activate: install msal-node, fill in TENANT_ID / CLIENT_ID / CLIENT_SECRET +// / SITE_ID / LIST_ID, drop this file in src/actions/ (out of examples/), add +// `import "./sharepoint-list-sync.ts";` to src/actions/index.ts. + +import { defineAction } from "../../runtime/registry.ts"; +import uploadBlob from "../../helpers/sample-upload-blob.ts"; + +defineAction({ + name: "sharepointListSync", + description: "Pull a SharePoint list via Graph, write to blob.", + schedule: "*/30 * * * *", + timeoutMs: 2 * 60_000, + handler: async () => { + // Uncomment after `npm install @azure/msal-node`: + // + // const { ConfidentialClientApplication } = await import("@azure/msal-node"); + // const tenantId = process.env.TENANT_ID ?? ""; + // const cca = new ConfidentialClientApplication({ + // auth: { + // clientId: process.env.CLIENT_ID ?? "", + // clientSecret: process.env.CLIENT_SECRET ?? "", + // authority: `https://login.microsoftonline.com/${tenantId}`, + // }, + // }); + // const tokenResp = await cca.acquireTokenByClientCredential({ + // scopes: ["https://graph.microsoft.com/.default"], + // }); + // const token = tokenResp?.accessToken; + // if (!token) throw new Error("Failed to acquire Graph token"); + // + // const items: unknown[] = []; + // let next: string | null = + // `https://graph.microsoft.com/v1.0/sites/${process.env.SITE_ID}` + + // `/lists/${process.env.LIST_ID}/items?expand=fields&$top=200`; + // while (next) { + // const r = await fetch(next, { headers: { Authorization: `Bearer ${token}` } }); + // if (!r.ok) throw new Error(`Graph ${r.status}: ${await r.text()}`); + // const page = (await r.json()) as { value: unknown[]; "@odata.nextLink"?: string }; + // items.push(...page.value); + // next = page["@odata.nextLink"] ?? null; + // } + // await uploadBlob("sharepoint-list.json", JSON.stringify(items)); + // return { count: items.length }; + + void uploadBlob; + return { todo: "install @azure/msal-node and uncomment the handler body" }; + }, +}); diff --git a/src/actions/index.ts b/src/actions/index.ts new file mode 100644 index 0000000..d529d7f --- /dev/null +++ b/src/actions/index.ts @@ -0,0 +1,3 @@ +import "./sample-handler.ts"; +import "./sample-stream.ts"; +import "./sample-sequence.ts"; diff --git a/src/actions/sample-handler.ts b/src/actions/sample-handler.ts new file mode 100644 index 0000000..d3df9a9 --- /dev/null +++ b/src/actions/sample-handler.ts @@ -0,0 +1,11 @@ +import { defineAction } from "../runtime/registry.ts"; + +defineAction({ + name: "sampleHandler", + description: "Basic handler. Echoes request id and timestamp.", + handler: ({ requestId }) => ({ + requestId, + timestamp: new Date().toISOString(), + message: "Replace this with your own logic.", + }), +}); diff --git a/src/actions/sample-sequence.ts b/src/actions/sample-sequence.ts new file mode 100644 index 0000000..e440c0f --- /dev/null +++ b/src/actions/sample-sequence.ts @@ -0,0 +1,17 @@ +import { defineAction } from "../runtime/registry.ts"; +import { log } from "../runtime/log.ts"; + +defineAction({ + name: "samplePing", + description: "Trivial second handler used by sampleSequence.", + handler: () => ({ pong: true, at: Date.now() }), +}); + +defineAction({ + name: "sampleSequence", + description: "Sequence: handler → stream → handler. Streaming step's yields appear as `progress` events.", + steps: ["sampleHandler", "sampleStream", "samplePing"], + onSuccess: async (_result, { requestId }) => { + log(`${requestId} sampleSequence finished; this is where you would ping downstream.`); + }, +}); diff --git a/src/actions/sample-stream.ts b/src/actions/sample-stream.ts new file mode 100644 index 0000000..76ede54 --- /dev/null +++ b/src/actions/sample-stream.ts @@ -0,0 +1,19 @@ +import { defineAction } from "../runtime/registry.ts"; + +const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); + +defineAction({ + name: "sampleStream", + description: "Streaming action. Yields five progress chunks with a delay between each.", + timeoutMs: 5_000, + stream: async function* ({ requestId }) { + yield { phase: "starting", requestId }; + for (let i = 1; i <= 4; i++) { + await sleep(400); + yield { phase: "progress", step: i, of: 4 }; + } + await sleep(200); + yield { phase: "done", requestId }; + return { ok: true }; + }, +}); diff --git a/src/config/windows-service.ts b/src/config/windows-service.ts index f7c7c18..f0a0454 100644 --- a/src/config/windows-service.ts +++ b/src/config/windows-service.ts @@ -2,8 +2,8 @@ import { Service } from "node-windows"; import * as path from "path"; export const serviceDefinition = { - name: "Node Service - Web Integrations Template", - description: "The Description for your Windows Service", - script: path.join(__dirname, "..\\..\\node_modules\\nodemon\\bin\\nodemon.js"), + name: "AZ Functions OnPrem", + description: "Self-hosted Azure-Functions-style integrations runtime.", + script: path.join(import.meta.dirname, "..", "server.ts"), }; export const service = new Service(serviceDefinition); diff --git a/src/utils/uploadBlob.ts b/src/helpers/sample-upload-blob.ts similarity index 58% rename from src/utils/uploadBlob.ts rename to src/helpers/sample-upload-blob.ts index e0de83f..7528dff 100644 --- a/src/utils/uploadBlob.ts +++ b/src/helpers/sample-upload-blob.ts @@ -1,23 +1,25 @@ import { BlobClient } from "@azure/storage-blob"; -import getSettings from "./settings"; -import { log } from "./log"; +import getSettings from "../runtime/settings.ts"; +import { log } from "../runtime/log.ts"; export default async function uploadBlob(fileName: string, fileContents: string) { + if (!fileName || fileName.includes("/") || fileName.includes("\\") || fileName.includes("..") || fileName.includes("?")) { + throw new Error("Invalid filename"); + } + const safeName = encodeURIComponent(fileName); + let connStrings = []; let s1 = getSettings("WEBSITE_BLOB_SAS"); if (s1) connStrings.push(s1); if (connStrings.length === 0) throw new Error("No connection strings found"); - // upload to all found locations await Promise.all( connStrings.map(async (connStr) => { - // insert the filename into the connection string - const fileConStr = connStr.replace("?sv=", `/${fileName}?sv=`); + const fileConStr = connStr.replace("?sv=", `/${safeName}?sv=`); - // connect and upload let blobClient = new BlobClient(fileConStr); let blockBlobClient = blobClient.getBlockBlobClient(); - let uploadBlobResponse = await blockBlobClient.upload(fileContents, fileContents.length); + await blockBlobClient.upload(fileContents, fileContents.length); log(` Upload block blob ${blobClient.containerName}/${blobClient.name} successfully`); }) ); diff --git a/src/helpers/sample-verify-hmac.ts b/src/helpers/sample-verify-hmac.ts new file mode 100644 index 0000000..766ffde --- /dev/null +++ b/src/helpers/sample-verify-hmac.ts @@ -0,0 +1,29 @@ +import { createHmac, timingSafeEqual } from "node:crypto"; + +/** + * Constant-time HMAC verification for inbound webhooks. + * + * Usage: + * const ok = verifyHmac(secret, rawBody, req.get("x-signature") ?? ""); + * + * For real webhook providers (Stripe, GitHub, etc.) you typically need the + * **raw** request body (pre-JSON-parse) - express.json discards it by default. + * Capture it with: `express.json({ verify: (req, res, buf) => { (req as any).rawBody = buf; } })`. + */ +export function verifyHmac( + secret: string, + payload: string | Buffer, + signature: string, + algorithm: "sha256" | "sha1" = "sha256", +): boolean { + if (!secret || !signature) return false; + const computed = createHmac(algorithm, secret).update(payload).digest("hex"); + const a = Buffer.from(computed); + const b = Buffer.from(signature); + if (a.length !== b.length) return false; + try { + return timingSafeEqual(a, b); + } catch { + return false; + } +} diff --git a/src/middleware/auth.ts b/src/middleware/auth.ts deleted file mode 100644 index dbf64a4..0000000 --- a/src/middleware/auth.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { Request, Response, NextFunction } from "express"; -import getSettings from "../utils/settings"; -import { log } from "../utils/log"; - -const apiKey = getSettings("WEB_INTEGRATIONS_API_KEY"); -const azureToken = getSettings("AZURE_CUSTOM_HEADER_TOKEN"); - -export function auth(req: Request, res: Response, next: NextFunction) { - // Allow bypass for local machine in non-production environments - if (process.env.NODE_ENV !== "production" && (req.ip === "127.0.0.1" || req.ip === "::1")) { - return next(); - } else if (req.headers.token == azureToken) { - log(`${req.headers.upn} accessed ${req.path} via proxy`); - return next(); - } else if (req.query.apiKey === apiKey) { - return next(); - } else { - return res.status(401).send("Invalid API key or auth. Are you accessing the site via the proxy URL?"); - } -} diff --git a/src/public/app.js b/src/public/app.js new file mode 100644 index 0000000..7327b14 --- /dev/null +++ b/src/public/app.js @@ -0,0 +1,90 @@ +function outputFor(actionName) { + const wrapper = document.querySelector(`[data-output-wrapper-for="${CSS.escape(actionName)}"]`); + const body = document.querySelector(`[data-output-for="${CSS.escape(actionName)}"]`); + return { wrapper, body }; +} + +function appendChunk(body, line) { + if (!line) return; + try { + const parsed = JSON.parse(line); + if (parsed && typeof parsed === "object" && "error" in parsed) { + body.textContent += `ERROR: ${parsed.error}\n`; + } else { + body.textContent += JSON.stringify(parsed) + "\n"; + } + } catch { + body.textContent += line + "\n"; + } + body.scrollTop = body.scrollHeight; +} + +async function readNdjson(response, body) { + if (!response.body) { + body.textContent = "(empty response body)"; + return; + } + body.textContent = ""; + const reader = response.body.getReader(); + const decoder = new TextDecoder(); + let buffer = ""; + while (true) { + const { value, done } = await reader.read(); + if (done) break; + buffer += decoder.decode(value, { stream: true }); + const lines = buffer.split("\n"); + buffer = lines.pop() ?? ""; + for (const line of lines) appendChunk(body, line); + } + if (buffer) appendChunk(body, buffer); +} + +async function runAction(actionName, triggerButton) { + const { wrapper, body } = outputFor(actionName); + if (!wrapper || !body) return; + + wrapper.hidden = false; + body.textContent = `Running ${actionName}...\n`; + triggerButton.setAttribute("aria-busy", "true"); + triggerButton.disabled = true; + + try { + const response = await fetch(`/action/${encodeURIComponent(actionName)}`); + const contentType = (response.headers.get("content-type") ?? "").toLowerCase(); + if (contentType.includes("application/x-ndjson")) { + await readNdjson(response, body); + } else { + const text = await response.text(); + try { + body.textContent = JSON.stringify(JSON.parse(text), null, 2); + } catch { + body.textContent = text; + } + } + } catch (error) { + body.textContent += `\nRequest failed: ${error instanceof Error ? error.message : String(error)}`; + } finally { + triggerButton.removeAttribute("aria-busy"); + triggerButton.disabled = false; + } +} + +document.addEventListener("click", (event) => { + const target = event.target instanceof Element ? event.target : null; + if (!target) return; + + const closeButton = target.closest("[data-close-output]"); + if (closeButton instanceof HTMLElement && closeButton.dataset.closeOutput) { + event.preventDefault(); + const { wrapper, body } = outputFor(closeButton.dataset.closeOutput); + if (wrapper) wrapper.hidden = true; + if (body) body.textContent = ""; + return; + } + + const actionTrigger = target.closest("[data-action]"); + if (actionTrigger instanceof HTMLButtonElement && actionTrigger.dataset.action) { + event.preventDefault(); + runAction(actionTrigger.dataset.action, actionTrigger); + } +}); diff --git a/src/public/docs-init.js b/src/public/docs-init.js new file mode 100644 index 0000000..a6d02f1 --- /dev/null +++ b/src/public/docs-init.js @@ -0,0 +1,8 @@ +window.addEventListener("DOMContentLoaded", () => { + window.ui = SwaggerUIBundle({ + url: "/openapi.json", + dom_id: "#swagger-ui", + deepLinking: true, + docExpansion: "list", + }); +}); diff --git a/src/public/styles.css b/src/public/styles.css new file mode 100644 index 0000000..1784fbc --- /dev/null +++ b/src/public/styles.css @@ -0,0 +1,278 @@ +/* Clear Path Research brand tokens - hand-rolled subset, no build step. */ + +:root { + color-scheme: dark; + --bg: oklch(13% 0.01 55); + --surface: oklch(17% 0.01 55); + --border: oklch(27% 0.01 55); + --text: oklch(93% 0.01 55); + --text2: oklch(62% 0.01 55); + --accent: oklch(64% 0.17 25); + --accent-mid: oklch(50% 0.12 25); + --accent-light: oklch(38% 0.08 25); + --teal: oklch(64% 0.12 195); + --green: oklch(68% 0.17 145); + --red: oklch(58% 0.20 25); + --yellow: oklch(68% 0.15 85); + --code-bg: oklch(9% 0.01 55); + + --font-sans: "Poppins", -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif; + --font-mono: "JetBrains Mono", ui-monospace, "SF Mono", Menlo, Consolas, monospace; + + --radius: 4px; + --radius-lg: 8px; + --transition: 0.15s ease; +} + +@media (prefers-color-scheme: light) { + :root:not(.dark) { + color-scheme: light; + --bg: oklch(97% 0.005 80); + --surface: oklch(94% 0.005 80); + --border: oklch(86% 0.01 70); + --text: oklch(18% 0.01 55); + --text2: oklch(47% 0.01 55); + --accent: oklch(48% 0.17 25); + --accent-mid: oklch(75% 0.10 25); + --accent-light: oklch(86% 0.06 25); + --teal: oklch(48% 0.12 195); + --green: oklch(42% 0.15 150); + --red: oklch(48% 0.19 25); + --yellow: oklch(50% 0.13 80); + --code-bg: oklch(91% 0.005 80); + } +} + +.light { + color-scheme: light; + --bg: oklch(97% 0.005 80); + --surface: oklch(94% 0.005 80); + --border: oklch(86% 0.01 70); + --text: oklch(18% 0.01 55); + --text2: oklch(47% 0.01 55); + --accent: oklch(48% 0.17 25); + --accent-mid: oklch(75% 0.10 25); + --accent-light: oklch(86% 0.06 25); + --teal: oklch(48% 0.12 195); + --green: oklch(42% 0.15 150); + --red: oklch(48% 0.19 25); + --yellow: oklch(50% 0.13 80); + --code-bg: oklch(91% 0.005 80); +} + +* { + box-sizing: border-box; +} + +html, body { + margin: 0; + padding: 0; + background: var(--bg); + color: var(--text); + font-family: var(--font-sans); + font-size: 16px; + line-height: 1.5; + -webkit-font-smoothing: antialiased; +} + +main.container { + max-width: 64rem; + margin: 0 auto; + padding: 2rem 1.5rem; +} + +footer.container { + max-width: 64rem; + margin: 3rem auto 1rem; + padding: 0 1.5rem; + color: var(--text2); +} + +h1, h2, h3 { + font-family: var(--font-sans); + font-weight: 600; + letter-spacing: -0.025em; + line-height: 1.2; +} + +h1 { + font-size: 28px; + margin: 0 0 0.25rem; +} + +h2 { font-size: 21px; } +h3 { font-size: 18px; } + +hgroup { + margin-bottom: 2rem; +} + +hgroup > p { + color: var(--text2); + margin: 0; +} + +p { + margin: 0.5rem 0; +} + +hr { + border: 0; + border-top: 1px solid var(--border); + margin: 1.5rem 0 1rem; +} + +a { + color: var(--accent); + text-decoration: none; + transition: opacity var(--transition); +} + +a:hover { + opacity: 0.9; + text-decoration: underline; +} + +code { + font-family: var(--font-mono); + font-size: 0.875em; + background: var(--code-bg); + padding: 0.1em 0.3em; + border-radius: var(--radius); + color: var(--text); +} + +article { + background: var(--surface); + border: 1px solid var(--border); + border-radius: var(--radius-lg); + padding: 1.25rem; + margin: 0 0 1rem; +} + +article > header { + margin-bottom: 0.5rem; +} + +button { + font-family: var(--font-sans); + font-size: 0.875rem; + font-weight: 500; + background: var(--accent); + color: white; + border: 0; + border-radius: var(--radius); + padding: 0.5rem 1rem; + min-height: 38px; + cursor: pointer; + transition: opacity var(--transition), transform var(--transition); +} + +button:hover { + opacity: 0.9; +} + +button:active { + transform: scale(0.98); +} + +button:focus-visible { + outline: 2px solid var(--accent); + outline-offset: 2px; +} + +button:disabled, +button[aria-busy="true"] { + opacity: 0.5; + cursor: not-allowed; +} + +button[aria-busy="true"]::before { + content: ""; + display: inline-block; + width: 0.85em; + height: 0.85em; + margin-right: 0.5em; + border: 2px solid currentColor; + border-right-color: transparent; + border-radius: 50%; + vertical-align: -0.15em; + animation: spin 0.6s linear infinite; +} + +@keyframes spin { + to { transform: rotate(360deg); } +} + +kbd { + font-family: var(--font-mono); + font-size: 0.7rem; + font-weight: 500; + background: var(--bg); + color: var(--text2); + border: 1px solid var(--border); + border-radius: var(--radius); + padding: 0.1rem 0.4rem; + line-height: 1.4; + white-space: nowrap; +} + +small { + font-size: 0.85em; + color: var(--text2); +} + +strong { + font-weight: 600; + color: var(--text); +} + +.action-output-wrapper { + margin-top: 1rem; + position: relative; +} + +.action-output-wrapper[hidden] { + display: none; +} + +.action-output { + max-height: 40vh; + overflow: auto; + background: var(--code-bg); + color: var(--text); + padding: 0.75rem 2rem 0.75rem 0.75rem; + margin: 0; + font: 0.85rem/1.4 var(--font-mono); + white-space: pre-wrap; + word-break: break-word; + border-radius: var(--radius); + border: 1px solid var(--border); +} + +.action-output:focus-visible { + outline: 2px solid var(--accent); + outline-offset: 2px; +} + +.action-output-close { + position: absolute; + top: 0.25rem; + right: 0.25rem; + background: transparent; + color: var(--text2); + border: 0; + cursor: pointer; + padding: 0.25rem 0.5rem; + font-size: 1.1rem; + line-height: 1; + width: auto; + min-height: auto; + border-radius: var(--radius); +} + +.action-output-close:hover { + background: var(--bg); + color: var(--text); + opacity: 1; +} diff --git a/src/runtime/auth.ts b/src/runtime/auth.ts new file mode 100644 index 0000000..c52d1a3 --- /dev/null +++ b/src/runtime/auth.ts @@ -0,0 +1,54 @@ +import type { Request, Response, NextFunction } from "express"; +import { timingSafeEqual } from "crypto"; +import getSettings from "./settings.ts"; +import { log } from "./log.ts"; +import { getAction } from "./registry.ts"; + +export type AuthLevel = "anonymous" | "key" | "proxy"; + +const apiKey = getSettings("WEB_INTEGRATIONS_API_KEY"); +const azureToken = getSettings("AZURE_CUSTOM_HEADER_TOKEN"); +const serviceType = getSettings("SERVICE_TYPE"); + +function safeEqual(a: unknown, b: string): boolean { + if (typeof a !== "string" || a.length === 0 || b.length === 0) return false; + const aBuf = Buffer.from(a); + const bBuf = Buffer.from(b); + if (aBuf.length !== bBuf.length) return false; + return timingSafeEqual(aBuf, bBuf); +} + +function isLoopback(req: Request): boolean { + return serviceType === "dev" && (req.ip === "127.0.0.1" || req.ip === "::1"); +} + +export function checkAuth(req: Request, level: AuthLevel): boolean { + if (level === "anonymous") return true; + if (isLoopback(req)) return true; + if (safeEqual(req.headers.token, azureToken)) { + log(`${req.headers.upn} accessed ${req.path} via proxy`); + return true; + } + if (level === "proxy") return false; + return safeEqual(req.query.apiKey, apiKey); +} + +function actionAuthLevelForPath(path: string): AuthLevel | undefined { + if (!path.startsWith("/action/")) return undefined; + const rest = path.slice("/action/".length).split("/")[0] ?? ""; + if (!rest) return undefined; + let name: string; + try { + name = decodeURIComponent(rest); + } catch { + return undefined; + } + const action = getAction(name); + return action?.auth; +} + +export function auth(req: Request, res: Response, next: NextFunction) { + const level: AuthLevel = actionAuthLevelForPath(req.path) ?? "key"; + if (checkAuth(req, level)) return next(); + return res.status(401).send("Unauthorized"); +} diff --git a/src/runtime/log.ts b/src/runtime/log.ts new file mode 100644 index 0000000..c4ee42e --- /dev/null +++ b/src/runtime/log.ts @@ -0,0 +1,32 @@ +import { createRequire } from "module"; +import getSettings from "./settings.ts"; + +const localRequire = createRequire(import.meta.url); +const windows = getSettings("SERVICE_TYPE") == "windows"; +let winLog: any; + +if (windows) { + const { serviceDefinition } = await import("../config/windows-service.ts"); + const { EventLogger } = localRequire("node-windows"); + winLog = new EventLogger(serviceDefinition.name); +} + +export function log(message: string, level: "info" | "warn" | "error" = "info") { + if (level === "info") { + console.info(message); + if (windows) winLog.info(message); + } else if (level === "warn") { + console.log(message); + if (windows) winLog.warn(message); + } else { + console.error(message); + if (windows) winLog.error(message); + } +} + +export function report(error: Error) { + console.error(error); + if (windows) { + winLog.error(error.message); + } +} diff --git a/src/runtime/openapi.ts b/src/runtime/openapi.ts new file mode 100644 index 0000000..f45e1b5 --- /dev/null +++ b/src/runtime/openapi.ts @@ -0,0 +1,149 @@ +import { getActions, type ActionDefinition } from "./registry.ts"; + +type SecurityRequirement = Record; + +interface OpenApiOperation { + summary: string; + operationId: string; + tags: string[]; + security: SecurityRequirement[]; + requestBody?: Record; + responses: Record; +} + +type OpenApiPath = { get: OpenApiOperation } | { post: OpenApiOperation }; + +function kindOf(action: ActionDefinition): "handler" | "sequence" | "stream" { + if ("stream" in action) return "stream"; + if ("steps" in action) return "sequence"; + return "handler"; +} + +function securityFor(action: ActionDefinition): SecurityRequirement[] { + const level = action.auth ?? "key"; + if (level === "anonymous") return []; + if (level === "proxy") return [{ headerToken: [] }]; + return [{ headerToken: [] }, { apiKeyQuery: [] }]; +} + +function responseSchemaFor(action: ActionDefinition): Record { + const kind = kindOf(action); + if (kind === "handler") { + return { + description: "Action completed.", + content: { + "application/json": { + schema: { + type: "object", + properties: { + action: { type: "string", example: action.name }, + result: { type: "object" }, + }, + }, + }, + }, + }; + } + if (kind === "sequence") { + return { + description: "Sequence stream. Each NDJSON line describes a step transition (started / progress / done / failed).", + content: { "application/x-ndjson": { schema: { type: "string" } } }, + }; + } + return { + description: "Streaming output. Each NDJSON line is a yielded value from the generator.", + content: { "application/x-ndjson": { schema: { type: "string" } } }, + }; +} + +export function buildOpenApiSpec(info: { title: string; version: string }) { + const paths: Record = {}; + + paths["/healthz"] = { + get: { + summary: "Liveness probe. Always public.", + operationId: "healthz", + tags: ["meta"], + security: [], + responses: { + "200": { + description: "ok", + content: { + "application/json": { + schema: { + type: "object", + properties: { status: { type: "string", example: "ok" } }, + }, + }, + }, + }, + }, + }, + }; + + for (const action of getActions()) { + const kind = kindOf(action); + const summary = [action.description].filter(Boolean).join(" "); + const tags: string[] = [kind]; + if (action.schedule) tags.push("scheduled"); + const method = action.method ?? "GET"; + const operation: OpenApiOperation = { + summary, + operationId: action.name, + tags, + security: securityFor(action), + responses: { + "200": responseSchemaFor(action), + "401": { description: "Unauthorized" }, + "404": { + description: "Action not found", + content: { + "application/json": { + schema: { + type: "object", + properties: { error: { type: "string" }, name: { type: "string" } }, + }, + }, + }, + }, + "405": { description: "Method not allowed" }, + "500": { + description: "Action failed", + content: { + "application/json": { + schema: { + type: "object", + properties: { action: { type: "string" }, error: { type: "string" } }, + }, + }, + }, + }, + }, + }; + if (method === "POST") { + operation.requestBody = { + required: false, + content: { + "application/json": { schema: { type: "object" } }, + }, + }; + paths[`/action/${action.name}`] = { post: operation }; + } else { + paths[`/action/${action.name}`] = { get: operation }; + } + } + + return { + openapi: "3.0.3", + info, + servers: [{ url: "/" }], + components: { + securitySchemes: { + headerToken: { type: "apiKey", in: "header", name: "token" }, + apiKeyQuery: { type: "apiKey", in: "query", name: "apiKey" }, + }, + }, + security: [{ headerToken: [] }, { apiKeyQuery: [] }], + paths, + }; +} diff --git a/src/runtime/registry.ts b/src/runtime/registry.ts new file mode 100644 index 0000000..9658ed9 --- /dev/null +++ b/src/runtime/registry.ts @@ -0,0 +1,236 @@ +import type { Request, Response } from "express"; +import { log as runtimeLog } from "./log.ts"; + +export type LogLevel = "info" | "warn" | "error"; + +export interface ActionLogger { + (message: string): void; + info(message: string): void; + warn(message: string): void; + error(message: string): void; +} + +/** Functions-style HttpResponseInit shape. Handlers may return this directly to control status/headers/body. */ +export interface HttpResponseInit { + status?: number; + jsonBody?: unknown; + body?: string | Buffer | unknown; + headers?: Record; +} + +interface BaseContext { + /** Per-invocation correlation id. */ + requestId: string; + /** Alias for `requestId` that matches Azure Functions context API. */ + invocationId: string; + /** Functions-style logger. Prefixes the requestId for you. `ctx.log("msg")` is info; `.warn` / `.error` for higher levels. */ + log: ActionLogger; +} + +export function makeActionLogger(requestId: string): ActionLogger { + const at = (level: LogLevel) => (message: string) => runtimeLog(`${requestId} ${message}`, level); + const fn = at("info") as ActionLogger; + fn.info = at("info"); + fn.warn = at("warn"); + fn.error = at("error"); + return fn; +} + +/** Helper to mark a return value as an HttpResponseInit. Identity at runtime; documents intent. */ +export function httpResponse(init: HttpResponseInit): HttpResponseInit { + return init; +} + +export function isHttpResponseInit(value: unknown): value is HttpResponseInit { + if (!value || typeof value !== "object") return false; + const v = value as Record; + if ("jsonBody" in v) return true; + if (typeof v.status === "number" && ("body" in v || "headers" in v)) return true; + return false; +} + +export interface HttpContext extends BaseContext { + trigger: "http"; + req: Request; + res: Response; +} + +export interface ScheduleContext extends BaseContext { + trigger: "schedule"; +} + +export type ActionContext = HttpContext | ScheduleContext; + +type Awaitable = T | Promise; +type Handler = (ctx: ActionContext) => Awaitable; +type StreamHandler = (ctx: ActionContext) => AsyncGenerator; +type SuccessHook = (result: unknown, ctx: ActionContext) => Awaitable; + +export type AuthLevel = "anonymous" | "key" | "proxy"; + +interface BaseActionDefinition { + name: string; + description: string; + onSuccess?: SuccessHook; + timeoutMs?: number; + /** Cron expression for the internal scheduler. Action remains HTTP-callable regardless. */ + schedule?: string; + /** Override the global auth level for this action. Defaults to "key". */ + auth?: AuthLevel; + /** HTTP method this action accepts. Defaults to "GET". Use "POST" for webhooks / actions that take a JSON body. */ + method?: "GET" | "POST"; +} + +export interface HandlerActionDefinition extends BaseActionDefinition { + handler: Handler; +} + +export interface SequenceActionDefinition extends BaseActionDefinition { + steps: readonly string[]; +} + +export interface StreamActionDefinition extends BaseActionDefinition { + stream: StreamHandler; +} + +export type ActionDefinition = HandlerActionDefinition | SequenceActionDefinition | StreamActionDefinition; + +const actions = new Map(); + +export function defineAction(def: ActionDefinition): void { + if (!/^[a-z][a-zA-Z0-9_-]*$/.test(def.name)) { + throw new Error(`Action name "${def.name}" must start with a lowercase letter and contain only [a-zA-Z0-9_-].`); + } + if (actions.has(def.name)) { + throw new Error(`Action "${def.name}" is already defined.`); + } + actions.set(def.name, def); +} + +export function getActions(): readonly ActionDefinition[] { + return [...actions.values()]; +} + +export function getAction(name: string): ActionDefinition | undefined { + return actions.get(name); +} + +/** Narrow an ActionContext to HttpContext, throwing if invoked from the scheduler. */ +export function httpCtx(ctx: ActionContext): HttpContext { + if (ctx.trigger !== "http") { + throw new Error("This action requires HTTP context (req/res); it was invoked from the scheduler."); + } + return ctx; +} + +async function withTimeout(promise: Promise, ms: number, label: string): Promise { + let timer: ReturnType | undefined; + const timeoutPromise = new Promise((_, reject) => { + timer = setTimeout(() => reject(new Error(`${label} timed out after ${ms}ms`)), ms); + }); + try { + return await Promise.race([promise, timeoutPromise]); + } finally { + if (timer) clearTimeout(timer); + } +} + +/** Run a handler-only action and return its result. */ +export async function runAction(action: HandlerActionDefinition, ctx: ActionContext): Promise { + const work = action.handler(ctx); + const promise = work instanceof Promise ? work : Promise.resolve(work); + const result = action.timeoutMs ? await withTimeout(promise, action.timeoutMs, action.name) : await promise; + if (action.onSuccess) await action.onSuccess(result, ctx); + return result; +} + +/** Async generator that yields whatever a stream or sequence action emits. */ +export async function* streamOf( + action: StreamActionDefinition | SequenceActionDefinition, + ctx: HttpContext, +): AsyncGenerator { + if ("stream" in action) { + const iter = action.stream(ctx); + try { + while (true) { + const nextStep = iter.next(); + const step = action.timeoutMs + ? await withTimeout(nextStep, action.timeoutMs, `${action.name} (idle between yields)`) + : await nextStep; + if (step.done) { + if (action.onSuccess) await action.onSuccess(step.value, ctx); + return; + } + yield step.value; + } + } finally { + try { + await iter.return?.(undefined); + } catch { + /* ignore generator cleanup failures */ + } + } + } else { + // Sequence: yield one event per step (started, done / failed / progress). + for (const stepName of action.steps) { + const step = getAction(stepName); + if (!step) { + yield { step: stepName, status: "failed", error: `"${stepName}" is not registered`, at: new Date().toISOString() }; + return; + } + yield { step: stepName, status: "started", at: new Date().toISOString() }; + try { + if ("stream" in step || "steps" in step) { + for await (const chunk of streamOf(step, ctx)) { + yield { step: stepName, status: "progress", chunk }; + } + yield { step: stepName, status: "done", at: new Date().toISOString() }; + } else { + const result = await runAction(step, ctx); + yield { step: stepName, status: "done", result, at: new Date().toISOString() }; + } + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + yield { step: stepName, status: "failed", error: message, at: new Date().toISOString() }; + return; + } + } + if (action.onSuccess) await action.onSuccess(undefined, ctx); + } +} + +/** Write an action's stream to an HTTP response as NDJSON. */ +export async function runStreamingAction( + action: StreamActionDefinition | SequenceActionDefinition, + ctx: HttpContext, +): Promise { + const { res } = ctx; + let started = false; + try { + for await (const chunk of streamOf(action, ctx)) { + if (!started) { + res.setHeader("Content-Type", "application/x-ndjson"); + res.setHeader("Cache-Control", "no-cache"); + res.setHeader("X-Accel-Buffering", "no"); + started = true; + } + res.write(JSON.stringify(chunk) + "\n"); + } + if (!started) { + res.setHeader("Content-Type", "application/x-ndjson"); + } + res.end(); + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + if (!started && !res.headersSent) { + res.status(500).json({ action: action.name, error: message }); + } else { + try { + res.write(JSON.stringify({ error: message }) + "\n"); + } catch { + /* connection probably closed; ignore */ + } + res.end(); + } + } +} diff --git a/src/runtime/scheduler.ts b/src/runtime/scheduler.ts new file mode 100644 index 0000000..6e7beb1 --- /dev/null +++ b/src/runtime/scheduler.ts @@ -0,0 +1,71 @@ +import cron from "node-cron"; +import { getAction, getActions, runAction, makeActionLogger, type ActionContext, type ActionDefinition } from "./registry.ts"; +import { log } from "./log.ts"; + +function containsStream(action: ActionDefinition, seen: Set = new Set()): boolean { + if (seen.has(action.name)) return false; + seen.add(action.name); + if ("stream" in action) return true; + if ("steps" in action) { + for (const stepName of action.steps) { + const step = getAction(stepName); + if (step && containsStream(step, seen)) return true; + } + } + return false; +} + +async function runHandlerOrSequenceFromScheduler(action: ActionDefinition, ctx: ActionContext): Promise { + if ("stream" in action) { + throw new Error(`Streaming action "${action.name}" cannot be invoked from the scheduler.`); + } + if ("handler" in action) { + await runAction(action, ctx); + return; + } + // Sequence: walk steps in order, logging per step. + for (const stepName of action.steps) { + const step = getAction(stepName); + if (!step) throw new Error(`Step "${stepName}" referenced by "${action.name}" is not registered.`); + log(`${ctx.requestId} scheduler: running step "${stepName}"`); + await runHandlerOrSequenceFromScheduler(step, ctx); + log(`${ctx.requestId} scheduler: step "${stepName}" done`); + } + if (action.onSuccess) await action.onSuccess(undefined, ctx); +} + +export function registerSchedules(): void { + for (const action of getActions()) { + if (typeof action.schedule !== "string" || action.schedule.length === 0) continue; + + if (containsStream(action)) { + log(`scheduler: skipping "${action.name}" - streaming actions are HTTP-only.`, "warn"); + continue; + } + + if (!cron.validate(action.schedule)) { + log(`scheduler: invalid cron expression "${action.schedule}" for "${action.name}"; skipping.`, "warn"); + continue; + } + + cron.schedule(action.schedule, async () => { + const requestId = crypto.randomUUID(); + const ctx: ActionContext = { + trigger: "schedule", + requestId, + invocationId: requestId, + log: makeActionLogger(requestId), + }; + log(`${requestId} scheduler: running "${action.name}"`); + try { + await runHandlerOrSequenceFromScheduler(action, ctx); + log(`${requestId} scheduler: "${action.name}" finished`); + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + log(`${requestId} scheduler: "${action.name}" failed: ${message}`, "warn"); + } + }); + + log(`scheduler: registered "${action.name}" with cron "${action.schedule}"`); + } +} diff --git a/src/runtime/settings.ts b/src/runtime/settings.ts new file mode 100644 index 0000000..a0eee68 --- /dev/null +++ b/src/runtime/settings.ts @@ -0,0 +1,36 @@ +import { createRequire } from "module"; + +type ServiceType = "docker" | "windows" | "dev"; + +type SettingsValues = { + AZURE_CUSTOM_HEADER_TOKEN: string; + SERVICE_TYPE: ServiceType; + WEB_INTEGRATIONS_API_KEY: string; + WEBSITE_BLOB_SAS: string; + PORT?: number | string; + CORS_ORIGINS?: string; +}; + +type SettingsFile = { + Values?: Partial; +}; + +const localRequire = createRequire(import.meta.url); +let settings: SettingsFile = {}; +try { + settings = localRequire("../../local.settings.json") as SettingsFile; +} catch (err) { + settings = {}; +} + +export default function getSettings(key: K, defaultValue?: SettingsValues[K]): SettingsValues[K] { + if (settings.Values?.[key] !== undefined) { + return settings.Values[key]; + } else if (process.env[key] !== undefined) { + return process.env[key] as SettingsValues[K]; + } else if (defaultValue !== undefined) { + return defaultValue; + } else { + throw new Error(`${key} is required but was not set in local.settings.json or the environment`); + } +} diff --git a/src/server.ts b/src/server.ts index f988314..325f0a4 100644 --- a/src/server.ts +++ b/src/server.ts @@ -1,19 +1,153 @@ -import express, { Request, Response } from "express"; -import getSettings from "./utils/settings"; +// === Observability hook === +// To enable APM (New Relic, OpenTelemetry, Datadog, etc.), import your tracer +// here as the very first line so it patches modules before they load: +// import "./observability.ts"; +// ========================== + +import express from "express"; +import type { Request, Response } from "express"; +import helmet from "helmet"; +import cors from "cors"; import path from "path"; -import { log } from "./utils/log"; -import { auth } from "./middleware/auth"; +import { createRequire } from "module"; +import getSettings from "./runtime/settings.ts"; +import { log } from "./runtime/log.ts"; +import { auth } from "./runtime/auth.ts"; +import { getAction, getActions, runAction, runStreamingAction, makeActionLogger, isHttpResponseInit, type HttpResponseInit } from "./runtime/registry.ts"; +import { registerSchedules } from "./runtime/scheduler.ts"; +import { buildOpenApiSpec } from "./runtime/openapi.ts"; +import "./actions/index.ts"; + +const packageJson = createRequire(import.meta.url)("../package.json") as { name: string; version: string }; + +export const app: express.Express = express(); + +app.disable("x-powered-by"); +app.use( + helmet({ + contentSecurityPolicy: { + directives: { + ...helmet.contentSecurityPolicy.getDefaultDirectives(), + "script-src": ["'self'", "https://cdnjs.cloudflare.com"], + "style-src": ["'self'", "https:", "'unsafe-inline'"], + "font-src": ["'self'", "https:", "data:"], + "worker-src": ["'self'", "blob:"], + }, + }, + }), +); + +const corsOriginsRaw = getSettings("CORS_ORIGINS", "") ?? ""; +const corsOrigins = corsOriginsRaw.split(",").map((o) => o.trim()).filter((o) => o.length > 0); +if (corsOrigins.length > 0) { + app.use(cors({ origin: corsOrigins, credentials: false })); + log(`cors: allowed origins ${corsOrigins.join(", ")}`); +} + +app.use(express.json({ limit: "1mb" })); +app.use(express.urlencoded({ extended: false, limit: "1mb" })); + +app.use((req, res, next) => { + const startedAt = process.hrtime.bigint(); + const requestId = req.get("x-request-id") ?? crypto.randomUUID(); + + res.setHeader("x-request-id", requestId); + res.on("finish", () => { + const durationMs = Number(process.hrtime.bigint() - startedAt) / 1_000_000; + log(`${requestId} ${req.method} ${req.path} ${res.statusCode} ${durationMs.toFixed(1)}ms`); + }); + + next(); +}); + +app.get("/healthz", (req: Request, res: Response) => res.status(200).json({ status: "ok" })); -export const app = express(); app.use(auth); +app.use(express.static(path.join(import.meta.dirname, "public"))); + // static content -app.set("views", path.join(__dirname, "views")); +app.set("views", path.join(import.meta.dirname, "views")); app.set("view engine", "ejs"); // express pages / routes / controllers go here -app.get("/", (req: Request, res: Response) => res.render("index")); +app.get("/", (req: Request, res: Response) => res.render("index", { actions: getActions() })); + +app.get("/openapi.json", (req: Request, res: Response) => + res.json(buildOpenApiSpec({ title: packageJson.name, version: packageJson.version })), +); + +app.get("/docs", (req: Request, res: Response) => res.render("docs")); + +app.all("/action/:name", async (req: Request, res: Response) => { + if (req.method !== "GET" && req.method !== "POST") { + res.status(405).set("Allow", "GET, POST").json({ error: `Method ${req.method} not allowed` }); + return; + } + const nameParam = req.params.name; + const name = typeof nameParam === "string" ? nameParam : ""; + const action = name ? getAction(name) : undefined; + if (!action) { + res.status(404).json({ error: "Action not found", name }); + return; + } + const expectedMethod = action.method ?? "GET"; + if (req.method !== expectedMethod) { + res.status(405).set("Allow", expectedMethod).json({ error: `Action "${action.name}" expects ${expectedMethod}, got ${req.method}.` }); + return; + } + const requestId = String(res.getHeader("x-request-id") ?? ""); + const ctx = { + trigger: "http" as const, + req, + res, + requestId, + invocationId: requestId, + log: makeActionLogger(requestId), + }; + try { + if ("handler" in action) { + const result = await runAction(action, ctx); + if (!res.headersSent) { + if (isHttpResponseInit(result)) { + const r = result as HttpResponseInit; + if (r.headers) for (const [k, v] of Object.entries(r.headers)) res.setHeader(k, v); + if (typeof r.status === "number") res.status(r.status); + if (r.jsonBody !== undefined) res.json(r.jsonBody); + else if (r.body !== undefined) res.send(r.body as string | Buffer); + else res.end(); + } else { + res.json({ action: action.name, result }); + } + } + } else { + await runStreamingAction(action, ctx); + } + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + log(`${requestId} action "${action.name}" failed: ${message}`, "warn"); + if (!res.headersSent) res.status(500).json({ action: action.name, error: message }); + } +}); // start const port = getSettings("PORT", 3000); -app.listen(port, () => log(`Running at http://localhost:${port}/?apiKey=${getSettings("WEB_INTEGRATIONS_API_KEY")}`)); +const server = app.listen(port, () => { + log(`Running at http://localhost:${port}/`); + registerSchedules(); +}); + +function shutdown(signal: NodeJS.Signals) { + log(`${signal} received. Shutting down.`); + server.close((error) => { + if (error) { + console.error(error); + process.exit(1); + } + + process.exit(0); + }); +} + +process.on("SIGINT", shutdown); +process.on("SIGTERM", shutdown); diff --git a/src/utils/log.ts b/src/utils/log.ts deleted file mode 100644 index 6923bca..0000000 --- a/src/utils/log.ts +++ /dev/null @@ -1,27 +0,0 @@ -import getSettings from "./settings"; -import { serviceDefinition } from "../config/windows-service"; - -var EventLogger = require("node-windows").EventLogger; -var winLog = new EventLogger(serviceDefinition.name); -let windows = getSettings("SERVICE_TYPE") == "windows"; - -export function log(message: string, level: "info" | "warn" = "info") { - if (level == "info") { - console.info(message); - if (windows) { - winLog.info(message); - } - } else { - console.log(message); - if (windows) { - winLog.warn(message); - } - } -} - -export function report(error: Error) { - console.error(error); - if (windows) { - winLog.error(error.message); - } -} diff --git a/src/utils/settings.ts b/src/utils/settings.ts deleted file mode 100644 index e7b036d..0000000 --- a/src/utils/settings.ts +++ /dev/null @@ -1,18 +0,0 @@ -let settings: any = {}; -try { - settings = require("../../local.settings.json"); -} catch (err) { - settings = { Values: false }; -} - -export default function getSettings(key: string, defaultValue?: any) { - if (settings.Values && settings.Values[key] !== undefined) { - return settings.Values[key]; - } else if (process.env[key] !== undefined) { - return process.env[key]; - } else if (defaultValue) { - return defaultValue; - } else { - throw `${key} not set`; - } -} diff --git a/src/views/docs.ejs b/src/views/docs.ejs new file mode 100644 index 0000000..2861745 --- /dev/null +++ b/src/views/docs.ejs @@ -0,0 +1,25 @@ + + + + + + az-functions-onprem - API docs + + + + + + +
+ + + + + diff --git a/src/views/index.ejs b/src/views/index.ejs index 7f728aa..ff90b1b 100644 --- a/src/views/index.ejs +++ b/src/views/index.ejs @@ -2,31 +2,47 @@ - Web Integrations Template + AZ Functions OnPrem <%- include("./partials/header.ejs") -%> -
-

Web Integrations Template

- - + + <% }) %> + <%- include("./partials/footer.ejs") -%> - \ No newline at end of file + diff --git a/src/views/partials/footer.ejs b/src/views/partials/footer.ejs index 01c516f..3eb6df7 100644 --- a/src/views/partials/footer.ejs +++ b/src/views/partials/footer.ejs @@ -1,37 +1,9 @@ -
-
-
Help
- Contact us on Teams if something isn't working, or you need help. -
- -