diff --git a/.github/actions/setup/action.yml b/.github/actions/setup/action.yml new file mode 100644 index 0000000..38e31d8 --- /dev/null +++ b/.github/actions/setup/action.yml @@ -0,0 +1,32 @@ +name: 'Setup Environment' +description: 'Sets up Node, OpenTofu, AWS credentials, and installs dependencies' +inputs: + aws_account_id: + description: 'AWS Account ID for STS AssumeRole' + required: true + aws_region: + description: 'AWS Region' + required: true +runs: + using: "composite" + steps: + - name: Setup Node.js 24 + uses: actions/setup-node@v4 + with: + node-version: '24' + cache: 'npm' + + - name: Setup OpenTofu + uses: opentofu/setup-opentofu@v1 + with: + tofu_version: '1.8.8' + + - name: Configure AWS Credentials + uses: aws-actions/configure-aws-credentials@v4 + with: + role-to-assume: arn:aws:iam::${{ inputs.aws_account_id }}:role/GitHubActionsRole + aws-region: ${{ inputs.aws_region }} + + - name: Install Node dependencies + run: npm ci + shell: bash \ No newline at end of file diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..98b3ad7 --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,149 @@ +name: CI/CD Pipeline + +on: + pull_request: + push: + branches: + - main + workflow_dispatch: + +env: + AWS_DEFAULT_REGION: "eu-west-1" + STATE_BUCKET: "rhosys-opentofu-${{ secrets.AWS_ACCOUNT_ID }}-eu-west-1" + STATE_KEY: "${{ github.event.repository.name }}/terraform.tfstate" + TF_VAR_aws_account_id: "${{ secrets.AWS_ACCOUNT_ID }}" + TF_VAR_service_name: "${{ github.event.repository.name }}" + +permissions: + id-token: write + contents: read + +jobs: + # ========================================== + # PATH A: PULL REQUEST VALIDATION + # ========================================== + pr_validate: + name: Validate PR + if: github.event_name == 'pull_request' + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Setup Environment + uses: ./.github/actions/setup + with: + aws_account_id: ${{ secrets.AWS_ACCOUNT_ID }} + aws_region: ${{ env.AWS_DEFAULT_REGION }} + + - name: Build Code + run: npm run build + + - name: Test Code + run: npm test + + - name: Tofu Init + run: tofu -chdir=deploy init -input=false -backend-config="bucket=${STATE_BUCKET}" -backend-config="key=${STATE_KEY}" + + - name: Tofu Plan + run: tofu -chdir=deploy plan -out=plan.cache + + # ========================================== + # PATH B: PRODUCTION BUILD & PLAN + # ========================================== + prod_build_and_plan: + name: Prod Build & Plan + if: github.event_name == 'push' || github.event_name == 'workflow_dispatch' + runs-on: ubuntu-latest + outputs: + requires_approval: ${{ steps.plan_check.outputs.requires_approval }} + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Setup Environment + uses: ./.github/actions/setup + with: + aws_account_id: ${{ secrets.AWS_ACCOUNT_ID }} + aws_region: ${{ env.AWS_DEFAULT_REGION }} + + - name: Build Code + run: npm run build + + - name: Test Code + run: npm test + + - name: Tofu Init + run: tofu -chdir=deploy init -input=false -backend-config="bucket=${STATE_BUCKET}" -backend-config="key=${STATE_KEY}" + + - name: Tofu Plan + run: tofu -chdir=deploy plan -out=plan.cache + + - name: Convert Plan to JSON + run: tofu -chdir=deploy show -json plan.cache > deploy/plan.json + + - name: Check for Destructive Changes + id: plan_check + run: | + DESTROYS=$(jq '[.resource_changes[]? | select(.change.actions | contains(["delete"]))] | length' deploy/plan.json) + + echo "Found $DESTROYS destructive changes." + + if [ "$DESTROYS" -gt 0 ]; then + echo "requires_approval=true" >> $GITHUB_OUTPUT + echo "⚠️ DESTRUCTIVE CHANGES DETECTED. Routing to manual approval." >> $GITHUB_STEP_SUMMARY + else + echo "requires_approval=false" >> $GITHUB_OUTPUT + echo "✅ No destructive changes. Routing to auto-deploy." >> $GITHUB_STEP_SUMMARY + fi + + - name: Pass Artifacts to Deploy Job + uses: actions/upload-artifact@v4 + with: + name: prod-artifacts + path: | + deploy/plan.cache + deploy/plan.json + dist/ + retention-days: 1 + + # ========================================== + # PATH B: PRODUCTION DEPLOY (Gated) + # ========================================== + prod_deploy: + name: Prod Deploy + needs: prod_build_and_plan + if: github.event_name == 'push' || github.event_name == 'workflow_dispatch' + runs-on: ubuntu-latest + concurrency: production + environment: + name: ${{ needs.prod_build_and_plan.outputs.requires_approval == 'true' && 'production-gated' || 'production-auto' }} + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Setup Environment + uses: ./.github/actions/setup + with: + aws_account_id: ${{ secrets.AWS_ACCOUNT_ID }} + aws_region: ${{ env.AWS_DEFAULT_REGION }} + + - name: Retrieve Artifacts + uses: actions/download-artifact@v4 + with: + name: prod-artifacts + + - name: Show Plan Summary + if: needs.prod_build_and_plan.outputs.requires_approval == 'true' + run: | + curl -sL https://github.com/dineshba/tf-summarize/releases/latest/download/tf-summarize_linux_amd64.tar.gz | tar xz + ./tf-summarize -json deploy/plan.json >> $GITHUB_STEP_SUMMARY + + - name: Tofu Init + run: tofu -chdir=deploy init -input=false -backend-config="bucket=${STATE_BUCKET}" -backend-config="key=${STATE_KEY}" + + - name: Tofu Apply + run: tofu -chdir=deploy apply -input=false -auto-approve plan.cache + + - name: Deploy Application Code + run: npm run deploy \ No newline at end of file diff --git a/.gitignore b/.gitignore index 12ca0f0..44e4a2b 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,3 @@ node_modules/ dist/ -*.tsbuildinfo -package-lock.json +*.tsbuildinfo \ No newline at end of file diff --git a/TODO.md b/TODO.md index 79c6c29..1824d63 100644 --- a/TODO.md +++ b/TODO.md @@ -1,16 +1,23 @@ # TODO -- [ ] Review and complete `WORKFLOW_UX_SPEC.md` implementation -- [ ] Wire infra (see `infra/`) -- [ ] Set up CI (lint, typecheck, test) for backend, site, and extension independently +- [x] Review and complete `WORKFLOW_UX_SPEC.md` implementation — urgency system rules (SR-15–SR-24) encode conversation/crm/support urgency mappings; `baseUrgency` handles remaining workflows. +- [x] **Urgency is a first-class field** — `Signal.urgency` and `Arc.urgency` are explicit fields; no `system:urgency:*` labels; label-to-urgency conversion rules (SR-08–SR-12) removed; `priorityCalculator` removed; urgency resolves as: set_urgency rule outcome ?? arc.urgency ?? "normal"; arc.urgency set on creation only, user-settable via API after that. +- [x] **Classifier workflow names aligned** — classifier prompt rewritten to use the 15 consolidated workflow names matching TypeScript `Workflow` type (removed: invoice, order, financial, newsletter, promotions, social, personal, security, developer, subscription, government, notice). +- [x] **Block phishing/status emails by default** — SR-05 blocks all `status` workflow emails; first-rule-wins for status-changing actions (`statusSet` flag in `deriveOutcome`). +- [x] Wire infra (see `infra/`) +- [x] Set up CI (lint, typecheck, test) for backend, site, and extension independently — `.github/workflows/backend.yml`, `site.yml`, `extension.yml` - [x] **API modernization** — collection envelopes, error shapes, PUT→PATCH, consistent create/update responses. See "API Breaking Changes" section below. - [ ] Review AWS Bedrock comparison with Aurora pg vectors. I think we are looking for RAG, the question is should we store that data in aurora or is there an optimized bedrock version available for us here? -- [ ] Use Zed/Zod or whatever validate in coming requests -- [ ] Dynamically generate the OpenAPI Specification from the types. Build it on deployment using an npm run script, and server it on the `/` endpoint. -- [ ] Global /Search endpoint is wrong, we should always be searching something specific. And we never need a /search do that, the generic GET /whatever is already a search. -- [ ] Digests? Does that even make sense? Basically once per month expose a digest of just list of things I think the idea would be to reuse the same ARC. -- [ ] Use quickjs-emscripten to support custom functions execution as a rule type. -- [ ] Create a WebSocket/WebPush APIGW API, with custom domain. Update the lambda to support connections also from websocket APIGW, through HONO if possible, to send messages back to the extension and to the UI when necessary. +- [x] Use Zod to validate incoming requests — all POST/PATCH handlers now use `zParse()` with typed schemas in `src/api/requests.ts` +- [x] Dynamically generate the OpenAPI Specification from the types. Build it on deployment using an npm run script, and serve it on the `/` endpoint — `scripts/openapi.ts`, `npm run openapi`, `GET /openapi.json` +- [x] Remove or redesign `GET /search` — merged into `GET /arcs?q=`; `GET /search` endpoint removed +- [ ] Digests? Does that even make sense? Basically once per month expose a digest of just list of things — the idea would be to reuse the same Arc. +- [x] Rules support tags — key/value pairs stored on the rule for user annotation (e.g. team, category, notes). No functional effect on rule evaluation. +- [x] Use quickjs-emscripten to support custom JS function execution as a rule type — `src/processor/rule-engine.ts`; conditions prefixed `js:` run in sandboxed QuickJS VM; JSONLogic still handles non-prefixed conditions +- [x] Create WebSocket APIGW API. WebSocket API GW (`infra/api-gateway.tf`); Lambda handles all event types (EventBridge → SQS → WsAuthorizer → WebSocket → HTTP); connections stored in ACCOUNTS_TABLE at `CONN#` SK; notifier fans out via `WS_API_ENDPOINT`; Web Push removed in favour of WebSocket +- [ ] We should also set up the S3 bucket as an origin and connect it to the CloudFront distribution, so that the front end can deploy there. Which means we need the necessary website related bucket, policy, stuff. +- [ ] And all buckets should use the new format from AWS to ensure local regional buckets for the account, so that we can be sure these buckets haven't already been claimed. ALL BUCKETS. +- [ ] For the dkim_private_key, I passing it into open tofu is just not really secure, right? Discuss with me alternatives to make that happen. --- @@ -105,17 +112,9 @@ DELETE /accounts/:accountId/signals/:id — discard draft; 400 if not draf - [x] **Spam score threshold configurable** — `spamScoreThreshold` on both `AccountFilteringConfig` and `EmailAddressConfig` with account → per-address override chain. - [x] **Two-tier domain setup model** — `receivingSetupComplete`, `senderSetupComplete`, per-record `DnsRecord` status, all in `Domain` type and API. - [ ] **Become FedCM identity provider** — meaning other apps log in via our app. This means registering as a FedCM provider so other apps can log in. -- [ ] **Block phishing-warning and terms-update emails by default** — these two classes of email are almost universally unwanted noise. No user toggle — just block them: - 1. **Phishing-warning notices** ("Beware of phishing — we will never ask for your password") — bulk security awareness emails sent by banks and SaaS services. Already classified as `notice` workflow. Block silently by default. - 2. **Terms-of-service / privacy-policy updates** — already classified as `notice` workflow. Currently silently auto-archived; upgrade to **block** (silent drop, not quarantine). Set `blockDisposition.notice: "block"` in the default account filtering config. - - **Classifier prompt**: add examples under the `### notice` section distinguishing "bank phishing warning" from "actual phishing email" — the former is `notice`, the latter is e.g. `auth` with high spamScore. This prevents mis-classification. -- [ ] Add `DELETE /domains/:id` endpoint and handler — remove SES email identity if it exists, delete domain record from DynamoDB; inbound mail for that domain will stop routing to SES naturally -- [ ] **Domain health monitoring** — weekly proactive DNS check across all accounts and domains: - - **Primary detection — scheduled DNS resolution**: SES only gives positive signals for identities we've registered; if a customer removes their MX record, email silently stops arriving and SES never tells us. The only reliable detection is us actively resolving DNS. EventBridge weekly rule → Lambda → scan all accounts → all registered domains per account → DNS-resolve each record that belongs to the setup tier the customer has completed → notify if degraded. **Do not write health status back to DynamoDB** — health is computed live, not cached, to avoid stale state discrepancies. - - **Secondary detection — SES bounce/complaint feedback**: `feedback-processor.ts` already consumes SNS feedback events. If hard-bounce rate exceeds 5% in a rolling window for a given domain, trigger an on-demand DNS health check for that domain. Not a substitute for the weekly scan but catches real-world delivery failures between scheduled runs. - - **SES reputation SNS event**: listen for `AmazonSesAccountReputationNotification` — if SES suspends a sending identity, notify all `owner` and `admin` users immediately. - - **On degradation**: email and in-app notify all `owner` and `admin` users with domain name, which records are failing, and correct expected values. Do not halt inbound processing immediately — SES may still route for a period. - - **On-demand re-check**: `POST /domains/:id/verify` runs a live DNS check immediately — powers the UI "Re-check DNS" button. The domain GET endpoint also resolves DNS on demand to return current per-record status: `{ name, type, value, currentValue?, status: "verified"|"failing"|"pending" }`. No stale cache, no stored health fields needed. +- [x] **Block phishing-warning and terms-update emails by default** — covered above. +- [x] Add `DELETE /domains/:id` endpoint and handler — handler exists at `src/api/app.ts` +- [x] **Domain health monitoring** — `src/dns/dns-checker.ts`, `src/jobs/domain-health-job.ts`; `POST /domains/:id/verify` for on-demand check; `GET /domains/:id` includes live DNS check; EventBridge weekly cron in `infra/storage.tf`; handler dispatches via `source: "domain-health-job"` in EventBridge event detail - [ ] **Submit to awesome-privacy-tools** — open a PR at https://github.com/anondotli/awesome-privacy-tools/blob/main/CONTRIBUTING.md to add this project to the list. Follow the contributing guidelines before submitting. @@ -232,11 +231,11 @@ The extension (`extension/`) has a working implementation that assumes a `/alias ### What the backend needs to add for extension support - [x] **`Alias` type** — renamed from `EmailAddressConfig`; now includes `createdForOrigin?: string` for alias-per-site tracking. Stored embedded in the `Account` DynamoDB record, keyed by address. -- [ ] **`POST /accounts/:accountId/aliases`** — create a new alias (the extension calls this on signup; currently only `PUT` exists). `PUT` upserts by address, so `POST` can be a thin wrapper or the extension can switch to `PUT` directly. -- [ ] **`GET /accounts/:accountId/aliases?domain=`** — the list endpoint returns all aliases; add a `domain` query param filter so the extension can find the alias for a specific origin without fetching everything. -- [ ] **`PUT /aliases/:email` with `newEmail` rename** — if the user edits the generated alias before submitting, the extension sends `{ newEmail }`. Requires deleting the old map key and re-inserting — handle this in the `PUT` handler. -- [ ] **Web Push subscription endpoint** — `POST /accounts/:accountId/push-subscriptions` to register the extension's push endpoint. Required for OTP delivery (see extension TODO). -- [ ] **Notifier: push `auth` arcs via Web Push** — when an `auth` signal arrives, the notifier should send a Web Push payload `{ code, expiresInMinutes, originDomain }` to all registered push subscriptions for the account, in addition to (or instead of) the existing email notification. +- [x] **`POST /accounts/:accountId/aliases`** — handler exists in `src/api/app.ts` +- [x] **`GET /accounts/:accountId/aliases?domain=`** — `?domain=` filter added; filters by `createdForOrigin` +- [x] **`PATCH /aliases/:address` with `newAddress` rename** — `renameAlias` in `account-database.ts`; copies all sender items to new address, deletes old +- [x] **Web Push subscription endpoint** — `POST/DELETE /accounts/:accountId/push-subscriptions` +- [x] **Notifier: push `auth` arcs via Web Push** — `pushNotify` in `ses-notifier.ts` fires for `auth` workflow; handles 410 Gone by deleting expired subscriptions - These are all **free tier** per the pricing strategy. ### What the extension needs to fix @@ -258,7 +257,7 @@ The primary view. Arcs are the browsing unit — not individual emails. - Each arc row shows: workflow icon, sender name/domain, AI-generated summary, urgency badge, last signal timestamp, label chips - Urgency drives visual prominence: `critical` = red/bold, `high` = orange, `normal` = default, `low` = muted, `silent` arcs are never shown -- Arcs with `sentMessageIds` (user has replied) should show a "replied" indicator — the backend already promotes urgency to `high` on these, the UI should also visually distinguish them +- Arcs with `sentMessageIds` (user has replied) should show a "replied" indicator — these carry the `system:replied` label; the UI should visually distinguish them - Arc status filter: REST-style `?status=active|archived|snoozed|deleted` query param (four statuses: `active`, `archived`, `snoozed`, `deleted`) - Swipe/hover actions: archive, delete, label - Inline "unread" state (client-side or via a future `Arc.readAt` field) @@ -274,15 +273,16 @@ Drill-in from inbox. Shows all signals in the arc as a chronological thread. - Each signal card shows: from, to, cc, subject, received timestamp, AI summary, spam score (if > 0.3, show warning indicator), body (text or HTML rendered in sandboxed iframe), attachments list - `original:john@gmail.com` label (forwarded email detection) appears in the label chips alongside all other labels - Workflow-specific structured data panels — each workflow has rich `workflowData` fields the UI should render as a card rather than raw JSON: - - `order` → order number, tracking link, items list, estimated delivery, status - - `invoice` → amount, due date, invoice number, download link + - `package` → order number, tracking link, items list, estimated delivery, status + - `payments` → amount, due date, invoice number, download link, payment type - `travel` → flight number, departure/arrival, confirmation code, boarding pass link - `auth` → OTP/magic link action button (copy code, open link), expiry countdown - - `financial` → amount, account last 4, transaction date, `isSuspicious` flag (bank has explicitly flagged unusual/unauthorized activity — renders as a red "Fraud alert" banner on the card; drives `critical` urgency) + - `alert` → service, severity, requiresAction flag, error message snippet - `job` → company, role, stage (applied / interview / offer), action required flag - - `subscription` → service name, renewal date, payment failed flag, action CTA - `healthcare` → appointment date, provider, action required flag - - `developer` → service, severity, requiresAction flag, error message snippet + - `crm` → sender company, role, deal value, urgency, requiresReply flag + - `support` → ticket ID, service, priority, agent name, eventType status + - `scheduling` → event title, start/end time, location, organizer, requiresResponse - AI-suggested labels shown with one-click accept - User can manually override workflow classification (dropdown) - User can manually add/remove labels @@ -580,7 +580,7 @@ Progress bar at top spanning all steps. Every step is resumable — if the user - **Signal ID prefix** (`SES#`, `SYS#`, `USR#`) indicates origin — could show a subtle badge on signals that were system- or user-created vs inbound email - **Spam score** should surface as a warning on signals > 0.3 and a strong warning > 0.7; never shown as a raw number to end users — use labels like "Likely spam" / "Possible spam" - **Arc grouping key** is deterministic per workflow (e.g. all Amazon order updates for order #123 thread together) — UI should not expose the key but should make the threading feel natural, like iMessage threads -- **`notice` workflow** arcs are `silent` urgency and auto-archived — they should not appear in the main inbox; accessible via Archive view only +- **`status` workflow** arcs are blocked by default (SR-05) — they never reach the arc inbox; if SR-05 is disabled by the user they will be silent urgency - **RBAC**: hide destructive actions (delete domain, remove user, edit rules) from `viewer` and `member` roles --- @@ -596,11 +596,11 @@ Creative feature ideas not yet committed to. Separate from the confirmed list ab The classifier already extracts structured `workflowData`. Extend this to surface one-tap CTAs directly on the arc row and signal card, without opening the email: - `auth` → **Copy OTP** button on the arc row (code + countdown timer inline); one tap copies to clipboard; auto-detected from `workflowData.code` -- `order` → **Track Package** deep-link button; carrier + tracking number already in `workflowData` -- `invoice` → **Pay Now** link if `workflowData.paymentUrl` is present +- `package` → **Track Package** deep-link button; tracking number already in `workflowData` +- `payments` → **Pay Now** link if `workflowData.managementUrl` is present; **Download** if `workflowData.downloadUrl` is present - `travel` → **Add to Calendar** (generates `.ics`); **Check In** link if within 24h of departure - `job` → **Stage tracker** inline (Applied → Phone Screen → Interview → Offer) — user updates stage, stored as a label or urgency override -- `subscription` → **Renew** or **Cancel** deep-link if `workflowData.manageUrl` is present +- `scheduling` → **Accept / Decline** if `workflowData.requiresResponse` is true; **Add to Calendar** ### Snooze / Remind Me Later diff --git a/infra/api-gateway.tf b/infra/api-gateway.tf index edca62f..d1cc215 100644 --- a/infra/api-gateway.tf +++ b/infra/api-gateway.tf @@ -66,3 +66,75 @@ resource "aws_lambda_permission" "api_gateway" { principal = "apigateway.amazonaws.com" source_arn = "${aws_apigatewayv2_api.main.execution_arn}/*/*" } + +# --------------------------------------------------------------------------- +# WebSocket API Gateway +# --------------------------------------------------------------------------- + +resource "aws_apigatewayv2_api" "ws" { + name = "${local.prefix}-ws" + protocol_type = "WEBSOCKET" + route_selection_expression = "$request.body.action" +} + +resource "aws_apigatewayv2_integration" "ws_lambda" { + api_id = aws_apigatewayv2_api.ws.id + integration_type = "AWS_PROXY" + integration_uri = aws_lambda_alias.production.invoke_arn + content_handling_strategy = "CONVERT_TO_TEXT" +} + +resource "aws_apigatewayv2_authorizer" "ws" { + api_id = aws_apigatewayv2_api.ws.id + authorizer_type = "REQUEST" + authorizer_uri = aws_lambda_alias.production.invoke_arn + identity_sources = ["$request.querystring.token"] + name = "${local.prefix}-ws-authorizer" + + # Cache the Allow result per token for 5 minutes + authorizer_result_ttl_in_seconds = 300 +} + +resource "aws_lambda_permission" "ws_authorizer" { + statement_id = "AllowWSAuthorizerInvoke" + action = "lambda:InvokeFunction" + function_name = aws_lambda_function.main.function_name + qualifier = aws_lambda_alias.production.name + principal = "apigateway.amazonaws.com" + source_arn = "${aws_apigatewayv2_api.ws.execution_arn}/authorizers/*" +} + +resource "aws_apigatewayv2_route" "ws_connect" { + api_id = aws_apigatewayv2_api.ws.id + route_key = "$connect" + target = "integrations/${aws_apigatewayv2_integration.ws_lambda.id}" + authorization_type = "CUSTOM" + authorizer_id = aws_apigatewayv2_authorizer.ws.id +} + +resource "aws_apigatewayv2_route" "ws_disconnect" { + api_id = aws_apigatewayv2_api.ws.id + route_key = "$disconnect" + target = "integrations/${aws_apigatewayv2_integration.ws_lambda.id}" +} + +resource "aws_apigatewayv2_route" "ws_default" { + api_id = aws_apigatewayv2_api.ws.id + route_key = "$default" + target = "integrations/${aws_apigatewayv2_integration.ws_lambda.id}" +} + +resource "aws_apigatewayv2_stage" "ws" { + api_id = aws_apigatewayv2_api.ws.id + name = "production" + auto_deploy = true +} + +resource "aws_lambda_permission" "ws_gateway" { + statement_id = "AllowWSAPIGatewayInvoke" + action = "lambda:InvokeFunction" + function_name = aws_lambda_function.main.function_name + qualifier = aws_lambda_alias.production.name + principal = "apigateway.amazonaws.com" + source_arn = "${aws_apigatewayv2_api.ws.execution_arn}/*/*" +} diff --git a/infra/aurora.tf b/infra/aurora.tf index a1bf11e..8d9a8bf 100644 --- a/infra/aurora.tf +++ b/infra/aurora.tf @@ -62,6 +62,7 @@ resource "aws_rds_cluster" "aurora" { final_snapshot_identifier = var.env == "prod" ? "${local.prefix}-final" : null enabled_cloudwatch_logs_exports = ["postgresql"] + enable_http_endpoint = true # Aurora Data API } resource "aws_rds_cluster_instance" "aurora" { @@ -72,76 +73,30 @@ resource "aws_rds_cluster_instance" "aurora" { engine_version = aws_rds_cluster.aurora.engine_version } -# --------------------------------------------------------------------------- -# RDS Proxy -# Pools Lambda connections to avoid exhausting Aurora's connection limit -# --------------------------------------------------------------------------- - -resource "aws_iam_role" "rds_proxy" { - name = "${local.prefix}-rds-proxy" - - assume_role_policy = jsonencode({ - Version = "2012-10-17" - Statement = [{ - Effect = "Allow" - Principal = { Service = "rds.amazonaws.com" } - Action = "sts:AssumeRole" - }] - }) -} - -resource "aws_iam_role_policy" "rds_proxy_secrets" { - role = aws_iam_role.rds_proxy.id - - policy = jsonencode({ - Version = "2012-10-17" - Statement = [{ - Effect = "Allow" - Action = ["secretsmanager:GetSecretValue", "secretsmanager:DescribeSecret"] - Resource = aws_secretsmanager_secret.aurora_master.arn - }] - }) -} - -resource "aws_db_proxy" "aurora" { - name = "${local.prefix}-proxy" - debug_logging = false - engine_family = "POSTGRESQL" - idle_client_timeout = 1800 - require_tls = true - role_arn = aws_iam_role.rds_proxy.arn - vpc_security_group_ids = [aws_security_group.rds_proxy.id] - vpc_subnet_ids = aws_subnet.private[*].id - - auth { - auth_scheme = "SECRETS" - secret_arn = aws_secretsmanager_secret.aurora_master.arn - iam_auth = "REQUIRED" - } -} - -resource "aws_db_proxy_default_target_group" "aurora" { - db_proxy_name = aws_db_proxy.aurora.name - - connection_pool_config { - max_connections_percent = 90 - max_idle_connections_percent = 50 - connection_borrow_timeout = 120 - } -} - -resource "aws_db_proxy_target" "aurora" { - db_proxy_name = aws_db_proxy.aurora.name - target_group_name = aws_db_proxy_default_target_group.aurora.name - db_cluster_identifier = aws_rds_cluster.aurora.cluster_identifier -} resource "terraform_data" "pgvector_init" { triggers_replace = [aws_rds_cluster.aurora.id] - # Run after cluster is available to enable the pgvector extension - # In practice this is handled by a migration script in your CI pipeline: - # psql $DATABASE_URL -c "CREATE EXTENSION IF NOT EXISTS vector;" - # psql $DATABASE_URL -c "CREATE TABLE arc_embeddings (arc_id TEXT PRIMARY KEY, embedding vector(1024));" - # psql $DATABASE_URL -c "CREATE INDEX ON arc_embeddings USING ivfflat (embedding vector_cosine_ops) WITH (lists = 100);" + # Run once after cluster creation via CI migration script (requires VPC access): + # + # CREATE EXTENSION IF NOT EXISTS vector; + # + # CREATE TABLE arc_embeddings ( + # arc_id TEXT PRIMARY KEY, + # account_id TEXT NOT NULL, + # recipient_address TEXT NOT NULL, + # embedding vector(1024) NOT NULL, + # updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() + # ); + # + # CREATE INDEX ON arc_embeddings + # USING hnsw (embedding vector_cosine_ops); + # + # -- Row-Level Security: Lambda must SET LOCAL app.current_account_id before + # -- any query. If unset, current_setting returns NULL and no rows are visible. + # ALTER TABLE arc_embeddings ENABLE ROW LEVEL SECURITY; + # ALTER TABLE arc_embeddings FORCE ROW LEVEL SECURITY; + # CREATE POLICY arc_tenant_isolation ON arc_embeddings + # USING (account_id = current_setting('app.current_account_id', true)) + # WITH CHECK (account_id = current_setting('app.current_account_id', true)); } diff --git a/infra/lambda.tf b/infra/lambda.tf index e01177e..5baae92 100644 --- a/infra/lambda.tf +++ b/infra/lambda.tf @@ -15,11 +15,6 @@ resource "aws_iam_role" "lambda" { }) } -resource "aws_iam_role_policy_attachment" "lambda_vpc" { - role = aws_iam_role.lambda.name - policy_arn = "arn:aws:iam::aws:policy/service-role/AWSLambdaVPCAccessExecutionRole" -} - resource "aws_iam_role_policy" "lambda_permissions" { role = aws_iam_role.lambda.id @@ -52,8 +47,16 @@ resource "aws_iam_role_policy" "lambda_permissions" { "${aws_dynamodb_table.signals.arn}/index/*", aws_dynamodb_table.processing.arn, "${aws_dynamodb_table.processing.arn}/index/*", + aws_dynamodb_table.audit.arn, + "${aws_dynamodb_table.audit.arn}/index/*", ] }, + { + Sid = "WebSocketManage" + Effect = "Allow" + Action = ["execute-api:ManageConnections"] + Resource = "${aws_apigatewayv2_api.ws.execution_arn}/*/@connections/*" + }, { Sid = "BedrockInvoke" Effect = "Allow" @@ -64,10 +67,15 @@ resource "aws_iam_role_policy" "lambda_permissions" { ] }, { - Sid = "RdsProxyConnect" - Effect = "Allow" - Action = ["rds-db:connect"] - Resource = "*" + Sid = "AuroraDataAPI" + Effect = "Allow" + Action = [ + "rds-data:ExecuteStatement", + "rds-data:BeginTransaction", + "rds-data:CommitTransaction", + "rds-data:RollbackTransaction", + ] + Resource = aws_rds_cluster.aurora.arn }, { Sid = "SESSend" @@ -143,24 +151,22 @@ resource "aws_lambda_function" "main" { filename = data.archive_file.lambda_stub.output_path source_code_hash = data.archive_file.lambda_stub.output_base64sha256 - vpc_config { - subnet_ids = aws_subnet.private[*].id - security_group_ids = [aws_security_group.lambda.id] - } - environment { variables = { NODE_ENV = var.env ACCOUNTS_TABLE = aws_dynamodb_table.accounts.name SIGNALS_TABLE = aws_dynamodb_table.signals.name PROCESSING_TABLE = aws_dynamodb_table.processing.name + AUDIT_TABLE = aws_dynamodb_table.audit.name EMAIL_BUCKET = aws_s3_bucket.emails.name - RDS_PROXY_ENDPOINT = aws_db_proxy.aurora.endpoint + AURORA_CLUSTER_ARN = aws_rds_cluster.aurora.arn + AURORA_SECRET_ARN = aws_secretsmanager_secret.aurora_master.arn AURORA_DB_NAME = "signals" - DB_USER = "lambda" NOTIFICATION_FROM = var.notification_from_address SES_CONFIGURATION_SET = aws_sesv2_configuration_set.sending.configuration_set_name APP_BASE_URL = var.app_base_url + WS_API_ENDPOINT = "https://${aws_apigatewayv2_api.ws.id}.execute-api.${data.aws_region.current.name}.amazonaws.com/${aws_apigatewayv2_stage.ws.name}" + CF_ORIGIN_SECRET = random_password.cf_origin_secret.result } } diff --git a/infra/outputs.tf b/infra/outputs.tf index be5620b..9801d74 100644 --- a/infra/outputs.tf +++ b/infra/outputs.tf @@ -42,11 +42,6 @@ output "signals_dlq_url" { value = aws_sqs_queue.signals_dlq.url } -output "rds_proxy_endpoint" { - description = "RDS Proxy endpoint for Lambda to connect to Aurora" - value = aws_db_proxy.aurora.endpoint -} - output "aurora_cluster_identifier" { value = aws_rds_cluster.aurora.cluster_identifier } @@ -54,3 +49,16 @@ output "aurora_cluster_identifier" { output "ses_rule_set_name" { value = aws_ses_receipt_rule_set.main.rule_set_name } + +output "dynamodb_audit_table" { + value = aws_dynamodb_table.audit.name +} + +output "feedback_queue_url" { + value = aws_sqs_queue.feedback.url +} + +output "ws_api_endpoint" { + description = "WebSocket API endpoint — clients connect to wss://.execute-api..amazonaws.com/production" + value = "wss://${aws_apigatewayv2_api.ws.id}.execute-api.${data.aws_region.current.name}.amazonaws.com/${aws_apigatewayv2_stage.ws.name}" +} diff --git a/infra/providers.tf b/infra/providers.tf index f3de889..0c760ec 100644 --- a/infra/providers.tf +++ b/infra/providers.tf @@ -65,6 +65,8 @@ provider "aws" { } } +data "aws_region" "current" {} + locals { prefix = "ses-email-adapter-${var.env}" mail_domain = split("@", var.notification_from_address)[1] diff --git a/infra/ses.tf b/infra/ses.tf index 9c263a7..c44b840 100644 --- a/infra/ses.tf +++ b/infra/ses.tf @@ -78,7 +78,7 @@ resource "aws_route53_record" "ses_mx_host" { name = "mx.${local.mail_domain}" type = "CNAME" ttl = 300 - records = ["inbound-smtp.eu-west-1.amazonaws.com"] + records = ["inbound-smtp.${data.aws_region.current.name}.amazonaws.com"] } # Platform domain's own MX record — points to our branded hostname @@ -103,7 +103,7 @@ resource "aws_route53_record" "bounce_mx" { name = "bounce.${local.mail_domain}" type = "MX" ttl = 300 - records = ["10 feedback-smtp.eu-west-1.amazonses.com"] + records = ["10 feedback-smtp.${data.aws_region.current.name}.amazonses.com"] } # SPF on the bounce subdomain — SES is the only authorised sender diff --git a/infra/storage.tf b/infra/storage.tf index 6acc206..3284ece 100644 --- a/infra/storage.tf +++ b/infra/storage.tf @@ -151,8 +151,17 @@ resource "aws_dynamodb_table" "accounts" { hash_key = "pk" range_key = "sk" - attribute { name = "pk"; type = "S" } - attribute { name = "sk"; type = "S" } + attribute { name = "pk"; type = "S" } + attribute { name = "sk"; type = "S" } + attribute { name = "gsi1pk"; type = "S" } + attribute { name = "gsi1sk"; type = "S" } + + global_secondary_index { + name = "gsi1" + hash_key = "gsi1pk" + range_key = "gsi1sk" + projection_type = "ALL" + } point_in_time_recovery { enabled = true } deletion_protection_enabled = true @@ -223,3 +232,57 @@ resource "aws_dynamodb_table" "processing" { region_name = "eu-central-1" } } + +resource "aws_dynamodb_table" "audit" { + name = "${local.prefix}-audit" + billing_mode = "PAY_PER_REQUEST" + hash_key = "pk" + range_key = "sk" + + attribute { name = "pk"; type = "S" } + attribute { name = "sk"; type = "S" } + attribute { name = "gsi1pk"; type = "S" } + attribute { name = "gsi1sk"; type = "S" } + + global_secondary_index { + name = "gsi1" + hash_key = "gsi1pk" + range_key = "gsi1sk" + projection_type = "ALL" + } + + ttl { + attribute_name = "ttl" + enabled = true + } + + point_in_time_recovery { enabled = true } + deletion_protection_enabled = true +} + +# --------------------------------------------------------------------------- +# EventBridge — weekly domain health check +# --------------------------------------------------------------------------- + +resource "aws_cloudwatch_event_rule" "domain_health" { + name = "${local.prefix}-domain-health" + description = "Weekly DNS health check for all registered domains" + schedule_expression = "cron(0 6 ? * MON *)" +} + +resource "aws_cloudwatch_event_target" "domain_health" { + rule = aws_cloudwatch_event_rule.domain_health.name + target_id = "domain-health-lambda" + arn = aws_lambda_alias.production.arn + + input = jsonencode({ source = "domain-health-job" }) +} + +resource "aws_lambda_permission" "domain_health_eventbridge" { + statement_id = "AllowDomainHealthEventBridge" + action = "lambda:InvokeFunction" + function_name = aws_lambda_function.main.function_name + qualifier = aws_lambda_alias.production.name + principal = "events.amazonaws.com" + source_arn = aws_cloudwatch_event_rule.domain_health.arn +} diff --git a/infra/vpc.tf b/infra/vpc.tf index a4938e4..07dc02b 100644 --- a/infra/vpc.tf +++ b/infra/vpc.tf @@ -2,9 +2,12 @@ locals { azs = ["eu-west-1a", "eu-west-1b", "eu-west-1c"] vpc_cidr = "10.0.0.0/16" private_cidrs = ["10.0.1.0/24", "10.0.2.0/24", "10.0.3.0/24"] - public_cidrs = ["10.0.101.0/24", "10.0.102.0/24", "10.0.103.0/24"] } +# VPC exists solely to host Aurora — Lambda accesses it via Data API over HTTPS, +# not via a VPC connection, so no NAT gateways, public subnets, or internet +# gateway are needed. + resource "aws_vpc" "main" { cidr_block = local.vpc_cidr enable_dns_hostnames = true @@ -20,110 +23,11 @@ resource "aws_subnet" "private" { tags = { Name = "${local.prefix}-private-${count.index}" } } -resource "aws_subnet" "public" { - count = length(local.azs) - vpc_id = aws_vpc.main.id - cidr_block = local.public_cidrs[count.index] - availability_zone = local.azs[count.index] - map_public_ip_on_launch = true - - tags = { Name = "${local.prefix}-public-${count.index}" } -} - -resource "aws_internet_gateway" "main" { - vpc_id = aws_vpc.main.id -} - -resource "aws_eip" "nat" { - count = length(local.azs) - domain = "vpc" -} - -resource "aws_nat_gateway" "main" { - count = length(local.azs) - allocation_id = aws_eip.nat[count.index].id - subnet_id = aws_subnet.public[count.index].id - depends_on = [aws_internet_gateway.main] -} - -resource "aws_route_table" "public" { - vpc_id = aws_vpc.main.id - route { - cidr_block = "0.0.0.0/0" - gateway_id = aws_internet_gateway.main.id - } -} - -resource "aws_route_table_association" "public" { - count = length(aws_subnet.public) - subnet_id = aws_subnet.public[count.index].id - route_table_id = aws_route_table.public.id -} - -resource "aws_route_table" "private" { - count = length(local.azs) - vpc_id = aws_vpc.main.id - route { - cidr_block = "0.0.0.0/0" - nat_gateway_id = aws_nat_gateway.main[count.index].id - } -} - -resource "aws_route_table_association" "private" { - count = length(aws_subnet.private) - subnet_id = aws_subnet.private[count.index].id - route_table_id = aws_route_table.private[count.index].id -} - -# --------------------------------------------------------------------------- -# Security groups -# --------------------------------------------------------------------------- - -resource "aws_security_group" "lambda" { - name = "${local.prefix}-lambda" - description = "Lambda function — outbound only" - vpc_id = aws_vpc.main.id - - egress { - from_port = 0 - to_port = 0 - protocol = "-1" - cidr_blocks = ["0.0.0.0/0"] - } -} - -resource "aws_security_group" "rds_proxy" { - name = "${local.prefix}-rds-proxy" - description = "RDS Proxy — accepts connections from Lambda" - vpc_id = aws_vpc.main.id - - ingress { - from_port = 5432 - to_port = 5432 - protocol = "tcp" - security_groups = [aws_security_group.lambda.id] - } - - egress { - from_port = 0 - to_port = 0 - protocol = "-1" - cidr_blocks = ["0.0.0.0/0"] - } -} - resource "aws_security_group" "aurora" { name = "${local.prefix}-aurora" - description = "Aurora — accepts connections from RDS Proxy only" + description = "Aurora — no inbound connections needed (accessed via Data API)" vpc_id = aws_vpc.main.id - ingress { - from_port = 5432 - to_port = 5432 - protocol = "tcp" - security_groups = [aws_security_group.rds_proxy.id] - } - egress { from_port = 0 to_port = 0 diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..57365a0 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,6148 @@ +{ + "name": "ses-email-adapter", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "ses-email-adapter", + "version": "1.0.0", + "dependencies": { + "@authress/sdk": "^3.2.266", + "@aws-sdk/client-bedrock-runtime": "^3.0.0", + "@aws-sdk/client-dynamodb": "^3.0.0", + "@aws-sdk/client-rds-data": "^3.1045.0", + "@aws-sdk/client-s3": "^3.0.0", + "@aws-sdk/client-sesv2": "^3.0.0", + "@aws-sdk/lib-dynamodb": "^3.0.0", + "@aws-sdk/s3-request-presigner": "^3.0.0", + "@hono/zod-openapi": "^1.3.0", + "@types/web-push": "^3.6.4", + "hono": "^4.0.0", + "json-logic-js": "^2.0.0", + "mailparser": "^3.7.0", + "quickjs-emscripten": "^0.32.0", + "tldts": "^7.0.28", + "web-push": "^3.6.7", + "zod": "^4.4.3" + }, + "devDependencies": { + "@types/aws-lambda": "^8.10.0", + "@types/json-logic-js": "^2.0.0", + "@types/mailparser": "^3.4.0", + "@types/node": "^22.0.0", + "@vitest/coverage-v8": "^2.0.0", + "commander": "^12.0.0", + "esbuild": "^0.24.0", + "tsx": "^4.0.0", + "typescript": "^5.7.0", + "vitest": "^2.0.0" + }, + "engines": { + "node": ">=24" + } + }, + "node_modules/@ampproject/remapping": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", + "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@asteasolutions/zod-to-openapi": { + "version": "8.5.0", + "resolved": "https://registry.npmjs.org/@asteasolutions/zod-to-openapi/-/zod-to-openapi-8.5.0.tgz", + "integrity": "sha512-SABbKiObg5dLRiTFnqiW1WWwGcg1BJfmHtT2asIBnBHg6Smy/Ms2KHc650+JI4Hw7lSkdiNebEGXpwoxfben8Q==", + "license": "MIT", + "dependencies": { + "openapi3-ts": "^4.1.2" + }, + "peerDependencies": { + "zod": "^4.0.0" + } + }, + "node_modules/@authress/login": { + "version": "2.6.450", + "resolved": "https://registry.npmjs.org/@authress/login/-/login-2.6.450.tgz", + "integrity": "sha512-BKLLnkfT2DAjOKRkTU2s3hjhqiYsLmHTatu3XSKHaQWJi7xgsRcKMyORFCnejdA2x5GiwdfplQx9HSs/tDjrSw==", + "license": "Apache-2.0", + "dependencies": { + "cookie": "<1", + "lodash.take": "^4.1.1" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@authress/sdk": { + "version": "3.2.266", + "resolved": "https://registry.npmjs.org/@authress/sdk/-/sdk-3.2.266.tgz", + "integrity": "sha512-jZdYTKRL5+AeyQ4Svp8Iw0XAL19QsnEEAXgccSKTDUuETgbKja/jlMm7Lhb5ckZHeC2DfDne3Jfv5WzStu1YoQ==", + "license": "Apache-2.0", + "dependencies": { + "@authress/login": "*", + "base64url": "^3.0.1", + "jose": "^4.8.3", + "luxon": "3" + }, + "engines": { + "node": ">=22" + }, + "peerDependencies": { + "@aws-sdk/client-kms": ">=3" + }, + "peerDependenciesMeta": { + "@aws-sdk/client-kms": { + "optional": true + } + } + }, + "node_modules/@aws-crypto/crc32": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/crc32/-/crc32-5.2.0.tgz", + "integrity": "sha512-nLbCWqQNgUiwwtFsen1AdzAtvuLRsQS8rYgMuxCrdKf9kOssamGLuPwyTY9wyYblNr9+1XM8v6zoDTPPSIeANg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/util": "^5.2.0", + "@aws-sdk/types": "^3.222.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-crypto/crc32c": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/crc32c/-/crc32c-5.2.0.tgz", + "integrity": "sha512-+iWb8qaHLYKrNvGRbiYRHSdKRWhto5XlZUEBwDjYNf+ly5SVYG6zEoYIdxvf5R3zyeP16w4PLBn3rH1xc74Rag==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/util": "^5.2.0", + "@aws-sdk/types": "^3.222.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-crypto/sha1-browser": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/sha1-browser/-/sha1-browser-5.2.0.tgz", + "integrity": "sha512-OH6lveCFfcDjX4dbAvCFSYUjJZjDr/3XJ3xHtjn3Oj5b9RjojQo8npoLeA/bNwkOkrSQ0wgrHzXk4tDRxGKJeg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/supports-web-crypto": "^5.2.0", + "@aws-crypto/util": "^5.2.0", + "@aws-sdk/types": "^3.222.0", + "@aws-sdk/util-locate-window": "^3.0.0", + "@smithy/util-utf8": "^2.0.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-crypto/sha1-browser/node_modules/@smithy/is-array-buffer": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-2.2.0.tgz", + "integrity": "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-crypto/sha1-browser/node_modules/@smithy/util-buffer-from": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-2.2.0.tgz", + "integrity": "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/is-array-buffer": "^2.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-crypto/sha1-browser/node_modules/@smithy/util-utf8": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-2.3.0.tgz", + "integrity": "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/util-buffer-from": "^2.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-crypto/sha256-browser": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/sha256-browser/-/sha256-browser-5.2.0.tgz", + "integrity": "sha512-AXfN/lGotSQwu6HNcEsIASo7kWXZ5HYWvfOmSNKDsEqC4OashTp8alTmaz+F7TC2L083SFv5RdB+qU3Vs1kZqw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha256-js": "^5.2.0", + "@aws-crypto/supports-web-crypto": "^5.2.0", + "@aws-crypto/util": "^5.2.0", + "@aws-sdk/types": "^3.222.0", + "@aws-sdk/util-locate-window": "^3.0.0", + "@smithy/util-utf8": "^2.0.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-crypto/sha256-browser/node_modules/@smithy/is-array-buffer": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-2.2.0.tgz", + "integrity": "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-crypto/sha256-browser/node_modules/@smithy/util-buffer-from": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-2.2.0.tgz", + "integrity": "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/is-array-buffer": "^2.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-crypto/sha256-browser/node_modules/@smithy/util-utf8": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-2.3.0.tgz", + "integrity": "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/util-buffer-from": "^2.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-crypto/sha256-js": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/sha256-js/-/sha256-js-5.2.0.tgz", + "integrity": "sha512-FFQQyu7edu4ufvIZ+OadFpHHOt+eSTBaYaki44c+akjg7qZg9oOQeLlk77F6tSYqjDAFClrHJk9tMf0HdVyOvA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/util": "^5.2.0", + "@aws-sdk/types": "^3.222.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-crypto/supports-web-crypto": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/supports-web-crypto/-/supports-web-crypto-5.2.0.tgz", + "integrity": "sha512-iAvUotm021kM33eCdNfwIN//F77/IADDSs58i+MDaOqFrVjZo9bAal0NK7HurRuWLLpF1iLX7gbWrjHjeo+YFg==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-crypto/util": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/util/-/util-5.2.0.tgz", + "integrity": "sha512-4RkU9EsI6ZpBve5fseQlGNUWKMa1RLPQ1dnjnQoe07ldfIzcsGb5hC5W0Dm7u423KWzawlrpbjXBrXCEv9zazQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.222.0", + "@smithy/util-utf8": "^2.0.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-crypto/util/node_modules/@smithy/is-array-buffer": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-2.2.0.tgz", + "integrity": "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-crypto/util/node_modules/@smithy/util-buffer-from": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-2.2.0.tgz", + "integrity": "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/is-array-buffer": "^2.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-crypto/util/node_modules/@smithy/util-utf8": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-2.3.0.tgz", + "integrity": "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/util-buffer-from": "^2.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/client-bedrock-runtime": { + "version": "3.1041.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-bedrock-runtime/-/client-bedrock-runtime-3.1041.0.tgz", + "integrity": "sha512-1QehYO3jhdvNQ5mOKtwIiNV04y4aywaNZw9HzCp7SSYCX4yy+AGXc2hhYjCiMDUvQPIELuvbR8MXw81NGAj8ZQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "^3.974.8", + "@aws-sdk/credential-provider-node": "^3.972.39", + "@aws-sdk/eventstream-handler-node": "^3.972.14", + "@aws-sdk/middleware-eventstream": "^3.972.10", + "@aws-sdk/middleware-host-header": "^3.972.10", + "@aws-sdk/middleware-logger": "^3.972.10", + "@aws-sdk/middleware-recursion-detection": "^3.972.11", + "@aws-sdk/middleware-user-agent": "^3.972.38", + "@aws-sdk/middleware-websocket": "^3.972.16", + "@aws-sdk/region-config-resolver": "^3.972.13", + "@aws-sdk/token-providers": "3.1041.0", + "@aws-sdk/types": "^3.973.8", + "@aws-sdk/util-endpoints": "^3.996.8", + "@aws-sdk/util-user-agent-browser": "^3.972.10", + "@aws-sdk/util-user-agent-node": "^3.973.24", + "@smithy/config-resolver": "^4.4.17", + "@smithy/core": "^3.23.17", + "@smithy/eventstream-serde-browser": "^4.2.14", + "@smithy/eventstream-serde-config-resolver": "^4.3.14", + "@smithy/eventstream-serde-node": "^4.2.14", + "@smithy/fetch-http-handler": "^5.3.17", + "@smithy/hash-node": "^4.2.14", + "@smithy/invalid-dependency": "^4.2.14", + "@smithy/middleware-content-length": "^4.2.14", + "@smithy/middleware-endpoint": "^4.4.32", + "@smithy/middleware-retry": "^4.5.7", + "@smithy/middleware-serde": "^4.2.20", + "@smithy/middleware-stack": "^4.2.14", + "@smithy/node-config-provider": "^4.3.14", + "@smithy/node-http-handler": "^4.6.1", + "@smithy/protocol-http": "^5.3.14", + "@smithy/smithy-client": "^4.12.13", + "@smithy/types": "^4.14.1", + "@smithy/url-parser": "^4.2.14", + "@smithy/util-base64": "^4.3.2", + "@smithy/util-body-length-browser": "^4.2.2", + "@smithy/util-body-length-node": "^4.2.3", + "@smithy/util-defaults-mode-browser": "^4.3.49", + "@smithy/util-defaults-mode-node": "^4.2.54", + "@smithy/util-endpoints": "^3.4.2", + "@smithy/util-middleware": "^4.2.14", + "@smithy/util-retry": "^4.3.6", + "@smithy/util-stream": "^4.5.25", + "@smithy/util-utf8": "^4.2.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/client-dynamodb": { + "version": "3.1041.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-dynamodb/-/client-dynamodb-3.1041.0.tgz", + "integrity": "sha512-q0dhIfdm3R9EEElaUXvoLD0EBm4KYPcJC4So/skToMPACiznNgVI2Vup4PKM716KTaDsIcF8V1EA7s33H7oWMg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "^3.974.8", + "@aws-sdk/credential-provider-node": "^3.972.39", + "@aws-sdk/dynamodb-codec": "^3.973.8", + "@aws-sdk/middleware-endpoint-discovery": "^3.972.11", + "@aws-sdk/middleware-host-header": "^3.972.10", + "@aws-sdk/middleware-logger": "^3.972.10", + "@aws-sdk/middleware-recursion-detection": "^3.972.11", + "@aws-sdk/middleware-user-agent": "^3.972.38", + "@aws-sdk/region-config-resolver": "^3.972.13", + "@aws-sdk/types": "^3.973.8", + "@aws-sdk/util-endpoints": "^3.996.8", + "@aws-sdk/util-user-agent-browser": "^3.972.10", + "@aws-sdk/util-user-agent-node": "^3.973.24", + "@smithy/config-resolver": "^4.4.17", + "@smithy/core": "^3.23.17", + "@smithy/fetch-http-handler": "^5.3.17", + "@smithy/hash-node": "^4.2.14", + "@smithy/invalid-dependency": "^4.2.14", + "@smithy/middleware-content-length": "^4.2.14", + "@smithy/middleware-endpoint": "^4.4.32", + "@smithy/middleware-retry": "^4.5.7", + "@smithy/middleware-serde": "^4.2.20", + "@smithy/middleware-stack": "^4.2.14", + "@smithy/node-config-provider": "^4.3.14", + "@smithy/node-http-handler": "^4.6.1", + "@smithy/protocol-http": "^5.3.14", + "@smithy/smithy-client": "^4.12.13", + "@smithy/types": "^4.14.1", + "@smithy/url-parser": "^4.2.14", + "@smithy/util-base64": "^4.3.2", + "@smithy/util-body-length-browser": "^4.2.2", + "@smithy/util-body-length-node": "^4.2.3", + "@smithy/util-defaults-mode-browser": "^4.3.49", + "@smithy/util-defaults-mode-node": "^4.2.54", + "@smithy/util-endpoints": "^3.4.2", + "@smithy/util-middleware": "^4.2.14", + "@smithy/util-retry": "^4.3.6", + "@smithy/util-utf8": "^4.2.2", + "@smithy/util-waiter": "^4.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/client-rds-data": { + "version": "3.1045.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-rds-data/-/client-rds-data-3.1045.0.tgz", + "integrity": "sha512-CvrWbrXwOWfPRrvaBt2cDAaWBtIEIkBGIt2l1HERQxxsHFDYIpQ9+Gj6Nyu7eQjuU1nl34DbNSZhbVDcZxPbHQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "^3.974.8", + "@aws-sdk/credential-provider-node": "^3.972.39", + "@aws-sdk/middleware-host-header": "^3.972.10", + "@aws-sdk/middleware-logger": "^3.972.10", + "@aws-sdk/middleware-recursion-detection": "^3.972.11", + "@aws-sdk/middleware-user-agent": "^3.972.38", + "@aws-sdk/region-config-resolver": "^3.972.13", + "@aws-sdk/types": "^3.973.8", + "@aws-sdk/util-endpoints": "^3.996.8", + "@aws-sdk/util-user-agent-browser": "^3.972.10", + "@aws-sdk/util-user-agent-node": "^3.973.24", + "@smithy/config-resolver": "^4.4.17", + "@smithy/core": "^3.23.17", + "@smithy/fetch-http-handler": "^5.3.17", + "@smithy/hash-node": "^4.2.14", + "@smithy/invalid-dependency": "^4.2.14", + "@smithy/middleware-content-length": "^4.2.14", + "@smithy/middleware-endpoint": "^4.4.32", + "@smithy/middleware-retry": "^4.5.7", + "@smithy/middleware-serde": "^4.2.20", + "@smithy/middleware-stack": "^4.2.14", + "@smithy/node-config-provider": "^4.3.14", + "@smithy/node-http-handler": "^4.6.1", + "@smithy/protocol-http": "^5.3.14", + "@smithy/smithy-client": "^4.12.13", + "@smithy/types": "^4.14.1", + "@smithy/url-parser": "^4.2.14", + "@smithy/util-base64": "^4.3.2", + "@smithy/util-body-length-browser": "^4.2.2", + "@smithy/util-body-length-node": "^4.2.3", + "@smithy/util-defaults-mode-browser": "^4.3.49", + "@smithy/util-defaults-mode-node": "^4.2.54", + "@smithy/util-endpoints": "^3.4.2", + "@smithy/util-middleware": "^4.2.14", + "@smithy/util-retry": "^4.3.6", + "@smithy/util-utf8": "^4.2.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/client-s3": { + "version": "3.1041.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-s3/-/client-s3-3.1041.0.tgz", + "integrity": "sha512-sQV14bIqslnBHuSlLMD+fc3pH+ajop6vnrFlJ4wM4JDqcYwVik4O+9srnZUrkesFw5y+CN0GfOQ06CAgtC4mjQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha1-browser": "5.2.0", + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "^3.974.8", + "@aws-sdk/credential-provider-node": "^3.972.39", + "@aws-sdk/middleware-bucket-endpoint": "^3.972.10", + "@aws-sdk/middleware-expect-continue": "^3.972.10", + "@aws-sdk/middleware-flexible-checksums": "^3.974.16", + "@aws-sdk/middleware-host-header": "^3.972.10", + "@aws-sdk/middleware-location-constraint": "^3.972.10", + "@aws-sdk/middleware-logger": "^3.972.10", + "@aws-sdk/middleware-recursion-detection": "^3.972.11", + "@aws-sdk/middleware-sdk-s3": "^3.972.37", + "@aws-sdk/middleware-ssec": "^3.972.10", + "@aws-sdk/middleware-user-agent": "^3.972.38", + "@aws-sdk/region-config-resolver": "^3.972.13", + "@aws-sdk/signature-v4-multi-region": "^3.996.25", + "@aws-sdk/types": "^3.973.8", + "@aws-sdk/util-endpoints": "^3.996.8", + "@aws-sdk/util-user-agent-browser": "^3.972.10", + "@aws-sdk/util-user-agent-node": "^3.973.24", + "@smithy/config-resolver": "^4.4.17", + "@smithy/core": "^3.23.17", + "@smithy/eventstream-serde-browser": "^4.2.14", + "@smithy/eventstream-serde-config-resolver": "^4.3.14", + "@smithy/eventstream-serde-node": "^4.2.14", + "@smithy/fetch-http-handler": "^5.3.17", + "@smithy/hash-blob-browser": "^4.2.15", + "@smithy/hash-node": "^4.2.14", + "@smithy/hash-stream-node": "^4.2.14", + "@smithy/invalid-dependency": "^4.2.14", + "@smithy/md5-js": "^4.2.14", + "@smithy/middleware-content-length": "^4.2.14", + "@smithy/middleware-endpoint": "^4.4.32", + "@smithy/middleware-retry": "^4.5.7", + "@smithy/middleware-serde": "^4.2.20", + "@smithy/middleware-stack": "^4.2.14", + "@smithy/node-config-provider": "^4.3.14", + "@smithy/node-http-handler": "^4.6.1", + "@smithy/protocol-http": "^5.3.14", + "@smithy/smithy-client": "^4.12.13", + "@smithy/types": "^4.14.1", + "@smithy/url-parser": "^4.2.14", + "@smithy/util-base64": "^4.3.2", + "@smithy/util-body-length-browser": "^4.2.2", + "@smithy/util-body-length-node": "^4.2.3", + "@smithy/util-defaults-mode-browser": "^4.3.49", + "@smithy/util-defaults-mode-node": "^4.2.54", + "@smithy/util-endpoints": "^3.4.2", + "@smithy/util-middleware": "^4.2.14", + "@smithy/util-retry": "^4.3.6", + "@smithy/util-stream": "^4.5.25", + "@smithy/util-utf8": "^4.2.2", + "@smithy/util-waiter": "^4.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/client-sesv2": { + "version": "3.1041.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-sesv2/-/client-sesv2-3.1041.0.tgz", + "integrity": "sha512-CjcZxCkpJaj37Dv6Zgcl0z05uNK8KbfwNpUsHDdH351S/ecD9j2ZfUDbdSvm7onWE3SNpmHMRiJyjkefah0Vgw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "^3.974.8", + "@aws-sdk/credential-provider-node": "^3.972.39", + "@aws-sdk/middleware-host-header": "^3.972.10", + "@aws-sdk/middleware-logger": "^3.972.10", + "@aws-sdk/middleware-recursion-detection": "^3.972.11", + "@aws-sdk/middleware-user-agent": "^3.972.38", + "@aws-sdk/region-config-resolver": "^3.972.13", + "@aws-sdk/signature-v4-multi-region": "^3.996.25", + "@aws-sdk/types": "^3.973.8", + "@aws-sdk/util-endpoints": "^3.996.8", + "@aws-sdk/util-user-agent-browser": "^3.972.10", + "@aws-sdk/util-user-agent-node": "^3.973.24", + "@smithy/config-resolver": "^4.4.17", + "@smithy/core": "^3.23.17", + "@smithy/fetch-http-handler": "^5.3.17", + "@smithy/hash-node": "^4.2.14", + "@smithy/invalid-dependency": "^4.2.14", + "@smithy/middleware-content-length": "^4.2.14", + "@smithy/middleware-endpoint": "^4.4.32", + "@smithy/middleware-retry": "^4.5.7", + "@smithy/middleware-serde": "^4.2.20", + "@smithy/middleware-stack": "^4.2.14", + "@smithy/node-config-provider": "^4.3.14", + "@smithy/node-http-handler": "^4.6.1", + "@smithy/protocol-http": "^5.3.14", + "@smithy/smithy-client": "^4.12.13", + "@smithy/types": "^4.14.1", + "@smithy/url-parser": "^4.2.14", + "@smithy/util-base64": "^4.3.2", + "@smithy/util-body-length-browser": "^4.2.2", + "@smithy/util-body-length-node": "^4.2.3", + "@smithy/util-defaults-mode-browser": "^4.3.49", + "@smithy/util-defaults-mode-node": "^4.2.54", + "@smithy/util-endpoints": "^3.4.2", + "@smithy/util-middleware": "^4.2.14", + "@smithy/util-retry": "^4.3.6", + "@smithy/util-utf8": "^4.2.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/core": { + "version": "3.974.8", + "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.974.8.tgz", + "integrity": "sha512-njR2qoG6ZuB0kvAS2FyICsFZJ6gmCcf2X/7JcD14sUvGDm26wiZ5BrA6LOiUxKFEF+IVe7kdroxyE00YlkiYsw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.973.8", + "@aws-sdk/xml-builder": "^3.972.22", + "@smithy/core": "^3.23.17", + "@smithy/node-config-provider": "^4.3.14", + "@smithy/property-provider": "^4.2.14", + "@smithy/protocol-http": "^5.3.14", + "@smithy/signature-v4": "^5.3.14", + "@smithy/smithy-client": "^4.12.13", + "@smithy/types": "^4.14.1", + "@smithy/util-base64": "^4.3.2", + "@smithy/util-middleware": "^4.2.14", + "@smithy/util-retry": "^4.3.6", + "@smithy/util-utf8": "^4.2.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/crc64-nvme": { + "version": "3.972.7", + "resolved": "https://registry.npmjs.org/@aws-sdk/crc64-nvme/-/crc64-nvme-3.972.7.tgz", + "integrity": "sha512-QUagVVBbC8gODCF6e1aV0mE2TXWB9Opz4k8EJFdNrujUVQm5R4AjJa1mpOqzwOuROBzqJU9zawzig7M96L8Ejg==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-env": { + "version": "3.972.34", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.972.34.tgz", + "integrity": "sha512-XT0jtf8Fw9JE6ppsQeoNnZRiG+jqRixMT1v1ZR17G60UvVdsQmTG8nbEyHuEPfMxDXEhfdARaM/XiEhca4lGHQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.974.8", + "@aws-sdk/types": "^3.973.8", + "@smithy/property-provider": "^4.2.14", + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-http": { + "version": "3.972.36", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.972.36.tgz", + "integrity": "sha512-DPoGWfy7J7RKxvbf5kOKIGQkD2ek3dbKgzKIGrnLuvZBz5myU+Im/H6pmc14QcnFbqHMqxvtWSgRDSJW3qXLQg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.974.8", + "@aws-sdk/types": "^3.973.8", + "@smithy/fetch-http-handler": "^5.3.17", + "@smithy/node-http-handler": "^4.6.1", + "@smithy/property-provider": "^4.2.14", + "@smithy/protocol-http": "^5.3.14", + "@smithy/smithy-client": "^4.12.13", + "@smithy/types": "^4.14.1", + "@smithy/util-stream": "^4.5.25", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-ini": { + "version": "3.972.38", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.972.38.tgz", + "integrity": "sha512-oDzUBu2MGJFgoar05sPMCwSrhw44ASyccrHzj66vO69OZqi7I6hZZxXfuPLC8OCzW7C+sU+bI73XHij41yekgQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.974.8", + "@aws-sdk/credential-provider-env": "^3.972.34", + "@aws-sdk/credential-provider-http": "^3.972.36", + "@aws-sdk/credential-provider-login": "^3.972.38", + "@aws-sdk/credential-provider-process": "^3.972.34", + "@aws-sdk/credential-provider-sso": "^3.972.38", + "@aws-sdk/credential-provider-web-identity": "^3.972.38", + "@aws-sdk/nested-clients": "^3.997.6", + "@aws-sdk/types": "^3.973.8", + "@smithy/credential-provider-imds": "^4.2.14", + "@smithy/property-provider": "^4.2.14", + "@smithy/shared-ini-file-loader": "^4.4.9", + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-login": { + "version": "3.972.38", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-login/-/credential-provider-login-3.972.38.tgz", + "integrity": "sha512-g1NosS8qe4OF++G2UFCM5ovSkgipC7YYor5KCWatG0UoMSO5YFj9C8muePlyVmOBV/WTI16Jo3/s1NUo/o1Bww==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.974.8", + "@aws-sdk/nested-clients": "^3.997.6", + "@aws-sdk/types": "^3.973.8", + "@smithy/property-provider": "^4.2.14", + "@smithy/protocol-http": "^5.3.14", + "@smithy/shared-ini-file-loader": "^4.4.9", + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-node": { + "version": "3.972.39", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.972.39.tgz", + "integrity": "sha512-HEswDQyxUtadoZ/bJsPPENHg7R0Lzym5LuMksJeHvqhCOpP+rtkDLKI4/ZChH4w3cf5kG8n6bZuI8PzajoiqMg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/credential-provider-env": "^3.972.34", + "@aws-sdk/credential-provider-http": "^3.972.36", + "@aws-sdk/credential-provider-ini": "^3.972.38", + "@aws-sdk/credential-provider-process": "^3.972.34", + "@aws-sdk/credential-provider-sso": "^3.972.38", + "@aws-sdk/credential-provider-web-identity": "^3.972.38", + "@aws-sdk/types": "^3.973.8", + "@smithy/credential-provider-imds": "^4.2.14", + "@smithy/property-provider": "^4.2.14", + "@smithy/shared-ini-file-loader": "^4.4.9", + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-process": { + "version": "3.972.34", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.972.34.tgz", + "integrity": "sha512-T3IFs4EVmVi1dVN5RciFnklCANSzvrQd/VuHY9ThHSQmYkTogjcGkoJEr+oNUPQZnso52183088NqysMPji1/Q==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.974.8", + "@aws-sdk/types": "^3.973.8", + "@smithy/property-provider": "^4.2.14", + "@smithy/shared-ini-file-loader": "^4.4.9", + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-sso": { + "version": "3.972.38", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.972.38.tgz", + "integrity": "sha512-5ZxG+t0+3Q3QPh8KEjX6syskhgNf7I0MN7oGioTf6Lm1NTjfP7sIcYGNsthXC2qR8vcD3edNZwCr2ovfSSWuRA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.974.8", + "@aws-sdk/nested-clients": "^3.997.6", + "@aws-sdk/token-providers": "3.1041.0", + "@aws-sdk/types": "^3.973.8", + "@smithy/property-provider": "^4.2.14", + "@smithy/shared-ini-file-loader": "^4.4.9", + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-web-identity": { + "version": "3.972.38", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.972.38.tgz", + "integrity": "sha512-lYHFF30DGI20jZcYX8cm6Ns0V7f1dDN6g/MBDLTyD/5iw+bXs3yBr2iAiHDkx4RFU5JgsnZvCHYKiRVPRdmOgw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.974.8", + "@aws-sdk/nested-clients": "^3.997.6", + "@aws-sdk/types": "^3.973.8", + "@smithy/property-provider": "^4.2.14", + "@smithy/shared-ini-file-loader": "^4.4.9", + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/dynamodb-codec": { + "version": "3.973.8", + "resolved": "https://registry.npmjs.org/@aws-sdk/dynamodb-codec/-/dynamodb-codec-3.973.8.tgz", + "integrity": "sha512-dYQ/cQqHZd23hcl8oEGwPphTqyGnmvf2HrVmz4J90Q5Bv89oJjlwcBcifiiTvApqsVpx7Pr0IebMpkYwWJvZlQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.974.8", + "@smithy/core": "^3.23.17", + "@smithy/types": "^4.14.1", + "@smithy/util-base64": "^4.3.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/endpoint-cache": { + "version": "3.972.5", + "resolved": "https://registry.npmjs.org/@aws-sdk/endpoint-cache/-/endpoint-cache-3.972.5.tgz", + "integrity": "sha512-itVdge0NozgtgmtbZ25FVwWU3vGlE7x7feE/aOEJNkQfEpbkrF8Rj1QmnK+2blFfYE1xWt/iU+6/jUp/pv1+MA==", + "license": "Apache-2.0", + "dependencies": { + "mnemonist": "0.38.3", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/eventstream-handler-node": { + "version": "3.972.14", + "resolved": "https://registry.npmjs.org/@aws-sdk/eventstream-handler-node/-/eventstream-handler-node-3.972.14.tgz", + "integrity": "sha512-m4X56gxG76/CKfxNVbOFuYwnAZcHgS6HOH8lgp15HoGHIAVTcZfZrXvcYzJFOMLEJgVn+JHBu6EiNV+xSNXXFg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.973.8", + "@smithy/eventstream-codec": "^4.2.14", + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/lib-dynamodb": { + "version": "3.1041.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/lib-dynamodb/-/lib-dynamodb-3.1041.0.tgz", + "integrity": "sha512-AxiXvrAE4cfm/3KPV61OmfL4YuWP2Z9ZyxcC6OuVQW15vzGkxSE0om+lBtiooQ9Y6BjNOPHKWu1aUog7fa0B5Q==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.974.8", + "@aws-sdk/util-dynamodb": "^3.996.2", + "@smithy/core": "^3.23.17", + "@smithy/smithy-client": "^4.12.13", + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "@aws-sdk/client-dynamodb": "^3.1041.0" + } + }, + "node_modules/@aws-sdk/middleware-bucket-endpoint": { + "version": "3.972.10", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-bucket-endpoint/-/middleware-bucket-endpoint-3.972.10.tgz", + "integrity": "sha512-Vbc2frZH7wXlMNd+ZZSXUEs/l1Sv8Jj4zUnIfwrYF5lwaLdXHZ9xx4U3rjUcaye3HRhFVc+E5DbBxpRAbB16BA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.973.8", + "@aws-sdk/util-arn-parser": "^3.972.3", + "@smithy/node-config-provider": "^4.3.14", + "@smithy/protocol-http": "^5.3.14", + "@smithy/types": "^4.14.1", + "@smithy/util-config-provider": "^4.2.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/middleware-endpoint-discovery": { + "version": "3.972.11", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-endpoint-discovery/-/middleware-endpoint-discovery-3.972.11.tgz", + "integrity": "sha512-vXARCZVFQHdsd6qPPZyC/hh+5x2XsCYKqUQDCqnUlpGpChMpDojOOacQWdLJ+FFXKN8X3cmLOGrtgx/zysCKqQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/endpoint-cache": "^3.972.5", + "@aws-sdk/types": "^3.973.8", + "@smithy/node-config-provider": "^4.3.14", + "@smithy/protocol-http": "^5.3.14", + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/middleware-eventstream": { + "version": "3.972.10", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-eventstream/-/middleware-eventstream-3.972.10.tgz", + "integrity": "sha512-QUqLs7Af1II9X4fCRAu+EGHG3KHyOp4RkuLhRKoA3NuFlh6TL8i+zXBl8w2LUxqm44B/Kom45hgSlwA1SpTsXQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.973.8", + "@smithy/protocol-http": "^5.3.14", + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/middleware-expect-continue": { + "version": "3.972.10", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-expect-continue/-/middleware-expect-continue-3.972.10.tgz", + "integrity": "sha512-2Yn0f1Qiq/DjxYR3wfI3LokXnjOhFM7Ssn4LTdFDIxRMCE6I32MAsVnhPX1cUZsuVA9tiZtwwhlSLAtFGxAZlQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.973.8", + "@smithy/protocol-http": "^5.3.14", + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/middleware-flexible-checksums": { + "version": "3.974.16", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-flexible-checksums/-/middleware-flexible-checksums-3.974.16.tgz", + "integrity": "sha512-6ru8doI0/XzszqLIPXf0E/V7HhAw1Pu94010XCKYtBUfD0LxF0BuOzrUf8OQGR6j2o6wgKTHUniOmndQycHwCA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/crc32": "5.2.0", + "@aws-crypto/crc32c": "5.2.0", + "@aws-crypto/util": "5.2.0", + "@aws-sdk/core": "^3.974.8", + "@aws-sdk/crc64-nvme": "^3.972.7", + "@aws-sdk/types": "^3.973.8", + "@smithy/is-array-buffer": "^4.2.2", + "@smithy/node-config-provider": "^4.3.14", + "@smithy/protocol-http": "^5.3.14", + "@smithy/types": "^4.14.1", + "@smithy/util-middleware": "^4.2.14", + "@smithy/util-stream": "^4.5.25", + "@smithy/util-utf8": "^4.2.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/middleware-host-header": { + "version": "3.972.10", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-host-header/-/middleware-host-header-3.972.10.tgz", + "integrity": "sha512-IJSsIMeVQ8MMCPbuh1AbltkFhLBLXn7aejzfX5YKT/VLDHn++Dcz8886tXckE+wQssyPUhaXrJhdakO2VilRhg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.973.8", + "@smithy/protocol-http": "^5.3.14", + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/middleware-location-constraint": { + "version": "3.972.10", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-location-constraint/-/middleware-location-constraint-3.972.10.tgz", + "integrity": "sha512-rI3NZvJcEvjoD0+0PI0iUAwlPw2IlSlhyvgBK/3WkKJQE/YiKFedd9dMN2lVacdNxPNhxL/jzQaKQdrGtQagjQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.973.8", + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/middleware-logger": { + "version": "3.972.10", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-logger/-/middleware-logger-3.972.10.tgz", + "integrity": "sha512-OOuGvvz1Dm20SjZo5oEBePFqxt5nf8AwkNDSyUHvD9/bfNASmstcYxFAHUowy4n6Io7mWUZ04JURZwSBvyQanQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.973.8", + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/middleware-recursion-detection": { + "version": "3.972.11", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-recursion-detection/-/middleware-recursion-detection-3.972.11.tgz", + "integrity": "sha512-+zz6f79Kj9V5qFK2P+D8Ehjnw4AhphAlCAsPjUqEcInA9umtSSKMrHbSagEeOIsDNuvVrH98bjRHcyQukTrhaQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.973.8", + "@aws/lambda-invoke-store": "^0.2.2", + "@smithy/protocol-http": "^5.3.14", + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/middleware-sdk-s3": { + "version": "3.972.37", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-sdk-s3/-/middleware-sdk-s3-3.972.37.tgz", + "integrity": "sha512-Km7M+i8DrLArVzrid1gfxeGhYHBd3uxvE77g0s5a52zPSVosxzQBnJ0gwWb6NIp/DOk8gsBMhi7V+cpJG0ndTA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.974.8", + "@aws-sdk/types": "^3.973.8", + "@aws-sdk/util-arn-parser": "^3.972.3", + "@smithy/core": "^3.23.17", + "@smithy/node-config-provider": "^4.3.14", + "@smithy/protocol-http": "^5.3.14", + "@smithy/signature-v4": "^5.3.14", + "@smithy/smithy-client": "^4.12.13", + "@smithy/types": "^4.14.1", + "@smithy/util-config-provider": "^4.2.2", + "@smithy/util-middleware": "^4.2.14", + "@smithy/util-stream": "^4.5.25", + "@smithy/util-utf8": "^4.2.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/middleware-ssec": { + "version": "3.972.10", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-ssec/-/middleware-ssec-3.972.10.tgz", + "integrity": "sha512-Gli9A0u8EVVb+5bFDGS/QbSVg28w/wpEidg1ggVcSj65BDTdGR6punsOcVjqdiu1i42WHWo51MCvARPIIz9juw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.973.8", + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/middleware-user-agent": { + "version": "3.972.38", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.972.38.tgz", + "integrity": "sha512-iz+B29TXcAZsJpwB+AwG/TTGA5l/VnmMZ2UxtiySOZjI6gCdmviXPwdgzcmuazMy16rXoPY4mYCGe7zdNKfx5A==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.974.8", + "@aws-sdk/types": "^3.973.8", + "@aws-sdk/util-endpoints": "^3.996.8", + "@smithy/core": "^3.23.17", + "@smithy/protocol-http": "^5.3.14", + "@smithy/types": "^4.14.1", + "@smithy/util-retry": "^4.3.6", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/middleware-websocket": { + "version": "3.972.16", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-websocket/-/middleware-websocket-3.972.16.tgz", + "integrity": "sha512-86+S9oCyRVGzoMRpQhxkArp7kD2K75GPmaNevd9B6EyNhWoNvnCZZ3WbgN4j7ZT+jvtvBCGZvI2XHsWZJ+BRIg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.973.8", + "@aws-sdk/util-format-url": "^3.972.10", + "@smithy/eventstream-codec": "^4.2.14", + "@smithy/eventstream-serde-browser": "^4.2.14", + "@smithy/fetch-http-handler": "^5.3.17", + "@smithy/protocol-http": "^5.3.14", + "@smithy/signature-v4": "^5.3.14", + "@smithy/types": "^4.14.1", + "@smithy/util-base64": "^4.3.2", + "@smithy/util-hex-encoding": "^4.2.2", + "@smithy/util-utf8": "^4.2.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@aws-sdk/nested-clients": { + "version": "3.997.6", + "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.997.6.tgz", + "integrity": "sha512-WBDnqatJl+kGObpfmfSxqnXeYTu3Me8wx8WCtvoxX3pfWrrTv8I4WTMSSs7PZqcRcVh8WeUKMgGFjMG+52SR1w==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "^3.974.8", + "@aws-sdk/middleware-host-header": "^3.972.10", + "@aws-sdk/middleware-logger": "^3.972.10", + "@aws-sdk/middleware-recursion-detection": "^3.972.11", + "@aws-sdk/middleware-user-agent": "^3.972.38", + "@aws-sdk/region-config-resolver": "^3.972.13", + "@aws-sdk/signature-v4-multi-region": "^3.996.25", + "@aws-sdk/types": "^3.973.8", + "@aws-sdk/util-endpoints": "^3.996.8", + "@aws-sdk/util-user-agent-browser": "^3.972.10", + "@aws-sdk/util-user-agent-node": "^3.973.24", + "@smithy/config-resolver": "^4.4.17", + "@smithy/core": "^3.23.17", + "@smithy/fetch-http-handler": "^5.3.17", + "@smithy/hash-node": "^4.2.14", + "@smithy/invalid-dependency": "^4.2.14", + "@smithy/middleware-content-length": "^4.2.14", + "@smithy/middleware-endpoint": "^4.4.32", + "@smithy/middleware-retry": "^4.5.7", + "@smithy/middleware-serde": "^4.2.20", + "@smithy/middleware-stack": "^4.2.14", + "@smithy/node-config-provider": "^4.3.14", + "@smithy/node-http-handler": "^4.6.1", + "@smithy/protocol-http": "^5.3.14", + "@smithy/smithy-client": "^4.12.13", + "@smithy/types": "^4.14.1", + "@smithy/url-parser": "^4.2.14", + "@smithy/util-base64": "^4.3.2", + "@smithy/util-body-length-browser": "^4.2.2", + "@smithy/util-body-length-node": "^4.2.3", + "@smithy/util-defaults-mode-browser": "^4.3.49", + "@smithy/util-defaults-mode-node": "^4.2.54", + "@smithy/util-endpoints": "^3.4.2", + "@smithy/util-middleware": "^4.2.14", + "@smithy/util-retry": "^4.3.6", + "@smithy/util-utf8": "^4.2.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/region-config-resolver": { + "version": "3.972.13", + "resolved": "https://registry.npmjs.org/@aws-sdk/region-config-resolver/-/region-config-resolver-3.972.13.tgz", + "integrity": "sha512-CvJ2ZIjK/jVD/lbOpowBVElJyC1YxLTIJ13yM0AEo0t2v7swOzGjSA6lJGH+DwZXQhcjUjoYwc8bVYCX5MDr1A==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.973.8", + "@smithy/config-resolver": "^4.4.17", + "@smithy/node-config-provider": "^4.3.14", + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/s3-request-presigner": { + "version": "3.1041.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/s3-request-presigner/-/s3-request-presigner-3.1041.0.tgz", + "integrity": "sha512-DlKsPQ8Z75wgeDSHbjUPNDQCYUF0OLBkqllZqFei61KIoQDqEeKUCwuCf6RhNLjaP4b8oSpBA9+FmUS+zm3xUg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/signature-v4-multi-region": "^3.996.25", + "@aws-sdk/types": "^3.973.8", + "@aws-sdk/util-format-url": "^3.972.10", + "@smithy/middleware-endpoint": "^4.4.32", + "@smithy/protocol-http": "^5.3.14", + "@smithy/smithy-client": "^4.12.13", + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/signature-v4-multi-region": { + "version": "3.996.25", + "resolved": "https://registry.npmjs.org/@aws-sdk/signature-v4-multi-region/-/signature-v4-multi-region-3.996.25.tgz", + "integrity": "sha512-+CMIt3e1VzlklAECmG+DtP1sV8iKq25FuA0OKpnJ4KA0kxUtd7CgClY7/RU6VzJBQwbN4EJ9Ue6plvqx1qGadw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/middleware-sdk-s3": "^3.972.37", + "@aws-sdk/types": "^3.973.8", + "@smithy/protocol-http": "^5.3.14", + "@smithy/signature-v4": "^5.3.14", + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/token-providers": { + "version": "3.1041.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.1041.0.tgz", + "integrity": "sha512-Th7kPI6YPtvJUcdznooXJMy+9rQWjmEF81LxaJssngBzuysK4a/x+l8kjm1zb7nYsUPbndnBdUnwng/3PLvtGw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.974.8", + "@aws-sdk/nested-clients": "^3.997.6", + "@aws-sdk/types": "^3.973.8", + "@smithy/property-provider": "^4.2.14", + "@smithy/shared-ini-file-loader": "^4.4.9", + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/types": { + "version": "3.973.8", + "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.973.8.tgz", + "integrity": "sha512-gjlAdtHMbtR9X5iIhVUvbVcy55KnznpC6bkDUWW9z915bi0ckdUr5cjf16Kp6xq0bP5HBD2xzgbL9F9Quv5vUw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/util-arn-parser": { + "version": "3.972.3", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-arn-parser/-/util-arn-parser-3.972.3.tgz", + "integrity": "sha512-HzSD8PMFrvgi2Kserxuff5VitNq2sgf3w9qxmskKDiDTThWfVteJxuCS9JXiPIPtmCrp+7N9asfIaVhBFORllA==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/util-dynamodb": { + "version": "3.996.2", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-dynamodb/-/util-dynamodb-3.996.2.tgz", + "integrity": "sha512-ddpwaZmjBzcApYN7lgtAXjk+u+GO8fiPsxzuc59UqP+zqdxI1gsenPvkyiHiF9LnYnyRGijz6oN2JylnN561qQ==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "@aws-sdk/client-dynamodb": "^3.1003.0" + } + }, + "node_modules/@aws-sdk/util-endpoints": { + "version": "3.996.8", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.996.8.tgz", + "integrity": "sha512-oOZHcRDihk5iEe5V25NVWg45b3qEA8OpHWVdU/XQh8Zj4heVPAJqWvMphQnU7LkufmUo10EpvFPZuQMiFLJK3g==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.973.8", + "@smithy/types": "^4.14.1", + "@smithy/url-parser": "^4.2.14", + "@smithy/util-endpoints": "^3.4.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/util-format-url": { + "version": "3.972.10", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-format-url/-/util-format-url-3.972.10.tgz", + "integrity": "sha512-DEKiHNJVtNxdyTeQspzY+15Po/kHm6sF0Cs4HV9Q2+lplB63+DrvdeiSoOSdWEWAoO2RcY1veoXVDz2tWxWCgQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.973.8", + "@smithy/querystring-builder": "^4.2.14", + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/util-locate-window": { + "version": "3.965.5", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-locate-window/-/util-locate-window-3.965.5.tgz", + "integrity": "sha512-WhlJNNINQB+9qtLtZJcpQdgZw3SCDCpXdUJP7cToGwHbCWCnRckGlc6Bx/OhWwIYFNAn+FIydY8SZ0QmVu3xTQ==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/util-user-agent-browser": { + "version": "3.972.10", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-browser/-/util-user-agent-browser-3.972.10.tgz", + "integrity": "sha512-FAzqXvfEssGdSIz8ejatan0bOdx1qefBWKF/gWmVBXIP1HkS7v/wjjaqrAGGKvyihrXTXW00/2/1nTJtxpXz7g==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.973.8", + "@smithy/types": "^4.14.1", + "bowser": "^2.11.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-sdk/util-user-agent-node": { + "version": "3.973.24", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.973.24.tgz", + "integrity": "sha512-ZWwlkjcIp7cEL8ZfTpTAPNkwx25p7xol0xlKoWVVf22+nsjwmLcHYtTPjIV1cSpmB/b6DaK4cb1fSkvCXHgRdw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/middleware-user-agent": "^3.972.38", + "@aws-sdk/types": "^3.973.8", + "@smithy/node-config-provider": "^4.3.14", + "@smithy/types": "^4.14.1", + "@smithy/util-config-provider": "^4.2.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "aws-crt": ">=1.0.0" + }, + "peerDependenciesMeta": { + "aws-crt": { + "optional": true + } + } + }, + "node_modules/@aws-sdk/xml-builder": { + "version": "3.972.22", + "resolved": "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.972.22.tgz", + "integrity": "sha512-PMYKKtJd70IsSG0yHrdAbxBr+ZWBKLvzFZfD3/urxgf6hXVMzuU5M+3MJ5G67RpOmLBu1fAUN65SbWuKUCOlAA==", + "license": "Apache-2.0", + "dependencies": { + "@nodable/entities": "2.1.0", + "@smithy/types": "^4.14.1", + "fast-xml-parser": "5.7.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws/lambda-invoke-store": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/@aws/lambda-invoke-store/-/lambda-invoke-store-0.2.4.tgz", + "integrity": "sha512-iY8yvjE0y651BixKNPgmv1WrQc+GZ142sb0z4gYnChDDY2YqI4P/jsSopBWrKfAt7LOJAkOXt7rC/hms+WclQQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.3", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.3.tgz", + "integrity": "sha512-b3ctpQwp+PROvU/cttc4OYl4MzfJUWy6FZg+PMXfzmt/+39iHVF0sDfqay8TQM3JA2EUOyKcFZt75jWriQijsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.0" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", + "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@bcoe/v8-coverage": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", + "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.24.2.tgz", + "integrity": "sha512-thpVCb/rhxE/BnMLQ7GReQLLN8q9qbHmI55F4489/ByVg2aQaQ6kbcLb6FHkocZzQhxc4gx0sCk0tJkKBFzDhA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.24.2.tgz", + "integrity": "sha512-tmwl4hJkCfNHwFB3nBa8z1Uy3ypZpxqxfTQOcHX+xRByyYgunVbZ9MzUUfb0RxaHIMnbHagwAxuTL+tnNM+1/Q==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.24.2.tgz", + "integrity": "sha512-cNLgeqCqV8WxfcTIOeL4OAtSmL8JjcN6m09XIgro1Wi7cF4t/THaWEa7eL5CMoMBdjoHOTh/vwTO/o2TRXIyzg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.24.2.tgz", + "integrity": "sha512-B6Q0YQDqMx9D7rvIcsXfmJfvUYLoP722bgfBlO5cGvNVb5V/+Y7nhBE3mHV9OpxBf4eAS2S68KZztiPaWq4XYw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.24.2.tgz", + "integrity": "sha512-kj3AnYWc+CekmZnS5IPu9D+HWtUI49hbnyqk0FLEJDbzCIQt7hg7ucF1SQAilhtYpIujfaHr6O0UHlzzSPdOeA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.24.2.tgz", + "integrity": "sha512-WeSrmwwHaPkNR5H3yYfowhZcbriGqooyu3zI/3GGpF8AyUdsrrP0X6KumITGA9WOyiJavnGZUwPGvxvwfWPHIA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.24.2.tgz", + "integrity": "sha512-UN8HXjtJ0k/Mj6a9+5u6+2eZ2ERD7Edt1Q9IZiB5UZAIdPnVKDoG7mdTVGhHJIeEml60JteamR3qhsr1r8gXvg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.24.2.tgz", + "integrity": "sha512-TvW7wE/89PYW+IevEJXZ5sF6gJRDY/14hyIGFXdIucxCsbRmLUcjseQu1SyTko+2idmCw94TgyaEZi9HUSOe3Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.24.2.tgz", + "integrity": "sha512-n0WRM/gWIdU29J57hJyUdIsk0WarGd6To0s+Y+LwvlC55wt+GT/OgkwoXCXvIue1i1sSNWblHEig00GBWiJgfA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.24.2.tgz", + "integrity": "sha512-7HnAD6074BW43YvvUmE/35Id9/NB7BeX5EoNkK9obndmZBUk8xmJJeU7DwmUeN7tkysslb2eSl6CTrYz6oEMQg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.24.2.tgz", + "integrity": "sha512-sfv0tGPQhcZOgTKO3oBE9xpHuUqguHvSo4jl+wjnKwFpapx+vUDcawbwPNuBIAYdRAvIDBfZVvXprIj3HA+Ugw==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.24.2.tgz", + "integrity": "sha512-CN9AZr8kEndGooS35ntToZLTQLHEjtVB5n7dl8ZcTZMonJ7CCfStrYhrzF97eAecqVbVJ7APOEe18RPI4KLhwQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.24.2.tgz", + "integrity": "sha512-iMkk7qr/wl3exJATwkISxI7kTcmHKE+BlymIAbHO8xanq/TjHaaVThFF6ipWzPHryoFsesNQJPE/3wFJw4+huw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.24.2.tgz", + "integrity": "sha512-shsVrgCZ57Vr2L8mm39kO5PPIb+843FStGt7sGGoqiiWYconSxwTiuswC1VJZLCjNiMLAMh34jg4VSEQb+iEbw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.24.2.tgz", + "integrity": "sha512-4eSFWnU9Hhd68fW16GD0TINewo1L6dRrB+oLNNbYyMUAeOD2yCK5KXGK1GH4qD/kT+bTEXjsyTCiJGHPZ3eM9Q==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.24.2.tgz", + "integrity": "sha512-S0Bh0A53b0YHL2XEXC20bHLuGMOhFDO6GN4b3YjRLK//Ep3ql3erpNcPlEFed93hsQAjAQDNsvcK+hV90FubSw==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.24.2.tgz", + "integrity": "sha512-8Qi4nQcCTbLnK9WoMjdC9NiTG6/E38RNICU6sUNqK0QFxCYgoARqVqxdFmWkdonVsvGqWhmm7MO0jyTqLqwj0Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.24.2.tgz", + "integrity": "sha512-wuLK/VztRRpMt9zyHSazyCVdCXlpHkKm34WUyinD2lzK07FAHTq0KQvZZlXikNWkDGoT6x3TD51jKQ7gMVpopw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.24.2.tgz", + "integrity": "sha512-VefFaQUc4FMmJuAxmIHgUmfNiLXY438XrL4GDNV1Y1H/RW3qow68xTwjZKfj/+Plp9NANmzbH5R40Meudu8mmw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.24.2.tgz", + "integrity": "sha512-YQbi46SBct6iKnszhSvdluqDmxCJA+Pu280Av9WICNwQmMxV7nLRHZfjQzwbPs3jeWnuAhE9Jy0NrnJ12Oz+0A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.24.2.tgz", + "integrity": "sha512-+iDS6zpNM6EnJyWv0bMGLWSWeXGN/HTaF/LXHXHwejGsVi+ooqDfMCCTerNFxEkM3wYVcExkeGXNqshc9iMaOA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.7.tgz", + "integrity": "sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.24.2.tgz", + "integrity": "sha512-hTdsW27jcktEvpwNHJU4ZwWFGkz2zRJUz8pvddmXPtXDzVKTTINmlmga3ZzwcuMpUvLw7JkLy9QLKyGpD2Yxig==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.24.2.tgz", + "integrity": "sha512-LihEQ2BBKVFLOC9ZItT9iFprsE9tqjDjnbulhHoFxYQtQfai7qfluVODIYxt1PgdoyQkz23+01rzwNwYfutxUQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.24.2.tgz", + "integrity": "sha512-q+iGUwfs8tncmFC9pcnD5IvRHAzmbwQ3GPS5/ceCyHdjXubwQWI12MKWSNSMYLJMq23/IUCvJMS76PDqXe1fxA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.24.2.tgz", + "integrity": "sha512-7VTgWzgMGvup6aSqDPLiW5zHaxYJGTO4OokMjIlrCtf+VpEL+cXKtCvg723iguPYI5oaUNdS+/V7OU2gvXVWEg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@hono/zod-openapi": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@hono/zod-openapi/-/zod-openapi-1.3.0.tgz", + "integrity": "sha512-loDVevfMaaNa0slskhpMcqjSdidVXba2QJwNVmnS5Dp6L8AqSgtjJxWGJfRZtosyzYOb5gx4ZzXNCe+QhwY7xw==", + "license": "MIT", + "dependencies": { + "@asteasolutions/zod-to-openapi": "^8.5.0", + "@hono/zod-validator": "^0.7.6", + "openapi3-ts": "^4.5.0" + }, + "engines": { + "node": ">=16.0.0" + }, + "peerDependencies": { + "hono": ">=4.3.6", + "zod": "^4.0.0" + } + }, + "node_modules/@hono/zod-validator": { + "version": "0.7.6", + "resolved": "https://registry.npmjs.org/@hono/zod-validator/-/zod-validator-0.7.6.tgz", + "integrity": "sha512-Io1B6d011Gj1KknV4rXYz4le5+5EubcWEU/speUjuw9XMMIaP3n78yXLhjd2A3PXaXaUwEAluOiAyLqhBEJgsw==", + "license": "MIT", + "peerDependencies": { + "hono": ">=3.9.0", + "zod": "^3.25.0 || ^4.0.0" + } + }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@istanbuljs/schema": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.6.tgz", + "integrity": "sha512-+Sg6GCR/wy1oSmQDFq4LQDAhm3ETKnorxN+y5nbLULOR3P0c14f2Wurzj3/xqPXtasLFfHd5iRFQ7AJt4KH2cw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@jitl/quickjs-ffi-types": { + "version": "0.32.0", + "resolved": "https://registry.npmjs.org/@jitl/quickjs-ffi-types/-/quickjs-ffi-types-0.32.0.tgz", + "integrity": "sha512-v9T+GQpmk43VDJ7d72sf0Nexhk+ArvtUihW27dy7lqAl0zBObFKtSBBIm5RBjwIhE8VwsPPm9PNuvPvNqLWUEg==", + "license": "MIT" + }, + "node_modules/@jitl/quickjs-wasmfile-debug-asyncify": { + "version": "0.32.0", + "resolved": "https://registry.npmjs.org/@jitl/quickjs-wasmfile-debug-asyncify/-/quickjs-wasmfile-debug-asyncify-0.32.0.tgz", + "integrity": "sha512-EX8zbXwGqCgAE764M+qvkHtyXDi/FUoMBea0JnES7vCM3P7a2+EOZOjGv85wtZ2sJhI1oJ+nekmqpOODFDY+hw==", + "license": "MIT", + "dependencies": { + "@jitl/quickjs-ffi-types": "0.32.0" + } + }, + "node_modules/@jitl/quickjs-wasmfile-debug-sync": { + "version": "0.32.0", + "resolved": "https://registry.npmjs.org/@jitl/quickjs-wasmfile-debug-sync/-/quickjs-wasmfile-debug-sync-0.32.0.tgz", + "integrity": "sha512-LeYWrPGC1uNCTBWvibo3ZLJj0CSVNYUXvJpXMCmuQ5Sap2cCACc3uvGvYV4homHHBAzfw5akoTqMMS4YFRtw+Q==", + "license": "MIT", + "dependencies": { + "@jitl/quickjs-ffi-types": "0.32.0" + } + }, + "node_modules/@jitl/quickjs-wasmfile-release-asyncify": { + "version": "0.32.0", + "resolved": "https://registry.npmjs.org/@jitl/quickjs-wasmfile-release-asyncify/-/quickjs-wasmfile-release-asyncify-0.32.0.tgz", + "integrity": "sha512-3oSwPfja12ICz4aIblB58cuY8JlEq5Txt8Cut4VLo+LH47QN+mzCnSgnbB03hWzg1LBcc+VyyI9UOag7a1NF+Q==", + "license": "MIT", + "dependencies": { + "@jitl/quickjs-ffi-types": "0.32.0" + } + }, + "node_modules/@jitl/quickjs-wasmfile-release-sync": { + "version": "0.32.0", + "resolved": "https://registry.npmjs.org/@jitl/quickjs-wasmfile-release-sync/-/quickjs-wasmfile-release-sync-0.32.0.tgz", + "integrity": "sha512-BKNDI/TPBfGlLNGYpLrhcDGXmIk4xHm4MRAisOBnOzpXVn9HZWsfmMAc9WMBrAHjvvds6HOikKeaOBKdPdpVrg==", + "license": "MIT", + "dependencies": { + "@jitl/quickjs-ffi-types": "0.32.0" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@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/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.3.tgz", + "integrity": "sha512-x35CNW/ANXG3hE/EZpRU8MXX1JDN86hBb2wMGAtltkz7pc6cxgjpy1OMMfDosOQ+2hWqIkag/fGok1Yady9nGw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.60.3.tgz", + "integrity": "sha512-xw3xtkDApIOGayehp2+Rz4zimfkaX65r4t47iy+ymQB2G4iJCBBfj0ogVg5jpvjpn8UWn/+q9tprxleYeNp3Hw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.60.3.tgz", + "integrity": "sha512-vo6Y5Qfpx7/5EaamIwi0WqW2+zfiusVihKatLvtN1VFVy3D13uERk/6gZLU1UiHRL6fDXqj/ELIeVRGnvcTE1g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.60.3.tgz", + "integrity": "sha512-D+0QGcZhBzTN82weOnsSlY7V7+RMmPuF1CkbxyMAGE8+ZHeUjyb76ZiWmBlCu//AQQONvxcqRbwZTajZKqjuOw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.60.3.tgz", + "integrity": "sha512-6HnvHCT7fDyj6R0Ph7A6x8dQS/S38MClRWeDLqc0MdfWkxjiu1HSDYrdPhqSILzjTIC/pnXbbJbo+ft+gy/9hQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.60.3.tgz", + "integrity": "sha512-KHLgC3WKlUYW3ShFKnnosZDOJ0xjg9zp7au3sIm2bs/tGBeC2ipmvRh/N7JKi0t9Ue20C0dpEshi8WUubg+cnA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.60.3.tgz", + "integrity": "sha512-DV6fJoxEYWJOvaZIsok7KrYl0tPvga5OZ2yvKHNNYyk/2roMLqQAbGhr78EQ5YhHpnhLKJD3S1WFusAkmUuV5g==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.60.3.tgz", + "integrity": "sha512-mQKoJAzvuOs6F+TZybQO4GOTSMUu7v0WdxEk24krQ/uUxXoPTtHjuaUuPmFhtBcM4K0ons8nrE3JyhTuCFtT/w==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.60.3.tgz", + "integrity": "sha512-Whjj2qoiJ6+OOJMGptTYazaJvjOJm+iKHpXQM1P3LzGjt7Ff++Tp7nH4N8J/BUA7R9IHfDyx4DJIflifwnbmIA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.60.3.tgz", + "integrity": "sha512-4YTNHKqGng5+yiZt3mg77nmyuCfmNfX4fPmyUapBcIk+BdwSwmCWGXOUxhXbBEkFHtoN5boLj/5NON+u5QC9tg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.60.3.tgz", + "integrity": "sha512-SU3kNlhkpI4UqlUc2VXPGK9o886ZsSeGfMAX2ba2b8DKmMXq4AL7KUrkSWVbb7koVqx41Yczx6dx5PNargIrEA==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.60.3.tgz", + "integrity": "sha512-6lDLl5h4TXpB1mTf2rQWnAk/LcXrx9vBfu/DT5TIPhvMhRWaZ5MxkIc8u4lJAmBo6klTe1ywXIUHFjylW505sg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.60.3.tgz", + "integrity": "sha512-BMo8bOw8evlup/8G+cj5xWtPyp93xPdyoSN16Zy90Q2QZ0ZYRhCt6ZJSwbrRzG9HApFabjwj2p25TUPDWrhzqQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.60.3.tgz", + "integrity": "sha512-E0L8X1dZN1/Rph+5VPF6Xj2G7JJvMACVXtamTJIDrVI44Y3K+G8gQaMEAavbqCGTa16InptiVrX6eM6pmJ+7qA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.60.3.tgz", + "integrity": "sha512-oZJ/WHaVfHUiRAtmTAeo3DcevNsVvH8mbvodjZy7D5QKvCefO371SiKRpxoDcCxB3PTRTLayWBkvmDQKTcX/sw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.60.3.tgz", + "integrity": "sha512-Dhbyh7j9FybM3YaTgaHmVALwA8AkUwTPccyCQ79TG9AJUsMQqgN1DDEZNr4+QUfwiWvLDumW5vdwzoeUF+TNxQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.60.3.tgz", + "integrity": "sha512-cJd1X5XhHHlltkaypz1UcWLA8AcoIi1aWhsvaWDskD1oz2eKCypnqvTQ8ykMNI0RSmm7NkTdSqSSD7zM0xa6Ig==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.60.3.tgz", + "integrity": "sha512-DAZDBHQfG2oQuhY7mc6I3/qB4LU2fQCjRvxbDwd/Jdvb9fypP4IJ4qmtu6lNjes6B531AI8cg1aKC2di97bUxA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.60.3.tgz", + "integrity": "sha512-cRxsE8c13mZOh3vP+wLDxpQBRrOHDIGOWyDL93Sy0Ga8y515fBcC2pjUfFwUe5T7tqvTvWbCpg1URM/AXdWIXA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.60.3.tgz", + "integrity": "sha512-QaWcIgRxqEdQdhJqW4DJctsH6HCmo5vHxY0krHSX4jMtOqfzC+dqDGuHM87bu4H8JBeibWx7jFz+h6/4C8wA5Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.60.3.tgz", + "integrity": "sha512-AaXwSvUi3QIPtroAUw1t5yHGIyqKEXwH54WUocFolZhpGDruJcs8c+xPNDRn4XiQsS7MEwnYsHW2l0MBLDMkWg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.60.3.tgz", + "integrity": "sha512-65LAKM/bAWDqKNEelHlcHvm2V+Vfb8C6INFxQXRHCvaVN1rJfwr4NvdP4FyzUaLqWfaCGaadf6UbTm8xJeYfEg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.60.3.tgz", + "integrity": "sha512-EEM2gyhBF5MFnI6vMKdX1LAosE627RGBzIoGMdLloPZkXrUN0Ckqgr2Qi8+J3zip/8NVVro3/FjB+tjhZUgUHA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.60.3.tgz", + "integrity": "sha512-E5Eb5H/DpxaoXH++Qkv28RcUJboMopmdDUALBczvHMf7hNIxaDZqwY5lK12UK1BHacSmvupoEWGu+n993Z0y1A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.60.3.tgz", + "integrity": "sha512-hPt/bgL5cE+Qp+/TPHBqptcAgPzgj46mPcg/16zNUmbQk0j+mOEQV/+Lqu8QRtDV3Ek95Q6FeFITpuhl6OTsAA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@selderee/plugin-htmlparser2": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@selderee/plugin-htmlparser2/-/plugin-htmlparser2-0.11.0.tgz", + "integrity": "sha512-P33hHGdldxGabLFjPPpaTxVolMrzrcegejx+0GxjrIb9Zv48D8yAIA/QTDR2dFl7Uz7urX8aX6+5bCZslr+gWQ==", + "license": "MIT", + "dependencies": { + "domhandler": "^5.0.3", + "selderee": "^0.11.0" + }, + "funding": { + "url": "https://ko-fi.com/killymxi" + } + }, + "node_modules/@smithy/chunked-blob-reader": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/@smithy/chunked-blob-reader/-/chunked-blob-reader-5.2.2.tgz", + "integrity": "sha512-St+kVicSyayWQca+I1rGitaOEH6uKgE8IUWoYnnEX26SWdWQcL6LvMSD19Lg+vYHKdT9B2Zuu7rd3i6Wnyb/iw==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/chunked-blob-reader-native": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/@smithy/chunked-blob-reader-native/-/chunked-blob-reader-native-4.2.3.tgz", + "integrity": "sha512-jA5k5Udn7Y5717L86h4EIv06wIr3xn8GM1qHRi/Nf31annXcXHJjBKvgztnbn2TxH3xWrPBfgwHsOwZf0UmQWw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/util-base64": "^4.3.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/config-resolver": { + "version": "4.4.17", + "resolved": "https://registry.npmjs.org/@smithy/config-resolver/-/config-resolver-4.4.17.tgz", + "integrity": "sha512-TzDZcAnhTyAHbXVxWZo7/tEcrIeFq20IBk8So3OLOetWpR8EwY/yEqBMBFaJMeyEiREDq4NfEl+qO3OAUD+vbQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/node-config-provider": "^4.3.14", + "@smithy/types": "^4.14.1", + "@smithy/util-config-provider": "^4.2.2", + "@smithy/util-endpoints": "^3.4.2", + "@smithy/util-middleware": "^4.2.14", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/core": { + "version": "3.23.17", + "resolved": "https://registry.npmjs.org/@smithy/core/-/core-3.23.17.tgz", + "integrity": "sha512-x7BlLbUFL8NWCGjMF9C+1N5cVCxcPa7g6Tv9B4A2luWx3be3oU8hQ96wIwxe/s7OhIzvoJH73HAUSg5JXVlEtQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/protocol-http": "^5.3.14", + "@smithy/types": "^4.14.1", + "@smithy/url-parser": "^4.2.14", + "@smithy/util-base64": "^4.3.2", + "@smithy/util-body-length-browser": "^4.2.2", + "@smithy/util-middleware": "^4.2.14", + "@smithy/util-stream": "^4.5.25", + "@smithy/util-utf8": "^4.2.2", + "@smithy/uuid": "^1.1.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/credential-provider-imds": { + "version": "4.2.14", + "resolved": "https://registry.npmjs.org/@smithy/credential-provider-imds/-/credential-provider-imds-4.2.14.tgz", + "integrity": "sha512-Au28zBN48ZAoXdooGUHemuVBrkE+Ie6RPmGNIAJsFqj33Vhb6xAgRifUydZ2aY+M+KaMAETAlKk5NC5h1G7wpg==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/node-config-provider": "^4.3.14", + "@smithy/property-provider": "^4.2.14", + "@smithy/types": "^4.14.1", + "@smithy/url-parser": "^4.2.14", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/eventstream-codec": { + "version": "4.2.14", + "resolved": "https://registry.npmjs.org/@smithy/eventstream-codec/-/eventstream-codec-4.2.14.tgz", + "integrity": "sha512-erZq0nOIpzfeZdCyzZjdJb4nVSKLUmSkaQUVkRGQTXs30gyUGeKnrYEg+Xe1W5gE3aReS7IgsvANwVPxSzY6Pw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/crc32": "5.2.0", + "@smithy/types": "^4.14.1", + "@smithy/util-hex-encoding": "^4.2.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/eventstream-serde-browser": { + "version": "4.2.14", + "resolved": "https://registry.npmjs.org/@smithy/eventstream-serde-browser/-/eventstream-serde-browser-4.2.14.tgz", + "integrity": "sha512-8IelTCtTctWRbb+0Dcy+C0aICh1qa0qWXqgjcXDmMuCvPJRnv26hiDZoAau2ILOniki65mCPKqOQs/BaWvO4CQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/eventstream-serde-universal": "^4.2.14", + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/eventstream-serde-config-resolver": { + "version": "4.3.14", + "resolved": "https://registry.npmjs.org/@smithy/eventstream-serde-config-resolver/-/eventstream-serde-config-resolver-4.3.14.tgz", + "integrity": "sha512-sqHiHpYRYo3FJlaIxD1J8PhbcmJAm7IuM16mVnwSkCToD7g00IBZzKuiLNMGmftULmEUX6/UAz8/NN5uMP8bVA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/eventstream-serde-node": { + "version": "4.2.14", + "resolved": "https://registry.npmjs.org/@smithy/eventstream-serde-node/-/eventstream-serde-node-4.2.14.tgz", + "integrity": "sha512-Ht/8BuGlKfFTy0H3+8eEu0vdpwGztCnaLLXtpXNdQqiR7Hj4vFScU3T436vRAjATglOIPjJXronY+1WxxNLSiw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/eventstream-serde-universal": "^4.2.14", + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/eventstream-serde-universal": { + "version": "4.2.14", + "resolved": "https://registry.npmjs.org/@smithy/eventstream-serde-universal/-/eventstream-serde-universal-4.2.14.tgz", + "integrity": "sha512-lWyt4T2XQZUZgK3tQ3Wn0w3XBvZsK/vjTuJl6bXbnGZBHH0ZUSONTYiK9TgjTTzU54xQr3DRFwpjmhp0oLm3gg==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/eventstream-codec": "^4.2.14", + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/fetch-http-handler": { + "version": "5.3.17", + "resolved": "https://registry.npmjs.org/@smithy/fetch-http-handler/-/fetch-http-handler-5.3.17.tgz", + "integrity": "sha512-bXOvQzaSm6MnmLaWA1elgfQcAtN4UP3vXqV97bHuoOrHQOJiLT3ds6o9eo5bqd0TJfRFpzdGnDQdW3FACiAVdw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/protocol-http": "^5.3.14", + "@smithy/querystring-builder": "^4.2.14", + "@smithy/types": "^4.14.1", + "@smithy/util-base64": "^4.3.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/hash-blob-browser": { + "version": "4.2.15", + "resolved": "https://registry.npmjs.org/@smithy/hash-blob-browser/-/hash-blob-browser-4.2.15.tgz", + "integrity": "sha512-0PJ4Al3fg2nM4qKrAIxyNcApgqHAXcBkN8FeizOz69z0rb26uZ6lMESYtxegaTlXB5Hj84JfwMPavMrwDMjucA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/chunked-blob-reader": "^5.2.2", + "@smithy/chunked-blob-reader-native": "^4.2.3", + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/hash-node": { + "version": "4.2.14", + "resolved": "https://registry.npmjs.org/@smithy/hash-node/-/hash-node-4.2.14.tgz", + "integrity": "sha512-8ZBDY2DD4wr+GGjTpPtiglEsqr0lUP+KHqgZcWczFf6qeZ/YRjMIOoQWVQlmwu7EtxKTd8YXD8lblmYcpBIA1g==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.14.1", + "@smithy/util-buffer-from": "^4.2.2", + "@smithy/util-utf8": "^4.2.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/hash-stream-node": { + "version": "4.2.14", + "resolved": "https://registry.npmjs.org/@smithy/hash-stream-node/-/hash-stream-node-4.2.14.tgz", + "integrity": "sha512-tw4GANWkZPb6+BdD4Fgucqzey2+r73Z/GRo9zklsCdwrnxxumUV83ZIaBDdudV4Ylazw3EPTiJZhpX42105ruQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.14.1", + "@smithy/util-utf8": "^4.2.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/invalid-dependency": { + "version": "4.2.14", + "resolved": "https://registry.npmjs.org/@smithy/invalid-dependency/-/invalid-dependency-4.2.14.tgz", + "integrity": "sha512-c21qJiTSb25xvvOp+H2TNZzPCngrvl5vIPqPB8zQ/DmJF4QWXO19x1dWfMJZ6wZuuWUPPm0gV8C0cU3+ifcWuw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/is-array-buffer": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-4.2.2.tgz", + "integrity": "sha512-n6rQ4N8Jj4YTQO3YFrlgZuwKodf4zUFs7EJIWH86pSCWBaAtAGBFfCM7Wx6D2bBJ2xqFNxGBSrUWswT3M0VJow==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/md5-js": { + "version": "4.2.14", + "resolved": "https://registry.npmjs.org/@smithy/md5-js/-/md5-js-4.2.14.tgz", + "integrity": "sha512-V2v0vx+h0iUSNG1Alt+GNBMSLGCrl9iVsdd+Ap67HPM9PN479x12V8LkuMoKImNZxn3MXeuyUjls+/7ZACZghA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.14.1", + "@smithy/util-utf8": "^4.2.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/middleware-content-length": { + "version": "4.2.14", + "resolved": "https://registry.npmjs.org/@smithy/middleware-content-length/-/middleware-content-length-4.2.14.tgz", + "integrity": "sha512-xhHq7fX4/3lv5NHxLUk3OeEvl0xZ+Ek3qIbWaCL4f9JwgDZEclPBElljaZCAItdGPQl/kSM4LPMOpy1MYgprpw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/protocol-http": "^5.3.14", + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/middleware-endpoint": { + "version": "4.4.32", + "resolved": "https://registry.npmjs.org/@smithy/middleware-endpoint/-/middleware-endpoint-4.4.32.tgz", + "integrity": "sha512-ZZkgyjnJppiZbIm6Qbx92pbXYi1uzenIvGhBSCDlc7NwuAkiqSgS75j1czAD25ZLs2FjMjYy1q7gyRVWG6JA0Q==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/core": "^3.23.17", + "@smithy/middleware-serde": "^4.2.20", + "@smithy/node-config-provider": "^4.3.14", + "@smithy/shared-ini-file-loader": "^4.4.9", + "@smithy/types": "^4.14.1", + "@smithy/url-parser": "^4.2.14", + "@smithy/util-middleware": "^4.2.14", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/middleware-retry": { + "version": "4.5.7", + "resolved": "https://registry.npmjs.org/@smithy/middleware-retry/-/middleware-retry-4.5.7.tgz", + "integrity": "sha512-bRt6ZImqVSeTk39Nm81K20ObIiAZ3WefY7G6+iz/0tZjs4dgRRjvRX2sgsH+zi6iDCRR/aQvQofLKxxz4rPBZg==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/core": "^3.23.17", + "@smithy/node-config-provider": "^4.3.14", + "@smithy/protocol-http": "^5.3.14", + "@smithy/service-error-classification": "^4.3.1", + "@smithy/smithy-client": "^4.12.13", + "@smithy/types": "^4.14.1", + "@smithy/util-middleware": "^4.2.14", + "@smithy/util-retry": "^4.3.6", + "@smithy/uuid": "^1.1.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/middleware-serde": { + "version": "4.2.20", + "resolved": "https://registry.npmjs.org/@smithy/middleware-serde/-/middleware-serde-4.2.20.tgz", + "integrity": "sha512-Lx9JMO9vArPtiChE3wbEZ5akMIDQpWQtlu90lhACQmNOXcGXRbaDywMHDzuDZ2OkZzP+9wQfZi3YJT9F67zTQQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/core": "^3.23.17", + "@smithy/protocol-http": "^5.3.14", + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/middleware-stack": { + "version": "4.2.14", + "resolved": "https://registry.npmjs.org/@smithy/middleware-stack/-/middleware-stack-4.2.14.tgz", + "integrity": "sha512-2dvkUKLuFdKsCRmOE4Mn63co0Djtsm+JMh0bYZQupN1pJwMeE8FmQmRLLzzEMN0dnNi7CDCYYH8F0EVwWiPBeA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/node-config-provider": { + "version": "4.3.14", + "resolved": "https://registry.npmjs.org/@smithy/node-config-provider/-/node-config-provider-4.3.14.tgz", + "integrity": "sha512-S+gFjyo/weSVL0P1b9Ts8C/CwIfNCgUPikk3sl6QVsfE/uUuO+QsF+NsE/JkpvWqqyz1wg7HFdiaZuj5CoBMRg==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/property-provider": "^4.2.14", + "@smithy/shared-ini-file-loader": "^4.4.9", + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/node-http-handler": { + "version": "4.6.1", + "resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-4.6.1.tgz", + "integrity": "sha512-iB+orM4x3xrr57X3YaXazfKnntl0LHlZB1kcXSGzMV1Tt0+YwEjGlbjk/44qEGtBzXAz6yFDzkYTKSV6Pj2HUg==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/protocol-http": "^5.3.14", + "@smithy/querystring-builder": "^4.2.14", + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/property-provider": { + "version": "4.2.14", + "resolved": "https://registry.npmjs.org/@smithy/property-provider/-/property-provider-4.2.14.tgz", + "integrity": "sha512-WuM31CgfsnQ/10i7NYr0PyxqknD72Y5uMfUMVSniPjbEPceiTErb4eIqJQ+pdxNEAUEWrewrGjIRjVbVHsxZiQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/protocol-http": { + "version": "5.3.14", + "resolved": "https://registry.npmjs.org/@smithy/protocol-http/-/protocol-http-5.3.14.tgz", + "integrity": "sha512-dN5F8kHx8RNU0r+pCwNmFZyz6ChjMkzShy/zup6MtkRmmix4vZzJdW+di7x//b1LiynIev88FM18ie+wwPcQtQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/querystring-builder": { + "version": "4.2.14", + "resolved": "https://registry.npmjs.org/@smithy/querystring-builder/-/querystring-builder-4.2.14.tgz", + "integrity": "sha512-XYA5Z0IqTeF+5XDdh4BBmSA0HvbgVZIyv4cmOoUheDNR57K1HgBp9ukUMx3Cr3XpDHHpLBnexPE3LAtDsZkj2A==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.14.1", + "@smithy/util-uri-escape": "^4.2.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/querystring-parser": { + "version": "4.2.14", + "resolved": "https://registry.npmjs.org/@smithy/querystring-parser/-/querystring-parser-4.2.14.tgz", + "integrity": "sha512-hr+YyqBD23GVvRxGGrcc/oOeNlK3PzT5Fu4dzrDXxzS1LpFiuL2PQQqKPs87M79aW7ziMs+nvB3qdw77SqE7Lw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/service-error-classification": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@smithy/service-error-classification/-/service-error-classification-4.3.1.tgz", + "integrity": "sha512-aUQuDGh760ts/8MU+APjIZhlLPKhIIfqyzZaJikLEIMrdxFvxuLYD0WxWzaYWpmLbQlXDe9p7EWM3HsBe0K6Gw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.14.1" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/shared-ini-file-loader": { + "version": "4.4.9", + "resolved": "https://registry.npmjs.org/@smithy/shared-ini-file-loader/-/shared-ini-file-loader-4.4.9.tgz", + "integrity": "sha512-495/V2I15SHgedSJoDPD23JuSfKAp726ZI1V0wtjB07Wh7q/0tri/0e0DLefZCHgxZonrGKt/OCTpAtP1wE1kQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/signature-v4": { + "version": "5.3.14", + "resolved": "https://registry.npmjs.org/@smithy/signature-v4/-/signature-v4-5.3.14.tgz", + "integrity": "sha512-1D9Y/nmlVjCeSivCbhZ7hgEpmHyY1h0GvpSZt3l0xcD9JjmjVC1CHOozS6+Gh+/ldMH8JuJ6cujObQqfayAVFA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/is-array-buffer": "^4.2.2", + "@smithy/protocol-http": "^5.3.14", + "@smithy/types": "^4.14.1", + "@smithy/util-hex-encoding": "^4.2.2", + "@smithy/util-middleware": "^4.2.14", + "@smithy/util-uri-escape": "^4.2.2", + "@smithy/util-utf8": "^4.2.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/smithy-client": { + "version": "4.12.13", + "resolved": "https://registry.npmjs.org/@smithy/smithy-client/-/smithy-client-4.12.13.tgz", + "integrity": "sha512-y/Pcj1V9+qG98gyu1gvftHB7rDpdh+7kIBIggs55yGm3JdtBV8GT8IFF3a1qxZ79QnaJHX9GXzvBG6tAd+czJA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/core": "^3.23.17", + "@smithy/middleware-endpoint": "^4.4.32", + "@smithy/middleware-stack": "^4.2.14", + "@smithy/protocol-http": "^5.3.14", + "@smithy/types": "^4.14.1", + "@smithy/util-stream": "^4.5.25", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/types": { + "version": "4.14.1", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-4.14.1.tgz", + "integrity": "sha512-59b5HtSVrVR/eYNei3BUj3DCPKD/G7EtDDe7OEJE7i7FtQFugYo6MxbotS8mVJkLNVf8gYaAlEBwwtJ9HzhWSg==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/url-parser": { + "version": "4.2.14", + "resolved": "https://registry.npmjs.org/@smithy/url-parser/-/url-parser-4.2.14.tgz", + "integrity": "sha512-p06BiBigJ8bTA3MgnOfCtDUWnAMY0YfedO/GRpmc7p+wg3KW8vbXy1xwSu5ASy0wV7rRYtlfZOIKH4XqfhjSQQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/querystring-parser": "^4.2.14", + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-base64": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@smithy/util-base64/-/util-base64-4.3.2.tgz", + "integrity": "sha512-XRH6b0H/5A3SgblmMa5ErXQ2XKhfbQB+Fm/oyLZ2O2kCUrwgg55bU0RekmzAhuwOjA9qdN5VU2BprOvGGUkOOQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/util-buffer-from": "^4.2.2", + "@smithy/util-utf8": "^4.2.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-body-length-browser": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@smithy/util-body-length-browser/-/util-body-length-browser-4.2.2.tgz", + "integrity": "sha512-JKCrLNOup3OOgmzeaKQwi4ZCTWlYR5H4Gm1r2uTMVBXoemo1UEghk5vtMi1xSu2ymgKVGW631e2fp9/R610ZjQ==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-body-length-node": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/@smithy/util-body-length-node/-/util-body-length-node-4.2.3.tgz", + "integrity": "sha512-ZkJGvqBzMHVHE7r/hcuCxlTY8pQr1kMtdsVPs7ex4mMU+EAbcXppfo5NmyxMYi2XU49eqaz56j2gsk4dHHPG/g==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-buffer-from": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-4.2.2.tgz", + "integrity": "sha512-FDXD7cvUoFWwN6vtQfEta540Y/YBe5JneK3SoZg9bThSoOAC/eGeYEua6RkBgKjGa/sz6Y+DuBZj3+YEY21y4Q==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/is-array-buffer": "^4.2.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-config-provider": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@smithy/util-config-provider/-/util-config-provider-4.2.2.tgz", + "integrity": "sha512-dWU03V3XUprJwaUIFVv4iOnS1FC9HnMHDfUrlNDSh4315v0cWyaIErP8KiqGVbf5z+JupoVpNM7ZB3jFiTejvQ==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-defaults-mode-browser": { + "version": "4.3.49", + "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-browser/-/util-defaults-mode-browser-4.3.49.tgz", + "integrity": "sha512-a5bNrdiONYB/qE2BuKegvUMd/+ZDwdg4vsNuuSzYE8qs2EYAdK9CynL+Rzn29PbPiUqoz/cbpRbcLzD5lEevHw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/property-provider": "^4.2.14", + "@smithy/smithy-client": "^4.12.13", + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-defaults-mode-node": { + "version": "4.2.54", + "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-node/-/util-defaults-mode-node-4.2.54.tgz", + "integrity": "sha512-g1cvrJvOnzeJgEdf7AE4luI7gp6L8weE0y9a9wQUSGtjb8QRHDbCJYuE4Sy0SD9N8RrnNPFsPltAz/OSoBR9Zw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/config-resolver": "^4.4.17", + "@smithy/credential-provider-imds": "^4.2.14", + "@smithy/node-config-provider": "^4.3.14", + "@smithy/property-provider": "^4.2.14", + "@smithy/smithy-client": "^4.12.13", + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-endpoints": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/@smithy/util-endpoints/-/util-endpoints-3.4.2.tgz", + "integrity": "sha512-a55Tr+3OKld4TTtnT+RhKOQHyPxm3j/xL4OR83WBUhLJaKDS9dnJ7arRMOp3t31dcLhApwG9bgvrRXBHlLdIkg==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/node-config-provider": "^4.3.14", + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-hex-encoding": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@smithy/util-hex-encoding/-/util-hex-encoding-4.2.2.tgz", + "integrity": "sha512-Qcz3W5vuHK4sLQdyT93k/rfrUwdJ8/HZ+nMUOyGdpeGA1Wxt65zYwi3oEl9kOM+RswvYq90fzkNDahPS8K0OIg==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-middleware": { + "version": "4.2.14", + "resolved": "https://registry.npmjs.org/@smithy/util-middleware/-/util-middleware-4.2.14.tgz", + "integrity": "sha512-1Su2vj9RYNDEv/V+2E+jXkkwGsgR7dc4sfHn9Z7ruzQHJIEni9zzw5CauvRXlFJfmgcqYP8fWa0dkh2Q2YaQyw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-retry": { + "version": "4.3.8", + "resolved": "https://registry.npmjs.org/@smithy/util-retry/-/util-retry-4.3.8.tgz", + "integrity": "sha512-LUIxbTBi+OpvXpg91poGA6BdyoleMDLnfXjVDqyi2RvZmTveY5loE/FgYUBCR5LU2BThW2SoZRh8dTIIy38IPw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/service-error-classification": "^4.3.1", + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-stream": { + "version": "4.5.25", + "resolved": "https://registry.npmjs.org/@smithy/util-stream/-/util-stream-4.5.25.tgz", + "integrity": "sha512-/PFpG4k8Ze8Ei+mMKj3oiPICYekthuzePZMgZbCqMiXIHHf4n2aZ4Ps0aSRShycFTGuj/J6XldmC0x0DwednIA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/fetch-http-handler": "^5.3.17", + "@smithy/node-http-handler": "^4.6.1", + "@smithy/types": "^4.14.1", + "@smithy/util-base64": "^4.3.2", + "@smithy/util-buffer-from": "^4.2.2", + "@smithy/util-hex-encoding": "^4.2.2", + "@smithy/util-utf8": "^4.2.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-uri-escape": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@smithy/util-uri-escape/-/util-uri-escape-4.2.2.tgz", + "integrity": "sha512-2kAStBlvq+lTXHyAZYfJRb/DfS3rsinLiwb+69SstC9Vb0s9vNWkRwpnj918Pfi85mzi42sOqdV72OLxWAISnw==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-utf8": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-4.2.2.tgz", + "integrity": "sha512-75MeYpjdWRe8M5E3AW0O4Cx3UadweS+cwdXjwYGBW5h/gxxnbeZ877sLPX/ZJA9GVTlL/qG0dXP29JWFCD1Ayw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/util-buffer-from": "^4.2.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-waiter": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@smithy/util-waiter/-/util-waiter-4.3.0.tgz", + "integrity": "sha512-JyjYmLAfS+pdxF92o4yLgEoy0zhayKTw73FU1aofLWwLcJw7iSqIY2exGmMTrl/lmZugP5p/zxdFSippJDfKWA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/uuid": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@smithy/uuid/-/uuid-1.1.2.tgz", + "integrity": "sha512-O/IEdcCUKkubz60tFbGA7ceITTAJsty+lBjNoorP4Z6XRqaFb/OjQjZODophEcuq68nKm6/0r+6/lLQ+XVpk8g==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@types/aws-lambda": { + "version": "8.10.161", + "resolved": "https://registry.npmjs.org/@types/aws-lambda/-/aws-lambda-8.10.161.tgz", + "integrity": "sha512-rUYdp+MQwSFocxIOcSsYSF3YYYC/uUpMbCY/mbO21vGqfrEYvNSoPyKYDj6RhXXpPfS0KstW9RwG3qXh9sL7FQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/json-logic-js": { + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/@types/json-logic-js/-/json-logic-js-2.0.8.tgz", + "integrity": "sha512-WgNsDPuTPKYXl0Jh0IfoCoJoAGGYZt5qzpmjuLSEg7r0cKp/kWtWp0HAsVepyPSPyXiHo6uXp/B/kW/2J1fa2Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/mailparser": { + "version": "3.4.6", + "resolved": "https://registry.npmjs.org/@types/mailparser/-/mailparser-3.4.6.tgz", + "integrity": "sha512-wVV3cnIKzxTffaPH8iRnddX1zahbYB1ZEoAxyhoBo3TBCBuK6nZ8M8JYO/RhsCuuBVOw/DEN/t/ENbruwlxn6Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "iconv-lite": "^0.6.3" + } + }, + "node_modules/@types/node": { + "version": "22.19.17", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.17.tgz", + "integrity": "sha512-wGdMcf+vPYM6jikpS/qhg6WiqSV/OhG+jeeHT/KlVqxYfD40iYJf9/AE1uQxVWFvU7MipKRkRv8NSHiCGgPr8Q==", + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@types/web-push": { + "version": "3.6.4", + "resolved": "https://registry.npmjs.org/@types/web-push/-/web-push-3.6.4.tgz", + "integrity": "sha512-GnJmSr40H3RAnj0s34FNTcJi1hmWFV5KXugE0mYWnYhgTAHLJ/dJKAwDmvPJYMke0RplY2XE9LnM4hqSqKIjhQ==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@vitest/coverage-v8": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-2.1.9.tgz", + "integrity": "sha512-Z2cOr0ksM00MpEfyVE8KXIYPEcBFxdbLSs56L8PO0QQMxt/6bDj45uQfxoc96v05KW3clk7vvgP0qfDit9DmfQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@ampproject/remapping": "^2.3.0", + "@bcoe/v8-coverage": "^0.2.3", + "debug": "^4.3.7", + "istanbul-lib-coverage": "^3.2.2", + "istanbul-lib-report": "^3.0.1", + "istanbul-lib-source-maps": "^5.0.6", + "istanbul-reports": "^3.1.7", + "magic-string": "^0.30.12", + "magicast": "^0.3.5", + "std-env": "^3.8.0", + "test-exclude": "^7.0.1", + "tinyrainbow": "^1.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@vitest/browser": "2.1.9", + "vitest": "2.1.9" + }, + "peerDependenciesMeta": { + "@vitest/browser": { + "optional": true + } + } + }, + "node_modules/@vitest/expect": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-2.1.9.tgz", + "integrity": "sha512-UJCIkTBenHeKT1TTlKMJWy1laZewsRIzYighyYiJKZreqtdxSos/S1t+ktRMQWu2CKqaarrkeszJx1cgC5tGZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "2.1.9", + "@vitest/utils": "2.1.9", + "chai": "^5.1.2", + "tinyrainbow": "^1.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-2.1.9.tgz", + "integrity": "sha512-tVL6uJgoUdi6icpxmdrn5YNo3g3Dxv+IHJBr0GXHaEdTcw3F+cPKnsXFhli6nO+f/6SDKPHEK1UN+k+TQv0Ehg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "2.1.9", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.12" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^5.0.0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/pretty-format": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-2.1.9.tgz", + "integrity": "sha512-KhRIdGV2U9HOUzxfiHmY8IFHTdqtOhIzCpd8WRdJiE7D/HUcZVD0EgQCVjm+Q9gkUXWgBvMmTtZgIG48wq7sOQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^1.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-2.1.9.tgz", + "integrity": "sha512-ZXSSqTFIrzduD63btIfEyOmNcBmQvgOVsPNPe0jYtESiXkhd8u2erDLnMxmGrDCwHCCHE7hxwRDCT3pt0esT4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "2.1.9", + "pathe": "^1.1.2" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-2.1.9.tgz", + "integrity": "sha512-oBO82rEjsxLNJincVhLhaxxZdEtV0EFHMK5Kmx5sJ6H9L183dHECjiefOAdnqpIgT5eZwT04PoggUnW88vOBNQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "2.1.9", + "magic-string": "^0.30.12", + "pathe": "^1.1.2" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-2.1.9.tgz", + "integrity": "sha512-E1B35FwzXXTs9FHNK6bDszs7mtydNi5MIfUWpceJ8Xbfb1gBMscAnwLbEu+B44ed6W3XjL9/ehLPHR1fkf1KLQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyspy": "^3.0.2" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-2.1.9.tgz", + "integrity": "sha512-v0psaMSkNJ3A2NMrUEHFRzJtDPFn+/VWZ5WxImB21T9fjucJRmS7xCS3ppEnARb9y11OAzaD+P2Ps+b+BGX5iQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "2.1.9", + "loupe": "^3.1.2", + "tinyrainbow": "^1.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@zone-eu/mailsplit": { + "version": "5.4.8", + "resolved": "https://registry.npmjs.org/@zone-eu/mailsplit/-/mailsplit-5.4.8.tgz", + "integrity": "sha512-eEyACj4JZ7sjzRvy26QhLgKEMWwQbsw1+QZnlLX+/gihcNH07lVPOcnwf5U6UAL7gkc//J3jVd76o/WS+taUiA==", + "license": "(MIT OR EUPL-1.1+)", + "dependencies": { + "libbase64": "1.3.0", + "libmime": "5.3.7", + "libqp": "2.1.1" + } + }, + "node_modules/@zone-eu/mailsplit/node_modules/libmime": { + "version": "5.3.7", + "resolved": "https://registry.npmjs.org/libmime/-/libmime-5.3.7.tgz", + "integrity": "sha512-FlDb3Wtha8P01kTL3P9M+ZDNDWPKPmKHWaU/cG/lg5pfuAwdflVpZE+wm9m7pKmC5ww6s+zTxBKS1p6yl3KpSw==", + "license": "MIT", + "dependencies": { + "encoding-japanese": "2.2.0", + "iconv-lite": "0.6.3", + "libbase64": "1.3.0", + "libqp": "2.1.1" + } + }, + "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": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/asn1.js": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/asn1.js/-/asn1.js-5.4.1.tgz", + "integrity": "sha512-+I//4cYPccV8LdmBLiX8CYvf9Sp3vQsrqu2QNXRcrbiWvcx/UdlFiqUJJzxRQxgsZmvhXhn4cSKeSmoFjVdupA==", + "license": "MIT", + "dependencies": { + "bn.js": "^4.0.0", + "inherits": "^2.0.1", + "minimalistic-assert": "^1.0.0", + "safer-buffer": "^2.1.0" + } + }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/base64url": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/base64url/-/base64url-3.0.1.tgz", + "integrity": "sha512-ir1UPr3dkwexU7FdV8qBBbNDRUhMmIekYMFZfi+C/sLNnRESKPl23nB9b2pltqfOQNnGzsDdId90AEtG5tCx4A==", + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/bn.js": { + "version": "4.12.3", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.3.tgz", + "integrity": "sha512-fGTi3gxV/23FTYdAoUtLYp6qySe2KE3teyZitipKNRuVYcBkoP/bB3guXN/XVKUe9mxCHXnc9C4ocyz8OmgN0g==", + "license": "MIT" + }, + "node_modules/bowser": { + "version": "2.14.1", + "resolved": "https://registry.npmjs.org/bowser/-/bowser-2.14.1.tgz", + "integrity": "sha512-tzPjzCxygAKWFOJP011oxFHs57HzIhOEracIgAePE4pqB3LikALKnSzUyU4MGs9/iCEUuHlAJTjTc5M+u7YEGg==", + "license": "MIT" + }, + "node_modules/brace-expansion": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz", + "integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", + "license": "BSD-3-Clause" + }, + "node_modules/cac": { + "version": "6.7.14", + "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", + "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/chai": { + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/chai/-/chai-5.3.3.tgz", + "integrity": "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "assertion-error": "^2.0.1", + "check-error": "^2.1.1", + "deep-eql": "^5.0.1", + "loupe": "^3.1.0", + "pathval": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/check-error": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.3.tgz", + "integrity": "sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 16" + } + }, + "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==", + "dev": true, + "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==", + "dev": true, + "license": "MIT" + }, + "node_modules/commander": { + "version": "12.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz", + "integrity": "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "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/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-eql": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", + "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/deepmerge": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/dom-serializer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", + "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", + "license": "MIT", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.2", + "entities": "^4.2.0" + }, + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + } + }, + "node_modules/domelementtype": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", + "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "BSD-2-Clause" + }, + "node_modules/domhandler": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", + "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", + "license": "BSD-2-Clause", + "dependencies": { + "domelementtype": "^2.3.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/domutils": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz", + "integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==", + "license": "BSD-2-Clause", + "dependencies": { + "dom-serializer": "^2.0.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3" + }, + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" + } + }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "dev": true, + "license": "MIT" + }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, + "node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true, + "license": "MIT" + }, + "node_modules/encoding-japanese": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/encoding-japanese/-/encoding-japanese-2.2.0.tgz", + "integrity": "sha512-EuJWwlHPZ1LbADuKTClvHtwbaFn4rOD+dRAbWysqEOXRc2Uui0hJInNJrsdH0c+OhJA4nrCBdSkW4DD5YxAo6A==", + "license": "MIT", + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/es-module-lexer": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", + "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", + "dev": true, + "license": "MIT" + }, + "node_modules/esbuild": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.24.2.tgz", + "integrity": "sha512-+9egpBW8I3CD5XPe0n6BfT5fxLzxrlDzqydF3aviG+9ni1lDC/OvMHcxqEFV0+LANZG5R1bFMWfUrjVsdwxJvA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.24.2", + "@esbuild/android-arm": "0.24.2", + "@esbuild/android-arm64": "0.24.2", + "@esbuild/android-x64": "0.24.2", + "@esbuild/darwin-arm64": "0.24.2", + "@esbuild/darwin-x64": "0.24.2", + "@esbuild/freebsd-arm64": "0.24.2", + "@esbuild/freebsd-x64": "0.24.2", + "@esbuild/linux-arm": "0.24.2", + "@esbuild/linux-arm64": "0.24.2", + "@esbuild/linux-ia32": "0.24.2", + "@esbuild/linux-loong64": "0.24.2", + "@esbuild/linux-mips64el": "0.24.2", + "@esbuild/linux-ppc64": "0.24.2", + "@esbuild/linux-riscv64": "0.24.2", + "@esbuild/linux-s390x": "0.24.2", + "@esbuild/linux-x64": "0.24.2", + "@esbuild/netbsd-arm64": "0.24.2", + "@esbuild/netbsd-x64": "0.24.2", + "@esbuild/openbsd-arm64": "0.24.2", + "@esbuild/openbsd-x64": "0.24.2", + "@esbuild/sunos-x64": "0.24.2", + "@esbuild/win32-arm64": "0.24.2", + "@esbuild/win32-ia32": "0.24.2", + "@esbuild/win32-x64": "0.24.2" + } + }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/expect-type": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz", + "integrity": "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/fast-xml-builder": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/fast-xml-builder/-/fast-xml-builder-1.1.7.tgz", + "integrity": "sha512-Yh7/7rQuMXICNr0oMYDR2yHP6oUvmQsTToFeOWj/kIDhAwQ+c4Ol/lbcwOmEM5OHYQmh6S6EQSQ1sljCKP36bQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "dependencies": { + "path-expression-matcher": "^1.1.3" + } + }, + "node_modules/fast-xml-parser": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.7.2.tgz", + "integrity": "sha512-P7oW7tLbYnhOLQk/Gv7cZgzgMPP/XN03K02/Jy6Y/NHzyIAIpxuZIM/YqAkfiXFPxA2CTm7NtCijK9EDu09u2w==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "dependencies": { + "@nodable/entities": "^2.1.0", + "fast-xml-builder": "^1.1.5", + "path-expression-matcher": "^1.5.0", + "strnum": "^2.2.3" + }, + "bin": { + "fxparser": "src/cli/cli.js" + } + }, + "node_modules/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "dev": true, + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/get-tsconfig": { + "version": "4.14.0", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.14.0.tgz", + "integrity": "sha512-yTb+8DXzDREzgvYmh6s9vHsSVCHeC0G3PI5bEXNBHtmshPnO+S5O7qgLEOn0I5QvMy6kpZN8K1NKGyilLb93wA==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, + "node_modules/glob": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", + "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/glob/node_modules/brace-expansion": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.0.tgz", + "integrity": "sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/glob/node_modules/minimatch": { + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.2" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/he": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", + "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", + "license": "MIT", + "bin": { + "he": "bin/he" + } + }, + "node_modules/hono": { + "version": "4.12.16", + "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.16.tgz", + "integrity": "sha512-jN0ZewiNAWSe5khM3EyCmBb250+b40wWbwNILNfEvq84VREWwOIkuUsFONk/3i3nqkz7Oe1PcpM2mwQEK2L9Kg==", + "license": "MIT", + "engines": { + "node": ">=16.9.0" + } + }, + "node_modules/html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true, + "license": "MIT" + }, + "node_modules/html-to-text": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/html-to-text/-/html-to-text-9.0.5.tgz", + "integrity": "sha512-qY60FjREgVZL03vJU6IfMV4GDjGBIoOyvuFdpBDIX9yTlDw0TjxVBQp+P8NvpdIXNJvfWBTNul7fsAQJq2FNpg==", + "license": "MIT", + "dependencies": { + "@selderee/plugin-htmlparser2": "^0.11.0", + "deepmerge": "^4.3.1", + "dom-serializer": "^2.0.0", + "htmlparser2": "^8.0.2", + "selderee": "^0.11.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/htmlparser2": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-8.0.2.tgz", + "integrity": "sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==", + "funding": [ + "https://github.com/fb55/htmlparser2?sponsor=1", + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "MIT", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.0.1", + "entities": "^4.4.0" + } + }, + "node_modules/http_ece": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/http_ece/-/http_ece-1.2.0.tgz", + "integrity": "sha512-JrF8SSLVmcvc5NducxgyOrKXe3EsyHMgBFgSaIUGmArKe+rwr0uphRkRXvwiom3I+fpIfoItveHrfudL8/rxuA==", + "license": "MIT", + "engines": { + "node": ">=16" + } + }, + "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.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "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/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==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/istanbul-lib-coverage": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", + "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-report": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", + "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-source-maps": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-5.0.6.tgz", + "integrity": "sha512-yg2d+Em4KizZC5niWhQaIomgf5WlL4vOOjZ5xGCmF8SnPE/mDWWXgvRExdcpCgh9lLRRa1/fSYp2ymmbJ1pI+A==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.23", + "debug": "^4.1.1", + "istanbul-lib-coverage": "^3.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-reports": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.2.0.tgz", + "integrity": "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "node_modules/jose": { + "version": "4.15.9", + "resolved": "https://registry.npmjs.org/jose/-/jose-4.15.9.tgz", + "integrity": "sha512-1vUQX+IdDMVPj4k8kOxgUqlcK518yluMuGZwqlr44FS1ppZB/5GWh4rZG89erpOBOJjU/OBsnCVFfapsRz6nEA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, + "node_modules/json-logic-js": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/json-logic-js/-/json-logic-js-2.0.5.tgz", + "integrity": "sha512-rTT2+lqcuUmj4DgWfmzupZqQDA64AdmYqizzMPWj3DxGdfFNsxPpcNVSaTj4l8W2tG/+hg7/mQhxjU3aPacO6g==", + "license": "MIT" + }, + "node_modules/jwa": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz", + "integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==", + "license": "MIT", + "dependencies": { + "buffer-equal-constant-time": "^1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jws": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.1.tgz", + "integrity": "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==", + "license": "MIT", + "dependencies": { + "jwa": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/leac": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/leac/-/leac-0.6.0.tgz", + "integrity": "sha512-y+SqErxb8h7nE/fiEX07jsbuhrpO9lL8eca7/Y1nuWV2moNlXhyd59iDGcRf6moVyDMbmTNzL40SUyrFU/yDpg==", + "license": "MIT", + "funding": { + "url": "https://ko-fi.com/killymxi" + } + }, + "node_modules/libbase64": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/libbase64/-/libbase64-1.3.0.tgz", + "integrity": "sha512-GgOXd0Eo6phYgh0DJtjQ2tO8dc0IVINtZJeARPeiIJqge+HdsWSuaDTe8ztQ7j/cONByDZ3zeB325AHiv5O0dg==", + "license": "MIT" + }, + "node_modules/libmime": { + "version": "5.3.8", + "resolved": "https://registry.npmjs.org/libmime/-/libmime-5.3.8.tgz", + "integrity": "sha512-ZrCY+Q66mPvasAfjsQ/IgahzoBvfE1VdtGRpo1hwRB1oK3wJKxhKA3GOcd2a6j7AH5eMFccxK9fBoCpRZTf8ng==", + "license": "MIT", + "dependencies": { + "encoding-japanese": "2.2.0", + "iconv-lite": "0.7.2", + "libbase64": "1.3.0", + "libqp": "2.1.1" + } + }, + "node_modules/libmime/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/libqp": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/libqp/-/libqp-2.1.1.tgz", + "integrity": "sha512-0Wd+GPz1O134cP62YU2GTOPNA7Qgl09XwCqM5zpBv87ERCXdfDtyKXvV7c9U22yWJh44QZqBocFnXN11K96qow==", + "license": "MIT" + }, + "node_modules/linkify-it": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-5.0.0.tgz", + "integrity": "sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==", + "license": "MIT", + "dependencies": { + "uc.micro": "^2.0.0" + } + }, + "node_modules/lodash.take": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.take/-/lodash.take-4.1.1.tgz", + "integrity": "sha512-3T118EQjnhr9c0aBKCCMhQn0OBwRMz/O2WaRU6VH0TSKoMCmFtUpr0iUp+eWKODEiRXtYOK7R7SiBneKHdk7og==", + "license": "MIT" + }, + "node_modules/loupe": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.2.1.tgz", + "integrity": "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/luxon": { + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/luxon/-/luxon-3.7.2.tgz", + "integrity": "sha512-vtEhXh/gNjI9Yg1u4jX/0YVPMvxzHuGgCm6tC5kZyb08yjGWGnqAjGJvcXbqQR2P3MyMEFnRbpcdFS6PBcLqew==", + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/magicast": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/magicast/-/magicast-0.3.5.tgz", + "integrity": "sha512-L0WhttDl+2BOsybvEOLK7fW3UA0OQ0IQ2d6Zl2x/a6vVRs3bAY0ECOSHHeL5jD+SbOpOCUEi0y1DgHEn9Qn1AQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.25.4", + "@babel/types": "^7.25.4", + "source-map-js": "^1.2.0" + } + }, + "node_modules/mailparser": { + "version": "3.9.8", + "resolved": "https://registry.npmjs.org/mailparser/-/mailparser-3.9.8.tgz", + "integrity": "sha512-7jSlFGXiianVnhnb6wdutJFloD34488nrHY7r6FNqwXAhZ7YiJDYrKKTxZJ0oSrXcAPHm8YoYnh97xyGtrBQ3w==", + "license": "MIT", + "dependencies": { + "@zone-eu/mailsplit": "5.4.8", + "encoding-japanese": "2.2.0", + "he": "1.2.0", + "html-to-text": "9.0.5", + "iconv-lite": "0.7.2", + "libmime": "5.3.8", + "linkify-it": "5.0.0", + "nodemailer": "8.0.5", + "punycode.js": "2.3.1", + "tlds": "1.261.0" + } + }, + "node_modules/mailparser/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/make-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/minimalistic-assert": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", + "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==", + "license": "ISC" + }, + "node_modules/minimatch": { + "version": "10.2.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", + "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.5" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/minipass": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", + "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/mnemonist": { + "version": "0.38.3", + "resolved": "https://registry.npmjs.org/mnemonist/-/mnemonist-0.38.3.tgz", + "integrity": "sha512-2K9QYubXx/NAjv4VLq1d1Ly8pWNC5L3BrixtdkyTegXWJIqY+zLNDhhX/A+ZwWt70tB1S8H4BE8FLYEFyNoOBw==", + "license": "MIT", + "dependencies": { + "obliterator": "^1.6.1" + } + }, + "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/nanoid": { + "version": "3.3.12", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz", + "integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/nodemailer": { + "version": "8.0.5", + "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-8.0.5.tgz", + "integrity": "sha512-0PF8Yb1yZuQfQbq+5/pZJrtF6WQcjTd5/S4JOHs9PGFxuTqoB/icwuB44pOdURHJbRKX1PPoJZtY7R4VUoCC8w==", + "license": "MIT-0", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/obliterator": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/obliterator/-/obliterator-1.6.1.tgz", + "integrity": "sha512-9WXswnqINnnhOG/5SLimUlzuU1hFJUc8zkwyD59Sd+dPOMf05PmnYG/d6Q7HZ+KmgkZJa1PxRso6QdM3sTNHig==", + "license": "MIT" + }, + "node_modules/openapi3-ts": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/openapi3-ts/-/openapi3-ts-4.5.0.tgz", + "integrity": "sha512-jaL+HgTq2Gj5jRcfdutgRGLosCy/hT8sQf6VOy+P+g36cZOjI1iukdPnijC+4CmeRzg/jEllJUboEic2FhxhtQ==", + "license": "MIT", + "dependencies": { + "yaml": "^2.8.0" + } + }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "dev": true, + "license": "BlueOak-1.0.0" + }, + "node_modules/parseley": { + "version": "0.12.1", + "resolved": "https://registry.npmjs.org/parseley/-/parseley-0.12.1.tgz", + "integrity": "sha512-e6qHKe3a9HWr0oMRVDTRhKce+bRO8VGQR3NyVwcjwrbhMmFCX9KszEV35+rn4AdilFAq9VPxP/Fe1wC9Qjd2lw==", + "license": "MIT", + "dependencies": { + "leac": "^0.6.0", + "peberminta": "^0.9.0" + }, + "funding": { + "url": "https://ko-fi.com/killymxi" + } + }, + "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-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/pathe": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz", + "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/pathval": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.1.tgz", + "integrity": "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.16" + } + }, + "node_modules/peberminta": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/peberminta/-/peberminta-0.9.0.tgz", + "integrity": "sha512-XIxfHpEuSJbITd1H3EeQwpcZbTLHc+VVr8ANI9t5sit565tsI4/xK3KWTUFE2e6QiangUkh3B0jihzmGnNrRsQ==", + "license": "MIT", + "funding": { + "url": "https://ko-fi.com/killymxi" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/postcss": { + "version": "8.5.14", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.14.tgz", + "integrity": "sha512-SoSL4+OSEtR99LHFZQiJLkT59C5B1amGO1NzTwj7TT1qCUgUO6hxOvzkOYxD+vMrXBM3XJIKzokoERdqQq/Zmg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/punycode.js": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode.js/-/punycode.js-2.3.1.tgz", + "integrity": "sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/quickjs-emscripten": { + "version": "0.32.0", + "resolved": "https://registry.npmjs.org/quickjs-emscripten/-/quickjs-emscripten-0.32.0.tgz", + "integrity": "sha512-So0Sqw869y/S2oE3Nuc0uT3Dhqgvsj8FSrwBdsuTosVsG8ME5/OcudU1GxsrIFdFABgy17GHnTVO9TYV/bLQcA==", + "license": "MIT", + "dependencies": { + "@jitl/quickjs-wasmfile-debug-asyncify": "0.32.0", + "@jitl/quickjs-wasmfile-debug-sync": "0.32.0", + "@jitl/quickjs-wasmfile-release-asyncify": "0.32.0", + "@jitl/quickjs-wasmfile-release-sync": "0.32.0", + "quickjs-emscripten-core": "0.32.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/quickjs-emscripten-core": { + "version": "0.32.0", + "resolved": "https://registry.npmjs.org/quickjs-emscripten-core/-/quickjs-emscripten-core-0.32.0.tgz", + "integrity": "sha512-QFnPfjFey8EqknSrSxe1hZrf1/8z7/6s1QzGOmKo6++02r7QRRX7ZoyNaZh7JuVjWsVW87KnQrbZqnHkOAzUyg==", + "license": "MIT", + "dependencies": { + "@jitl/quickjs-ffi-types": "0.32.0" + } + }, + "node_modules/resolve-pkg-maps": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + } + }, + "node_modules/rollup": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.3.tgz", + "integrity": "sha512-pAQK9HalE84QSm4Po3EmWIZPd3FnjkShVkiMlz1iligWYkWQ7wHYd1PF/T7QZ5TVSD6uSTon5gBVMSM4JfBV+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.60.3", + "@rollup/rollup-android-arm64": "4.60.3", + "@rollup/rollup-darwin-arm64": "4.60.3", + "@rollup/rollup-darwin-x64": "4.60.3", + "@rollup/rollup-freebsd-arm64": "4.60.3", + "@rollup/rollup-freebsd-x64": "4.60.3", + "@rollup/rollup-linux-arm-gnueabihf": "4.60.3", + "@rollup/rollup-linux-arm-musleabihf": "4.60.3", + "@rollup/rollup-linux-arm64-gnu": "4.60.3", + "@rollup/rollup-linux-arm64-musl": "4.60.3", + "@rollup/rollup-linux-loong64-gnu": "4.60.3", + "@rollup/rollup-linux-loong64-musl": "4.60.3", + "@rollup/rollup-linux-ppc64-gnu": "4.60.3", + "@rollup/rollup-linux-ppc64-musl": "4.60.3", + "@rollup/rollup-linux-riscv64-gnu": "4.60.3", + "@rollup/rollup-linux-riscv64-musl": "4.60.3", + "@rollup/rollup-linux-s390x-gnu": "4.60.3", + "@rollup/rollup-linux-x64-gnu": "4.60.3", + "@rollup/rollup-linux-x64-musl": "4.60.3", + "@rollup/rollup-openbsd-x64": "4.60.3", + "@rollup/rollup-openharmony-arm64": "4.60.3", + "@rollup/rollup-win32-arm64-msvc": "4.60.3", + "@rollup/rollup-win32-ia32-msvc": "4.60.3", + "@rollup/rollup-win32-x64-gnu": "4.60.3", + "@rollup/rollup-win32-x64-msvc": "4.60.3", + "fsevents": "~2.3.2" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "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/selderee": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/selderee/-/selderee-0.11.0.tgz", + "integrity": "sha512-5TF+l7p4+OsnP8BCCvSyZiSPc4x4//p5uPwK8TCnVPJYRmU2aYKMpOXvw8zM5a5JvuuCGN1jmsMwuU2W02ukfA==", + "license": "MIT", + "dependencies": { + "parseley": "^0.12.0" + }, + "funding": { + "url": "https://ko-fi.com/killymxi" + } + }, + "node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/std-env": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", + "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", + "dev": true, + "license": "MIT" + }, + "node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/string-width-cjs": { + "name": "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==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/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==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/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==", + "dev": true, + "license": "MIT" + }, + "node_modules/string-width-cjs/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==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.2.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi-cjs/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==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/strnum": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/strnum/-/strnum-2.2.3.tgz", + "integrity": "sha512-oKx6RUCuHfT3oyVjtnrmn19H1SiCqgJSg+54XqURKp5aCMbrXrhLjRN9TjuwMjiYstZ0MzDrHqkGZ5dFTKd+zg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT" + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/test-exclude": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-7.0.2.tgz", + "integrity": "sha512-u9E6A+ZDYdp7a4WnarkXPZOx8Ilz46+kby6p1yZ8zsGTz9gYa6FIS7lj2oezzNKmtdyyJNNmmXDppga5GB7kSw==", + "dev": true, + "license": "ISC", + "dependencies": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^10.4.1", + "minimatch": "^10.2.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz", + "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinypool": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.1.1.tgz", + "integrity": "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.0.0 || >=20.0.0" + } + }, + "node_modules/tinyrainbow": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-1.2.0.tgz", + "integrity": "sha512-weEDEq7Z5eTHPDh4xjX789+fHfF+P8boiFB+0vbWzpbnbsEr/GRaohi/uMKxg8RZMXnl1ItAi/IUHWMsjDV7kQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tinyspy": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-3.0.2.tgz", + "integrity": "sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tlds": { + "version": "1.261.0", + "resolved": "https://registry.npmjs.org/tlds/-/tlds-1.261.0.tgz", + "integrity": "sha512-QXqwfEl9ddlGBaRFXIvNKK6OhipSiLXuRuLJX5DErz0o0Q0rYxulWLdFryTkV5PkdZct5iMInwYEGe/eR++1AA==", + "license": "MIT", + "bin": { + "tlds": "bin.js" + } + }, + "node_modules/tldts": { + "version": "7.0.30", + "resolved": "https://registry.npmjs.org/tldts/-/tldts-7.0.30.tgz", + "integrity": "sha512-ELrFxuqsDdHUwoh0XxDbxuLD3Wnz49Z57IFvTtvWy1hJdcMZjXLIuonjilCiWHlT2GbE4Wlv1wKVTzDFnXH1aw==", + "license": "MIT", + "dependencies": { + "tldts-core": "^7.0.30" + }, + "bin": { + "tldts": "bin/cli.js" + } + }, + "node_modules/tldts-core": { + "version": "7.0.30", + "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-7.0.30.tgz", + "integrity": "sha512-uiHN8PIB1VmWyS98eZYja4xzlYqeFZVjb4OuYlJQnZAuJhMw4PbKQOKgHKhBdJR3FE/t5mUQ1Kd80++B+qhD1Q==", + "license": "MIT" + }, + "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/tsx": { + "version": "4.21.0", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.21.0.tgz", + "integrity": "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "~0.27.0", + "get-tsconfig": "^4.7.5" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + } + }, + "node_modules/tsx/node_modules/@esbuild/aix-ppc64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.7.tgz", + "integrity": "sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/android-arm": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.7.tgz", + "integrity": "sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/android-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.7.tgz", + "integrity": "sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/android-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.7.tgz", + "integrity": "sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/darwin-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.7.tgz", + "integrity": "sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/darwin-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.7.tgz", + "integrity": "sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.7.tgz", + "integrity": "sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/freebsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.7.tgz", + "integrity": "sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-arm": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.7.tgz", + "integrity": "sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.7.tgz", + "integrity": "sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-ia32": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.7.tgz", + "integrity": "sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-loong64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.7.tgz", + "integrity": "sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-mips64el": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.7.tgz", + "integrity": "sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-ppc64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.7.tgz", + "integrity": "sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-riscv64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.7.tgz", + "integrity": "sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-s390x": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.7.tgz", + "integrity": "sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.7.tgz", + "integrity": "sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.7.tgz", + "integrity": "sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/netbsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.7.tgz", + "integrity": "sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.7.tgz", + "integrity": "sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/openbsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.7.tgz", + "integrity": "sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/sunos-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.7.tgz", + "integrity": "sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/win32-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.7.tgz", + "integrity": "sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/win32-ia32": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.7.tgz", + "integrity": "sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/win32-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.7.tgz", + "integrity": "sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/esbuild": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.7.tgz", + "integrity": "sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.7", + "@esbuild/android-arm": "0.27.7", + "@esbuild/android-arm64": "0.27.7", + "@esbuild/android-x64": "0.27.7", + "@esbuild/darwin-arm64": "0.27.7", + "@esbuild/darwin-x64": "0.27.7", + "@esbuild/freebsd-arm64": "0.27.7", + "@esbuild/freebsd-x64": "0.27.7", + "@esbuild/linux-arm": "0.27.7", + "@esbuild/linux-arm64": "0.27.7", + "@esbuild/linux-ia32": "0.27.7", + "@esbuild/linux-loong64": "0.27.7", + "@esbuild/linux-mips64el": "0.27.7", + "@esbuild/linux-ppc64": "0.27.7", + "@esbuild/linux-riscv64": "0.27.7", + "@esbuild/linux-s390x": "0.27.7", + "@esbuild/linux-x64": "0.27.7", + "@esbuild/netbsd-arm64": "0.27.7", + "@esbuild/netbsd-x64": "0.27.7", + "@esbuild/openbsd-arm64": "0.27.7", + "@esbuild/openbsd-x64": "0.27.7", + "@esbuild/openharmony-arm64": "0.27.7", + "@esbuild/sunos-x64": "0.27.7", + "@esbuild/win32-arm64": "0.27.7", + "@esbuild/win32-ia32": "0.27.7", + "@esbuild/win32-x64": "0.27.7" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/uc.micro": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-2.1.0.tgz", + "integrity": "sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==", + "license": "MIT" + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "license": "MIT" + }, + "node_modules/vite": { + "version": "5.4.21", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz", + "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.21.3", + "postcss": "^8.4.43", + "rollup": "^4.20.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/vite-node": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-2.1.9.tgz", + "integrity": "sha512-AM9aQ/IPrW/6ENLQg3AGY4K1N2TGZdR5e4gu/MmmR2xR3Ll1+dib+nook92g4TV3PXVyeyxdWwtaCAiUL0hMxA==", + "dev": true, + "license": "MIT", + "dependencies": { + "cac": "^6.7.14", + "debug": "^4.3.7", + "es-module-lexer": "^1.5.4", + "pathe": "^1.1.2", + "vite": "^5.0.0" + }, + "bin": { + "vite-node": "vite-node.mjs" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/vite/node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/esbuild": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, + "node_modules/vitest": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-2.1.9.tgz", + "integrity": "sha512-MSmPM9REYqDGBI8439mA4mWhV5sKmDlBKWIYbA3lRb2PTHACE0mgKwA8yQ2xq9vxDTuk4iPrECBAEW2aoFXY0Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/expect": "2.1.9", + "@vitest/mocker": "2.1.9", + "@vitest/pretty-format": "^2.1.9", + "@vitest/runner": "2.1.9", + "@vitest/snapshot": "2.1.9", + "@vitest/spy": "2.1.9", + "@vitest/utils": "2.1.9", + "chai": "^5.1.2", + "debug": "^4.3.7", + "expect-type": "^1.1.0", + "magic-string": "^0.30.12", + "pathe": "^1.1.2", + "std-env": "^3.8.0", + "tinybench": "^2.9.0", + "tinyexec": "^0.3.1", + "tinypool": "^1.0.1", + "tinyrainbow": "^1.2.0", + "vite": "^5.0.0", + "vite-node": "2.1.9", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@types/node": "^18.0.0 || >=20.0.0", + "@vitest/browser": "2.1.9", + "@vitest/ui": "2.1.9", + "happy-dom": "*", + "jsdom": "*" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + } + } + }, + "node_modules/web-push": { + "version": "3.6.7", + "resolved": "https://registry.npmjs.org/web-push/-/web-push-3.6.7.tgz", + "integrity": "sha512-OpiIUe8cuGjrj3mMBFWY+e4MMIkW3SVT+7vEIjvD9kejGUypv8GPDf84JdPWskK8zMRIJ6xYGm+Kxr8YkPyA0A==", + "license": "MPL-2.0", + "dependencies": { + "asn1.js": "^5.3.0", + "http_ece": "1.2.0", + "https-proxy-agent": "^7.0.0", + "jws": "^4.0.0", + "minimist": "^1.2.5" + }, + "bin": { + "web-push": "src/cli.js" + }, + "engines": { + "node": ">= 16" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs": { + "name": "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==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/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==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/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==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/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==", + "dev": true, + "license": "MIT" + }, + "node_modules/wrap-ansi-cjs/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==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/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==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yaml": { + "version": "2.8.4", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.4.tgz", + "integrity": "sha512-ml/JPOj9fOQK8RNnWojA67GbZ0ApXAUlN2UQclwv2eVgTgn7O9gg9o7paZWKMp4g0H3nTLtS9LVzhkpOFIKzog==", + "license": "ISC", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + }, + "funding": { + "url": "https://github.com/sponsors/eemeli" + } + }, + "node_modules/zod": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.4.3.tgz", + "integrity": "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + } + } +} diff --git a/package.json b/package.json index cacca00..ff46b05 100644 --- a/package.json +++ b/package.json @@ -8,34 +8,41 @@ "test": "vitest run", "test:watch": "vitest", "test:coverage": "vitest run --coverage", - "make": "tsx make.ts" + "make": "tsx make.ts", + "openapi": "tsx scripts/openapi.ts" }, "dependencies": { "@authress/sdk": "^3.2.266", "@aws-sdk/client-bedrock-runtime": "^3.0.0", "@aws-sdk/client-dynamodb": "^3.0.0", + "@aws-sdk/client-rds-data": "^3.1045.0", "@aws-sdk/client-s3": "^3.0.0", "@aws-sdk/client-sesv2": "^3.0.0", "@aws-sdk/lib-dynamodb": "^3.0.0", - "@aws-sdk/rds-signer": "^3.0.0", "@aws-sdk/s3-request-presigner": "^3.0.0", + "@hono/zod-openapi": "^1.3.0", + "@types/web-push": "^3.6.4", "hono": "^4.0.0", "json-logic-js": "^2.0.0", "mailparser": "^3.7.0", - "pg": "^8.0.0", - "tldts": "^7.0.28" + "quickjs-emscripten": "^0.32.0", + "tldts": "^7.0.28", + "web-push": "^3.6.7", + "zod": "^4.4.3" }, "devDependencies": { "@types/aws-lambda": "^8.10.0", "@types/json-logic-js": "^2.0.0", "@types/mailparser": "^3.4.0", "@types/node": "^22.0.0", - "@types/pg": "^8.0.0", "@vitest/coverage-v8": "^2.0.0", "commander": "^12.0.0", "esbuild": "^0.24.0", "tsx": "^4.0.0", "typescript": "^5.7.0", "vitest": "^2.0.0" + }, + "engines": { + "node": ">=24" } } diff --git a/scripts/openapi.ts b/scripts/openapi.ts new file mode 100644 index 0000000..51a67e3 --- /dev/null +++ b/scripts/openapi.ts @@ -0,0 +1,23 @@ +import { writeFileSync } from "fs"; +import { resolve } from "path"; +import { createApp } from "../src/api/app.js"; + +// Stub deps — only needed to instantiate the router; no actual DB calls happen at startup +const noop = () => { throw new Error("stub"); }; +const storeStub = new Proxy({} as Parameters[0]["store"], { + get: () => noop, +}); +const authStub: Parameters[0]["auth"] = { + validateToken: noop as never, +}; + +const app = createApp({ store: storeStub, auth: authStub }); + +const spec = app.getOpenAPIDocument({ + openapi: "3.1.0", + info: { title: "SES Email Adapter", version: "1.0.0" }, +}); + +const outPath = resolve(process.cwd(), "openapi.json"); +writeFileSync(outPath, JSON.stringify(spec, null, 2)); +console.log(`OpenAPI spec written to ${outPath}`); diff --git a/src/api/api.spec.ts b/src/api/api.spec.ts index 018c600..4e301c2 100644 --- a/src/api/api.spec.ts +++ b/src/api/api.spec.ts @@ -33,6 +33,10 @@ function makeStore(): ApiDatabase { getArc: vi.fn().mockResolvedValue(null), updateArc: vi.fn().mockResolvedValue(makeArc()), listSignals: vi.fn().mockResolvedValue({ items: [] }), + listPreArcSignals: vi.fn().mockResolvedValue({ items: [] }), + blockSignal: vi.fn().mockImplementation((_, id) => Promise.resolve({ id, status: "blocked" })), + saveArc: vi.fn().mockResolvedValue(undefined), + findArcByGroupingKey: vi.fn().mockResolvedValue(null), getSignal: vi.fn().mockResolvedValue(null), updateSignal: vi.fn().mockResolvedValue(makeSignal()), deleteSignal: vi.fn().mockResolvedValue(undefined), @@ -41,7 +45,6 @@ function makeStore(): ApiDatabase { createView: vi.fn().mockResolvedValue(makeView()), updateView: vi.fn().mockResolvedValue(makeView()), deleteView: vi.fn().mockResolvedValue(undefined), - reorderViews: vi.fn().mockResolvedValue(undefined), listLabels: vi.fn().mockResolvedValue([]), createLabel: vi.fn().mockResolvedValue(makeLabel()), updateLabel: vi.fn().mockResolvedValue(makeLabel()), @@ -50,7 +53,6 @@ function makeStore(): ApiDatabase { createRule: vi.fn().mockResolvedValue(makeRule()), updateRule: vi.fn().mockResolvedValue(makeRule()), deleteRule: vi.fn().mockResolvedValue(undefined), - reorderRules: vi.fn().mockResolvedValue(undefined), listDomains: vi.fn().mockResolvedValue([]), getDomain: vi.fn().mockResolvedValue(null), createDomain: vi.fn().mockResolvedValue(makeDomain()), @@ -69,6 +71,17 @@ function makeStore(): ApiDatabase { getVerifiedForwardingAddress: vi.fn().mockResolvedValue(null), saveVerifiedForwardingAddress: vi.fn().mockResolvedValue(undefined), deleteVerifiedForwardingAddress: vi.fn().mockResolvedValue(undefined), + updateDomainHealth: vi.fn().mockResolvedValue(undefined), + renameAlias: vi.fn().mockResolvedValue(makeAlias()), + saveSender: vi.fn().mockResolvedValue(undefined), + removeSender: vi.fn().mockResolvedValue(undefined), + listSenders: vi.fn().mockResolvedValue([]), + createTemplate: vi.fn().mockResolvedValue(undefined), + getTemplate: vi.fn().mockResolvedValue(null), + updateTemplate: vi.fn().mockResolvedValue(undefined), + deleteTemplate: vi.fn().mockResolvedValue(undefined), + listTemplates: vi.fn().mockResolvedValue([]), + listAuditEvents: vi.fn().mockResolvedValue({ items: [] }), }; } @@ -89,8 +102,7 @@ function makeAlias(overrides: Partial = {}): Alias { id: "cfg-001", accountId: TEST_ACCOUNT_ID, address: "user@example.com", - filterMode: "notify_new", - approvedSenders: ["amazon.com", "google.com"], + filterMode: "quarantine_visible", createdAt: "2024-01-01T00:00:00Z", updatedAt: "2024-01-01T00:00:00Z", ...overrides, @@ -182,7 +194,8 @@ function makeRule(overrides: Partial = {}): Rule { name: "Archive newsletters", condition: '{"==": [{"var": "arc.category"}, "newsletter"]}', actions: [{ type: "archive" }], - position: 0, + status: "enabled", + priorityOrder: 100, createdAt: "2024-01-01T00:00:00Z", updatedAt: "2024-01-01T00:00:00Z", ...overrides, @@ -387,6 +400,91 @@ describe("API", () => { }); }); + describe("GET /accounts/:accountId/signals?status=", () => { + it("returns quarantined signals (both visible and hidden) when status=quarantined", async () => { + const s = makeSignal({ status: "quarantine_visible" }); + vi.mocked(store.listPreArcSignals).mockResolvedValueOnce({ items: [s] }); + const res = await req(app, "GET", `${A}/signals?status=quarantined`); + expect(res.status).toBe(200); + const body = await res.json() as { signals: Signal[]; pagination: { cursor: string | null } }; + expect(body.signals).toHaveLength(1); + expect(store.listPreArcSignals).toHaveBeenCalledWith(TEST_ACCOUNT_ID, "quarantined", expect.any(Object)); + }); + + it("returns blocked signals", async () => { + const s = makeSignal({ status: "blocked" }); + vi.mocked(store.listPreArcSignals).mockResolvedValueOnce({ items: [s] }); + const res = await req(app, "GET", `${A}/signals?status=blocked`); + expect(res.status).toBe(200); + const body = await res.json() as { signals: Signal[]; pagination: { cursor: string | null } }; + expect(body.signals).toHaveLength(1); + expect(store.listPreArcSignals).toHaveBeenCalledWith(TEST_ACCOUNT_ID, "blocked", expect.any(Object)); + }); + + it("returns 400 when status param is missing", async () => { + const res = await req(app, "GET", `${A}/signals`); + expect(res.status).toBe(400); + }); + + it("returns 400 when status param is invalid", async () => { + const res = await req(app, "GET", `${A}/signals?status=active`); + expect(res.status).toBe(400); + }); + }); + + describe("PUT /accounts/:accountId/signals/:id/quarantineResponse", () => { + it("blocks a quarantined signal", async () => { + const s = makeSignal({ status: "quarantine_visible" }); + vi.mocked(store.getSignal).mockResolvedValueOnce(s); + const res = await req(app, "POST", `${A}/signals/SES%23msg-001/quarantineResponse`, { body: { status: "blocked" } }); + expect(res.status).toBe(200); + expect(store.blockSignal).toHaveBeenCalledWith(TEST_ACCOUNT_ID, s.id); + }); + + it("allows a quarantined signal — creates new arc when no grouping key match", async () => { + const s = makeSignal({ status: "quarantine_visible" }); + vi.mocked(store.getSignal).mockResolvedValueOnce(s); + vi.mocked(store.findArcByGroupingKey).mockResolvedValueOnce(null); + const res = await req(app, "POST", `${A}/signals/SES%23msg-001/quarantineResponse`, { body: { status: "active" } }); + expect(res.status).toBe(200); + const body = await res.json() as { arc: Arc; signal: Signal }; + expect(body.arc.workflow).toBe(s.workflow); + expect(body.signal.status).toBe("active"); + expect(store.createArc).toHaveBeenCalledOnce(); + expect(store.unblockSignal).toHaveBeenCalledWith(TEST_ACCOUNT_ID, s.id, body.arc.id); + }); + + it("allows a quarantined signal — attaches to existing arc when grouping key matches", async () => { + const s = makeSignal({ status: "quarantine_visible", workflow: "auth" }); + const existingArc = makeArc(); + vi.mocked(store.getSignal).mockResolvedValueOnce(s); + vi.mocked(store.findArcByGroupingKey).mockResolvedValueOnce(existingArc); + const res = await req(app, "POST", `${A}/signals/SES%23msg-001/quarantineResponse`, { body: { status: "active" } }); + expect(res.status).toBe(200); + const body = await res.json() as { arc: Arc; signal: Signal }; + expect(body.arc.id).toBe(existingArc.id); + expect(store.createArc).not.toHaveBeenCalled(); + expect(store.unblockSignal).toHaveBeenCalledWith(TEST_ACCOUNT_ID, s.id, existingArc.id); + }); + + it("returns 400 when signal is already active", async () => { + vi.mocked(store.getSignal).mockResolvedValueOnce(makeSignal({ status: "active" })); + const res = await req(app, "POST", `${A}/signals/SES%23msg-001/quarantineResponse`, { body: { status: "active" } }); + expect(res.status).toBe(400); + }); + + it("returns 400 when body is missing status", async () => { + vi.mocked(store.getSignal).mockResolvedValueOnce(makeSignal({ status: "quarantine_visible" })); + const res = await req(app, "POST", `${A}/signals/SES%23msg-001/quarantineResponse`, { body: {} }); + expect(res.status).toBe(400); + }); + + it("returns 404 for unknown signal", async () => { + const res = await req(app, "POST", `${A}/signals/nonexistent/quarantineResponse`, { body: { status: "active" } }); + expect(res.status).toBe(404); + }); + }); + describe("GET /accounts/:accountId/signals/:id", () => { it("returns full Signal detail", async () => { vi.mocked(store.getSignal).mockResolvedValueOnce(makeSignal({ textBody: "Hello world" })); @@ -542,14 +640,6 @@ describe("API", () => { }); }); - describe("POST /accounts/:accountId/views/reorder", () => { - it("reorders Views by ID array", async () => { - const res = await req(app, "POST", `${A}/views/reorder`, { body: { orderedIds: ["view-002", "view-001"] } }); - expect(res.status).toBe(200); - expect(store.reorderViews).toHaveBeenCalledWith(TEST_ACCOUNT_ID, ["view-002", "view-001"]); - }); - }); - // ------------------------------------------------------------------------- // Label routes // ------------------------------------------------------------------------- @@ -669,14 +759,6 @@ describe("API", () => { }); }); - describe("POST /accounts/:accountId/rules/reorder", () => { - it("reorders rules by ID array", async () => { - const res = await req(app, "POST", `${A}/rules/reorder`, { body: { orderedIds: ["rule-002", "rule-001"] } }); - expect(res.status).toBe(200); - expect(store.reorderRules).toHaveBeenCalledWith(TEST_ACCOUNT_ID, ["rule-002", "rule-001"]); - }); - }); - // ------------------------------------------------------------------------- // Domain routes // ------------------------------------------------------------------------- @@ -794,21 +876,16 @@ describe("API", () => { // Search // ------------------------------------------------------------------------- - describe("GET /accounts/:accountId/search", () => { + describe("GET /accounts/:accountId/arcs?q=", () => { it("returns Arc search results in named envelope", async () => { vi.mocked(store.searchArcs).mockResolvedValueOnce({ items: [makeArc()] }); - const res = await req(app, "GET", `${A}/search?q=invoice+from+stripe`); + const res = await req(app, "GET", `${A}/arcs?q=invoice+from+stripe`); expect(res.status).toBe(200); const body = await res.json() as { arcs: unknown[]; pagination: { cursor: null } }; expect(body.arcs).toHaveLength(1); expect(body.pagination).toEqual({ cursor: null }); expect(store.searchArcs).toHaveBeenCalledWith(TEST_ACCOUNT_ID, "invoice from stripe", expect.any(Object)); }); - - it("returns 400 when query is missing", async () => { - const res = await req(app, "GET", `${A}/search`); - expect(res.status).toBe(400); - }); }); // ------------------------------------------------------------------------- @@ -852,18 +929,18 @@ describe("API", () => { it("updates account filtering config including blockOnboardingEmails", async () => { const res = await req(app, "PATCH", `${A}`, { - body: { filtering: { defaultFilterMode: "strict", blockOnboardingEmails: true } }, + body: { filtering: { defaultFilterMode: "block", blockOnboardingEmails: true } }, }); expect(res.status).toBe(200); expect(store.updateAccount).toHaveBeenCalledWith( TEST_ACCOUNT_ID, - expect.objectContaining({ filtering: { defaultFilterMode: "strict", blockOnboardingEmails: true } }), + expect.objectContaining({ filtering: { defaultFilterMode: "block", blockOnboardingEmails: true } }), ); }); it("updates account-level spamScoreThreshold in filtering config", async () => { const res = await req(app, "PATCH", `${A}`, { - body: { filtering: { defaultFilterMode: "notify_new", newAddressHandling: "auto_allow", spamScoreThreshold: 0.75 } }, + body: { filtering: { defaultFilterMode: "quarantine_visible", newAddressHandling: "auto_allow", spamScoreThreshold: 0.75 } }, }); expect(res.status).toBe(200); expect(store.updateAccount).toHaveBeenCalledWith( @@ -962,7 +1039,7 @@ describe("API", () => { const res = await req(app, "GET", `${A}/aliases/user%40example.com`); expect(res.status).toBe(200); const body = await res.json() as Alias; - expect(body.filterMode).toBe("notify_new"); + expect(body.filterMode).toBe("quarantine_visible"); }); it("returns 404 when no alias exists", async () => { @@ -975,13 +1052,13 @@ describe("API", () => { it("creates an alias and returns 201 + full resource", async () => { vi.mocked(store.createAlias).mockResolvedValueOnce(makeAlias({ address: "me@mydomain.com" })); const res = await req(app, "POST", `${A}/aliases`, { - body: { address: "me@mydomain.com", filterMode: "strict" }, + body: { address: "me@mydomain.com", filterMode: "block" }, }); expect(res.status).toBe(201); const body = await res.json() as Alias; expect(body.address).toBe("me@mydomain.com"); expect(store.createAlias).toHaveBeenCalledWith( - expect.objectContaining({ accountId: TEST_ACCOUNT_ID, address: "me@mydomain.com", filterMode: "strict" }), + expect.objectContaining({ accountId: TEST_ACCOUNT_ID, address: "me@mydomain.com", filterMode: "block" }), ); }); @@ -994,7 +1071,7 @@ describe("API", () => { }); it("returns 400 when address is missing", async () => { - const res = await req(app, "POST", `${A}/aliases`, { body: { filterMode: "strict" } }); + const res = await req(app, "POST", `${A}/aliases`, { body: { filterMode: "block" } }); expect(res.status).toBe(400); }); @@ -1011,15 +1088,15 @@ describe("API", () => { describe("PATCH /accounts/:accountId/aliases/:address", () => { it("creates or updates an alias and returns 200 + full resource", async () => { - vi.mocked(store.upsertAlias).mockResolvedValueOnce(makeAlias({ filterMode: "strict" })); + vi.mocked(store.upsertAlias).mockResolvedValueOnce(makeAlias({ filterMode: "block" })); const res = await req(app, "PATCH", `${A}/aliases/me%40mydomain.com`, { - body: { filterMode: "strict", approvedSenders: ["amazon.com"] }, + body: { filterMode: "block" }, }); expect(res.status).toBe(200); const body = await res.json() as Alias; - expect(body.filterMode).toBe("strict"); + expect(body.filterMode).toBe("block"); expect(store.upsertAlias).toHaveBeenCalledWith( - expect.objectContaining({ accountId: TEST_ACCOUNT_ID, address: "me@mydomain.com", filterMode: "strict" }), + expect.objectContaining({ accountId: TEST_ACCOUNT_ID, address: "me@mydomain.com", filterMode: "block" }), ); }); @@ -1035,7 +1112,7 @@ describe("API", () => { it("stores spamScoreThreshold when included in the request body", async () => { await req(app, "PATCH", `${A}/aliases/me%40mydomain.com`, { - body: { filterMode: "strict", approvedSenders: [], spamScoreThreshold: 0.7 }, + body: { filterMode: "block", spamScoreThreshold: 0.7 }, }); const saved = vi.mocked(store.upsertAlias).mock.calls[0]![0] as Alias; expect(saved.spamScoreThreshold).toBe(0.7); @@ -1043,7 +1120,7 @@ describe("API", () => { it("does not set spamScoreThreshold when absent from request body", async () => { await req(app, "PATCH", `${A}/aliases/me%40mydomain.com`, { - body: { filterMode: "notify_new", approvedSenders: ["amazon.com"] }, + body: { filterMode: "quarantine_visible" }, }); const saved = vi.mocked(store.upsertAlias).mock.calls[0]![0] as Alias; expect(saved.spamScoreThreshold).toBeUndefined(); @@ -1075,7 +1152,7 @@ describe("API", () => { }); it("creates an Arc from a quarantined signal and returns 201", async () => { - vi.mocked(store.getSignal).mockResolvedValueOnce(makeSignal({ status: "quarantined" })); + vi.mocked(store.getSignal).mockResolvedValueOnce(makeSignal({ status: "quarantine_visible" })); const res = await req(app, "POST", `${A}/arcs`, { body: { signalId: "SES#msg-001" } }); expect(res.status).toBe(201); expect(store.createArc).toHaveBeenCalledOnce(); @@ -1112,9 +1189,7 @@ describe("API", () => { makeSignal({ status: "blocked", from: { address: "noreply@mail.amazon.com" }, recipientAddress: "me@mydomain.com" }), ); await req(app, "POST", `${A}/arcs`, { body: { signalId: "SES#msg-001", approveSender: true } }); - const saved = vi.mocked(store.upsertAlias).mock.calls[0]![0] as Alias; - expect(saved.approvedSenders).toContain("amazon.com"); - expect(saved.address).toBe("me@mydomain.com"); + expect(store.saveSender).toHaveBeenCalledWith(TEST_ACCOUNT_ID, "me@mydomain.com", "amazon.com", "allow"); }); it("updates filter mode when updateFilterMode is provided", async () => { @@ -1124,20 +1199,6 @@ describe("API", () => { expect(saved.filterMode).toBe("allow_all"); }); - it("preserves existing approved senders when approving a new one", async () => { - vi.mocked(store.getSignal).mockResolvedValueOnce( - makeSignal({ status: "blocked", from: { address: "support@github.com" }, recipientAddress: "user@example.com" }), - ); - vi.mocked(store.getAlias).mockResolvedValueOnce( - makeAlias({ approvedSenders: ["amazon.com", "google.com"] }), - ); - await req(app, "POST", `${A}/arcs`, { body: { signalId: "SES#msg-001", approveSender: true } }); - const saved = vi.mocked(store.upsertAlias).mock.calls[0]![0] as Alias; - expect(saved.approvedSenders).toContain("amazon.com"); - expect(saved.approvedSenders).toContain("google.com"); - expect(saved.approvedSenders).toContain("github.com"); - }); - it("does not modify aliases when neither approveSender nor updateFilterMode is set", async () => { vi.mocked(store.getSignal).mockResolvedValueOnce(makeSignal({ status: "blocked" })); await req(app, "POST", `${A}/arcs`, { body: { signalId: "SES#msg-001" } }); @@ -1145,26 +1206,14 @@ describe("API", () => { expect(store.getAlias).not.toHaveBeenCalled(); }); - it("does not add sender to approvedSenders when approveSender is false and updateFilterMode is set", async () => { + it("does not call saveSender when approveSender is false and updateFilterMode is set", async () => { vi.mocked(store.getSignal).mockResolvedValueOnce( makeSignal({ status: "blocked", from: { address: "news@amazon.com" }, recipientAddress: "me@mydomain.com" }), ); await req(app, "POST", `${A}/arcs`, { body: { signalId: "SES#msg-001", updateFilterMode: "allow_all" } }); const saved = vi.mocked(store.upsertAlias).mock.calls[0]![0] as Alias; expect(saved.filterMode).toBe("allow_all"); - expect(saved.approvedSenders).not.toContain("amazon.com"); - }); - - it("does not add duplicate sender when sender is already in approvedSenders", async () => { - vi.mocked(store.getSignal).mockResolvedValueOnce( - makeSignal({ status: "blocked", from: { address: "deals@amazon.com" }, recipientAddress: "user@example.com" }), - ); - vi.mocked(store.getAlias).mockResolvedValueOnce( - makeAlias({ approvedSenders: ["amazon.com", "google.com"] }), // amazon.com already approved - ); - await req(app, "POST", `${A}/arcs`, { body: { signalId: "SES#msg-001", approveSender: true } }); - const saved = vi.mocked(store.upsertAlias).mock.calls[0]![0] as Alias; - expect(saved.approvedSenders.filter((s) => s === "amazon.com")).toHaveLength(1); // no duplicate + expect(store.saveSender).not.toHaveBeenCalled(); }); }); diff --git a/src/api/app.ts b/src/api/app.ts index 4d16627..b772178 100644 --- a/src/api/app.ts +++ b/src/api/app.ts @@ -1,7 +1,24 @@ -import { Hono } from "hono"; +import { OpenAPIHono } from "@hono/zod-openapi"; +import type { Context } from "hono"; import { randomUUID } from "crypto"; import { getDomain } from "tldts"; -import type { Arc, Signal, View, Label, Rule, Domain, DnsRecord, Account, Page, PageParams, ArcStatus, Workflow, Alias, SenderFilterMode, AccountFilteringConfig, VerifiedForwardingAddress, Pagination } from "../types/index.js"; +import { checkDomain } from "../dns/dns-checker.js"; +import type { AuditEvent } from "../database/audit-database.js"; +import type { Arc, Signal, View, Label, Rule, Domain, DnsRecord, Account, Page, PageParams, ArcStatus, Workflow, WorkflowData, Alias, AliasSender, SenderMode, SenderFilterMode, VerifiedForwardingAddress, Pagination, EmailTemplate } from "../types/index.js"; +import { deriveGroupingKey } from "../processor/processor.js"; +import { zParse } from "./validate.js"; +import { + UpdateArcRequest, CreateArcFromSignalRequest, UpdateSignalRequest, UpdateSignalStatusRequest, + CreateViewRequest, UpdateViewRequest, + CreateLabelRequest, UpdateLabelRequest, + CreateRuleRequest, UpdateRuleRequest, + CreateDomainRequest, + CreateAliasRequest, UpdateAliasRequest, + UpdateAccountRequest, + CreateForwardingAddressRequest, VerifyForwardingAddressRequest, + InviteUserRequest, UpdateUserRequest, + CreateSenderRequest, CreateTemplateRequest, UpdateTemplateRequest, +} from "./requests.js"; // --------------------------------------------------------------------------- // Auth @@ -45,58 +62,7 @@ export interface ListArcsParams extends PageParams { status?: ArcStatus; } -export interface UpdateArcRequest { - status?: ArcStatus; - labels?: string[]; -} - -export interface CreateViewRequest { - name: string; - workflow?: Workflow; - labels?: string[]; - sortField?: View["sortField"]; - sortDirection?: View["sortDirection"]; - icon?: string; - color?: string; - position?: number; -} - -export interface UpdateViewRequest { - name?: string; - workflow?: Workflow; - labels?: string[]; - sortField?: View["sortField"]; - sortDirection?: View["sortDirection"]; - icon?: string; - color?: string; - position?: number; -} - -export interface CreateLabelRequest { - name: string; - color?: string; - icon?: string; -} - -export interface UpdateLabelRequest { - name?: string; - color?: string; - icon?: string; -} - -export interface CreateRuleRequest { - name: string; - condition: string; - actions: Rule["actions"]; - position?: number; -} - -export interface UpdateRuleRequest { - name?: string; - condition?: string; - actions?: Rule["actions"]; - position?: number; -} +export type { UpdateArcRequest, UpdateSignalStatusRequest, CreateViewRequest, UpdateViewRequest, CreateLabelRequest, UpdateLabelRequest, CreateRuleRequest, UpdateRuleRequest }; export interface ApiDatabase { // Arcs @@ -106,6 +72,7 @@ export interface ApiDatabase { // Signals listSignals(accountId: string, arcId: string, params: PageParams): Promise>; + listPreArcSignals(accountId: string, status: "blocked" | "quarantined", params: PageParams): Promise>; getSignal(accountId: string, id: string): Promise; updateSignal(accountId: string, id: string, update: Partial>): Promise; deleteSignal(accountId: string, id: string): Promise; @@ -116,7 +83,6 @@ export interface ApiDatabase { createView(accountId: string, data: CreateViewRequest): Promise; updateView(accountId: string, id: string, data: UpdateViewRequest): Promise; deleteView(accountId: string, id: string): Promise; - reorderViews(accountId: string, orderedIds: string[]): Promise; // Labels listLabels(accountId: string): Promise; @@ -129,13 +95,13 @@ export interface ApiDatabase { createRule(accountId: string, data: CreateRuleRequest): Promise; updateRule(accountId: string, id: string, data: UpdateRuleRequest): Promise; deleteRule(accountId: string, id: string): Promise; - reorderRules(accountId: string, orderedIds: string[]): Promise; // Domains listDomains(accountId: string): Promise; getDomain(accountId: string, id: string): Promise; createDomain(accountId: string, domain: string): Promise; deleteDomain(accountId: string, id: string): Promise; + updateDomainHealth(accountId: string, id: string, health: { receivingHealthy: boolean; senderHealthy: boolean; failingRecords: string[]; lastCheckedAt: string; lastHealthyAt?: string }): Promise; // Search searchArcs(accountId: string, query: string, params: PageParams): Promise>; @@ -150,16 +116,36 @@ export interface ApiDatabase { createAlias(alias: Alias): Promise; upsertAlias(alias: Alias): Promise; deleteAlias(accountId: string, address: string): Promise; + renameAlias(accountId: string, oldAddress: string, newAddress: string): Promise; + + // Alias Senders + saveSender(accountId: string, address: string, domain: string, mode: SenderMode): Promise; + removeSender(accountId: string, address: string, domain: string): Promise; + listSenders(accountId: string, address: string): Promise; - // Signal unblocking + // Templates + createTemplate(template: EmailTemplate): Promise; + getTemplate(accountId: string, id: string): Promise; + updateTemplate(accountId: string, id: string, update: Partial>): Promise; + deleteTemplate(accountId: string, id: string): Promise; + listTemplates(accountId: string): Promise; + + + // Signal status management + blockSignal(accountId: string, signalId: string): Promise; unblockSignal(accountId: string, signalId: string, arcId: string): Promise; createArc(arc: Arc): Promise; + saveArc(arc: Arc): Promise; + findArcByGroupingKey(accountId: string, key: string): Promise; // Verified forwarding addresses listVerifiedForwardingAddresses(accountId: string): Promise; getVerifiedForwardingAddress(accountId: string, address: string): Promise; saveVerifiedForwardingAddress(addr: VerifiedForwardingAddress): Promise; deleteVerifiedForwardingAddress(accountId: string, address: string): Promise; + + // Audit + listAuditEvents(accountId: string, params: PageParams): Promise>; } // --------------------------------------------------------------------------- @@ -187,27 +173,38 @@ type AppEnv = { Variables: { auth: AuthContext } }; // Helpers // --------------------------------------------------------------------------- -function apiError(c: Parameters[1] extends infer H ? never : never, status: number, title: string, errorCode?: string, details?: unknown): Response { - return Response.json( - { title, ...(errorCode ? { errorCode } : {}), ...(details !== undefined ? { details } : {}) }, - { status }, - ); -} - function page(key: K, items: T[], nextCursor?: string): Record & { pagination: Pagination } { return { [key]: items, pagination: { cursor: nextCursor ?? null } } as Record & { pagination: Pagination }; } export function createApp({ store, auth, access, verificationMailer }: AppDeps) { - const app = new Hono(); + const app = new OpenAPIHono(); - function err(c: Parameters>[0], status: number, title: string, errorCode?: string, details?: unknown) { + app.doc("/openapi.json", { + openapi: "3.1.0", + info: { title: "SES Email Adapter", version: "1.0.0" }, + }); + + app.get("/", (c) => c.redirect("/openapi.json", 301)); + + function err(c: Context, status: number, title: string, errorCode?: string, details?: unknown) { return c.json( { title, ...(errorCode ? { errorCode } : {}), ...(details !== undefined ? { details } : {}) }, status as 400 | 401 | 403 | 404 | 409 | 501, ); } + // CloudFront origin verification — reject requests that bypass CloudFront + const CF_ORIGIN_SECRET = process.env["CF_ORIGIN_SECRET"]; + if (CF_ORIGIN_SECRET) { + app.use("*", async (c, next) => { + if (c.req.header("x-origin-verify") !== CF_ORIGIN_SECRET) { + return err(c, 403, "Forbidden"); + } + await next(); + }); + } + // JWT verification app.use("*", async (c, next) => { const header = c.req.header("Authorization"); @@ -246,6 +243,15 @@ export function createApp({ store, auth, access, verificationMailer }: AppDeps) app.get("/accounts/:accountId/arcs", async (c) => { const { accountId } = c.get("auth"); const query = c.req.query(); + const q = query["q"]; + if (q) { + const params: PageParams = { + ...(query["cursor"] ? { cursor: query["cursor"] } : {}), + ...(query["limit"] ? { limit: parseInt(query["limit"], 10) } : {}), + }; + const result = await store.searchArcs(accountId, q, params); + return c.json(page("arcs", result.items, result.nextCursor)); + } const params: ListArcsParams = { ...(query["workflow"] ? { workflow: query["workflow"] as Workflow } : {}), ...(query["label"] ? { label: query["label"] } : {}), @@ -270,22 +276,18 @@ export function createApp({ store, auth, access, verificationMailer }: AppDeps) const arc = await store.getArc(accountId, c.req.param("id")); if (!arc) return err(c, 404, "Arc not found", "ARC_NOT_FOUND"); if (arc.accountId !== accountId) return err(c, 403, "Forbidden"); - const body = await c.req.json() as UpdateArcRequest; + const body = await zParse(UpdateArcRequest, c.req.raw); const updated = await store.updateArc(accountId, arc.id, body); return c.json(updated); }); app.post("/accounts/:accountId/arcs", async (c) => { const { accountId } = c.get("auth"); - const body = await c.req.json() as { - signalId: string; - approveSender?: boolean; - updateFilterMode?: SenderFilterMode; - }; + const body = await zParse(CreateArcFromSignalRequest, c.req.raw); const signal = await store.getSignal(accountId, body.signalId); if (!signal) return err(c, 404, "Signal not found", "SIGNAL_NOT_FOUND"); - if (signal.status !== "blocked" && signal.status !== "quarantined") { + if (signal.status !== "blocked" && signal.status !== "quarantine_visible" && signal.status !== "quarantine_hidden") { return err(c, 400, "Signal is not blocked or quarantined", "SIGNAL_NOT_BLOCKED"); } @@ -316,8 +318,7 @@ export function createApp({ store, auth, access, verificationMailer }: AppDeps) id: randomUUID(), accountId, address: signal.recipientAddress, - filterMode: "notify_new" as SenderFilterMode, - approvedSenders: [] as string[], + filterMode: "quarantine_visible" as SenderFilterMode, createdAt: now, updatedAt: now, }; @@ -325,11 +326,11 @@ export function createApp({ store, auth, access, verificationMailer }: AppDeps) await store.upsertAlias({ ...base, filterMode: body.updateFilterMode ?? base.filterMode, - approvedSenders: body.approveSender && !base.approvedSenders.includes(senderETLD1) - ? [...base.approvedSenders, senderETLD1] - : base.approvedSenders, updatedAt: now, }); + if (body.approveSender) { + await store.saveSender(accountId, signal.recipientAddress, senderETLD1, "allow"); + } } return c.json(arc, 201); @@ -353,6 +354,76 @@ export function createApp({ store, auth, access, verificationMailer }: AppDeps) return c.json(page("signals", result.items, result.nextCursor)); }); + app.get("/accounts/:accountId/signals", async (c) => { + const { accountId } = c.get("auth"); + const query = c.req.query(); + const status = query["status"]; + if (status !== "blocked" && status !== "quarantined" && status !== "quarantine_visible" && status !== "quarantine_hidden") { + return err(c, 400, "status query param must be 'blocked', 'quarantined', 'quarantine_visible', or 'quarantine_hidden'", "INVALID_STATUS"); + } + const quarantineCategory = (status === "quarantined" || status === "quarantine_visible" || status === "quarantine_hidden") ? "quarantined" : "blocked"; + const params: PageParams = { + ...(query["cursor"] ? { cursor: query["cursor"] } : {}), + ...(query["limit"] ? { limit: parseInt(query["limit"], 10) } : {}), + }; + const result = await store.listPreArcSignals(accountId, quarantineCategory as "blocked" | "quarantined", params); + const items = (status === "quarantine_visible" || status === "quarantine_hidden") + ? result.items.filter(s => s.status === status) + : result.items; + return c.json(page("signals", items, result.nextCursor)); + }); + + app.post("/accounts/:accountId/signals/:id/quarantineResponse", async (c) => { + const { accountId } = c.get("auth"); + const signal = await store.getSignal(accountId, c.req.param("id")); + if (!signal) return err(c, 404, "Signal not found", "SIGNAL_NOT_FOUND"); + if (signal.accountId !== accountId) return err(c, 403, "Forbidden"); + if (signal.status !== "blocked" && signal.status !== "quarantine_visible" && signal.status !== "quarantine_hidden") { + return err(c, 400, "Only blocked or quarantined signals can have their status updated", "SIGNAL_NOT_REVIEWABLE"); + } + + const body = await zParse(UpdateSignalStatusRequest, c.req.raw); + + if (body.status === "blocked") { + const updated = await store.blockSignal(accountId, signal.id); + return c.json(updated); + } + + // status === "active": find existing arc or create one, bypassing rule evaluation + const senderDomain = signal.from.address.includes("@") ? signal.from.address.split("@").pop()! : signal.from.address; + const senderETLD1 = getDomain(senderDomain) ?? senderDomain; + const groupingKey = deriveGroupingKey(signal.workflow, signal.workflowData, signal.recipientAddress, senderETLD1); + const matchedArc = groupingKey ? await store.findArcByGroupingKey(accountId, groupingKey) : null; + + const now = new Date().toISOString(); + let arc: Arc; + if (matchedArc) { + arc = matchedArc; + if (signal.receivedAt > arc.lastSignalAt) { + arc = { ...arc, lastSignalAt: signal.receivedAt, updatedAt: now }; + await store.saveArc(arc); + } + } else { + arc = { + id: randomUUID(), + accountId, + workflow: signal.workflow, + labels: [], + status: "active", + summary: signal.summary, + lastSignalAt: signal.receivedAt, + createdAt: now, + updatedAt: now, + ...(groupingKey ? { groupingKey } : {}), + }; + await store.createArc(arc); + } + + await store.unblockSignal(accountId, signal.id, arc.id); + + return c.json({ arc, signal: { ...signal, status: "active", arcId: arc.id } }); + }); + app.get("/accounts/:accountId/signals/:id", async (c) => { const { accountId } = c.get("auth"); const signal = await store.getSignal(accountId, c.req.param("id")); @@ -367,8 +438,8 @@ export function createApp({ store, auth, access, verificationMailer }: AppDeps) if (!signal) return err(c, 404, "Signal not found", "SIGNAL_NOT_FOUND"); if (signal.accountId !== accountId) return err(c, 403, "Forbidden"); if (signal.status !== "draft") return err(c, 400, "Only draft signals can be updated", "SIGNAL_NOT_DRAFT"); - const body = await c.req.json() as Partial>; - const updated = await store.updateSignal(accountId, signal.id, body); + const body = await zParse(UpdateSignalRequest, c.req.raw); + const updated = await store.updateSignal(accountId, signal.id, body as Parameters[2]); return c.json(updated); }); @@ -403,21 +474,10 @@ export function createApp({ store, auth, access, verificationMailer }: AppDeps) return c.json(page("views", views)); }); - app.post("/accounts/:accountId/views/reorder", async (c) => { - const { accountId } = c.get("auth"); - const body = await c.req.json() as { orderedIds: string[] }; - await store.reorderViews(accountId, body.orderedIds); - return c.json({ ok: true }); - }); - app.post("/accounts/:accountId/views", async (c) => { const { accountId } = c.get("auth"); - const body = await c.req.json() as Partial; - if (!body.name) return err(c, 400, "name is required", "MISSING_FIELD", { field: "name" }); - if (body.workflow !== undefined && !VALID_WORKFLOWS.has(body.workflow)) { - return err(c, 400, "Invalid workflow", "INVALID_WORKFLOW", { workflow: body.workflow }); - } - const view = await store.createView(accountId, body as CreateViewRequest); + const body = await zParse(CreateViewRequest, c.req.raw); + const view = await store.createView(accountId, body); return c.json(view, 201); }); @@ -425,7 +485,7 @@ export function createApp({ store, auth, access, verificationMailer }: AppDeps) const { accountId } = c.get("auth"); const view = await store.getView(accountId, c.req.param("id")); if (!view) return err(c, 404, "View not found", "VIEW_NOT_FOUND"); - const body = await c.req.json() as UpdateViewRequest; + const body = await zParse(UpdateViewRequest, c.req.raw); const updated = await store.updateView(accountId, view.id, body); return c.json(updated); }); @@ -450,9 +510,8 @@ export function createApp({ store, auth, access, verificationMailer }: AppDeps) app.post("/accounts/:accountId/labels", async (c) => { const { accountId } = c.get("auth"); - const body = await c.req.json() as Partial; - if (!body.name) return err(c, 400, "name is required", "MISSING_FIELD", { field: "name" }); - const label = await store.createLabel(accountId, body as CreateLabelRequest); + const body = await zParse(CreateLabelRequest, c.req.raw); + const label = await store.createLabel(accountId, body); return c.json(label, 201); }); @@ -461,7 +520,7 @@ export function createApp({ store, auth, access, verificationMailer }: AppDeps) const labels = await store.listLabels(accountId); const label = labels.find((l) => l.id === c.req.param("id")); if (!label) return err(c, 404, "Label not found", "LABEL_NOT_FOUND"); - const body = await c.req.json() as UpdateLabelRequest; + const body = await zParse(UpdateLabelRequest, c.req.raw); const updated = await store.updateLabel(accountId, label.id, body); return c.json(updated); }); @@ -485,23 +544,12 @@ export function createApp({ store, auth, access, verificationMailer }: AppDeps) return c.json(page("rules", rules)); }); - app.post("/accounts/:accountId/rules/reorder", async (c) => { - const { accountId } = c.get("auth"); - const body = await c.req.json() as { orderedIds: string[] }; - await store.reorderRules(accountId, body.orderedIds); - return c.json({ ok: true }); - }); - app.post("/accounts/:accountId/rules", async (c) => { const { accountId } = c.get("auth"); - const body = await c.req.json() as Partial; - if (!body.name) return err(c, 400, "name is required", "MISSING_FIELD", { field: "name" }); - if (!body.actions || body.actions.length === 0) { - return err(c, 400, "actions must not be empty", "MISSING_FIELD", { field: "actions" }); - } - const forwardError = await validateForwardTargets(accountId, body.actions, store); + const body = await zParse(CreateRuleRequest, c.req.raw); + const forwardError = await validateForwardTargets(accountId, body.actions as Rule["actions"], store); if (forwardError) return err(c, 400, forwardError, "UNVERIFIED_FORWARD_TARGET"); - const rule = await store.createRule(accountId, body as CreateRuleRequest); + const rule = await store.createRule(accountId, body as Parameters[1]); return c.json(rule, 201); }); @@ -510,12 +558,12 @@ export function createApp({ store, auth, access, verificationMailer }: AppDeps) const rules = await store.listRules(accountId); const rule = rules.find((r) => r.id === c.req.param("id")); if (!rule) return err(c, 404, "Rule not found", "RULE_NOT_FOUND"); - const body = await c.req.json() as UpdateRuleRequest; + const body = await zParse(UpdateRuleRequest, c.req.raw); if (body.actions) { - const forwardError = await validateForwardTargets(accountId, body.actions, store); + const forwardError = await validateForwardTargets(accountId, body.actions as Rule["actions"], store); if (forwardError) return err(c, 400, forwardError, "UNVERIFIED_FORWARD_TARGET"); } - const updated = await store.updateRule(accountId, rule.id, body); + const updated = await store.updateRule(accountId, rule.id, body as Parameters[2]); return c.json(updated); }); @@ -540,12 +588,20 @@ export function createApp({ store, auth, access, verificationMailer }: AppDeps) app.post("/accounts/:accountId/domains", async (c) => { const { accountId } = c.get("auth"); - const body = await c.req.json() as { domain?: string }; - if (!body.domain) return err(c, 400, "domain is required", "MISSING_FIELD", { field: "domain" }); + const body = await zParse(CreateDomainRequest, c.req.raw); const domain = await store.createDomain(accountId, body.domain); return c.json(domain, 201); }); + app.get("/accounts/:accountId/domains/:id", async (c) => { + const { accountId } = c.get("auth"); + const domain = await store.getDomain(accountId, c.req.param("id")); + if (!domain) return err(c, 404, "Domain not found", "DOMAIN_NOT_FOUND"); + if (domain.accountId !== accountId) return err(c, 403, "Forbidden"); + const records = await checkDomain(domain); + return c.json({ ...domain, records }); + }); + app.get("/accounts/:accountId/domains/:id/records", async (c) => { const { accountId } = c.get("auth"); const domain = await store.getDomain(accountId, c.req.param("id")); @@ -554,6 +610,26 @@ export function createApp({ store, auth, access, verificationMailer }: AppDeps) return c.json(buildDnsRecords(domain)); }); + app.post("/accounts/:accountId/domains/:id/verify", async (c) => { + const { accountId } = c.get("auth"); + const domain = await store.getDomain(accountId, c.req.param("id")); + if (!domain) return err(c, 404, "Domain not found", "DOMAIN_NOT_FOUND"); + if (domain.accountId !== accountId) return err(c, 403, "Forbidden"); + const records = await checkDomain(domain); + const now = new Date().toISOString(); + const failingRecords = records.filter((r) => r.status === "failing").map((r) => r.name); + const receivingHealthy = records.find((r) => r.type === "MX")?.status === "verified"; + const senderHealthy = records.filter((r) => r.type !== "MX").every((r) => r.status === "verified"); + await store.updateDomainHealth(accountId, domain.id, { + receivingHealthy, + senderHealthy, + failingRecords, + lastCheckedAt: now, + ...(failingRecords.length === 0 ? { lastHealthyAt: now } : {}), + }); + return c.json(records); + }); + app.delete("/accounts/:accountId/domains/:id", async (c) => { const { accountId } = c.get("auth"); const domain = await store.getDomain(accountId, c.req.param("id")); @@ -576,8 +652,8 @@ export function createApp({ store, auth, access, verificationMailer }: AppDeps) app.patch("/accounts/:accountId", async (c) => { const { accountId } = c.get("auth"); - const body = await c.req.json() as Partial>; - const updated = await store.updateAccount(accountId, body); + const body = await zParse(UpdateAccountRequest, c.req.raw); + const updated = await store.updateAccount(accountId, body as Partial>); return c.json(updated); }); @@ -595,23 +671,16 @@ export function createApp({ store, auth, access, verificationMailer }: AppDeps) app.post("/accounts/:accountId/users", async (c) => { if (!access) return err(c, 501, "Not implemented"); const { accountId } = c.get("auth"); - const body = await c.req.json() as { userId?: string; role?: string }; - if (!body.userId) return err(c, 400, "userId is required", "MISSING_FIELD", { field: "userId" }); - if (!body.role || !VALID_ROLES.has(body.role as AccountRole)) { - return err(c, 400, "Invalid role", "INVALID_ROLE", { role: body.role }); - } - await access.addUser(accountId, body.userId, body.role as AccountRole); + const body = await zParse(InviteUserRequest, c.req.raw); + await access.addUser(accountId, body.userId, body.role); return c.json({ userId: body.userId, role: body.role }, 201); }); app.patch("/accounts/:accountId/users/:userId", async (c) => { if (!access) return err(c, 501, "Not implemented"); const { accountId } = c.get("auth"); - const body = await c.req.json() as { role?: string }; - if (!body.role || !VALID_ROLES.has(body.role as AccountRole)) { - return err(c, 400, "Invalid role", "INVALID_ROLE", { role: body.role }); - } - await access.updateUserRole(accountId, c.req.param("userId"), body.role as AccountRole); + const body = await zParse(UpdateUserRequest, c.req.raw); + await access.updateUserRole(accountId, c.req.param("userId"), body.role); return c.json({ userId: c.req.param("userId"), role: body.role }); }); @@ -628,7 +697,9 @@ export function createApp({ store, auth, access, verificationMailer }: AppDeps) app.get("/accounts/:accountId/aliases", async (c) => { const { accountId } = c.get("auth"); - const aliases = await store.listAliases(accountId); + const domain = c.req.query("domain"); + let aliases = await store.listAliases(accountId); + if (domain) aliases = aliases.filter(a => a.createdForOrigin?.includes(domain)); return c.json(page("aliases", aliases)); }); @@ -642,12 +713,7 @@ export function createApp({ store, auth, access, verificationMailer }: AppDeps) app.post("/accounts/:accountId/aliases", async (c) => { const { accountId } = c.get("auth"); - const body = await c.req.json() as { - address: string; - filterMode?: SenderFilterMode; - createdForOrigin?: string; - }; - if (!body.address) return err(c, 400, "address is required", "MISSING_FIELD", { field: "address" }); + const body = await zParse(CreateAliasRequest, c.req.raw); const existing = await store.getAlias(accountId, body.address); if (existing) return err(c, 409, "Alias already exists", "ALIAS_EXISTS"); const now = new Date().toISOString(); @@ -655,8 +721,7 @@ export function createApp({ store, auth, access, verificationMailer }: AppDeps) id: randomUUID(), accountId, address: body.address, - filterMode: body.filterMode ?? "notify_new", - approvedSenders: [], + filterMode: body.filterMode ?? "quarantine_visible", ...(body.createdForOrigin !== undefined ? { createdForOrigin: body.createdForOrigin } : {}), createdAt: now, updatedAt: now, @@ -667,20 +732,18 @@ export function createApp({ store, auth, access, verificationMailer }: AppDeps) app.patch("/accounts/:accountId/aliases/:address", async (c) => { const { accountId } = c.get("auth"); const address = decodeURIComponent(c.req.param("address")); - const body = await c.req.json() as { - filterMode?: SenderFilterMode; - approvedSenders?: string[]; - spamScoreThreshold?: number; - createdForOrigin?: string; - }; + const body = await zParse(UpdateAliasRequest, c.req.raw); + if (body.newAddress) { + const renamed = await store.renameAlias(accountId, address, body.newAddress); + return c.json(renamed); + } const existing = await store.getAlias(accountId, address); const now = new Date().toISOString(); const updated = await store.upsertAlias({ id: existing?.id ?? randomUUID(), accountId, address, - filterMode: body.filterMode ?? existing?.filterMode ?? "notify_new", - approvedSenders: body.approvedSenders ?? existing?.approvedSenders ?? [], + filterMode: body.filterMode ?? existing?.filterMode ?? "quarantine_visible", ...(body.spamScoreThreshold !== undefined ? { spamScoreThreshold: body.spamScoreThreshold } : existing?.spamScoreThreshold !== undefined ? { spamScoreThreshold: existing.spamScoreThreshold } : {}), ...(body.createdForOrigin !== undefined ? { createdForOrigin: body.createdForOrigin } : existing?.createdForOrigin !== undefined ? { createdForOrigin: existing.createdForOrigin } : {}), createdAt: existing?.createdAt ?? now, @@ -696,6 +759,72 @@ export function createApp({ store, auth, access, verificationMailer }: AppDeps) return new Response(null, { status: 204 }); }); + // ------------------------------------------------------------------------- + // Alias Senders — /accounts/:accountId/aliases/:address/senders + // ------------------------------------------------------------------------- + + app.get("/accounts/:accountId/aliases/:address/senders", async (c) => { + const { accountId } = c.get("auth"); + const address = decodeURIComponent(c.req.param("address")); + const senders = await store.listSenders(accountId, address); + return c.json({ senders }); + }); + + app.post("/accounts/:accountId/aliases/:address/senders", async (c) => { + const { accountId } = c.get("auth"); + const address = decodeURIComponent(c.req.param("address")); + const body = await zParse(CreateSenderRequest, c.req.raw); + await store.saveSender(accountId, address, body.domain, body.mode); + return new Response(null, { status: 201 }); + }); + + app.delete("/accounts/:accountId/aliases/:address/senders/:domain", async (c) => { + const { accountId } = c.get("auth"); + const address = decodeURIComponent(c.req.param("address")); + const domain = decodeURIComponent(c.req.param("domain")); + await store.removeSender(accountId, address, domain); + return new Response(null, { status: 204 }); + }); + + // ------------------------------------------------------------------------- + // Email Templates — /accounts/:accountId/templates + // ------------------------------------------------------------------------- + + app.get("/accounts/:accountId/templates", async (c) => { + const { accountId } = c.get("auth"); + const templates = await store.listTemplates(accountId); + return c.json({ templates }); + }); + + app.post("/accounts/:accountId/templates", async (c) => { + const { accountId } = c.get("auth"); + const body = await zParse(CreateTemplateRequest, c.req.raw); + const now = new Date().toISOString(); + const template = await store.createTemplate({ + id: randomUUID(), accountId, name: body.name, subject: body.subject, body: body.body, + createdAt: now, updatedAt: now, + }); + return c.json(template, 201); + }); + + app.patch("/accounts/:accountId/templates/:id", async (c) => { + const { accountId } = c.get("auth"); + const body = await zParse(UpdateTemplateRequest, c.req.raw); + const existing = await store.getTemplate(accountId, c.req.param("id")); + if (!existing) return err(c, 404, "Template not found", "TEMPLATE_NOT_FOUND"); + const updated = await store.updateTemplate(accountId, c.req.param("id"), body as Parameters[2]); + return c.json(updated); + }); + + app.delete("/accounts/:accountId/templates/:id", async (c) => { + const { accountId } = c.get("auth"); + const existing = await store.getTemplate(accountId, c.req.param("id")); + if (!existing) return err(c, 404, "Template not found", "TEMPLATE_NOT_FOUND"); + await store.deleteTemplate(accountId, c.req.param("id")); + return new Response(null, { status: 204 }); + }); + + // ------------------------------------------------------------------------- // Verified forwarding addresses — /accounts/:accountId/forwarding-addresses // ------------------------------------------------------------------------- @@ -708,8 +837,7 @@ export function createApp({ store, auth, access, verificationMailer }: AppDeps) app.post("/accounts/:accountId/forwarding-addresses", async (c) => { const { accountId } = c.get("auth"); - const body = await c.req.json() as { address?: string }; - if (!body.address) return err(c, 400, "address is required", "MISSING_FIELD", { field: "address" }); + const body = await zParse(CreateForwardingAddressRequest, c.req.raw); const existing = await store.getVerifiedForwardingAddress(accountId, body.address); if (existing?.status === "verified") return c.json(existing, 200); @@ -738,8 +866,7 @@ export function createApp({ store, auth, access, verificationMailer }: AppDeps) app.post("/accounts/:accountId/forwarding-addresses/:address/verify", async (c) => { const { accountId } = c.get("auth"); const address = decodeURIComponent(c.req.param("address")); - const body = await c.req.json() as { token?: string }; - if (!body.token) return err(c, 400, "token is required", "MISSING_FIELD", { field: "token" }); + const body = await zParse(VerifyForwardingAddressRequest, c.req.raw); const existing = await store.getVerifiedForwardingAddress(accountId, address); if (!existing) return err(c, 404, "Forwarding address not found", "FORWARDING_ADDRESS_NOT_FOUND"); @@ -759,20 +886,16 @@ export function createApp({ store, auth, access, verificationMailer }: AppDeps) }); // ------------------------------------------------------------------------- - // Search — /accounts/:accountId/search + // Audit — /accounts/:accountId/audit // ------------------------------------------------------------------------- - app.get("/accounts/:accountId/search", async (c) => { + app.get("/accounts/:accountId/audit", async (c) => { const { accountId } = c.get("auth"); - const q = c.req.query("q"); - if (!q) return err(c, 400, "q is required", "MISSING_FIELD", { field: "q" }); - const query = c.req.query(); - const params: PageParams = { - ...(query["cursor"] ? { cursor: query["cursor"] } : {}), - ...(query["limit"] ? { limit: parseInt(query["limit"], 10) } : {}), - }; - const result = await store.searchArcs(accountId, q, params); - return c.json(page("arcs", result.items, result.nextCursor)); + const cursor = c.req.query("cursor"); + const rawLimit = c.req.query("limit"); + const params: PageParams = { ...(cursor ? { cursor } : {}), ...(rawLimit ? { limit: parseInt(rawLimit, 10) } : {}) }; + const result = await store.listAuditEvents(accountId, params); + return c.json(result); }); return app; @@ -782,10 +905,6 @@ export function createApp({ store, auth, access, verificationMailer }: AppDeps) // Helpers // --------------------------------------------------------------------------- -import { WORKFLOWS } from "../types/index.js"; -const VALID_WORKFLOWS = new Set(WORKFLOWS); -const VALID_ROLES = new Set(["owner", "admin", "member", "viewer"]); - // Validate that all forward targets in a rule's actions are verified for this account. // Returns an error string if invalid, null if OK. async function validateForwardTargets( diff --git a/src/api/requests.ts b/src/api/requests.ts new file mode 100644 index 0000000..0466604 --- /dev/null +++ b/src/api/requests.ts @@ -0,0 +1,228 @@ +import { z } from "zod"; +import { WORKFLOWS } from "../types/index.js"; + +// ---- Shared primitives ---- + +const SenderFilterMode = z.enum(["allow_all", "quarantine_visible", "quarantine_hidden", "block"]); +const ArcStatus = z.enum(["active", "archived", "deleted"]); +const ArcUrgency = z.enum(["critical", "high", "normal", "low", "silent"]); +const Workflow = z.enum(WORKFLOWS); +const SortField = z.enum(["lastSignalAt", "createdAt"]); +const SortDirection = z.enum(["asc", "desc"]); +const NewAddressHandling = z.enum(["auto_allow", "block_until_approved"]); +const AccountRole = z.enum(["owner", "admin", "member", "viewer"]); +const RuleActionType = z.enum([ + "assign_label", "assign_workflow", "archive", "delete", "forward", + "block", "quarantine", "quarantine_hidden", "set_urgency", "suppress_notification", "pong", "approve_sender", + "auto_reply", "auto_draft", +]); +const RuleStatus = z.enum(["enabled", "disabled"]); + +const EmailAddressSchema = z.object({ + address: z.string(), + name: z.string().optional(), +}); + +const RuleActionSchema = z.object({ + type: RuleActionType, + value: z.string().optional(), + disabled: z.boolean().optional(), +}); + +// ---- Arc ---- + +export const UpdateArcRequest = z.object({ + status: ArcStatus.optional(), + urgency: ArcUrgency.optional(), + labels: z.array(z.string()).optional(), +}); +export type UpdateArcRequest = z.infer; + +export const CreateArcFromSignalRequest = z.object({ + signalId: z.string(), + approveSender: z.boolean().optional(), + updateFilterMode: SenderFilterMode.optional(), +}); +export type CreateArcFromSignalRequest = z.infer; + +// ---- Signal ---- + +export const UpdateSignalStatusRequest = z.object({ + status: z.enum(["active", "blocked"]), +}); +export type UpdateSignalStatusRequest = z.infer; + +export const UpdateSignalRequest = z.object({ + subject: z.string().optional(), + textBody: z.string().optional(), + from: EmailAddressSchema.optional(), + to: z.array(EmailAddressSchema).optional(), +}); +export type UpdateSignalRequest = z.infer; + +// ---- View ---- + +export const CreateViewRequest = z.object({ + name: z.string(), + workflow: Workflow.optional(), + labels: z.array(z.string()).optional(), + sortField: SortField.optional(), + sortDirection: SortDirection.optional(), + icon: z.string().optional(), + color: z.string().optional(), + position: z.number().optional(), +}); +export type CreateViewRequest = z.infer; + +export const UpdateViewRequest = z.object({ + name: z.string().optional(), + workflow: Workflow.optional(), + labels: z.array(z.string()).optional(), + sortField: SortField.optional(), + sortDirection: SortDirection.optional(), + icon: z.string().optional(), + color: z.string().optional(), + position: z.number().optional(), +}); +export type UpdateViewRequest = z.infer; + +// ---- Label ---- + +export const CreateLabelRequest = z.object({ + name: z.string(), + color: z.string().optional(), + icon: z.string().optional(), +}); +export type CreateLabelRequest = z.infer; + +export const UpdateLabelRequest = z.object({ + name: z.string().optional(), + color: z.string().optional(), + icon: z.string().optional(), +}); +export type UpdateLabelRequest = z.infer; + +// ---- Rule ---- + +export const CreateRuleRequest = z.object({ + name: z.string(), + condition: z.string().max(10_240).optional(), + actions: z.array(RuleActionSchema).min(1), + priorityOrder: z.number().int().min(0).optional(), + tags: z.record(z.string(), z.string()).optional(), +}); +export type CreateRuleRequest = z.infer; + +export const UpdateRuleRequest = z.object({ + name: z.string().optional(), + condition: z.string().max(10_240).optional(), + actions: z.array(RuleActionSchema).optional(), + priorityOrder: z.number().int().min(0).optional(), + status: RuleStatus.optional(), + tags: z.record(z.string(), z.string()).optional(), +}); +export type UpdateRuleRequest = z.infer; + +// ---- Domain ---- + +export const CreateDomainRequest = z.object({ + domain: z.string(), +}); +export type CreateDomainRequest = z.infer; + +// ---- Alias ---- + +export const CreateAliasRequest = z.object({ + address: z.string(), + filterMode: SenderFilterMode.optional(), + createdForOrigin: z.string().optional(), +}); +export type CreateAliasRequest = z.infer; + +export const UpdateAliasRequest = z.object({ + newAddress: z.string().email().optional(), + filterMode: SenderFilterMode.optional(), + spamScoreThreshold: z.number().min(0).max(1).optional(), + createdForOrigin: z.string().optional(), +}); +export type UpdateAliasRequest = z.infer; + +// ---- Alias Senders ---- + +export const CreateSenderRequest = z.object({ + domain: z.string(), + mode: z.enum(["allow", "block"]), +}); +export type CreateSenderRequest = z.infer; + +// ---- Email Templates ---- + +export const CreateTemplateRequest = z.object({ + name: z.string().min(1), + subject: z.string(), + body: z.string(), +}); +export type CreateTemplateRequest = z.infer; + +export const UpdateTemplateRequest = z.object({ + name: z.string().min(1).optional(), + subject: z.string().optional(), + body: z.string().optional(), +}); +export type UpdateTemplateRequest = z.infer; + +// ---- Account ---- + +const EmailNotificationSettingsSchema = z.object({ + enabled: z.boolean(), + address: z.string(), + frequency: z.enum(["instant", "hourly", "daily"]), +}); + +const PushNotificationSettingsSchema = z.object({ + enabled: z.boolean(), +}); + +const NotificationSettingsSchema = z.object({ + email: EmailNotificationSettingsSchema.optional(), + push: PushNotificationSettingsSchema.optional(), +}); + +const AccountFilteringConfigSchema = z.object({ + defaultFilterMode: SenderFilterMode.optional(), + newAddressHandling: NewAddressHandling.optional(), + spamScoreThreshold: z.number().min(0).max(1).optional(), +}).passthrough(); + +export const UpdateAccountRequest = z.object({ + name: z.string().optional(), + deletionRetentionDays: z.number().int().positive().optional(), + notifications: NotificationSettingsSchema.optional(), + filtering: AccountFilteringConfigSchema.optional(), +}); +export type UpdateAccountRequest = z.infer; + +// ---- Forwarding addresses ---- + +export const CreateForwardingAddressRequest = z.object({ + address: z.string(), +}); +export type CreateForwardingAddressRequest = z.infer; + +export const VerifyForwardingAddressRequest = z.object({ + token: z.string(), +}); +export type VerifyForwardingAddressRequest = z.infer; + +// ---- Users ---- + +export const InviteUserRequest = z.object({ + userId: z.string(), + role: AccountRole, +}); +export type InviteUserRequest = z.infer; + +export const UpdateUserRequest = z.object({ + role: AccountRole, +}); +export type UpdateUserRequest = z.infer; diff --git a/src/api/validate.ts b/src/api/validate.ts new file mode 100644 index 0000000..add04af --- /dev/null +++ b/src/api/validate.ts @@ -0,0 +1,17 @@ +import { z } from "zod"; +import { HTTPException } from "hono/http-exception"; + +export async function zParse(schema: z.ZodType, req: Request): Promise { + let raw: unknown; + try { raw = await req.json(); } catch { raw = null; } + const result = schema.safeParse(raw); + if (!result.success) { + throw new HTTPException(400, { + res: Response.json( + { title: "Invalid request body", errorCode: "INVALID_REQUEST", details: result.error.flatten() }, + { status: 400 }, + ), + }); + } + return result.data; +} diff --git a/src/classifier/classifier.ts b/src/classifier/classifier.ts index 906fcf4..ab11f35 100644 --- a/src/classifier/classifier.ts +++ b/src/classifier/classifier.ts @@ -163,17 +163,13 @@ const CLASSIFICATION_SYSTEM_PROMPT = `You are an email workflow classification e OTPs, password resets, magic links, email verification, 2FA codes { "workflow": "auth", "authType": "otp"|"password_reset"|"magic_link"|"verification"|"two_factor"|"other", "code": "", "expiresInMinutes": , "service": "", "actionUrl": "" } -### invoice -Invoices, receipts, billing statements, payment confirmations, refund notices -{ "workflow": "invoice", "invoiceType": "invoice"|"receipt"|"statement"|"payment_confirmation"|"refund", "vendor": "", "amount": , "currency": "", "invoiceNumber": "", "dueDate": "", "lineItems": [], "downloadUrl": "" } +### payments +Invoices, receipts, billing statements, payment confirmations, refunds, bank statements, wire transfers, transaction alerts, subscription renewals, trial expiry, payment failures, plan changes, tax documents +{ "workflow": "payments", "paymentType": "invoice"|"receipt"|"subscription_renewal"|"payment_failed"|"plan_changed"|"tax"|"wire_transfer"|"refund"|"statement"|"other", "vendor": "", "amount": , "currency": "", "invoiceNumber": "", "dueDate": "", "accountLastFour": "", "downloadUrl": "", "managementUrl": "" } -### order +### package Order confirmations, shipping updates, delivery notifications, returns, refunds, cancellations -{ "workflow": "order", "orderType": "confirmation"|"shipping"|"out_for_delivery"|"delivered"|"return"|"refund"|"cancellation", "retailer": "", "orderNumber": "", "trackingNumber": "", "trackingUrl": "", "estimatedDelivery": "", "items": [], "totalAmount": , "currency": "" } - -### financial -Bank statements, wire transfers, transaction alerts, fraud alerts, tax documents -{ "workflow": "financial", "financialType": "statement"|"transaction"|"alert"|"transfer"|"tax"|"fraud_alert", "institution": "", "amount": , "currency": "", "accountLastFour": "", "transactionDate": "", "statementPeriod": "", "isSuspicious": true|false } +{ "workflow": "package", "packageType": "confirmation"|"shipping"|"out_for_delivery"|"delivered"|"return"|"refund"|"cancellation", "retailer": "", "orderNumber": "", "trackingNumber": "", "trackingUrl": "", "estimatedDelivery": "", "items": [], "totalAmount": , "currency": "" } ### travel Flight bookings, hotel reservations, car rentals, itineraries, check-in reminders, boarding passes @@ -183,33 +179,25 @@ Flight bookings, hotel reservations, car rentals, itineraries, check-in reminder Job applications, recruiter outreach, interview scheduling, offers, rejections, job postings { "workflow": "job", "jobType": "application_status"|"recruiter_outreach"|"interview_request"|"offer"|"rejection"|"job_posting", "company": "", "role": "", "location": "", "salary": "", "interviewDate": "", "applicationStatus": "submitted"|"reviewing"|"interview"|"offer"|"rejected"|null, "actionUrl": "" } -### newsletter -Publications, editorial digests, blog mailing lists, curated content -{ "workflow": "newsletter", "publication": "", "topics": [""], "frequency": "daily"|"weekly"|"monthly"|"irregular"|null, "unsubscribeUrl": "" } - -### promotions -Promotional offers, discount codes, flash sales, abandoned cart reminders, loyalty rewards, product launches — commercial emails whose primary goal is driving a purchase or re-engagement with an offer -{ "workflow": "promotions", "promotionType": "discount"|"sale"|"flash_sale"|"loyalty"|"referral"|"product_launch"|"abandoned_cart"|"win_back", "brand": "", "discountCode": "", "discountAmount": "", "expiryDate": "", "shopUrl": "" } +### content +Publications, editorial digests, blog mailing lists, promotional offers, discount codes, flash sales, abandoned cart reminders, loyalty rewards, product launches, social media digests — commercial or editorial bulk emails +{ "workflow": "content", "contentType": "newsletter"|"promotion"|"social_digest"|"product_update"|"announcement", "publisher": "", "topics": [""], "discountCode": "", "discountAmount": "", "expiryDate": "", "unsubscribeUrl": "" } ### onboarding -Welcome emails, account setup guides, getting-started tutorials, feature tours, product tips, and re-engagement check-ins sent by apps or services after a user signs up. Distinct from promotions (no purchase intent) and newsletters (not editorial). +Welcome emails, account setup guides, getting-started tutorials, feature tours, product tips, and re-engagement check-ins sent by apps or services after a user signs up. Distinct from content (no purchase intent) and from support (no ticket). { "workflow": "onboarding", "service": "", "onboardingType": "welcome"|"setup_guide"|"feature_tour"|"tip"|"check_in"|"re_engagement", "stepNumber": , "totalSteps": , "actionUrl": "" } -### social -Social media notifications, community platforms, forum activity, event invites -{ "workflow": "social", "platform": "", "notificationType": "mention"|"follow"|"message"|"like"|"comment"|"friend_request"|"digest"|"event", "actorName": "", "contentPreview": "", "actionUrl": "" } - ### crm Sales outreach, business proposals, client emails, contract follow-ups { "workflow": "crm", "crmType": "sales_outreach"|"follow_up"|"client_message"|"proposal"|"contract"|"support", "senderCompany": "", "senderRole": "", "dealValue": , "currency": "", "urgency": "low"|"medium"|"high", "requiresReply": true|false } -### personal +### conversation Direct human-to-human correspondence not generated by automated systems -{ "workflow": "personal", "senderName": "", "isReply": true|false, "threadLength": , "sentiment": "positive"|"neutral"|"negative"|"urgent", "requiresReply": true|false } +{ "workflow": "conversation", "senderName": "", "isReply": true|false, "threadLength": , "sentiment": "positive"|"neutral"|"negative"|"urgent", "requiresReply": true|false } -### security -Suspicious login alerts, new device notifications, breach notices, API key exposure, account lockout -{ "workflow": "security", "alertType": "suspicious_login"|"new_device"|"password_changed"|"breach_notice"|"api_key_exposed"|"account_locked"|"other", "service": "", "ipAddress": "", "location": "", "deviceName": "", "requiresAction": true|false, "actionUrl": "" } +### alert +Suspicious login alerts, new device notifications, breach notices, API key exposure, account lockout, fraud alerts, CI/CD failures, deployment failures, error monitoring alerts, domain/certificate expiry, security scan results — anything that signals a system event requiring attention +{ "workflow": "alert", "alertType": "suspicious_login"|"new_device"|"password_changed"|"breach_notice"|"api_key_exposed"|"account_locked"|"fraud_alert"|"ci_failure"|"deployment_failed"|"error_spike"|"domain_expiry"|"cert_expiry"|"security_scan"|"other", "service": "", "severity": "info"|"warning"|"critical"|null, "requiresAction": true|false, "actionUrl": "", "ipAddress": "", "location": "", "deviceName": "", "repository": "", "errorMessage": "" } ### scheduling Calendar invites, appointment confirmations, meeting reminders, cancellations, reschedule requests @@ -219,25 +207,18 @@ Calendar invites, appointment confirmations, meeting reminders, cancellations, r Customer support ticket updates, helpdesk responses, service status notifications { "workflow": "support", "eventType": "ticket_opened"|"ticket_updated"|"ticket_resolved"|"ticket_closed"|"awaiting_response"|"status_update", "ticketId": "", "service": "", "priority": "low"|"normal"|"high"|"urgent"|null, "agentName": "", "responseUrl": "" } -### developer -GitHub/GitLab PRs and reviews, CI/CD results, error monitoring alerts, domain/certificate expiry -{ "workflow": "developer", "platform": "github"|"gitlab"|"bitbucket"|"jira"|"sentry"|"datadog"|"pagerduty"|"vercel"|"aws"|"cloudflare"|"other", "eventType": "pull_request"|"code_review"|"ci_failure"|"ci_success"|"deployment"|"error_alert"|"domain_expiry"|"cert_expiry"|"security_scan"|"other", "repository": "", "severity": "info"|"warning"|"critical"|null, "requiresAction": true|false, "actionUrl": "" } - -### subscription -SaaS subscription renewals, trial expiry, payment failures, plan changes, cancellations -{ "workflow": "subscription", "eventType": "renewal"|"trial_expiring"|"payment_failed"|"plan_changed"|"cancelled"|"reactivated"|"usage_alert", "service": "", "planName": "", "amount": , "currency": "", "nextBillingDate": "", "trialEndsAt": "", "managementUrl": "" } - ### healthcare Medical appointment reminders, test results, prescription notifications, insurance updates { "workflow": "healthcare", "eventType": "appointment_reminder"|"appointment_confirmation"|"test_results"|"prescription"|"insurance_update"|"billing"|"referral", "provider": "", "appointmentDate": "", "location": "", "requiresAction": true|false, "portalUrl": "" } -### government -Tax notices, benefits updates, license renewals, official government correspondence -{ "workflow": "government", "agency": "", "documentType": "tax"|"benefits"|"license"|"permit"|"notice"|"fine"|"voting"|"healthcare"|"other", "referenceNumber": "", "deadlineDate": "", "requiresResponse": true|false, "portalUrl": "" } +### status +Passive informational notices that users are not expected to act on: privacy policy changes, terms of service updates, data processor notices, cookie policy updates, GDPR/compliance notices, phishing-warning bulletins from banks/SaaS ("we will never ask for your password"), and official government correspondence (tax notices, benefits updates, license renewals). + +IMPORTANT — phishing-warning notices vs actual phishing: +- "Beware of phishing — we will never ask for your password" from a bank → workflow:"status", statusType:"compliance", spamScore:0.0–0.1 +- An email pretending to be a bank and asking you to click a suspicious link → assign the real workflow (e.g. "auth") with spamScore:0.8–1.0 -### notice -Privacy policy changes, terms of service updates, data processor changes, cookie policy updates, GDPR/compliance notices — bulk regulatory emails sent by automated systems that users are not expected to read or act on -{ "workflow": "notice", "noticeType": "privacy_policy"|"terms_update"|"data_processor"|"cookie_policy"|"compliance"|"other", "provider": "", "effectiveDate": "", "documentUrl": "" } +{ "workflow": "status", "statusType": "terms_update"|"privacy_policy"|"data_processor"|"cookie_policy"|"compliance"|"service_notice"|"government"|"account_notification"|"other", "provider": "", "effectiveDate": "", "referenceNumber": "", "documentUrl": "" } ### test Emails sent by a user to test that their own inbox is working. Detected by obvious test content (subject "test", "testing 123", "hello world", "is this thing on?" etc.) or by the processor overriding the workflow based on sender identity. The processor handles sender-based detection; classify here only when content makes the intent unambiguous. @@ -246,9 +227,9 @@ Emails sent by a user to test that their own inbox is working. Detected by obvio ## Spam scoring spamScore is ALWAYS required and is orthogonal to workflow. Assign the real workflow even for spam: - A phishing email pretending to be a bank login → workflow:"auth", spamScore:0.95 -- A scam pretending to be a shipping update → workflow:"order", spamScore:0.9 -- Unsolicited bulk marketing → workflow:"promotions", spamScore:0.7 -- A legitimate newsletter → workflow:"newsletter", spamScore:0.05 +- A scam pretending to be a shipping update → workflow:"package", spamScore:0.9 +- Unsolicited bulk marketing → workflow:"content", spamScore:0.7 +- A legitimate newsletter → workflow:"content", spamScore:0.05 Score ranges: - 0.0–0.2: Clearly legitimate diff --git a/src/database/account-database.ts b/src/database/account-database.ts index fa9b9bf..fbf4407 100644 --- a/src/database/account-database.ts +++ b/src/database/account-database.ts @@ -1,7 +1,7 @@ import { randomUUID } from "crypto"; import { DeleteCommand, GetCommand, PutCommand, QueryCommand, UpdateCommand } from "@aws-sdk/lib-dynamodb"; import { dynamo, ACCOUNTS_TABLE } from "./shared.js"; -import type { Account, View, Label, Rule, Domain, Alias, AccountFilteringConfig, VerifiedForwardingAddress } from "../types/index.js"; +import type { Account, View, Label, Rule, RuleStatus, Domain, Alias, AliasSender, SenderMode, AccountFilteringConfig, VerifiedForwardingAddress, EmailTemplate, WsConnection } from "../types/index.js"; import type { CreateViewRequest, UpdateViewRequest, CreateLabelRequest, UpdateLabelRequest, CreateRuleRequest, UpdateRuleRequest } from "../api/app.js"; // --------------------------------------------------------------------------- @@ -10,6 +10,11 @@ import type { CreateViewRequest, UpdateViewRequest, CreateLabelRequest, UpdateLa const pk = (accountId: string) => `ACCT#${accountId}`; +function ruleGsi1pk(accountId: string) { return `ACCT#${accountId}`; } +function ruleGsi1sk(status: RuleStatus, priorityOrder: number, id: string) { + return `RULE#${status}#${String(priorityOrder).padStart(6, "0")}#${id}`; +} + // --------------------------------------------------------------------------- // AccountDatabase // Owns: Account record, Aliases, Views, Labels, Rules, Domains @@ -94,6 +99,73 @@ export class AccountDatabase { })); } + async renameAlias(accountId: string, oldAddress: string, newAddress: string): Promise { + const [old, senders] = await Promise.all([ + this.getAlias(accountId, oldAddress), + this.listSenders(accountId, oldAddress), + ]); + if (!old) throw new Error("Alias not found"); + const renamed: Alias = { ...old, address: newAddress, updatedAt: new Date().toISOString() }; + await this.saveAlias(renamed); + await Promise.all(senders.map(s => this.saveSender(accountId, newAddress, s.domain, s.mode))); + await this.deleteAlias(accountId, oldAddress); + await Promise.all(senders.map(s => this.removeSender(accountId, oldAddress, s.domain))); + return renamed; + } + + // --------------------------------------------------------------------------- + // Alias Senders (per-alias allowed/blocked sender domains) + // sk = SENDER#${address}#${domain} — distinct prefix from alias items + // --------------------------------------------------------------------------- + + async saveSender(accountId: string, address: string, domain: string, mode: SenderMode): Promise { + await dynamo.send(new PutCommand({ + TableName: ACCOUNTS_TABLE, + Item: { + pk: pk(accountId), + sk: `SENDER#${address}#${domain}`, + gsi1pk: `SENDERS#${accountId}#${domain}`, + gsi1sk: `ALIAS#${address}`, + accountId, aliasAddress: address, domain, mode, + addedAt: new Date().toISOString(), + }, + })); + } + + async removeSender(accountId: string, address: string, domain: string): Promise { + await dynamo.send(new DeleteCommand({ + TableName: ACCOUNTS_TABLE, + Key: { pk: pk(accountId), sk: `SENDER#${address}#${domain}` }, + })); + } + + async getSender(accountId: string, address: string, domain: string): Promise { + const res = await dynamo.send(new GetCommand({ + TableName: ACCOUNTS_TABLE, + Key: { pk: pk(accountId), sk: `SENDER#${address}#${domain}` }, + })); + return res.Item ? (res.Item as AliasSender) : null; + } + + async listSenders(accountId: string, address: string): Promise { + const res = await dynamo.send(new QueryCommand({ + TableName: ACCOUNTS_TABLE, + KeyConditionExpression: "pk = :pk AND begins_with(sk, :prefix)", + ExpressionAttributeValues: { ":pk": pk(accountId), ":prefix": `SENDER#${address}#` }, + })); + return (res.Items ?? []) as AliasSender[]; + } + + async listAliasesForDomain(accountId: string, domain: string): Promise { + const res = await dynamo.send(new QueryCommand({ + TableName: ACCOUNTS_TABLE, + IndexName: "gsi1", + KeyConditionExpression: "gsi1pk = :pk", + ExpressionAttributeValues: { ":pk": `SENDERS#${accountId}#${domain}` }, + })); + return (res.Items ?? []) as AliasSender[]; + } + async getAccountFilteringConfig(accountId: string): Promise { const account = await this.getAccount(accountId); return account?.filtering ?? null; @@ -267,39 +339,78 @@ export class AccountDatabase { async listRules(accountId: string): Promise { const res = await dynamo.send(new QueryCommand({ TableName: ACCOUNTS_TABLE, - KeyConditionExpression: "pk = :pk AND begins_with(sk, :prefix)", - ExpressionAttributeValues: { ":pk": pk(accountId), ":prefix": "RULE#" }, + IndexName: "gsi1", + KeyConditionExpression: "gsi1pk = :pk AND begins_with(gsi1sk, :prefix)", + ExpressionAttributeValues: { ":pk": ruleGsi1pk(accountId), ":prefix": "RULE#" }, })); - return ((res.Items ?? []) as Rule[]).sort((a, b) => a.position - b.position); + return (res.Items ?? []) as Rule[]; + } + + async listEnabledRules(accountId: string): Promise { + const res = await dynamo.send(new QueryCommand({ + TableName: ACCOUNTS_TABLE, + IndexName: "gsi1", + KeyConditionExpression: "gsi1pk = :pk AND begins_with(gsi1sk, :prefix)", + ExpressionAttributeValues: { ":pk": ruleGsi1pk(accountId), ":prefix": "RULE#enabled#" }, + })); + return (res.Items ?? []) as Rule[]; } async createRule(accountId: string, data: CreateRuleRequest): Promise { - const rules = await this.listRules(accountId); + const allRules = await this.listRules(accountId); + const userRules = allRules.filter((r) => r.priorityOrder >= 100); const now = new Date().toISOString(); const rule: Rule = { id: randomUUID(), accountId, name: data.name, - condition: data.condition, - actions: data.actions, - position: data.position ?? (rules.length > 0 ? Math.max(...rules.map((r) => r.position)) + 1 : 0), + condition: data.condition ?? "", + actions: data.actions as Rule["actions"], + status: "enabled", + priorityOrder: data.priorityOrder ?? (userRules.length > 0 ? Math.max(...userRules.map((r) => r.priorityOrder)) + 1 : 100), + ...(data.tags !== undefined ? { tags: data.tags } : {}), createdAt: now, updatedAt: now, }; - await dynamo.send(new PutCommand({ TableName: ACCOUNTS_TABLE, Item: { ...rule, pk: pk(accountId), sk: `RULE#${rule.id}` } })); + await dynamo.send(new PutCommand({ + TableName: ACCOUNTS_TABLE, + Item: { + ...rule, + pk: pk(accountId), sk: `RULE#${rule.id}`, + gsi1pk: ruleGsi1pk(accountId), gsi1sk: ruleGsi1sk(rule.status, rule.priorityOrder, rule.id), + }, + })); return rule; } async updateRule(accountId: string, id: string, data: UpdateRuleRequest): Promise { + const current = await dynamo.send(new GetCommand({ + TableName: ACCOUNTS_TABLE, + Key: { pk: pk(accountId), sk: `RULE#${id}` }, + })); + const existing = current.Item as Rule; const now = new Date().toISOString(); - const setParts: string[] = ["updatedAt = :now"]; - const exprValues: Record = { ":now": now }; + const mergedStatus = data.status ?? existing.status; + const mergedPriority = data.priorityOrder ?? existing.priorityOrder; + + const setParts: string[] = [ + "updatedAt = :now", + "gsi1pk = :g1pk", + "gsi1sk = :g1sk", + ]; + const exprValues: Record = { + ":now": now, + ":g1pk": ruleGsi1pk(accountId), + ":g1sk": ruleGsi1sk(mergedStatus, mergedPriority, id), + }; const exprNames: Record = {}; if (data.name !== undefined) { setParts.push("#name = :name"); exprValues[":name"] = data.name; exprNames["#name"] = "name"; } if (data.condition !== undefined) { setParts.push("#cond = :cond"); exprValues[":cond"] = data.condition; exprNames["#cond"] = "condition"; } if (data.actions !== undefined) { setParts.push("actions = :actions"); exprValues[":actions"] = data.actions; } - if (data.position !== undefined) { setParts.push("#pos = :pos"); exprValues[":pos"] = data.position; exprNames["#pos"] = "position"; } + if (data.priorityOrder !== undefined) { setParts.push("#pri = :pri"); exprValues[":pri"] = data.priorityOrder; exprNames["#pri"] = "priorityOrder"; } + if (data.status !== undefined) { setParts.push("#status = :status"); exprValues[":status"] = data.status; exprNames["#status"] = "status"; } + if (data.tags !== undefined) { setParts.push("tags = :tags"); exprValues[":tags"] = data.tags; } await dynamo.send(new UpdateCommand({ TableName: ACCOUNTS_TABLE, @@ -308,26 +419,18 @@ export class AccountDatabase { ExpressionAttributeValues: exprValues, ...(Object.keys(exprNames).length ? { ExpressionAttributeNames: exprNames } : {}), })); - const rules = await this.listRules(accountId); - return rules.find((r) => r.id === id)!; + + const updated = await dynamo.send(new GetCommand({ + TableName: ACCOUNTS_TABLE, + Key: { pk: pk(accountId), sk: `RULE#${id}` }, + })); + return updated.Item as Rule; } async deleteRule(accountId: string, id: string): Promise { await dynamo.send(new DeleteCommand({ TableName: ACCOUNTS_TABLE, Key: { pk: pk(accountId), sk: `RULE#${id}` } })); } - async reorderRules(accountId: string, orderedIds: string[]): Promise { - await Promise.all(orderedIds.map((id, position) => - dynamo.send(new UpdateCommand({ - TableName: ACCOUNTS_TABLE, - Key: { pk: pk(accountId), sk: `RULE#${id}` }, - UpdateExpression: "SET #pos = :pos", - ExpressionAttributeNames: { "#pos": "position" }, - ExpressionAttributeValues: { ":pos": position }, - })), - )); - } - // --------------------------------------------------------------------------- // Domains // --------------------------------------------------------------------------- @@ -370,6 +473,57 @@ export class AccountDatabase { await dynamo.send(new DeleteCommand({ TableName: ACCOUNTS_TABLE, Key: { pk: pk(accountId), sk: `DOMAIN#${id}` } })); } + async updateDomainHealth(accountId: string, id: string, health: { + receivingHealthy: boolean; + senderHealthy: boolean; + failingRecords: string[]; + lastCheckedAt: string; + lastHealthyAt?: string; + }): Promise { + await dynamo.send(new UpdateCommand({ + TableName: ACCOUNTS_TABLE, + Key: { pk: pk(accountId), sk: `DOMAIN#${id}` }, + UpdateExpression: "SET receivingHealthy = :rh, senderHealthy = :sh, failingRecords = :fr, lastCheckedAt = :lc, updatedAt = :ua" + + (health.lastHealthyAt ? ", lastHealthyAt = :lha" : ""), + ExpressionAttributeValues: { + ":rh": health.receivingHealthy, + ":sh": health.senderHealthy, + ":fr": health.failingRecords, + ":lc": health.lastCheckedAt, + ":ua": health.lastCheckedAt, + ...(health.lastHealthyAt ? { ":lha": health.lastHealthyAt } : {}), + }, + })); + } + + async scanAllDomains(): Promise> { + const { ScanCommand } = await import("@aws-sdk/lib-dynamodb"); + const results: Array<{ accountId: string; domains: Domain[] }> = []; + let lastKey: Record | undefined; + const accountDomains = new Map(); + + do { + const res = await dynamo.send(new ScanCommand({ + TableName: ACCOUNTS_TABLE, + FilterExpression: "begins_with(sk, :prefix)", + ExpressionAttributeValues: { ":prefix": "DOMAIN#" }, + ...(lastKey ? { ExclusiveStartKey: lastKey } : {}), + })); + for (const item of res.Items ?? []) { + const domain = item as Domain; + const list = accountDomains.get(domain.accountId) ?? []; + list.push(domain); + accountDomains.set(domain.accountId, list); + } + lastKey = res.LastEvaluatedKey as Record | undefined; + } while (lastKey); + + for (const [accountId, domains] of accountDomains) { + results.push({ accountId, domains }); + } + return results; + } + // --------------------------------------------------------------------------- // Verified forwarding addresses // --------------------------------------------------------------------------- @@ -419,4 +573,85 @@ export class AccountDatabase { ), ); } + + // --------------------------------------------------------------------------- + // Email Templates + // --------------------------------------------------------------------------- + + async createTemplate(template: EmailTemplate): Promise { + await dynamo.send(new PutCommand({ + TableName: ACCOUNTS_TABLE, + Item: { ...template, pk: pk(template.accountId), sk: `TEMPLATE#${template.id}` }, + })); + return template; + } + + async getTemplate(accountId: string, id: string): Promise { + const res = await dynamo.send(new GetCommand({ + TableName: ACCOUNTS_TABLE, + Key: { pk: pk(accountId), sk: `TEMPLATE#${id}` }, + })); + return res.Item ? (res.Item as EmailTemplate) : null; + } + + async updateTemplate(accountId: string, id: string, update: Partial>): Promise { + const now = new Date().toISOString(); + const setParts: string[] = ["updatedAt = :now"]; + const exprValues: Record = { ":now": now }; + const exprNames: Record = {}; + if (update.name !== undefined) { setParts.push("#name = :name"); exprValues[":name"] = update.name; exprNames["#name"] = "name"; } + if (update.subject !== undefined) { setParts.push("#subject = :subject"); exprValues[":subject"] = update.subject; exprNames["#subject"] = "subject"; } + if (update.body !== undefined) { setParts.push("body = :body"); exprValues[":body"] = update.body; } + await dynamo.send(new UpdateCommand({ + TableName: ACCOUNTS_TABLE, + Key: { pk: pk(accountId), sk: `TEMPLATE#${id}` }, + UpdateExpression: `SET ${setParts.join(", ")}`, + ExpressionAttributeValues: exprValues, + ...(Object.keys(exprNames).length ? { ExpressionAttributeNames: exprNames } : {}), + })); + return (await this.getTemplate(accountId, id))!; + } + + async deleteTemplate(accountId: string, id: string): Promise { + await dynamo.send(new DeleteCommand({ + TableName: ACCOUNTS_TABLE, + Key: { pk: pk(accountId), sk: `TEMPLATE#${id}` }, + })); + } + + async listTemplates(accountId: string): Promise { + const res = await dynamo.send(new QueryCommand({ + TableName: ACCOUNTS_TABLE, + KeyConditionExpression: "pk = :pk AND begins_with(sk, :prefix)", + ExpressionAttributeValues: { ":pk": pk(accountId), ":prefix": "TEMPLATE#" }, + })); + return (res.Items ?? []) as EmailTemplate[]; + } + + // --------------------------------------------------------------------------- + // WebSocket Connections + // --------------------------------------------------------------------------- + + async saveWsConnection(conn: WsConnection): Promise { + await dynamo.send(new PutCommand({ + TableName: ACCOUNTS_TABLE, + Item: { ...conn, pk: pk(conn.accountId), sk: `CONN#${conn.connectionId}` }, + })); + } + + async listWsConnections(accountId: string): Promise { + const res = await dynamo.send(new QueryCommand({ + TableName: ACCOUNTS_TABLE, + KeyConditionExpression: "pk = :pk AND begins_with(sk, :prefix)", + ExpressionAttributeValues: { ":pk": pk(accountId), ":prefix": "CONN#" }, + })); + return (res.Items ?? []) as WsConnection[]; + } + + async deleteWsConnection(accountId: string, connectionId: string): Promise { + await dynamo.send(new DeleteCommand({ + TableName: ACCOUNTS_TABLE, + Key: { pk: pk(accountId), sk: `CONN#${connectionId}` }, + })); + } } diff --git a/src/database/adapters.ts b/src/database/adapters.ts index 093ffd6..f93e686 100644 --- a/src/database/adapters.ts +++ b/src/database/adapters.ts @@ -1,9 +1,10 @@ import type { ProcessorDatabase } from "../processor/processor.js"; import type { ApiDatabase, ListArcsParams, UpdateArcRequest, CreateViewRequest, UpdateViewRequest, CreateLabelRequest, UpdateLabelRequest, CreateRuleRequest, UpdateRuleRequest } from "../api/app.js"; -import type { Arc, Signal, View, Label, Rule, Domain, Account, Page, PageParams, Alias, VerifiedForwardingAddress } from "../types/index.js"; +import type { Arc, Signal, View, Label, Rule, Domain, Account, Page, PageParams, Alias, AliasSender, SenderMode, VerifiedForwardingAddress, EmailTemplate } from "../types/index.js"; import type { AccountDatabase } from "./account-database.js"; import type { ArcDatabase } from "./arc-database.js"; import type { ProcessingDatabase } from "./processing-database.js"; +import type { AuditDatabase } from "./audit-database.js"; // --------------------------------------------------------------------------- // ProcessorDatabaseAdapter @@ -22,9 +23,12 @@ export class ProcessorDatabaseAdapter implements ProcessorDatabase { getArc(accountId: string, id: string) { return this.arc.getArc(accountId, id); } findArcByGroupingKey(accountId: string, key: string) { return this.arc.findArcByGroupingKey(accountId, key); } saveArc(arc: Arc) { return this.arc.saveArc(arc); } - listRules(accountId: string) { return this.account.listRules(accountId); } + listEnabledRules(accountId: string) { return this.account.listEnabledRules(accountId); } getProcessorAccountContext(accountId: string, recipientAddress: string) { return this.account.getProcessorAccountContext(accountId, recipientAddress); } saveAlias(alias: Alias) { return this.account.saveAlias(alias); } + getSender(accountId: string, address: string, domain: string) { return this.account.getSender(accountId, address, domain); } + saveSender(accountId: string, address: string, domain: string, mode: SenderMode) { return this.account.saveSender(accountId, address, domain, mode); } + getTemplate(accountId: string, id: string) { return this.account.getTemplate(accountId, id); } updateGlobalReputation(domain: string, update: { wasSpam: boolean; wasBlocked: boolean }) { return this.processing.updateGlobalReputation(domain, update); } getDomainByName(accountId: string, domainName: string) { return this.account.getDomainByName(accountId, domainName); } } @@ -38,6 +42,7 @@ export class ApiDatabaseAdapter implements ApiDatabase { constructor( private readonly arc: ArcDatabase, private readonly account: AccountDatabase, + private readonly audit: AuditDatabase, ) {} // Arcs @@ -48,11 +53,17 @@ export class ApiDatabaseAdapter implements ApiDatabase { // Signals listSignals(accountId: string, arcId: string, params: PageParams) { return this.arc.listSignals(accountId, arcId, params); } + listPreArcSignals(accountId: string, status: "blocked" | "quarantined", params: PageParams) { return this.arc.listPreArcSignals(accountId, status, params); } getSignal(accountId: string, id: string) { return this.arc.getSignal(accountId, id); } updateSignal(accountId: string, id: string, update: Partial>) { return this.arc.updateSignal(accountId, id, update); } deleteSignal(accountId: string, id: string) { return this.arc.deleteSignal(accountId, id); } + blockSignal(accountId: string, signalId: string) { return this.arc.blockSignal(accountId, signalId); } unblockSignal(accountId: string, signalId: string, arcId: string) { return this.arc.unblockSignal(accountId, signalId, arcId); } + // Arcs (additional) + saveArc(arc: Arc) { return this.arc.saveArc(arc); } + findArcByGroupingKey(accountId: string, key: string) { return this.arc.findArcByGroupingKey(accountId, key); } + // Search searchArcs(accountId: string, query: string, params: PageParams) { return this.arc.searchArcs(accountId, query, params); } @@ -79,20 +90,40 @@ export class ApiDatabaseAdapter implements ApiDatabase { createRule(accountId: string, data: CreateRuleRequest) { return this.account.createRule(accountId, data); } updateRule(accountId: string, id: string, data: UpdateRuleRequest) { return this.account.updateRule(accountId, id, data); } deleteRule(accountId: string, id: string) { return this.account.deleteRule(accountId, id); } - reorderRules(accountId: string, orderedIds: string[]) { return this.account.reorderRules(accountId, orderedIds); } // Domains listDomains(accountId: string) { return this.account.listDomains(accountId); } getDomain(accountId: string, id: string) { return this.account.getDomain(accountId, id); } createDomain(accountId: string, domain: string) { return this.account.createDomain(accountId, domain); } deleteDomain(accountId: string, id: string) { return this.account.deleteDomain(accountId, id); } + updateDomainHealth(accountId: string, id: string, health: { receivingHealthy: boolean; senderHealthy: boolean; failingRecords: string[]; lastCheckedAt: string; lastHealthyAt?: string }) { return this.account.updateDomainHealth(accountId, id, health); } // Aliases listAliases(accountId: string) { return this.account.listAliases(accountId); } getAlias(accountId: string, address: string) { return this.account.getAlias(accountId, address); } createAlias(alias: Alias) { return this.account.createAlias(alias); } + saveAlias(alias: Alias) { return this.account.saveAlias(alias); } upsertAlias(alias: Alias) { return this.account.upsertAlias(alias); } deleteAlias(accountId: string, address: string) { return this.account.deleteAlias(accountId, address); } + renameAlias(accountId: string, oldAddress: string, newAddress: string) { return this.account.renameAlias(accountId, oldAddress, newAddress); } + + // Senders + saveSender(accountId: string, address: string, domain: string, mode: SenderMode) { return this.account.saveSender(accountId, address, domain, mode); } + removeSender(accountId: string, address: string, domain: string) { return this.account.removeSender(accountId, address, domain); } + getSender(accountId: string, address: string, domain: string) { return this.account.getSender(accountId, address, domain); } + listSenders(accountId: string, address: string) { return this.account.listSenders(accountId, address); } + listAliasesForDomain(accountId: string, domain: string) { return this.account.listAliasesForDomain(accountId, domain); } + + // Templates + createTemplate(template: EmailTemplate) { return this.account.createTemplate(template); } + getTemplate(accountId: string, id: string) { return this.account.getTemplate(accountId, id); } + updateTemplate(accountId: string, id: string, update: Partial>) { return this.account.updateTemplate(accountId, id, update); } + deleteTemplate(accountId: string, id: string) { return this.account.deleteTemplate(accountId, id); } + listTemplates(accountId: string) { return this.account.listTemplates(accountId); } + + // Audit + listAuditEvents(accountId: string, params: PageParams) { return this.audit.listAuditEvents(accountId, params); } + listResourceHistory(accountId: string, resourceType: import("./audit-database.js").AuditResourceType, resourceId: string) { return this.audit.listResourceHistory(accountId, resourceType, resourceId); } // Verified forwarding addresses listVerifiedForwardingAddresses(accountId: string) { return this.account.listVerifiedForwardingAddresses(accountId); } diff --git a/src/database/arc-database.ts b/src/database/arc-database.ts index 35b3066..5c11bcc 100644 --- a/src/database/arc-database.ts +++ b/src/database/arc-database.ts @@ -1,6 +1,5 @@ import { randomUUID } from "crypto"; -import { Pool } from "pg"; -import { Signer } from "@aws-sdk/rds-signer"; +import { RDSDataClient, ExecuteStatementCommand, BeginTransactionCommand, CommitTransactionCommand, RollbackTransactionCommand } from "@aws-sdk/client-rds-data"; import { DeleteCommand, GetCommand, PutCommand, QueryCommand, UpdateCommand } from "@aws-sdk/lib-dynamodb"; import { dynamo, SIGNALS_TABLE, encodeCursor, decodeCursor } from "./shared.js"; import type { ArcMatcher } from "../processor/processor.js"; @@ -8,46 +7,23 @@ import type { ListArcsParams, UpdateArcRequest, CreateViewRequest, UpdateViewReq import type { Arc, Signal, Page, PageParams } from "../types/index.js"; // --------------------------------------------------------------------------- -// pgvector pool (module-scoped, lazy, reused across warm Lambda invocations) +// Aurora Data API client (stateless — no connection pool needed) // --------------------------------------------------------------------------- const SIMILARITY_THRESHOLD = 0.5; -const RDS_HOST = process.env["RDS_PROXY_ENDPOINT"] ?? ""; -const DB_USER = process.env["DB_USER"] ?? "lambda"; -const DB_NAME = process.env["AURORA_DB_NAME"] ?? "signals"; -const AWS_REGION = process.env["AWS_REGION"] ?? "eu-west-1"; - -const signer = new Signer({ hostname: RDS_HOST, port: 5432, region: AWS_REGION, username: DB_USER }); - -let _pool: Pool | null = null; - -function getPool(): Pool { - if (_pool) return _pool; - _pool = new Pool({ - host: RDS_HOST, - database: DB_NAME, - user: DB_USER, - password: () => signer.getAuthToken(), - port: 5432, - ssl: { rejectUnauthorized: true }, - max: 2, - idleTimeoutMillis: 30_000, - connectionTimeoutMillis: 5_000, - }); - _pool.on("error", (err) => { - console.error("pgvector pool error:", err); - _pool = null; - }); - return _pool; -} +const CLUSTER_ARN = process.env["AURORA_CLUSTER_ARN"] ?? ""; +const SECRET_ARN = process.env["AURORA_SECRET_ARN"] ?? ""; +const DB_NAME = process.env["AURORA_DB_NAME"] ?? "signals"; + +const rdsData = new RDSDataClient({}); // --------------------------------------------------------------------------- // Key helpers // --------------------------------------------------------------------------- -const acctPk = (accountId: string) => `ACCT#${accountId}`; -const arcSk = (id: string) => `ARC#${id}`; -const sigSk = (id: string) => `SIG#${id}`; +const arcPk = (accountId: string, id: string) => `ACCT#${accountId}#ARC#${id}`; +const sigPk = (accountId: string, id: string) => `ACCT#${accountId}#SIG#${id}`; +const ITEM_SK = "#"; const gkeyPk = (accountId: string, key: string) => `GKEY#${accountId}#${key}`; // --------------------------------------------------------------------------- @@ -63,21 +39,28 @@ export class ArcDatabase implements ArcMatcher { async getSignalByMessageId(accountId: string, sesMessageId: string): Promise | null> { const result = await dynamo.send(new GetCommand({ TableName: SIGNALS_TABLE, - Key: { pk: acctPk(accountId), sk: sigSk(`SES#${sesMessageId}`) }, + Key: { pk: sigPk(accountId, `SES#${sesMessageId}`), sk: ITEM_SK }, ProjectionExpression: "id", })); return result.Item ? (result.Item as Pick) : null; } async saveSignal(signal: Signal): Promise { - const gsi1pk = signal.arcId ? `ARCSIG#${signal.arcId}` : `BLOCKED#${signal.accountId}`; + let gsi1pk: string; + if (signal.arcId) { + gsi1pk = `ARCSIG#${signal.arcId}`; + } else if (signal.status === "quarantine_visible" || signal.status === "quarantine_hidden") { + gsi1pk = `QUARANTINED#${signal.accountId}`; + } else { + gsi1pk = `BLOCKED#${signal.accountId}`; + } const gsi1sk = `RECV#${signal.receivedAt}#${signal.id}`; await dynamo.send(new PutCommand({ TableName: SIGNALS_TABLE, Item: { ...signal, - pk: acctPk(signal.accountId), - sk: sigSk(signal.id), + pk: sigPk(signal.accountId, signal.id), + sk: ITEM_SK, gsi1pk, gsi1sk, }, @@ -87,7 +70,7 @@ export class ArcDatabase implements ArcMatcher { async getSignal(accountId: string, id: string): Promise { const result = await dynamo.send(new GetCommand({ TableName: SIGNALS_TABLE, - Key: { pk: acctPk(accountId), sk: sigSk(id) }, + Key: { pk: sigPk(accountId, id), sk: ITEM_SK }, })); return result.Item ? (result.Item as Signal) : null; } @@ -110,10 +93,42 @@ export class ArcDatabase implements ArcMatcher { return { items: page, ...(nextKey ? { nextCursor: nextKey } : {}) }; } + async listPreArcSignals(accountId: string, status: "blocked" | "quarantined", params: PageParams): Promise> { + const limit = Math.min(params.limit ?? 20, 100); + const gsi1pk = status === "quarantined" ? `QUARANTINED#${accountId}` : `BLOCKED#${accountId}`; + const res = await dynamo.send(new QueryCommand({ + TableName: SIGNALS_TABLE, + IndexName: "gsi1", + KeyConditionExpression: "gsi1pk = :pk", + ExpressionAttributeValues: { ":pk": gsi1pk }, + ScanIndexForward: false, + Limit: limit + 1, + ...(params.cursor ? { ExclusiveStartKey: decodeCursor(params.cursor) } : {}), + })); + const items = (res.Items ?? []) as Signal[]; + const page = items.slice(0, limit); + const nextKey = items.length > limit && res.LastEvaluatedKey ? encodeCursor(res.LastEvaluatedKey) : null; + return { items: page, ...(nextKey ? { nextCursor: nextKey } : {}) }; + } + + async blockSignal(accountId: string, signalId: string): Promise { + await dynamo.send(new UpdateCommand({ + TableName: SIGNALS_TABLE, + Key: { pk: sigPk(accountId, signalId), sk: ITEM_SK }, + UpdateExpression: "SET #status = :status, gsi1pk = :gsi1pk", + ExpressionAttributeNames: { "#status": "status" }, + ExpressionAttributeValues: { + ":status": "blocked", + ":gsi1pk": `BLOCKED#${accountId}`, + }, + })); + return (await this.getSignal(accountId, signalId))!; + } + async unblockSignal(accountId: string, signalId: string, arcId: string): Promise { await dynamo.send(new UpdateCommand({ TableName: SIGNALS_TABLE, - Key: { pk: acctPk(accountId), sk: sigSk(signalId) }, + Key: { pk: sigPk(accountId, signalId), sk: ITEM_SK }, UpdateExpression: "SET arcId = :arcId, #status = :status, gsi1pk = :gsi1pk", ExpressionAttributeNames: { "#status": "status" }, ExpressionAttributeValues: { @@ -131,7 +146,7 @@ export class ArcDatabase implements ArcMatcher { async getArc(accountId: string, id: string): Promise { const result = await dynamo.send(new GetCommand({ TableName: SIGNALS_TABLE, - Key: { pk: acctPk(accountId), sk: arcSk(id) }, + Key: { pk: arcPk(accountId, id), sk: ITEM_SK }, })); return result.Item ? (result.Item as Arc) : null; } @@ -154,10 +169,10 @@ export class ArcDatabase implements ArcMatcher { TableName: SIGNALS_TABLE, Item: { ...arc, - pk: acctPk(arc.accountId), - sk: arcSk(arc.id), - gsi1pk: acctPk(arc.accountId), - gsi1sk: `LASTACT#${arc.lastSignalAt}#${arc.id}`, + pk: arcPk(arc.accountId, arc.id), + sk: ITEM_SK, + gsi1pk: `ACCT#${arc.accountId}`, + gsi1sk: `LASTACT#${arc.status}#${arc.lastSignalAt}#${arc.id}`, }, })), ]; @@ -187,15 +202,25 @@ export class ArcDatabase implements ArcMatcher { exprValues[":status"] = update.status; exprNames["#status"] = "status"; if (update.status === "deleted") setParts.push("deletedAt = :now"); + // Fetch current arc to reconstruct gsi1sk with new status + const current = await this.getArc(accountId, id); + if (current) { + setParts.push("gsi1sk = :gsi1sk"); + exprValues[":gsi1sk"] = `LASTACT#${update.status}#${current.lastSignalAt}#${id}`; + } } if (update.labels !== undefined) { setParts.push("labels = :labels"); exprValues[":labels"] = update.labels; } + if (update.urgency !== undefined) { + setParts.push("urgency = :urgency"); + exprValues[":urgency"] = update.urgency; + } await dynamo.send(new UpdateCommand({ TableName: SIGNALS_TABLE, - Key: { pk: acctPk(accountId), sk: arcSk(id) }, + Key: { pk: arcPk(accountId, id), sk: ITEM_SK }, UpdateExpression: `SET ${setParts.join(", ")}`, ExpressionAttributeValues: exprValues, ...(Object.keys(exprNames).length ? { ExpressionAttributeNames: exprNames } : {}), @@ -216,7 +241,7 @@ export class ArcDatabase implements ArcMatcher { await dynamo.send(new UpdateCommand({ TableName: SIGNALS_TABLE, - Key: { pk: acctPk(accountId), sk: sigSk(id) }, + Key: { pk: sigPk(accountId, id), sk: ITEM_SK }, UpdateExpression: `SET ${setParts.join(", ")}`, ExpressionAttributeValues: exprValues, ...(Object.keys(exprNames).length ? { ExpressionAttributeNames: exprNames } : {}), @@ -227,29 +252,53 @@ export class ArcDatabase implements ArcMatcher { async deleteSignal(accountId: string, id: string): Promise { await dynamo.send(new DeleteCommand({ TableName: SIGNALS_TABLE, - Key: { pk: acctPk(accountId), sk: sigSk(id) }, + Key: { pk: sigPk(accountId, id), sk: ITEM_SK }, })); } async listArcs(accountId: string, params: ListArcsParams): Promise> { const limit = Math.min(params.limit ?? 20, 100); - const res = await dynamo.send(new QueryCommand({ - TableName: SIGNALS_TABLE, - IndexName: "gsi1", - KeyConditionExpression: "gsi1pk = :pk AND begins_with(gsi1sk, :prefix)", - ExpressionAttributeValues: { ":pk": acctPk(accountId), ":prefix": "LASTACT#" }, - ScanIndexForward: false, - Limit: 200, - ...(params.cursor ? { ExclusiveStartKey: decodeCursor(params.cursor) } : {}), - })); + const gsi1pk = `ACCT#${accountId}`; + + let items: Arc[]; + let lastKey: Record | undefined; + + if (params.status) { + // Single query — status is encoded in gsi1sk prefix for efficient reads + const res = await dynamo.send(new QueryCommand({ + TableName: SIGNALS_TABLE, + IndexName: "gsi1", + KeyConditionExpression: "gsi1pk = :pk AND begins_with(gsi1sk, :prefix)", + ExpressionAttributeValues: { ":pk": gsi1pk, ":prefix": `LASTACT#${params.status}#` }, + ScanIndexForward: false, + Limit: limit + 1, + ...(params.cursor ? { ExclusiveStartKey: decodeCursor(params.cursor) } : {}), + })); + items = (res.Items ?? []) as Arc[]; + lastKey = res.LastEvaluatedKey; + } else { + // Multi-status view: parallel queries per status, merge by lastSignalAt + const statuses: Array<"active" | "archived" | "deleted"> = ["active", "archived", "deleted"]; + const results = await Promise.all(statuses.map(s => + dynamo.send(new QueryCommand({ + TableName: SIGNALS_TABLE, + IndexName: "gsi1", + KeyConditionExpression: "gsi1pk = :pk AND begins_with(gsi1sk, :prefix)", + ExpressionAttributeValues: { ":pk": gsi1pk, ":prefix": `LASTACT#${s}#` }, + ScanIndexForward: false, + Limit: limit + 1, + })) + )); + items = results.flatMap(r => (r.Items ?? []) as Arc[]); + items.sort((a, b) => b.lastSignalAt.localeCompare(a.lastSignalAt)); + lastKey = undefined; // no cursor for multi-status merge + } - let items = (res.Items ?? []) as Arc[]; if (params.workflow) items = items.filter((a) => a.workflow === params.workflow); - if (params.status) items = items.filter((a) => a.status === params.status); if (params.label) items = items.filter((a) => a.labels.includes(params.label!)); const page = items.slice(0, limit); - const nextKey = items.length > limit && res.LastEvaluatedKey ? encodeCursor(res.LastEvaluatedKey) : null; + const nextKey = items.length > limit && lastKey ? encodeCursor(lastKey) : null; return { items: page, ...(nextKey ? { nextCursor: nextKey } : {}) }; } @@ -259,7 +308,7 @@ export class ArcDatabase implements ArcMatcher { TableName: SIGNALS_TABLE, IndexName: "gsi1", KeyConditionExpression: "gsi1pk = :pk AND begins_with(gsi1sk, :prefix)", - ExpressionAttributeValues: { ":pk": acctPk(accountId), ":prefix": "LASTACT#" }, + ExpressionAttributeValues: { ":pk": `ACCT#${accountId}`, ":prefix": "LASTACT#active#" }, ScanIndexForward: false, Limit: 500, ...(params.cursor ? { ExclusiveStartKey: decodeCursor(params.cursor) } : {}), @@ -275,38 +324,74 @@ export class ArcDatabase implements ArcMatcher { } // --------------------------------------------------------------------------- - // ArcMatcher (pgvector — internal implementation detail) + // ArcMatcher (pgvector via Aurora Data API) // --------------------------------------------------------------------------- + // Wraps a Data API call in an explicit transaction so SET LOCAL is scoped + // to that transaction — required for the RLS policy to apply correctly. + private async withAccountContext(accountId: string, fn: (transactionId: string) => Promise): Promise { + const { transactionId } = await rdsData.send(new BeginTransactionCommand({ + resourceArn: CLUSTER_ARN, secretArn: SECRET_ARN, database: DB_NAME, + })); + try { + await rdsData.send(new ExecuteStatementCommand({ + resourceArn: CLUSTER_ARN, secretArn: SECRET_ARN, database: DB_NAME, + transactionId, + sql: "SET LOCAL app.current_account_id = :accountId", + parameters: [{ name: "accountId", value: { stringValue: accountId } }], + })); + const result = await fn(transactionId!); + await rdsData.send(new CommitTransactionCommand({ + resourceArn: CLUSTER_ARN, secretArn: SECRET_ARN, transactionId, + })); + return result; + } catch (err) { + await rdsData.send(new RollbackTransactionCommand({ + resourceArn: CLUSTER_ARN, secretArn: SECRET_ARN, transactionId, + })); + throw err; + } + } + async findMatch(accountId: string, recipientAddress: string, embedding: number[]): Promise { - const pool = getPool(); - const vectorLiteral = `[${embedding.join(",")}]`; - - const res = await pool.query<{ arc_id: string }>( - `SELECT arc_id - FROM arc_embeddings - WHERE account_id = $1 AND recipient_address = $2 - AND embedding <=> $3::vector < $4 - ORDER BY embedding <=> $3::vector - LIMIT 1`, - [accountId, recipientAddress, vectorLiteral, SIMILARITY_THRESHOLD], + const res = await this.withAccountContext(accountId, (transactionId) => + rdsData.send(new ExecuteStatementCommand({ + resourceArn: CLUSTER_ARN, secretArn: SECRET_ARN, database: DB_NAME, + transactionId, + sql: `SELECT arc_id FROM arc_embeddings + WHERE account_id = :accountId AND recipient_address = :recipient + AND embedding <=> :embedding::vector < :threshold + ORDER BY embedding <=> :embedding::vector + LIMIT 1`, + parameters: [ + { name: "accountId", value: { stringValue: accountId } }, + { name: "recipient", value: { stringValue: recipientAddress } }, + { name: "embedding", value: { stringValue: `[${embedding.join(",")}]` } }, + { name: "threshold", value: { doubleValue: SIMILARITY_THRESHOLD } }, + ], + })), ); - - if (!res.rows[0]) return null; - return this.getArc(accountId, res.rows[0].arc_id); + const arcId = res.records?.[0]?.[0]?.stringValue; + if (!arcId) return null; + return this.getArc(accountId, arcId); } async upsertEmbedding(arcId: string, embedding: number[], accountId: string, recipientAddress: string): Promise { - const pool = getPool(); - const vectorLiteral = `[${embedding.join(",")}]`; - - await pool.query( - `INSERT INTO arc_embeddings (arc_id, account_id, recipient_address, embedding, updated_at) - VALUES ($1, $2, $3, $4::vector, NOW()) - ON CONFLICT (arc_id) DO UPDATE - SET embedding = EXCLUDED.embedding, - updated_at = NOW()`, - [arcId, accountId, recipientAddress, vectorLiteral], + await this.withAccountContext(accountId, (transactionId) => + rdsData.send(new ExecuteStatementCommand({ + resourceArn: CLUSTER_ARN, secretArn: SECRET_ARN, database: DB_NAME, + transactionId, + sql: `INSERT INTO arc_embeddings (arc_id, account_id, recipient_address, embedding, updated_at) + VALUES (:arcId, :accountId, :recipient, :embedding::vector, NOW()) + ON CONFLICT (arc_id) DO UPDATE + SET embedding = EXCLUDED.embedding, updated_at = NOW()`, + parameters: [ + { name: "arcId", value: { stringValue: arcId } }, + { name: "accountId", value: { stringValue: accountId } }, + { name: "recipient", value: { stringValue: recipientAddress } }, + { name: "embedding", value: { stringValue: `[${embedding.join(",")}]` } }, + ], + })), ); } } diff --git a/src/database/audit-database.ts b/src/database/audit-database.ts new file mode 100644 index 0000000..bd29cd3 --- /dev/null +++ b/src/database/audit-database.ts @@ -0,0 +1,85 @@ +import { randomUUID } from "crypto"; +import { PutCommand, QueryCommand } from "@aws-sdk/lib-dynamodb"; +import { dynamo, AUDIT_TABLE, encodeCursor, decodeCursor } from "./shared.js"; +import type { Page, PageParams } from "../types/index.js"; + +export type AuditResourceType = + | "rule" + | "alias" + | "domain" + | "account" + | "label" + | "view" + | "template" + | "forwarding_address"; + +export type AuditAction = "created" | "updated" | "deleted" | "reordered"; + +export interface AuditEvent { + eventId: string; + accountId: string; + userId: string; + action: AuditAction; + resourceType: AuditResourceType; + resourceId: string; + timestamp: string; + before?: unknown; + after?: unknown; + ttl?: number; +} + +// Key design: +// PK = AUDIT# +// SK = ### (resource-scoped range; use begins_with for history) +// GSI1PK = AUDIT# +// GSI1SK = # (time-ordered account activity feed) + +export class AuditDatabase { + async saveAuditEvent(event: Omit): Promise { + const timestamp = new Date().toISOString(); + const eventId = randomUUID(); + const item: AuditEvent = { ...event, eventId, timestamp }; + await dynamo.send(new PutCommand({ + TableName: AUDIT_TABLE, + Item: { + ...item, + pk: `AUDIT#${event.accountId}`, + sk: `${event.resourceType}#${event.resourceId}#${timestamp}#${eventId}`, + gsi1pk: `AUDIT#${event.accountId}`, + gsi1sk: `${timestamp}#${eventId}`, + }, + })); + } + + // Time-ordered feed for the account — uses GSI1 + async listAuditEvents(accountId: string, params: PageParams): Promise> { + const limit = Math.min(params.limit ?? 50, 200); + const res = await dynamo.send(new QueryCommand({ + TableName: AUDIT_TABLE, + IndexName: "gsi1", + KeyConditionExpression: "gsi1pk = :pk", + ExpressionAttributeValues: { ":pk": `AUDIT#${accountId}` }, + ScanIndexForward: false, + Limit: limit + 1, + ...(params.cursor ? { ExclusiveStartKey: decodeCursor(params.cursor) } : {}), + })); + const items = (res.Items ?? []) as AuditEvent[]; + const page = items.slice(0, limit); + const nextKey = items.length > limit && res.LastEvaluatedKey ? encodeCursor(res.LastEvaluatedKey) : null; + return { items: page, ...(nextKey ? { nextCursor: nextKey } : {}) }; + } + + // History for a specific resource — uses main table SK with begins_with + async listResourceHistory(accountId: string, resourceType: AuditResourceType, resourceId: string): Promise { + const res = await dynamo.send(new QueryCommand({ + TableName: AUDIT_TABLE, + KeyConditionExpression: "pk = :pk AND begins_with(sk, :prefix)", + ExpressionAttributeValues: { + ":pk": `AUDIT#${accountId}`, + ":prefix": `${resourceType}#${resourceId}#`, + }, + ScanIndexForward: false, + })); + return (res.Items ?? []) as AuditEvent[]; + } +} diff --git a/src/database/shared.ts b/src/database/shared.ts index f9244a8..9839337 100644 --- a/src/database/shared.ts +++ b/src/database/shared.ts @@ -4,6 +4,7 @@ import { DynamoDBDocumentClient } from "@aws-sdk/lib-dynamodb"; export const ACCOUNTS_TABLE = process.env["ACCOUNTS_TABLE"] ?? "ses-accounts"; export const SIGNALS_TABLE = process.env["SIGNALS_TABLE"] ?? "ses-signals"; export const PROCESSING_TABLE = process.env["PROCESSING_TABLE"] ?? "ses-processing"; +export const AUDIT_TABLE = process.env["AUDIT_TABLE"] ?? "ses-audit"; export const dynamo = DynamoDBDocumentClient.from(new DynamoDBClient({}), { marshallOptions: { removeUndefinedValues: true }, diff --git a/src/dns/dns-checker.ts b/src/dns/dns-checker.ts new file mode 100644 index 0000000..151c02e --- /dev/null +++ b/src/dns/dns-checker.ts @@ -0,0 +1,77 @@ +import dns from "dns/promises"; +import type { DnsRecord, Domain } from "../types/index.js"; + +const DKIM_SELECTOR = "mail"; +const MAIL_DOMAIN = process.env["MAIL_DOMAIN"] ?? "mail.ses-email-adapter.example.com"; +const SES_INBOUND_ENDPOINT = process.env["SES_INBOUND_ENDPOINT"] ?? "inbound-smtp.eu-west-1.amazonaws.com"; + +async function resolveMx(name: string): Promise { + try { + const records = await dns.resolveMx(name); + const sorted = records.sort((a, b) => a.priority - b.priority); + return sorted[0] ? `${sorted[0].priority} ${sorted[0].exchange}` : undefined; + } catch { + return undefined; + } +} + +async function resolveTxt(name: string): Promise { + try { + const records = await dns.resolveTxt(name); + return records.map((r) => r.join("")).find((r) => r.startsWith("v=spf1")) ?? records[0]?.join("") ?? undefined; + } catch { + return undefined; + } +} + +async function resolveCname(name: string): Promise { + try { + const records = await dns.resolveCname(name); + return records[0] ?? undefined; + } catch { + return undefined; + } +} + +function normalize(v: string): string { + return v.trim().toLowerCase().replace(/\.+$/, ""); +} + +function matches(expected: string, current: string | undefined): boolean { + if (current === undefined) return false; + return normalize(expected) === normalize(current); +} + +function toRecord(name: string, type: DnsRecord["type"], value: string, current: string | undefined): DnsRecord { + const status: DnsRecord["status"] = matches(value, current) ? "verified" : current !== undefined ? "failing" : "pending"; + return current !== undefined + ? { name, type, value, currentValue: current, status } + : { name, type, value, status }; +} + +export async function checkDomain(domain: Domain): Promise { + const d = domain.domain; + const mxName = d; + const dkimName = `${DKIM_SELECTOR}._domainkey.${d}`; + const spfName = `bounce.${d}`; + const dmarcName = `_dmarc.${d}`; + + const expectedMx = `10 ${SES_INBOUND_ENDPOINT}`; + const expectedDkim = `${DKIM_SELECTOR}.${MAIL_DOMAIN}._domainkey.amazonses.com`; + const expectedSpf = `v=spf1 include:amazonses.com ~all`; + const expectedDmarc = `_dmarc.${MAIL_DOMAIN}`; + + const [mx, dkim, spf, dmarc] = await Promise.all([ + resolveMx(mxName), + resolveCname(dkimName), + resolveTxt(spfName), + resolveCname(dmarcName), + ]); + + return [ + toRecord(mxName, "MX", expectedMx, mx), + toRecord(dkimName, "CNAME", expectedDkim, dkim), + toRecord(spfName, "TXT", expectedSpf, spf), + toRecord(dmarcName,"CNAME", expectedDmarc, dmarc), + ]; +} diff --git a/src/handler.ts b/src/handler.ts index 723729c..2be6009 100644 --- a/src/handler.ts +++ b/src/handler.ts @@ -1,4 +1,4 @@ -import type { APIGatewayProxyEventV2, SQSEvent, Context, APIGatewayProxyResultV2 } from "aws-lambda"; +import type { APIGatewayProxyEventV2, SQSEvent, Context, APIGatewayProxyResultV2, EventBridgeEvent, APIGatewayProxyWebsocketEventV2 } from "aws-lambda"; import { BedrockRuntimeClient } from "@aws-sdk/client-bedrock-runtime"; import { S3Client, GetObjectCommand } from "@aws-sdk/client-s3"; import { SignalClassifier } from "./classifier/classifier.js"; @@ -9,10 +9,12 @@ import { AccountDatabase } from "./database/account-database.js"; import { ArcDatabase } from "./database/arc-database.js"; import { ProcessingDatabase } from "./database/processing-database.js"; import { ProcessorDatabaseAdapter, ApiDatabaseAdapter } from "./database/adapters.js"; +import { AuditDatabase } from "./database/audit-database.js"; import { SESv2Client, SendEmailCommand } from "@aws-sdk/client-sesv2"; import { SesNotifier } from "./notifier/ses-notifier.js"; import { SesForwarder } from "./notifier/ses-forwarder.js"; import { FeedbackProcessor } from "./notifier/feedback-processor.js"; +import { handler as domainHealthHandler } from "./jobs/domain-health-job.js"; import type { VerificationMailer } from "./api/app.js"; import { AuthressAuthService } from "./api/authress-auth.js"; import { AuthressAccessService } from "./api/authress-access.js"; @@ -52,6 +54,7 @@ const classifier = new SignalClassifier(bedrock); const accountDb = new AccountDatabase(); const arcDb = new ArcDatabase(); const processingDb = new ProcessingDatabase(); +const auditDb = new AuditDatabase(); const processor = new SignalProcessor({ store: new ProcessorDatabaseAdapter(arcDb, accountDb, processingDb), @@ -91,9 +94,11 @@ const sesVerificationMailer: VerificationMailer = { }, }; +const authService = new AuthressAuthService(); + const app = createApp({ - store: new ApiDatabaseAdapter(arcDb, accountDb), - auth: new AuthressAuthService(), + store: new ApiDatabaseAdapter(arcDb, accountDb, auditDb), + auth: authService, access: new AuthressAccessService(), verificationMailer: sesVerificationMailer, }); @@ -103,9 +108,15 @@ const app = createApp({ // --------------------------------------------------------------------------- export async function handler( - event: APIGatewayProxyEventV2 | SQSEvent, + event: APIGatewayProxyEventV2 | APIGatewayProxyWebsocketEventV2 | SQSEvent | EventBridgeEvent, _context: Context, -): Promise { +): Promise { + if (isEventBridgeEvent(event)) { + if ((event as EventBridgeEvent).detail?.source === "domain-health-job") { + await domainHealthHandler(); + } + return; + } if (isSqsEvent(event)) { if (isFeedbackEvent(event)) { await feedbackProcessor.process(event); @@ -114,6 +125,12 @@ export async function handler( } return; } + if (isWsAuthorizerEvent(event)) { + return handleWsAuthorizer(event as WsAuthorizerEvent); + } + if (isWebSocketEvent(event)) { + return handleWebSocket(event as APIGatewayProxyWebsocketEventV2); + } return honoToApiGateway(app, event as APIGatewayProxyEventV2); } @@ -121,6 +138,104 @@ export async function handler( // Helpers // --------------------------------------------------------------------------- +// --------------------------------------------------------------------------- +// WebSocket authorizer (fires on $connect; injects accountId into context) +// --------------------------------------------------------------------------- + +type WsAuthorizerEvent = { + type: "REQUEST"; + methodArn: string; + headers?: Record; + queryStringParameters?: Record; +}; + +type WsAuthorizerResult = { + principalId: string; + policyDocument: { + Version: string; + Statement: Array<{ Action: string; Effect: "Allow" | "Deny"; Resource: string }>; + }; + context: Record; +}; + +function isWsAuthorizerEvent(event: unknown): event is WsAuthorizerEvent { + const e = event as Record; + return e["type"] === "REQUEST" && typeof e["methodArn"] === "string"; +} + +async function handleWsAuthorizer(event: WsAuthorizerEvent): Promise { + // Browsers can't set headers on WebSocket upgrades — token comes as ?token= + const token = + event.queryStringParameters?.["token"] ?? + (event.headers?.["Authorization"] ?? event.headers?.["authorization"])?.replace(/^Bearer\s+/i, ""); + + if (!token) return wsDeny(event.methodArn); + + try { + const ctx = await authService.verify(token); + return { + principalId: ctx.userId, + policyDocument: { + Version: "2012-10-17", + Statement: [{ Action: "execute-api:Invoke", Effect: "Allow", Resource: event.methodArn }], + }, + context: { accountId: ctx.accountId, userId: ctx.userId }, + }; + } catch { + return wsDeny(event.methodArn); + } +} + +function wsDeny(methodArn: string): WsAuthorizerResult { + return { + principalId: "anonymous", + policyDocument: { + Version: "2012-10-17", + Statement: [{ Action: "execute-api:Invoke", Effect: "Deny", Resource: methodArn }], + }, + context: {}, + }; +} + +// --------------------------------------------------------------------------- +// WebSocket route handlers ($connect / $disconnect / $default) +// --------------------------------------------------------------------------- + +function isWebSocketEvent(event: unknown): event is APIGatewayProxyWebsocketEventV2 { + const ctx = (event as { requestContext?: Record }).requestContext; + return typeof ctx === "object" && ctx !== null && "connectionId" in ctx; +} + +async function handleWebSocket(event: APIGatewayProxyWebsocketEventV2): Promise<{ statusCode: number }> { + const { routeKey, connectionId } = event.requestContext; + // accountId is injected by the Lambda authorizer into requestContext.authorizer + const authorizer = (event.requestContext as unknown as { authorizer?: Record }).authorizer; + const accountId = authorizer?.["accountId"] ?? ""; + + switch (routeKey) { + case "$connect": + await accountDb.saveWsConnection({ + connectionId, + accountId, + connectedAt: new Date().toISOString(), + // 2-hour TTL — API Gateway closes idle connections after 10 min anyway + ttl: Math.floor(Date.now() / 1000) + 7200, + }); + return { statusCode: 200 }; + + case "$disconnect": + if (accountId) await accountDb.deleteWsConnection(accountId, connectionId); + return { statusCode: 200 }; + + default: + return { statusCode: 200 }; + } +} + +function isEventBridgeEvent(event: unknown): event is EventBridgeEvent { + return typeof event === "object" && event !== null && "source" in event && "detail-type" in event; +} + function isSqsEvent(event: unknown): event is SQSEvent { return ( typeof event === "object" && diff --git a/src/jobs/domain-health-job.ts b/src/jobs/domain-health-job.ts new file mode 100644 index 0000000..2c6806d --- /dev/null +++ b/src/jobs/domain-health-job.ts @@ -0,0 +1,57 @@ +import { SESv2Client, SendEmailCommand } from "@aws-sdk/client-sesv2"; +import { AccountDatabase } from "../database/account-database.js"; +import { checkDomain } from "../dns/dns-checker.js"; + +const FROM_ADDRESS = process.env["NOTIFICATION_FROM"] ?? ""; +const APP_BASE_URL = process.env["APP_BASE_URL"] ?? "https://app.example.com"; + +const sesv2 = new SESv2Client({}); +const db = new AccountDatabase(); + +export async function handler(): Promise { + const allAccounts = await db.scanAllDomains(); + + await Promise.all(allAccounts.map(async ({ accountId, domains }) => { + const account = await db.getAccount(accountId); + const notifyEmail = account?.notifications?.email?.enabled ? account.notifications.email.address : null; + + for (const domain of domains) { + const records = await checkDomain(domain); + const now = new Date().toISOString(); + const failingRecords = records.filter((r) => r.status === "failing").map((r) => r.name); + const receivingHealthy = records.find((r) => r.type === "MX")?.status === "verified"; + const senderHealthy = records.filter((r) => r.type !== "MX").every((r) => r.status === "verified"); + const allHealthy = failingRecords.length === 0; + + await db.updateDomainHealth(accountId, domain.id, { + receivingHealthy, + senderHealthy, + failingRecords, + lastCheckedAt: now, + ...(allHealthy ? { lastHealthyAt: now } : {}), + }); + + if (!allHealthy && notifyEmail && FROM_ADDRESS) { + const body = [ + `DNS health check failed for domain: ${domain.domain}`, + ``, + `Failing records:`, + ...failingRecords.map((r) => ` - ${r}`), + ``, + `Review your DNS settings: ${APP_BASE_URL}/domains/${domain.id}`, + ].join("\n"); + + await sesv2.send(new SendEmailCommand({ + FromEmailAddress: FROM_ADDRESS, + Destination: { ToAddresses: [notifyEmail] }, + Content: { + Simple: { + Subject: { Data: `[DNS Alert] ${domain.domain} has failing records`, Charset: "UTF-8" }, + Body: { Text: { Data: body, Charset: "UTF-8" } }, + }, + }, + })).catch((e) => console.error(`Failed to notify ${notifyEmail} for domain ${domain.domain}:`, e)); + } + } + })); +} diff --git a/src/notifier/ses-notifier.ts b/src/notifier/ses-notifier.ts index 86ef0b1..49ac0e5 100644 --- a/src/notifier/ses-notifier.ts +++ b/src/notifier/ses-notifier.ts @@ -1,8 +1,8 @@ import { SESv2Client, SendEmailCommand } from "@aws-sdk/client-sesv2"; import { DynamoDBClient } from "@aws-sdk/client-dynamodb"; -import { DynamoDBDocumentClient, GetCommand } from "@aws-sdk/lib-dynamodb"; +import { DynamoDBDocumentClient, DeleteCommand, GetCommand, QueryCommand } from "@aws-sdk/lib-dynamodb"; import type { Notifier } from "../processor/processor.js"; -import type { Arc, Signal, Account } from "../types/index.js"; +import type { Arc, Signal, Account, WsConnection, AuthData } from "../types/index.js"; const ACCOUNTS_TABLE = process.env["ACCOUNTS_TABLE"] ?? "ses-accounts"; const PROCESSING_TABLE = process.env["PROCESSING_TABLE"] ?? "ses-processing"; @@ -10,6 +10,8 @@ const FROM_ADDRESS = process.env["NOTIFICATION_FROM"] ?? ""; const CONFIG_SET = process.env["SES_CONFIGURATION_SET"] ?? ""; const APP_BASE_URL = process.env["APP_BASE_URL"] ?? "https://app.example.com"; +const WS_ENDPOINT = process.env["WS_API_ENDPOINT"] ?? ""; + const sesv2 = new SESv2Client({}); const dynamo = DynamoDBDocumentClient.from(new DynamoDBClient({})); @@ -57,6 +59,10 @@ export class SesNotifier implements Notifier { { Name: "type", Value: "signal_notify" }, ], })); + + if (signal.workflow === "auth") { + await this.wsNotify(accountId, arc, signal); + } } async notifyBlocked(accountId: string, signal: Signal): Promise { @@ -105,6 +111,44 @@ export class SesNotifier implements Notifier { return result.Item ? (result.Item as Account) : null; } + private async wsNotify(accountId: string, _arc: Arc, signal: Signal): Promise { + if (!WS_ENDPOINT) return; + const authData = signal.workflowData as AuthData; + + const res = await dynamo.send(new QueryCommand({ + TableName: ACCOUNTS_TABLE, + KeyConditionExpression: "pk = :pk AND begins_with(sk, :prefix)", + ExpressionAttributeValues: { ":pk": `ACCT#${accountId}`, ":prefix": "CONN#" }, + ProjectionExpression: "connectionId", + })); + const connections = (res.Items ?? []) as Pick[]; + + const payload = JSON.stringify({ + type: "auth", + code: authData.code, + expiresInMinutes: authData.expiresInMinutes, + originDomain: authData.service, + }); + + await Promise.all(connections.map(async ({ connectionId }) => { + try { + await fetch(`${WS_ENDPOINT}/${connectionId}`, { + method: "POST", + body: payload, + }); + } catch (err: unknown) { + // Stale connection — remove it + const status = (err as { status?: number }).status; + if (status === 410) { + await dynamo.send(new DeleteCommand({ + TableName: ACCOUNTS_TABLE, + Key: { pk: `ACCT#${accountId}`, sk: `CONN#${connectionId}` }, + })); + } + } + })); + } + private async isAddressSuppressed(address: string): Promise { const result = await dynamo.send(new GetCommand({ TableName: PROCESSING_TABLE, diff --git a/src/processor/filter.spec.ts b/src/processor/filter.spec.ts index 23f1460..a3ca77d 100644 --- a/src/processor/filter.spec.ts +++ b/src/processor/filter.spec.ts @@ -12,8 +12,8 @@ function makeCtx(overrides: Partial = {}): SystemLabelContex spamScore: LOW_SPAM, spamScoreThreshold: DEFAULT_SPAM_SCORE_THRESHOLD, senderETLD1: "amazon.com", - approvedSenders: ["amazon.com"], - filterMode: "notify_new", + senderEntry: { accountId: "acct-001", aliasAddress: "user@example.com", domain: "amazon.com", mode: "allow", addedAt: "2024-01-01T00:00:00Z" }, + filterMode: "quarantine_visible", hasSentMessages: false, ...overrides, }; @@ -94,67 +94,26 @@ describe("assignSystemLabels — spam labels", () => { describe("assignSystemLabels — sender trust", () => { it("emits system:sender:untrusted when sender not in approvedSenders", () => { - const labels = assignSystemLabels(makeCtx({ senderETLD1: "unknown.com", approvedSenders: [] })); + const labels = assignSystemLabels(makeCtx({ senderETLD1: "unknown.com", senderEntry: null })); expect(labels).toContain("system:sender:untrusted"); }); it("does not emit system:sender:untrusted when sender is in approvedSenders", () => { - const labels = assignSystemLabels(makeCtx({ senderETLD1: "amazon.com", approvedSenders: ["amazon.com"] })); + const labels = assignSystemLabels(makeCtx({ senderETLD1: "amazon.com", senderEntry: { accountId: "acct-001", aliasAddress: "user@example.com", domain: "amazon.com", mode: "allow", addedAt: "2024-01-01T00:00:00Z" } })); expect(labels).not.toContain("system:sender:untrusted"); }); it("does not emit system:sender:untrusted in allow_all mode regardless of approvedSenders", () => { - const labels = assignSystemLabels(makeCtx({ senderETLD1: "unknown.com", approvedSenders: [], filterMode: "allow_all" })); + const labels = assignSystemLabels(makeCtx({ senderETLD1: "unknown.com", senderEntry: null, filterMode: "allow_all" })); expect(labels).not.toContain("system:sender:untrusted"); }); it("emits system:sender:untrusted for matched arc if not in approvedSenders (trust is purely from approvedSenders)", () => { - const labels = assignSystemLabels(makeCtx({ senderETLD1: "unknown.com", approvedSenders: [], workflow: "content", workflowData: { workflow: "content", contentType: "newsletter", publisher: "foo" } })); + const labels = assignSystemLabels(makeCtx({ senderETLD1: "unknown.com", senderEntry: null, workflow: "content", workflowData: { workflow: "content", contentType: "newsletter", publisher: "foo" } })); expect(labels).toContain("system:sender:untrusted"); }); }); -// --------------------------------------------------------------------------- -// assignSystemLabels — urgency labels -// --------------------------------------------------------------------------- - -describe("assignSystemLabels — urgency labels", () => { - it("auth workflow emits system:urgency:critical", () => { - const labels = assignSystemLabels(makeCtx({ workflow: "auth", workflowData: { workflow: "auth", authType: "otp", service: "github.com" } })); - expect(labels).toContain("system:urgency:critical"); - }); - - it("status workflow emits system:urgency:silent", () => { - const labels = assignSystemLabels(makeCtx({ workflow: "status", workflowData: { workflow: "status", statusType: "terms_update", provider: "acme" } })); - expect(labels).toContain("system:urgency:silent"); - }); - - it("content workflow emits system:urgency:low", () => { - const labels = assignSystemLabels(makeCtx({ workflow: "content", workflowData: { workflow: "content", contentType: "newsletter", publisher: "foo" } })); - expect(labels).toContain("system:urgency:low"); - }); - - it("replied arc is promoted to at least high urgency", () => { - const labels = assignSystemLabels(makeCtx({ - workflow: "content", - workflowData: { workflow: "content", contentType: "newsletter", publisher: "foo" }, - hasSentMessages: true, - })); - expect(labels).toContain("system:urgency:high"); - expect(labels).not.toContain("system:urgency:low"); - }); - - it("already-critical urgency is not downgraded by replied promotion", () => { - const labels = assignSystemLabels(makeCtx({ - workflow: "auth", - workflowData: { workflow: "auth", authType: "otp", service: "github.com" }, - hasSentMessages: true, - })); - expect(labels).toContain("system:urgency:critical"); - expect(labels).not.toContain("system:urgency:high"); - }); -}); - // --------------------------------------------------------------------------- // assignSystemLabels — replied and test labels // --------------------------------------------------------------------------- diff --git a/src/processor/filter.ts b/src/processor/filter.ts index 9c687ec..0ef7e79 100644 --- a/src/processor/filter.ts +++ b/src/processor/filter.ts @@ -1,6 +1,5 @@ import { getDomain } from "tldts"; -import type { Workflow, WorkflowData, SenderFilterMode, SystemLabel } from "../types/index.js"; -import { baseUrgency, URGENCY_RANK } from "./priority.js"; +import type { Workflow, WorkflowData, SenderFilterMode, SystemLabel, AliasSender } from "../types/index.js"; export const DEFAULT_SPAM_SCORE_THRESHOLD = 0.9; @@ -18,7 +17,7 @@ export interface SystemLabelContext { spamScore: number; spamScoreThreshold: number; senderETLD1: string; - approvedSenders: string[]; + senderEntry: AliasSender | null; // pre-fetched allow/block entry for this (alias, sender domain) pair filterMode: SenderFilterMode; hasSentMessages: boolean; } @@ -34,12 +33,10 @@ export function assignSystemLabels(ctx: SystemLabelContext): SystemLabel[] { if (ctx.spamScore >= ctx.spamScoreThreshold) labels.push("system:spam:high"); else if (ctx.spamScore >= 0.4) labels.push("system:spam:medium"); - const senderApproved = ctx.approvedSenders.includes(ctx.senderETLD1) || ctx.filterMode === "allow_all"; - if (!senderApproved) labels.push("system:sender:untrusted"); - - let urgency = baseUrgency(ctx.workflow, ctx.workflowData); - if (ctx.hasSentMessages && URGENCY_RANK[urgency] < URGENCY_RANK["high"]) urgency = "high"; - labels.push(`system:urgency:${urgency}` as SystemLabel); + const senderTrusted = + ctx.senderEntry?.mode === "allow" || + ctx.filterMode === "allow_all"; + if (!senderTrusted) labels.push("system:sender:untrusted"); if (ctx.hasSentMessages) labels.push("system:replied"); if (ctx.workflow === "test") labels.push("system:test"); diff --git a/src/processor/mime.ts b/src/processor/mime.ts index eea464c..8ef52b8 100644 --- a/src/processor/mime.ts +++ b/src/processor/mime.ts @@ -22,7 +22,7 @@ export class MailparserMimeParser { async parse(rawEmail: Buffer | string): Promise { const parsed = await simpleParser(rawEmail); - const toAddr = (addr: { address?: string; name?: string }): EmailAddress => ({ + const toAddr = (addr: { address?: string | undefined; name?: string }): EmailAddress => ({ address: addr.address ?? "", ...(addr.name ? { name: addr.name } : {}), }); diff --git a/src/processor/priority.ts b/src/processor/priority.ts index f032db6..a16a660 100644 --- a/src/processor/priority.ts +++ b/src/processor/priority.ts @@ -1,10 +1,4 @@ -import type { Arc, Signal, ArcUrgency, PushPriority, Workflow, WorkflowData } from "../types/index.js"; - -export const URGENCY_RANK: Record = { critical: 4, high: 3, normal: 2, low: 1, silent: 0 }; - -function promote(current: ArcUrgency, floor: ArcUrgency): ArcUrgency { - return URGENCY_RANK[current] >= URGENCY_RANK[floor] ? current : floor; -} +import type { ArcUrgency, PushPriority, Workflow, WorkflowData } from "../types/index.js"; export function baseUrgency(workflow: Workflow, data: WorkflowData): ArcUrgency { switch (workflow) { @@ -19,9 +13,6 @@ export function baseUrgency(workflow: Workflow, data: WorkflowData): ArcUrgency case "payments": return (data as { paymentType?: string }).paymentType === "payment_failed" ? "critical" : "normal"; - case "support": - return (data as { priority?: string }).priority === "urgent" ? "critical" : "normal"; - // passive content — low noise case "content": return "low"; @@ -40,17 +31,6 @@ export function baseUrgency(workflow: Workflow, data: WorkflowData): ArcUrgency } } -// Derives the urgency level for an arc after a new signal arrives. -// Arcs where the user has sent at least one outbound email are promoted to at -// least "high" — any reply represents explicit prior engagement. -export function priorityCalculator(arc: Arc, signal: Signal): ArcUrgency { - const base = baseUrgency(signal.workflow, signal.workflowData); - if (arc.sentMessageIds && arc.sentMessageIds.length > 0) { - return promote(base, "high"); - } - return base; -} - // Maps an ArcUrgency to the equivalent mobile push notification tier. export function urgencyToPushPriority(urgency: ArcUrgency): PushPriority { if (urgency === "critical" || urgency === "high") return "interrupt"; diff --git a/src/processor/processor.spec.ts b/src/processor/processor.spec.ts index 00d0c56..62e89ed 100644 --- a/src/processor/processor.spec.ts +++ b/src/processor/processor.spec.ts @@ -3,7 +3,7 @@ import { describe, it, expect, vi, beforeEach } from "vitest"; import type { SQSEvent } from "aws-lambda"; import { SignalProcessor, deriveGroupingKey, SYSTEM_RULES } from "./processor.js"; import { JsonLogicRuleEvaluator } from "./rule-evaluator.js"; -import { baseUrgency, priorityCalculator } from "./priority.js"; +import { baseUrgency } from "./priority.js"; import type { ProcessorDatabase, ArcMatcher, RuleEvaluator, Notifier, Forwarder, ForwardOptions, TestReplier } from "./processor.js"; import type { MimeParser } from "./mime.js"; import type { SignalClassifier, ClassificationOutput } from "../classifier/classifier.js"; @@ -15,13 +15,18 @@ import type { Arc, Rule, Signal, Alias, AccountFilteringConfig } from "../types/ const TEST_ACCOUNT_ID = "acct-001"; -// Default context: sender example.com is pre-approved so most tests exercise the happy path without hitting SR-02. +// Default context: sender example.com is pre-approved so most tests exercise the happy path without triggering the filter-mode fallback. // Tests that specifically test sender filtering use explicit mockResolvedValueOnce overrides. const DEFAULT_EMAIL_CONFIG: Alias = { id: "cfg-default", accountId: "acct-test-001", address: "user@example.com", - filterMode: "notify_new", approvedSenders: ["example.com"], + filterMode: "quarantine_visible", createdAt: "2024-01-01T00:00:00Z", updatedAt: "2024-01-01T00:00:00Z", }; + +// Default AliasSender: marks example.com as an allowed sender for the default alias. +const DEFAULT_SENDER_ENTRY: import("../types/index.js").AliasSender = { + accountId: "acct-test-001", aliasAddress: "user@example.com", domain: "example.com", mode: "allow", addedAt: "2024-01-01T00:00:00Z", +}; const DEFAULT_CTX = { retentionDays: 0, filtering: null, emailConfig: DEFAULT_EMAIL_CONFIG, registeredDomains: [], userEmails: [] }; function makeStore(): ProcessorDatabase { @@ -31,9 +36,12 @@ function makeStore(): ProcessorDatabase { getArc: vi.fn().mockResolvedValue(null), findArcByGroupingKey: vi.fn().mockResolvedValue(null), saveArc: vi.fn().mockResolvedValue(undefined), - listRules: vi.fn().mockResolvedValue(SYSTEM_RULES), + listEnabledRules: vi.fn().mockResolvedValue(SYSTEM_RULES), getProcessorAccountContext: vi.fn().mockResolvedValue(DEFAULT_CTX), saveAlias: vi.fn().mockImplementation((a: Alias) => Promise.resolve(a)), + getSender: vi.fn().mockResolvedValue(DEFAULT_SENDER_ENTRY), + saveSender: vi.fn().mockResolvedValue(undefined), + getTemplate: vi.fn().mockResolvedValue(null), updateGlobalReputation: vi.fn().mockResolvedValue(undefined), getDomainByName: vi.fn().mockResolvedValue(null), }; @@ -50,14 +58,18 @@ function makeAlias(overrides: Partial = {}): Alias { id: "cfg-001", accountId: TEST_ACCOUNT_ID, address: "user@example.com", - filterMode: "notify_new", - approvedSenders: ["example.com"], + filterMode: "quarantine_visible", createdAt: "2024-01-01T00:00:00Z", updatedAt: "2024-01-01T00:00:00Z", ...overrides, }; } +// Helper to make an AliasSender entry (approved sender for a given alias+domain). +function makeSenderEntry(domain: string, aliasAddress = "user@example.com"): import("../types/index.js").AliasSender { + return { accountId: TEST_ACCOUNT_ID, aliasAddress, domain, mode: "allow", addedAt: "2024-01-01T00:00:00Z" }; +} + function makeMimeParser(): MimeParser { return { parse: vi.fn().mockResolvedValue({ @@ -168,7 +180,8 @@ function makeRule(overrides: Partial = {}): Rule { name: "Test rule", condition: "true", actions: [], - position: 100, + status: "enabled", + priorityOrder: 100, createdAt: "2024-01-01T00:00:00Z", updatedAt: "2024-01-01T00:00:00Z", ...overrides, @@ -343,11 +356,12 @@ describe("SignalProcessor", () => { name: "Label billing", condition: "true", actions: [{ type: "assign_label", value: "billing" }], - position: 0, + status: "enabled", + priorityOrder: 0, createdAt: "2024-01-01T00:00:00Z", updatedAt: "2024-01-01T00:00:00Z", }; - vi.mocked(store.listRules).mockResolvedValueOnce([rule]); + vi.mocked(store.listEnabledRules).mockResolvedValueOnce([rule]); await processor.process(makeSqsEvent([{}])); @@ -362,11 +376,12 @@ describe("SignalProcessor", () => { name: "Archive newsletters", condition: "true", actions: [{ type: "archive" }], - position: 0, + status: "enabled", + priorityOrder: 0, createdAt: "2024-01-01T00:00:00Z", updatedAt: "2024-01-01T00:00:00Z", }; - vi.mocked(store.listRules).mockResolvedValueOnce([rule]); + vi.mocked(store.listEnabledRules).mockResolvedValueOnce([rule]); await processor.process(makeSqsEvent([{}])); @@ -381,11 +396,12 @@ describe("SignalProcessor", () => { name: "Never matches", condition: '{"==": [1, 2]}', actions: [{ type: "archive" }], - position: 0, + status: "enabled", + priorityOrder: 0, createdAt: "2024-01-01T00:00:00Z", updatedAt: "2024-01-01T00:00:00Z", }; - vi.mocked(store.listRules).mockResolvedValueOnce([rule]); + vi.mocked(store.listEnabledRules).mockResolvedValueOnce([rule]); await processor.process(makeSqsEvent([{}])); @@ -400,11 +416,12 @@ describe("SignalProcessor", () => { name: "Disabled label rule", condition: "true", actions: [{ type: "assign_label", value: "important", disabled: true }], - position: 0, + status: "enabled", + priorityOrder: 0, createdAt: "2024-01-01T00:00:00Z", updatedAt: "2024-01-01T00:00:00Z", }; - vi.mocked(store.listRules).mockResolvedValueOnce([rule]); + vi.mocked(store.listEnabledRules).mockResolvedValueOnce([rule]); await processor.process(makeSqsEvent([{}])); @@ -419,11 +436,12 @@ describe("SignalProcessor", () => { name: "Forward all", condition: "true", actions: [{ type: "forward", value: "backup@personal.com" }], - position: 0, + status: "enabled", + priorityOrder: 0, createdAt: "2024-01-01T00:00:00Z", updatedAt: "2024-01-01T00:00:00Z", }; - vi.mocked(store.listRules).mockResolvedValueOnce([rule]); + vi.mocked(store.listEnabledRules).mockResolvedValueOnce([rule]); // No error — processor without forwarder silently skips forward actions await processor.process(makeSqsEvent([{}])); @@ -432,6 +450,72 @@ describe("SignalProcessor", () => { }); }); + // ------------------------------------------------------------------------- + // matchedRules + // ------------------------------------------------------------------------- + + describe("matchedRules", () => { + it("writes matched rule with labelsAdded to signal", async () => { + const rule: Rule = { + id: "rule-label", + accountId: TEST_ACCOUNT_ID, + name: "Tag billing", + condition: "true", + actions: [{ type: "assign_label", value: "billing" }], + status: "enabled", + priorityOrder: 100, + createdAt: "2024-01-01T00:00:00Z", + updatedAt: "2024-01-01T00:00:00Z", + }; + vi.mocked(store.listEnabledRules).mockResolvedValueOnce([rule]); + + await processor.process(makeSqsEvent([{}])); + + const signal = vi.mocked(store.saveSignal).mock.calls[0]![0] as Signal; + expect(signal.matchedRules).toHaveLength(1); + expect(signal.matchedRules![0]!.ruleId).toBe("rule-label"); + expect(signal.matchedRules![0]!.labelsAdded).toContain("billing"); + expect(signal.matchedRules![0]!.statusChange).toBeUndefined(); + }); + + it("writes statusChange on the matching rule for a quarantined signal", async () => { + const rule: Rule = { + id: "rule-quarantine", + accountId: TEST_ACCOUNT_ID, + name: "Quarantine unknown", + condition: "true", + actions: [{ type: "quarantine" }], + status: "enabled", + priorityOrder: 100, + createdAt: "2024-01-01T00:00:00Z", + updatedAt: "2024-01-01T00:00:00Z", + }; + vi.mocked(store.listEnabledRules).mockResolvedValueOnce([rule]); + vi.mocked(store.getProcessorAccountContext).mockResolvedValueOnce({ + ...DEFAULT_CTX, + emailConfig: { ...DEFAULT_EMAIL_CONFIG, filterMode: "allow_all" }, + }); + + await processor.process(makeSqsEvent([{}])); + + const signal = vi.mocked(store.saveSignal).mock.calls[0]![0] as Signal; + expect(signal.status).toBe("quarantine_visible"); + expect(signal.matchedRules).toHaveLength(1); + expect(signal.matchedRules![0]!.statusChange).toBe("quarantine_visible"); + }); + + it("does not include rules that did not match", async () => { + const matching: Rule = { ...makeRule({ id: "r-match", name: "Matches", condition: "true", actions: [{ type: "archive" }] }) }; + const nonMatching: Rule = { ...makeRule({ id: "r-skip", name: "Never", condition: '{"==": [1, 2]}', actions: [{ type: "block" }] }) }; + vi.mocked(store.listEnabledRules).mockResolvedValueOnce([matching, nonMatching]); + + await processor.process(makeSqsEvent([{}])); + + const signal = vi.mocked(store.saveSignal).mock.calls[0]![0] as Signal; + expect(signal.matchedRules?.map((r) => r.ruleId)).toEqual(["r-match"]); + }); + }); + // ------------------------------------------------------------------------- // Forwarding // ------------------------------------------------------------------------- @@ -451,11 +535,12 @@ describe("SignalProcessor", () => { name: "Forward to backup", condition: "true", actions: [{ type: "forward", value: "backup@personal.com" }], - position: 0, + status: "enabled", + priorityOrder: 0, createdAt: "2024-01-01T00:00:00Z", updatedAt: "2024-01-01T00:00:00Z", }; - vi.mocked(store.listRules).mockResolvedValueOnce([rule]); + vi.mocked(store.listEnabledRules).mockResolvedValueOnce([rule]); await processor.process(makeSqsEvent([{ s3Key: "emails/msg-123" }])); @@ -477,11 +562,12 @@ describe("SignalProcessor", () => { { type: "forward", value: "first@example.com" }, { type: "forward", value: "second@example.com" }, ], - position: 0, + status: "enabled", + priorityOrder: 0, createdAt: "2024-01-01T00:00:00Z", updatedAt: "2024-01-01T00:00:00Z", }; - vi.mocked(store.listRules).mockResolvedValueOnce([rule]); + vi.mocked(store.listEnabledRules).mockResolvedValueOnce([rule]); await processor.process(makeSqsEvent([{}])); @@ -498,11 +584,12 @@ describe("SignalProcessor", () => { name: "Forward invoices", condition: '{"==": [1, 2]}', actions: [{ type: "forward", value: "accountant@firm.com" }], - position: 0, + status: "enabled", + priorityOrder: 0, createdAt: "2024-01-01T00:00:00Z", updatedAt: "2024-01-01T00:00:00Z", }; - vi.mocked(store.listRules).mockResolvedValueOnce([rule]); + vi.mocked(store.listEnabledRules).mockResolvedValueOnce([rule]); await processor.process(makeSqsEvent([{}])); @@ -520,11 +607,12 @@ describe("SignalProcessor", () => { name: "Forward all", condition: "true", actions: [{ type: "forward", value: "copy@example.com" }], - position: 0, + status: "enabled", + priorityOrder: 0, createdAt: "2024-01-01T00:00:00Z", updatedAt: "2024-01-01T00:00:00Z", }; - vi.mocked(store.listRules).mockResolvedValueOnce([rule]); + vi.mocked(store.listEnabledRules).mockResolvedValueOnce([rule]); await processor.process(makeSqsEvent([{}])); @@ -539,11 +627,12 @@ describe("SignalProcessor", () => { name: "Forward all", condition: "true", actions: [{ type: "forward", value: "copy@example.com" }], - position: 0, + status: "enabled", + priorityOrder: 0, createdAt: "2024-01-01T00:00:00Z", updatedAt: "2024-01-01T00:00:00Z", }; - vi.mocked(store.listRules).mockResolvedValueOnce([rule]); + vi.mocked(store.listEnabledRules).mockResolvedValueOnce([rule]); await processor.process(makeSqsEvent([{}])); @@ -681,6 +770,8 @@ describe("SignalProcessor", () => { vi.mocked(store.getProcessorAccountContext).mockResolvedValueOnce( { ...DEFAULT_CTX, emailConfig: null }, ); + // No existing sender entry for a brand-new address + vi.mocked(store.getSender).mockResolvedValueOnce(null); await processor.process(makeSqsEvent([{}])); expect(store.saveSignal).toHaveBeenCalledOnce(); @@ -688,14 +779,15 @@ describe("SignalProcessor", () => { expect(store.saveAlias).toHaveBeenCalledOnce(); const savedConfig = vi.mocked(store.saveAlias).mock.calls[0]![0] as Alias; - expect(savedConfig.filterMode).toBe("notify_new"); - expect(savedConfig.approvedSenders).toContain("example.com"); // sender domain from mimeParser mock + expect(savedConfig.filterMode).toBe("quarantine_visible"); + expect(store.saveSender).toHaveBeenCalledWith(TEST_ACCOUNT_ID, expect.any(String), "example.com", "allow"); }); it("allows signal from a known sender (eTLD+1 in approved list)", async () => { vi.mocked(store.getProcessorAccountContext).mockResolvedValueOnce( - { ...DEFAULT_CTX, emailConfig: makeAlias({ approvedSenders: ["example.com"] }) }, + { ...DEFAULT_CTX, emailConfig: makeAlias() }, ); + // getSender returns an approved entry for example.com (default mock already does this) await processor.process(makeSqsEvent([{}])); @@ -704,24 +796,26 @@ describe("SignalProcessor", () => { expect(store.saveAlias).not.toHaveBeenCalled(); // no auto-approve needed }); - it("quarantines signal from unknown sender by default (notify user for review)", async () => { + it("unknown sender with default filter mode → quarantine_visible (shown in review queue)", async () => { vi.mocked(store.getProcessorAccountContext).mockResolvedValueOnce( - { ...DEFAULT_CTX, emailConfig: makeAlias({ approvedSenders: ["trusted.com"] }) }, // sender is example.com, not trusted.com + { ...DEFAULT_CTX, emailConfig: makeAlias() }, // default filterMode: quarantine_visible ); + vi.mocked(store.getSender).mockResolvedValueOnce(null); await processor.process(makeSqsEvent([{}])); expect(store.saveArc).not.toHaveBeenCalled(); expect(store.saveSignal).toHaveBeenCalledOnce(); const saved = vi.mocked(store.saveSignal).mock.calls[0]![0] as Signal; - expect(saved.status).toBe("quarantined"); + expect(saved.status).toBe("quarantine_visible"); expect(saved.arcId).toBeUndefined(); }); - it("calls notifyBlocked when a signal is quarantined", async () => { + it("calls notifyBlocked when an unknown sender is quarantined (quarantine_visible fallback)", async () => { vi.mocked(store.getProcessorAccountContext).mockResolvedValueOnce( - { ...DEFAULT_CTX, emailConfig: makeAlias({ approvedSenders: ["other.com"] }) }, + { ...DEFAULT_CTX, emailConfig: makeAlias() }, ); + vi.mocked(store.getSender).mockResolvedValueOnce(null); await processor.process(makeSqsEvent([{}])); @@ -729,11 +823,55 @@ describe("SignalProcessor", () => { expect(notifier.notify).not.toHaveBeenCalled(); }); + it("calls notifyBlocked when a signal is quarantine_visible (e.g. high-spam from approved sender)", async () => { + vi.mocked(store.getProcessorAccountContext).mockResolvedValueOnce( + { ...DEFAULT_CTX, emailConfig: makeAlias() }, + ); + // Approved sender → SR-03 fires on high spam → quarantine_visible + vi.mocked(classifier.classify as ReturnType).mockResolvedValueOnce({ + ...validClassification, + spamScore: 0.95, + }); + + await processor.process(makeSqsEvent([{}])); + + expect(notifier.notifyBlocked).toHaveBeenCalledOnce(); + const saved = vi.mocked(store.saveSignal).mock.calls[0]![0] as Signal; + expect(saved.status).toBe("quarantine_visible"); + }); + + it("filter mode quarantine_visible: unknown sender → quarantine_visible + notifies", async () => { + vi.mocked(store.getProcessorAccountContext).mockResolvedValueOnce( + { ...DEFAULT_CTX, emailConfig: makeAlias({ filterMode: "quarantine_visible" }) }, + ); + vi.mocked(store.getSender).mockResolvedValueOnce(null); + + await processor.process(makeSqsEvent([{}])); + + expect(notifier.notifyBlocked).toHaveBeenCalledOnce(); + const saved = vi.mocked(store.saveSignal).mock.calls[0]![0] as Signal; + expect(saved.status).toBe("quarantine_visible"); + }); + + it("filter mode quarantine_hidden: unknown sender → quarantine_hidden + does not notify", async () => { + vi.mocked(store.getProcessorAccountContext).mockResolvedValueOnce( + { ...DEFAULT_CTX, emailConfig: makeAlias({ filterMode: "quarantine_hidden" }) }, + ); + vi.mocked(store.getSender).mockResolvedValueOnce(null); + + await processor.process(makeSqsEvent([{}])); + + expect(notifier.notifyBlocked).not.toHaveBeenCalled(); + const saved = vi.mocked(store.saveSignal).mock.calls[0]![0] as Signal; + expect(saved.status).toBe("quarantine_hidden"); + }); + it("does NOT call notifyBlocked when a signal is silently blocked by a block rule", async () => { vi.mocked(store.getProcessorAccountContext).mockResolvedValueOnce( - { ...DEFAULT_CTX, emailConfig: makeAlias({ approvedSenders: ["other.com"] }) }, + { ...DEFAULT_CTX, emailConfig: makeAlias() }, ); - vi.mocked(store.listRules).mockResolvedValueOnce([ + vi.mocked(store.getSender).mockResolvedValueOnce(null); + vi.mocked(store.listEnabledRules).mockResolvedValueOnce([ makeRule({ condition: JSON.stringify({ "in": ["system:sender:untrusted", { var: "arc.labels" }] }), actions: [{ type: "block" }] }), ]); @@ -744,10 +882,15 @@ describe("SignalProcessor", () => { expect(saved.status).toBe("blocked"); }); - it("does not fail when notifyBlocked throws", async () => { + it("does not fail when notifyBlocked throws (quarantine_visible via SR-03)", async () => { vi.mocked(store.getProcessorAccountContext).mockResolvedValueOnce( - { ...DEFAULT_CTX, emailConfig: makeAlias({ approvedSenders: [] }) }, + { ...DEFAULT_CTX, emailConfig: makeAlias() }, ); + // Approved sender + high spam → SR-03 fires → quarantine_visible → notifyBlocked called + vi.mocked(classifier.classify as ReturnType).mockResolvedValueOnce({ + ...validClassification, + spamScore: 0.95, + }); vi.mocked(notifier.notifyBlocked).mockRejectedValueOnce(new Error("SES error")); await processor.process(makeSqsEvent([{}])); @@ -771,17 +914,18 @@ describe("SignalProcessor", () => { await processor.process(makeSqsEvent([{}])); - // Filtering logic was bypassed — signal is active despite restrictive default config + // Filtering fallback bypassed on matched arc — signal is active despite untrusted sender expect(store.saveArc).toHaveBeenCalledOnce(); expect(store.saveSignal).toHaveBeenCalledOnce(); const saved = vi.mocked(store.saveSignal).mock.calls[0]![0] as Signal; expect(saved.status).toBe("active"); }); - it("strict mode quarantines a known sender with high spam score (default disposition)", async () => { + it("quarantines a known sender with high spam score (SR-03 fires regardless of filter mode)", async () => { vi.mocked(store.getProcessorAccountContext).mockResolvedValueOnce( - { ...DEFAULT_CTX, emailConfig: makeAlias({ filterMode: "strict", approvedSenders: ["example.com"] }) }, + { ...DEFAULT_CTX, emailConfig: makeAlias({ filterMode: "quarantine_visible" }) }, ); + // Sender is known/approved but spam score is too high — SR-03 quarantines independently of filter mode vi.mocked(classifier.classify as ReturnType).mockResolvedValueOnce({ ...validClassification, spamScore: 0.95, @@ -791,25 +935,26 @@ describe("SignalProcessor", () => { expect(store.saveArc).not.toHaveBeenCalled(); const saved = vi.mocked(store.saveSignal).mock.calls[0]![0] as Signal; - expect(saved.status).toBe("quarantined"); + expect(saved.status).toBe("quarantine_visible"); }); it("allow_all mode auto-approves new sender without blocking", async () => { vi.mocked(store.getProcessorAccountContext).mockResolvedValueOnce( - { ...DEFAULT_CTX, emailConfig: makeAlias({ filterMode: "allow_all", approvedSenders: [] }) }, + { ...DEFAULT_CTX, emailConfig: makeAlias({ filterMode: "allow_all" }) }, ); + vi.mocked(store.getSender).mockResolvedValueOnce(null); // sender not yet in list await processor.process(makeSqsEvent([{}])); expect(store.saveArc).toHaveBeenCalledOnce(); - const savedConfig = vi.mocked(store.saveAlias).mock.calls[0]![0] as Alias; - expect(savedConfig.approvedSenders).toContain("example.com"); + expect(store.saveSender).toHaveBeenCalledWith(TEST_ACCOUNT_ID, expect.any(String), "example.com", "allow"); }); it("saves blocked signal with classification data for user review", async () => { vi.mocked(store.getProcessorAccountContext).mockResolvedValueOnce( - { ...DEFAULT_CTX, emailConfig: makeAlias({ approvedSenders: [] }) }, + { ...DEFAULT_CTX, emailConfig: makeAlias() }, ); + vi.mocked(store.getSender).mockResolvedValueOnce(null); await processor.process(makeSqsEvent([{}])); @@ -822,7 +967,7 @@ describe("SignalProcessor", () => { it("quarantines new address when newAddressHandling is block_until_approved (default disposition)", async () => { vi.mocked(store.getProcessorAccountContext).mockResolvedValueOnce({ retentionDays: 0, - filtering: { newAddressHandling: "block_until_approved", defaultFilterMode: "notify_new" }, + filtering: { newAddressHandling: "block_until_approved", defaultFilterMode: "quarantine_visible" }, emailConfig: null, registeredDomains: [], userEmails: [], @@ -832,7 +977,8 @@ describe("SignalProcessor", () => { expect(store.saveArc).not.toHaveBeenCalled(); const saved = vi.mocked(store.saveSignal).mock.calls[0]![0] as Signal; - expect(saved.status).toBe("quarantined"); + // emailConfig is null → effectiveSenderEntry = null → system:sender:untrusted → filter mode fallback (quarantine_visible) + expect(saved.status).toBe("quarantine_visible"); }); }); @@ -843,8 +989,9 @@ describe("SignalProcessor", () => { describe("global reputation tracking", () => { it("updates reputation with wasBlocked=true for blocked signals", async () => { vi.mocked(store.getProcessorAccountContext).mockResolvedValueOnce( - { ...DEFAULT_CTX, emailConfig: makeAlias({ approvedSenders: [] }) }, + { ...DEFAULT_CTX, emailConfig: makeAlias() }, ); + vi.mocked(store.getSender).mockResolvedValueOnce(null); await processor.process(makeSqsEvent([{}])); @@ -1070,13 +1217,10 @@ describe("SignalProcessor", () => { expect(baseUrgency("test", { workflow: "test", triggeredBy: "user" })).toBe("high"); }); - it("support is critical only when priority=urgent", () => { - expect(baseUrgency("support", { workflow: "support", eventType: "ticket_opened", service: "Zendesk", priority: "urgent" })).toBe("critical"); - }); - - it("support is normal for high/normal/low priorities", () => { - expect(baseUrgency("support", { workflow: "support", eventType: "ticket_updated", service: "Zendesk", priority: "high" })).toBe("normal"); - expect(baseUrgency("support", { workflow: "support", eventType: "ticket_resolved", service: "Zendesk", priority: "normal" })).toBe("normal"); + it("support falls through to normal (urgency handled by system rules SR-20–SR-24)", () => { + expect(baseUrgency("support", { workflow: "support", eventType: "ticket_updated", service: "Zendesk", priority: "urgent" })).toBe("normal"); + expect(baseUrgency("support", { workflow: "support", eventType: "awaiting_response", service: "Zendesk" })).toBe("normal"); + expect(baseUrgency("support", { workflow: "support", eventType: "ticket_opened", service: "Zendesk" })).toBe("normal"); }); it("content is always low", () => { @@ -1097,33 +1241,126 @@ describe("SignalProcessor", () => { expect(baseUrgency("travel", { workflow: "travel", travelType: "flight", provider: "Delta" })).toBe("normal"); }); - it("conversation is normal", () => { + it("conversation falls through to normal (urgency handled by system rules SR-15–SR-16)", () => { + expect(baseUrgency("conversation", { workflow: "conversation", isReply: false, sentiment: "urgent", requiresReply: true })).toBe("normal"); + expect(baseUrgency("conversation", { workflow: "conversation", isReply: false, sentiment: "positive", requiresReply: false })).toBe("normal"); expect(baseUrgency("conversation", { workflow: "conversation", isReply: false, sentiment: "neutral", requiresReply: false })).toBe("normal"); }); - it("crm is normal", () => { + it("crm falls through to normal (urgency handled by system rules SR-17–SR-19)", () => { + expect(baseUrgency("crm", { workflow: "crm", crmType: "contract", urgency: "low", requiresReply: false })).toBe("normal"); expect(baseUrgency("crm", { workflow: "crm", crmType: "sales_outreach", urgency: "high", requiresReply: true })).toBe("normal"); + expect(baseUrgency("crm", { workflow: "crm", crmType: "follow_up", urgency: "medium", requiresReply: false })).toBe("normal"); }); }); - describe("priorityCalculator", () => { - it("returns base urgency when arc has no sent messages", () => { - const arc = makeArc({}); - const signal = { workflow: "conversation", workflowData: { workflow: "conversation", isReply: false, sentiment: "neutral", requiresReply: false } } as Parameters[1]; - expect(priorityCalculator(arc, signal)).toBe("normal"); + // ------------------------------------------------------------------------- + // Workflow-specific urgency rules (SR-15–SR-24) + // ------------------------------------------------------------------------- + + describe("workflow urgency system rules", () => { + async function processWithWorkflow(classification: Partial): Promise { + const full: ClassificationOutput = { + workflow: "conversation", + workflowData: { workflow: "conversation", isReply: false, sentiment: "neutral", requiresReply: false }, + spamScore: 0.05, summary: "test", labels: [], + classificationModelId: "us.anthropic.claude-opus-4-5-20251101-v1:0", + ...classification, + }; + vi.mocked(classifier.classify as ReturnType).mockResolvedValueOnce(full); + await processor.process(makeSqsEvent([{ sesMessageId: randomUUID() }])); + return vi.mocked(store.saveArc).mock.calls.at(-1)![0] as Arc; + } + + it("SR-15: conversation + requiresReply + urgent sentiment → high urgency", async () => { + const arc = await processWithWorkflow({ workflow: "conversation", workflowData: { workflow: "conversation", isReply: false, sentiment: "urgent", requiresReply: true } }); + expect(arc.urgency).toBe("high"); + }); + + it("SR-15: conversation + requiresReply + negative sentiment → high urgency", async () => { + const arc = await processWithWorkflow({ workflow: "conversation", workflowData: { workflow: "conversation", isReply: false, sentiment: "negative", requiresReply: true } }); + expect(arc.urgency).toBe("high"); + }); + + it("SR-16: conversation with no prior replies → low urgency", async () => { + const arc = await processWithWorkflow({ workflow: "conversation", workflowData: { workflow: "conversation", isReply: false, sentiment: "neutral", requiresReply: false } }); + expect(arc.urgency).toBe("low"); + }); + + it("SR-16: conversation with prior replies (system:replied) → not low (falls back to arc urgency)", async () => { + vi.mocked(arcMatcher.findMatch).mockResolvedValueOnce(makeArc({ + workflow: "conversation", labels: [], urgency: "normal", + sentMessageIds: [""], + })); + vi.mocked(classifier.classify as ReturnType).mockResolvedValueOnce({ + workflow: "conversation", workflowData: { workflow: "conversation", isReply: true, sentiment: "neutral", requiresReply: false }, + spamScore: 0.05, summary: "test", labels: [], classificationModelId: "us.anthropic.claude-opus-4-5-20251101-v1:0", + }); + await processor.process(makeSqsEvent([{ sesMessageId: randomUUID() }])); + const signal = vi.mocked(store.saveSignal).mock.calls.at(-1)![0] as Signal; + expect(signal.urgency).toBe("normal"); }); - it("promotes to at least high when arc has sent messages", () => { - const arc = makeArc({ sentMessageIds: [""] }); - const signal = { workflow: "content", workflowData: { workflow: "content", contentType: "newsletter", publisher: "TLDR" } } as unknown as Parameters[1]; - expect(priorityCalculator(arc, signal)).toBe("high"); + it("SR-17: crm + contract → high urgency", async () => { + const arc = await processWithWorkflow({ workflow: "crm", workflowData: { workflow: "crm", crmType: "contract", urgency: "low", requiresReply: false } }); + expect(arc.urgency).toBe("high"); }); - it("does not demote critical when arc has sent messages", () => { - const arc = makeArc({ sentMessageIds: [""] }); - const signal = { workflow: "auth", workflowData: { workflow: "auth", authType: "otp", service: "GitHub" } } as Parameters[1]; - expect(priorityCalculator(arc, signal)).toBe("critical"); + it("SR-17: crm + proposal → high urgency", async () => { + const arc = await processWithWorkflow({ workflow: "crm", workflowData: { workflow: "crm", crmType: "proposal", urgency: "low", requiresReply: false } }); + expect(arc.urgency).toBe("high"); }); + + it("SR-18: crm + urgency:high → high urgency", async () => { + const arc = await processWithWorkflow({ workflow: "crm", workflowData: { workflow: "crm", crmType: "sales_outreach", urgency: "high", requiresReply: true } }); + expect(arc.urgency).toBe("high"); + }); + + it("SR-19: crm + urgency:low → low urgency", async () => { + const arc = await processWithWorkflow({ workflow: "crm", workflowData: { workflow: "crm", crmType: "sales_outreach", urgency: "low", requiresReply: false } }); + expect(arc.urgency).toBe("low"); + }); + + it("crm + urgency:medium → normal urgency (label fallback)", async () => { + const arc = await processWithWorkflow({ workflow: "crm", workflowData: { workflow: "crm", crmType: "follow_up", urgency: "medium", requiresReply: false } }); + expect(arc.urgency).toBe("normal"); + }); + + it("SR-20: support + priority:urgent → critical urgency", async () => { + const arc = await processWithWorkflow({ workflow: "support", workflowData: { workflow: "support", eventType: "ticket_updated", service: "Zendesk", priority: "urgent" } }); + expect(arc.urgency).toBe("critical"); + }); + + it("SR-21: support + priority:high → high urgency", async () => { + const arc = await processWithWorkflow({ workflow: "support", workflowData: { workflow: "support", eventType: "ticket_updated", service: "Zendesk", priority: "high" } }); + expect(arc.urgency).toBe("high"); + }); + + it("SR-22: support + awaiting_response → high urgency", async () => { + const arc = await processWithWorkflow({ workflow: "support", workflowData: { workflow: "support", eventType: "awaiting_response", service: "Zendesk" } }); + expect(arc.urgency).toBe("high"); + }); + + it("SR-23: support + priority:low → low urgency", async () => { + const arc = await processWithWorkflow({ workflow: "support", workflowData: { workflow: "support", eventType: "ticket_updated", service: "Zendesk", priority: "low" } }); + expect(arc.urgency).toBe("low"); + }); + + it("SR-24: support + ticket_opened → low urgency", async () => { + const arc = await processWithWorkflow({ workflow: "support", workflowData: { workflow: "support", eventType: "ticket_opened", service: "Zendesk" } }); + expect(arc.urgency).toBe("low"); + }); + + it("SR-24: support + ticket_resolved → low urgency", async () => { + const arc = await processWithWorkflow({ workflow: "support", workflowData: { workflow: "support", eventType: "ticket_resolved", service: "Zendesk" } }); + expect(arc.urgency).toBe("low"); + }); + + it("SR-20 wins over SR-24: support + priority:urgent + ticket_opened → critical (first-rule-wins)", async () => { + const arc = await processWithWorkflow({ workflow: "support", workflowData: { workflow: "support", eventType: "ticket_opened", service: "Zendesk", priority: "urgent" } }); + expect(arc.urgency).toBe("critical"); + }); + }); // ------------------------------------------------------------------------- @@ -1142,7 +1379,7 @@ describe("SignalProcessor", () => { it("processes onboarding emails as active when no blocking rule is configured", async () => { vi.mocked(classifier.classify as ReturnType).mockResolvedValueOnce(onboardingClassification); - vi.mocked(store.listRules).mockResolvedValueOnce([]); // no system rules — SR-01 (block onboarding) is disabled + vi.mocked(store.listEnabledRules).mockResolvedValueOnce([]); // no system rules — SR-01 (block onboarding) is disabled await processor.process(makeSqsEvent([{}])); @@ -1154,7 +1391,7 @@ describe("SignalProcessor", () => { it("blocks onboarding emails when a block rule targeting system:workflow:onboarding is active", async () => { vi.mocked(classifier.classify as ReturnType).mockResolvedValueOnce(onboardingClassification); - vi.mocked(store.listRules).mockResolvedValueOnce([ + vi.mocked(store.listEnabledRules).mockResolvedValueOnce([ makeRule({ condition: JSON.stringify({ "in": ["system:workflow:onboarding", { var: "arc.labels" }] }), actions: [{ type: "block" }] }), ]); @@ -1170,7 +1407,7 @@ describe("SignalProcessor", () => { it("quarantines onboarding emails when a quarantine rule is active", async () => { vi.mocked(classifier.classify as ReturnType).mockResolvedValueOnce(onboardingClassification); - vi.mocked(store.listRules).mockResolvedValueOnce([ + vi.mocked(store.listEnabledRules).mockResolvedValueOnce([ makeRule({ condition: JSON.stringify({ "in": ["system:workflow:onboarding", { var: "arc.labels" }] }), actions: [{ type: "quarantine" }] }), ]); @@ -1180,7 +1417,8 @@ describe("SignalProcessor", () => { expect(store.saveArc).not.toHaveBeenCalled(); const saved = vi.mocked(store.saveSignal).mock.calls[0]![0] as Signal; - expect(saved.status).toBe("quarantined"); + // Plain `quarantine` action → quarantine_visible (shown in review queue) + expect(saved.status).toBe("quarantine_visible"); expect(notifier.notifyBlocked).toHaveBeenCalledOnce(); }); }); @@ -1252,35 +1490,40 @@ describe("SignalProcessor", () => { processor = new SignalProcessor({ store, mimeParser, classifier, arcMatcher, ruleEvaluator, notifier }); }); - it("creates a notice arc with status=archived (buried on arrival, never active)", async () => { + it("blocks status emails silently — no arc created, signal saved as blocked", async () => { vi.mocked(classifier.classify as ReturnType).mockResolvedValueOnce(noticeClassification); await processor.process(makeSqsEvent([{}])); - const arc = vi.mocked(store.saveArc).mock.calls[0]![0] as Arc; - expect(arc.status).toBe("archived"); - expect(arc.workflow).toBe("status"); + expect(store.saveArc).not.toHaveBeenCalled(); + const signal = vi.mocked(store.saveSignal).mock.calls[0]![0] as Signal; + expect(signal.status).toBe("blocked"); + expect(signal.workflow).toBe("status"); }); - it("does not call notifier for a notice arc even when the signal is new", async () => { + it("does not call notifier for a blocked status email", async () => { vi.mocked(classifier.classify as ReturnType).mockResolvedValueOnce(noticeClassification); await processor.process(makeSqsEvent([{}])); expect(notifier.notify).not.toHaveBeenCalled(); + expect(notifier.notifyBlocked).not.toHaveBeenCalled(); }); - it("does not bump lastSignalAt on an existing arc when a rule archives the incoming signal", async () => { + it("blocks status emails from untrusted senders (SR-05 rule fires, fallback does not apply)", async () => { vi.mocked(classifier.classify as ReturnType).mockResolvedValueOnce(noticeClassification); - // notice uses groupingKey so findArcByGroupingKey is the match path - vi.mocked(store.findArcByGroupingKey).mockResolvedValueOnce( - makeArc({ lastSignalAt: "2024-01-10T00:00:00Z" }), - ); + // Untrusted sender: no approved sender entry — filter-mode fallback would quarantine, but SR-05 fires first + vi.mocked(store.getProcessorAccountContext).mockResolvedValueOnce({ + ...DEFAULT_CTX, + emailConfig: makeAlias(), + }); + vi.mocked(store.getSender).mockResolvedValueOnce(null); await processor.process(makeSqsEvent([{}])); - const saved = vi.mocked(store.saveArc).mock.calls[0]![0] as Arc; - expect(saved.lastSignalAt).toBe("2024-01-10T00:00:00Z"); // unchanged + const signal = vi.mocked(store.saveSignal).mock.calls[0]![0] as Signal; + expect(signal.status).toBe("blocked"); // SR-05 sets status → fallback skipped (hasStatusOutcome = true) + expect(store.saveArc).not.toHaveBeenCalled(); }); }); @@ -1472,12 +1715,11 @@ describe("SignalProcessor", () => { // ------------------------------------------------------------------------- describe("spam threshold override", () => { - it("blocks signal when per-address spamScoreThreshold is lower than default and score exceeds it", async () => { + it("quarantines signal when per-address spamScoreThreshold is lower than default and score exceeds it", async () => { vi.mocked(store.getProcessorAccountContext).mockResolvedValueOnce({ ...DEFAULT_CTX, emailConfig: makeAlias({ - filterMode: "strict", - approvedSenders: ["example.com"], + filterMode: "quarantine_visible", spamScoreThreshold: 0.5, }), }); @@ -1488,16 +1730,16 @@ describe("SignalProcessor", () => { await processor.process(makeSqsEvent([{}])); + // DEFAULT_SENDER_ENTRY is approved → SR-03 fires → quarantine_visible const saved = vi.mocked(store.saveSignal).mock.calls[0]![0] as Signal; - expect(saved.status).toBe("quarantined"); - expect(saved.status).toBe("quarantined"); + expect(saved.status).toBe("quarantine_visible"); }); it("uses account-level spamScoreThreshold when no per-address override is set", async () => { vi.mocked(store.getProcessorAccountContext).mockResolvedValueOnce({ ...DEFAULT_CTX, - emailConfig: makeAlias({ filterMode: "strict", approvedSenders: ["example.com"] }), - filtering: { defaultFilterMode: "notify_new", newAddressHandling: "auto_allow", spamScoreThreshold: 0.6 }, + emailConfig: makeAlias({ filterMode: "quarantine_visible" }), + filtering: { defaultFilterMode: "quarantine_visible", newAddressHandling: "auto_allow", spamScoreThreshold: 0.6 }, }); vi.mocked(classifier.classify as ReturnType).mockResolvedValueOnce({ ...validClassification, @@ -1506,9 +1748,9 @@ describe("SignalProcessor", () => { await processor.process(makeSqsEvent([{}])); + // DEFAULT_SENDER_ENTRY is approved → SR-03 fires → quarantine_visible const saved = vi.mocked(store.saveSignal).mock.calls[0]![0] as Signal; - expect(saved.status).toBe("quarantined"); - expect(saved.status).toBe("quarantined"); + expect(saved.status).toBe("quarantine_visible"); }); }); @@ -1524,7 +1766,8 @@ describe("SignalProcessor", () => { name: "Forward all", condition: "true", actions: [{ type: "forward", value: "backup@personal.com" }], - position: 0, + status: "enabled", + priorityOrder: 0, createdAt: "2024-01-01T00:00:00Z", updatedAt: "2024-01-01T00:00:00Z", }; @@ -1533,7 +1776,7 @@ describe("SignalProcessor", () => { it("passes dkimPass=true and dmarcPass=true when both SES verdicts are PASS", async () => { const forwarder: Forwarder = { forward: vi.fn().mockResolvedValue(undefined) }; const proc = new SignalProcessor({ store, mimeParser, classifier, arcMatcher, ruleEvaluator, forwarder }); - vi.mocked(store.listRules).mockResolvedValueOnce([makeForwardRule()]); + vi.mocked(store.listEnabledRules).mockResolvedValueOnce([makeForwardRule()]); await proc.process(makeSqsEvent([{ dkimVerdict: "PASS", dmarcVerdict: "PASS" }])); @@ -1548,7 +1791,7 @@ describe("SignalProcessor", () => { it("passes dkimPass=false and dmarcPass=false when SES verdicts are FAIL and GRAY", async () => { const forwarder: Forwarder = { forward: vi.fn().mockResolvedValue(undefined) }; const proc = new SignalProcessor({ store, mimeParser, classifier, arcMatcher, ruleEvaluator, forwarder }); - vi.mocked(store.listRules).mockResolvedValueOnce([makeForwardRule()]); + vi.mocked(store.listEnabledRules).mockResolvedValueOnce([makeForwardRule()]); await proc.process(makeSqsEvent([{ dkimVerdict: "FAIL", dmarcVerdict: "GRAY" }])); @@ -1567,13 +1810,14 @@ describe("SignalProcessor", () => { describe("rule actions — assign_workflow and delete", () => { it("assign_workflow action changes the arc workflow to the specified value", async () => { - vi.mocked(store.listRules).mockResolvedValueOnce([{ + vi.mocked(store.listEnabledRules).mockResolvedValueOnce([{ id: "rw-rule", accountId: TEST_ACCOUNT_ID, name: "Reclassify as content", condition: "true", actions: [{ type: "assign_workflow", value: "content" }], - position: 0, + status: "enabled", + priorityOrder: 0, createdAt: "2024-01-01T00:00:00Z", updatedAt: "2024-01-01T00:00:00Z", }]); @@ -1585,13 +1829,14 @@ describe("SignalProcessor", () => { }); it("delete action sets arc.status=deleted and records arc.deletedAt", async () => { - vi.mocked(store.listRules).mockResolvedValueOnce([{ + vi.mocked(store.listEnabledRules).mockResolvedValueOnce([{ id: "del-rule", accountId: TEST_ACCOUNT_ID, name: "Auto-delete promotions", condition: "true", actions: [{ type: "delete" }], - position: 0, + status: "enabled", + priorityOrder: 0, createdAt: "2024-01-01T00:00:00Z", updatedAt: "2024-01-01T00:00:00Z", }]); @@ -1604,7 +1849,7 @@ describe("SignalProcessor", () => { }); it("multiple actions in one rule are all applied in order", async () => { - vi.mocked(store.listRules).mockResolvedValueOnce([{ + vi.mocked(store.listEnabledRules).mockResolvedValueOnce([{ id: "multi-rule", accountId: TEST_ACCOUNT_ID, name: "Label and archive", @@ -1613,7 +1858,8 @@ describe("SignalProcessor", () => { { type: "assign_label", value: "archived-auto" }, { type: "archive" }, ], - position: 0, + status: "enabled", + priorityOrder: 0, createdAt: "2024-01-01T00:00:00Z", updatedAt: "2024-01-01T00:00:00Z", }]); diff --git a/src/processor/processor.ts b/src/processor/processor.ts index 35a6f7f..e1f4bb8 100644 --- a/src/processor/processor.ts +++ b/src/processor/processor.ts @@ -1,7 +1,6 @@ import { randomUUID } from "crypto"; import type { SQSEvent } from "aws-lambda"; -import type { Signal, Arc, Rule, Workflow, WorkflowData, Alias, AccountFilteringConfig, SignalSource, SchedulingData, SignalStatus, Domain, ArcUrgency, SenderFilterMode } from "../types/index.js"; -import { baseUrgency } from "./priority.js"; +import type { Signal, Arc, Rule, Workflow, WorkflowData, Alias, AliasSender, SenderMode, AccountFilteringConfig, SignalSource, SchedulingData, SignalStatus, Domain, ArcUrgency, SenderFilterMode, MatchedRuleResult } from "../types/index.js"; import type { MimeParser } from "./mime.js"; import type { SignalClassifier } from "../classifier/classifier.js"; import { getETLD1, assignSystemLabels, DEFAULT_SPAM_SCORE_THRESHOLD } from "./filter.js"; @@ -24,9 +23,12 @@ export interface ProcessorDatabase { getArc(accountId: string, id: string): Promise; findArcByGroupingKey(accountId: string, key: string): Promise; saveArc(arc: Arc): Promise; - listRules(accountId: string): Promise; + listEnabledRules(accountId: string): Promise; getProcessorAccountContext(accountId: string, recipientAddress: string): Promise; saveAlias(alias: Alias): Promise; + getSender(accountId: string, address: string, domain: string): Promise; + saveSender(accountId: string, address: string, domain: string, mode: SenderMode): Promise; + getTemplate(accountId: string, id: string): Promise; updateGlobalReputation(domain: string, update: { wasSpam: boolean; wasBlocked: boolean }): Promise; getDomainByName(accountId: string, domainName: string): Promise | null>; } @@ -37,7 +39,7 @@ export interface ArcMatcher { } export interface RuleEvaluator { - evaluate(rule: Rule, context: { signal: Signal; arc: Arc; isMatchedArc: boolean }): boolean; + evaluate(rule: Rule, context: { signal: Signal; arc: Arc; isMatchedArc: boolean }): Promise; } export interface Notifier { @@ -98,6 +100,7 @@ interface InboundSignalMessage { interface ProcessingOutcome { block: boolean; quarantine: boolean; + quarantineHidden: boolean; // true → quarantine_hidden status; false → quarantine_visible approveSender: boolean; archive: boolean; delete: boolean; @@ -106,12 +109,15 @@ interface ProcessingOutcome { forwardAddresses: string[]; additionalLabels: string[]; doPong: boolean; + autoReplyTemplateIds: string[]; + autoDraftTemplateIds: string[]; } function emptyOutcome(): ProcessingOutcome { return { block: false, quarantine: false, + quarantineHidden: false, approveSender: false, archive: false, delete: false, @@ -119,31 +125,67 @@ function emptyOutcome(): ProcessingOutcome { forwardAddresses: [], additionalLabels: [], doPong: false, + autoReplyTemplateIds: [], + autoDraftTemplateIds: [], }; } -function applyRules( +async function applyRules( rules: Rule[], context: { signal: Signal; arc: Arc; isMatchedArc: boolean }, evaluator: RuleEvaluator, - outcome: ProcessingOutcome, -): ProcessingOutcome { +): Promise { + const matchedRules: MatchedRuleResult[] = []; for (const rule of rules) { - if (!evaluator.evaluate(rule, context)) continue; - for (const action of rule.actions) { - if (action.disabled) continue; + if (!await evaluator.evaluate(rule, context)) continue; + const actions = rule.actions.filter((a) => !a.disabled).map(({ type, value }) => ({ type, ...(value !== undefined ? { value } : {}) })); + const labelsAdded = actions.filter((a) => a.type === "assign_label" && a.value).map((a) => a.value!); + const statusChange: MatchedRuleResult["statusChange"] = ( + actions.some((a) => a.type === "block") ? "blocked" : + actions.some((a) => a.type === "quarantine_hidden") ? "quarantine_hidden" : + actions.some((a) => a.type === "quarantine") ? "quarantine_visible" : + actions.some((a) => a.type === "archive") ? "archived" : + actions.some((a) => a.type === "delete") ? "deleted" : + undefined + ); + matchedRules.push({ ruleId: rule.id, actions, labelsAdded, ...(statusChange ? { statusChange } : {}) }); + // assign_workflow mutates the arc so subsequent rules evaluate against the updated workflow + const workflowAction = actions.find((a) => a.type === "assign_workflow"); + if (workflowAction?.value) context.arc.workflow = workflowAction.value as Workflow; + } + return matchedRules; +} + +function deriveOutcome(matchedRules: MatchedRuleResult[]): ProcessingOutcome { + const outcome = emptyOutcome(); + let statusSet = false; // first-rule-wins: the first status-changing action determines fate + let urgencySet = false; // first-rule-wins: the first set_urgency action determines urgency + for (const { actions } of matchedRules) { + for (const action of actions) { switch (action.type) { - case "block": outcome.block = true; break; - case "quarantine": outcome.quarantine = true; break; + case "block": + if (!statusSet) { outcome.block = true; statusSet = true; } + break; + case "quarantine": + if (!statusSet) { outcome.quarantine = true; statusSet = true; } + break; + case "quarantine_hidden": + if (!statusSet) { outcome.quarantine = true; outcome.quarantineHidden = true; statusSet = true; } + break; + case "archive": + if (!statusSet) { outcome.archive = true; statusSet = true; } + break; + case "delete": + if (!statusSet) { outcome.delete = true; statusSet = true; } + break; case "approve_sender": outcome.approveSender = true; break; - case "archive": outcome.archive = true; break; - case "delete": outcome.delete = true; break; case "suppress_notification": outcome.suppressNotification = true; break; - case "set_urgency": if (action.value) outcome.urgency = action.value as ArcUrgency; break; + case "set_urgency": if (!urgencySet && action.value) { outcome.urgency = action.value as ArcUrgency; urgencySet = true; } break; case "assign_label": if (action.value) outcome.additionalLabels.push(action.value); break; - case "assign_workflow": if (action.value) context.arc.workflow = action.value as Workflow; break; case "forward": if (action.value) outcome.forwardAddresses.push(action.value); break; case "pong": outcome.doPong = true; break; + case "auto_reply": if (action.value) outcome.autoReplyTemplateIds.push(action.value); break; + case "auto_draft": if (action.value) outcome.autoDraftTemplateIds.push(action.value); break; } } } @@ -155,22 +197,34 @@ function applyRules( // --------------------------------------------------------------------------- const in_ = (label: string) => ({ "in": [label, { "var": "arc.labels" }] }); +const wf_ = (w: string) => ({ "==": [{ "var": "signal.workflow" }, w] }); +const wfData_ = (field: string) => ({ "var": `signal.workflowData.${field}` }); export const SYSTEM_RULES: Rule[] = [ - { id: "SR-14", accountId: "SYSTEM", name: "Auto-approve sender on matched conversation", condition: JSON.stringify({ "and": [in_("system:workflow:conversation"), in_("system:sender:untrusted"), { "var": "isMatchedArc" }] }), actions: [{ type: "approve_sender" }], position: 1, createdAt: "", updatedAt: "" }, - { id: "SR-01", accountId: "SYSTEM", name: "Block onboarding emails", condition: JSON.stringify(in_("system:workflow:onboarding")), actions: [{ type: "block" }], position: 2, createdAt: "", updatedAt: "" }, - { id: "SR-02", accountId: "SYSTEM", name: "Quarantine untrusted senders", condition: JSON.stringify(in_("system:sender:untrusted")), actions: [{ type: "quarantine" }], position: 3, createdAt: "", updatedAt: "" }, - { id: "SR-03", accountId: "SYSTEM", name: "Quarantine high-spam signals", condition: JSON.stringify(in_("system:spam:high")), actions: [{ type: "quarantine" }], position: 4, createdAt: "", updatedAt: "" }, - { id: "SR-04", accountId: "SYSTEM", name: "Suppress notification for medium spam", condition: JSON.stringify(in_("system:spam:medium")), actions: [{ type: "suppress_notification" }], position: 5, createdAt: "", updatedAt: "" }, - { id: "SR-05", accountId: "SYSTEM", name: "Auto-archive status emails", condition: JSON.stringify(in_("system:workflow:status")), actions: [{ type: "archive" }], position: 6, createdAt: "", updatedAt: "" }, - { id: "SR-06", accountId: "SYSTEM", name: "Suppress notification for status emails", condition: JSON.stringify(in_("system:workflow:status")), actions: [{ type: "suppress_notification" }], position: 7, createdAt: "", updatedAt: "" }, - { id: "SR-07", accountId: "SYSTEM", name: "Suppress notification for content emails", condition: JSON.stringify(in_("system:workflow:content")), actions: [{ type: "suppress_notification" }], position: 8, createdAt: "", updatedAt: "" }, - { id: "SR-08", accountId: "SYSTEM", name: "Set urgency: critical", condition: JSON.stringify(in_("system:urgency:critical")), actions: [{ type: "set_urgency", value: "critical" }], position: 9, createdAt: "", updatedAt: "" }, - { id: "SR-09", accountId: "SYSTEM", name: "Set urgency: high", condition: JSON.stringify(in_("system:urgency:high")), actions: [{ type: "set_urgency", value: "high" }], position: 10, createdAt: "", updatedAt: "" }, - { id: "SR-10", accountId: "SYSTEM", name: "Set urgency: normal", condition: JSON.stringify(in_("system:urgency:normal")), actions: [{ type: "set_urgency", value: "normal" }], position: 11, createdAt: "", updatedAt: "" }, - { id: "SR-11", accountId: "SYSTEM", name: "Set urgency: low", condition: JSON.stringify(in_("system:urgency:low")), actions: [{ type: "set_urgency", value: "low" }], position: 12, createdAt: "", updatedAt: "" }, - { id: "SR-12", accountId: "SYSTEM", name: "Set urgency: silent", condition: JSON.stringify(in_("system:urgency:silent")), actions: [{ type: "set_urgency", value: "silent" }], position: 13, createdAt: "", updatedAt: "" }, - { id: "SR-13", accountId: "SYSTEM", name: "Auto-reply to test emails (pong)", condition: JSON.stringify(in_("system:test")), actions: [{ type: "pong" }], position: 14, createdAt: "", updatedAt: "" }, + // --- Sender / content gating (1–8) ---------------------------------------- + { id: "SR-14", accountId: "SYSTEM", name: "Auto-approve sender on matched conversation", condition: JSON.stringify({ "and": [in_("system:workflow:conversation"), in_("system:sender:untrusted"), { "var": "isMatchedArc" }] }), actions: [{ type: "approve_sender" }], status: "enabled", priorityOrder: 1, createdAt: "", updatedAt: "" }, + { id: "SR-01", accountId: "SYSTEM", name: "Block onboarding emails", condition: JSON.stringify(in_("system:workflow:onboarding")), actions: [{ type: "block" }], status: "enabled", priorityOrder: 2, createdAt: "", updatedAt: "" }, + { id: "SR-05", accountId: "SYSTEM", name: "Block status emails", condition: JSON.stringify(in_("system:workflow:status")), actions: [{ type: "block" }], status: "enabled", priorityOrder: 3, createdAt: "", updatedAt: "" }, + { id: "SR-03", accountId: "SYSTEM", name: "Quarantine high-spam signals", condition: JSON.stringify(in_("system:spam:high")), actions: [{ type: "quarantine" }], status: "enabled", priorityOrder: 4, createdAt: "", updatedAt: "" }, + { id: "SR-04", accountId: "SYSTEM", name: "Suppress notification for medium spam", condition: JSON.stringify(in_("system:spam:medium")), actions: [{ type: "suppress_notification" }], status: "enabled", priorityOrder: 6, createdAt: "", updatedAt: "" }, + { id: "SR-06", accountId: "SYSTEM", name: "Suppress notification for status emails", condition: JSON.stringify(in_("system:workflow:status")), actions: [{ type: "suppress_notification" }], status: "enabled", priorityOrder: 7, createdAt: "", updatedAt: "" }, + { id: "SR-07", accountId: "SYSTEM", name: "Suppress notification for content emails", condition: JSON.stringify(in_("system:workflow:content")), actions: [{ type: "suppress_notification" }], status: "enabled", priorityOrder: 8, createdAt: "", updatedAt: "" }, + // --- Workflow-specific urgency (9–18) ---------------------------------------- + // conversation: high when reply is needed and tone is urgent/negative + { id: "SR-15", accountId: "SYSTEM", name: "Conversation: high urgency when reply needed and urgent/negative", condition: JSON.stringify({ "and": [wf_("conversation"), { "==": [wfData_("requiresReply"), true] }, { "in": [wfData_("sentiment"), ["urgent", "negative"]] }] }), actions: [{ type: "set_urgency", value: "high" }], status: "enabled", priorityOrder: 9, createdAt: "", updatedAt: "" }, + { id: "SR-16", accountId: "SYSTEM", name: "Conversation: low urgency when user has never replied", condition: JSON.stringify({ "and": [wf_("conversation"), { "!": [in_("system:replied")] }] }), actions: [{ type: "set_urgency", value: "low" }], status: "enabled", priorityOrder: 10, createdAt: "", updatedAt: "" }, + // crm: contract/proposal always warrant a decision — treat as high regardless of urgency field + { id: "SR-17", accountId: "SYSTEM", name: "CRM: high urgency for contracts and proposals", condition: JSON.stringify({ "and": [wf_("crm"), { "in": [wfData_("crmType"), ["contract", "proposal"]] }] }), actions: [{ type: "set_urgency", value: "high" }], status: "enabled", priorityOrder: 11, createdAt: "", updatedAt: "" }, + { id: "SR-18", accountId: "SYSTEM", name: "CRM: high urgency when urgency field is high", condition: JSON.stringify({ "and": [wf_("crm"), { "==": [wfData_("urgency"), "high"] }] }), actions: [{ type: "set_urgency", value: "high" }], status: "enabled", priorityOrder: 12, createdAt: "", updatedAt: "" }, + { id: "SR-19", accountId: "SYSTEM", name: "CRM: low urgency for low-priority outreach", condition: JSON.stringify({ "and": [wf_("crm"), { "==": [wfData_("urgency"), "low"] }, { "!": [in_("system:replied")] }] }), actions: [{ type: "set_urgency", value: "low" }], status: "enabled", priorityOrder: 13, createdAt: "", updatedAt: "" }, + // support: priority field drives urgency; urgent > priority-based > awaiting_response > lifecycle + { id: "SR-20", accountId: "SYSTEM", name: "Support: critical urgency for urgent-priority tickets", condition: JSON.stringify({ "and": [wf_("support"), { "==": [wfData_("priority"), "urgent"] }] }), actions: [{ type: "set_urgency", value: "critical" }], status: "enabled", priorityOrder: 14, createdAt: "", updatedAt: "" }, + { id: "SR-21", accountId: "SYSTEM", name: "Support: high urgency for high-priority tickets", condition: JSON.stringify({ "and": [wf_("support"), { "==": [wfData_("priority"), "high"] }] }), actions: [{ type: "set_urgency", value: "high" }], status: "enabled", priorityOrder: 15, createdAt: "", updatedAt: "" }, + { id: "SR-22", accountId: "SYSTEM", name: "Support: high urgency when agent is awaiting response", condition: JSON.stringify({ "and": [wf_("support"), { "==": [wfData_("eventType"), "awaiting_response"] }] }), actions: [{ type: "set_urgency", value: "high" }], status: "enabled", priorityOrder: 16, createdAt: "", updatedAt: "" }, + { id: "SR-23", accountId: "SYSTEM", name: "Support: low urgency for low-priority tickets", condition: JSON.stringify({ "and": [wf_("support"), { "==": [wfData_("priority"), "low"] }, { "!": [in_("system:replied")] }] }), actions: [{ type: "set_urgency", value: "low" }], status: "enabled", priorityOrder: 17, createdAt: "", updatedAt: "" }, + // ticket_opened/resolved/closed are passive lifecycle events — low unless urgency field says otherwise (fired after priority rules so those win) + { id: "SR-24", accountId: "SYSTEM", name: "Support: low urgency for passive lifecycle events", condition: JSON.stringify({ "and": [wf_("support"), { "in": [wfData_("eventType"), ["ticket_opened", "ticket_resolved", "ticket_closed"]] }, { "!": [in_("system:replied")] }] }), actions: [{ type: "set_urgency", value: "low" }], status: "enabled", priorityOrder: 18, createdAt: "", updatedAt: "" }, + { id: "SR-13", accountId: "SYSTEM", name: "Auto-reply to test emails (pong)", condition: JSON.stringify(in_("system:test")), actions: [{ type: "pong" }], status: "enabled", priorityOrder: 19, createdAt: "", updatedAt: "" }, ]; // --------------------------------------------------------------------------- @@ -244,7 +298,16 @@ export class SignalProcessor { const senderETLD1 = getETLD1(parsed.from.address); // 3. Embed + classify in parallel - const embedText = [parsed.subject, parsed.textBody ?? ""].join(" ").slice(0, 4000); + const returnPath = parsed.headers["return-path"] ?? parsed.headers["Return-Path"] ?? ""; + const embedText = [ + `Account: ${accountId}`, + `From: ${parsed.from.address}`, + parsed.replyTo ? `Reply-To: ${parsed.replyTo.address}` : "", + returnPath ? `Return-Path: ${returnPath}` : "", + `To: ${recipientAddress}`, + `Subject: ${parsed.subject}`, + parsed.textBody ?? "", + ].filter(Boolean).join("\n").slice(0, 4000); const [embedding, classification] = await Promise.all([ this.classifier.embed(embedText), this.classifier.classify({ @@ -260,8 +323,11 @@ export class SignalProcessor { const now = new Date().toISOString(); - // 4. Fetch account context - const accountCtx = await this.store.getProcessorAccountContext(accountId, recipientAddress); + // 4. Fetch account context + sender entry in parallel + const [accountCtx, senderEntry] = await Promise.all([ + this.store.getProcessorAccountContext(accountId, recipientAddress), + this.store.getSender(accountId, recipientAddress, senderETLD1), + ]); const ttl = accountCtx.retentionDays > 0 ? Math.floor(Date.now() / 1000) + accountCtx.retentionDays * 86400 : undefined; @@ -316,19 +382,20 @@ export class SignalProcessor { // 8. Assign system labels and merge classifier labels const emailConfig = accountCtx.emailConfig; - // Brand-new address (null emailConfig) with auto-allow account policy → treat as allow_all for label purposes const effectiveFilterMode: SenderFilterMode = emailConfig ? emailConfig.filterMode : accountCtx.filtering?.newAddressHandling === "block_until_approved" - ? "notify_new" + ? "quarantine_visible" : "allow_all"; + // When no alias exists for the recipient, sender entries don't apply — treat as no entry + const effectiveSenderEntry = emailConfig ? senderEntry : null; const systemLabels = assignSystemLabels({ workflow: classification.workflow, workflowData: classification.workflowData, spamScore: classification.spamScore, spamScoreThreshold, senderETLD1, - approvedSenders: emailConfig?.approvedSenders ?? [], + senderEntry: effectiveSenderEntry, filterMode: effectiveFilterMode, hasSentMessages: (arc.sentMessageIds?.length ?? 0) > 0, }); @@ -353,34 +420,40 @@ export class SignalProcessor { }); // 10. Evaluate all rules (system rules seeded at low position numbers, user rules at higher positions) - const rules = await this.store.listRules(accountId); - const outcome = applyRules(rules, { signal: signalShell, arc, isMatchedArc }, this.ruleEvaluator, emptyOutcome()); - - // Block/quarantine: approveSender overrides quarantine (SR-14 fires before SR-02) - if (outcome.block || (outcome.quarantine && !outcome.approveSender)) { - const status: SignalStatus = outcome.quarantine ? "quarantined" : "blocked"; - const blockedSignal = buildSignal({ - status, - accountId, - sesMessageId, - recipientAddress, - parsed, - classification, - s3Key, - receivedAt: timestamp, - now, - ...(ttl !== undefined ? { ttl } : {}), - }); - await this.store.saveSignal(blockedSignal); - if (status === "quarantined" && this.notifier) { - await this.notifier.notifyBlocked(accountId, blockedSignal).catch((err) => { + const rules = await this.store.listEnabledRules(accountId); + const matchedRules = await applyRules(rules, { signal: signalShell, arc, isMatchedArc }, this.ruleEvaluator); + const outcome = deriveOutcome(matchedRules); + + // Fallback: if no rule set a status, apply filter mode for untrusted senders + const hasStatusOutcome = outcome.block || outcome.quarantine || outcome.archive || outcome.delete; + if (!hasStatusOutcome && arc.labels.includes("system:sender:untrusted")) { + switch (effectiveFilterMode) { + case "block": outcome.block = true; break; + case "quarantine_hidden": outcome.quarantine = true; outcome.quarantineHidden = true; break; + case "quarantine_visible": outcome.quarantine = true; break; + // "allow_all": signal proceeds as active + } + } + + const buildArgs = { accountId, sesMessageId, recipientAddress, parsed, classification, s3Key, receivedAt: timestamp, now, ...(ttl !== undefined ? { ttl } : {}) }; + + if (outcome.block) { + await this.store.saveSignal({ ...buildSignal({ status: "blocked", ...buildArgs }), matchedRules }); + this.store.updateGlobalReputation(senderETLD1, { wasSpam: classification.spamScore >= spamScoreThreshold, wasBlocked: true }).catch((err) => console.error("Reputation update failed:", err)); + return; + } + + // approveSender overrides quarantine — SR-14 (auto-approve on matched conversation) fires before SR-02 + if (outcome.quarantine && !outcome.approveSender) { + const quarantineStatus = outcome.quarantineHidden ? "quarantine_hidden" : "quarantine_visible"; + const quarantinedSignal: Signal = { ...buildSignal({ status: quarantineStatus, ...buildArgs }), matchedRules }; + await this.store.saveSignal(quarantinedSignal); + if (this.notifier && !outcome.quarantineHidden) { + await this.notifier.notifyBlocked(accountId, quarantinedSignal).catch((err) => { console.error("Quarantine notification failed:", err); }); } - this.store.updateGlobalReputation(senderETLD1, { - wasSpam: classification.spamScore >= spamScoreThreshold, - wasBlocked: true, - }).catch((err) => console.error("Reputation update failed:", err)); + this.store.updateGlobalReputation(senderETLD1, { wasSpam: classification.spamScore >= spamScoreThreshold, wasBlocked: true }).catch((err) => console.error("Reputation update failed:", err)); return; } @@ -398,11 +471,11 @@ export class SignalProcessor { } if (outcome.archive) arc.status = "archived"; if (outcome.delete) { arc.status = "deleted"; arc.deletedAt = now; } - if (outcome.urgency) arc.urgency = outcome.urgency; - // Fall back to baseUrgency if no set_urgency rule fired - if (!arc.urgency) arc.urgency = baseUrgency(arc.workflow, classification.workflowData); - const signal: Signal = { ...signalShell, arcId: arc.id }; + const signalUrgency = outcome.urgency ?? arc.urgency ?? "normal"; + if (!matchedArc) arc.urgency = signalUrgency; + + const signal: Signal = { ...signalShell, arcId: arc.id, matchedRules, urgency: signalUrgency }; // 12. Pong (driven by SR-13 rule action) if (outcome.doPong && this.testReplier) { @@ -451,7 +524,75 @@ export class SignalProcessor { } } - // 15. Notify + // 15. Auto-reply (fire-and-forget composed emails from templates) + if (this.testReplier && outcome.autoReplyTemplateIds.length > 0) { + const recipientDomain = recipientAddress.split("@")[1] ?? ""; + const domain = await this.store.getDomainByName(accountId, recipientDomain); + if (domain?.senderSetupComplete) { + const vars = { + "signal.subject": parsed.subject, + "sender.name": parsed.from.name ?? "", + "sender.address": parsed.from.address, + "arc.workflow": classification.workflow, + }; + for (const templateId of outcome.autoReplyTemplateIds) { + const tmpl = await this.store.getTemplate(accountId, templateId); + if (!tmpl) continue; + const replyResult = await this.testReplier.pong({ + to: parsed.from.address, + from: recipientAddress, + subject: renderTemplate(tmpl.subject, vars), + body: renderTemplate(tmpl.body, vars), + inReplyTo: sesMessageId, + }).catch((err) => { console.error("Auto-reply failed:", err); return null; }); + if (replyResult) { + arc.sentMessageIds = [...(arc.sentMessageIds ?? []), replyResult.messageId]; + await this.store.saveArc(arc); + } + } + } + } + + // 16. Auto-draft (create held draft signals from templates) + if (outcome.autoDraftTemplateIds.length > 0) { + const vars = { + "signal.subject": parsed.subject, + "sender.name": parsed.from.name ?? "", + "sender.address": parsed.from.address, + "arc.workflow": classification.workflow, + }; + for (const templateId of outcome.autoDraftTemplateIds) { + const tmpl = await this.store.getTemplate(accountId, templateId); + if (!tmpl) continue; + const draft: Signal = { + id: `USR#${randomUUID()}`, + arcId: arc.id, + accountId, + source: "user", + status: "draft", + receivedAt: now, + from: { address: recipientAddress }, + to: [parsed.from], + cc: [], + subject: renderTemplate(tmpl.subject, vars), + textBody: renderTemplate(tmpl.body, vars), + attachments: [], + headers: {}, + recipientAddress: parsed.from.address, + workflow: classification.workflow, + workflowData: classification.workflowData, + spamScore: 0, + summary: "", + classificationModelId: "", + s3Key: "", + createdAt: now, + ...(ttl !== undefined ? { ttl } : {}), + }; + await this.store.saveSignal(draft); + } + } + + // 17. Notify if (this.notifier && !outcome.suppressNotification) { await this.notifier.notify(accountId, arc, signal).catch((err) => { console.error("Notification failed:", err); @@ -469,26 +610,20 @@ export class SignalProcessor { address: string, senderETLD1: string, existing: Alias | null, - defaultFilterMode: AccountFilteringConfig["defaultFilterMode"] = "notify_new", + defaultFilterMode: AccountFilteringConfig["defaultFilterMode"] = "quarantine_visible", ): Promise { const now = new Date().toISOString(); - if (existing) { - await this.store.saveAlias({ - ...existing, - approvedSenders: [...existing.approvedSenders, senderETLD1], - updatedAt: now, - }); - } else { + if (!existing) { await this.store.saveAlias({ id: randomUUID(), accountId, address, filterMode: defaultFilterMode, - approvedSenders: [senderETLD1], createdAt: now, updatedAt: now, }); } + await this.store.saveSender(accountId, address, senderETLD1, "allow"); } } @@ -496,6 +631,10 @@ export class SignalProcessor { // Helpers // --------------------------------------------------------------------------- +function renderTemplate(text: string, vars: Record): string { + return text.replace(/\{\{([^}]+)\}\}/g, (_, key: string) => vars[key.trim()] ?? ""); +} + function buildSignal(opts: { arcId?: string; status: Signal["status"]; diff --git a/src/processor/rule-engine.ts b/src/processor/rule-engine.ts new file mode 100644 index 0000000..ad267fe --- /dev/null +++ b/src/processor/rule-engine.ts @@ -0,0 +1,28 @@ +import { getQuickJS } from "quickjs-emscripten"; +import jsonLogic from "json-logic-js"; + +export async function evalCondition(condition: string, ctx: object): Promise { + if (!condition.startsWith("js:")) { + try { + return Boolean(jsonLogic.apply(JSON.parse(condition) as object, ctx)); + } catch { + return false; + } + } + + const qjs = await getQuickJS(); + const vm = qjs.newContext(); + try { + vm.evalCode(`const ctx = ${JSON.stringify(ctx)};`); + const result = vm.evalCode(`(function(ctx){ ${condition.slice(3)} })(ctx)`); + if (result.error) { + result.error.dispose(); + return false; + } + const val = vm.dump(result.value); + result.value.dispose(); + return Boolean(val); + } finally { + vm.dispose(); + } +} diff --git a/src/processor/rule-evaluator.ts b/src/processor/rule-evaluator.ts index 968c90b..7a896a3 100644 --- a/src/processor/rule-evaluator.ts +++ b/src/processor/rule-evaluator.ts @@ -1,14 +1,13 @@ -import jsonLogic from "json-logic-js"; import type { RuleEvaluator } from "./processor.js"; import type { Rule, Signal, Arc } from "../types/index.js"; +import { evalCondition } from "./rule-engine.js"; export class JsonLogicRuleEvaluator implements RuleEvaluator { - evaluate(rule: Rule, context: { signal: Signal; arc: Arc; isMatchedArc: boolean }): boolean { + async evaluate(rule: Rule, context: { signal: Signal; arc: Arc; isMatchedArc: boolean }): Promise { try { - const condition = JSON.parse(rule.condition) as object; - return Boolean(jsonLogic.apply(condition, context)); + return await evalCondition(rule.condition, context); } catch { - console.error(`Rule ${rule.id} has invalid JSONLogic condition:`, rule.condition); + console.error(`Rule ${rule.id} condition evaluation failed:`, rule.condition); return false; } } diff --git a/src/types/index.ts b/src/types/index.ts index bf2b6f3..5dc2317 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -169,7 +169,9 @@ export interface OnboardingData { export interface StatusData { workflow: "status"; - statusType: "terms_update" | "privacy_policy" | "service_notice" | "government" | "account_notification" | "other"; + statusType: + | "terms_update" | "privacy_policy" | "data_processor" | "cookie_policy" | "compliance" + | "service_notice" | "government" | "account_notification" | "other"; provider: string; effectiveDate?: string; referenceNumber?: string; @@ -222,15 +224,15 @@ export type NewAddressHandling = | "auto_allow" // First contact always allowed; sender eTLD+1 auto-approved (default) | "block_until_approved"; // New addresses blocked until user explicitly approves via POST /arcs -// How strictly an email address filters incoming signals by sender +// Default disposition for emails from unknown senders, applied after rules run export type SenderFilterMode = - | "strict" // sender eTLD+1 must be approved AND spam score must be low - | "sender_match" // sender eTLD+1 must be approved (spam score ignored) - | "notify_new" // allow approved senders, block + notify on new senders (default) - | "allow_all"; // no filtering + | "allow_all" // all senders pass through; system:sender:untrusted label suppressed + | "quarantine_visible" // unknown sender → quarantine, surfaced in review queue (default) + | "quarantine_hidden" // unknown sender → quarantine, hidden from review queue + | "block"; // unknown sender → silent block -// active = visible; quarantined = user notified + shown for review; blocked = silent; draft = user-authored, unsent -export type SignalStatus = "active" | "blocked" | "quarantined" | "draft"; +// active = visible in inbox; quarantine_visible = surfaced in review queue; quarantine_hidden = stored but not shown in queue; blocked = silent drop; draft = user-authored, unsent +export type SignalStatus = "active" | "blocked" | "quarantine_visible" | "quarantine_hidden" | "draft"; // "email" = inbound SES email; "system" = processor-created (e.g. extracted calendar event); "user" = user-created export type SignalSource = "email" | "system" | "user"; @@ -239,7 +241,6 @@ export type SignalSource = "email" | "system" | "user"; export type PushPriority = "interrupt" | "ambient" | "silent"; // Unified urgency level that drives all notification channels (push, digest, UI). -// Derived by priorityCalculator — do not set manually. export type ArcUrgency = "critical" | "high" | "normal" | "low" | "silent"; // Per-recipient-address configuration (an "alias" is any address on a custom domain routed into the system) @@ -248,7 +249,6 @@ export interface Alias { accountId: string; address: string; // The recipient address, e.g. me@mydomain.com filterMode: SenderFilterMode; - approvedSenders: string[]; // eTLD+1 domains (e.g. "amazon.com", "google.com") // Spam score at which a signal is treated as spam (0–1). Overrides account default when set. spamScoreThreshold?: number; // eTLD+1 of the site this alias was created for (set by the extension on alias generation) @@ -257,6 +257,36 @@ export interface Alias { updatedAt: string; } +// Approved/blocked sender domain per alias — stored as individual DynamoDB items +export type SenderMode = "allow" | "block"; + +export interface AliasSender { + accountId: string; + aliasAddress: string; + domain: string; // eTLD+1 + mode: SenderMode; + addedAt: string; +} + +// Email template for auto_reply and auto_draft rule actions +export interface EmailTemplate { + id: string; + accountId: string; + name: string; + subject: string; // supports {{signal.subject}}, {{sender.name}}, {{sender.address}}, {{arc.workflow}} + body: string; // same interpolation; unrecognised tokens render as "" + createdAt: string; + updatedAt: string; +} + +// Active API Gateway WebSocket connection for an account +export interface WsConnection { + connectionId: string; + accountId: string; + connectedAt: string; + ttl?: number; +} + // Account-level filtering defaults export interface AccountFilteringConfig { defaultFilterMode: SenderFilterMode; @@ -295,6 +325,17 @@ export interface Attachment { contentId?: string; } +// --------------------------------------------------------------------------- +// MatchedRuleResult — per-rule trace written to Signal.matchedRules +// --------------------------------------------------------------------------- + +export interface MatchedRuleResult { + ruleId: string; + actions: Array>; + labelsAdded: string[]; + statusChange?: "blocked" | "quarantine_visible" | "quarantine_hidden" | "archived" | "deleted"; +} + // --------------------------------------------------------------------------- // Signal (immutable inbound email event) // --------------------------------------------------------------------------- @@ -303,6 +344,7 @@ export interface Signal { // Discriminated ID encoding origin: "SES#${sesMessageId}" | "SYS#${uuid}" | "USR#${uuid}" id: string; arcId?: string; // Undefined while signal is blocked pending user action + matchedRules?: MatchedRuleResult[]; accountId: string; source: SignalSource; receivedAt: string; // ISO datetime @@ -330,6 +372,7 @@ export interface Signal { s3Key: string; status: SignalStatus; + urgency?: ArcUrgency; createdAt: string; ttl?: number; // Unix seconds; absent = never expire } @@ -354,9 +397,8 @@ export interface Arc { createdAt: string; updatedAt: string; ttl?: number; // Unix seconds; absent = never expire - // Message-IDs of emails the user sent on this arc — checked by priorityCalculator to detect replies + // Message-IDs of emails the user sent on this arc sentMessageIds?: string[]; - // Derived by priorityCalculator; drives push, email digest section, and UI prominence urgency?: ArcUrgency; } @@ -406,11 +448,14 @@ export type RuleActionType = | "delete" | "forward" | "block" - | "quarantine" + | "quarantine" // quarantine, shown in review queue + | "quarantine_hidden" // quarantine, hidden from review queue | "set_urgency" | "suppress_notification" | "pong" - | "approve_sender"; + | "approve_sender" + | "auto_reply" // send immediately using template (value = templateId) + | "auto_draft"; // create draft signal for human review (value = templateId) // System-assigned labels. Return type of assignSystemLabels() — adding here requires explicit approval. // The compile-time gate: assignSystemLabels() returns SystemLabel[], so any unlisted label is a type error. @@ -423,8 +468,6 @@ export type SystemLabel = | "system:spam:high" | "system:spam:medium" | "system:sender:untrusted" - | "system:urgency:critical" | "system:urgency:high" | "system:urgency:normal" - | "system:urgency:low" | "system:urgency:silent" | "system:replied" | "system:test"; @@ -448,13 +491,17 @@ export interface VerifiedForwardingAddress { verifiedAt?: string; } +export type RuleStatus = "enabled" | "disabled"; + export interface Rule { id: string; accountId: string; name: string; condition: string; // JSONLogic expression as JSON string actions: RuleAction[]; - position: number; + status: RuleStatus; + priorityOrder: number; + tags?: Record; createdAt: string; updatedAt: string; }