From 5124bd6c50092193deb279818810815481abf8b3 Mon Sep 17 00:00:00 2001 From: bakerboy448 <55419169+bakerboy448@users.noreply.github.com> Date: Thu, 15 Oct 2026 22:22:47 -0500 Subject: [PATCH 1/5] ci(devvit): type-check, test, build and check Python parity on the hetzner runner --- .github/actionlint.yaml | 2 ++ .github/workflows/devvit.yml | 63 ++++++++++++++++++++++++++++++++++++ .pre-commit-config.yaml | 2 ++ 3 files changed, 67 insertions(+) create mode 100644 .github/actionlint.yaml create mode 100644 .github/workflows/devvit.yml diff --git a/.github/actionlint.yaml b/.github/actionlint.yaml new file mode 100644 index 0000000..02e4a0a --- /dev/null +++ b/.github/actionlint.yaml @@ -0,0 +1,2 @@ +self-hosted-runner: + labels: [hetzner, ci-general, t16, gaming] diff --git a/.github/workflows/devvit.yml b/.github/workflows/devvit.yml new file mode 100644 index 0000000..44ca07d --- /dev/null +++ b/.github/workflows/devvit.yml @@ -0,0 +1,63 @@ +--- +name: Devvit app + +"on": + push: + branches: [main] + paths: + - 'devvit/**' + - 'modlog_wiki_publisher.py' + - '.github/workflows/devvit.yml' + pull_request: + branches: [main] + paths: + - 'devvit/**' + - 'modlog_wiki_publisher.py' + - '.github/workflows/devvit.yml' + workflow_dispatch: + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +permissions: + contents: read + +jobs: + test: + runs-on: [self-hosted, Linux, X64, hetzner] + timeout-minutes: 15 + defaults: + run: + working-directory: devvit + steps: + - name: Checkout + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 + + - name: Set up Node + uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0 + with: + node-version-file: devvit/.nvmrc + cache: npm + cache-dependency-path: devvit/package-lock.json + + - name: Install + run: npm ci --no-fund --no-audit + + - name: Type-check + run: npm run type-check + + - name: Test + run: npm test + + - name: Build + run: npm run build + + - name: Set up uv + uses: astral-sh/setup-uv@1c37ad07a6a961277cf70c0d37d6f313000f5884 # v10.3.0 + + - name: Python parity golden is current + run: | + uv run --no-project --python 3.13 --with praw==8.0.3 \ + python -I test/parity/python_render.py test/parity/fixture.json "$GITHUB_WORKSPACE" > "$RUNNER_TEMP/golden.md" + diff -u test/parity/golden.md "$RUNNER_TEMP/golden.md" diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index d02d477..a404464 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -7,7 +7,9 @@ repos: files: \.json$ - id: check-merge-conflict - id: end-of-file-fixer + exclude: ^devvit/test/parity/golden\.md$ - id: trailing-whitespace + exclude: ^devvit/test/parity/golden\.md$ - id: no-commit-to-branch args: ['--branch', 'main'] - id: check-added-large-files From e9adc38727fea84afe6c9a60d4aaf91a80f235b1 Mon Sep 17 00:00:00 2001 From: bakerboy448 <55419169+bakerboy448@users.noreply.github.com> Date: Thu, 15 Oct 2026 22:22:49 -0500 Subject: [PATCH 2/5] feat(devvit): port the app to Devvit Web as autowikimod Replace the classic @devvit/public-api scaffold with a Devvit Web server (devvit.json, Hono /internal routes, @devvit/web 0.14.8). - Cron publish every 20 minutes; onModAction dropped because its payload carries no reason text and the cron already reads the mod log. - Renderer ported from build_wiki_content, including FR-7 removal/reason merging and FR-8 approval gating via a Redis hash keyed by target id. - Settings: wiki page (default modlog-devvit), days shown/kept (90), action list, anonymize (cannot be disabled), ignored moderators. - Menu: Publish now, Force rebuild, Test connection. --- devvit/.nvmrc | 1 + devvit/README.md | 191 +- devvit/devvit.json | 280 ++ devvit/devvit.yaml | 12 - devvit/package-lock.json | 8481 ++++++++++++++++++++++++++++++---- devvit/package.json | 30 +- devvit/src/core/constants.ts | 81 + devvit/src/core/publish.ts | 121 + devvit/src/core/record.ts | 66 + devvit/src/core/render.ts | 291 ++ devvit/src/core/settings.ts | 116 + devvit/src/core/store.ts | 175 + devvit/src/core/types.ts | 52 + devvit/src/main.ts | 200 - devvit/src/menu.ts | 220 - devvit/src/modlog.ts | 246 - devvit/src/render.ts | 428 -- devvit/src/server/index.ts | 102 + devvit/src/server/reddit.ts | 37 + devvit/src/settings.ts | 387 -- devvit/src/storage.ts | 406 -- devvit/src/types.ts | 193 - devvit/src/wiki.ts | 211 - devvit/tsconfig.json | 32 +- devvit/vite.config.ts | 6 + devvit/vitest.config.ts | 7 + 26 files changed, 8895 insertions(+), 3477 deletions(-) create mode 100644 devvit/.nvmrc create mode 100644 devvit/devvit.json delete mode 100644 devvit/devvit.yaml create mode 100644 devvit/src/core/constants.ts create mode 100644 devvit/src/core/publish.ts create mode 100644 devvit/src/core/record.ts create mode 100644 devvit/src/core/render.ts create mode 100644 devvit/src/core/settings.ts create mode 100644 devvit/src/core/store.ts create mode 100644 devvit/src/core/types.ts delete mode 100644 devvit/src/main.ts delete mode 100644 devvit/src/menu.ts delete mode 100644 devvit/src/modlog.ts delete mode 100644 devvit/src/render.ts create mode 100644 devvit/src/server/index.ts create mode 100644 devvit/src/server/reddit.ts delete mode 100644 devvit/src/settings.ts delete mode 100644 devvit/src/storage.ts delete mode 100644 devvit/src/types.ts delete mode 100644 devvit/src/wiki.ts create mode 100644 devvit/vite.config.ts create mode 100644 devvit/vitest.config.ts diff --git a/devvit/.nvmrc b/devvit/.nvmrc new file mode 100644 index 0000000..ca5c350 --- /dev/null +++ b/devvit/.nvmrc @@ -0,0 +1 @@ +24.18.0 diff --git a/devvit/README.md b/devvit/README.md index 11af86f..65dd3e9 100644 --- a/devvit/README.md +++ b/devvit/README.md @@ -1,142 +1,61 @@ -# RedditModLog (Devvit) +# autowikimod (RedditModLog on Devvit Web) + +Reddit Developer Platform port of [`../modlog_wiki_publisher.py`](../modlog_wiki_publisher.py). +Each install reads its subreddit's moderation log every 20 minutes and writes it +to a wiki page as daily markdown tables with a prefilled modmail inquiry link per +row. Reddit hosts and authenticates it: no bot password, no daemon, no SQLite. + +## Layout + +| Path | Purpose | +| --- | --- | +| `devvit.json` | App manifest: server bundle, Redis + Reddit permissions, cron task, menu items, settings | +| `src/server/index.ts` | Hono server: `/internal/cron/*`, `/internal/menu/*`, `/internal/settings/*` | +| `src/server/reddit.ts` | Mod log and wiki calls through `reddit` from `@devvit/web/server` | +| `src/core/record.ts` | Mod-log entry to stored row (Python `store_processed_action`) | +| `src/core/render.ts` | Page renderer (Python `build_wiki_content`), FR-7 merging, FR-8 approval gating | +| `src/core/store.ts` | Redis layout (hashes and sorted sets only) | +| `src/core/publish.ts` | One cycle: ingest, render, hash-skip, size guard, write, prune | +| `src/core/settings.ts` | Setting validators and config loader | +| `test/` | Vitest + `@devvit/test`; `test/parity/` holds the Python golden comparison | + +## Behaviour + +- Cron `*/20 * * * *` fetches up to 500 mod-log entries, stores new ones, rebuilds the page from storage and writes it only when the content hash (minus the timestamp line) changed. +- Rows match the Python output: human moderators show as `HumanModerator`, AutoModerator removals show as `filter-`, links point only at posts and comments, emails in reasons become `[EMAIL]`, pipes become spaces. +- FR-7: a removal and its `addremovalreason` on the same target render as one row carrying the reason. +- FR-8: an approval is shown only when it reverses a Reddit or AutoModerator removal of the same post or comment. The latest such removal per target is kept in the `prior` Redis hash. +- Pages are trimmed oldest day first at 90% of Reddit's 512 KiB limit. + +## Settings (per subreddit) + +| Setting | Default | Notes | +| --- | --- | --- | +| Wiki page name | `modlog-devvit` | `config/*`, `automoderator`, `usernotes`, `toolbox` are refused | +| Days shown on the wiki page | 90 | 1-365, capped at days kept | +| Days of history kept | 90 | 1-365 | +| Actions shown on the wiki page | removals, spam, `addremovalreason`, approvals | Python `DEFAULT_WIKI_ACTIONS` | +| Anonymize moderators | on | Cannot be turned off, as in the Python app | +| Ignored moderators | empty | Comma-separated; their actions are never stored | + +## Menu (subreddit menu, moderators only) + +- **Modlog: Publish now**: same as one cron run. +- **Modlog: Force rebuild**: rewrite the page from storage without fetching (Python `--force-wiki`). +- **Modlog: Test connection**: checks settings, mod-log read, wiki page and storage, shown in a toast. + +## Develop -A [Reddit Devvit](https://developers.reddit.com) app that publishes a -subreddit's **moderation log** to a subreddit **wiki page** as markdown tables, -each row carrying a prefilled "removal inquiry" modmail link. - -This is the Devvit port of the legacy self-hosted Python/PRAW daemon -(`../modlog_wiki_publisher.py`). It runs entirely on Reddit's Developer Platform -(serverless TypeScript/Node) — **no server, no database, no credentials to -manage**. Reddit OAuth, scheduling, and storage are provided by the platform. - -> Anonymization is mandatory and non-configurable. Real moderator names are -> never published; user profiles are never linked. These are hard invariants -> (see [Parity & invariants](#parity--invariants)). - ---- - -## How it works - -``` - Reddit mod log ──getModerationLog──▶ modlog.ingest ──▶ Redis (dedup + records) - │ - scheduler cron (every 10 min) ──▶ wiki.publishFromStore ◀────┘ - │ - render.buildContent (markdown, capped) - │ - reddit.create/updateWikiPage (hash-skipped) +```bash +npm ci +npm run type-check +npm test +npm run build ``` -- **Scheduler job** (`publish-modlog`, cron `*/10 * * * *`) runs the full cycle: - ingest new actions → render → publish (skipped if unchanged) → prune old - records past the retention window. -- **`ModAction` trigger** does a cheap incremental ingest on each moderation - action so removals are captured promptly; the actual wiki write is coalesced - into the next cron run (no write-per-action churn). -- **Menu items** (moderator-only): *Mod Log: Publish now* and *Mod Log: Show - status*. - -### Module map (`src/`) - -| File | Responsibility | -|---|---| -| `types.ts` | Shared types + frozen constants (single source of truth). No I/O. | -| `storage.ts` | Redis data-access (Repository pattern). The only Redis module. | -| `modlog.ts` | Fetch + filter + extract `ModAction` → `ModRecord` (anonymize, dedup). | -| `render.ts` | **Pure** markdown builder: tables, links, censor, byte-cap, hash. | -| `wiki.ts` | Read/create/update wiki page + hash-skip orchestration. | -| `settings.ts` | Settings schema + validation → frozen `AppConfig`. | -| `menu.ts` | Moderator menu actions (publish now / show status). | -| `main.ts` | Entrypoint: `Devvit.configure` + all capability registrations. | - ---- - -## Develop & deploy - -Prerequisites: Node 18+, a Reddit account enrolled in the Developer Platform, -and the Devvit CLI (installed as a dev dependency here, or globally via -`npm i -g devvit`). +Regenerate the Python golden after changing the fixture or the Python renderer: ```bash -cd devvit -npm install - -# One-time: authenticate the CLI with your Reddit account. -npm run login # devvit login - -# Type-check. -npm run type-check # tsc --build - -# Playtest on a TEST subreddit you moderate (live install, hot-reload). -npm run playtest # devvit playtest - -# Upload a new version to Reddit (private, installable on subs you mod). -npm run deploy # devvit upload - -# Submit for review to make it publicly installable (requires app review). -npm run launch # devvit publish +uv run --no-project --python 3.13 --with praw==8.0.3 \ + python -I test/parity/python_render.py test/parity/fixture.json .. > test/parity/golden.md ``` - -> `devvit playtest` and `devvit upload` install/run the app on a subreddit you -> moderate. Use a throwaway test subreddit first. Publishing to the public app -> directory requires Reddit's app-review process. - -### App name - -`devvit.yaml` sets `name: redditmodlog`. This name is **globally unique** and is -claimed on first `devvit upload`. If it's taken, change it in `devvit.yaml` -before the first upload (renaming after publish is disruptive). - ---- - -## Configuration (per-install settings) - -Configured per subreddit on the app's install **Settings** page. The 19 legacy -Python options collapse to **6 settings** (Devvit owns OAuth, scheduling, and -storage; one option is hardcoded; the rest are obsolete on-platform). - -| Setting | Default | Range | Legacy option | -|---|---|---|---| -| Wiki page name | `modlog` | slug | `wiki_page` | -| Action types to publish | the 7 below | multi-select | `wiki_actions` | -| Additional ignored moderators | *(none)* | CSV usernames | `ignored_moderators` | -| Retention (days) | `90` | 1–365 | `retention_days` | -| Max entries on page | `1000` | 100–2000 | `max_wiki_entries_per_page` | -| Mod-log fetch per run | `500` | 50–1000 | `batch_size` | - -Default tracked actions (INV-7): `removelink`, `removecomment`, `spamlink`, -`spamcomment`, `addremovalreason`, `approvelink`, `approvecomment`. - -`AutoModerator` is **always** excluded (INV-8); the CSV setting only *adds* to -that list. - ---- - -## Parity & invariants - -These rules are carried verbatim from the Python app and enforced in code: - -| # | Invariant | Where enforced | -|---|---|---| -| INV-1 | **Anonymize moderators ALWAYS** — human mods → `HumanModerator`; `AutoModerator`/`Reddit` kept literal. No toggle. | `modlog.anonymizeMod` (at ingest) | -| INV-2 | **Never link user profiles** — only post/comment permalinks become hyperlinks. | `modlog.extractRecord` + `render.contentLink` | -| INV-3 | **512 KB wiki cap** — content trimmed oldest-day-first to ≤90% of cap. | `render.enforceByteCap` (+ `wiki` guard) | -| INV-4 | **Email censor + pipe-escape** on all free text. | `render.censorEmail` / `escapePipes` | -| INV-5 | **Dedup by `ModAction.id`** — each action processed once. | `storage.markSeen` (atomic NX) | -| INV-6 | **Wiki hash-skip** — never write unchanged content (SHA-256). | `wiki.publish` + `storage.getWikiHash` | -| INV-7 | **Default tracked actions** (7 types). | `types.DEFAULT_WIKI_ACTIONS` | -| INV-8 | **Default ignored mods** = `[AutoModerator]`. | `types.DEFAULT_IGNORED_MODS` + `settings` | -| INV-9 | **One install == one subreddit** — Redis is install-scoped; no cross-sub mixing. | platform + `storage` key layout | - -### What changed from the Python app - -- **Auth**: password-grant OAuth → platform-managed (no credentials). -- **Storage**: SQLite (schema v5) → Redis KV (dedup hash, records hash, - time-sorted set, wiki-hash cache). -- **Loop**: continuous daemon (`update_interval`) → scheduler cron + - `ModAction` trigger. -- **Config**: CLI/env/JSON (19 opts) → 6 install settings + 1 hardcoded. -- **Multi-subreddit**: one shared store → one install per subreddit (isolation - is structural). - -See `../devvit-migration/docs/STATUS.md` for the full scaffolded-vs-TODO matrix. diff --git a/devvit/devvit.json b/devvit/devvit.json new file mode 100644 index 0000000..fd1ec9f --- /dev/null +++ b/devvit/devvit.json @@ -0,0 +1,280 @@ +{ + "$schema": "https://developers.reddit.com/schema/config-file.v1.json", + "name": "autowikimod", + "server": { + "dir": "dist/server", + "entry": "index.cjs" + }, + "permissions": { + "reddit": true, + "redis": true + }, + "scheduler": { + "tasks": { + "publish-modlog": { + "endpoint": "/internal/cron/publish", + "cron": "*/20 * * * *" + } + } + }, + "menu": { + "items": [ + { + "label": "Modlog: Publish now", + "description": "Fetch new mod-log entries and update the wiki page.", + "location": "subreddit", + "forUserType": "moderator", + "endpoint": "/internal/menu/publish-now" + }, + { + "label": "Modlog: Force rebuild", + "description": "Rewrite the wiki page from stored entries, even if unchanged.", + "location": "subreddit", + "forUserType": "moderator", + "endpoint": "/internal/menu/force-rebuild" + }, + { + "label": "Modlog: Test connection", + "description": "Check settings, mod-log access, wiki page and storage.", + "location": "subreddit", + "forUserType": "moderator", + "endpoint": "/internal/menu/test-connection" + } + ] + }, + "settings": { + "subreddit": { + "wikiPage": { + "type": "string", + "label": "Wiki page name", + "helpText": "Page the modlog is written to, e.g. modlog.", + "defaultValue": "modlog-devvit", + "validationEndpoint": "/internal/settings/validate-wiki-page" + }, + "displayDays": { + "type": "number", + "label": "Days shown on the wiki page", + "helpText": "1-365. Capped at the retention days.", + "defaultValue": 90, + "validationEndpoint": "/internal/settings/validate-days" + }, + "retentionDays": { + "type": "number", + "label": "Days of history kept", + "helpText": "1-365. Older entries are deleted from app storage.", + "defaultValue": 90, + "validationEndpoint": "/internal/settings/validate-days" + }, + "wikiActions": { + "type": "multiSelect", + "label": "Actions shown on the wiki page", + "options": [ + { + "label": "removelink", + "value": "removelink" + }, + { + "label": "removecomment", + "value": "removecomment" + }, + { + "label": "spamlink", + "value": "spamlink" + }, + { + "label": "spamcomment", + "value": "spamcomment" + }, + { + "label": "approvelink", + "value": "approvelink" + }, + { + "label": "approvecomment", + "value": "approvecomment" + }, + { + "label": "addremovalreason", + "value": "addremovalreason" + }, + { + "label": "addnote", + "value": "addnote" + }, + { + "label": "deletenote", + "value": "deletenote" + }, + { + "label": "banuser", + "value": "banuser" + }, + { + "label": "unbanuser", + "value": "unbanuser" + }, + { + "label": "muteuser", + "value": "muteuser" + }, + { + "label": "unmuteuser", + "value": "unmuteuser" + }, + { + "label": "invitemoderator", + "value": "invitemoderator" + }, + { + "label": "acceptmoderatorinvite", + "value": "acceptmoderatorinvite" + }, + { + "label": "distinguish", + "value": "distinguish" + }, + { + "label": "undistinguish", + "value": "undistinguish" + }, + { + "label": "sticky", + "value": "sticky" + }, + { + "label": "unsticky", + "value": "unsticky" + }, + { + "label": "lock", + "value": "lock" + }, + { + "label": "unlock", + "value": "unlock" + }, + { + "label": "marknsfw", + "value": "marknsfw" + }, + { + "label": "unmarknsfw", + "value": "unmarknsfw" + }, + { + "label": "wikirevise", + "value": "wikirevise" + }, + { + "label": "wikipagelisted", + "value": "wikipagelisted" + }, + { + "label": "wikipermlevel", + "value": "wikipermlevel" + }, + { + "label": "wikibanned", + "value": "wikibanned" + }, + { + "label": "wikicontributor", + "value": "wikicontributor" + }, + { + "label": "editsettings", + "value": "editsettings" + }, + { + "label": "editflair", + "value": "editflair" + }, + { + "label": "createrule", + "value": "createrule" + }, + { + "label": "editrule", + "value": "editrule" + }, + { + "label": "deleterule", + "value": "deleterule" + }, + { + "label": "reorderrules", + "value": "reorderrules" + }, + { + "label": "ignorereports", + "value": "ignorereports" + }, + { + "label": "unignorereports", + "value": "unignorereports" + }, + { + "label": "request_assistance", + "value": "request_assistance" + }, + { + "label": "community_widgets", + "value": "community_widgets" + }, + { + "label": "community_welcome_page", + "value": "community_welcome_page" + }, + { + "label": "edit_post_requirements", + "value": "edit_post_requirements" + }, + { + "label": "edit_comment_requirements", + "value": "edit_comment_requirements" + }, + { + "label": "edit_saved_response", + "value": "edit_saved_response" + }, + { + "label": "addtocollection", + "value": "addtocollection" + }, + { + "label": "removefromcollection", + "value": "removefromcollection" + } + ], + "defaultValue": [ + "removelink", + "removecomment", + "spamlink", + "spamcomment", + "addremovalreason", + "approvelink", + "approvecomment" + ], + "validationEndpoint": "/internal/settings/validate-actions" + }, + "anonymizeModerators": { + "type": "boolean", + "label": "Anonymize moderators", + "helpText": "Human moderators show as HumanModerator. Required; cannot be turned off.", + "defaultValue": true, + "validationEndpoint": "/internal/settings/validate-anonymize" + }, + "ignoredModerators": { + "type": "string", + "label": "Ignored moderators", + "helpText": "Comma-separated usernames whose actions are never logged, e.g. AutoModerator.", + "defaultValue": "", + "validationEndpoint": "/internal/settings/validate-ignored" + } + } + }, + "scripts": { + "build": "vite build", + "dev": "vite build --watch" + } +} diff --git a/devvit/devvit.yaml b/devvit/devvit.yaml deleted file mode 100644 index 6174633..0000000 --- a/devvit/devvit.yaml +++ /dev/null @@ -1,12 +0,0 @@ -# Devvit app manifest (classic @devvit/public-api model). -# -# `name` MUST be globally unique across the Reddit Developer Platform and is -# claimed on first `devvit upload`. If "redditmodlog" is taken, pick a unique -# variant (e.g. "redditmodlog-wiki") here BEFORE the first upload — renaming a -# published app is disruptive. -# -# Settings, the scheduler job, the menu items, and the triggers are declared in -# code (src/main.ts via Devvit.addSettings / addSchedulerJob / addMenuItem / -# addTrigger), not in this file. The CLI infers required permissions (Reddit API, -# Redis, scheduler) from the registered capabilities at upload time. -name: redditmodlog diff --git a/devvit/package-lock.json b/devvit/package-lock.json index f585a84..d0d5e24 100644 --- a/devvit/package-lock.json +++ b/devvit/package-lock.json @@ -1,272 +1,347 @@ { "name": "redditmodlog-devvit", - "version": "0.0.1", + "version": "0.1.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "redditmodlog-devvit", - "version": "0.0.1", - "license": "BSD-3-Clause", + "version": "0.1.0", + "license": "GPL-3.0", "dependencies": { - "@devvit/public-api": "0.13.5" + "@devvit/start": "0.14.8", + "@devvit/web": "0.14.8", + "@hono/node-server": "2.1.4", + "devvit": "0.14.8", + "hono": "4.13.13" }, "devDependencies": { - "devvit": "1.0.0", - "typescript": "5.9.3", + "@devvit/test": "0.14.8", + "@types/node": "24.19.2", + "typescript": "6.0.3", + "vite": "8.3.3", "vitest": "4.1.11" + }, + "engines": { + "node": ">=24.0.0" } }, - "node_modules/@devvit/metrics": { - "version": "0.13.5", - "resolved": "https://registry.npmjs.org/@devvit/metrics/-/metrics-0.13.5.tgz", - "integrity": "sha512-KXzMbR/8oGkVEFLphjnSXnJOjyfH37e7A7QHwzuY/SQWgX/UW3s/HRMTV5Y85Hs/hxh8QTo2+K63hUKldE9ZVg==", - "license": "BSD-3-Clause", + "node_modules/@asamuzakjp/css-color": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-3.2.0.tgz", + "integrity": "sha512-K1A6z8tS3XsmCMM86xoWdn7Fkdn9m6RSVtocUrJYIwZnFVkng/PvkEoWtOWmP+Scc6saYWHWZYbndEEXxl24jw==", + "license": "MIT", "dependencies": { - "@devvit/protos": "0.13.5" + "@csstools/css-calc": "^2.1.3", + "@csstools/css-color-parser": "^3.0.9", + "@csstools/css-parser-algorithms": "^3.0.4", + "@csstools/css-tokenizer": "^3.0.3", + "lru-cache": "^10.4.3" } }, - "node_modules/@devvit/protos": { - "version": "0.13.5", - "resolved": "https://registry.npmjs.org/@devvit/protos/-/protos-0.13.5.tgz", - "integrity": "sha512-MZaqEX0XVwofCKMIdTew5z8aNv6M2uBhsoT91YEy2JNckrPcNJKdhg98WZ2+bQvtxq9Omh7mWx2+13KWdkQd9w==", - "license": "BSD-3-Clause", + "node_modules/@borewit/text-codec": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/@borewit/text-codec/-/text-codec-0.2.2.tgz", + "integrity": "sha512-DDaRehssg1aNrH4+2hnj1B7vnUGEjU6OIlyRdkMd0aUdIUvKXrJfXsy8LVtXAy7DRvYVluWbMspsRhz2lcW0mQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, + "node_modules/@cspotcode/source-map-support": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", + "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "license": "MIT", "dependencies": { - "protobufjs": "7.5.8", - "rxjs": "7.8.1" - }, - "peerDependencies": { - "twirp-ts": "^2.5.0" + "@jridgewell/trace-mapping": "0.3.9" }, - "peerDependenciesMeta": { - "twirp-ts": { - "optional": true - } + "engines": { + "node": ">=12" } }, - "node_modules/@devvit/public-api": { - "version": "0.13.5", - "resolved": "https://registry.npmjs.org/@devvit/public-api/-/public-api-0.13.5.tgz", - "integrity": "sha512-/i+EMoHRgY0vI4xfRpdBidOtxXtwI2vyQ6SuMIUnWvELWrCeFHlAhmbHQGiAmGoEYkpWP/XPGj+9S1fCz5Q7Yw==", - "license": "BSD-3-Clause", - "dependencies": { - "@devvit/metrics": "0.13.5", - "@devvit/protos": "0.13.5", - "@devvit/shared": "0.13.5", - "@devvit/shared-types": "0.13.5", - "base64-js": "1.5.1", - "clone-deep": "4.0.1", - "jwt-decode": "4.0.0", - "moderndash": "4.0.0" + "node_modules/@csstools/color-helpers": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-5.1.0.tgz", + "integrity": "sha512-S11EXWJyy0Mz5SYvRmY8nJYTFFd1LCNV+7cXyAgQtOOuzb4EsgfqDufL+9esx72/eLhsRdGZwaldu/h+E4t4BA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "engines": { + "node": ">=18" } }, - "node_modules/@devvit/shared": { - "version": "0.13.5", - "resolved": "https://registry.npmjs.org/@devvit/shared/-/shared-0.13.5.tgz", - "integrity": "sha512-RcJWMadvrJBXB7MR4YqHzmUSef24SLdMwtcNBYM5Hh4SKtXu+dV/6sr+tdNdo4R4j8B/B47cUcsDUccm8bdITA==", - "license": "BSD-3-Clause", - "dependencies": { - "@devvit/protos": "0.13.5", - "@devvit/shared-types": "0.13.5" + "node_modules/@csstools/css-calc": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-2.1.4.tgz", + "integrity": "sha512-3N8oaj+0juUw/1H3YwmDDJXCgTB1gKU6Hc/bB502u9zR0q2vd786XJH9QfrKIEgFlZmhZiq6epXl4rHqhzsIgQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4" } }, - "node_modules/@devvit/shared-types": { - "version": "0.13.5", - "resolved": "https://registry.npmjs.org/@devvit/shared-types/-/shared-types-0.13.5.tgz", - "integrity": "sha512-yQM7TReqkqGpBPYXDyx/aKC0FguTVYBzy1NhwJj2rdF3db+MKU3hIml9ZFu3f8cVHpgCxcC0Wh/p5e/zPfCjOQ==", - "license": "BSD-3-Clause", + "node_modules/@csstools/css-color-parser": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-3.1.0.tgz", + "integrity": "sha512-nbtKwh3a6xNVIp/VRuXV64yTKnb1IjTAEEh3irzS+HkKjAOYLTGNb9pmVNntZ8iVBHcWDA2Dof0QtPgFI1BaTA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", "dependencies": { - "@devvit/protos": "0.13.5", - "jsonschema": "1.4.1", - "uuid": "14.0.0" + "@csstools/color-helpers": "^5.1.0", + "@csstools/css-calc": "^2.1.4" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4" } }, - "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/@oxc-project/types": { - "version": "0.150.0", - "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.150.0.tgz", - "integrity": "sha512-rDS5/31E9HfPl/CIzGrn0DOlvBbXFseQ5URJ9sYMfstbKLD/c6Gm9vmRzRGDdAXyOIL4zmO37lc9RIwYqVruZw==", - "dev": true, + "node_modules/@csstools/css-parser-algorithms": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-3.0.5.tgz", + "integrity": "sha512-DaDeUkXZKjdGhgYaHNJTV9pV7Y9B3b644jCLs9Upc3VeNGg6LWARAT6O+Q+/COo+2gg/bM5rhpMAtf70WqfBdQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/oxc-project" + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-tokenizer": "^3.0.4" } }, - "node_modules/@protobufjs/aspromise": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", - "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==", - "license": "BSD-3-Clause" - }, - "node_modules/@protobufjs/base64": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz", - "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==", - "license": "BSD-3-Clause" - }, - "node_modules/@protobufjs/codegen": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.5.tgz", - "integrity": "sha512-zgXFLzW3Ap33e6d0Wlj4MGIm6Ce8O89n/apUaGNB/jx+hw+ruWEp7EwGUshdLKVRCxZW12fp9r40E1mQrf/34g==", - "license": "BSD-3-Clause" - }, - "node_modules/@protobufjs/eventemitter": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.1.tgz", - "integrity": "sha512-vW1GmwMZNnL+gMRaovlh9yZX74kc+TTU3FObkkurpMaRtBfLP3ldjS9KQWlwZgraRE0+dheEEoAxdzcJQ8eXZg==", - "license": "BSD-3-Clause" + "node_modules/@csstools/css-tokenizer": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-3.0.4.tgz", + "integrity": "sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=18" + } }, - "node_modules/@protobufjs/fetch": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.1.tgz", - "integrity": "sha512-GpptLrs57adMSuHi3VNj0mAF8dwh36LMaYF6XyJ6JMWlVsc+t42tm1HSEDmOs3A8fC9yyeisgLhsTVQokOZ0zw==", + "node_modules/@devvit/analytics": { + "version": "0.14.8", + "resolved": "https://registry.npmjs.org/@devvit/analytics/-/analytics-0.14.8.tgz", + "integrity": "sha512-Wkbv+V/hCEEAogHIS1q+xYMoyPZsdG+lQVx69iw3UPzOfsBHYUSNW4nUTdpBnkzNlElvxN3KZWwfAFsNEPECCA==", + "dev": true, "license": "BSD-3-Clause", "dependencies": { - "@protobufjs/aspromise": "^1.1.1" + "@devvit/cache": "0.14.8", + "@devvit/protos": "0.14.8", + "@devvit/shared-types": "0.14.8" + }, + "peerDependencies": { + "@devvit/server": "*", + "express": "*", + "posthog-js": "*" } }, - "node_modules/@protobufjs/float": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz", - "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==", - "license": "BSD-3-Clause" - }, - "node_modules/@protobufjs/path": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz", - "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==", - "license": "BSD-3-Clause" - }, - "node_modules/@protobufjs/pool": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz", - "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==", - "license": "BSD-3-Clause" + "node_modules/@devvit/build-pack": { + "version": "0.14.8", + "resolved": "https://registry.npmjs.org/@devvit/build-pack/-/build-pack-0.14.8.tgz", + "integrity": "sha512-VG9MLtiRq8Fe9lWvvfqIR+QNNuHdDYztXGzlZpuWXqujRO1WatU+WjdFFXNuIKpXOgl1cTV1mVnsR1oU4iAfCg==", + "license": "BSD-3-Clause", + "dependencies": { + "@devvit/payments": "0.14.8", + "@devvit/protos": "0.14.8", + "@devvit/shared-types": "0.14.8", + "@types/node": "24.13.3", + "esbuild": "0.25.9", + "rxjs": "7.8.1", + "tsv": "0.2.0", + "typescript": "5.8.3" + }, + "peerDependencies": { + "@devvit/server": "*", + "@devvit/shared": "*" + } }, - "node_modules/@protobufjs/utf8": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.1.tgz", - "integrity": "sha512-oOAWABowe8EAbMyWKM0tYDKi8Yaox52D+HWZhAIJqQXbqe0xI/GV7FhLWqlEKreMkfDjshR5FKgi3mnle0h6Eg==", - "license": "BSD-3-Clause" + "node_modules/@devvit/build-pack/node_modules/@esbuild/aix-ppc64": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.9.tgz", + "integrity": "sha512-OaGtL73Jck6pBKjNIe24BnFE6agGl+6KxDtTfHhy1HmhthfKouEcOhqpSL64K4/0WCtbKFLOdzD/44cJ4k9opA==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } }, - "node_modules/@rolldown/binding-android-arm-eabi": { - "version": "1.2.9", - "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm-eabi/-/binding-android-arm-eabi-1.2.9.tgz", - "integrity": "sha512-tNISae1QEf/vkb3xkRcjV5SEdzPE97We5IVaa2Z8jSszQPZ8U60B/YCYpw4QI7VidYsBtKavczXf+DyDs9WGxw==", + "node_modules/@devvit/build-pack/node_modules/@esbuild/android-arm": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.9.tgz", + "integrity": "sha512-5WNI1DaMtxQ7t7B6xa572XMXpHAaI/9Hnhk8lcxF4zVN4xstUgTlvuGDorBguKEnZO70qwEcLpfifMLoxiPqHQ==", "cpu": [ "arm" ], - "dev": true, "license": "MIT", "optional": true, "os": [ "android" ], "engines": { - "node": "^20.19.0 || >=22.12.0" + "node": ">=18" } }, - "node_modules/@rolldown/binding-android-arm64": { - "version": "1.2.9", - "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.2.9.tgz", - "integrity": "sha512-YC8YsI30o606GTZi0VyzYlsDKFP8W61i/QzayHDkLbNEz/IShqAmTa+hsJRj13xTHA0H+6fk4b2UmGn+Q/cMlg==", + "node_modules/@devvit/build-pack/node_modules/@esbuild/android-arm64": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.9.tgz", + "integrity": "sha512-IDrddSmpSv51ftWslJMvl3Q2ZT98fUSL2/rlUXuVqRXHCs5EUF1/f+jbjF5+NG9UffUDMCiTyh8iec7u8RlTLg==", "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ "android" ], "engines": { - "node": "^20.19.0 || >=22.12.0" + "node": ">=18" } }, - "node_modules/@rolldown/binding-darwin-arm64": { - "version": "1.2.9", - "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.2.9.tgz", - "integrity": "sha512-IwhlH3qK5urrY8hZiEgGkHKEFN901p/p2bjxCxJlr4GyNnF7wYpUvK+Y43uaRYuC4hpfjzbR3SJC3arX1jGvmw==", + "node_modules/@devvit/build-pack/node_modules/@esbuild/android-x64": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.9.tgz", + "integrity": "sha512-I853iMZ1hWZdNllhVZKm34f4wErd4lMyeV7BLzEExGEIZYsOzqDWDf+y082izYUE8gtJnYHdeDpN/6tUdwvfiw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@devvit/build-pack/node_modules/@esbuild/darwin-arm64": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.9.tgz", + "integrity": "sha512-XIpIDMAjOELi/9PB30vEbVMs3GV1v2zkkPnuyRRURbhqjyzIINwj+nbQATh4H9GxUgH1kFsEyQMxwiLFKUS6Rg==", "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ "darwin" ], "engines": { - "node": "^20.19.0 || >=22.12.0" + "node": ">=18" } }, - "node_modules/@rolldown/binding-darwin-x64": { - "version": "1.2.9", - "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.2.9.tgz", - "integrity": "sha512-XxpJfVzFh+jilRxIXUqcfYAYcunIc/XEzIizsOL1fcJee5Sf7H3mH8WlLmfHfluz5amqR88QQo9izKtmMlavAw==", + "node_modules/@devvit/build-pack/node_modules/@esbuild/darwin-x64": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.9.tgz", + "integrity": "sha512-jhHfBzjYTA1IQu8VyrjCX4ApJDnH+ez+IYVEoJHeqJm9VhG9Dh2BYaJritkYK3vMaXrf7Ogr/0MQ8/MeIefsPQ==", "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ "darwin" ], "engines": { - "node": "^20.19.0 || >=22.12.0" + "node": ">=18" } }, - "node_modules/@rolldown/binding-freebsd-x64": { - "version": "1.2.9", - "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.2.9.tgz", - "integrity": "sha512-kSfvhmgeWyfkbT3p/1s5vSgboogoah2zkm9fX2zjg2hHxSV7T4KhMWRUUaRk4OXNqoD3QAUeRqLcs1aZOK4U1g==", + "node_modules/@devvit/build-pack/node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.9.tgz", + "integrity": "sha512-z93DmbnY6fX9+KdD4Ue/H6sYs+bhFQJNCPZsi4XWJoYblUqT06MQUdBCpcSfuiN72AbqeBFu5LVQTjfXDE2A6Q==", "cpu": [ - "x64" + "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ "freebsd" ], "engines": { - "node": "^20.19.0 || >=22.12.0" + "node": ">=18" } }, - "node_modules/@rolldown/binding-linux-arm-gnueabihf": { - "version": "1.2.9", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.2.9.tgz", - "integrity": "sha512-1RVzG17pxqbTfYLC352JlLt6kKLG+6Hr30n8DlIJqsnV5luUDd2Qdx9Ayw1Cabfyb1K9k0jXEZ7evxkRoT+uiw==", + "node_modules/@devvit/build-pack/node_modules/@esbuild/freebsd-x64": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.9.tgz", + "integrity": "sha512-mrKX6H/vOyo5v71YfXWJxLVxgy1kyt1MQaD8wZJgJfG4gq4DpQGpgTB74e5yBeQdyMTbgxp0YtNj7NuHN0PoZg==", "cpu": [ - "arm" + "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ - "linux" + "freebsd" ], "engines": { - "node": "^20.19.0 || >=22.12.0" + "node": ">=18" } }, - "node_modules/@rolldown/binding-linux-arm64-gnu": { - "version": "1.2.9", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.2.9.tgz", - "integrity": "sha512-BXqPvZ2drqVD+/Z8UpKwcs4Mp7grM+eGFku4CAEKrEtcbAsUpzREphK1sogCRZGreVPiMkiiBtw0n3TPteuqvw==", + "node_modules/@devvit/build-pack/node_modules/@esbuild/linux-arm": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.9.tgz", + "integrity": "sha512-HBU2Xv78SMgaydBmdor38lg8YDnFKSARg1Q6AT0/y2ezUAKiZvc211RDFHlEZRFNRVhcMamiToo7bDx3VEOYQw==", "cpu": [ - "arm64" - ], - "dev": true, - "libc": [ - "glibc" + "arm" ], "license": "MIT", "optional": true, @@ -274,39 +349,31 @@ "linux" ], "engines": { - "node": "^20.19.0 || >=22.12.0" + "node": ">=18" } }, - "node_modules/@rolldown/binding-linux-arm64-musl": { - "version": "1.2.9", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.2.9.tgz", - "integrity": "sha512-11vWvo8YDwLzukt27J3aYDWU+gg2P7J+ZOmiJ0hkF5BXZDW7pVya7r40MXDy6ya0i9KamoENSVKIugvJNgFXIA==", + "node_modules/@devvit/build-pack/node_modules/@esbuild/linux-arm64": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.9.tgz", + "integrity": "sha512-BlB7bIcLT3G26urh5Dmse7fiLmLXnRlopw4s8DalgZ8ef79Jj4aUcYbk90g8iCa2467HX8SAIidbL7gsqXHdRw==", "cpu": [ "arm64" ], - "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ "linux" ], "engines": { - "node": "^20.19.0 || >=22.12.0" + "node": ">=18" } }, - "node_modules/@rolldown/binding-linux-ppc64-gnu": { - "version": "1.2.9", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.2.9.tgz", - "integrity": "sha512-a1tijMkdwsIARtc0F39ApURROkf3NwqinI6TOiSSWCTR7dT96dffNvMUtDHnq64wKNTIZOIlzKrFvvFUznJiyw==", + "node_modules/@devvit/build-pack/node_modules/@esbuild/linux-ia32": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.9.tgz", + "integrity": "sha512-e7S3MOJPZGp2QW6AK6+Ly81rC7oOSerQ+P8L0ta4FhVi+/j/v2yZzx5CqqDaWjtPFfYz21Vi1S0auHrap3Ma3A==", "cpu": [ - "ppc64" - ], - "dev": true, - "libc": [ - "glibc" + "ia32" ], "license": "MIT", "optional": true, @@ -314,19 +381,15 @@ "linux" ], "engines": { - "node": "^20.19.0 || >=22.12.0" + "node": ">=18" } }, - "node_modules/@rolldown/binding-linux-s390x-gnu": { - "version": "1.2.9", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.2.9.tgz", - "integrity": "sha512-x6SQNdAvv4c3hWqTMaWuawzMX9myaCs/yEmlGsxJzkdClnHW7FbrjQuSiRDhuSYzEYoEMhsaJy9qHG/XNemJPQ==", + "node_modules/@devvit/build-pack/node_modules/@esbuild/linux-loong64": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.9.tgz", + "integrity": "sha512-Sbe10Bnn0oUAB2AalYztvGcK+o6YFFA/9829PhOCUS9vkJElXGdphz0A3DbMdP8gmKkqPmPcMJmJOrI3VYB1JQ==", "cpu": [ - "s390x" - ], - "dev": true, - "libc": [ - "glibc" + "loong64" ], "license": "MIT", "optional": true, @@ -334,19 +397,15 @@ "linux" ], "engines": { - "node": "^20.19.0 || >=22.12.0" + "node": ">=18" } }, - "node_modules/@rolldown/binding-linux-x64-gnu": { - "version": "1.2.9", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.2.9.tgz", - "integrity": "sha512-9s0AZ8BFK5/n7B/TBoa2yJE3gI3KURrbXcPBlsAsvjU4VeJKgE90y1YtNxyEUIcHPQkg6/yfF3qihUrcM/Kf0Q==", + "node_modules/@devvit/build-pack/node_modules/@esbuild/linux-mips64el": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.9.tgz", + "integrity": "sha512-YcM5br0mVyZw2jcQeLIkhWtKPeVfAerES5PvOzaDxVtIyZ2NUBZKNLjC5z3/fUlDgT6w89VsxP2qzNipOaaDyA==", "cpu": [ - "x64" - ], - "dev": true, - "libc": [ - "glibc" + "mips64el" ], "license": "MIT", "optional": true, @@ -354,19 +413,15 @@ "linux" ], "engines": { - "node": "^20.19.0 || >=22.12.0" + "node": ">=18" } }, - "node_modules/@rolldown/binding-linux-x64-musl": { - "version": "1.2.9", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.2.9.tgz", - "integrity": "sha512-P7VWAmV+WdJluH7ovnRGoiv2i8To7GAZ+kGzfGup635cyL7SyYl3lSUaA3Gp5THf0n/Co5EyEqb2zbqq+nMOHQ==", + "node_modules/@devvit/build-pack/node_modules/@esbuild/linux-ppc64": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.9.tgz", + "integrity": "sha512-++0HQvasdo20JytyDpFvQtNrEsAgNG2CY1CLMwGXfFTKGBGQT3bOeLSYE2l1fYdvML5KUuwn9Z8L1EWe2tzs1w==", "cpu": [ - "x64" - ], - "dev": true, - "libc": [ - "musl" + "ppc64" ], "license": "MIT", "optional": true, @@ -374,919 +429,7226 @@ "linux" ], "engines": { - "node": "^20.19.0 || >=22.12.0" + "node": ">=18" } }, - "node_modules/@rolldown/binding-openharmony-arm64": { - "version": "1.2.9", - "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.2.9.tgz", - "integrity": "sha512-1qixtsE4BK8h+yS3BfmZ09UhA7O/N4IACva6YBr7EBvCJraByTuRcgOTaiA62Tm0vey3UcKXLOaoGHtYmNGEVg==", + "node_modules/@devvit/build-pack/node_modules/@esbuild/linux-riscv64": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.9.tgz", + "integrity": "sha512-uNIBa279Y3fkjV+2cUjx36xkx7eSjb8IvnL01eXUKXez/CBHNRw5ekCGMPM0BcmqBxBcdgUWuUXmVWwm4CH9kg==", "cpu": [ - "arm64" + "riscv64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ - "openharmony" + "linux" ], "engines": { - "node": "^20.19.0 || >=22.12.0" + "node": ">=18" } }, - "node_modules/@rolldown/binding-win32-arm64-msvc": { - "version": "1.2.9", - "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.2.9.tgz", - "integrity": "sha512-ok8IQjcEPs1AKZfuEUznVBrJw+gK4soq+bx8b1X2XoMqVClarc1q5JDmVtWXY1xfr6ZuHTAsPXHTgTrqKTZeww==", + "node_modules/@devvit/build-pack/node_modules/@esbuild/linux-s390x": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.9.tgz", + "integrity": "sha512-Mfiphvp3MjC/lctb+7D287Xw1DGzqJPb/J2aHHcHxflUo+8tmN/6d4k6I2yFR7BVo5/g7x2Monq4+Yew0EHRIA==", "cpu": [ - "arm64" + "s390x" ], - "dev": true, "license": "MIT", "optional": true, "os": [ - "win32" + "linux" ], "engines": { - "node": "^20.19.0 || >=22.12.0" + "node": ">=18" } }, - "node_modules/@rolldown/binding-win32-x64-msvc": { - "version": "1.2.9", - "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.2.9.tgz", - "integrity": "sha512-Ip2mXoU0hM0boq3Rf+ekuT653OROSo6aSYcPT1VHE4q52KvyxgFkQgrgb/IEsxOuvQ2fZZbs8khJAyCEPM24/g==", + "node_modules/@devvit/build-pack/node_modules/@esbuild/linux-x64": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.9.tgz", + "integrity": "sha512-iSwByxzRe48YVkmpbgoxVzn76BXjlYFXC7NvLYq+b+kDjyyk30J0JY47DIn8z1MO3K0oSl9fZoRmZPQI4Hklzg==", "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ - "win32" + "linux" ], "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/pluginutils": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz", - "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==", - "dev": true, - "license": "MIT" - }, - "node_modules/@standard-schema/spec": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", - "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/chai": { - "version": "5.2.3", - "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", - "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/deep-eql": "*", - "assertion-error": "^2.0.1" + "node": ">=18" } }, - "node_modules/@types/deep-eql": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", - "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/estree": { - "version": "1.0.9", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", - "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/node": { - "version": "26.0.0", - "resolved": "https://registry.npmjs.org/@types/node/-/node-26.0.0.tgz", - "integrity": "sha512-vf2YFi1iY9lHGwNJMs01biZFbKJkrZR1T6/MlzjhJLPdntOHLhTrDSnSVcdtvjihi4VQNlrFRIxLsDBlQpAipA==", + "node_modules/@devvit/build-pack/node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.9.tgz", + "integrity": "sha512-9jNJl6FqaUG+COdQMjSCGW4QiMHH88xWbvZ+kRVblZsWrkXlABuGdFJ1E9L7HK+T0Yqd4akKNa/lO0+jDxQD4Q==", + "cpu": [ + "arm64" + ], "license": "MIT", - "dependencies": { - "undici-types": "~8.3.0" + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" } }, - "node_modules/@vitest/expect": { - "version": "4.1.11", - "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.11.tgz", - "integrity": "sha512-VX2x5vNJXET47KAFzwERI+KRMtTTCSWTfSMKsW7JsUsXV4psq++e3DvZpuTDOpHcxytiDs6p2nhVb2tVDiiUYw==", - "dev": true, + "node_modules/@devvit/build-pack/node_modules/@esbuild/netbsd-x64": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.9.tgz", + "integrity": "sha512-RLLdkflmqRG8KanPGOU7Rpg829ZHu8nFy5Pqdi9U01VYtG9Y0zOG6Vr2z4/S+/3zIyOxiK6cCeYNWOFR9QP87g==", + "cpu": [ + "x64" + ], "license": "MIT", - "dependencies": { - "@standard-schema/spec": "^1.1.0", - "@types/chai": "^5.2.2", - "@vitest/spy": "4.1.11", - "@vitest/utils": "4.1.11", - "chai": "^6.2.2", - "tinyrainbow": "^3.1.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" } }, - "node_modules/@vitest/pretty-format": { - "version": "4.1.11", - "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.11.tgz", - "integrity": "sha512-yiZzPbGTS9Sr/JpFl8zHrcIkAofNbFV6k21vIgQN/cY/oxZeXhJv5sc/MBJ5jFKWmWs+oJHw0UXLZjmf931+Vw==", - "dev": true, + "node_modules/@devvit/build-pack/node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.9.tgz", + "integrity": "sha512-YaFBlPGeDasft5IIM+CQAhJAqS3St3nJzDEgsgFixcfZeyGPCd6eJBWzke5piZuZ7CtL656eOSYKk4Ls2C0FRQ==", + "cpu": [ + "arm64" + ], "license": "MIT", - "dependencies": { - "tinyrainbow": "^3.1.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" } }, - "node_modules/@vitest/runner": { - "version": "4.1.11", - "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.11.tgz", - "integrity": "sha512-LztvUgdwMNJMIkj3hQnnxiC2Xy1zNxq928W/xhjCLaNCzqTZOudjwbQf6v9IntZGPw132i2Lq2rgTRZHD3JHNw==", - "dev": true, + "node_modules/@devvit/build-pack/node_modules/@esbuild/openbsd-x64": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.9.tgz", + "integrity": "sha512-1MkgTCuvMGWuqVtAvkpkXFmtL8XhWy+j4jaSO2wxfJtilVCi0ZE37b8uOdMItIHz4I6z1bWWtEX4CJwcKYLcuA==", + "cpu": [ + "x64" + ], "license": "MIT", - "dependencies": { - "@vitest/utils": "4.1.11", - "pathe": "^2.0.3" - }, - "funding": { - "url": "https://opencollective.com/vitest" + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" } }, - "node_modules/@vitest/snapshot": { - "version": "4.1.11", - "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.11.tgz", - "integrity": "sha512-pN7ikn1ON7h8ee4gIAp4AzyK+zBtJPzVbqOgu5LCEh4VaJVbPQcgYQYJIMGQPXVeJJq1fnfazis7a5pFNPahog==", - "dev": true, + "node_modules/@devvit/build-pack/node_modules/@esbuild/openharmony-arm64": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.9.tgz", + "integrity": "sha512-4Xd0xNiMVXKh6Fa7HEJQbrpP3m3DDn43jKxMjxLLRjWnRsfxjORYJlXPO4JNcXtOyfajXorRKY9NkOpTHptErg==", + "cpu": [ + "arm64" + ], "license": "MIT", - "dependencies": { - "@vitest/pretty-format": "4.1.11", - "@vitest/utils": "4.1.11", - "magic-string": "^0.30.21", - "pathe": "^2.0.3" - }, - "funding": { - "url": "https://opencollective.com/vitest" + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" } }, - "node_modules/@vitest/spy": { - "version": "4.1.11", - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.11.tgz", - "integrity": "sha512-apNa/prQy2qCeywhnixOHPRCgGNhvg7T4Dapfl1GahLp/R+uhBm5cPyFoNVyqsNd2h1nJxL6BqqdIjiABL60YA==", - "dev": true, + "node_modules/@devvit/build-pack/node_modules/@esbuild/sunos-x64": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.9.tgz", + "integrity": "sha512-WjH4s6hzo00nNezhp3wFIAfmGZ8U7KtrJNlFMRKxiI9mxEK1scOMAaa9i4crUtu+tBr+0IN6JCuAcSBJZfnphw==", + "cpu": [ + "x64" + ], "license": "MIT", - "funding": { - "url": "https://opencollective.com/vitest" + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" } }, - "node_modules/@vitest/utils": { - "version": "4.1.11", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.11.tgz", - "integrity": "sha512-zTCVGpyFsGWBhllOyKlTw/vnr6D9qxsfSDyfbyZmTyjHw5N/VuvzHpHoQjm2ZJzn4RJgx5w4r7V0er69CmLgPQ==", - "dev": true, + "node_modules/@devvit/build-pack/node_modules/@esbuild/win32-arm64": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.9.tgz", + "integrity": "sha512-mGFrVJHmZiRqmP8xFOc6b84/7xa5y5YvR1x8djzXpJBSv/UsNK6aqec+6JDjConTgvvQefdGhFDAs2DLAds6gQ==", + "cpu": [ + "arm64" + ], "license": "MIT", - "dependencies": { - "@vitest/pretty-format": "4.1.11", - "convert-source-map": "^2.0.0", - "tinyrainbow": "^3.1.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" } }, - "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, + "node_modules/@devvit/build-pack/node_modules/@esbuild/win32-ia32": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.9.tgz", + "integrity": "sha512-b33gLVU2k11nVx1OhX3C8QQP6UHQK4ZtN56oFWvVXvz2VkDoe6fbG8TOgHFxEvqeqohmRnIHe5A1+HADk4OQww==", + "cpu": [ + "ia32" + ], "license": "MIT", + "optional": true, + "os": [ + "win32" + ], "engines": { - "node": ">=12" + "node": ">=18" } }, - "node_modules/base64-js": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", - "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } + "node_modules/@devvit/build-pack/node_modules/@esbuild/win32-x64": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.9.tgz", + "integrity": "sha512-PPOl1mi6lpLNQxnGoyAfschAodRFYXJ+9fs6WHXz7CSWKbOqiMZsubC+BQsVKuul+3vKLuwTHsS2c2y9EoKwxQ==", + "cpu": [ + "x64" ], - "license": "MIT" - }, - "node_modules/chai": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz", - "integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==", - "dev": true, "license": "MIT", + "optional": true, + "os": [ + "win32" + ], "engines": { "node": ">=18" } }, - "node_modules/clone-deep": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/clone-deep/-/clone-deep-4.0.1.tgz", - "integrity": "sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ==", + "node_modules/@devvit/build-pack/node_modules/@types/node": { + "version": "24.13.3", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.13.3.tgz", + "integrity": "sha512-Dh8vAsV36ig5wa9OX4pXvMc9D3Veibfw2wix0CUwYODLD8nkj9UsLjASr49nPg+2eKzxhBV+v7L8pXvT4e639Q==", "license": "MIT", "dependencies": { - "is-plain-object": "^2.0.4", - "kind-of": "^6.0.2", - "shallow-clone": "^3.0.0" + "undici-types": "~7.18.0" + } + }, + "node_modules/@devvit/build-pack/node_modules/esbuild": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.9.tgz", + "integrity": "sha512-CRbODhYyQx3qp7ZEwzxOk4JBqmD/seJrzPa/cGjY1VtIn5E09Oi9/dB4JwctnfZ8Q8iT7rioVv5k/FNT/uf54g==", + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" }, "engines": { - "node": ">=6" + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.9", + "@esbuild/android-arm": "0.25.9", + "@esbuild/android-arm64": "0.25.9", + "@esbuild/android-x64": "0.25.9", + "@esbuild/darwin-arm64": "0.25.9", + "@esbuild/darwin-x64": "0.25.9", + "@esbuild/freebsd-arm64": "0.25.9", + "@esbuild/freebsd-x64": "0.25.9", + "@esbuild/linux-arm": "0.25.9", + "@esbuild/linux-arm64": "0.25.9", + "@esbuild/linux-ia32": "0.25.9", + "@esbuild/linux-loong64": "0.25.9", + "@esbuild/linux-mips64el": "0.25.9", + "@esbuild/linux-ppc64": "0.25.9", + "@esbuild/linux-riscv64": "0.25.9", + "@esbuild/linux-s390x": "0.25.9", + "@esbuild/linux-x64": "0.25.9", + "@esbuild/netbsd-arm64": "0.25.9", + "@esbuild/netbsd-x64": "0.25.9", + "@esbuild/openbsd-arm64": "0.25.9", + "@esbuild/openbsd-x64": "0.25.9", + "@esbuild/openharmony-arm64": "0.25.9", + "@esbuild/sunos-x64": "0.25.9", + "@esbuild/win32-arm64": "0.25.9", + "@esbuild/win32-ia32": "0.25.9", + "@esbuild/win32-x64": "0.25.9" } }, - "node_modules/convert-source-map": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", - "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", - "dev": true, - "license": "MIT" - }, - "node_modules/detect-libc": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", - "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", - "dev": true, + "node_modules/@devvit/build-pack/node_modules/typescript": { + "version": "5.8.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", + "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, "engines": { - "node": ">=8" + "node": ">=14.17" } }, - "node_modules/devvit": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/devvit/-/devvit-1.0.0.tgz", - "integrity": "sha512-YcKrn2VizQQ2HSTLuDh16wyxrDJc/EnuoxnfQwONSU/EE/c56SH1L2biefwkPLDAgnwnSJoDsVbXVcg+dud+qA==", - "deprecated": "this package has been deprecated", - "dev": true, - "license": "ISC" + "node_modules/@devvit/build-pack/node_modules/undici-types": { + "version": "7.18.2", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz", + "integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==", + "license": "MIT" }, - "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", + "node_modules/@devvit/builders": { + "version": "0.14.8", + "resolved": "https://registry.npmjs.org/@devvit/builders/-/builders-0.14.8.tgz", + "integrity": "sha512-Cek7B7MGeso2FPHYxcR/cF8jTtjWpP6ZLKoOkudEebPH0K1/rV1J14BdJtZCl2MrLCqobasMc9wnHHsR3FsMzw==", + "license": "BSD-3-Clause", "dependencies": { - "@types/estree": "^1.0.0" + "@devvit/build-pack": "0.14.8", + "@devvit/linkers": "0.14.8", + "@devvit/protos": "0.14.8", + "@devvit/shared-types": "0.14.8" } }, - "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/@devvit/cache": { + "version": "0.14.8", + "resolved": "https://registry.npmjs.org/@devvit/cache/-/cache-0.14.8.tgz", + "integrity": "sha512-I2RCzKbiBPNDT+A2YT5u/Iw28y3u9jtRZBd8XWBJ/rmLjKT5UKuom2MKMaS4VDAOgd2v+wjgrFKAdF3erF5vsw==", + "license": "BSD-3-Clause", + "dependencies": { + "@devvit/redis": "0.14.8", + "@devvit/server": "0.14.8", + "@devvit/shared-types": "0.14.8" } }, - "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" - ], + "node_modules/@devvit/cli": { + "version": "0.14.8", + "resolved": "https://registry.npmjs.org/@devvit/cli/-/cli-0.14.8.tgz", + "integrity": "sha512-YW4XKeOvs8JbTOOR/acMuaAEMTkMChWmvSP8HDz76x/o7NlyHQXa8yQ+6pKJdPPtzDJV1YEvhACq/o+Et4Zg+Q==", + "license": "BSD-3-Clause", + "dependencies": { + "@devvit/build-pack": "0.14.8", + "@devvit/builders": "0.14.8", + "@devvit/linkers": "0.14.8", + "@devvit/protos": "0.14.8", + "@devvit/public-api": "0.14.8", + "@devvit/shared-types": "0.14.8", + "@improbable-eng/grpc-web": "0.15.0", + "@improbable-eng/grpc-web-node-http-transport": "0.15.0", + "@oclif/core": "2.9.4", + "@oclif/plugin-autocomplete": "2.3.3", + "@oclif/plugin-help": "5.2.14", + "@oclif/plugin-not-found": "2.3.34", + "@oclif/plugin-warn-if-update-available": "2.0.45", + "@types/ws": "8.5.12", + "chalk": "4.1.2", + "chokidar": "3.5.3", + "date-fns": "2.29.3", + "dotenv": "16.5.0", + "execa": "9.6.1", + "file-type": "21.3.2", + "ignore": "7.0.5", + "image-size": "2.0.2", + "inquirer": "9.1.4", + "isomorphic-git": "1.33.1", + "js-yaml": "4.3.1", + "jsdom": "24.1.0", + "jszip": "3.10.1", + "mime-types": "3.0.2", + "mustache": "4.2.0", + "open": "10.1.0", + "rxjs": "7.8.1", + "semver": "7.6.3", + "string-length": "5.0.1", + "tiny-glob": "0.2.9", + "twirp-ts": "2.5.0", + "ws": "8.21.0" + }, + "bin": { + "devvit-cli": "bin/devvit.js" + }, "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + "node": ">=20" } }, - "node_modules/hotscript": { - "version": "1.0.13", - "resolved": "https://registry.npmjs.org/hotscript/-/hotscript-1.0.13.tgz", - "integrity": "sha512-C++tTF1GqkGYecL+2S1wJTfoH6APGAsbb7PAWQ3iVIwgG/EFseAfEVOKFgAFq4yK3+6j1EjUD4UQ9dRJHX/sSQ==", - "license": "ISC" + "node_modules/@devvit/client": { + "version": "0.14.8", + "resolved": "https://registry.npmjs.org/@devvit/client/-/client-0.14.8.tgz", + "integrity": "sha512-7Nhs+rXcWxKVM2P1ARaSMWupK6oEfYz8Uu7kDx86EfpH1RAbhP2bPOfJf2m5W4ZeCD5GA9YkUyzLq6blXXz/dQ==", + "license": "BSD-3-Clause", + "dependencies": { + "@devvit/protos": "0.14.8", + "@devvit/shared": "0.14.8", + "@devvit/shared-types": "0.14.8", + "@devvit/web-view-scripts": "0.14.8" + } }, - "node_modules/is-plain-object": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", - "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", - "license": "MIT", + "node_modules/@devvit/external-endpoints": { + "version": "0.14.8", + "resolved": "https://registry.npmjs.org/@devvit/external-endpoints/-/external-endpoints-0.14.8.tgz", + "integrity": "sha512-I1rniShg1VD/AVjWbDljBEXUoN2QR0VC3d8ivkf2v7tMZGusZ2YVofLLnPfOWBZP4ZCY5Qik/bRZKtfWSa9bmQ==", + "license": "BSD-3-Clause", "dependencies": { - "isobject": "^3.0.1" + "@devvit/protos": "0.14.8", + "@devvit/shared-types": "0.14.8" }, - "engines": { - "node": ">=0.10.0" + "peerDependencies": { + "@devvit/server": "*" } }, - "node_modules/isobject": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" + "node_modules/@devvit/linkers": { + "version": "0.14.8", + "resolved": "https://registry.npmjs.org/@devvit/linkers/-/linkers-0.14.8.tgz", + "integrity": "sha512-P5h/lvgZXCR6PkbpF5ag6Ez+vX0s4iE/a9HeeaQXJL8DjFjKOnosRFApRoPPNv1y/A0iDhsFIaWnrCjvTL69OQ==", + "license": "BSD-3-Clause", + "dependencies": { + "@devvit/build-pack": "0.14.8", + "@devvit/protos": "0.14.8", + "@devvit/shared-types": "0.14.8" } }, - "node_modules/jsonschema": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/jsonschema/-/jsonschema-1.4.1.tgz", - "integrity": "sha512-S6cATIPVv1z0IlxdN+zUk5EPjkGCdnhN4wVSBlvoUO1tOLJootbo9CquNJmbIh4yikWHiUedhRYrNPn1arpEmQ==", - "license": "MIT", - "engines": { - "node": "*" + "node_modules/@devvit/media": { + "version": "0.14.8", + "resolved": "https://registry.npmjs.org/@devvit/media/-/media-0.14.8.tgz", + "integrity": "sha512-eUcd8nPgXC/SPjt1io9+bew05Ztt6J0/DTKzDtSQzwnMKHxIU1CgZD75P5raeMhvF7rrRhy+ayn+Da4b5qirug==", + "license": "BSD-3-Clause", + "dependencies": { + "@devvit/protos": "0.14.8" + }, + "peerDependencies": { + "@devvit/server": "*" } }, - "node_modules/jwt-decode": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/jwt-decode/-/jwt-decode-4.0.0.tgz", - "integrity": "sha512-+KJGIyHgkGuIq3IEBNftfhW/LfWhXUIY6OmyVWjliu5KH1y0fw7VQ8YndE2O4qZdMSd9SqbnC8GOcZEy0Om7sA==", - "license": "MIT", - "engines": { - "node": ">=18" + "node_modules/@devvit/notifications": { + "version": "0.14.8", + "resolved": "https://registry.npmjs.org/@devvit/notifications/-/notifications-0.14.8.tgz", + "integrity": "sha512-toHFOQ8en9oaw32XkmMFYTIArI18Q/piHOFcm/nh0wZbZWXh6z2wahIs8x0Jp4RnlPhQVoT9Llaa1jlEazo1tw==", + "license": "BSD-3-Clause", + "dependencies": { + "@devvit/protos": "0.14.8", + "@devvit/shared-types": "0.14.8" + }, + "peerDependencies": { + "@devvit/server": "*" } }, - "node_modules/kind-of": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", - "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" + "node_modules/@devvit/payments": { + "version": "0.14.8", + "resolved": "https://registry.npmjs.org/@devvit/payments/-/payments-0.14.8.tgz", + "integrity": "sha512-rDSX/uqB3R5bohVSl9wFXCqNV65hAh6OuE8LPYdHOdyrjkU2DDHug+2HHEUo6BTObJK42zQJAHmmiTiXqpANjQ==", + "license": "BSD-3-Clause", + "dependencies": { + "@devvit/protos": "0.14.8", + "@devvit/server": "0.14.8", + "@devvit/shared-types": "0.14.8" } }, - "node_modules/lightningcss": { - "version": "1.33.0", - "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.33.0.tgz", - "integrity": "sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==", - "dev": true, - "license": "MPL-2.0", + "node_modules/@devvit/protos": { + "version": "0.14.8", + "resolved": "https://registry.npmjs.org/@devvit/protos/-/protos-0.14.8.tgz", + "integrity": "sha512-II00fuWuf4SzkV1CCkS206mtRLVyss6Qqre/phrpMHDi0GifJfrIf/Zbhkfwk5IQ9XStgyNo6SmW+Epolprx+Q==", + "license": "BSD-3-Clause", "dependencies": { - "detect-libc": "^2.0.3" + "protobufjs": "7.6.5", + "rxjs": "7.8.1" }, - "engines": { - "node": ">= 12.0.0" + "peerDependencies": { + "twirp-ts": "^2.5.0" }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" + "peerDependenciesMeta": { + "twirp-ts": { + "optional": true + } + } + }, + "node_modules/@devvit/public-api": { + "version": "0.14.8", + "resolved": "https://registry.npmjs.org/@devvit/public-api/-/public-api-0.14.8.tgz", + "integrity": "sha512-clqYF0WuLTAoDLoZeLl2e91WZHrAyg0QhvfAygnQWgBgtlhRdzzdUdXyU6XnAeBJK0trDRXgU+TusvyMJVg2CA==", + "license": "BSD-3-Clause", + "dependencies": { + "@devvit/cache": "0.14.8", + "@devvit/media": "0.14.8", + "@devvit/protos": "0.14.8", + "@devvit/reddit": "0.14.8", + "@devvit/redis": "0.14.8", + "@devvit/scheduler": "0.14.8", + "@devvit/server": "0.14.8", + "@devvit/settings": "0.14.8", + "@devvit/shared": "0.14.8", + "@devvit/shared-types": "0.14.8" + } + }, + "node_modules/@devvit/realtime": { + "version": "0.14.8", + "resolved": "https://registry.npmjs.org/@devvit/realtime/-/realtime-0.14.8.tgz", + "integrity": "sha512-p3ipkLZNXDHrIBZxuST4Qeqo/jbFhXSGInCGrjC/NCXydnHRxhYUwp71fca9FRYJLYqZP0j5EmeBWUttKiNm7w==", + "license": "BSD-3-Clause", + "dependencies": { + "@devvit/protos": "0.14.8", + "@devvit/shared": "0.14.8", + "@devvit/shared-types": "0.14.8", + "rxjs": "7.8.1" }, - "optionalDependencies": { - "lightningcss-android-arm64": "1.33.0", - "lightningcss-darwin-arm64": "1.33.0", - "lightningcss-darwin-x64": "1.33.0", - "lightningcss-freebsd-x64": "1.33.0", - "lightningcss-linux-arm-gnueabihf": "1.33.0", - "lightningcss-linux-arm64-gnu": "1.33.0", - "lightningcss-linux-arm64-musl": "1.33.0", - "lightningcss-linux-x64-gnu": "1.33.0", - "lightningcss-linux-x64-musl": "1.33.0", - "lightningcss-win32-arm64-msvc": "1.33.0", - "lightningcss-win32-x64-msvc": "1.33.0" + "peerDependencies": { + "@devvit/client": "*", + "@devvit/server": "*" } }, - "node_modules/lightningcss-android-arm64": { - "version": "1.33.0", - "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.33.0.tgz", - "integrity": "sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg==", + "node_modules/@devvit/reddit": { + "version": "0.14.8", + "resolved": "https://registry.npmjs.org/@devvit/reddit/-/reddit-0.14.8.tgz", + "integrity": "sha512-rb3mQMoathYjxNyte6xcGQuVDYdTM4vdxRE0plPNN6yoZPOxieaUDeROj2wW9pnj5w5uvzOTNu8jLgIdQ+hC5w==", + "license": "BSD-3-Clause", + "dependencies": { + "@devvit/protos": "0.14.8", + "@devvit/shared": "0.14.8", + "@devvit/shared-types": "0.14.8" + }, + "peerDependencies": { + "@devvit/server": "*" + } + }, + "node_modules/@devvit/redis": { + "version": "0.14.8", + "resolved": "https://registry.npmjs.org/@devvit/redis/-/redis-0.14.8.tgz", + "integrity": "sha512-yQ1Zy8ulvQfq+S5yEHYoxF+rb29+twYSaSPtsC+qMl5RpU/JWpiRnl3kv6/iOI4ofs/M0ZiDRM3Shoq25nMgrA==", + "license": "BSD-3-Clause", + "dependencies": { + "@devvit/protos": "0.14.8", + "@devvit/shared-types": "0.14.8" + }, + "peerDependencies": { + "@devvit/server": "*" + } + }, + "node_modules/@devvit/scheduler": { + "version": "0.14.8", + "resolved": "https://registry.npmjs.org/@devvit/scheduler/-/scheduler-0.14.8.tgz", + "integrity": "sha512-39wUC1yb2yHskweR74Fr8SY9xDke/vitOp8ONvn6dwAgFwOYnFlbMVQ55oe5pMCFb1kmmow6HXa4XR77uzWc9Q==", + "license": "BSD-3-Clause", + "dependencies": { + "@devvit/protos": "0.14.8", + "@devvit/shared": "0.14.8", + "@devvit/shared-types": "0.14.8" + }, + "peerDependencies": { + "@devvit/server": "*" + } + }, + "node_modules/@devvit/server": { + "version": "0.14.8", + "resolved": "https://registry.npmjs.org/@devvit/server/-/server-0.14.8.tgz", + "integrity": "sha512-nP+hlqOAeD5t9DS650CfoKsALJqSZO3E18OspclcMexQfpeDJahNDoWSZsNkUpM1k1INe2BGGC2bvBJQd86zfg==", + "license": "BSD-3-Clause", + "dependencies": { + "@devvit/protos": "0.14.8", + "@devvit/shared": "0.14.8", + "@devvit/shared-types": "0.14.8" + } + }, + "node_modules/@devvit/settings": { + "version": "0.14.8", + "resolved": "https://registry.npmjs.org/@devvit/settings/-/settings-0.14.8.tgz", + "integrity": "sha512-wZ28izT/dqv8xVZtYkQ8Zpmoq3qFyY5KEKyAhgclNYNpW34Hkm7MRFFGr50tFcSU0hGSJmmFv7Qpnt9N+o4Kbg==", + "license": "BSD-3-Clause", + "dependencies": { + "@devvit/protos": "0.14.8", + "@devvit/shared": "0.14.8", + "@devvit/shared-types": "0.14.8" + }, + "peerDependencies": { + "@devvit/server": "*" + } + }, + "node_modules/@devvit/shared": { + "version": "0.14.8", + "resolved": "https://registry.npmjs.org/@devvit/shared/-/shared-0.14.8.tgz", + "integrity": "sha512-ear88TmFZgHsi5NKpSYrWxQOGYPs0xRok3kV4g8GUqZBUgFHRqL88Wvp5ZpaT4FtokFPeCtZRvdirqNvYlePLw==", + "license": "BSD-3-Clause", + "dependencies": { + "@devvit/protos": "0.14.8", + "@devvit/shared-types": "0.14.8" + } + }, + "node_modules/@devvit/shared-types": { + "version": "0.14.8", + "resolved": "https://registry.npmjs.org/@devvit/shared-types/-/shared-types-0.14.8.tgz", + "integrity": "sha512-Q4b46TM919KhNJT0SbU7hjPeUsVbTXyLOk3xCch/W8zP4eOOTHuYrSHSPI2Q0KFOuurhtONECr1PdqKGNtJZPw==", + "license": "BSD-3-Clause", + "dependencies": { + "@devvit/protos": "0.14.8", + "jsonschema": "1.4.1", + "jwt-decode": "4.0.0", + "uuid": "14.0.0" + } + }, + "node_modules/@devvit/start": { + "version": "0.14.8", + "resolved": "https://registry.npmjs.org/@devvit/start/-/start-0.14.8.tgz", + "integrity": "sha512-8299OH25t3mQwhC5Nl0s4t6IOjABVcwrdEALqpU3n46JWC3y04K8Bev8P/Q091PFK/u7T61Cc3MTkUM8GIN8VQ==", + "license": "BSD-3-Clause", + "dependencies": { + "@devvit/shared-types": "0.14.8", + "chalk": "4.1.2" + }, + "peerDependencies": { + "vite": ">=7.3.5" + } + }, + "node_modules/@devvit/test": { + "version": "0.14.8", + "resolved": "https://registry.npmjs.org/@devvit/test/-/test-0.14.8.tgz", + "integrity": "sha512-qMPVYv6vZrFxU/Kq9WOnyvZjP5K/ux+dUUfkQdqS6RRFro3HDF8HSrxdOYTJf25x2x1N8ZmV0dNQP8jCOUWHyg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@devvit/analytics": "0.14.8", + "@devvit/media": "0.14.8", + "@devvit/notifications": "0.14.8", + "@devvit/protos": "0.14.8", + "@devvit/realtime": "0.14.8", + "@devvit/reddit": "0.14.8", + "@devvit/redis": "0.14.8", + "@devvit/scheduler": "0.14.8", + "@devvit/server": "0.14.8", + "@devvit/settings": "0.14.8", + "@devvit/shared-types": "0.14.8", + "@types/ioredis-mock": "8.2.8", + "ioredis": "5.7.0", + "ioredis-mock": "8.13.1" + }, + "peerDependencies": { + "vitest": "*" + } + }, + "node_modules/@devvit/web": { + "version": "0.14.8", + "resolved": "https://registry.npmjs.org/@devvit/web/-/web-0.14.8.tgz", + "integrity": "sha512-HODNVNnmcQMptcSt4Le2MhRRJh686amN2WqxVgATfg1srnHRihTLOA3jtm3n21VfK6UUslOuI2eZS4wMb+4FYQ==", + "license": "BSD-3-Clause", + "dependencies": { + "@devvit/cache": "0.14.8", + "@devvit/client": "0.14.8", + "@devvit/external-endpoints": "0.14.8", + "@devvit/media": "0.14.8", + "@devvit/notifications": "0.14.8", + "@devvit/payments": "0.14.8", + "@devvit/realtime": "0.14.8", + "@devvit/reddit": "0.14.8", + "@devvit/redis": "0.14.8", + "@devvit/scheduler": "0.14.8", + "@devvit/server": "0.14.8", + "@devvit/settings": "0.14.8", + "@devvit/shared": "0.14.8" + } + }, + "node_modules/@devvit/web-view-scripts": { + "version": "0.14.8", + "resolved": "https://registry.npmjs.org/@devvit/web-view-scripts/-/web-view-scripts-0.14.8.tgz", + "integrity": "sha512-km7xeuiigN/YDBUKeDqGsGVBO0Ij1S9Hlsn4EEJpySkxxgJGy9nynME6bP2jF9HHKkFZCB2GWs4rqFBnTlFf0w==", + "license": "BSD-3-Clause", + "dependencies": { + "@devvit/protos": "0.14.8", + "@devvit/shared": "0.14.8", + "@devvit/shared-types": "0.14.8", + "html-to-image": "1.11.13", + "jwt-decode": "4.0.0", + "web-vitals": "4.2.4" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.2.tgz", + "integrity": "sha512-XExcO+dvLKvVtNTibSTBej1NCAbaGhWn9Ww1ZPx80qsahhPFe/8jgWP0IchNe0F3HwkU7n8ejhH8bjonqht8mQ==", "cpu": [ - "arm64" + "ppc64" ], - "dev": true, - "license": "MPL-2.0", - "optional": true, + "extraneous": true, + "license": "MIT", "os": [ - "android" + "aix" ], "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" + "node": ">=18" } }, - "node_modules/lightningcss-darwin-arm64": { - "version": "1.33.0", - "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.33.0.tgz", - "integrity": "sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg==", + "node_modules/@esbuild/android-arm": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.2.tgz", + "integrity": "sha512-kXXoiPVVGQcnIYGOeaovwOURpniDBpSq4A03qkQ+BMQqtGG6HYap3xne9C1O1yo4TR3qxlCX5IqqmX6fFo2Lqg==", "cpu": [ - "arm64" + "arm" ], - "dev": true, - "license": "MPL-2.0", - "optional": true, + "extraneous": true, + "license": "MIT", "os": [ - "darwin" + "android" ], "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" + "node": ">=18" } }, - "node_modules/lightningcss-darwin-x64": { - "version": "1.33.0", - "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.33.0.tgz", - "integrity": "sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ==", + "node_modules/@esbuild/android-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.2.tgz", + "integrity": "sha512-5YfKeeI8qWfBZIX+u2xZC3Zlb3Os/gLS2sbEKM+I4ZOcsWmHS2WLysCcQZDAFRslDUU5Oiq44gf6PYN1vGwG5A==", "cpu": [ - "x64" + "arm64" ], - "dev": true, - "license": "MPL-2.0", - "optional": true, + "extraneous": true, + "license": "MIT", "os": [ - "darwin" + "android" ], "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" + "node": ">=18" } }, - "node_modules/lightningcss-freebsd-x64": { - "version": "1.33.0", - "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.33.0.tgz", - "integrity": "sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg==", + "node_modules/@esbuild/android-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.2.tgz", + "integrity": "sha512-O387ite7SzUyCcy3JQX4P4bLtEA7bLLkx+esve5JHnyYfNTxcVpXZo9jhdB0lTKN44gztELTdU7nS8Nr16Fs1Q==", "cpu": [ "x64" ], - "dev": true, - "license": "MPL-2.0", - "optional": true, + "extraneous": true, + "license": "MIT", "os": [ - "freebsd" + "android" ], "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" + "node": ">=18" } }, - "node_modules/lightningcss-linux-arm-gnueabihf": { - "version": "1.33.0", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.33.0.tgz", - "integrity": "sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ==", + "node_modules/@esbuild/darwin-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.2.tgz", + "integrity": "sha512-n4KqkOQrraxHJcgjM1RvwbigfQKIKJVpM7xp+KsxiyUSrRdIXnt73VhrPAx0fV44hgfmIVKjxMN9J1t5jySVkw==", "cpu": [ - "arm" + "arm64" ], - "dev": true, - "license": "MPL-2.0", - "optional": true, + "extraneous": true, + "license": "MIT", "os": [ - "linux" + "darwin" ], "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" + "node": ">=18" } }, - "node_modules/lightningcss-linux-arm64-gnu": { - "version": "1.33.0", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.33.0.tgz", - "integrity": "sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg==", + "node_modules/@esbuild/darwin-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.2.tgz", + "integrity": "sha512-uq6suIWYP37qzGddBKPw5QEQPi6HiLGsO7UmkpfyaYNQ3D+rN6w6WfwH+nuqcGXWvawGwxOEroO4YGnFh95azw==", + "cpu": [ + "x64" + ], + "extraneous": true, + "license": "MIT", + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.2.tgz", + "integrity": "sha512-n+I0BTSRIoy+d6RPKnEVwql5UwBJolytvY4mAOIEJorKlqgPII8ix6slVVrfZ5Tnj7glIZvloylbB/EJPMWEXw==", "cpu": [ "arm64" ], - "dev": true, - "libc": [ - "glibc" + "extraneous": true, + "license": "MIT", + "os": [ + "freebsd" ], - "license": "MPL-2.0", - "optional": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.2.tgz", + "integrity": "sha512-78XJTJkvPs0kz2w61301PJjXl4g7q3JqiYMZ/M/yVI73EHBrCRTgkhu9oqG7vPqq+a/yadEW8aD+agKlk5xrmg==", + "cpu": [ + "x64" + ], + "extraneous": true, + "license": "MIT", + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.2.tgz", + "integrity": "sha512-XlDnu2q5yoqems+xay6wSAcg9DDD7K9RLKZEBOMZm3ckNpJBvOX20tSfby8KfrrhINDyv9V2YVZKY/SpoGJI8w==", + "cpu": [ + "arm" + ], + "extraneous": true, + "license": "MIT", "os": [ "linux" ], "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" + "node": ">=18" } }, - "node_modules/lightningcss-linux-arm64-musl": { - "version": "1.33.0", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.33.0.tgz", - "integrity": "sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ==", + "node_modules/@esbuild/linux-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.2.tgz", + "integrity": "sha512-pW4AC0P3it8c7do9MVM4p51FzHzdM/TZrerurgRcHJ2WTa1VQ1CIq18xncfpBJw4ojkiZZrKW2yIBWBP92j6Ug==", "cpu": [ "arm64" ], - "dev": true, - "libc": [ - "musl" + "extraneous": true, + "license": "MIT", + "os": [ + "linux" ], - "license": "MPL-2.0", - "optional": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.2.tgz", + "integrity": "sha512-CYbnj78HsIeA+DhgUKgFCfvNsTHFhMMrinUrMZpDXJXKN8T3XViTZ/+wtHeVxEWY8ewSzTFN+nRmSwO2tZaLUQ==", + "cpu": [ + "ia32" + ], + "extraneous": true, + "license": "MIT", "os": [ "linux" ], "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" + "node": ">=18" } }, - "node_modules/lightningcss-linux-x64-gnu": { - "version": "1.33.0", - "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.33.0.tgz", - "integrity": "sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg==", + "node_modules/@esbuild/linux-loong64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.2.tgz", + "integrity": "sha512-buwkd8nsph4R+ajRvw0qM5Hja/TXQow3ptzWO2EbG/cqcIkHloRrdlBtQlshyYGTNFvfkfJ5tpPLVkY4DtsPfQ==", "cpu": [ - "x64" + "loong64" ], - "dev": true, - "libc": [ - "glibc" + "extraneous": true, + "license": "MIT", + "os": [ + "linux" ], - "license": "MPL-2.0", - "optional": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.2.tgz", + "integrity": "sha512-ZVykbDyk7519VwiNb9Lcj9m8XM6v5V9uKPvrEMkkEedVewf+0itkhahp4HDpgERXhwLRpWFypsGbG/J8s0QjJA==", + "cpu": [ + "mips64el" + ], + "extraneous": true, + "license": "MIT", "os": [ "linux" ], "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" + "node": ">=18" } }, - "node_modules/lightningcss-linux-x64-musl": { - "version": "1.33.0", - "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.33.0.tgz", - "integrity": "sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw==", + "node_modules/@esbuild/linux-ppc64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.2.tgz", + "integrity": "sha512-CAXl+Dtd9UUuJd8pKKdwh6MLm3MUMiqMPmhZ3tTSXPqfyQ3vDl6R5hZdZ/kYojK4ofXtdfSv1tFq8XzWx3heNQ==", "cpu": [ - "x64" + "ppc64" ], - "dev": true, - "libc": [ - "musl" + "extraneous": true, + "license": "MIT", + "os": [ + "linux" ], - "license": "MPL-2.0", - "optional": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.2.tgz", + "integrity": "sha512-GeXCej4IQtU1B+QlDV8W/RRvbzI3O/Stss+/bCXv4lZls5WGRtu2a+3JkA3i4qIUlMXpcHebWpF8AkJhATowuA==", + "cpu": [ + "riscv64" + ], + "extraneous": true, + "license": "MIT", "os": [ "linux" ], "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" + "node": ">=18" } }, - "node_modules/lightningcss-win32-arm64-msvc": { - "version": "1.33.0", - "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.33.0.tgz", - "integrity": "sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA==", + "node_modules/@esbuild/linux-s390x": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.2.tgz", + "integrity": "sha512-3H1weTYZPxt/WOhByszQZybS9w5lKzUn1FDMsgEChbHWQwHYQQRfBxgCcZvPhjHfKyJjIievvMmEUawJrdY9Dg==", + "cpu": [ + "s390x" + ], + "extraneous": true, + "license": "MIT", + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.2.tgz", + "integrity": "sha512-4xTZr1FUmSoQW4XIWmit3tzQrUTZM+N3P0XV8xROKYF50XfI7xeO90+1bZvNwxIufQ9hDQVRJH5YhgPVF8A/HQ==", + "cpu": [ + "x64" + ], + "extraneous": true, + "license": "MIT", + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.2.tgz", + "integrity": "sha512-sSATRjPeDBg3pdgHoQfoYBob11Kk1FGa9lui5RIHZCoCkJa9QKlvl3/vKz2usCmYYjs7ymJR/2Nnsqe+Hjt5nw==", "cpu": [ "arm64" ], - "dev": true, - "license": "MPL-2.0", - "optional": true, + "extraneous": true, + "license": "MIT", "os": [ - "win32" + "netbsd" ], "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" + "node": ">=18" } }, - "node_modules/lightningcss-win32-x64-msvc": { - "version": "1.33.0", - "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.33.0.tgz", - "integrity": "sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA==", + "node_modules/@esbuild/netbsd-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.2.tgz", + "integrity": "sha512-lqnzCV+mM0gIADaKihiCg6ifgfU2L3h5E33rNQBN1Y4MaVGnzryzmvvf7UHxprpQdE8hpqLolJ9Rl+SkIRDpyw==", "cpu": [ "x64" ], - "dev": true, - "license": "MPL-2.0", - "optional": true, + "extraneous": true, + "license": "MIT", + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.2.tgz", + "integrity": "sha512-AL2qJILH7lNjrDmCQDvdxMfAUIv8KMNZOvrwAQ8i8//ntL9FflhOyMJ8OZSMBb8/AWXe3/5v5S20y3zCoZWKoQ==", + "cpu": [ + "arm64" + ], + "extraneous": true, + "license": "MIT", + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.2.tgz", + "integrity": "sha512-QtiuPytchRyC4rwUKhexJdQKvDuZ6hWloi3igqPQNUJCS1/v9EiO3UTOXR6A3FoMo4fnAKbWJdqaIwhOzh8qEw==", + "cpu": [ + "x64" + ], + "extraneous": true, + "license": "MIT", + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.2.tgz", + "integrity": "sha512-WkhYDmpTjLvGlScA1rwjRUmhl4k8oXR3cIbtqWmELgU/dFeHHlEllxDvdWcNJV9rbzCexB5vz8gtNewWLgCT7Q==", + "cpu": [ + "arm64" + ], + "extraneous": true, + "license": "MIT", + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.2.tgz", + "integrity": "sha512-GPMSkTOtMnv2U2F8gxe4Io6qmVs+YKyp832Etqqxr0hFngmXQ3rzwytelm3GIn7T4VviRUlf3sOgBOiTdvaf7g==", + "cpu": [ + "x64" + ], + "extraneous": true, + "license": "MIT", + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.2.tgz", + "integrity": "sha512-PIhhEkE9uPBleRBrQEJpUn7MBnibZzbGzYWPmY3x+YoVg/95zbjB4CxPPOQ8l5tYYM4mMaCthF8/1DIfBQQyWQ==", + "cpu": [ + "arm64" + ], + "extraneous": true, + "license": "MIT", "os": [ "win32" ], "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" + "node": ">=18" } }, - "node_modules/long": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz", - "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==", - "license": "Apache-2.0" + "node_modules/@esbuild/win32-ia32": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.2.tgz", + "integrity": "sha512-YmJbfTlvU7Sdn9BB+4PRES4oB6pxgS37MAONj+hBr/cpXS1aBPKXxNnDbu+QCWPj0o9dgyxeq79g6c5P8KeuYA==", + "cpu": [ + "ia32" + ], + "extraneous": true, + "license": "MIT", + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } }, - "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, + "node_modules/@esbuild/win32-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.2.tgz", + "integrity": "sha512-5ebpxr3nWMzrL/rnUI755Jkuee0bHL/Gq0WTF9lvcpv73wAp5eu8MfBUgWK9bhWvZjj7yX8etf/8tI8Ney695g==", + "cpu": [ + "x64" + ], + "extraneous": true, "license": "MIT", - "dependencies": { + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@hono/node-server": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-2.1.4.tgz", + "integrity": "sha512-2g1qeHl4BduwDn6L8nkAvDrPUWPMlkQ//a5NhP/zIyaXKMjZNRV+cG9Csu+BiJFf6+68pqzjf4Hi5sJGKHuS1g==", + "license": "MIT", + "engines": { + "node": ">=20" + }, + "peerDependencies": { + "hono": "^4 || ^5.0.0-0" + } + }, + "node_modules/@improbable-eng/grpc-web": { + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/@improbable-eng/grpc-web/-/grpc-web-0.15.0.tgz", + "integrity": "sha512-ERft9/0/8CmYalqOVnJnpdDry28q+j+nAlFFARdjyxXDJ+Mhgv9+F600QC8BR9ygOfrXRlAk6CvST2j+JCpQPg==", + "license": "Apache-2.0", + "dependencies": { + "browser-headers": "^0.4.1" + }, + "peerDependencies": { + "google-protobuf": "^3.14.0" + } + }, + "node_modules/@improbable-eng/grpc-web-node-http-transport": { + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/@improbable-eng/grpc-web-node-http-transport/-/grpc-web-node-http-transport-0.15.0.tgz", + "integrity": "sha512-HLgJfVolGGpjc9DWPhmMmXJx8YGzkek7jcCFO1YYkSOoO81MWRZentPOd/JiKiZuU08wtc4BG+WNuGzsQB5jZA==", + "license": "Apache-2.0", + "peerDependencies": { + "@improbable-eng/grpc-web": ">=0.13.0" + } + }, + "node_modules/@ioredis/as-callback": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@ioredis/as-callback/-/as-callback-3.0.0.tgz", + "integrity": "sha512-Kqv1rZ3WbgOrS+hgzJ5xG5WQuhvzzSTRYvNeyPMLOAM78MHSnuKI20JeJGbpuAt//LCuP0vsexZcorqW7kWhJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@ioredis/commands": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/@ioredis/commands/-/commands-1.11.0.tgz", + "integrity": "sha512-tuMmOu6dtyGFv/fzCjtapCJj/zgoHaFsqs3wKsroJSRXtlLmyL/t+B7uaQiavGk1F3WWFQcUqZwk92bpp9jKcA==", + "dev": true, + "license": "MIT" + }, + "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==", + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.6.0.tgz", + "integrity": "sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw==", + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@oclif/color": { + "version": "1.0.13", + "resolved": "https://registry.npmjs.org/@oclif/color/-/color-1.0.13.tgz", + "integrity": "sha512-/2WZxKCNjeHlQogCs1VBtJWlPXjwWke/9gMrwsVsrUt00g2V6LUBvwgwrxhrXepjOmq4IZ5QeNbpDMEOUlx/JA==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.2.1", + "chalk": "^4.1.0", + "strip-ansi": "^6.0.1", + "supports-color": "^8.1.1", + "tslib": "^2" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/@oclif/color/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/@oclif/core": { + "version": "2.9.4", + "resolved": "https://registry.npmjs.org/@oclif/core/-/core-2.9.4.tgz", + "integrity": "sha512-eFRRpV+tJ6nMkhay2M9IppjSF3atRrgj6Qo83qUslaFSAW3NAl4mIhx1mKmTwQx5rgSrar03xICtSAWJ6gZtag==", + "license": "MIT", + "dependencies": { + "@types/cli-progress": "^3.11.0", + "ansi-escapes": "^4.3.2", + "ansi-styles": "^4.3.0", + "cardinal": "^2.1.1", + "chalk": "^4.1.2", + "clean-stack": "^3.0.1", + "cli-progress": "^3.12.0", + "debug": "^4.3.4", + "ejs": "^3.1.8", + "fs-extra": "^9.1.0", + "get-package-type": "^0.1.0", + "globby": "^11.1.0", + "hyperlinker": "^1.0.0", + "indent-string": "^4.0.0", + "is-wsl": "^2.2.0", + "js-yaml": "^3.14.1", + "natural-orderby": "^2.0.3", + "object-treeify": "^1.1.33", + "password-prompt": "^1.1.2", + "semver": "^7.5.3", + "slice-ansi": "^4.0.0", + "string-width": "^4.2.3", + "strip-ansi": "^6.0.1", + "supports-color": "^8.1.1", + "supports-hyperlinks": "^2.2.0", + "ts-node": "^10.9.1", + "tslib": "^2.5.0", + "widest-line": "^3.1.0", + "wordwrap": "^1.0.0", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@oclif/core/node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "license": "MIT", + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/@oclif/core/node_modules/js-yaml": { + "version": "3.15.2", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.15.2.tgz", + "integrity": "sha512-6EuL879VkRA+1Cz578mKMiKvjPNEuk6+r1JaFzoSWejZmtf7xWbIyw1e3KkxlkzTIt9Taw6JBhEppG7utc1P+w==", + "license": "MIT", + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/@oclif/core/node_modules/sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", + "license": "BSD-3-Clause" + }, + "node_modules/@oclif/core/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/@oclif/plugin-autocomplete": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/@oclif/plugin-autocomplete/-/plugin-autocomplete-2.3.3.tgz", + "integrity": "sha512-4/7l3YrACS7KfONoams7+xAbVfo7tje0t2Qp84/KAUpY1Yb/11AMPHtzgoRp5A5Y3OFPmLCfRF1YUydz+jGuwg==", + "license": "MIT", + "dependencies": { + "@oclif/core": "^2.8.2", + "chalk": "^4.1.0", + "debug": "^4.3.4", + "fs-extra": "^9.0.1" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/@oclif/plugin-help": { + "version": "5.2.14", + "resolved": "https://registry.npmjs.org/@oclif/plugin-help/-/plugin-help-5.2.14.tgz", + "integrity": "sha512-7hMLc6zqxeRfG4nvHHQPpbaBj60efM3ULFkCpHZkdLms/ezIkNo40F661QuraIjMP/NN+U6VSfBCGuPkRyxVkA==", + "license": "MIT", + "dependencies": { + "@oclif/core": "^2.9.3" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/@oclif/plugin-not-found": { + "version": "2.3.34", + "resolved": "https://registry.npmjs.org/@oclif/plugin-not-found/-/plugin-not-found-2.3.34.tgz", + "integrity": "sha512-uXUpw6o2e0aqnNn+XkGL7LbL+Th2rBD1JGtFbb6anmvUvz2skiGz0o23BYmrQW8tvU92ajPOykfClKD75ptZcw==", + "license": "MIT", + "dependencies": { + "@oclif/color": "^1.0.9", + "@oclif/core": "^2.9.4", + "fast-levenshtein": "^3.0.0" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/@oclif/plugin-warn-if-update-available": { + "version": "2.0.45", + "resolved": "https://registry.npmjs.org/@oclif/plugin-warn-if-update-available/-/plugin-warn-if-update-available-2.0.45.tgz", + "integrity": "sha512-MEncCUHW1vCOQdvt1z46jAblwvuGcs3Q1Gjl8IghazGJ0GRHzGOMILABpqVWR5uH/YJ3gfs05Tt7M4LdZ40N3g==", + "license": "MIT", + "dependencies": { + "@oclif/core": "^2.9.4", + "chalk": "^4.1.0", + "debug": "^4.1.0", + "fs-extra": "^9.0.1", + "http-call": "^5.2.2", + "lodash": "^4.17.21", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/@oxc-project/types": { + "version": "0.154.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.154.0.tgz", + "integrity": "sha512-KYOUGQFlLWR+s4QMV2Q9ko38TZm7EdO87Qm7rvZLqk/webse/cDjtyxKhg/47l0byCbs/k46LT8qc4PKjAikzA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/oxc-project" + } + }, + "node_modules/@posthog/browser-common": { + "version": "0.9.5", + "resolved": "https://registry.npmjs.org/@posthog/browser-common/-/browser-common-0.9.5.tgz", + "integrity": "sha512-oXnBgZvM8UQ84IcBLfGP0WSGd9rA0PCGR37H8Pw0NszJRwxwbJnFXIUih0pvI/ze2eGmzkIEC9i3SwC+bMAgjQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@posthog/core": "^1.59.1", + "@posthog/types": "^1.416.0" + } + }, + "node_modules/@posthog/core": { + "version": "1.59.1", + "resolved": "https://registry.npmjs.org/@posthog/core/-/core-1.59.1.tgz", + "integrity": "sha512-P39YgzQVDh6oz6wAzQXEnoHgTYpM7gifonmVKq1scAJco79h31XmmNVurRq9tTmmyIrr1uX8FHT0riCsyfV9AQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@posthog/types": "^1.416.0" + } + }, + "node_modules/@posthog/types": { + "version": "1.416.0", + "resolved": "https://registry.npmjs.org/@posthog/types/-/types-1.416.0.tgz", + "integrity": "sha512-0/9+02s/0uDyrH4G/qUg1yyl6gXUg8Uuxw/PlDad3KLzo9FFS9zS/kAQaEshx/saVGupj1aAy/4EUBxWisBogw==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/@protobuf-ts/plugin-framework": { + "version": "2.11.0", + "resolved": "https://registry.npmjs.org/@protobuf-ts/plugin-framework/-/plugin-framework-2.11.0.tgz", + "integrity": "sha512-D6Mno2i/NcRrDzbTq/2QSXUheXds6Ye+uaBSAHvPy2GAre4vjz1S0B6JTzVbU/B8hI3Msc/zMQSHb5Ak5Iy97g==", + "license": "(Apache-2.0 AND BSD-3-Clause)", + "dependencies": { + "@protobuf-ts/runtime": "^2.11.0", + "typescript": "^3.9" + } + }, + "node_modules/@protobuf-ts/plugin-framework/node_modules/typescript": { + "version": "3.9.10", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.9.10.tgz", + "integrity": "sha512-w6fIxVE/H1PkLKcCPsFqKE7Kv7QUwhU8qQY2MueZXWx5cPZdwFupLgKK3vntcK98BtNHZtAF4LA/yl2a7k8R6Q==", + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=4.2.0" + } + }, + "node_modules/@protobuf-ts/runtime": { + "version": "2.11.1", + "resolved": "https://registry.npmjs.org/@protobuf-ts/runtime/-/runtime-2.11.1.tgz", + "integrity": "sha512-KuDaT1IfHkugM2pyz+FwiY80ejWrkH1pAtOBOZFuR6SXEFTsnb/jiQWQ1rCIrcKx2BtyxnxW6BWwsVSA/Ie+WQ==", + "license": "(Apache-2.0 AND BSD-3-Clause)" + }, + "node_modules/@protobufjs/aspromise": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", + "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/base64": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz", + "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/codegen": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.5.tgz", + "integrity": "sha512-zgXFLzW3Ap33e6d0Wlj4MGIm6Ce8O89n/apUaGNB/jx+hw+ruWEp7EwGUshdLKVRCxZW12fp9r40E1mQrf/34g==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/eventemitter": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.1.tgz", + "integrity": "sha512-vW1GmwMZNnL+gMRaovlh9yZX74kc+TTU3FObkkurpMaRtBfLP3ldjS9KQWlwZgraRE0+dheEEoAxdzcJQ8eXZg==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/fetch": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.1.tgz", + "integrity": "sha512-GpptLrs57adMSuHi3VNj0mAF8dwh36LMaYF6XyJ6JMWlVsc+t42tm1HSEDmOs3A8fC9yyeisgLhsTVQokOZ0zw==", + "license": "BSD-3-Clause", + "dependencies": { + "@protobufjs/aspromise": "^1.1.1" + } + }, + "node_modules/@protobufjs/float": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz", + "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/path": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz", + "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/pool": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz", + "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/utf8": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.2.tgz", + "integrity": "sha512-b1UQwcEZ4yCnMCD8DAL1VlbvBJE9/IX4FTIp7BG1xYpf29SLazLSrqUkj4w7Y5y7cCVP6E5tcqqcI0xemPkHug==", + "license": "BSD-3-Clause" + }, + "node_modules/@rolldown/binding-android-arm-eabi": { + "version": "1.2.14", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm-eabi/-/binding-android-arm-eabi-1.2.14.tgz", + "integrity": "sha512-aWR+XKH9+rW7m5fWq/5360J3mHycrx/o6xZM2DQFdJnTMdyP7/WzxPjhtLbVWdKo2y612/4U1bWqb3HQhiF7vg==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-android-arm64": { + "version": "1.2.14", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.2.14.tgz", + "integrity": "sha512-QiVxHofl6A8lQU7WAtOUY4QMVZXohxwz7WJJoeh8NBEUyKfIkMRKhYyq8az/1RypNiIXzNTtiS6KwHz0erxYRQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-arm64": { + "version": "1.2.14", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.2.14.tgz", + "integrity": "sha512-lTQOzHuBIooh3jIr5XINQkjPGEuGy+Dsy0meXDr6Sjty0b99kYgGh4uBC/8mJX1T0kC8d6S0aPP1Y+bHkh6aBw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-x64": { + "version": "1.2.14", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.2.14.tgz", + "integrity": "sha512-hyMKEGatjmBYCs/Q4sbIcL/GqpFEqsX3wc+HYVQ/85A4rxc1o7rPm4DUI4GV4k/h0ITddwFSGpqQunI14GXjTA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-freebsd-x64": { + "version": "1.2.14", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.2.14.tgz", + "integrity": "sha512-u371GbXoVXrEgP20PwzoGNOdAOqAMhzo19JzzY+V4YmmnC+DTOoqdbMu+O+54emM59Mik/UdrtWgcuEDKWRPKA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm-gnueabihf": { + "version": "1.2.14", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.2.14.tgz", + "integrity": "sha512-BqwKsdZQf6EqVWr8fv2MrDYzAvOZ7+NYFSJESRGs87fkseEhs0TU9TXgRJTkZoRRkxTizyEg1LKUXfU3/78uqg==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-gnu": { + "version": "1.2.14", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.2.14.tgz", + "integrity": "sha512-SD9kY8M009x/7s57Z1dgUVlP5+uYBgKQK0Tkr1WIWngrtUJmRCW1RJFMY7QDla2khXhmg2i5Io7MUjcmZNZ/KQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-musl": { + "version": "1.2.14", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.2.14.tgz", + "integrity": "sha512-BJ2CL19M6uuWesMgnqJzgxoI+N+eV7b0A9O9vLV42y+gitC41fNa8KElau1tHVzuLWX5GzEuxMPhAO0tQXQMkg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-ppc64-gnu": { + "version": "1.2.14", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.2.14.tgz", + "integrity": "sha512-4+Kl8daPV8NDHaD7mfCcc1pRM+Qg7ab8DyMm5FG5WoZgv4h2i3KFN1CybYBNbQqmuYm7ftJLvmMI1eHYJ22S/Q==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-s390x-gnu": { + "version": "1.2.14", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.2.14.tgz", + "integrity": "sha512-V/h8ePq1fKDrmtFMz6xcCN3DKvuvMRrwwD1q0PkBeeo4DpK64biaKvqWGVxqCeYKqeRk7gE7/iKPxLCn08O97A==", + "cpu": [ + "s390x" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-gnu": { + "version": "1.2.14", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.2.14.tgz", + "integrity": "sha512-WxalXzgrLVpKC93xfpaJ3gCXUWzeCr+FRCbDVD2mUAy4L6Vo3lNVz0epUEUpxPWJkLNYUwM4OSIikv36OJ9mLg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-musl": { + "version": "1.2.14", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.2.14.tgz", + "integrity": "sha512-3G3tnAmTpdk9jO3GQtyHXqeFD2SlecfTws/Y3M2mmj9nMBwwuLT4kur8sZKeu7ENUHSpVO3cIsm5A52LEWWwlg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-openharmony-arm64": { + "version": "1.2.14", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.2.14.tgz", + "integrity": "sha512-qyr5XiNGxm2ytx17vvtcJ9nXn6v1gENETi503bJx3OGwZ1tSonj7BosxbMa/xeAfSl5iS9X6KjEWYXLu6v1L7Q==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.2.14", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.2.14.tgz", + "integrity": "sha512-oA4o5o0S2yB199YyYXDzKUv1Z56Rc+mgOjwm7Z/vJ9Z8igiDPYvoOuDyo6g7AK/ZaF09nPhDKVnGBcYHO11GXA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-x64-msvc": { + "version": "1.2.14", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.2.14.tgz", + "integrity": "sha512-9mE94bf2GZUaR/fUSXCMpBoWkctu421Mi0ORpm4zqfbkjKep7RxzPFa5etcGs5h+2KApoWBOA/hAJa/7saGgEQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz", + "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==", + "license": "MIT" + }, + "node_modules/@sec-ant/readable-stream": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@sec-ant/readable-stream/-/readable-stream-0.4.1.tgz", + "integrity": "sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg==", + "license": "MIT" + }, + "node_modules/@sindresorhus/merge-streams": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/merge-streams/-/merge-streams-4.0.0.tgz", + "integrity": "sha512-tlqY9xq5ukxTUZBmoOp+m61cqwQD5pHJtFY3Mn8CA8ps6yghLH/Hw8UPdqg4OLmFW3IFlcXnQNmo/dh8HzXYIQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tokenizer/inflate": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@tokenizer/inflate/-/inflate-0.4.1.tgz", + "integrity": "sha512-2mAv+8pkG6GIZiF1kNg1jAjh27IDxEPKwdGul3snfztFerfPGI1LjDezZp3i7BElXompqEtPmoPx6c2wgtWsOA==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.3", + "token-types": "^6.1.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, + "node_modules/@tokenizer/token": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@tokenizer/token/-/token-0.3.0.tgz", + "integrity": "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A==", + "license": "MIT" + }, + "node_modules/@tsconfig/node10": { + "version": "1.0.13", + "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.13.tgz", + "integrity": "sha512-gcLdvR9HO1ZJBypsOGqaP6TFEzb6vIta0KSTLt9NAQ6pXQO3cRgSVyCN6pzYqI9DlJgY71XKO0dpDhCf08b3pg==", + "license": "MIT" + }, + "node_modules/@tsconfig/node12": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", + "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", + "license": "MIT" + }, + "node_modules/@tsconfig/node14": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", + "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", + "license": "MIT" + }, + "node_modules/@tsconfig/node16": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", + "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", + "license": "MIT" + }, + "node_modules/@types/chai": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" + } + }, + "node_modules/@types/cli-progress": { + "version": "3.11.6", + "resolved": "https://registry.npmjs.org/@types/cli-progress/-/cli-progress-3.11.6.tgz", + "integrity": "sha512-cE3+jb9WRlu+uOSAugewNpITJDt1VF8dHOopPO4IABFc3SXYL5WE/+PTz/FCdZRRfIujiWW3n3aMbv1eIGVRWA==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/ioredis-mock": { + "version": "8.2.8", + "resolved": "https://registry.npmjs.org/@types/ioredis-mock/-/ioredis-mock-8.2.8.tgz", + "integrity": "sha512-Nxr1CXk8NtGVzWQ8WoGCOyo+HARd4CFc/Nt2+ITt3WWLNlj1InW38SzZwOJNqzA38iplrpBFp6Ny4KjPfeI+bA==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "ioredis": "^5" + } + }, + "node_modules/@types/node": { + "version": "24.19.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.19.2.tgz", + "integrity": "sha512-93d49wCWJkCyN87MxLpNN6KXPCwFw7bVUg79mGUV1OS40JGWwVCr8n61DXCSqz7CV6/VfPkb7xec7uP2M+QHWQ==", + "license": "MIT", + "dependencies": { + "undici-types": ">=7.24.0 <7.24.7" + } + }, + "node_modules/@types/trusted-types": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", + "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true + }, + "node_modules/@types/ws": { + "version": "8.5.12", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.12.tgz", + "integrity": "sha512-3tPRkv1EtkDpzlgyKyI8pGsGZAGPEaXeu0DOj5DI25Ja91bdAYddYHbADRYVrZMRbfW+1l5YwXVDKohDJNQxkQ==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@vitest/expect": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.11.tgz", + "integrity": "sha512-VX2x5vNJXET47KAFzwERI+KRMtTTCSWTfSMKsW7JsUsXV4psq++e3DvZpuTDOpHcxytiDs6p2nhVb2tVDiiUYw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.1.0", + "@types/chai": "^5.2.2", + "@vitest/spy": "4.1.11", + "@vitest/utils": "4.1.11", + "chai": "^6.2.2", + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.11.tgz", + "integrity": "sha512-2XJVD55d1o5AZous5CCGKS74g/riOj9odEt2bQpCVZeblHyHdnMeFl4jl0XjU21stf4mbjUkew2eXQZt65g5CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "4.1.11", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.21" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/pretty-format": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.11.tgz", + "integrity": "sha512-yiZzPbGTS9Sr/JpFl8zHrcIkAofNbFV6k21vIgQN/cY/oxZeXhJv5sc/MBJ5jFKWmWs+oJHw0UXLZjmf931+Vw==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.11.tgz", + "integrity": "sha512-LztvUgdwMNJMIkj3hQnnxiC2Xy1zNxq928W/xhjCLaNCzqTZOudjwbQf6v9IntZGPw132i2Lq2rgTRZHD3JHNw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "4.1.11", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.11.tgz", + "integrity": "sha512-pN7ikn1ON7h8ee4gIAp4AzyK+zBtJPzVbqOgu5LCEh4VaJVbPQcgYQYJIMGQPXVeJJq1fnfazis7a5pFNPahog==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.1.11", + "@vitest/utils": "4.1.11", + "magic-string": "^0.30.21", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.11.tgz", + "integrity": "sha512-apNa/prQy2qCeywhnixOHPRCgGNhvg7T4Dapfl1GahLp/R+uhBm5cPyFoNVyqsNd2h1nJxL6BqqdIjiABL60YA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.11.tgz", + "integrity": "sha512-zTCVGpyFsGWBhllOyKlTw/vnr6D9qxsfSDyfbyZmTyjHw5N/VuvzHpHoQjm2ZJzn4RJgx5w4r7V0er69CmLgPQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.1.11", + "convert-source-map": "^2.0.0", + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/accepts": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", + "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "mime-types": "^3.0.0", + "negotiator": "^1.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/acorn": { + "version": "8.19.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.19.0.tgz", + "integrity": "sha512-oJlA3XiRm7Cyk6qFD2Jn8ak9B7jSy0qy00ADO3+8dpT0LSjFihQYv4C02LFCSYJV3Q37xYwwRy5m+IpIiUzqWw==", + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-walk": { + "version": "8.3.5", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.5.tgz", + "integrity": "sha512-HEHNfbars9v4pgpW6SO1KSPkfoS0xVOM/9UzkJltjlsHZmJasxg8aXkuZa7SMf8vKGIBhpUsPluQSqhJFCqebw==", + "license": "MIT", + "dependencies": { + "acorn": "^8.11.0" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "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-escapes": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", + "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", + "license": "MIT", + "dependencies": { + "type-fest": "^0.21.3" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/ansicolors": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/ansicolors/-/ansicolors-0.3.2.tgz", + "integrity": "sha512-QXu7BPrP29VllRxH8GwB7x5iX5qWKAAMLqKQGWTeLWVlNHNOpVMJ91dsxQAIWXpjuW5wqvxu3Jd/nRjrJ+0pqg==", + "license": "MIT" + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/arg": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", + "license": "MIT" + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "license": "Python-2.0" + }, + "node_modules/array-union": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", + "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "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/astral-regex": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz", + "integrity": "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/async": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", + "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", + "license": "MIT" + }, + "node_modules/async-lock": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/async-lock/-/async-lock-1.4.1.tgz", + "integrity": "sha512-Az2ZTpuytrtqENulXwO3GGv1Bztugx6TT37NIo7imr/Qo0gsYiGtSdBa2B6fsXhTpVZDNfu1Qn3pk531e3q+nQ==", + "license": "MIT" + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, + "node_modules/at-least-node": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz", + "integrity": "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==", + "license": "ISC", + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/available-typed-arrays": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", + "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", + "license": "MIT", + "dependencies": { + "possible-typed-array-names": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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==", + "license": "MIT" + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "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/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/bl": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-5.1.0.tgz", + "integrity": "sha512-tv1ZJHLfTDnXE6tMHv73YgSJaWR2AFuPwMntBe7XL/GBFHnT0CLnsHMogfk5+GzCDC5ZWarSCYaIGATZt9dNsQ==", + "license": "MIT", + "dependencies": { + "buffer": "^6.0.3", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "node_modules/body-parser": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.3.0.tgz", + "integrity": "sha512-2cGmJupaNgg+QUwVLAucDuWuoMZ6EX9iHDRswZ5lsNYEmwPaRknMPCLZz07yTzVq/83p4o/wzbDZbBrTvGGTIw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "bytes": "^3.1.2", + "content-type": "^2.0.0", + "debug": "^4.4.3", + "http-errors": "^2.0.1", + "iconv-lite": "^0.7.2", + "on-finished": "^2.4.1", + "qs": "^6.15.2", + "raw-body": "^3.0.2", + "type-is": "^2.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/brace-expansion": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.7.tgz", + "integrity": "sha512-uZbew1NqdmPDTMJ8ah1y+b+9QEJrfkXFk3RcTQw3X0jW/xRUvFKsg1CfQdSYGdTbXZWExtU3J3ccxtnfw1Fi0g==", + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browser-headers": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/browser-headers/-/browser-headers-0.4.1.tgz", + "integrity": "sha512-CA9hsySZVo9371qEHjHZtYxV2cFtVj5Wj/ZHi8ooEsrtm4vOnl9Y9HmyYWk9q+05d7K3rdoAE0j3MVEFVvtQtg==", + "license": "Apache-2.0" + }, + "node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "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", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, + "node_modules/bundle-name": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/bundle-name/-/bundle-name-4.1.1.tgz", + "integrity": "sha512-DdH81/zPLVS11EUgWq3tEu/xn+EzljlMYooDNdzWEnFha3R3NBMpMV1UqYIpjYHV/SgpFKMIX1Oh7o06SjM/oA==", + "license": "MIT", + "dependencies": { + "run-applescript": "^7.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.9.tgz", + "integrity": "sha512-a/hy+pNsFUTR+Iz8TCJvXudKVLAnz/DyeSUo10I5yvFDQJBFU2s9uqQpoSrJlroHUKoKqzg+epxyP9lqFdzfBQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "get-intrinsic": "^1.3.0", + "set-function-length": "^1.2.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/camel-case": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/camel-case/-/camel-case-4.1.2.tgz", + "integrity": "sha512-gxGWBrTT1JuMx6R+o5PTXMmUnhnVzLQ9SNutD4YqKtI6ap897t3tKECYla6gCWEkplXnlNybEkZg9GEGxKFCgw==", + "license": "MIT", + "dependencies": { + "pascal-case": "^3.1.2", + "tslib": "^2.0.3" + } + }, + "node_modules/cardinal": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/cardinal/-/cardinal-2.1.1.tgz", + "integrity": "sha512-JSr5eOgoEymtYHBjNWyjrMqet9Am2miJhlfKNdqLp6zoeAh0KN5dRAcxlecj5mAJrmQomgiOBj35xHLrFjqBpw==", + "license": "MIT", + "dependencies": { + "ansicolors": "~0.3.2", + "redeyed": "~2.1.0" + }, + "bin": { + "cdl": "bin/cdl.js" + } + }, + "node_modules/chai": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/chai/-/chai-6.3.0.tgz", + "integrity": "sha512-XWAtwJ6OHO+tj0EKCs0Y2UamnyOxseZWltU4x2U2wh8g4AigdjwvtUjvLP2tqkA/avxHEtzxNaqGq/YGNwckKg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/char-regex": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/char-regex/-/char-regex-2.0.2.tgz", + "integrity": "sha512-cbGOjAptfM2LVmWhwRFHEKTPkLwNddVmuqYZQt895yXwAsWsXObCG+YN4DGQ/JBtT4GP1a1lPPdio2z413LmTg==", + "license": "MIT", + "engines": { + "node": ">=12.20" + } + }, + "node_modules/chardet": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/chardet/-/chardet-0.7.0.tgz", + "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==", + "license": "MIT" + }, + "node_modules/chokidar": { + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", + "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==", + "funding": [ + { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + ], + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/clean-git-ref": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/clean-git-ref/-/clean-git-ref-2.0.1.tgz", + "integrity": "sha512-bLSptAy2P0s6hU4PzuIMKmMJJSE6gLXGH1cntDu7bWJUksvuM+7ReOK61mozULErYvP6a15rnYl0zFDef+pyPw==", + "license": "Apache-2.0" + }, + "node_modules/clean-stack": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-3.0.1.tgz", + "integrity": "sha512-lR9wNiMRcVQjSB3a7xXGLuz4cr4wJuuXlaAEbRutGowQTmlp7R72/DOgN21e8jdwblMWl9UOJMJXarX94pzKdg==", + "license": "MIT", + "dependencies": { + "escape-string-regexp": "4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-cursor": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-4.0.0.tgz", + "integrity": "sha512-VGtlMu3x/4DOtIUwEkRezxUZ2lBacNJCHash0N0WeZDBS+7Ux1dm3XWAgWYxLJFMMdOeXMHXorshEFhbMSGelg==", + "license": "MIT", + "dependencies": { + "restore-cursor": "^4.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-progress": { + "version": "3.12.0", + "resolved": "https://registry.npmjs.org/cli-progress/-/cli-progress-3.12.0.tgz", + "integrity": "sha512-tRkV3HJ1ASwm19THiiLIXLO7Im7wlTuKnvkYaTkyoAPefqjNg7W7DHKUlGRxy9vxDvbyCYQkQozvptuMkGCg8A==", + "license": "MIT", + "dependencies": { + "string-width": "^4.2.3" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/cli-spinners": { + "version": "2.9.2", + "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.9.2.tgz", + "integrity": "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==", + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-width": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-4.1.0.tgz", + "integrity": "sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==", + "license": "ISC", + "engines": { + "node": ">= 12" + } + }, + "node_modules/clone": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz", + "integrity": "sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==", + "license": "MIT", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/cluster-key-slot": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/cluster-key-slot/-/cluster-key-slot-1.1.2.tgz", + "integrity": "sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "license": "MIT" + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/commander": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-6.2.1.tgz", + "integrity": "sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA==", + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "license": "MIT" + }, + "node_modules/content-disposition": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-2.0.1.tgz", + "integrity": "sha512-e+H0ZXHSWYrENhQzw1LPuP4oF5MzVKmDU6d3hxlvaPEYLLg62MxtQNPRx4SYSuYJSBUgnQIG4HIN2tEtNv7Dog==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/content-type": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.1.0.tgz", + "integrity": "sha512-mj7UPXE0jaqaOsukNZRUEfEi2AcL7C/vwmwcHV0O97eO1E1pxBZuyjlZrx5seTaNBg1U6+o35wpa35Qfcc+7ag==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", + "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=6.6.0" + } + }, + "node_modules/core-js": { + "version": "3.50.0", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.50.0.tgz", + "integrity": "sha512-BRWgOLKkFeCgRudR6zrs8p9XJZcE14grzKMMssoYrk6krtuEZ7MTKPIY5RzOnqsEKIR9kst7wNzphttraT+Yqw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "peer": true, + "engines": { + "node": "*" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/core-js" + } + }, + "node_modules/core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", + "license": "MIT" + }, + "node_modules/crc-32": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz", + "integrity": "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==", + "license": "Apache-2.0", + "bin": { + "crc32": "bin/crc32.njs" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/create-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", + "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", + "license": "MIT" + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/cssstyle": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-4.6.0.tgz", + "integrity": "sha512-2z+rWdzbbSZv6/rhtvzvqeZQHrBaqgogqt85sqFNbabZOuFbCVFb8kPeEtZjiKkbrm395irpNKiYeFeLiQnFPg==", + "license": "MIT", + "dependencies": { + "@asamuzakjp/css-color": "^3.2.0", + "rrweb-cssom": "^0.8.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/cssstyle/node_modules/rrweb-cssom": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.8.0.tgz", + "integrity": "sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw==", + "license": "MIT" + }, + "node_modules/data-urls": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-5.0.0.tgz", + "integrity": "sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==", + "license": "MIT", + "dependencies": { + "whatwg-mimetype": "^4.0.0", + "whatwg-url": "^14.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/date-fns": { + "version": "2.29.3", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.29.3.tgz", + "integrity": "sha512-dDCnyH2WnnKusqvZZ6+jA1O51Ibt8ZMRNkDZdyAyK4YfbDwa/cEmuztzG5pk6hqlp9aSBPYcjOlktquahGwGeA==", + "license": "MIT", + "engines": { + "node": ">=0.11" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/date-fns" + } + }, + "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/decimal.js": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz", + "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==", + "license": "MIT" + }, + "node_modules/decompress-response": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "license": "MIT", + "dependencies": { + "mimic-response": "^3.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/default-browser": { + "version": "5.5.1", + "resolved": "https://registry.npmjs.org/default-browser/-/default-browser-5.5.1.tgz", + "integrity": "sha512-m1pAzaJgZ/gssEqlOhJkPJp8Xly7QyW6xcrkUa2KKcDeDSEMP7X8xipU3snUcfisTQx0w1AGae+9UtJSfVnXGw==", + "license": "MIT", + "dependencies": { + "bundle-name": "^4.1.0", + "default-browser-id": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/default-browser-id": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/default-browser-id/-/default-browser-id-5.0.1.tgz", + "integrity": "sha512-x1VCxdX4t+8wVfd1so/9w+vQ4vx7lKd2Qp5tDRutErwmR85OgmfX7RlLRMWafRMY7hbEiXIbudNrjOAPa/hL8Q==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/defaults": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/defaults/-/defaults-1.0.4.tgz", + "integrity": "sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A==", + "license": "MIT", + "dependencies": { + "clone": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/define-lazy-prop": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-3.0.0.tgz", + "integrity": "sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/denque": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz", + "integrity": "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=0.10" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/devvit": { + "version": "0.14.8", + "resolved": "https://registry.npmjs.org/devvit/-/devvit-0.14.8.tgz", + "integrity": "sha512-0nTKUAbWpYxuC23z3afjhZCW2pQMQI7vhN+IBct0LNXUHXEV63Cgkhpm1Ojs3GqvcpDIXhm+/UHzXe3sKnypRg==", + "license": "BSD-3-Clause", + "dependencies": { + "@devvit/cli": "0.14.8" + }, + "bin": { + "devvit": "bin/devvit.js" + } + }, + "node_modules/diff": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.4.tgz", + "integrity": "sha512-X07nttJQkwkfKfvTPG/KSnE2OMdcUCao6+eXF3wmnIQRn2aPAHH3VxDbDOdegkd6JbPsXqShpvEOHfAT+nCNwQ==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/diff3": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/diff3/-/diff3-0.0.3.tgz", + "integrity": "sha512-iSq8ngPOt0K53A6eVr4d5Kn6GNrM2nQZtC740pzIriHtn4pOQ2lyzEXQMBeVcWERN0ye7fhBsk9PbLLQOnUx/g==", + "license": "MIT" + }, + "node_modules/dir-glob": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", + "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", + "license": "MIT", + "dependencies": { + "path-type": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/dompurify": { + "version": "3.4.17", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.4.17.tgz", + "integrity": "sha512-B/pSlYVHQ58kqISD/yDjuJ1yfzI34m1gF4lAod2bwSRhiN4K5Cl3H8Af7hqB2PmTL911tdEudnnXn5FnhRoz0w==", + "dev": true, + "license": "(MPL-2.0 OR Apache-2.0)", + "peer": true, + "optionalDependencies": { + "@types/trusted-types": "^2.0.7" + } + }, + "node_modules/dot-object": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/dot-object/-/dot-object-2.1.5.tgz", + "integrity": "sha512-xHF8EP4XH/Ba9fvAF2LDd5O3IITVolerVV6xvkxoM8zlGEiCUrggpAnHyOoKJKCrhvPcGATFAUwIujj7bRG5UA==", + "license": "MIT", + "dependencies": { + "commander": "^6.1.0", + "glob": "^7.1.6" + }, + "bin": { + "dot-object": "bin/dot-object" + } + }, + "node_modules/dotenv": { + "version": "16.5.0", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.5.0.tgz", + "integrity": "sha512-m/C+AwOAr9/W1UOIZUo232ejMNnJAJtYQjUbHoNTBNTJSvqzzDh7vnrei3o3r3m9blf6ZoDkvcw0VmozNRFJxg==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "license": "MIT" + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/ejs": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.10.tgz", + "integrity": "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==", + "license": "Apache-2.0", + "dependencies": { + "jake": "^10.8.5" + }, + "bin": { + "ejs": "bin/cli.js" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/entities": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", + "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/error-ex": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz", + "integrity": "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==", + "license": "MIT", + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-module-lexer": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.3.2.tgz", + "integrity": "sha512-poHGpORABojJJucnV9KbOavETW8lBVnphkW77ER5/BQ5Fz7oXSoCNek7IH3vR5nRjdsEz926ibFYX8KtLQmdyw==", + "dev": true, + "license": "MIT" + }, + "node_modules/es-object-atoms": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz", + "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/esbuild": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.2.tgz", + "integrity": "sha512-HKVLS8dvII+xoKW9kmqxbRKrnWEXfJJr/FZhhJmiqIB0e053QNYFqOBouTMO/k5sID4MvCiUCvv8b9M4h32wIA==", + "extraneous": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.28.2", + "@esbuild/android-arm": "0.28.2", + "@esbuild/android-arm64": "0.28.2", + "@esbuild/android-x64": "0.28.2", + "@esbuild/darwin-arm64": "0.28.2", + "@esbuild/darwin-x64": "0.28.2", + "@esbuild/freebsd-arm64": "0.28.2", + "@esbuild/freebsd-x64": "0.28.2", + "@esbuild/linux-arm": "0.28.2", + "@esbuild/linux-arm64": "0.28.2", + "@esbuild/linux-ia32": "0.28.2", + "@esbuild/linux-loong64": "0.28.2", + "@esbuild/linux-mips64el": "0.28.2", + "@esbuild/linux-ppc64": "0.28.2", + "@esbuild/linux-riscv64": "0.28.2", + "@esbuild/linux-s390x": "0.28.2", + "@esbuild/linux-x64": "0.28.2", + "@esbuild/netbsd-arm64": "0.28.2", + "@esbuild/netbsd-x64": "0.28.2", + "@esbuild/openbsd-arm64": "0.28.2", + "@esbuild/openbsd-x64": "0.28.2", + "@esbuild/openharmony-arm64": "0.28.2", + "@esbuild/sunos-x64": "0.28.2", + "@esbuild/win32-arm64": "0.28.2", + "@esbuild/win32-ia32": "0.28.2", + "@esbuild/win32-x64": "0.28.2" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "license": "BSD-2-Clause", + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "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/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/execa": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-9.6.1.tgz", + "integrity": "sha512-9Be3ZoN4LmYR90tUoVu2te2BsbzHfhJyfEiAVfz7N5/zv+jduIfLrV2xdQXOHbaD6KgpGdO9PRPM1Y4Q9QkPkA==", + "license": "MIT", + "dependencies": { + "@sindresorhus/merge-streams": "^4.0.0", + "cross-spawn": "^7.0.6", + "figures": "^6.1.0", + "get-stream": "^9.0.0", + "human-signals": "^8.0.1", + "is-plain-obj": "^4.1.0", + "is-stream": "^4.0.1", + "npm-run-path": "^6.0.0", + "pretty-ms": "^9.2.0", + "signal-exit": "^4.1.0", + "strip-final-newline": "^4.0.0", + "yoctocolors": "^2.1.1" + }, + "engines": { + "node": "^18.19.0 || >=20.5.0" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/expect-type": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.4.0.tgz", + "integrity": "sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/express": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/express/-/express-5.3.0.tgz", + "integrity": "sha512-qCqy1xSoaugxOD4IVHb1ZWdkvCF2W3aE2okKouWj8VwJ+ztskSiXoBnCSJqfKT+cVPJhuJqe9YLbY425+Zh00g==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "accepts": "^2.0.0", + "body-parser": "^2.3.0", + "content-disposition": "^2.0.1", + "content-type": "^2.0.0", + "cookie": "^0.7.1", + "cookie-signature": "^1.2.1", + "debug": "^4.4.0", + "depd": "^2.0.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "finalhandler": "^2.1.0", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "merge-descriptors": "^2.0.0", + "mime-types": "^3.0.0", + "on-finished": "^2.4.1", + "once": "^1.4.0", + "parseurl": "^1.3.3", + "proxy-addr": "^2.0.8", + "qs": "^6.16.0", + "range-parser": "^1.2.1", + "router": "^2.2.0", + "send": "^1.1.0", + "serve-static": "^2.2.0", + "statuses": "^2.0.1", + "type-is": "^2.1.0", + "vary": "^1.1.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/external-editor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-3.1.0.tgz", + "integrity": "sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==", + "license": "MIT", + "dependencies": { + "chardet": "^0.7.0", + "iconv-lite": "^0.4.24", + "tmp": "^0.0.33" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/external-editor/node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/fast-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-levenshtein": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-3.0.0.tgz", + "integrity": "sha512-hKKNajm46uNmTlhHSyZkmToAc56uZJwYq7yrciZjqOxnlfQwERDQJmHPUp7m1m9wx8vgOe8IaCKZ5Kv2k1DdCQ==", + "license": "MIT", + "dependencies": { + "fastest-levenshtein": "^1.0.7" + } + }, + "node_modules/fastest-levenshtein": { + "version": "1.0.16", + "resolved": "https://registry.npmjs.org/fastest-levenshtein/-/fastest-levenshtein-1.0.16.tgz", + "integrity": "sha512-eRnCtTTtGZFpQCwhJiUOuxPQWRXVKYDn0b2PeHfXL6/Zi53SLAzAHfVhVWK2AryC/WH05kGfxhFIPvTF0SXQzg==", + "license": "MIT", + "engines": { + "node": ">= 4.9.1" + } + }, + "node_modules/fastq": { + "version": "1.20.3", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.3.tgz", + "integrity": "sha512-XKv5nnLs6nLF71NgiKJLIZFLkPyIEuOselLG7ujZnGrRfQK8HpvY+WqKhAJUAdLomwVHErVS4LfxFlPq0/FTAw==", + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/fengari": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/fengari/-/fengari-0.1.5.tgz", + "integrity": "sha512-0DS4Nn4rV8qyFlQCpKK8brT61EUtswynrpfFTcgLErcilBIBskSMQ86fO2WVuybr14ywyKdRjv91FiRZwnEuvQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "readline-sync": "^1.4.10", + "sprintf-js": "^1.1.3", + "tmp": "^0.2.5" + } + }, + "node_modules/fengari-interop": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/fengari-interop/-/fengari-interop-0.1.4.tgz", + "integrity": "sha512-4/CW/3PJUo3ebD4ACgE1g/3NGEYSq7OQAyETyypsAl/WeySDBbxExikkayNkZzbpgyC9GyJp8v1DU2VOXxNq7Q==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "fengari": "^0.1.0" + } + }, + "node_modules/fengari/node_modules/tmp": { + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.7.tgz", + "integrity": "sha512-e0votIpp4Uo2AJYSzVHV6xCcawuiez3DzqDAbrTc3YxBkplN6e+dM13ZeIcZnDg/QpSuU2zfZ3rzwY8ukEnaXw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.14" + } + }, + "node_modules/fflate": { + "version": "0.4.9", + "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.4.9.tgz", + "integrity": "sha512-zdxgIEddhfsyCaWpJ2SdXEP8ZMrKJ6+5jl4OupODcywU0IhRk6gdXuVGcPICyfx2H97hVK7xmJtRLPjkxAX8Vw==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/figures": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/figures/-/figures-6.1.0.tgz", + "integrity": "sha512-d+l3qxjSesT4V7v2fh+QnmFnUWv9lSpjarhShNTgBOfA0ttejbQUAlHLitbjkoRiDulW0OPoQPYIGhIC8ohejg==", + "license": "MIT", + "dependencies": { + "is-unicode-supported": "^2.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/file-type": { + "version": "21.3.2", + "resolved": "https://registry.npmjs.org/file-type/-/file-type-21.3.2.tgz", + "integrity": "sha512-DLkUvGwep3poOV2wpzbHCOnSKGk1LzyXTv+aHFgN2VFl96wnp8YA9YjO2qPzg5PuL8q/SW9Pdi6WTkYOIh995w==", + "license": "MIT", + "dependencies": { + "@tokenizer/inflate": "^0.4.1", + "strtok3": "^10.3.4", + "token-types": "^6.1.1", + "uint8array-extras": "^1.4.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sindresorhus/file-type?sponsor=1" + } + }, + "node_modules/filelist": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.6.tgz", + "integrity": "sha512-5giy2PkLYY1cP39p17Ech+2xlpTRL9HLspOfEgm0L6CwBXBTgsK5ou0JtzYuepxkaQ/tvhCFIJ5uXo0OrM2DxA==", + "license": "Apache-2.0", + "dependencies": { + "minimatch": "^5.0.1" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/finalhandler": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz", + "integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "on-finished": "^2.4.1", + "parseurl": "^1.3.3", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/for-each": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", + "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==", + "license": "MIT", + "dependencies": { + "is-callable": "^1.2.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/form-data": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.6.tgz", + "integrity": "sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.4", + "mime-types": "^2.1.35" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/form-data/node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/form-data/node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", + "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/fs-extra": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", + "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", + "license": "MIT", + "dependencies": { + "at-least-node": "^1.0.0", + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "license": "ISC" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-package-type": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", + "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==", + "license": "MIT", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/get-stream": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-9.0.1.tgz", + "integrity": "sha512-kVCxPF3vQM/N0B1PmoqVUqgHP+EeVjmZSQn+1oCRPxd2P21P2F19lIgbR3HBosbB1PUhOAoctJnfEn2GbN2eZA==", + "license": "MIT", + "dependencies": { + "@sec-ant/readable-stream": "^0.4.1", + "is-stream": "^4.0.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "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", + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/glob/node_modules/brace-expansion": { + "version": "1.1.21", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.21.tgz", + "integrity": "sha512-9zeA+KLZNNzglF2TPKRQEDyx6Yby7daAkuy8MiPzpXPsYDWi/DRM8jmwUDxokQjYqBpv5DgPiwD4h4ZZSy1Ujw==", + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/glob/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/globalyzer": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/globalyzer/-/globalyzer-0.1.0.tgz", + "integrity": "sha512-40oNTM9UfG6aBmuKxk/giHn5nQ8RVz/SS4Ir6zgzOv9/qC3kKZ9v4etGTcJbEl/NyVQH7FGU7d+X1egr57Md2Q==", + "license": "MIT" + }, + "node_modules/globby": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", + "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", + "license": "MIT", + "dependencies": { + "array-union": "^2.1.0", + "dir-glob": "^3.0.1", + "fast-glob": "^3.2.9", + "ignore": "^5.2.0", + "merge2": "^1.4.1", + "slash": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/globby/node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/globrex": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/globrex/-/globrex-0.1.2.tgz", + "integrity": "sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg==", + "license": "MIT" + }, + "node_modules/google-protobuf": { + "version": "3.21.4", + "resolved": "https://registry.npmjs.org/google-protobuf/-/google-protobuf-3.21.4.tgz", + "integrity": "sha512-MnG7N936zcKTco4Jd2PX2U96Kf9PxygAPKBug+74LHzmHXmceN16MmRcdgZv+DGef/S9YvQAfRsNCn4cjf9yyQ==", + "license": "(BSD-3-Clause AND Apache-2.0)", + "peer": true + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "license": "ISC" + }, + "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==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/hono": { + "version": "4.13.13", + "resolved": "https://registry.npmjs.org/hono/-/hono-4.13.13.tgz", + "integrity": "sha512-CQ46U0ZkAGmbT/4UxdzzGJpacP2IeKgY4a5/tOI9AABbpOMfK739wfDXmv1usCk+3RkKj1hQy4/fjhiwa2xlrA==", + "license": "MIT", + "engines": { + "node": ">=16.9.0" + } + }, + "node_modules/html-encoding-sniffer": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-4.0.0.tgz", + "integrity": "sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==", + "license": "MIT", + "dependencies": { + "whatwg-encoding": "^3.1.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/html-to-image": { + "version": "1.11.13", + "resolved": "https://registry.npmjs.org/html-to-image/-/html-to-image-1.11.13.tgz", + "integrity": "sha512-cuOPoI7WApyhBElTTb9oqsawRvZ0rHhaHwghRLlTuffoD1B2aDemlCruLeZrUIIdvG7gs9xeELEPm6PhuASqrg==", + "license": "MIT" + }, + "node_modules/http-call": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/http-call/-/http-call-5.3.0.tgz", + "integrity": "sha512-ahwimsC23ICE4kPl9xTBjKB4inbRaeLyZeRunC/1Jy/Z6X8tv22MEAjK+KBOMSVLaqXPTTmd8638waVIKLGx2w==", + "license": "ISC", + "dependencies": { + "content-type": "^1.0.4", + "debug": "^4.1.1", + "is-retry-allowed": "^1.1.0", + "is-stream": "^2.0.0", + "parse-json": "^4.0.0", + "tunnel-agent": "^0.6.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/http-call/node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/http-call/node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/human-signals": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-8.0.1.tgz", + "integrity": "sha512-eKCa6bwnJhvxj14kZk5NCPc6Hb6BdsU9DZcOnmQKSnO1VKrfV0zCvtttPZUsBvjmNDn8rpcJfpwSYnHBjc95MQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/hyperlinker": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/hyperlinker/-/hyperlinker-1.0.0.tgz", + "integrity": "sha512-Ty8UblRWFEcfSuIaajM34LdPXIhbs1ajEX/BBPv24J+enSVaEVY63xQ6lTO9VRYS5LAoghIG0IDJ+p+IPzKUQQ==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/iconv-lite": { + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.3.tgz", + "integrity": "sha512-IKXpvIzjnC9XTAUbVBcMfGS0EPaIXtW6v+zr+RRp+hqULEpo0owZax6wyRwPOJbWbzjYspQwusTsfVr0ifh4uQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "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": "BSD-3-Clause" + }, + "node_modules/ignore": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", + "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/image-size": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/image-size/-/image-size-2.0.2.tgz", + "integrity": "sha512-IRqXKlaXwgSMAMtpNzZa1ZAe8m+Sa1770Dhk8VkSsP9LS+iHD62Zd8FQKs8fbPiagBE7BzoFX23cxFnwshpV6w==", + "license": "MIT", + "bin": { + "image-size": "bin/image-size.js" + }, + "engines": { + "node": ">=16.x" + } + }, + "node_modules/immediate": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", + "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==", + "license": "MIT" + }, + "node_modules/indent-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "license": "ISC", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "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/inquirer": { + "version": "9.1.4", + "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-9.1.4.tgz", + "integrity": "sha512-9hiJxE5gkK/cM2d1mTEnuurGTAoHebbkX0BYl3h7iEg7FYfuNIom+nDfBCSWtvSnoSrWCeBxqqBZu26xdlJlXA==", + "license": "MIT", + "dependencies": { + "ansi-escapes": "^6.0.0", + "chalk": "^5.1.2", + "cli-cursor": "^4.0.0", + "cli-width": "^4.0.0", + "external-editor": "^3.0.3", + "figures": "^5.0.0", + "lodash": "^4.17.21", + "mute-stream": "0.0.8", + "ora": "^6.1.2", + "run-async": "^2.4.0", + "rxjs": "^7.5.7", + "string-width": "^5.1.2", + "strip-ansi": "^7.0.1", + "through": "^2.3.6", + "wrap-ansi": "^8.0.1" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/inquirer/node_modules/ansi-escapes": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-6.2.1.tgz", + "integrity": "sha512-4nJ3yixlEthEJ9Rk4vPcdBRkZvQZlYyu8j4/Mqz5sgIkddmEnH2Yj2ZrnP9S3tQOvSNRUIgVNF/1yPpRAGNRig==", + "license": "MIT", + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/inquirer/node_modules/ansi-regex": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.4.0.tgz", + "integrity": "sha512-KzTVk2tCWAHtYrvvvaP8bJKJq2pVinhLcGEQdtLIYPbmNGNyYe8QwNaTUYQp2J7/vIsUKt5QCqAfUkYyG9DkOw==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/inquirer/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==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/inquirer/node_modules/chalk": { + "version": "5.6.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", + "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", + "license": "MIT", + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/inquirer/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==", + "license": "MIT" + }, + "node_modules/inquirer/node_modules/escape-string-regexp": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz", + "integrity": "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/inquirer/node_modules/figures": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/figures/-/figures-5.0.0.tgz", + "integrity": "sha512-ej8ksPF4x6e5wvK9yevct0UCXh8TTFlWGVLlgjZuoBH1HwjIfKE/IdL5mq89sFA7zELi1VhKpmtDnrs7zWyeyg==", + "license": "MIT", + "dependencies": { + "escape-string-regexp": "^5.0.0", + "is-unicode-supported": "^1.2.0" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/inquirer/node_modules/is-unicode-supported": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-1.3.0.tgz", + "integrity": "sha512-43r2mRvz+8JRIKnWJ+3j8JtjRKZ6GmjzfaE/qiBJnikNnYv/6bagRJ1kUhNk8R5EX/GkobD+r+sfxCPJsiKBLQ==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/inquirer/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==", + "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/inquirer/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==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.2.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/inquirer/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==", + "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/ioredis": { + "version": "5.7.0", + "resolved": "https://registry.npmjs.org/ioredis/-/ioredis-5.7.0.tgz", + "integrity": "sha512-NUcA93i1lukyXU+riqEyPtSEkyFq8tX90uL659J+qpCZ3rEdViB/APC58oAhIh3+bJln2hzdlZbBZsGNrlsR8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@ioredis/commands": "^1.3.0", + "cluster-key-slot": "^1.1.0", + "debug": "^4.3.4", + "denque": "^2.1.0", + "lodash.defaults": "^4.2.0", + "lodash.isarguments": "^3.1.0", + "redis-errors": "^1.2.0", + "redis-parser": "^3.0.0", + "standard-as-callback": "^2.1.0" + }, + "engines": { + "node": ">=12.22.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/ioredis" + } + }, + "node_modules/ioredis-mock": { + "version": "8.13.1", + "resolved": "https://registry.npmjs.org/ioredis-mock/-/ioredis-mock-8.13.1.tgz", + "integrity": "sha512-Wsi50AU+cMiI32nAgfwpUaJVBtb4iQdVsOHl9M6R3tePCO/8vGsToCVIG82XWAxN4Se55TZoOzVseu+QngFLyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@ioredis/as-callback": "^3.0.0", + "@ioredis/commands": "^1.4.0", + "fengari": "^0.1.4", + "fengari-interop": "^0.1.3", + "semver": "^7.7.2" + }, + "engines": { + "node": ">=12.22" + }, + "peerDependencies": { + "@types/ioredis-mock": "^8", + "ioredis": "^5" + } + }, + "node_modules/ioredis-mock/node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "license": "MIT" + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "license": "MIT", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-callable": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", + "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-docker": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz", + "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==", + "license": "MIT", + "bin": { + "is-docker": "cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-inside-container": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-inside-container/-/is-inside-container-1.0.0.tgz", + "integrity": "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==", + "license": "MIT", + "dependencies": { + "is-docker": "^3.0.0" + }, + "bin": { + "is-inside-container": "cli.js" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-inside-container/node_modules/is-docker": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-3.0.0.tgz", + "integrity": "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==", + "license": "MIT", + "bin": { + "is-docker": "cli.js" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-interactive": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-2.0.0.tgz", + "integrity": "sha512-qP1vozQRI+BMOPcjFzrjXuQvdak2pHNUMZoeG2eRbiSqyvbEf/wQtEOTOX1guk6E3t36RkaqiSt8A/6YElNxLQ==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-plain-obj": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz", + "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-potential-custom-element-name": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", + "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", + "license": "MIT" + }, + "node_modules/is-promise": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", + "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/is-retry-allowed": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/is-retry-allowed/-/is-retry-allowed-1.2.0.tgz", + "integrity": "sha512-RUbUeKwvm3XG2VYamhJL1xFktgjvPzL0Hq8C+6yrWIswDy3BIXGqCxhxkc30N9jqK311gVU137K8Ei55/zVJRg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-stream": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-4.0.1.tgz", + "integrity": "sha512-Dnz92NInDqYckGEUJv689RbRiTSEHCQ7wOVeALbkOz999YpqT46yMRIGtSNl2iCL1waAZSx40+h59NV/EwzV/A==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-typed-array": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz", + "integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==", + "license": "MIT", + "dependencies": { + "which-typed-array": "^1.1.16" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-unicode-supported": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-2.1.0.tgz", + "integrity": "sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-wsl": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", + "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", + "license": "MIT", + "dependencies": { + "is-docker": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "license": "MIT" + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "license": "ISC" + }, + "node_modules/isomorphic-git": { + "version": "1.33.1", + "resolved": "https://registry.npmjs.org/isomorphic-git/-/isomorphic-git-1.33.1.tgz", + "integrity": "sha512-Fy5rPAncURJoqL9R+5nJXLl5rQH6YpcjJd7kdCoRJPhrBiLVkLm9b+esRqYQQlT1hKVtKtALbfNtpHjWWJgk6g==", + "license": "MIT", + "dependencies": { + "async-lock": "^1.4.1", + "clean-git-ref": "^2.0.1", + "crc-32": "^1.2.0", + "diff3": "0.0.3", + "ignore": "^5.1.4", + "minimisted": "^2.0.0", + "pako": "^1.0.10", + "path-browserify": "^1.0.1", + "pify": "^4.0.1", + "readable-stream": "^3.4.0", + "sha.js": "^2.4.12", + "simple-get": "^4.0.1" + }, + "bin": { + "isogit": "cli.cjs" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/isomorphic-git/node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/jake": { + "version": "10.9.4", + "resolved": "https://registry.npmjs.org/jake/-/jake-10.9.4.tgz", + "integrity": "sha512-wpHYzhxiVQL+IV05BLE2Xn34zW1S223hvjtqk0+gsPrwd/8JNLXJgZZM/iPFsYc1xyphF+6M6EvdE5E9MBGkDA==", + "license": "Apache-2.0", + "dependencies": { + "async": "^3.2.6", + "filelist": "^1.0.4", + "picocolors": "^1.1.1" + }, + "bin": { + "jake": "bin/cli.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/js-yaml": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.1.tgz", + "integrity": "sha512-CY6crGq313MX8GkwvB7tzgp99vjQxY1++5y10/BKN/GUfHqWaOGQMNZkBvqSzsZKWk/ijwHlWzzkLulsGHhjWQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/nodeca" + } + ], + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsdom": { + "version": "24.1.0", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-24.1.0.tgz", + "integrity": "sha512-6gpM7pRXCwIOKxX47cgOyvyQDN/Eh0f1MeKySBV2xGdKtqJBLj8P25eY3EVCWo2mglDDzozR2r2MW4T+JiNUZA==", + "license": "MIT", + "dependencies": { + "cssstyle": "^4.0.1", + "data-urls": "^5.0.0", + "decimal.js": "^10.4.3", + "form-data": "^4.0.0", + "html-encoding-sniffer": "^4.0.0", + "http-proxy-agent": "^7.0.2", + "https-proxy-agent": "^7.0.4", + "is-potential-custom-element-name": "^1.0.1", + "nwsapi": "^2.2.10", + "parse5": "^7.1.2", + "rrweb-cssom": "^0.7.0", + "saxes": "^6.0.0", + "symbol-tree": "^3.2.4", + "tough-cookie": "^4.1.4", + "w3c-xmlserializer": "^5.0.0", + "webidl-conversions": "^7.0.0", + "whatwg-encoding": "^3.1.1", + "whatwg-mimetype": "^4.0.0", + "whatwg-url": "^14.0.0", + "ws": "^8.17.0", + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "canvas": "^2.11.2" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } + } + }, + "node_modules/json-parse-better-errors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz", + "integrity": "sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==", + "license": "MIT" + }, + "node_modules/jsonfile": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.1.tgz", + "integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==", + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/jsonschema": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/jsonschema/-/jsonschema-1.4.1.tgz", + "integrity": "sha512-S6cATIPVv1z0IlxdN+zUk5EPjkGCdnhN4wVSBlvoUO1tOLJootbo9CquNJmbIh4yikWHiUedhRYrNPn1arpEmQ==", + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/jszip": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz", + "integrity": "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==", + "license": "(MIT OR GPL-3.0-or-later)", + "dependencies": { + "lie": "~3.3.0", + "pako": "~1.0.2", + "readable-stream": "~2.3.6", + "setimmediate": "^1.0.5" + } + }, + "node_modules/jszip/node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "license": "MIT" + }, + "node_modules/jszip/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/jszip/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" + }, + "node_modules/jszip/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/jwt-decode": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jwt-decode/-/jwt-decode-4.0.0.tgz", + "integrity": "sha512-+KJGIyHgkGuIq3IEBNftfhW/LfWhXUIY6OmyVWjliu5KH1y0fw7VQ8YndE2O4qZdMSd9SqbnC8GOcZEy0Om7sA==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/lie": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz", + "integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==", + "license": "MIT", + "dependencies": { + "immediate": "~3.0.5" + } + }, + "node_modules/lightningcss": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.33.0.tgz", + "integrity": "sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==", + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.33.0", + "lightningcss-darwin-arm64": "1.33.0", + "lightningcss-darwin-x64": "1.33.0", + "lightningcss-freebsd-x64": "1.33.0", + "lightningcss-linux-arm-gnueabihf": "1.33.0", + "lightningcss-linux-arm64-gnu": "1.33.0", + "lightningcss-linux-arm64-musl": "1.33.0", + "lightningcss-linux-x64-gnu": "1.33.0", + "lightningcss-linux-x64-musl": "1.33.0", + "lightningcss-win32-arm64-msvc": "1.33.0", + "lightningcss-win32-x64-msvc": "1.33.0" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.33.0.tgz", + "integrity": "sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.33.0.tgz", + "integrity": "sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.33.0.tgz", + "integrity": "sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.33.0.tgz", + "integrity": "sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.33.0.tgz", + "integrity": "sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ==", + "cpu": [ + "arm" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.33.0.tgz", + "integrity": "sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.33.0.tgz", + "integrity": "sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.33.0.tgz", + "integrity": "sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.33.0.tgz", + "integrity": "sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.33.0.tgz", + "integrity": "sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.33.0.tgz", + "integrity": "sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lodash": { + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz", + "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==", + "license": "MIT" + }, + "node_modules/lodash.defaults": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz", + "integrity": "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.isarguments": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz", + "integrity": "sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg==", + "dev": true, + "license": "MIT" + }, + "node_modules/log-symbols": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-5.1.0.tgz", + "integrity": "sha512-l0x2DvrW294C9uDCoQe1VSU4gf529FkSZ6leBl4TiqZH/e+0R7hSfHQBNut2mNygDgHwvYHfFLn6Oxb3VWj2rA==", + "license": "MIT", + "dependencies": { + "chalk": "^5.0.0", + "is-unicode-supported": "^1.1.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-symbols/node_modules/chalk": { + "version": "5.6.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", + "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", + "license": "MIT", + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/log-symbols/node_modules/is-unicode-supported": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-1.3.0.tgz", + "integrity": "sha512-43r2mRvz+8JRIKnWJ+3j8JtjRKZ6GmjzfaE/qiBJnikNnYv/6bagRJ1kUhNk8R5EX/GkobD+r+sfxCPJsiKBLQ==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/long": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz", + "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==", + "license": "Apache-2.0" + }, + "node_modules/lower-case": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/lower-case/-/lower-case-2.0.2.tgz", + "integrity": "sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.3" + } + }, + "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==", + "license": "ISC" + }, + "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/moderndash": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/moderndash/-/moderndash-4.0.0.tgz", - "integrity": "sha512-77kEJCsBo3YzqIrO4ZWHo1h7q8f8ZIRxQ0DE0fEvj3rZWsfTMLeRzctBjkz2zKM1BAIQsOqB79jm3dZ4RDFmqw==", - "license": "MIT", - "dependencies": { - "hotscript": "1.0.13", - "type-fest": "4.27.0" - }, + "node_modules/make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "license": "ISC" + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.1.tgz", + "integrity": "sha512-yz3xRaG20c6/BOzvYoDaGtPmGscs7YivItZEEqe6GbwNfHuxu9YNmvnEkMzKldAGY4/80pRcQRZSEnhquk9XuQ==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/merge-descriptors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", + "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", + "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/mimic-response": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/minimatch": { + "version": "5.1.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.9.tgz", + "integrity": "sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw==", + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "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/minimisted": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/minimisted/-/minimisted-2.0.1.tgz", + "integrity": "sha512-1oPjfuLQa2caorJUM8HV8lGgWCc0qqAO1MNv/k05G4qslmsndV/5WdNZrqCiyqiz3wohia2Ij2B7w2Dr7/IyrA==", + "license": "MIT", + "dependencies": { + "minimist": "^1.2.5" + } + }, + "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/mustache": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/mustache/-/mustache-4.2.0.tgz", + "integrity": "sha512-71ippSywq5Yb7/tVYyGbkBggbU8H3u5Rz56fH60jGFgr8uHwxs+aSKeqmluIVzM0m0kB7xQjKS6qPfd0b2ZoqQ==", + "license": "MIT", + "bin": { + "mustache": "bin/mustache" + } + }, + "node_modules/mute-stream": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.8.tgz", + "integrity": "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==", + "license": "ISC" + }, + "node_modules/nanoid": { + "version": "3.3.20", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.20.tgz", + "integrity": "sha512-uKdg2G3GNCKQn9byYOpxbGqrT2fGO5KRt5J/8b3pok8rT6qxGWF6hxMyJiEYtAf+FVyYuD9hRaDqX5uPFYJ4ZQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/natural-orderby": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/natural-orderby/-/natural-orderby-2.0.3.tgz", + "integrity": "sha512-p7KTHxU0CUrcOXe62Zfrb5Z13nLvPhSWR/so3kFulUQU0sgUll2Z0LwpsLN351eOOD+hRGu/F1g+6xDfPeD++Q==", + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/negotiator": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.1.0.tgz", + "integrity": "sha512-NMPBRMJgiQHjbd8phG3Vebdx4kZ1H121rbl5IkMqeOsahptB9BKo/d7oJ3zTXqTgagn2bWlNSXkh0QUGM31RYg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "content-type": "^2.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/no-case": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/no-case/-/no-case-3.0.4.tgz", + "integrity": "sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg==", + "license": "MIT", + "dependencies": { + "lower-case": "^2.0.2", + "tslib": "^2.0.3" + } + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/npm-run-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-6.0.0.tgz", + "integrity": "sha512-9qny7Z9DsQU8Ou39ERsPU4OZQlSTP47ShQzuKZ6PRXpYLtIFgl/DEBYEXKlvcEa+9tHVcK8CF81Y2V72qaZhWA==", + "license": "MIT", + "dependencies": { + "path-key": "^4.0.0", + "unicorn-magic": "^0.3.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/npm-run-path/node_modules/path-key": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz", + "integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/nwsapi": { + "version": "2.2.28", + "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.28.tgz", + "integrity": "sha512-IlVB7OS7qrOsVYlpnFIkETjMwT9jwvmocJmmM+GZU/PAB3uGi9Ezd7vcWhWBUnSc0ya4ppmQITOyP1ez9gg8cg==", + "license": "MIT" + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object-treeify": { + "version": "1.1.33", + "resolved": "https://registry.npmjs.org/object-treeify/-/object-treeify-1.1.33.tgz", + "integrity": "sha512-EFVjAYfzWqWsBMRHPMAXLCDIJnpMhdWAqR7xG6M6a2cs6PMFpl/+Z20w9zDW4vkxOFfddegBKq9Rehd0bxWE7A==", + "license": "MIT", + "engines": { + "node": ">= 10" + } + }, + "node_modules/obug": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/obug/-/obug-2.2.1.tgz", + "integrity": "sha512-XrsrhT5sybtKI6wakr2SPOlGZWWYbUXZ7a0jT8/QOeAPau+1X/bSegNe5YR75oJmEZQbKningirmGOEJCIk61Q==", + "dev": true, + "funding": [ + "https://github.com/sponsors/sxzz", + "https://opencollective.com/debug" + ], + "license": "MIT", + "engines": { + "node": ">=12.20.0" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "license": "MIT", + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/open": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/open/-/open-10.1.0.tgz", + "integrity": "sha512-mnkeQ1qP5Ue2wd+aivTD3NHd/lZ96Lu0jgf0pwktLPtx6cTZiH7tyeGRRHs0zX0rbrahXPnXlUnbeXyaBBuIaw==", + "license": "MIT", + "dependencies": { + "default-browser": "^5.2.1", + "define-lazy-prop": "^3.0.0", + "is-inside-container": "^1.0.0", + "is-wsl": "^3.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/open/node_modules/is-wsl": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-3.1.1.tgz", + "integrity": "sha512-e6rvdUCiQCAuumZslxRJWR/Doq4VpPR82kqclvcS0efgt430SlGIk05vdCN58+VrzgtIcfNODjozVielycD4Sw==", + "license": "MIT", + "dependencies": { + "is-inside-container": "^1.0.0" + }, + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ora": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/ora/-/ora-6.3.1.tgz", + "integrity": "sha512-ERAyNnZOfqM+Ao3RAvIXkYh5joP220yf59gVe2X/cI6SiCxIdi4c9HZKZD8R6q/RDXEje1THBju6iExiSsgJaQ==", + "license": "MIT", + "dependencies": { + "chalk": "^5.0.0", + "cli-cursor": "^4.0.0", + "cli-spinners": "^2.6.1", + "is-interactive": "^2.0.0", + "is-unicode-supported": "^1.1.0", + "log-symbols": "^5.1.0", + "stdin-discarder": "^0.1.0", + "strip-ansi": "^7.0.1", + "wcwidth": "^1.0.1" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ora/node_modules/ansi-regex": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.4.0.tgz", + "integrity": "sha512-KzTVk2tCWAHtYrvvvaP8bJKJq2pVinhLcGEQdtLIYPbmNGNyYe8QwNaTUYQp2J7/vIsUKt5QCqAfUkYyG9DkOw==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/ora/node_modules/chalk": { + "version": "5.6.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", + "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", + "license": "MIT", + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/ora/node_modules/is-unicode-supported": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-1.3.0.tgz", + "integrity": "sha512-43r2mRvz+8JRIKnWJ+3j8JtjRKZ6GmjzfaE/qiBJnikNnYv/6bagRJ1kUhNk8R5EX/GkobD+r+sfxCPJsiKBLQ==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ora/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==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.2.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/os-tmpdir": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", + "integrity": "sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/pako": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", + "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==", + "license": "(MIT AND Zlib)" + }, + "node_modules/parse-json": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-4.0.0.tgz", + "integrity": "sha512-aOIos8bujGN93/8Ox/jPLh7RwVnPEysynVFE+fQZyg6jKELEHwzgKdLRFHUgXJL6kylijVSBC4BvN9OmsB48Rw==", + "license": "MIT", + "dependencies": { + "error-ex": "^1.3.1", + "json-parse-better-errors": "^1.0.1" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/parse-ms": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/parse-ms/-/parse-ms-4.0.0.tgz", + "integrity": "sha512-TXfryirbmq34y8QBwgqCVLi+8oA3oWx2eAnSn62ITyEhEYaWRlVZ2DvMM9eZbMs/RfxPu/PK/aBLyGj4IrqMHw==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parse5": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz", + "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==", + "license": "MIT", + "dependencies": { + "entities": "^6.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/pascal-case": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/pascal-case/-/pascal-case-3.1.2.tgz", + "integrity": "sha512-uWlGT3YSnK9x3BQJaOdcZwrnV6hPpd8jFH1/ucpiLRPh/2zCVJKS19E4GvYHvaCcACn3foXZ0cLB9Wrx1KGe5g==", + "license": "MIT", + "dependencies": { + "no-case": "^3.0.4", + "tslib": "^2.0.3" + } + }, + "node_modules/password-prompt": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/password-prompt/-/password-prompt-1.1.3.tgz", + "integrity": "sha512-HkrjG2aJlvF0t2BMH0e2LB/EHf3Lcq3fNMzy4GYHcQblAvOl+QQji1Lx7WRBMqpVK8p+KR7bCg7oqAMXtdgqyw==", + "license": "0BSD", + "dependencies": { + "ansi-escapes": "^4.3.2", + "cross-spawn": "^7.0.3" + } + }, + "node_modules/path-browserify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz", + "integrity": "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==", + "license": "MIT" + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "license": "MIT", + "engines": { + "node": ">=0.10.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==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-to-regexp": { + "version": "8.4.2", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.4.2.tgz", + "integrity": "sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA==", + "dev": true, + "license": "MIT", + "peer": true, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pify": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", + "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/possible-typed-array-names": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", + "integrity": "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/postcss": { + "version": "8.5.30", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.30.tgz", + "integrity": "sha512-zkd+e9S1AQHhmaBKye9zlFHWjOwVgqXl1q3ECYy8561K7qxR2cREf/STLskulBFd78k6AOFeA+ff1TtEzk9bAA==", + "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.20", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.2" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/posthog-js": { + "version": "1.438.12", + "resolved": "https://registry.npmjs.org/posthog-js/-/posthog-js-1.438.12.tgz", + "integrity": "sha512-hjhvrtqbk6pFlbtNEGk0FrWKKFdeXe/WWA/LppN0f+uDoHKR9/SZJOxUHSVCGYbGTJvNRoDjjQvZuTcq4oKSlg==", + "dev": true, + "license": "(Apache-2.0 AND MIT)", + "peer": true, + "dependencies": { + "@posthog/browser-common": "^0.9.5", + "@posthog/core": "^1.59.1", + "@posthog/types": "^1.416.0", + "core-js": "^3.49.0", + "dompurify": "^3.4.13", + "fflate": "^0.4.8", + "preact": "^10.29.3", + "query-selector-shadow-dom": "^1.0.1", + "web-vitals": "^6.2.1", + "web-vitals-soft-navs": "npm:web-vitals@6.2.1" + }, + "peerDependencies": { + "@types/react": ">=16.8.0", + "react": ">=16.8.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "react": { + "optional": true + } + } + }, + "node_modules/posthog-js/node_modules/web-vitals": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/web-vitals/-/web-vitals-6.2.3.tgz", + "integrity": "sha512-Dmy+vdO1UPMqXiAtZSNayhl4f794Bxfd94W4HIj2HlYtczEQfEOon969memlcpcHrTuOKLa5baFjX7WbgmOq+A==", + "dev": true, + "license": "Apache-2.0", + "peer": true + }, + "node_modules/preact": { + "version": "10.29.8", + "resolved": "https://registry.npmjs.org/preact/-/preact-10.29.8.tgz", + "integrity": "sha512-ej2aVZ+vZ8WO7tvlQWRM9N63A0KzF9q4mWJfDUHgYaIofWY9hu74QdnQrjoPMmZi2/nZ5gN0bJCQF49xQqx09Q==", + "dev": true, + "license": "MIT", + "peer": true, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/preact" + }, + "peerDependencies": { + "preact-render-to-string": ">=5" + }, + "peerDependenciesMeta": { + "preact-render-to-string": { + "optional": true + } + } + }, + "node_modules/prettier": { + "version": "2.8.8", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.8.tgz", + "integrity": "sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==", + "license": "MIT", + "bin": { + "prettier": "bin-prettier.js" + }, + "engines": { + "node": ">=10.13.0" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/pretty-ms": { + "version": "9.3.1", + "resolved": "https://registry.npmjs.org/pretty-ms/-/pretty-ms-9.3.1.tgz", + "integrity": "sha512-HzMy3Geq23nVALD/M2LliU+F+M+gVNsvkQWWqeBZ8HDiCgzo6YPJ/Omrmtq24EFrIsk0a3EkQGEd7bDOo+IhGA==", + "license": "MIT", + "dependencies": { + "parse-ms": "^4.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "license": "MIT" + }, + "node_modules/protobufjs": { + "version": "7.6.5", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.6.5.tgz", + "integrity": "sha512-/FPD0nUc9jH6rfFjji9IBqOz4pcSE3CsT1m7Ep6Mdb0LxSUMj8hgl6GomOvZzpNpAqqGaXA0P3VSrZLFzIhQrw==", + "hasInstallScript": true, + "license": "BSD-3-Clause", + "dependencies": { + "@protobufjs/aspromise": "^1.1.2", + "@protobufjs/base64": "^1.1.2", + "@protobufjs/codegen": "^2.0.5", + "@protobufjs/eventemitter": "^1.1.1", + "@protobufjs/fetch": "^1.1.1", + "@protobufjs/float": "^1.0.2", + "@protobufjs/path": "^1.1.2", + "@protobufjs/pool": "^1.1.0", + "@protobufjs/utf8": "^1.1.1", + "@types/node": ">=13.7.0", + "long": "^5.3.2" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/proxy-addr": { + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.8.tgz", + "integrity": "sha512-5nnx0yGyVUcY6t9RnWcARWtwT9F1D8O9rt08htPvnd49W1IgZtmLkhu9WfMzQj1cFxjHIO6connUNVW5k7AVyQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/psl": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/psl/-/psl-1.15.0.tgz", + "integrity": "sha512-JZd3gMVBAVQkSs6HdNZo9Sdo0LNcQeMNP3CozBJb3JYC/QUYZTnKxP+f8oWRX4rHP5EurWxqAHTSwUCjlNKa1w==", + "license": "MIT", + "dependencies": { + "punycode": "^2.3.1" + }, + "funding": { + "url": "https://github.com/sponsors/lupomontero" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/qs": { + "version": "6.16.1", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.16.1.tgz", + "integrity": "sha512-5Px6J7rfHBAGUhNQ3es4YzcOdBBw0SDcVtThJml4RdnKw4tKs2epA0kPZ+K8X+WiHktT93RHVcQ/3ym5aTCgtg==", + "dev": true, + "license": "BSD-3-Clause", + "peer": true, + "dependencies": { + "es-define-property": "^1.0.1", + "side-channel": "^1.1.1" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/query-selector-shadow-dom": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/query-selector-shadow-dom/-/query-selector-shadow-dom-1.0.1.tgz", + "integrity": "sha512-lT5yCqEBgfoMYpf3F2xQRK7zEr1rhIIZuceDK6+xRkJQ4NMbHTwXqk4NkwDwQMNqXgG9r9fyHnzwNVs6zV5KRw==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/querystringify": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz", + "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==", + "license": "MIT" + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "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/range-parser": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.3.0.tgz", + "integrity": "sha512-hek2mFQpPuI4E1BBKrSto+BU3e3x4xuarsbiwr3+lf7p44juvFMV0XFWQAP3xUyqXA4RrXLIoaSUGbSt056ZMw==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/raw-body": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz", + "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.7.0", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/readline-sync": { + "version": "1.4.10", + "resolved": "https://registry.npmjs.org/readline-sync/-/readline-sync-1.4.10.tgz", + "integrity": "sha512-gNva8/6UAe8QYepIQH/jQ2qn91Qj0B9sYjMBBs3QOB8F2CXcKgLxQaJRP76sWVRQt+QU+8fAkCbCvjjMFu7Ycw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/redeyed": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/redeyed/-/redeyed-2.1.1.tgz", + "integrity": "sha512-FNpGGo1DycYAdnrKFxCMmKYgo/mILAqtRYbkdQD8Ep/Hk2PQ5+aEAEx+IU713RTDmuBaH0c8P5ZozurNu5ObRQ==", + "license": "MIT", + "dependencies": { + "esprima": "~4.0.0" + } + }, + "node_modules/redis-errors": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/redis-errors/-/redis-errors-1.2.0.tgz", + "integrity": "sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/redis-parser": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/redis-parser/-/redis-parser-3.0.0.tgz", + "integrity": "sha512-DJnGAeenTdpMEH6uAJRK/uiyEIH9WVsUmoLwzudwGJUwZPp80PDBWPHXSAGNPwNvIXAbe7MSUB1zQFugFml66A==", + "dev": true, + "license": "MIT", + "dependencies": { + "redis-errors": "^1.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/requires-port": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", + "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==", + "license": "MIT" + }, + "node_modules/restore-cursor": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-4.0.0.tgz", + "integrity": "sha512-I9fPXU9geO9bHOt9pHHOhOkYerIMsmVaWB0rA2AI9ERh/+x/i7MV5HKBNrg+ljO5eoPVgCcnFuRjJ9uH6I/3eg==", + "license": "MIT", + "dependencies": { + "onetime": "^5.1.0", + "signal-exit": "^3.0.2" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/restore-cursor/node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "license": "ISC" + }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rolldown": { + "version": "1.2.14", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.2.14.tgz", + "integrity": "sha512-eTBTTqMR6uHKLgcUqpJP8ntzDI6V4LYA8negAMBEI7gkgVvOdrx6VsvS2Ym+83KhOPdLCHWJu0w7UQFJZhHKNw==", + "license": "MIT", + "dependencies": { + "@oxc-project/types": "=0.154.0", + "@rolldown/pluginutils": "^1.0.0" + }, + "bin": { + "rolldown": "bin/cli.mjs" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "optionalDependencies": { + "@rolldown/binding-android-arm-eabi": "1.2.14", + "@rolldown/binding-android-arm64": "1.2.14", + "@rolldown/binding-darwin-arm64": "1.2.14", + "@rolldown/binding-darwin-x64": "1.2.14", + "@rolldown/binding-freebsd-x64": "1.2.14", + "@rolldown/binding-linux-arm-gnueabihf": "1.2.14", + "@rolldown/binding-linux-arm64-gnu": "1.2.14", + "@rolldown/binding-linux-arm64-musl": "1.2.14", + "@rolldown/binding-linux-ppc64-gnu": "1.2.14", + "@rolldown/binding-linux-s390x-gnu": "1.2.14", + "@rolldown/binding-linux-x64-gnu": "1.2.14", + "@rolldown/binding-linux-x64-musl": "1.2.14", + "@rolldown/binding-openharmony-arm64": "1.2.14", + "@rolldown/binding-win32-arm64-msvc": "1.2.14", + "@rolldown/binding-win32-x64-msvc": "1.2.14" + } + }, + "node_modules/router": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", + "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "debug": "^4.4.0", + "depd": "^2.0.0", + "is-promise": "^4.0.0", + "parseurl": "^1.3.3", + "path-to-regexp": "^8.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/rrweb-cssom": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.7.1.tgz", + "integrity": "sha512-TrEMa7JGdVm0UThDJSx7ddw5nVm3UJS9o9CCIZ72B1vSyEZoziDqBYP3XIoi/12lKrJR8rE3jeFHMok2F/Mnsg==", + "license": "MIT" + }, + "node_modules/run-applescript": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/run-applescript/-/run-applescript-7.1.0.tgz", + "integrity": "sha512-DPe5pVFaAsinSaV6QjQ6gdiedWDcRCbUuiQfQa2wmWV7+xC9bGulGI8+TdRmoFkAPaBXk8CrAbnlY2ISniJ47Q==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/run-async": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/run-async/-/run-async-2.4.1.tgz", + "integrity": "sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ==", + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "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", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/rxjs": { + "version": "7.8.1", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz", + "integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "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/saxes": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", + "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==", + "license": "ISC", + "dependencies": { + "xmlchars": "^2.2.0" + }, + "engines": { + "node": ">=v12.22.7" + } + }, + "node_modules/semver": { + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/send": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz", + "integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "debug": "^4.4.3", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "fresh": "^2.0.0", + "http-errors": "^2.0.1", + "mime-types": "^3.0.2", + "ms": "^2.1.3", + "on-finished": "^2.4.1", + "range-parser": "^1.2.1", + "statuses": "^2.0.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/serve-static": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.1.tgz", + "integrity": "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "parseurl": "^1.3.3", + "send": "^1.2.0" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/set-function-length": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/setimmediate": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", + "integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==", + "license": "MIT" + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "dev": true, + "license": "ISC", + "peer": true + }, + "node_modules/sha.js": { + "version": "2.4.12", + "resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.12.tgz", + "integrity": "sha512-8LzC5+bvI45BjpfXU8V5fdU2mfeKiQe1D1gIMn7XUlF3OTUrpdJpPPH4EMAnF0DsHHdSZqCdSss5qCmJKuiO3w==", + "license": "(MIT AND BSD-3-Clause)", + "dependencies": { + "inherits": "^2.0.4", + "safe-buffer": "^5.2.1", + "to-buffer": "^1.2.0" + }, + "bin": { + "sha.js": "bin.js" + }, + "engines": { + "node": ">= 0.10" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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==", + "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==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/side-channel": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.1.tgz", + "integrity": "sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4", + "side-channel-list": "^1.0.1", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz", + "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/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==", + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/simple-concat": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", + "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", + "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/simple-get": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz", + "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", + "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", + "dependencies": { + "decompress-response": "^6.0.0", + "once": "^1.3.1", + "simple-concat": "^1.0.0" + } + }, + "node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/slice-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-4.0.0.tgz", + "integrity": "sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "astral-regex": "^2.0.0", + "is-fullwidth-code-point": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/slice-ansi?sponsor=1" + } + }, + "node_modules/source-map-js": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.2.tgz", + "integrity": "sha512-KGj/8Y43x35aZVDtt+J4mK1hoLGHULMYfSkODJNQjNDC3oW1PqPoxMwo0pLUsWM/UEGzON/NxeHywEfNXNP3Vw==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/sprintf-js": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz", + "integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==", + "dev": true, + "license": "BSD-3-Clause" + }, + "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/standard-as-callback": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/standard-as-callback/-/standard-as-callback-2.1.0.tgz", + "integrity": "sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A==", + "dev": true, + "license": "MIT" + }, + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/std-env": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.3.0.tgz", + "integrity": "sha512-OtU/EgQ1kIm5KwqQpBC6ZEMXrZRui11w8zgfTWp8cdO9B8OaPsbA8bTHO2P+HNo1VlUTGMVBwPhydu6poeXiag==", + "dev": true, + "license": "MIT" + }, + "node_modules/stdin-discarder": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/stdin-discarder/-/stdin-discarder-0.1.0.tgz", + "integrity": "sha512-xhV7w8S+bUwlPTb4bAOUQhv8/cSS5offJuX8GQGq32ONF0ZtDWKfkdomM3HMRA+LhX6um/FZ0COqlwsjD53LeQ==", + "license": "MIT", + "dependencies": { + "bl": "^5.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/string-length": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/string-length/-/string-length-5.0.1.tgz", + "integrity": "sha512-9Ep08KAMUn0OadnVaBuRdE2l615CQ508kr0XMadjClfYpdCyvrbFp6Taebo8yyxokQ4viUd/xPPUA4FGgUa0ow==", + "license": "MIT", + "dependencies": { + "char-regex": "^2.0.0", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12.20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/string-length/node_modules/ansi-regex": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.4.0.tgz", + "integrity": "sha512-KzTVk2tCWAHtYrvvvaP8bJKJq2pVinhLcGEQdtLIYPbmNGNyYe8QwNaTUYQp2J7/vIsUKt5QCqAfUkYyG9DkOw==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/string-length/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==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.2.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-final-newline": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-4.0.0.tgz", + "integrity": "sha512-aulFJcD6YK8V1G7iRB5tigAP4TsHBZZrOV8pjV++zdUwmeV8uzbY7yn6h9MswN62adStNZFuCIx4haBnRuMDaw==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/strtok3": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/strtok3/-/strtok3-10.5.0.tgz", + "integrity": "sha512-PgAsgIuizbZN/r4+g4UNvbVCTpJWdoIdI71kIWwKmdIH0vJ0q7ylMVGU8PWj2s9O98y4MtFWiDTIrtNukZMMgQ==", + "license": "MIT", + "dependencies": { + "@tokenizer/token": "^0.3.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, + "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==", + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-hyperlinks": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/supports-hyperlinks/-/supports-hyperlinks-2.3.0.tgz", + "integrity": "sha512-RpsAZlpWcDwOPQA22aCH4J0t7L8JmAvsCxfOSEwm7cQs3LshN36QaTkwd70DnBOXDWGssw2eUoc8CaRWT0XunA==", + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0", + "supports-color": "^7.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/symbol-tree": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", + "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", + "license": "MIT" + }, + "node_modules/through": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", + "integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==", + "license": "MIT" + }, + "node_modules/tiny-glob": { + "version": "0.2.9", + "resolved": "https://registry.npmjs.org/tiny-glob/-/tiny-glob-0.2.9.tgz", + "integrity": "sha512-g/55ssRPUjShh+xkfx9UPDXqhckHEsHr4Vd9zX55oSdGZc/MD0m3sferOkwWtp98bv+kcVfEHtRJgBVJzelrzg==", + "license": "MIT", + "dependencies": { + "globalyzer": "0.1.0", + "globrex": "^0.1.2" + } + }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.3.1.tgz", + "integrity": "sha512-GCvB3aoys96IuDFBMcTB46JOR6mdMtAToqwiW8JlWhsoh1mhHi/xn9ss/Dg7N555GiJyEt2qzoG/NHCwM6h1EA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinyglobby/node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/tinyglobby/node_modules/picomatch": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.7.tgz", + "integrity": "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/tinyrainbow": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.2.0.tgz", + "integrity": "sha512-LgO3D9yZJjApUiuUfl9iFAwrtaX4+lok3wJIqttGoKCHlWUqHqbQpnxCf82L8FjgKsh4iGo78hqJwgL8F6To2A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tmp": { + "version": "0.0.33", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", + "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==", + "license": "MIT", + "dependencies": { + "os-tmpdir": "~1.0.2" + }, + "engines": { + "node": ">=0.6.0" + } + }, + "node_modules/to-buffer": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/to-buffer/-/to-buffer-1.2.2.tgz", + "integrity": "sha512-db0E3UJjcFhpDhAF4tLo03oli3pwl3dbnzXOUIlRKrp+ldk/VUxzpWYZENsw2SZiuBjHAk7DfB0VU7NKdpb6sw==", + "license": "MIT", + "dependencies": { + "isarray": "^2.0.5", + "safe-buffer": "^5.2.1", + "typed-array-buffer": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=0.6" + } + }, + "node_modules/token-types": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/token-types/-/token-types-6.1.2.tgz", + "integrity": "sha512-dRXchy+C0IgK8WPC6xvCHFRIWYUbqqdEIKPaKo/AcTUNzwLTK6AH7RjdLWsEZcAN/TBdtfUw3PYEgPr5VPr6ww==", + "license": "MIT", + "dependencies": { + "@borewit/text-codec": "^0.2.1", + "@tokenizer/token": "^0.3.0", + "ieee754": "^1.2.1" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, + "node_modules/tough-cookie": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.4.tgz", + "integrity": "sha512-Loo5UUvLD9ScZ6jh8beX1T6sO1w2/MpCRpEP7V280GKMVUQ0Jzar2U3UJPsrdbziLEMMhu3Ujnq//rhiFuIeag==", + "license": "BSD-3-Clause", + "dependencies": { + "psl": "^1.1.33", + "punycode": "^2.1.1", + "universalify": "^0.2.0", + "url-parse": "^1.5.3" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/tough-cookie/node_modules/universalify": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.2.0.tgz", + "integrity": "sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==", + "license": "MIT", + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/tr46": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-5.1.1.tgz", + "integrity": "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==", + "license": "MIT", + "dependencies": { + "punycode": "^2.3.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/ts-node": { + "version": "10.9.2", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", + "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", + "license": "MIT", + "dependencies": { + "@cspotcode/source-map-support": "^0.8.0", + "@tsconfig/node10": "^1.0.7", + "@tsconfig/node12": "^1.0.7", + "@tsconfig/node14": "^1.0.0", + "@tsconfig/node16": "^1.0.2", + "acorn": "^8.4.1", + "acorn-walk": "^8.1.1", + "arg": "^4.1.0", + "create-require": "^1.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "v8-compile-cache-lib": "^3.0.1", + "yn": "3.1.1" + }, + "bin": { + "ts-node": "dist/bin.js", + "ts-node-cwd": "dist/bin-cwd.js", + "ts-node-esm": "dist/bin-esm.js", + "ts-node-script": "dist/bin-script.js", + "ts-node-transpile-only": "dist/bin-transpile.js", + "ts-script": "dist/bin-script-deprecated.js" + }, + "peerDependencies": { + "@swc/core": ">=1.2.50", + "@swc/wasm": ">=1.2.50", + "@types/node": "*", + "typescript": ">=2.7" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "@swc/wasm": { + "optional": true + } + } + }, + "node_modules/ts-poet": { + "version": "4.15.0", + "resolved": "https://registry.npmjs.org/ts-poet/-/ts-poet-4.15.0.tgz", + "integrity": "sha512-sLLR8yQBvHzi9d4R1F4pd+AzQxBfzOSSjfxiJxQhkUoH5bL7RsAC6wgvtVUQdGqiCsyS9rT6/8X2FI7ipdir5g==", + "license": "Apache-2.0", + "dependencies": { + "lodash": "^4.17.15", + "prettier": "^2.5.1" + } + }, + "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/tsv": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/tsv/-/tsv-0.2.0.tgz", + "integrity": "sha512-GG6xbOP85giXXom0dS6z9uyDsxktznjpa1AuDlPrIXDqDnbhjr9Vk6Us8iz6U1nENL4CPS2jZDvIjEdaZsmc4Q==", + "license": "MIT (ricardo.mit-license.org)" + }, + "node_modules/tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + }, + "engines": { + "node": "*" + } + }, + "node_modules/twirp-ts": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/twirp-ts/-/twirp-ts-2.5.0.tgz", + "integrity": "sha512-JTKIK5Pf/+3qCrmYDFlqcPPUx+ohEWKBaZy8GL8TmvV2VvC0SXVyNYILO39+GCRbqnuP6hBIF+BVr8ZxRz+6fw==", + "license": "MIT", + "dependencies": { + "@protobuf-ts/plugin-framework": "^2.0.7", + "camel-case": "^4.1.2", + "dot-object": "^2.1.4", + "path-to-regexp": "^6.2.0", + "ts-poet": "^4.5.0", + "yaml": "^1.10.2" + }, + "bin": { + "protoc-gen-twirp_ts": "protoc-gen-twirp_ts" + }, + "peerDependencies": { + "@protobuf-ts/plugin": "^2.5.0", + "ts-proto": "^1.81.3" + }, + "peerDependenciesMeta": { + "@protobuf-ts/plugin": { + "optional": true + }, + "ts-proto": { + "optional": true + } + } + }, + "node_modules/twirp-ts/node_modules/path-to-regexp": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.3.0.tgz", + "integrity": "sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==", + "license": "MIT" + }, + "node_modules/twirp-ts/node_modules/yaml": { + "version": "1.10.3", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.3.tgz", + "integrity": "sha512-vIYeF1u3CjlhAFekPPAk2h/Kv4T3mAkMox5OymRiJQB0spDP10LHvt+K7G9Ny6NuuMAb25/6n1qyUjAcGNf/AA==", + "license": "ISC", "engines": { - "node": ">=20", - "npm": ">=10" + "node": ">= 6" } }, - "node_modules/moderndash/node_modules/type-fest": { - "version": "4.27.0", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.27.0.tgz", - "integrity": "sha512-3IMSWgP7C5KSQqmo1wjhKrwsvXAtF33jO3QY+Uy++ia7hqvgSK6iXbbg5PbDBc1P2ZbNEDgejOrN4YooXvhwCw==", + "node_modules/type-fest": { + "version": "0.21.3", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", + "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", "license": "(MIT OR CC0-1.0)", "engines": { - "node": ">=16" + "node": ">=10" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/nanoid": { - "version": "3.3.19", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.19.tgz", - "integrity": "sha512-Y2tUNy4ouw6tq5oDSKeQYGOyhkUBhNOcGV/02KC+6kd9eDGqdZd++mjMiIDilrBYvjEnCYvVtsuHCuP+okSfug==", + "node_modules/type-is": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.1.0.tgz", + "integrity": "sha512-faYHw0anBbc/kWF3zFTEnxSFOAGUX9GFbOBthvDdLsIlEoWOFOtS0zgCiQYwIskL9iGXZL3kAXD8OoZ4GmMATA==", "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], "license": "MIT", - "bin": { - "nanoid": "bin/nanoid.cjs" + "peer": true, + "dependencies": { + "content-type": "^2.0.0", + "media-typer": "^1.1.0", + "mime-types": "^3.0.0" }, "engines": { - "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" - } - }, - "node_modules/obug": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/obug/-/obug-2.2.1.tgz", - "integrity": "sha512-XrsrhT5sybtKI6wakr2SPOlGZWWYbUXZ7a0jT8/QOeAPau+1X/bSegNe5YR75oJmEZQbKningirmGOEJCIk61Q==", - "dev": true, - "funding": [ - "https://github.com/sponsors/sxzz", - "https://opencollective.com/debug" - ], - "license": "MIT", - "engines": { - "node": ">=12.20.0" + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, - "node_modules/pathe": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", - "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", - "dev": true, - "license": "MIT" - }, - "node_modules/picocolors": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", - "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", - "dev": true, - "license": "ISC" - }, - "node_modules/postcss": { - "version": "8.5.28", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.28.tgz", - "integrity": "sha512-RRuzqDtt5Y9h3quz5hWhK+TPnsmVs6WwSU6LkJMeY4HstUEDuYTG8UJSdawMRzmzAtV+KEoG8N3Qg2qLy5vM/A==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/postcss" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], + "node_modules/typed-array-buffer": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz", + "integrity": "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==", "license": "MIT", "dependencies": { - "nanoid": "^3.3.18", - "picocolors": "^1.1.1", - "source-map-js": "^1.2.1" + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-typed-array": "^1.1.14" }, "engines": { - "node": "^10 || ^12 || >=14" + "node": ">= 0.4" } }, - "node_modules/protobufjs": { - "version": "7.6.6", - "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.6.6.tgz", - "integrity": "sha512-dYDWdjSl5RNb7SgPxGQcRU+GtvP7s2fpkrY0r432PcOIaZ0/rBcxEZnQN67iJhFuQiVw754JDoPruPCNdGsbjg==", - "hasInstallScript": true, - "license": "BSD-3-Clause", - "dependencies": { - "@protobufjs/aspromise": "^1.1.2", - "@protobufjs/base64": "^1.1.2", - "@protobufjs/codegen": "^2.0.5", - "@protobufjs/eventemitter": "^1.1.1", - "@protobufjs/fetch": "^1.1.1", - "@protobufjs/float": "^1.0.2", - "@protobufjs/path": "^1.1.2", - "@protobufjs/pool": "^1.1.0", - "@protobufjs/utf8": "^1.1.1", - "@types/node": ">=13.7.0", - "long": "^5.3.2" + "node_modules/typescript": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz", + "integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==", + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" }, "engines": { - "node": ">=12.0.0" + "node": ">=14.17" } }, - "node_modules/rolldown": { - "version": "1.2.9", - "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.2.9.tgz", - "integrity": "sha512-hx/Pv0N1haXRb11qkfnK5MXB/iqr7i0yjWQqmO9uHqZpBgQSqzc8UsSnEpalsh+j1I8qQ2CkXAkJC8Br3dKSlg==", - "dev": true, + "node_modules/uint8array-extras": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/uint8array-extras/-/uint8array-extras-1.6.0.tgz", + "integrity": "sha512-8iAasVS4wUx0gPLjH8Xtz2PeDzTSiy8QiLGu2DT3X5GU+egFEQhpnbtkehbAwjvDcxIERmCYcysiODFmE3Ud0Q==", "license": "MIT", - "dependencies": { - "@oxc-project/types": "=0.150.0", - "@rolldown/pluginutils": "^1.0.0" - }, - "bin": { - "rolldown": "bin/cli.mjs" - }, "engines": { - "node": "^20.19.0 || >=22.12.0" + "node": ">=18" }, - "optionalDependencies": { - "@rolldown/binding-android-arm-eabi": "1.2.9", - "@rolldown/binding-android-arm64": "1.2.9", - "@rolldown/binding-darwin-arm64": "1.2.9", - "@rolldown/binding-darwin-x64": "1.2.9", - "@rolldown/binding-freebsd-x64": "1.2.9", - "@rolldown/binding-linux-arm-gnueabihf": "1.2.9", - "@rolldown/binding-linux-arm64-gnu": "1.2.9", - "@rolldown/binding-linux-arm64-musl": "1.2.9", - "@rolldown/binding-linux-ppc64-gnu": "1.2.9", - "@rolldown/binding-linux-s390x-gnu": "1.2.9", - "@rolldown/binding-linux-x64-gnu": "1.2.9", - "@rolldown/binding-linux-x64-musl": "1.2.9", - "@rolldown/binding-openharmony-arm64": "1.2.9", - "@rolldown/binding-win32-arm64-msvc": "1.2.9", - "@rolldown/binding-win32-x64-msvc": "1.2.9" + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/rxjs": { - "version": "7.8.1", - "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz", - "integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==", - "license": "Apache-2.0", - "dependencies": { - "tslib": "^2.1.0" - } + "node_modules/undici-types": { + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.24.6.tgz", + "integrity": "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg==", + "license": "MIT" }, - "node_modules/shallow-clone": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/shallow-clone/-/shallow-clone-3.0.1.tgz", - "integrity": "sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA==", + "node_modules/unicorn-magic": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/unicorn-magic/-/unicorn-magic-0.3.0.tgz", + "integrity": "sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA==", "license": "MIT", - "dependencies": { - "kind-of": "^6.0.2" - }, "engines": { - "node": ">=8" + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "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/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } }, - "node_modules/source-map-js": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", - "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", "dev": true, - "license": "BSD-3-Clause", + "license": "MIT", + "peer": true, "engines": { - "node": ">=0.10.0" + "node": ">= 0.8" } }, - "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/url-parse": { + "version": "1.5.10", + "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz", + "integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==", + "license": "MIT", + "dependencies": { + "querystringify": "^2.1.1", + "requires-port": "^1.0.0" + } }, - "node_modules/std-env": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.2.0.tgz", - "integrity": "sha512-oCUKSupKTHX53EyjDtuZQ64pjLJ6yYCtpmEw0goYxtjG9KpbRe8KAsl2tBUGU9DyMcJ0RwJ8GqJAFzMXcXW1Rw==", - "dev": true, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", "license": "MIT" }, - "node_modules/tinybench": { - "version": "2.9.0", - "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", - "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", - "dev": true, + "node_modules/uuid": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-14.0.0.tgz", + "integrity": "sha512-Qo+uWgilfSmAhXCMav1uYFynlQO7fMFiMVZsQqZRMIXp0O7rR7qjkj+cPvBHLgBqi960QCoo/PH2/6ZtVqKvrg==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist-node/bin/uuid" + } + }, + "node_modules/v8-compile-cache-lib": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", + "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", "license": "MIT" }, - "node_modules/tinyexec": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.3.1.tgz", - "integrity": "sha512-GCvB3aoys96IuDFBMcTB46JOR6mdMtAToqwiW8JlWhsoh1mhHi/xn9ss/Dg7N555GiJyEt2qzoG/NHCwM6h1EA==", + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", "dev": true, "license": "MIT", + "peer": true, "engines": { - "node": ">=18" + "node": ">= 0.8" } }, - "node_modules/tinyglobby": { - "version": "0.2.17", - "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", - "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", - "dev": true, + "node_modules/vite": { + "version": "8.3.3", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.3.3.tgz", + "integrity": "sha512-cTAldKPImjg6c+gk48U19POPn3GCBzZwpdsN8ZMEEcbpes+6/wvfqUd0C2y3qYY4wsj8PwgFwrZ/1jBPVttMSg==", "license": "MIT", "dependencies": { - "fdir": "^6.5.0", - "picomatch": "^4.0.4" + "lightningcss": "^1.33.0", + "picomatch": "^4.0.7", + "postcss": "^8.5.28", + "rolldown": "~1.2.11", + "tinyglobby": "^0.2.17" + }, + "bin": { + "vite": "bin/vite.js" }, "engines": { - "node": ">=12.0.0" + "node": "^20.19.0 || >=22.12.0" }, "funding": { - "url": "https://github.com/sponsors/SuperchupuDev" - } - }, - "node_modules/tinyglobby/node_modules/fdir": { - "version": "6.5.0", - "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", - "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12.0.0" + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" }, "peerDependencies": { - "picomatch": "^3 || ^4" + "@types/node": "^20.19.0 || >=22.12.0", + "@vitejs/devtools": "^0.7.1", + "esbuild": "^0.27.0 || ^0.28.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" }, "peerDependenciesMeta": { - "picomatch": { + "@types/node": { + "optional": true + }, + "@vitejs/devtools": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { "optional": true } } }, - "node_modules/tinyglobby/node_modules/picomatch": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", - "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", - "dev": true, + "node_modules/vite/node_modules/picomatch": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.7.tgz", + "integrity": "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==", "license": "MIT", "engines": { "node": ">=12" @@ -1295,55 +7657,6 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, - "node_modules/tinyrainbow": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.1.1.tgz", - "integrity": "sha512-yau8yJdTt989Mm0Bd/236QnzEiPf2xLLTqUZRUJOo/3CB078LSwzei343DgtJVmfJKJE3TMINY1u42SQsP6mXw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/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/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/undici-types": { - "version": "8.3.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-8.3.0.tgz", - "integrity": "sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ==", - "license": "MIT" - }, - "node_modules/uuid": { - "version": "14.0.0", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-14.0.0.tgz", - "integrity": "sha512-Qo+uWgilfSmAhXCMav1uYFynlQO7fMFiMVZsQqZRMIXp0O7rR7qjkj+cPvBHLgBqi960QCoo/PH2/6ZtVqKvrg==", - "funding": [ - "https://github.com/sponsors/broofa", - "https://github.com/sponsors/ctavan" - ], - "license": "MIT", - "bin": { - "uuid": "dist-node/bin/uuid" - } - }, "node_modules/vitest": { "version": "4.1.11", "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.11.tgz", @@ -1434,40 +7747,6 @@ } } }, - "node_modules/vitest/node_modules/@vitest/mocker": { - "version": "4.1.11", - "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.11.tgz", - "integrity": "sha512-2XJVD55d1o5AZous5CCGKS74g/riOj9odEt2bQpCVZeblHyHdnMeFl4jl0XjU21stf4mbjUkew2eXQZt65g5CQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@vitest/spy": "4.1.11", - "estree-walker": "^3.0.3", - "magic-string": "^0.30.21" - }, - "funding": { - "url": "https://opencollective.com/vitest" - }, - "peerDependencies": { - "msw": "^2.4.9", - "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" - }, - "peerDependenciesMeta": { - "msw": { - "optional": true - }, - "vite": { - "optional": true - } - } - }, - "node_modules/vitest/node_modules/es-module-lexer": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.3.2.tgz", - "integrity": "sha512-poHGpORABojJJucnV9KbOavETW8lBVnphkW77ER5/BQ5Fz7oXSoCNek7IH3vR5nRjdsEz926ibFYX8KtLQmdyw==", - "dev": true, - "license": "MIT" - }, "node_modules/vitest/node_modules/picomatch": { "version": "4.0.7", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.7.tgz", @@ -1481,82 +7760,132 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, - "node_modules/vitest/node_modules/vite": { - "version": "8.3.0", - "resolved": "https://registry.npmjs.org/vite/-/vite-8.3.0.tgz", - "integrity": "sha512-lhZBVvEHefgE+HQZC9O7EBJgCU/nVzFNl7vkS4RE0APtWLP02/8QVIkQtzBxPquh7lq5/78NHipTj7ODQ6XuyQ==", + "node_modules/w3c-xmlserializer": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz", + "integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==", + "license": "MIT", + "dependencies": { + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/wcwidth": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz", + "integrity": "sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==", + "license": "MIT", + "dependencies": { + "defaults": "^1.0.3" + } + }, + "node_modules/web-vitals": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/web-vitals/-/web-vitals-4.2.4.tgz", + "integrity": "sha512-r4DIlprAGwJ7YM11VZp4R884m0Vmgr6EAKe3P+kO0PPj3Unqyvv59rczf6UiGcb9Z8QxZVcqKNwv/g0WNdWwsw==", + "license": "Apache-2.0" + }, + "node_modules/web-vitals-soft-navs": { + "name": "web-vitals", + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/web-vitals/-/web-vitals-6.2.1.tgz", + "integrity": "sha512-rLcLXA2sx6+9dE88NHFubwTtGxpK4yYBLj6qHPdFoCaLr0cXGb4efOqtKLlm4loGA4OEKHIQKMKzZkKyOh5ctw==", "dev": true, + "license": "Apache-2.0", + "peer": true + }, + "node_modules/webidl-conversions": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", + "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/whatwg-encoding": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz", + "integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==", + "deprecated": "Use @exodus/bytes instead for a more spec-conformant and faster implementation", "license": "MIT", "dependencies": { - "lightningcss": "^1.33.0", - "picomatch": "^4.0.7", - "postcss": "^8.5.28", - "rolldown": "~1.2.6", - "tinyglobby": "^0.2.17" + "iconv-lite": "0.6.3" }, - "bin": { - "vite": "bin/vite.js" + "engines": { + "node": ">=18" + } + }, + "node_modules/whatwg-encoding/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": "^20.19.0 || >=22.12.0" + "node": ">=0.10.0" + } + }, + "node_modules/whatwg-mimetype": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz", + "integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/whatwg-url": { + "version": "14.2.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.2.0.tgz", + "integrity": "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==", + "license": "MIT", + "dependencies": { + "tr46": "^5.1.0", + "webidl-conversions": "^7.0.0" }, - "funding": { - "url": "https://github.com/vitejs/vite?sponsor=1" + "engines": { + "node": ">=18" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" }, - "optionalDependencies": { - "fsevents": "~2.3.3" + "bin": { + "node-which": "bin/node-which" }, - "peerDependencies": { - "@types/node": "^20.19.0 || >=22.12.0", - "@vitejs/devtools": "^0.7.1", - "esbuild": "^0.27.0 || ^0.28.0", - "jiti": ">=1.21.0", - "less": "^4.0.0", - "sass": "^1.70.0", - "sass-embedded": "^1.70.0", - "stylus": ">=0.54.8", - "sugarss": "^5.0.0", - "terser": "^5.16.0", - "tsx": "^4.8.1", - "yaml": "^2.4.2" + "engines": { + "node": ">= 8" + } + }, + "node_modules/which-typed-array": { + "version": "1.1.24", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.24.tgz", + "integrity": "sha512-wk4Mf4pR5mRP7eYuuTBCIQ9d0ud2Fv2jRLQpfgnRjbOxAFHmjKFValgTpitVKzJJS8ajnYQV2Du1SZ8j6b/EUQ==", + "license": "MIT", + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.9", + "call-bound": "^1.0.4", + "for-each": "^0.3.5", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2" }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - }, - "@vitejs/devtools": { - "optional": true - }, - "esbuild": { - "optional": true - }, - "jiti": { - "optional": true - }, - "less": { - "optional": true - }, - "sass": { - "optional": true - }, - "sass-embedded": { - "optional": true - }, - "stylus": { - "optional": true - }, - "sugarss": { - "optional": true - }, - "terser": { - "optional": true - }, - "tsx": { - "optional": true - }, - "yaml": { - "optional": true - } + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, "node_modules/why-is-node-running": { @@ -1575,6 +7904,120 @@ "engines": { "node": ">=8" } + }, + "node_modules/widest-line": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/widest-line/-/widest-line-3.1.0.tgz", + "integrity": "sha512-NsmoXalsWVDMGupxZ5R08ka9flZjjiLvHVAWYOKtiKM8ujtZWr9cRffak+uSE48+Ob8ObalXpwyeUiyDD6QFgg==", + "license": "MIT", + "dependencies": { + "string-width": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wordwrap": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", + "integrity": "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==", + "license": "MIT" + }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" + }, + "node_modules/ws": { + "version": "8.21.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.0.tgz", + "integrity": "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/xml-name-validator": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz", + "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==", + "license": "Apache-2.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/xmlchars": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", + "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", + "license": "MIT" + }, + "node_modules/yaml": { + "version": "2.9.1", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.9.1.tgz", + "integrity": "sha512-3NxN8+78OdzbT7C/WjGsyfPAtJaN3FNDsWxv7Y7mcDsT/oOmgW8BpyQQFFBnvZE3j9Y2Sdz1ULFLezL7Eb2yFw==", + "extraneous": true, + "license": "ISC", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + }, + "funding": { + "url": "https://github.com/sponsors/eemeli" + } + }, + "node_modules/yn": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/yoctocolors": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/yoctocolors/-/yoctocolors-2.2.0.tgz", + "integrity": "sha512-xYqdZFUK/VYazNl/oCDYN+3WloWQwMfZxBoiNt6qNyk+xfOdi598muWE42rNZFp1kNOiqW936q5RhUdnpqElSg==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } } } } diff --git a/devvit/package.json b/devvit/package.json index 90dce8d..56139e9 100644 --- a/devvit/package.json +++ b/devvit/package.json @@ -1,29 +1,33 @@ { "private": true, "name": "redditmodlog-devvit", - "version": "0.0.1", - "license": "BSD-3-Clause", + "version": "0.1.0", + "license": "GPL-3.0", "type": "module", - "description": "Publishes a subreddit's moderation log to a wiki page (Reddit Devvit app).", - "main": "src/main.ts", + "description": "Publishes a subreddit's moderation log to a wiki page (Reddit Devvit Web app).", "scripts": { - "deploy": "devvit upload", + "build": "vite build", "dev": "devvit playtest", - "playtest": "devvit playtest", + "deploy": "npm run type-check && npm test && devvit upload", "login": "devvit login", - "launch": "devvit publish", "type-check": "tsc --build", "test": "vitest run" }, + "engines": { + "node": ">=24.0.0" + }, "dependencies": { - "@devvit/public-api": "0.13.5" + "@devvit/start": "0.14.8", + "@devvit/web": "0.14.8", + "@hono/node-server": "2.1.4", + "devvit": "0.14.8", + "hono": "4.13.13" }, "devDependencies": { - "devvit": "1.0.0", - "typescript": "5.9.3", + "@devvit/test": "0.14.8", + "@types/node": "24.19.2", + "typescript": "6.0.3", + "vite": "8.3.3", "vitest": "4.1.11" - }, - "overrides": { - "protobufjs": "^7.6.6" } } diff --git a/devvit/src/core/constants.ts b/devvit/src/core/constants.ts new file mode 100644 index 0000000..2d2879a --- /dev/null +++ b/devvit/src/core/constants.ts @@ -0,0 +1,81 @@ +/** + * Constants ported from modlog_wiki_publisher.py (action sets, limits, wiki cap). + * No platform imports, so the pure render layer can be unit-tested anywhere. + */ + +export const REMOVAL_ACTIONS: readonly string[] = ['removelink', 'removecomment', 'spamlink', 'spamcomment']; +export const APPROVAL_ACTIONS: readonly string[] = ['approvelink', 'approvecomment']; +export const REASON_ACTIONS: readonly string[] = ['addremovalreason']; +export const DEFAULT_WIKI_ACTIONS: readonly string[] = [...REMOVAL_ACTIONS, ...REASON_ACTIONS, ...APPROVAL_ACTIONS]; + +/** Python VALID_MODLOG_ACTIONS; the settings multiSelect offers exactly these. */ +export const VALID_MODLOG_ACTIONS: readonly string[] = [ + 'removelink', + 'removecomment', + 'spamlink', + 'spamcomment', + 'approvelink', + 'approvecomment', + 'addremovalreason', + 'addnote', + 'deletenote', + 'banuser', + 'unbanuser', + 'muteuser', + 'unmuteuser', + 'invitemoderator', + 'acceptmoderatorinvite', + 'distinguish', + 'undistinguish', + 'sticky', + 'unsticky', + 'lock', + 'unlock', + 'marknsfw', + 'unmarknsfw', + 'wikirevise', + 'wikipagelisted', + 'wikipermlevel', + 'wikibanned', + 'wikicontributor', + 'editsettings', + 'editflair', + 'createrule', + 'editrule', + 'deleterule', + 'reorderrules', + 'ignorereports', + 'unignorereports', + 'request_assistance', + 'community_widgets', + 'community_welcome_page', + 'edit_post_requirements', + 'edit_comment_requirements', + 'edit_saved_response', + 'addtocollection', + 'removefromcollection', +]; + +export const ANON_LABEL = 'HumanModerator'; + +export const DAYS_MIN = 1; +export const DAYS_MAX = 365; +export const DEFAULT_RETENTION_DAYS = 90; +export const DEFAULT_DISPLAY_DAYS = 90; +export const DEFAULT_WIKI_PAGE = 'modlog-devvit'; + +/** Python max_wiki_entries_per_page default. */ +export const MAX_WIKI_ENTRIES = 1000; + +/** Mod-log entries fetched per run (Python batch_size max). Paging stops at the first already-stored id. */ +export const FETCH_LIMIT = 500; +export const FETCH_PAGE_SIZE = 100; + +/** Reddit wiki page size limit in UTF-8 bytes (512 KiB). */ +export const REDDIT_WIKI_LIMIT = 524288; +/** Day blocks are dropped once content would pass 90% of the limit. */ +export const TRIM_THRESHOLD = Math.floor(REDDIT_WIKI_LIMIT * 0.9); +/** Write guard: refuse to grow a page already above 95% of the limit. */ +export const WRITE_WARNING_THRESHOLD = Math.floor(REDDIT_WIKI_LIMIT * 0.95); + +export const WIKI_EDIT_REASON = 'Automated modlog update'; diff --git a/devvit/src/core/publish.ts b/devvit/src/core/publish.ts new file mode 100644 index 0000000..3ecb25b --- /dev/null +++ b/devvit/src/core/publish.ts @@ -0,0 +1,121 @@ +/** + * One publish cycle, shared by the cron task and the menu actions. Mirrors the + * Python continuous loop: ingest new mod-log entries, rebuild the page from + * stored rows, skip the write when the content hash is unchanged, then prune. + */ +import { FETCH_LIMIT, REDDIT_WIKI_LIMIT, WRITE_WARNING_THRESHOLD } from './constants.js'; +import { isIgnoredModerator, toStoredAction } from './record.js'; +import { buildWikiContent, contentHash } from './render.js'; +import type { Store } from './store.js'; +import type { AppConfig, ModLogEntry } from './types.js'; + +/** The Reddit calls a cycle needs. Production wiring lives in src/server/reddit.ts. */ +export interface RedditPort { + fetchModLog(subredditName: string, limit: number): Promise; + /** Current page markdown, or null when the page does not exist. */ + readWiki(subredditName: string, page: string): Promise; + writeWiki(subredditName: string, page: string, content: string, exists: boolean): Promise; +} + +export type CycleMode = 'scheduled' | 'publish-now' | 'force-rebuild'; + +export interface CycleResult { + status: 'published' | 'unchanged' | 'no-actions' | 'busy'; + ingested: number; + scanned: number; + rows: number; + bytes: number; + pruned: number; + trimmedDays: number; +} + +const DAY_SEC = 86_400; +const utf8Bytes = (s: string): number => Buffer.byteLength(s, 'utf8'); + +/** Store new entries oldest first so an interrupted run never leaves a gap behind a stored id. */ +export async function ingest(reddit: RedditPort, store: Store, cfg: AppConfig, nowSec: number): Promise<{ ingested: number; scanned: number }> { + const entries = await reddit.fetchModLog(cfg.subredditName, FETCH_LIMIT); + const cutoff = nowSec - cfg.retentionDays * DAY_SEC; + let ingested = 0; + for (const entry of [...entries].reverse()) { + if (isIgnoredModerator(entry.moderatorName, cfg.ignoredModerators)) continue; + const row = toStoredAction(entry, cfg.anonymizeModerators); + if (row.createdAt < cutoff) continue; + if (await store.saveAction(row)) ingested += 1; + } + return { ingested, scanned: entries.length }; +} + +export async function runCycle(reddit: RedditPort, store: Store, cfg: AppConfig, mode: CycleMode, now: Date = new Date()): Promise { + const nowSec = Math.floor(now.getTime() / 1000); + const result: CycleResult = { status: 'no-actions', ingested: 0, scanned: 0, rows: 0, bytes: 0, pruned: 0, trimmedDays: 0 }; + + if (!(await store.acquireLock(now.getTime()))) return { ...result, status: 'busy' }; + try { + await store.setStatus({ lastRunAt: now.toISOString(), lastMode: mode }); + + // Force rebuild matches Python --force-wiki: rebuild from storage without fetching. + if (mode !== 'force-rebuild') Object.assign(result, await ingest(reddit, store, cfg, nowSec)); + + const rows = await store.loadDisplayRows(cfg.wikiActions, nowSec - cfg.displayDays * DAY_SEC); + if (rows.length === 0) { + // Python skips the wiki write when storage holds no displayable actions. + result.pruned = await store.prune(nowSec - cfg.retentionDays * DAY_SEC); + await store.setStatus({ lastResult: 'no-actions' }); + return result; + } + + const priors = await store.priorRemovalsFor(rows); + const rendered = buildWikiContent(rows, { + subredditName: cfg.subredditName, + anonymizeModerators: cfg.anonymizeModerators, + now, + priorRemoval: (id) => priors.get(id), + }); + result.rows = rendered.rows; + result.trimmedDays = rendered.trimmedDays; + result.bytes = utf8Bytes(rendered.content); + + const hash = contentHash(rendered.content); + const force = mode === 'force-rebuild'; + if (!force && (await store.getWikiHash(cfg.wikiPage)) === hash) { + result.status = 'unchanged'; + } else { + const existing = await reddit.readWiki(cfg.subredditName, cfg.wikiPage); + if (existing !== null) { + const existingBytes = utf8Bytes(existing); + if (existingBytes > WRITE_WARNING_THRESHOLD && result.bytes >= existingBytes) { + throw new Error( + `Wiki page already at ${existingBytes} bytes of ${REDDIT_WIKI_LIMIT}; refusing to grow it. Lower the display days.`, + ); + } + } + await reddit.writeWiki(cfg.subredditName, cfg.wikiPage, rendered.content, existing !== null); + await store.setWikiHash(cfg.wikiPage, hash); + result.status = 'published'; + await store.setStatus({ lastPublishAt: now.toISOString() }); + } + + result.pruned = await store.prune(nowSec - cfg.retentionDays * DAY_SEC); + await store.setStatus({ lastResult: result.status, lastError: '' }); + return result; + } catch (err) { + await store.setStatus({ lastResult: 'error', lastError: err instanceof Error ? err.message : String(err) }); + throw err; + } finally { + await store.releaseLock(); + } +} + +export function describeResult(r: CycleResult, page: string): string { + switch (r.status) { + case 'busy': + return 'Another publish is already running. Try again in a minute.'; + case 'no-actions': + return `No moderation actions to publish yet (${r.ingested} new, ${r.scanned} scanned).`; + case 'unchanged': + return `wiki/${page} is already current (${r.ingested} new actions, ${r.rows} rows).`; + case 'published': + return `Published wiki/${page}: ${r.rows} rows, ${r.bytes} bytes, ${r.ingested} new actions.`; + } +} diff --git a/devvit/src/core/record.ts b/devvit/src/core/record.ts new file mode 100644 index 0000000..c273b01 --- /dev/null +++ b/devvit/src/core/record.ts @@ -0,0 +1,66 @@ +/** + * ModLogEntry -> StoredAction, ported from store_processed_action, + * get_moderator_name and get_target_permalink in modlog_wiki_publisher.py. + */ +import { ANON_LABEL } from './constants.js'; +import type { ModLogEntry, StoredAction } from './types.js'; + +const EMAIL_RE = /\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b/g; + +export function censorEmails(text: string): string { + return text.replace(EMAIL_RE, '[EMAIL]'); +} + +/** sanitize_for_markdown: pipes become spaces so a reason cannot break the table. */ +export function sanitizeForMarkdown(text: string | null | undefined): string { + if (text === null || text === undefined) return ''; + return String(text).replaceAll('|', ' '); +} + +/** get_moderator_name(action, anonymize). AutoModerator and Reddit always stay literal. */ +export function moderatorLabel(name: string | null | undefined, anonymize: boolean): string | null { + if (!name) return null; + const lower = name.toLowerCase(); + if (lower === 'automoderator') return 'AutoModerator'; + if (lower === 'reddit') return 'Reddit'; + return anonymize ? ANON_LABEL : name; +} + +/** Only post/comment links are kept; a /u/ profile link is never stored. */ +export function storedPermalink(permalink: string | undefined): string | null { + if (!permalink || !permalink.includes('/comments/') || permalink.includes('/u/')) return null; + return permalink.startsWith('http') ? permalink : `https://reddit.com${permalink}`; +} + +/** + * Python reads addremovalreason text from `description`, otherwise `mod_note`, then `details`. + * Devvit's ModAction has no mod_note field, so non-reason actions use `details`. + */ +export function extractReason(entry: ModLogEntry): string { + let reason: string | null = null; + if (entry.type === 'addremovalreason' && entry.description) { + reason = censorEmails(entry.description.trim()); + } else if (entry.details) { + reason = censorEmails(entry.details.trim()); + } + return sanitizeForMarkdown(reason); +} + +/** The moderator is stored already anonymized; the raw name never reaches Redis. */ +export function toStoredAction(entry: ModLogEntry, anonymize: boolean): StoredAction { + return { + id: entry.id, + action: entry.type, + moderator: moderatorLabel(entry.moderatorName, anonymize), + permalink: storedPermalink(entry.target?.permalink), + reason: extractReason(entry), + targetAuthor: entry.target?.author ? entry.target.author : null, + createdAt: Math.floor(entry.createdAt.getTime() / 1000), + }; +} + +export function isIgnoredModerator(name: string | undefined, ignored: readonly string[]): boolean { + if (!name) return false; + const lower = name.toLowerCase(); + return ignored.some((m) => m.toLowerCase() === lower); +} diff --git a/devvit/src/core/render.ts b/devvit/src/core/render.ts new file mode 100644 index 0000000..8f3a518 --- /dev/null +++ b/devvit/src/core/render.ts @@ -0,0 +1,291 @@ +/** + * Pure wiki renderer ported from build_wiki_content and its helpers in + * modlog_wiki_publisher.py. Output is byte-identical to Python for the same + * stored rows; test/parity.test.ts holds the golden comparison. + */ +import { createHash } from 'node:crypto'; + +import { + APPROVAL_ACTIONS, + MAX_WIKI_ENTRIES, + REASON_ACTIONS, + REDDIT_WIKI_LIMIT, + REMOVAL_ACTIONS, + TRIM_THRESHOLD, +} from './constants.js'; +import { moderatorLabel, sanitizeForMarkdown } from './record.js'; +import type { PriorRemoval, StoredAction } from './types.js'; + +export const FOOTER_LINES: readonly string[] = [ + '---', + '', + '*This modlog is automatically maintained by [RedditModLog](https://github.com/bakerboy448/RedditModLog) bot.*', +]; + +const REMOVAL_TYPE_LABELS: Readonly> = { + removelink: 'Post', + removepost: 'Post', + removecomment: 'Comment', + spamlink: 'Spam Post', + spamcomment: 'Spam Comment', + removecontent: 'Content', + addremovalreason: 'Removal Reason', +}; + +const MAX_TITLE_LENGTH = 50; + +export interface RenderOptions { + subredditName: string; + anonymizeModerators: boolean; + now: Date; + /** Latest Reddit/AutoModerator removal of a bare post/comment id, if any. */ + priorRemoval: (contentId: string) => PriorRemoval | undefined; +} + +export interface RenderResult { + content: string; + rows: number; + trimmedDays: number; +} + +/** A stored row plus the per-render annotations Python sets on its mock objects. */ +interface RenderRow { + row: StoredAction; + combinedReason?: string; + approvalContext?: string; +} + +const pad2 = (n: number): string => String(n).padStart(2, '0'); + +export function formatUtcDate(d: Date): string { + return `${d.getUTCFullYear()}-${pad2(d.getUTCMonth() + 1)}-${pad2(d.getUTCDate())}`; +} + +export function formatUtcTime(d: Date): string { + return `${pad2(d.getUTCHours())}:${pad2(d.getUTCMinutes())}:${pad2(d.getUTCSeconds())}`; +} + +const rowDate = (row: StoredAction): Date => new Date(row.createdAt * 1000); + +/** extract_content_id_from_permalink: "t1_" for comments, "t3_" for posts. */ +export function extractContentId(permalink: string | null | undefined): string | null { + if (!permalink) return null; + const comment = /\/comments\/[a-zA-Z0-9]+\/[^/]*\/([a-zA-Z0-9]+)\/?/.exec(permalink); + if (comment) return `t1_${comment[1]}`; + const post = /\/comments\/([a-zA-Z0-9]+)\//.exec(permalink); + if (post) return `t3_${post[1]}`; + return null; +} + +/** Bare id without the t1_/t3_ prefix. */ +export function bareContentId(permalink: string | null | undefined): string | null { + const id = extractContentId(permalink); + return id ? id.replace('t3_', '').replace('t1_', '') : null; +} + +/** The DB mock stores the permalink relative to https://reddit.com. */ +function relativePermalink(row: StoredAction): string | null { + const p = row.permalink; + if (!p) return null; + return p.startsWith('https://reddit.com') ? p.replaceAll('https://reddit.com', '') : p; +} + +function shortContentId(row: StoredAction): string { + const rel = relativePermalink(row); + if (!rel) return '-'; + const id = extractContentId(rel); + return id ? id.replace('t3_', '').replace('t1_', '').slice(0, 8) : '-'; +} + +/** format_content_link. Links only ever point at the post/comment, never a profile. */ +export function formatContentLink(row: StoredAction): string { + const rel = relativePermalink(row); + let link = ''; + if (rel) link = `https://www.reddit.com${rel}`; + else if (row.permalink) link = row.permalink; + + const isComment = Boolean(rel && rel.includes('/comments/') && rel.split('/').length - 1 > 6); + const author = row.targetAuthor ? row.targetAuthor : '[deleted]'; + let title = `${isComment ? 'Comment' : 'Post'} by u/${author}`; + if (link) title = `[${title}](${link})`; + return sanitizeForMarkdown(title); +} + +/** urllib.parse.quote with the default safe="/". */ +export function pyQuote(text: string): string { + return encodeURIComponent(text) + .replaceAll('%2F', '/') + .replace(/[!'()*]/g, (c) => `%${c.charCodeAt(0).toString(16).toUpperCase()}`); +} + +/** generate_modmail_link: a prefilled removal-inquiry modmail to the subreddit. */ +export function modmailLink(subreddit: string, row: StoredAction): string { + const removalType = REMOVAL_TYPE_LABELS[row.action] ?? 'Content'; + const contentId = shortContentId(row); + + let title = row.targetAuthor ? `Content by u/${row.targetAuthor}` : 'Unknown content'; + const chars = Array.from(title); + if (chars.length > MAX_TITLE_LENGTH) title = `${chars.slice(0, MAX_TITLE_LENGTH - 3).join('')}...`; + + let url = ''; + if (row.permalink) url = row.permalink; + + const subject = `${removalType} Removal Inquiry - ${title} [ID: ${contentId}]`; + const body = + `Hello Moderators of /r/${subreddit},\n\n` + + `I would like to inquire about the recent removal of the following ${removalType.toLowerCase()}:\n\n` + + `**Content ID:** ${contentId}\n\n` + + `**Title:** ${title}\n\n` + + `**Action Type:** ${row.action}\n\n` + + `**Link:** ${url}\n\n` + + 'Please provide details regarding this action.\n\n' + + 'Thank you!'; + + const href = `https://www.reddit.com/message/compose?to=/r/${subreddit}&subject=${pyQuote(subject)}&message=${pyQuote(body)}`; + return `[Contact Mods](${href})`; +} + +function formatEntry(r: RenderRow, opts: RenderOptions): string { + const { row } = r; + let reasonText = '-'; + if (r.combinedReason) reasonText = r.combinedReason.trim(); + else if (r.approvalContext) reasonText = r.approvalContext.trim(); + else if (row.reason) reasonText = row.reason.trim(); + + let displayAction = row.action; + if (REMOVAL_ACTIONS.includes(row.action) && moderatorLabel(row.moderator, false) === 'AutoModerator') { + displayAction = `filter-${row.action}`; + } + + const time = `${formatUtcTime(rowDate(row))} UTC`; + const moderator = moderatorLabel(row.moderator, opts.anonymizeModerators) ?? 'Unknown'; + const cells = [ + time, + displayAction, + shortContentId(row), + moderator, + formatContentLink(row), + sanitizeForMarkdown(reasonText), + modmailLink(opts.subredditName, row), + ]; + return `| ${cells.join(' | ')} |`; +} + +/** + * FR-8: keep an approval only when it reverses a Reddit/AutoModerator removal of + * the same post/comment, and annotate it with that removal. + */ +function gateApprovals(rows: StoredAction[], opts: RenderOptions): RenderRow[] { + const out: RenderRow[] = []; + for (const row of rows) { + if (!APPROVAL_ACTIONS.includes(row.action)) { + out.push({ row }); + continue; + } + const contentId = bareContentId(row.permalink); + if (!contentId) continue; + const prior = opts.priorRemoval(contentId); + if (!prior) continue; + let context = `Approved ${prior.moderator} removal`; + const reason = prior.reason.trim(); + if (reason && prior.reason !== '-') context += `: ${reason}`; + out.push({ row, approvalContext: context }); + } + return out; +} + +/** FR-7: a removal and its addremovalreason on the same target become one row. */ +function combineRemovalReasons(rows: RenderRow[]): RenderRow[] { + const byTarget = new Map(); + for (const r of rows) { + const key = bareContentId(r.row.permalink) ?? r.row.id; + const group = byTarget.get(key); + if (group) group.push(r); + else byTarget.set(key, [r]); + } + + const combined: RenderRow[] = []; + for (const group of byTarget.values()) { + let removal: RenderRow | undefined; + let reason: RenderRow | undefined; + const others: RenderRow[] = []; + for (const r of group) { + if (REMOVAL_ACTIONS.includes(r.row.action) && !removal) removal = r; + else if (REASON_ACTIONS.includes(r.row.action) && !reason) reason = r; + else others.push(r); + } + if (removal && reason) { + if (reason.row.reason) removal.combinedReason = reason.row.reason; + combined.push(removal); + } else { + if (removal) combined.push(removal); + if (reason) combined.push(reason); + } + combined.push(...others); + } + return combined; +} + +const utf8Bytes = (s: string): number => Buffer.byteLength(s, 'utf8'); + +/** + * build_wiki_content. `rows` must be ordered newest first, as the Python query + * (ORDER BY created_at DESC) returns them. + */ +export function buildWikiContent(rows: StoredAction[], opts: RenderOptions): RenderResult { + const header = `**Last Updated:** ${formatUtcDate(opts.now)} ${formatUtcTime(opts.now)} UTC\n\n---\n\n`; + if (rows.length === 0) { + return { content: `${header}No recent moderation actions found.`, rows: 0, trimmedDays: 0 }; + } + + let actions = combineRemovalReasons(gateApprovals(rows, opts)); + if (actions.length > MAX_WIKI_ENTRIES) actions = actions.slice(0, MAX_WIKI_ENTRIES); + + const byDate = new Map(); + for (const r of actions) { + const date = formatUtcDate(rowDate(r.row)); + const group = byDate.get(date); + if (group) group.push(r); + else byDate.set(date, [r]); + } + const dates = [...byDate.keys()].sort().reverse(); + + const blocks = dates.map((date) => { + const lines = [`## ${date}`, '| Time | Action | ID | Moderator | Content | Reason | Inquire |', '|------|--------|----|-----------|---------|--------|---------|']; + const dayRows = [...(byDate.get(date) ?? [])].sort((a, b) => b.row.createdAt - a.row.createdAt); + for (const r of dayRows) lines.push(formatEntry(r, opts)); + lines.push(''); + return lines.join('\n'); + }); + + const parts = [header]; + let size = utf8Bytes(header + FOOTER_LINES.join('\n')); + let trimmedDays = 0; + for (let i = 0; i < blocks.length; i++) { + const block = blocks[i] as string; + const next = size + utf8Bytes(block); + if (next > TRIM_THRESHOLD) { + trimmedDays = dates.length - i; + if (trimmedDays > 0) { + parts.push(`\n**Note:** ${trimmedDays} older day(s) trimmed due to wiki size limits.`); + parts.push(`Only showing entries from ${i > 0 ? dates[i - 1] : 'today'} onwards.\n`); + } + break; + } + parts.push(block); + size = next; + } + parts.push(...FOOTER_LINES); + + const content = parts.join('\n'); + if (utf8Bytes(content) > REDDIT_WIKI_LIMIT) { + throw new Error(`Wiki content too large: ${utf8Bytes(content)} bytes (limit ${REDDIT_WIKI_LIMIT})`); + } + return { content, rows: actions.length, trimmedDays }; +} + +/** get_content_hash: SHA-256 with the volatile "Last Updated" line removed. */ +export function contentHash(content: string): string { + const hashable = content.replace(/^\*\*Last Updated:\*\* .*\n/, ''); + return createHash('sha256').update(hashable, 'utf8').digest('hex'); +} diff --git a/devvit/src/core/settings.ts b/devvit/src/core/settings.ts new file mode 100644 index 0000000..3f4b67a --- /dev/null +++ b/devvit/src/core/settings.ts @@ -0,0 +1,116 @@ +/** + * Install settings: validators (wired to devvit.json validationEndpoint) and + * the loader that turns raw settings into an AppConfig. Defaults and bands + * follow CONFIG_LIMITS in modlog_wiki_publisher.py. + */ +import { + DAYS_MAX, + DAYS_MIN, + DEFAULT_DISPLAY_DAYS, + DEFAULT_RETENTION_DAYS, + DEFAULT_WIKI_ACTIONS, + DEFAULT_WIKI_PAGE, + VALID_MODLOG_ACTIONS, +} from './constants.js'; +import type { AppConfig } from './types.js'; + +export const SETTING_KEYS = { + wikiPage: 'wikiPage', + retentionDays: 'retentionDays', + displayDays: 'displayDays', + wikiActions: 'wikiActions', + anonymizeModerators: 'anonymizeModerators', + ignoredModerators: 'ignoredModerators', +} as const; + +/** A validator returns an error message, or undefined when the value is valid. */ +export type Validator = (value: T | undefined) => string | undefined; + +const WIKI_PAGE_RE = /^[a-z0-9_-]+(\/[a-z0-9_-]+)*$/; +const USERNAME_RE = /^[A-Za-z0-9_-]{1,20}$/; + +/** Reddit-managed pages a mistyped name could overwrite. */ +const PROTECTED_PAGE_PREFIXES = ['config/', 'automoderator', 'usernotes', 'toolbox']; + +export const validateWikiPage: Validator = (value) => { + const page = (value ?? '').trim().toLowerCase(); + if (!page) return 'Wiki page name is required.'; + if (page.length > 128) return 'Wiki page name must be 128 characters or fewer.'; + if (!WIKI_PAGE_RE.test(page)) return 'Use lowercase letters, digits, "_", "-" and "/" only.'; + if (PROTECTED_PAGE_PREFIXES.some((p) => page.startsWith(p))) return `"${page}" is a reserved page.`; + return undefined; +}; + +export const validateDays: Validator = (value) => { + if (value === undefined || !Number.isInteger(value)) return 'Enter a whole number of days.'; + if (value < DAYS_MIN || value > DAYS_MAX) return `Days must be between ${DAYS_MIN} and ${DAYS_MAX}.`; + return undefined; +}; + +export const validateWikiActions: Validator = (value) => { + if (!value || value.length === 0) return 'Select at least one action type.'; + const invalid = value.filter((a) => !VALID_MODLOG_ACTIONS.includes(a)); + if (invalid.length > 0) return `Unknown action types: ${invalid.join(', ')}`; + return undefined; +}; + +/** The Python publisher refuses to run with anonymization off; this app refuses the setting. */ +export const validateAnonymize: Validator = (value) => + value === false ? 'Moderator anonymization cannot be turned off.' : undefined; + +export function parseIgnoredModerators(value: string | undefined): string[] { + return (value ?? '') + .split(',') + .map((s) => s.trim().replace(/^\/?u\//i, '')) + .filter((s) => s.length > 0); +} + +export const validateIgnoredModerators: Validator = (value) => { + const bad = parseIgnoredModerators(value).filter((n) => !USERNAME_RE.test(n)); + return bad.length > 0 ? `Not valid usernames: ${bad.join(', ')}` : undefined; +}; + +function asNumber(v: unknown, fallback: number): number { + const n = typeof v === 'number' ? v : typeof v === 'string' && v.trim() !== '' ? Number(v) : NaN; + if (!Number.isFinite(n)) return fallback; + return Math.min(DAYS_MAX, Math.max(DAYS_MIN, Math.trunc(n))); +} + +function asStringArray(v: unknown): string[] | undefined { + if (Array.isArray(v)) return v.filter((x): x is string => typeof x === 'string'); + if (typeof v === 'string' && v.trim()) return [v]; + return undefined; +} + +/** + * Build the AppConfig. Unset values take defaults; out-of-band numbers are clamped + * as Python's validate_config_value does. An invalid page name or action list + * throws, because publishing to the wrong page is worse than not publishing. + */ +export function loadConfig(raw: Record, subredditName: string | undefined): AppConfig { + if (!subredditName) throw new Error('No subreddit in the request context.'); + + const pageRaw = typeof raw[SETTING_KEYS.wikiPage] === 'string' ? (raw[SETTING_KEYS.wikiPage] as string) : ''; + const wikiPage = (pageRaw.trim() || DEFAULT_WIKI_PAGE).toLowerCase(); + const pageError = validateWikiPage(wikiPage); + if (pageError) throw new Error(`Invalid wiki page setting: ${pageError}`); + + const actions = asStringArray(raw[SETTING_KEYS.wikiActions]) ?? [...DEFAULT_WIKI_ACTIONS]; + const wikiActions = actions.length > 0 ? actions : [...DEFAULT_WIKI_ACTIONS]; + const actionError = validateWikiActions(wikiActions); + if (actionError) throw new Error(`Invalid action list setting: ${actionError}`); + + const retentionDays = asNumber(raw[SETTING_KEYS.retentionDays], DEFAULT_RETENTION_DAYS); + const displayDays = Math.min(asNumber(raw[SETTING_KEYS.displayDays], DEFAULT_DISPLAY_DAYS), retentionDays); + + const ignoredRaw = raw[SETTING_KEYS.ignoredModerators]; + return { + subredditName, + wikiPage, + retentionDays, + displayDays, + wikiActions, + anonymizeModerators: true, + ignoredModerators: parseIgnoredModerators(typeof ignoredRaw === 'string' ? ignoredRaw : undefined), + }; +} diff --git a/devvit/src/core/store.ts b/devvit/src/core/store.ts new file mode 100644 index 0000000..12ac64e --- /dev/null +++ b/devvit/src/core/store.ts @@ -0,0 +1,175 @@ +/** + * Redis persistence. Devvit Redis has no sets, lists or SCAN, so the layout is + * hashes plus sorted sets: + * + * act hash action id -> StoredAction JSON (hSetNX is the dedup gate) + * idx: zset action id scored by createdAt, one per action type + * types hash action type -> "1" (which idx: keys exist) + * prior hash bare post/comment id -> latest Reddit/AutoModerator removal (FR-8) + * prior:idx zset bare id scored by that removal's createdAt (retention) + * wikihash hash wiki page -> content hash of the last write + * status hash last run / publish bookkeeping + * lock hash "cycle" -> epoch ms of the run holding the lock + */ +import { MAX_WIKI_ENTRIES, REMOVAL_ACTIONS } from './constants.js'; +import { bareContentId } from './render.js'; +import type { PriorRemoval, StoredAction } from './types.js'; + +/** The Redis calls this module makes; satisfied by `redis` from @devvit/web/server. */ +export interface RedisLike { + hGet(key: string, field: string): Promise; + hMGet(key: string, fields: string[]): Promise<(string | null)[]>; + hSet(key: string, fieldValues: Record): Promise; + hSetNX(key: string, field: string, value: string): Promise; + hDel(key: string, fields: string[]): Promise; + hKeys(key: string): Promise; + hGetAll(key: string): Promise>; + hLen(key: string): Promise; + zAdd(key: string, ...members: { member: string; score: number }[]): Promise; + zRange( + key: string, + start: number | string, + stop: number | string, + options?: { by: 'score' | 'lex' | 'rank'; reverse?: boolean; limit?: { offset: number; count: number } }, + ): Promise<{ member: string; score: number }[]>; + zRemRangeByScore(key: string, min: number, max: number): Promise; +} + +const K = { + act: 'act', + idx: (type: string) => `idx:${type}`, + types: 'types', + prior: 'prior', + priorIdx: 'prior:idx', + wikiHash: 'wikihash', + status: 'status', + lock: 'lock', +} as const; + +const LOCK_FIELD = 'cycle'; +const LOCK_STALE_MS = 120_000; +const CHUNK = 500; + +function chunks(items: T[], size: number): T[][] { + const out: T[][] = []; + for (let i = 0; i < items.length; i += size) out.push(items.slice(i, i + size)); + return out; +} + +const AUTOMATED_REMOVERS = new Set(['reddit', 'automoderator']); + +export class Store { + constructor(private readonly redis: RedisLike) {} + + /** Store a new action. Returns false when the id was already stored (dedup). */ + async saveAction(row: StoredAction): Promise { + const added = await this.redis.hSetNX(K.act, row.id, JSON.stringify(row)); + if (added !== 1) return false; + await this.redis.zAdd(K.idx(row.action), { member: row.id, score: row.createdAt }); + await this.redis.hSet(K.types, { [row.action]: '1' }); + if (REMOVAL_ACTIONS.includes(row.action) && row.moderator && AUTOMATED_REMOVERS.has(row.moderator.toLowerCase())) { + await this.recordPriorRemoval(row); + } + return true; + } + + /** Keep only the newest Reddit/AutoModerator removal per target (Python ORDER BY created_at DESC LIMIT 1). */ + private async recordPriorRemoval(row: StoredAction): Promise { + const contentId = bareContentId(row.permalink); + if (!contentId || !row.moderator) return; + const existing = await this.redis.hGet(K.prior, contentId); + if (existing) { + const prev = JSON.parse(existing) as PriorRemoval; + if (prev.createdAt > row.createdAt) return; + } + const prior: PriorRemoval = { moderator: row.moderator, reason: row.reason, createdAt: row.createdAt }; + await this.redis.hSet(K.prior, { [contentId]: JSON.stringify(prior) }); + await this.redis.zAdd(K.priorIdx, { member: contentId, score: row.createdAt }); + } + + /** All prior removals for the targets of the given rows, keyed by bare id. */ + async priorRemovalsFor(rows: StoredAction[]): Promise> { + const ids = [...new Set(rows.map((r) => bareContentId(r.permalink)).filter((id): id is string => id !== null))]; + const out = new Map(); + for (const batch of chunks(ids, CHUNK)) { + const values = await this.redis.hMGet(K.prior, batch); + values.forEach((v, i) => { + if (v) out.set(batch[i] as string, JSON.parse(v) as PriorRemoval); + }); + } + return out; + } + + /** + * The rows the wiki shows: the given action types, created at or after `sinceSec`, + * newest first, at most MAX_WIKI_ENTRIES (Python get_recent_actions_from_db). + */ + async loadDisplayRows(actionTypes: readonly string[], sinceSec: number): Promise { + const candidates: { member: string; score: number }[] = []; + for (const type of actionTypes) { + const top = await this.redis.zRange(K.idx(type), 0, MAX_WIKI_ENTRIES - 1, { by: 'rank', reverse: true }); + for (const m of top) if (m.score >= sinceSec) candidates.push(m); + } + candidates.sort((a, b) => b.score - a.score); + const ids = candidates.slice(0, MAX_WIKI_ENTRIES).map((m) => m.member); + + const rows: StoredAction[] = []; + for (const batch of chunks(ids, CHUNK)) { + const values = await this.redis.hMGet(K.act, batch); + for (const v of values) if (v) rows.push(JSON.parse(v) as StoredAction); + } + return rows; + } + + /** Delete everything created before `cutoffSec` (Python cleanup_old_entries). Returns rows removed. */ + async prune(cutoffSec: number): Promise { + let removed = 0; + const types = await this.redis.hKeys(K.types); + for (const type of types) { + const old = await this.redis.zRange(K.idx(type), 0, cutoffSec - 1, { by: 'score' }); + for (const batch of chunks(old.map((m) => m.member), CHUNK)) { + removed += await this.redis.hDel(K.act, batch); + } + await this.redis.zRemRangeByScore(K.idx(type), 0, cutoffSec - 1); + } + const oldPrior = await this.redis.zRange(K.priorIdx, 0, cutoffSec - 1, { by: 'score' }); + for (const batch of chunks(oldPrior.map((m) => m.member), CHUNK)) { + await this.redis.hDel(K.prior, batch); + } + await this.redis.zRemRangeByScore(K.priorIdx, 0, cutoffSec - 1); + return removed; + } + + async storedCount(): Promise { + return this.redis.hLen(K.act); + } + + async getWikiHash(page: string): Promise { + return this.redis.hGet(K.wikiHash, page); + } + + async setWikiHash(page: string, hash: string): Promise { + await this.redis.hSet(K.wikiHash, { [page]: hash }); + } + + async setStatus(fields: Record): Promise { + await this.redis.hSet(K.status, fields); + } + + async getStatus(): Promise> { + return this.redis.hGetAll(K.status); + } + + /** Single-run lock so the cron and a menu click cannot publish at the same time. */ + async acquireLock(nowMs: number): Promise { + if ((await this.redis.hSetNX(K.lock, LOCK_FIELD, String(nowMs))) === 1) return true; + const held = Number(await this.redis.hGet(K.lock, LOCK_FIELD)); + if (Number.isFinite(held) && nowMs - held < LOCK_STALE_MS) return false; + await this.redis.hSet(K.lock, { [LOCK_FIELD]: String(nowMs) }); + return true; + } + + async releaseLock(): Promise { + await this.redis.hDel(K.lock, [LOCK_FIELD]); + } +} diff --git a/devvit/src/core/types.ts b/devvit/src/core/types.ts new file mode 100644 index 0000000..07afc4d --- /dev/null +++ b/devvit/src/core/types.ts @@ -0,0 +1,52 @@ +/** + * Shared shapes. StoredAction mirrors one row of the Python `processed_actions` + * table, so the renderer consumes the same data build_wiki_content saw. + */ + +/** The subset of a Devvit ModAction the app reads. */ +export interface ModLogEntry { + id: string; + type: string; + moderatorName?: string | undefined; + createdAt: Date; + description?: string | undefined; + details?: string | undefined; + target?: + | { + id: string; + author?: string | undefined; + permalink?: string | undefined; + } + | undefined; +} + +export interface StoredAction { + id: string; + action: string; + /** "AutoModerator", "Reddit", the anonymized label, or null when the log had no moderator. */ + moderator: string | null; + /** Absolute "https://reddit.com/..." post/comment link, or null. Never a profile link. */ + permalink: string | null; + /** Email-censored, pipe-sanitized reason; "" when absent. */ + reason: string; + targetAuthor: string | null; + /** Epoch seconds. */ + createdAt: number; +} + +/** Latest Reddit/AutoModerator removal of one target, used to gate approval rows (FR-8). */ +export interface PriorRemoval { + moderator: string; + reason: string; + createdAt: number; +} + +export interface AppConfig { + subredditName: string; + wikiPage: string; + retentionDays: number; + displayDays: number; + wikiActions: string[]; + anonymizeModerators: boolean; + ignoredModerators: string[]; +} diff --git a/devvit/src/main.ts b/devvit/src/main.ts deleted file mode 100644 index eab53cf..0000000 --- a/devvit/src/main.ts +++ /dev/null @@ -1,200 +0,0 @@ -/** - * main.ts — Devvit app entrypoint and wiring. - * - * This is the ONLY module that registers Devvit capabilities. It wires the - * shared pipeline modules together; it contains no business logic of its own - * beyond orchestration and per-invocation error handling (P-29: no daemon loop — - * rely on the scheduler's retry semantics + bounded try/catch). - * - * Capabilities registered here: - * - Devvit.configure — enable Reddit API + Redis. - * - settings.registerSettings — the per-install configuration form. - * - menu.registerMenuItems — moderator-only "Publish now" / "Show status". - * - Devvit.addSchedulerJob — the recurring ingest+publish+retention job. - * - Devvit.addTrigger — AppInstall/AppUpgrade (schedule the cron) and - * ModAction (cheap incremental ingest). - * - * Pipeline (the single shared path, run by both the cron job and the menu): - * loadConfig -> ingest (fetch/filter/anonymize/dedup/persist) - * -> publishFromStore (render -> hash-skip -> create/update wiki) - * -> cleanupOld (retention prune) - * - * INVARIANTS are enforced in the modules this file calls, NOT here: - * INV-1 anonymize (modlog), INV-2 link-gating (modlog/render), - * INV-3 byte cap (render/wiki), INV-4 censor/escape (render), - * INV-5 dedup (storage/modlog), INV-6 hash-skip (wiki), - * INV-7/8 filters (modlog/settings), INV-9 per-install isolation (platform). - * - * Dependency direction (downward only): main -> { settings, menu, modlog, wiki, - * storage }. No module imports main. - */ - -import { Devvit } from '@devvit/public-api'; -import type { ScheduledJobEvent, TriggerContext } from '@devvit/public-api'; - -import { registerSettings, loadConfig } from './settings.js'; -import { registerMenuItems } from './menu.js'; -import { ingest } from './modlog.js'; -import { publishFromStore } from './wiki.js'; -import { - cleanupOld, - recordRunStarted, - recordPublished, -} from './storage.js'; - -// --------------------------------------------------------------------------- -// Constants -// --------------------------------------------------------------------------- - -/** Scheduler job name. Stable identifier — referenced by runJob/cancelJob. */ -const PUBLISH_JOB_NAME = 'publish-modlog'; - -/** - * Cron schedule for the recurring publish job. The legacy Python daemon polled - * every 600s (10 min); the closest sane cron that respects Devvit's scheduler - * cadence is every 10 minutes. Adjust here if the platform imposes a minimum - * interval for a given app tier. - */ -const PUBLISH_CRON = '*/10 * * * *'; - -// --------------------------------------------------------------------------- -// Devvit configuration -// --------------------------------------------------------------------------- - -Devvit.configure({ - redditAPI: true, - redis: true, -}); - -// --------------------------------------------------------------------------- -// Core pipeline — shared by the scheduler job and (manually) the menu action. -// --------------------------------------------------------------------------- - -/** - * Run one full publish cycle for the install's subreddit. - * - * Resolves config, ingests new mod-log actions, publishes the rendered wiki - * page (hash-skipped if unchanged), then prunes records past the retention - * window. Returns a small summary for logging. Never throws — callers in a - * serverless context must not propagate (the scheduler will retry on its own - * cadence). - */ -async function runPublishCycle(context: TriggerContext): Promise { - const { reddit, redis, settings, subredditName } = context; - - if (!subredditName) { - console.error('[main] runPublishCycle: no subredditName in context; skipping.'); - return; - } - - try { - await recordRunStarted(redis, Date.now()); - - // 1. Resolve config (per-install settings + context subreddit, INV-9). - const cfg = await loadConfig(settings, subredditName); - - // 2. Ingest new actions (fetch -> filter/anonymize -> dedup -> persist). - const { added, scanned } = await ingest(reddit, redis, cfg); - - // 3. Render the store and publish (hash-skip honored unless content changed). - const { wrote, reason } = await publishFromStore(reddit, redis, cfg); - if (wrote) { - await recordPublished(redis, Date.now()); - } - - // 4. Retention prune (INV-9 / time-based). nowSec injected for determinism. - const nowSec = Math.floor(Date.now() / 1000); - const removed = await cleanupOld(redis, cfg.wikiPage, cfg.retentionDays, nowSec); - - console.info( - `[main] cycle complete sub="${subredditName}" page="${cfg.wikiPage}" ` + - `scanned=${scanned} added=${added} wrote=${wrote} reason=${reason} pruned=${removed}`, - ); - } catch (err) { - // P-29/P-30: log rich context, swallow so the platform retries on schedule. - console.error(`[main] runPublishCycle failed for sub="${subredditName}":`, err); - } -} - -// --------------------------------------------------------------------------- -// Scheduler job -// --------------------------------------------------------------------------- - -Devvit.addSchedulerJob({ - name: PUBLISH_JOB_NAME, - onRun: async (_event: ScheduledJobEvent, context) => { - await runPublishCycle(context); - }, -}); - -// --------------------------------------------------------------------------- -// Lifecycle triggers — (re)schedule the recurring job on install/upgrade. -// --------------------------------------------------------------------------- - -/** - * Cancel any pre-existing instances of the publish job, then schedule a fresh - * cron instance. Running on both AppInstall and AppUpgrade keeps exactly one - * scheduled job alive and lets a cron-schedule change take effect on upgrade. - */ -async function schedulePublishJob(context: TriggerContext): Promise { - const { scheduler } = context; - try { - // Remove stale instances so an upgrade doesn't stack duplicate jobs. - const existing = await scheduler.listJobs(); - for (const job of existing) { - if (job.name === PUBLISH_JOB_NAME) { - await scheduler.cancelJob(job.id); - } - } - - await scheduler.runJob({ name: PUBLISH_JOB_NAME, cron: PUBLISH_CRON }); - console.info(`[main] scheduled "${PUBLISH_JOB_NAME}" cron="${PUBLISH_CRON}".`); - } catch (err) { - console.error('[main] failed to schedule publish job:', err); - } -} - -Devvit.addTrigger({ - events: ['AppInstall', 'AppUpgrade'], - onEvent: async (_event, context) => { - await schedulePublishJob(context); - }, -}); - -// --------------------------------------------------------------------------- -// ModAction trigger — cheap incremental ingest. -// --------------------------------------------------------------------------- -// -// On each moderation action we run an ingest pass so newly-removed content is -// captured promptly without waiting up to a full cron interval. We deliberately -// DO NOT publish from the trigger: publishing is coalesced into the next cron -// run to avoid a wiki write per individual mod action (rate + churn). Ingest is -// idempotent by ModAction.id (INV-5), so trigger + cron overlap is safe. - -Devvit.addTrigger({ - event: 'ModAction', - onEvent: async (_event, context) => { - const { reddit, redis, settings, subredditName } = context; - if (!subredditName) { - return; - } - try { - const cfg = await loadConfig(settings, subredditName); - const { added } = await ingest(reddit, redis, cfg); - if (added > 0) { - console.info(`[main] ModAction trigger ingested ${added} new action(s) for r/${subredditName}.`); - } - } catch (err) { - console.error(`[main] ModAction trigger failed for sub="${subredditName}":`, err); - } - }, -}); - -// --------------------------------------------------------------------------- -// Settings + menu registration (side-effecting; order after configure). -// --------------------------------------------------------------------------- - -registerSettings(); -registerMenuItems(); - -export default Devvit; diff --git a/devvit/src/menu.ts b/devvit/src/menu.ts deleted file mode 100644 index 6bff13d..0000000 --- a/devvit/src/menu.ts +++ /dev/null @@ -1,220 +0,0 @@ -/** - * menu.ts — Moderator-only menu actions for RedditModLog (Devvit). - * - * Provides two subreddit menu items, both restricted to moderators - * (`forUserType: 'moderator'`): - * - * 1. "Mod Log: Publish now" — manually triggers the full ingest + - * wiki-publish pipeline (the same path the scheduler runs), then - * reports the result via a toast. - * - * 2. "Mod Log: Show status" — a tiny read-only status view (last run - * time, last published time, stored entry count) read straight from - * Redis storage. Rendered as a toast to keep the UI minimal, per the - * architecture spec ("Keep UI minimal (blocks or a toast)"). - * - * This module owns NO business logic of its own — it is a thin adapter that - * wires Devvit menu events to the shared pipeline modules: - * settings.loadConfig() -> modlog.ingest() -> wiki.publishFromStore() - * and to the storage status accessors. That keeps the manual "Publish now" - * action behaviourally identical to the scheduled job (single code path, - * per the architecture's design note for `publishFromStore`). - * - * Dependency direction (downward only, no cycles): - * menu -> { settings, modlog, wiki, storage } - * - * Registration: import this file from `main.ts`; calling `registerMenuItems()` - * (or simply importing for its side effects) installs the menu items via - * `Devvit.addMenuItem`. We expose an explicit `registerMenuItems()` function - * rather than registering at import time so `main.ts` controls ordering - * (e.g. after `Devvit.configure`). - */ - -import { Devvit } from '@devvit/public-api'; -import type { Context, MenuItemOnPressEvent } from '@devvit/public-api'; - -import { loadConfig } from './settings.js'; -import { ingest } from './modlog.js'; -import { publishFromStore } from './wiki.js'; -import { - getStatus, - recordRunStarted, - recordPublished, -} from './storage.js'; - -/** - * Shape of the lightweight status snapshot read from Redis. - * - * Implemented by `storage.getStatus()`. Fields are optional because a - * freshly-installed app has never run or published yet. - * - * NOTE (storage contract): the §1.2 storage spec enumerates dedup / record / - * time-index / wiki-hash / dirty / schema keys. The "last run" and "last - * published" status fields are status metadata that `getStatus` is expected to - * surface. If `storage.ts` names these accessors differently, only the three - * imports above need to change — the handlers below are otherwise decoupled. - */ -interface AppStatus { - /** Epoch ms of the last time the ingest+publish pipeline started. */ - lastRunAtMs?: number; - /** Epoch ms of the last time the wiki page was actually written. */ - lastPublishedAtMs?: number; - /** Number of mod-action records currently retained in storage. */ - entryCount: number; -} - -// -------------------------------------------------------------------------- -// Formatting helpers (pure) -// -------------------------------------------------------------------------- - -/** - * Render an epoch-ms timestamp as a compact, human-readable UTC string, or a - * placeholder when the event has never occurred. Toast text is short-lived - * and space-constrained, so we keep this terse. - */ -function formatTimestamp(epochMs?: number): string { - if (!epochMs || !Number.isFinite(epochMs)) { - return 'never'; - } - // ISO 8601 trimmed to minute precision in UTC, e.g. "2026-06-23 14:05Z". - return new Date(epochMs).toISOString().replace('T', ' ').slice(0, 16) + 'Z'; -} - -/** - * Build the single-line status string shown in the "Show status" toast. - * Kept as a pure function so it is trivially unit-testable. - */ -function formatStatusText(status: AppStatus): string { - const entries = `${status.entryCount} entr${status.entryCount === 1 ? 'y' : 'ies'}`; - return ( - `Mod Log status — ${entries} stored. ` + - `Last run: ${formatTimestamp(status.lastRunAtMs)}. ` + - `Last published: ${formatTimestamp(status.lastPublishedAtMs)}.` - ); -} - -// -------------------------------------------------------------------------- -// Handlers -// -------------------------------------------------------------------------- - -/** - * Handle the "Publish now" menu action. - * - * Runs the same pipeline as the scheduled job: - * 1. Resolve config from app/install settings. - * 2. Ingest new mod-log actions into storage (deduped, anonymized). - * 3. Render the store to markdown and publish to the wiki, honouring the - * SHA-256 hash-skip (INV-6) so an unchanged page is not rewritten. - * - * All failures are caught and surfaced to the moderator as a neutral toast; - * a serverless menu invocation must never throw unhandled (P-29). Detailed - * context is logged server-side via `console.error` (P-30, coding-style: - * "Log detailed error context on the server side"). - */ -async function handlePublishNow( - _event: MenuItemOnPressEvent, - context: Context, -): Promise { - const { reddit, redis, settings, subredditName, ui } = context; - - // INV-9: the subreddit comes from the install context, never a setting. - if (!subredditName) { - console.error('[menu] Publish now: no subredditName in context; skipping.'); - ui.showToast({ - text: 'Mod Log: could not determine subreddit — see app logs.', - appearance: 'neutral', - }); - return; - } - - try { - // Mark the run start so "Show status" reflects manual runs too. - await recordRunStarted(redis, Date.now()); - - const cfg = await loadConfig(settings, subredditName); - - // Ingest is idempotent by ModAction.id (INV-5), so a manual run that - // overlaps the scheduler cannot double-insert records. The Reddit client - // comes from context (classic model has no `reddit` singleton). - const { added } = await ingest(reddit, redis, cfg); - - // Single shared publish path: render store -> hash-skip -> create/update. - const { wrote, reason } = await publishFromStore(reddit, redis, cfg); - - if (wrote) { - // Stamp the published time only when the wiki was actually written. - await recordPublished(redis, Date.now()); - } - - const summary = wrote - ? `Published (${reason}). ${added} new action${added === 1 ? '' : 's'} ingested.` - : `Wiki unchanged (${reason}). ${added} new action${added === 1 ? '' : 's'} ingested.`; - - ui.showToast({ text: `Mod Log: ${summary}`, appearance: 'success' }); - } catch (err) { - console.error('[menu] Publish now failed:', err); - ui.showToast({ - text: 'Mod Log: publish failed — see app logs for details.', - appearance: 'neutral', - }); - } -} - -/** - * Handle the "Show status" menu action. - * - * Read-only: pulls the status snapshot from Redis and renders it as a toast. - * Performs no Reddit calls and no writes, so it is safe to invoke freely. - */ -async function handleShowStatus( - _event: MenuItemOnPressEvent, - context: Context, -): Promise { - const { redis, ui } = context; - - try { - const status = (await getStatus(redis)) as AppStatus; - ui.showToast({ text: formatStatusText(status), appearance: 'neutral' }); - } catch (err) { - console.error('[menu] Show status failed:', err); - ui.showToast({ - text: 'Mod Log: could not read status — see app logs for details.', - appearance: 'neutral', - }); - } -} - -// -------------------------------------------------------------------------- -// Registration -// -------------------------------------------------------------------------- - -/** - * Register all moderator menu items. Call once from `main.ts`, after - * `Devvit.configure(...)`. - * - * Both items are scoped to `location: 'subreddit'` (the subreddit "..." menu) - * and `forUserType: 'moderator'` so only mods see/trigger them — this is the - * Devvit-native authorization gate for menu actions (no manual mod-check - * needed in the handler). - */ -export function registerMenuItems(): void { - Devvit.addMenuItem({ - label: 'Mod Log: Publish now', - description: 'Manually ingest the mod log and publish it to the wiki page.', - location: 'subreddit', - forUserType: 'moderator', - onPress: handlePublishNow, - }); - - Devvit.addMenuItem({ - label: 'Mod Log: Show status', - description: 'Show last run time, last published time, and stored entry count.', - location: 'subreddit', - forUserType: 'moderator', - onPress: handleShowStatus, - }); -} - -// Exported for unit testing of the pure formatting layer. -export { formatStatusText, formatTimestamp }; -export type { AppStatus }; diff --git a/devvit/src/modlog.ts b/devvit/src/modlog.ts deleted file mode 100644 index 41ee8b4..0000000 --- a/devvit/src/modlog.ts +++ /dev/null @@ -1,246 +0,0 @@ -/** - * modlog.ts — Moderation-log ingestion. - * - * Turns raw `reddit.getModerationLog` output into deduped, anonymized, - * render-ready `ModRecord`s and persists them via the storage layer. - * - * This module owns several binding invariants from the legacy Python app - * (see devvit architecture spec / modlog_wiki_publisher.py — read-only ref): - * - * INV-1 Anonymize moderators ALWAYS. Real names never leave this module; - * `ModRecord.moderator` always holds an anonymized label. - * INV-2 NEVER link user profiles. Only post (t3_) and comment (t1_) - * targets get a permalink; user (t2_) / subreddit (t5_) / other - * targets get `permalink: undefined`. - * INV-5 Dedup by ModAction.id — each action processed at most once. - * INV-7 Default tracked action types (client-side filter). - * INV-8 Default ignored moderators (filtered by moderatorName). - * - * Dependency direction (downward only): modlog -> { reddit, storage, types }. - * `extractRecord`, `anonymizeMod`, and `deriveDisplay` are pure and exported - * for unit testing. - */ - -import type { ModAction, RedditAPIClient, RedisClient } from '@devvit/public-api'; - -import type { AppConfig, DisplayKind, ModRecord } from './types.js'; -import { ANON_LABEL, LITERAL_MODS } from './types.js'; -import * as storage from './storage.js'; - -/** - * INV-1 — Map a raw moderator username to its render-safe label. - * - * `AutoModerator` and `Reddit` (the platform's own automated actor) are kept - * literal so their actions remain attributable to automation; every human - * moderator collapses to a single shared `HumanModerator` label so individual - * mods can never be singled out from the published log. - * - * A missing/empty name is treated as the platform actor and labeled literally - * as `Reddit` (matches legacy behavior where unattributed actions are system - * actions, never a human). - */ -export function anonymizeMod(name: string | undefined | null): string { - const raw = (name ?? '').trim(); - if (raw.length === 0) { - return 'Reddit'; - } - if (LITERAL_MODS.has(raw)) { - return raw; - } - return ANON_LABEL; -} - -/** - * Derive the display kind + short display id from a target fullname. - * - * Reddit fullnames are prefixed by type: - * t1_ -> comment (C) t3_ -> post (P) t2_ -> user (U) t5_ -> subreddit - * Anything else (or a missing target) is a non-content "action" target (A). - * - * Ported from the legacy `generate_display_id`: post/comment ids are shortened - * to the first 6 chars of the bare id when the bare id exceeds 8 chars; user - * and action ids are passed through (with their prefix stripped) verbatim. - * - * Returns the *display* kind/id only — link gating (INV-2) is decided in - * `extractRecord`, which has the full target context. - */ -export function deriveDisplay(targetId: string | undefined | null): { - kind: DisplayKind; - displayId?: string; -} { - const fullname = (targetId ?? '').trim(); - if (fullname.length === 0) { - return { kind: 'A' }; - } - - // Split the t#_ prefix from the bare id, if present. - const match = /^t(\d)_(.+)$/.exec(fullname); - const typeNum = match ? match[1] : undefined; - const bareId = match ? match[2] : fullname; - - let kind: DisplayKind; - switch (typeNum) { - case '3': - kind = 'P'; // post - break; - case '1': - kind = 'C'; // comment - break; - case '2': - kind = 'U'; // user - break; - default: - kind = 'A'; // subreddit / award / unknown — treated as a generic action - break; - } - - // Shorten long content ids for display (post/comment only), matching the - // legacy 8-char threshold -> 6-char truncation. - let shortBare = bareId; - if ((kind === 'P' || kind === 'C') && bareId.length > 8) { - shortBare = bareId.slice(0, 6); - } - - return { kind, displayId: `${kind}${shortBare}` }; -} - -/** - * Extract the human-readable removal/mod-reason text from a ModAction. - * - * Priority mirrors the legacy publisher: - * 1. `details` — for `addremovalreason` the operator-typed reason lands - * here; for most other actions this is the richest field. - * 2. `description` — fallback summary string. - * - * The text is stored RAW. Email censoring and pipe-escaping (INV-4) are - * applied later in the pure render layer so the censor regex can evolve - * without a data migration. - */ -function extractReason(action: ModAction): string | undefined { - const details = action.details?.trim(); - if (details) { - return details; - } - const description = action.description?.trim(); - if (description) { - return description; - } - return undefined; -} - -/** - * PURE mapper: ModAction -> ModRecord, or `null` if the action is filtered - * out (untracked type per INV-7, or ignored moderator per INV-8). - * - * No I/O. Dedup (INV-5) and persistence happen in `ingest`. - */ -export function extractRecord(action: ModAction, cfg: AppConfig): ModRecord | null { - // INV-7 — only configured action types are tracked. - if (!cfg.wikiActions.includes(action.type)) { - return null; - } - - // INV-8 — drop actions performed by ignored moderators (e.g. AutoModerator). - // Case-insensitive: Reddit usernames match case-insensitively, and the ignore - // list (default + per-install) may carry mixed case. Comparing raw case would - // silently leak ignored mods into the public wiki. - const rawModerator = (action.moderatorName ?? '').trim().toLowerCase(); - if (rawModerator.length > 0 && cfg.ignoredModerators.some((m) => m.toLowerCase() === rawModerator)) { - return null; - } - - const targetId = action.target?.id; - const { kind, displayId } = deriveDisplay(targetId); - - // INV-2 — only post/comment targets are ever linkable. A user (U) or - // generic-action (A) target must never produce a profile/permalink. - const permalink = kind === 'P' || kind === 'C' ? action.target?.permalink : undefined; - - const record: ModRecord = { - id: action.id, - createdAtSec: Math.floor(action.createdAt.getTime() / 1000), - actionType: action.type, - moderator: anonymizeMod(action.moderatorName), // INV-1 — anonymized at ingest - targetId: targetId, - displayKind: kind, - displayId: displayId, - permalink: permalink, - targetAuthor: action.target?.author, - reason: extractReason(action), - }; - - return record; -} - -/** - * Fetch raw moderation-log actions for the configured subreddit. - * - * Notes on the API shape (verified against @devvit/reddit ModAction model): - * - `type` is a SINGLE-valued filter on GetModerationLogOptions, so it - * cannot express the 7-type tracked set. We omit it and filter - * client-side in `extractRecord`. - * - `limit` must be passed explicitly; the listing default is unbounded - * (Infinity), which we never want. `pageSize` caps per-request fan-out. - * - * Returns the fully-resolved array (newest first, as Reddit returns it). - * - * In the classic `@devvit/public-api` model there is no `reddit` singleton; the - * client is `context.reddit` (a `RedditAPIClient`) threaded in by the caller — - * mirrors the pattern in `wiki.ts`. - */ -export async function fetchActions( - reddit: RedditAPIClient, - cfg: AppConfig, -): Promise { - const listing = reddit.getModerationLog({ - subredditName: cfg.subredditName, - limit: cfg.fetchLimit, - pageSize: 100, - }); - - return listing.all(); -} - -/** - * Orchestrate one ingestion pass — the single shared path used by both the - * scheduler cron and the onModAction trigger. - * - * fetch -> extract (filter/anonymize/normalize) -> dedup gate -> persist - * - * `markSeen` is an atomic NX set (INV-5): it returns `true` only the first - * time a given action id is seen, so concurrent cron/trigger invocations can - * never double-insert. Records that fail extraction (untracked type / ignored - * mod) are still marked seen so we don't re-evaluate them every pass. - * - * Returns the number of NEW records persisted this pass. On a partial failure - * mid-batch we surface the error to the caller (main.ts), which is responsible - * for logging and letting the scheduler retry — there is no daemon loop here. - */ -export async function ingest( - reddit: RedditAPIClient, - redis: RedisClient, - cfg: AppConfig, -): Promise<{ added: number; scanned: number }> { - const actions = await fetchActions(reddit, cfg); - - let added = 0; - for (const action of actions) { - // INV-5 — atomic dedup. Skip anything we've already accounted for. - const isNew = await storage.markSeen(redis, cfg.wikiPage, action.id, cfg.retentionDays); - if (!isNew) { - continue; - } - - const record = extractRecord(action, cfg); - if (record === null) { - // Filtered out (untracked type or ignored mod). It is now marked seen, - // so we won't reconsider it on the next pass. - continue; - } - - await storage.putRecord(redis, cfg.wikiPage, record); - added += 1; - } - - return { added, scanned: actions.length }; -} diff --git a/devvit/src/render.ts b/devvit/src/render.ts deleted file mode 100644 index c7e90ce..0000000 --- a/devvit/src/render.ts +++ /dev/null @@ -1,428 +0,0 @@ -/** - * render.ts — PURE markdown renderer. - * - * Ported from the legacy Python `modlog_wiki_publisher.py`: - * - build_wiki_content -> buildContent / enforceByteCap - * - format_modlog_entry -> renderRow - * - format_content_link -> contentLink - * - generate_modmail_link-> modmailLink - * - censor_email_addresses -> censorEmail - * - sanitize_for_markdown -> escapePipes - * - get_content_hash -> contentHash - * - * INVARIANTS enforced here: - * INV-2 Never link user profiles. Only records carrying a `permalink` - * (post/comment) emit a hyperlink. Profile records have no permalink. - * INV-3 512 KB wiki cap. Content is trimmed oldest-day-first to <= 90% of cap. - * INV-4 Email censor + pipe-escape applied to every cell of free text. - * INV-6 contentHash() exposes the SHA-256 the wiki layer uses to hash-skip. - * - * This module has ZERO Reddit/Redis I/O and ZERO `@devvit/*` runtime imports. - * It is fully deterministic and unit-testable. The only external dependency is - * the Web Crypto API (`crypto.subtle`), which is available in the Devvit - * serverless runtime. - * - * Types are imported from the shared contract module so the renderer never - * redeclares the record/config shapes (coding-style: no duplication). - */ - -import { - type AppConfig, - type ModRecord, - WIKI_BYTE_CAP, - WIKI_TRIM_TARGET, -} from './types.js'; - -// --------------------------------------------------------------------------- -// Constants ported verbatim from the Python source. -// --------------------------------------------------------------------------- - -/** Reddit base URL used to absolutize relative permalinks. */ -const REDDIT_BASE_URL = 'https://www.reddit.com'; - -/** - * Email-detection regex (INV-4). Ported from Python: - * r"\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b" - * The literal `|` inside the final character class is preserved exactly as the - * Python original wrote it (it matches a literal pipe as well as letters — a - * quirk we keep for byte-for-byte output parity). Global flag so every address - * in a multi-address reason is censored. - */ -const EMAIL_REGEX = /\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b/g; - -/** Placeholder substituted for any detected email address (INV-4). */ -const EMAIL_PLACEHOLDER = '[EMAIL]'; - -/** Empty-cell marker matching the Python "-" sentinel. */ -const EMPTY_CELL = '-'; - -/** Max characters of a content title before it is ellipsized in modmail. */ -const MAX_TITLE_LENGTH = 50; - -/** - * Human-readable removal-type labels keyed by action type. Ported from the - * Python `type_map` in generate_modmail_link. - */ -const REMOVAL_TYPE_LABELS: Readonly> = Object.freeze({ - removelink: 'Post', - removepost: 'Post', - removecomment: 'Comment', - spamlink: 'Spam Post', - spamcomment: 'Spam Comment', - removecontent: 'Content', - addremovalreason: 'Removal Reason', -}); - -/** Table header + separator rows. Column order ported verbatim. */ -const TABLE_HEADER = '| Time | Action | ID | Moderator | Content | Reason | Inquire |'; -const TABLE_SEPARATOR = '|------|--------|----|-----------|---------|--------|---------|'; - -/** GitHub-credit footer (P-31), ported verbatim. */ -const FOOTER_LINES: readonly string[] = Object.freeze([ - '---', - '', - '*This modlog is automatically maintained by ' + - '[RedditModLog](https://github.com/bakerboy448/RedditModLog) bot.*', -]); - -// --------------------------------------------------------------------------- -// Pure string transforms (INV-4). -// --------------------------------------------------------------------------- - -/** - * Censor email addresses in free text (INV-4). - * Ported from `censor_email_addresses`. Returns the input unchanged when empty. - */ -export function censorEmail(text: string | undefined | null): string { - if (!text) { - return text ?? ''; - } - // `replace` does not mutate the input string (immutability). - return text.replace(EMAIL_REGEX, EMAIL_PLACEHOLDER); -} - -/** - * Escape pipe characters so reason/title text cannot break a markdown table - * cell (INV-4). Ported from `sanitize_for_markdown` — pipes become spaces. - * `null`/`undefined` collapse to empty string. - */ -export function escapePipes(text: string | undefined | null): string { - if (text === null || text === undefined) { - return ''; - } - return String(text).replace(/\|/g, ' '); -} - -/** Apply both censor + pipe-escape, the full per-cell sanitization (INV-4). */ -function sanitizeCell(text: string | undefined | null): string { - return escapePipes(censorEmail(text ?? '')); -} - -// --------------------------------------------------------------------------- -// Per-record rendering helpers. -// --------------------------------------------------------------------------- - -/** - * Render the "Content" column hyperlink for a record (INV-2). - * - * A hyperlink is emitted ONLY when the record carries a `permalink`. Per the - * ingest layer (modlog.ts), `permalink` is populated solely for post (`t3_`) - * and comment (`t1_`) targets — user/subreddit targets arrive with no - * permalink, so this function can never produce a profile link. Relative - * permalinks are absolutized against the Reddit base URL; already-absolute - * permalinks are used as-is. - * - * The link text is the (sanitized) `displayId` when present, otherwise a - * generic content label. When there is no permalink, the bare display id is - * returned (no link), or the empty-cell marker if even that is absent. - */ -export function contentLink(rec: ModRecord): string { - const label = rec.displayId ? sanitizeCell(rec.displayId) : sanitizeCell('content'); - - if (!rec.permalink) { - // INV-2: no permalink -> never a hyperlink (covers user/subreddit targets). - return rec.displayId ? label : EMPTY_CELL; - } - - const href = rec.permalink.startsWith('http') - ? rec.permalink - : `${REDDIT_BASE_URL}${rec.permalink}`; - - // Markdown link. `href` is a reddit content URL (never a profile, per INV-2). - return `[${label}](${href})`; -} - -/** - * Build the prefilled modmail "removal inquiry" link (P-12). - * Ported from `generate_modmail_link`. - * - * The subject embeds the content id for tracking; the body is a templated - * inquiry. Subject and body are URL-encoded via `encodeURIComponent`, which is - * the JS equivalent of Python's `urllib.parse.quote` for these payloads. - */ -export function modmailLink(sub: string, rec: ModRecord): string { - const removalType = REMOVAL_TYPE_LABELS[rec.actionType] ?? 'Content'; - const contentId = rec.displayId ?? EMPTY_CELL; - - // Title falls back to "Content by u/" then "Unknown content". - let title = rec.targetAuthor - ? `Content by u/${rec.targetAuthor}` - : 'Unknown content'; - if (title.length > MAX_TITLE_LENGTH) { - title = `${title.slice(0, MAX_TITLE_LENGTH - 3)}...`; - } - - // Absolutize the permalink for the body link, if any. - const url = rec.permalink - ? rec.permalink.startsWith('http') - ? rec.permalink - : `${REDDIT_BASE_URL}${rec.permalink}` - : ''; - - const subject = `${removalType} Removal Inquiry - ${title} [ID: ${contentId}]`; - const body = - `Hello Moderators of /r/${sub},\n\n` + - `I would like to inquire about the recent removal of the following ${removalType.toLowerCase()}:\n\n` + - `**Content ID:** ${contentId}\n\n` + - `**Title:** ${title}\n\n` + - `**Action Type:** ${rec.actionType}\n\n` + - `**Link:** ${url}\n\n` + - 'Please provide details regarding this action.\n\n' + - 'Thank you!'; - - const composeUrl = - `${REDDIT_BASE_URL}/message/compose?to=/r/${sub}` + - `&subject=${encodeURIComponent(subject)}` + - `&message=${encodeURIComponent(body)}`; - - return `[Contact Mods](${composeUrl})`; -} - -/** - * Format a record's UTC time-of-day cell (e.g. "14:03:21 UTC"). - * Pure: derived only from the record's epoch-seconds timestamp. - */ -function formatTimeCell(createdAtSec: number): string { - const d = new Date(createdAtSec * 1000); - const hh = String(d.getUTCHours()).padStart(2, '0'); - const mm = String(d.getUTCMinutes()).padStart(2, '0'); - const ss = String(d.getUTCSeconds()).padStart(2, '0'); - return `${hh}:${mm}:${ss} UTC`; -} - -/** Derive the "YYYY-MM-DD" UTC day key used to group rows into tables. */ -function dayKey(createdAtSec: number): string { - return new Date(createdAtSec * 1000).toISOString().slice(0, 10); -} - -/** - * Render a single markdown table row for one record. - * Ported from `format_modlog_entry` + the row-join in `build_wiki_content`. - * - * Note INV-1: `rec.moderator` is ALREADY anonymized at ingest, so the renderer - * simply prints it — a real moderator name can never reach this layer. - */ -export function renderRow(rec: ModRecord, cfg: AppConfig): string { - const time = formatTimeCell(rec.createdAtSec); - const action = sanitizeCell(rec.actionType); - const id = rec.displayId ? sanitizeCell(rec.displayId) : EMPTY_CELL; - const moderator = sanitizeCell(rec.moderator) || 'Unknown'; // INV-1 (pre-anonymized) - const content = contentLink(rec); - const reason = rec.reason ? sanitizeCell(rec.reason) : EMPTY_CELL; - const inquire = modmailLink(cfg.subredditName, rec); - - return `| ${time} | ${action} | ${id} | ${moderator} | ${content} | ${reason} | ${inquire} |`; -} - -// --------------------------------------------------------------------------- -// Day-block assembly + byte-cap trimming (INV-3). -// --------------------------------------------------------------------------- - -/** A rendered table for one calendar day, with its day key for sorting/trim. */ -interface DayBlock { - readonly date: string; // "YYYY-MM-DD" - readonly markdown: string; // full "## date\n\n" block -} - -/** - * Group records by UTC day (newest day first) and render one markdown table - * per day. Records are not mutated; a new array of DayBlocks is produced. - */ -function buildDayBlocks(records: readonly ModRecord[], cfg: AppConfig): DayBlock[] { - const byDate = new Map(); - for (const rec of records) { - const key = dayKey(rec.createdAtSec); - const bucket = byDate.get(key); - if (bucket) { - bucket.push(rec); - } else { - byDate.set(key, [rec]); - } - } - - // Dates newest-first; within a day, rows newest-first (matches Python sort). - const sortedDates = [...byDate.keys()].sort((a, b) => (a < b ? 1 : a > b ? -1 : 0)); - - return sortedDates.map((date) => { - const rows = [...byDate.get(date)!].sort((a, b) => b.createdAtSec - a.createdAtSec); - const lines = [`## ${date}`, TABLE_HEADER, TABLE_SEPARATOR]; - for (const rec of rows) { - lines.push(renderRow(rec, cfg)); - } - lines.push(''); // trailing blank line between day tables - return { date, markdown: lines.join('\n') }; - }); -} - -/** UTF-8 byte length of a string (Devvit runtime provides TextEncoder). */ -function utf8ByteLength(s: string): number { - return new TextEncoder().encode(s).length; -} - -/** - * Assemble header + day blocks + footer, enforcing the 512 KB cap (INV-3). - * - * Mirrors the Python trimming loop: day blocks are added newest-first while the - * running UTF-8 size stays at/under the trim target (90% of cap). The first day - * that would push past the target stops inclusion, and a "trimmed" notice is - * inserted. The header + footer bytes are pre-counted so the final document is - * guaranteed <= WIKI_BYTE_CAP. - */ -export function enforceByteCap( - header: string, - dayBlocks: readonly DayBlock[], - footerLines: readonly string[], -): string { - const footer = footerLines.join('\n'); - - // Pre-count header + footer so day inclusion respects the real budget. - let runningSize = utf8ByteLength(`${header}\n${footer}`); - - const includedParts: string[] = [header]; - let skippedDays = 0; - let lastIncludedDate: string | null = null; - - for (let i = 0; i < dayBlocks.length; i++) { - const block = dayBlocks[i]; - const testSize = runningSize + utf8ByteLength(block.markdown); - - if (testSize > WIKI_TRIM_TARGET) { - // Stop here; everything from i onward is trimmed (oldest-day-first). - skippedDays = dayBlocks.length - i; - break; - } - - includedParts.push(block.markdown); - lastIncludedDate = block.date; - runningSize = testSize; - } - - if (skippedDays > 0) { - const fromDate = lastIncludedDate ?? 'today'; - includedParts.push( - `\n**Note:** ${skippedDays} older day(s) trimmed due to wiki size limits.`, - ); - includedParts.push(`Only showing entries from ${fromDate} onwards.\n`); - } - - includedParts.push(...footerLines); - - let result = includedParts.join('\n'); - - // Defensive hard guard: even after day-trimming, a single oversized day plus - // header/footer could (pathologically) exceed the absolute cap. Truncate on a - // UTF-8 boundary so we never hand the wiki layer an over-cap document (INV-3). - if (utf8ByteLength(result) > WIKI_BYTE_CAP) { - result = truncateToBytes(result, WIKI_BYTE_CAP); - } - - return result; -} - -/** - * Truncate a string so its UTF-8 encoding is at most `maxBytes`, never - * splitting a multi-byte character. Used only as the defensive last-resort - * guard in enforceByteCap. - */ -function truncateToBytes(s: string, maxBytes: number): string { - const encoder = new TextEncoder(); - if (encoder.encode(s).length <= maxBytes) { - return s; - } - // Binary search the largest prefix that fits. - let lo = 0; - let hi = s.length; - while (lo < hi) { - const mid = Math.ceil((lo + hi) / 2); - if (encoder.encode(s.slice(0, mid)).length <= maxBytes) { - lo = mid; - } else { - hi = mid - 1; - } - } - return s.slice(0, lo); -} - -// --------------------------------------------------------------------------- -// Top-level entry point. -// --------------------------------------------------------------------------- - -/** - * Build the full wiki markdown document from render-ready records (P-10/P-31). - * Ported from `build_wiki_content`. - * - * @param records Render-ready, already-anonymized records (INV-1 satisfied - * upstream). Order is irrelevant; grouped/sorted here. - * @param cfg Resolved app config (subreddit name, max entries, etc.). - * @param nowIso Caller-supplied timestamp string for the "Last Updated" - * header. Passing it in (rather than reading the clock here) - * keeps this function pure/deterministic for tests. - * @returns Capped markdown ready for the wiki layer. - */ -export function buildContent( - records: readonly ModRecord[], - cfg: AppConfig, - nowIso: string, -): string { - const header = `**Last Updated:** ${nowIso}\n\n---\n`; - - if (records.length === 0) { - return `${header}\nNo recent moderation actions found.`; - } - - // Cap to the newest `maxWikiEntries` records before rendering (P-10). - // Records are sorted newest-first so the cap keeps the most recent entries. - const ordered = [...records].sort((a, b) => b.createdAtSec - a.createdAtSec); - const limited = - cfg.maxWikiEntries > 0 && ordered.length > cfg.maxWikiEntries - ? ordered.slice(0, cfg.maxWikiEntries) - : ordered; - - const dayBlocks = buildDayBlocks(limited, cfg); - return enforceByteCap(header, dayBlocks, FOOTER_LINES); -} - -// --------------------------------------------------------------------------- -// Content hashing (INV-6). -// --------------------------------------------------------------------------- - -/** - * SHA-256 hex digest of the rendered markdown (INV-6). Async because Web - * Crypto's `subtle.digest` returns a Promise. The wiki layer compares this - * against the last-written hash to skip no-op writes. - * - * Ported from `get_content_hash` (Python used hashlib.sha256 hexdigest). - */ -export async function contentHash(markdown: string): Promise { - // Exclude the volatile "**Last Updated:** " header line so an - // unchanged modlog hashes stably (INV-6). Hashing the timestamped content - // would differ every run, defeating the skip and rewriting the wiki each - // cycle. Mirrors the Python get_content_hash fix. - const hashable = markdown.replace(/^\*\*Last Updated:\*\* .*\r?\n/, ''); - const data = new TextEncoder().encode(hashable); - // crypto.subtle is available globally in the Devvit serverless runtime. - const digest = await crypto.subtle.digest('SHA-256', data); - return [...new Uint8Array(digest)] - .map((b) => b.toString(16).padStart(2, '0')) - .join(''); -} diff --git a/devvit/src/server/index.ts b/devvit/src/server/index.ts new file mode 100644 index 0000000..f0384d9 --- /dev/null +++ b/devvit/src/server/index.ts @@ -0,0 +1,102 @@ +/** + * Devvit Web server. Every endpoint is internal: the scheduler, menu items and + * settings validators declared in devvit.json POST here. + */ +import { serve } from '@hono/node-server'; +import { context, createServer, getServerPort, redis, settings } from '@devvit/web/server'; +import type { TaskResponse } from '@devvit/web/server'; +import type { MenuItemRequest, SettingsValidationRequest, SettingsValidationResponse, UiResponse } from '@devvit/web/shared'; +import { Hono } from 'hono'; + +import { describeResult, runCycle, type CycleMode } from '../core/publish.js'; +import { + loadConfig, + validateAnonymize, + validateDays, + validateIgnoredModerators, + validateWikiActions, + validateWikiPage, + type Validator, +} from '../core/settings.js'; +import { Store } from '../core/store.js'; +import { devvitReddit } from './reddit.js'; + +const app = new Hono(); +const store = new Store(redis); + +async function currentConfig() { + return loadConfig(await settings.getAll>(), context.subredditName); +} + +const errorText = (err: unknown): string => (err instanceof Error ? err.message : String(err)); + +app.post('/internal/cron/publish', async (c) => { + try { + const cfg = await currentConfig(); + const result = await runCycle(devvitReddit, store, cfg, 'scheduled'); + console.log(`cron publish r/${cfg.subredditName}: ${describeResult(result, cfg.wikiPage)} pruned=${result.pruned}`); + } catch (err) { + console.error(`cron publish failed: ${errorText(err)}`); + } + return c.json({}, 200); +}); + +async function menuCycle(mode: CycleMode): Promise { + try { + const cfg = await currentConfig(); + const result = await runCycle(devvitReddit, store, cfg, mode); + console.log(`${mode} r/${cfg.subredditName}: ${describeResult(result, cfg.wikiPage)}`); + return { showToast: { text: describeResult(result, cfg.wikiPage), appearance: result.status === 'busy' ? 'neutral' : 'success' } }; + } catch (err) { + console.error(`${mode} failed: ${errorText(err)}`); + return { showToast: { text: `Modlog ${mode} failed: ${errorText(err)}`, appearance: 'neutral' } }; + } +} + +app.post('/internal/menu/publish-now', async (c) => { + await c.req.json(); + return c.json(await menuCycle('publish-now')); +}); + +app.post('/internal/menu/force-rebuild', async (c) => { + await c.req.json(); + return c.json(await menuCycle('force-rebuild')); +}); + +app.post('/internal/menu/test-connection', async (c) => { + await c.req.json(); + const checks: string[] = []; + try { + const cfg = await currentConfig(); + checks.push(`settings ok (wiki/${cfg.wikiPage}, ${cfg.displayDays}d shown, ${cfg.retentionDays}d kept)`); + const log = await devvitReddit.fetchModLog(cfg.subredditName, 1); + checks.push(`mod log readable (${log.length} entry)`); + const page = await devvitReddit.readWiki(cfg.subredditName, cfg.wikiPage); + checks.push(page === null ? 'wiki page not created yet' : `wiki page exists (${Buffer.byteLength(page, 'utf8')} bytes)`); + checks.push(`${await store.storedCount()} actions stored`); + const status = await store.getStatus(); + if (status.lastRunAt) checks.push(`last run ${status.lastRunAt} (${status.lastResult ?? 'unknown'})`); + console.log(`test-connection: ${checks.join('; ')}`); + return c.json({ showToast: { text: checks.join('; '), appearance: 'success' } }); + } catch (err) { + checks.push(`FAILED: ${errorText(err)}`); + console.error(`test-connection: ${checks.join('; ')}`); + return c.json({ showToast: { text: checks.join('; '), appearance: 'neutral' } }); + } +}); + +function validationRoute(path: string, validate: Validator): void { + app.post(path, async (c) => { + const { value } = await c.req.json>(); + const error = validate(value); + return c.json(error ? { success: false, error } : { success: true }); + }); +} + +validationRoute('/internal/settings/validate-wiki-page', validateWikiPage); +validationRoute('/internal/settings/validate-days', validateDays); +validationRoute('/internal/settings/validate-actions', validateWikiActions); +validationRoute('/internal/settings/validate-anonymize', validateAnonymize); +validationRoute('/internal/settings/validate-ignored', validateIgnoredModerators); + +serve({ fetch: app.fetch, createServer, port: getServerPort() }); diff --git a/devvit/src/server/reddit.ts b/devvit/src/server/reddit.ts new file mode 100644 index 0000000..233ead5 --- /dev/null +++ b/devvit/src/server/reddit.ts @@ -0,0 +1,37 @@ +/** Production RedditPort backed by the Devvit Reddit client. */ +import { reddit } from '@devvit/web/server'; + +import { FETCH_PAGE_SIZE, WIKI_EDIT_REASON } from '../core/constants.js'; +import type { RedditPort } from '../core/publish.js'; +import type { ModLogEntry } from '../core/types.js'; + +export const devvitReddit: RedditPort = { + async fetchModLog(subredditName: string, limit: number): Promise { + return reddit.getModerationLog({ subredditName, limit, pageSize: FETCH_PAGE_SIZE }).all(); + }, + + async readWiki(subredditName: string, page: string): Promise { + try { + const wiki = await reddit.getWikiPage(subredditName, page); + return wiki.content; + } catch (err) { + // A missing page throws; any other read failure surfaces again on write. + console.log(`wiki read r/${subredditName}/wiki/${page} failed, treating as absent: ${String(err)}`); + return null; + } + }, + + async writeWiki(subredditName: string, page: string, content: string, exists: boolean): Promise { + const options = { subredditName, page, content, reason: WIKI_EDIT_REASON }; + if (exists) { + await reddit.updateWikiPage(options); + return; + } + try { + await reddit.createWikiPage(options); + } catch (err) { + console.log(`createWikiPage failed (${String(err)}), retrying as update`); + await reddit.updateWikiPage(options); + } + }, +}; diff --git a/devvit/src/settings.ts b/devvit/src/settings.ts deleted file mode 100644 index c72d68d..0000000 --- a/devvit/src/settings.ts +++ /dev/null @@ -1,387 +0,0 @@ -/** - * settings.ts — Devvit settings schema + typed config resolver. - * - * Maps the 19 Python config options (CLI / env / JSON in the legacy - * `modlog_wiki_publisher.py`) onto Devvit install-scope (per-subreddit) and - * app-scope settings, then resolves them into a single frozen `AppConfig`. - * - * Binding invariants carried verbatim from the Python app: - * - INV-1 anonymize_moderators is ALWAYS true. It is NOT a user-facing - * setting here (the Python app refused to start when false). It is - * hardcoded below and surfaced read-only on AppConfig. - * - INV-7 default tracked actions = the 7 removal/approval/reason actions. - * - INV-8 default ignored moderators = [AutoModerator]. - * - INV-9 one install == one subreddit. The subreddit name comes from the - * install context, never from a user setting, so cross-subreddit - * mixing is structurally impossible. - * - * Settings API shape verified against reddit/devvit reference apps - * (devvit-sandbox/modlog-archive, devvit-docs app-configurations.md): - * - Devvit.addSettings([ ...fields ]) with field.type in - * 'string' | 'boolean' | 'number' | 'select' | 'paragraph' | 'group' - * - per-field `scope: SettingScope.Installation | SettingScope.App` - * - per-field `onValidate: async ({ value }) => string | void` - * (return a string to REJECT at save time, void/undefined to accept) - * - read at runtime via `context.settings.get(name)` / - * `settings.get(name)`. - * - * @module settings - */ - -import { Devvit, SettingScope } from '@devvit/public-api'; - -import { - ANONYMIZE_MODERATORS, - DEFAULT_FETCH_LIMIT, - DEFAULT_IGNORED_MODS, - DEFAULT_MAX_WIKI_ENTRIES, - DEFAULT_RETENTION_DAYS, - DEFAULT_WIKI_ACTIONS, - DEFAULT_WIKI_PAGE, - FETCH_LIMIT_MAX, - FETCH_LIMIT_MIN, - MAX_WIKI_ENTRIES_MAX, - MAX_WIKI_ENTRIES_MIN, - RETENTION_DAYS_MAX, - RETENTION_DAYS_MIN, - VALID_MODLOG_ACTIONS, - type AppConfig, - type ModActionType, -} from './types.js'; - -// --------------------------------------------------------------------------- -// Setting keys (single source of truth — used by both the schema and loader) -// --------------------------------------------------------------------------- - -/** - * Stable setting names. Changing a value here is a data migration (Devvit keys - * the stored value by name), so treat these as frozen identifiers. - */ -export const SETTING_KEYS = { - /** Wiki page slug to publish to. (Python: `wiki_page`, env WIKI_PAGE) */ - wikiPage: 'wikiPage', - /** multiSelect of tracked modlog action types. (Python: `wiki_actions`) */ - wikiActions: 'wikiActions', - /** Comma-separated extra mods to ignore. (Python: `ignored_moderators`) */ - ignoredModerators: 'ignoredModerators', - /** Retention window in days. (Python: `retention_days`, 1..365) */ - retentionDays: 'retentionDays', - /** Max entries rendered per wiki page. (Python: `max_wiki_entries_per_page`) */ - maxWikiEntries: 'maxWikiEntries', - /** Explicit modlog fetch limit per run. (Python: `batch_size`) */ - fetchLimit: 'fetchLimit', -} as const; - -// --------------------------------------------------------------------------- -// Mapping of the 19 legacy Python options -// --------------------------------------------------------------------------- -// -// Legacy option -> Devvit disposition -// -------------------------------------------------------------------------- -// 1 reddit.client_id -> DROP (Devvit owns OAuth; no app credential) -// 2 reddit.client_secret -> DROP (same) -// 3 reddit.username -> DROP (runs as the app account) -// 4 reddit.password -> DROP (no password grant on Devvit) -// 5 source_subreddit -> DROP as a setting; from install context (INV-9) -// 6 wiki_page -> Installation setting `wikiPage` -// 7 retention_days -> Installation setting `retentionDays` (clamped) -// 8 batch_size -> Installation setting `fetchLimit` (clamped) -// 9 update_interval -> DROP (Scheduler cron in devvit.json, not a setting) -// 10 max_wiki_entries_per_page -> Installation setting `maxWikiEntries` (clamped) -// 11 wiki_display_days -> DROP (collapsed into retentionDays; legacy -// constraint was display_days <= retention_days, -// so a single window is sufficient) -// 12 max_continuous_errors -> DROP (no daemon loop; serverless per-invoke) -// 13 rate_limit_buffer -> DROP (platform-managed rate limiting) -// 14 max_batch_retries -> DROP (Scheduler provides retry semantics) -// 15 archive_threshold_days -> DROP (no SQLite archive table; Redis TTL/ZSET) -// 16 anonymize_moderators -> HARDCODED true (INV-1); NOT a setting -// 17 ignored_moderators -> Installation setting `ignoredModerators` -// 18 wiki_actions -> Installation setting `wikiActions` (multiSelect) -// 19 database_path / display_format -> DROP (Redis storage; fixed render format) -// -// Net: 6 of 19 surface as Devvit settings; 1 is hardcoded; 12 are obsolete on -// the Devvit platform. All preserved options keep their Python defaults/ranges. - -// --------------------------------------------------------------------------- -// Settings schema -// --------------------------------------------------------------------------- - -/** - * Build the multiSelect option list for tracked action types from the canonical - * VALID_MODLOG_ACTIONS list so the form and validation never drift. - */ -const WIKI_ACTION_OPTIONS = VALID_MODLOG_ACTIONS.map((action) => ({ - label: action, - value: action, -})); - -/** - * Register the app's configuration form. Moderators edit these on the per-install - * Settings page; all are `SettingScope.Installation` so each subreddit is - * configured independently (reinforces INV-9). There is intentionally NO - * anonymize-moderators toggle (INV-1) and NO Reddit-credential fields (Devvit - * owns auth). - * - * Call once at module load from `main.ts`. - */ -export function registerSettings(): void { - Devvit.addSettings([ - { - type: 'string', - name: SETTING_KEYS.wikiPage, - label: 'Wiki page name to publish the mod log to (e.g. "modlog")', - scope: SettingScope.Installation, - defaultValue: DEFAULT_WIKI_PAGE, - onValidate: ({ value }) => validateWikiPage(value), - }, - { - type: 'select', - name: SETTING_KEYS.wikiActions, - label: 'Mod-log action types to publish', - scope: SettingScope.Installation, - multiSelect: true, - options: WIKI_ACTION_OPTIONS, - // Devvit `select` defaults are an array of option values. - defaultValue: [...DEFAULT_WIKI_ACTIONS], - onValidate: ({ value }) => validateWikiActions(value), - }, - { - type: 'string', - name: SETTING_KEYS.ignoredModerators, - label: - 'Additional moderators to exclude (comma-separated usernames). ' + - 'AutoModerator is always excluded.', - scope: SettingScope.Installation, - // Stored as a CSV string; AutoModerator is merged in by the loader. - defaultValue: '', - }, - { - type: 'number', - name: SETTING_KEYS.retentionDays, - label: `Days of history to keep (${RETENTION_DAYS_MIN}-${RETENTION_DAYS_MAX})`, - scope: SettingScope.Installation, - defaultValue: DEFAULT_RETENTION_DAYS, - onValidate: ({ value }) => - validateNumberRange(value, RETENTION_DAYS_MIN, RETENTION_DAYS_MAX, 'Retention days'), - }, - { - type: 'number', - name: SETTING_KEYS.maxWikiEntries, - label: `Maximum entries to render on the wiki page (${MAX_WIKI_ENTRIES_MIN}-${MAX_WIKI_ENTRIES_MAX})`, - scope: SettingScope.Installation, - defaultValue: DEFAULT_MAX_WIKI_ENTRIES, - onValidate: ({ value }) => - validateNumberRange(value, MAX_WIKI_ENTRIES_MIN, MAX_WIKI_ENTRIES_MAX, 'Max wiki entries'), - }, - { - type: 'number', - name: SETTING_KEYS.fetchLimit, - label: `Mod-log entries to fetch per run (${FETCH_LIMIT_MIN}-${FETCH_LIMIT_MAX})`, - scope: SettingScope.Installation, - defaultValue: DEFAULT_FETCH_LIMIT, - onValidate: ({ value }) => - validateNumberRange(value, FETCH_LIMIT_MIN, FETCH_LIMIT_MAX, 'Fetch limit'), - }, - ]); -} - -// --------------------------------------------------------------------------- -// Validation handlers (return a string to REJECT, void to accept) -// --------------------------------------------------------------------------- - -/** Reddit wiki slugs: lowercase letters, digits, and `/_-` separators. */ -const WIKI_PAGE_RE = /^[a-z0-9][a-z0-9/_-]*$/; - -/** - * Reject empty/malformed wiki page slugs. Returns an error string on failure, - * void on success (Devvit `onValidate` contract). - */ -export function validateWikiPage(value: string | undefined): string | void { - const slug = (value ?? '').trim(); - if (slug.length === 0) { - return 'Wiki page name is required.'; - } - if (slug.length > 256) { - return 'Wiki page name is too long (max 256 characters).'; - } - if (!WIKI_PAGE_RE.test(slug)) { - return 'Use lowercase letters, numbers, and / _ - only (e.g. "modlog" or "logs/modlog").'; - } -} - -/** - * Reject unknown / empty action selections. The multiSelect already constrains - * choices to VALID_MODLOG_ACTIONS, but we defend against an empty selection - * (which would publish nothing) and any out-of-set value. - */ -export function validateWikiActions(value: string[] | undefined): string | void { - const selected = value ?? []; - if (selected.length === 0) { - return 'Select at least one action type to publish.'; - } - const unknown = selected.filter( - (action) => !VALID_MODLOG_ACTIONS.includes(action as ModActionType), - ); - if (unknown.length > 0) { - return `Unknown action type(s): ${unknown.join(', ')}.`; - } -} - -/** - * Inclusive numeric range validator shared by retention/maxEntries/fetchLimit. - * Rejects non-finite, non-integer, and out-of-range values. - */ -export function validateNumberRange( - value: number | undefined, - min: number, - max: number, - label: string, -): string | void { - if (value === undefined || value === null || !Number.isFinite(value)) { - return `${label} is required and must be a number.`; - } - if (!Number.isInteger(value)) { - return `${label} must be a whole number.`; - } - if (value < min || value > max) { - return `${label} must be between ${min} and ${max}.`; - } -} - -// --------------------------------------------------------------------------- -// Config resolution -// --------------------------------------------------------------------------- - -/** - * Minimal structural shape of the Devvit settings reader. Both - * `context.settings` and the destructured `settings` handler arg satisfy this, - * so the loader is decoupled from how the caller obtained the handle. - */ -export interface SettingsReader { - get(name: string): Promise; -} - -/** - * Clamp a value into [min, max]. Validation already rejects out-of-range input - * at save time, but app-scope defaults or legacy stored values could still fall - * outside the band — clamp defensively and log when we do (parity with the - * Python `validate_config_value` warn-and-clamp behavior). - */ -function clamp(value: number, min: number, max: number, label: string): number { - if (value < min) { - console.warn(`[settings] ${label}=${value} below min ${min}; clamping to ${min}`); - return min; - } - if (value > max) { - console.warn(`[settings] ${label}=${value} above max ${max}; clamping to ${max}`); - return max; - } - return value; -} - -/** - * Coerce a possibly-undefined number setting to a finite integer, falling back - * to `fallback` when absent or invalid. - */ -function asInt(value: number | undefined, fallback: number): number { - if (value === undefined || value === null || !Number.isFinite(value)) { - return fallback; - } - return Math.trunc(value); -} - -/** - * Parse the comma-separated `ignoredModerators` CSV into a normalized list and - * ALWAYS union in the default ignored mods (INV-8: AutoModerator is - * non-removable). Usernames are lowercased for case-insensitive comparison at - * the filter site, deduped, and stripped of an optional `u/` prefix. - */ -function parseIgnoredModerators(csv: string | undefined): string[] { - const fromUser = (csv ?? '') - .split(',') - .map((name) => name.trim().replace(/^\/?u\//i, '')) - .filter((name) => name.length > 0); - - const merged = [...DEFAULT_IGNORED_MODS, ...fromUser].map((name) => name.toLowerCase()); - return Array.from(new Set(merged)); -} - -/** - * Validate the stored action selection against the canonical list, dropping any - * unknown values (P-22). Falls back to the INV-7 defaults if nothing valid - * remains, so the publisher never silently produces an empty page from a bad - * stored value. - */ -function parseWikiActions(value: string[] | undefined): ModActionType[] { - const selected = value ?? []; - const valid = selected.filter((action): action is ModActionType => - VALID_MODLOG_ACTIONS.includes(action as ModActionType), - ); - const dropped = selected.length - valid.length; - if (dropped > 0) { - console.warn(`[settings] dropped ${dropped} unknown wikiActions value(s)`); - } - return valid.length > 0 ? valid : [...DEFAULT_WIKI_ACTIONS]; -} - -/** - * Resolve all settings into a single validated, clamped, frozen `AppConfig`. - * - * @param settings A Devvit settings reader (`context.settings` or the - * destructured handler `settings` arg). - * @param subredditName The install's subreddit, from context (INV-9) — never - * a user setting. - * @returns A deeply-frozen `AppConfig`. `anonymizeModerators` is always true - * (INV-1) and is not derived from any setting. - */ -export async function loadConfig( - settings: SettingsReader, - subredditName: string, -): Promise { - // Read every setting up front; unrelated reads have no dependencies. - const [wikiPageRaw, wikiActionsRaw, ignoredRaw, retentionRaw, maxEntriesRaw, fetchLimitRaw] = - await Promise.all([ - settings.get(SETTING_KEYS.wikiPage), - settings.get(SETTING_KEYS.wikiActions), - settings.get(SETTING_KEYS.ignoredModerators), - settings.get(SETTING_KEYS.retentionDays), - settings.get(SETTING_KEYS.maxWikiEntries), - settings.get(SETTING_KEYS.fetchLimit), - ]); - - const wikiPage = (wikiPageRaw ?? '').trim() || DEFAULT_WIKI_PAGE; - - const config: AppConfig = { - subredditName, - wikiPage, - wikiActions: parseWikiActions(wikiActionsRaw), - ignoredModerators: parseIgnoredModerators(ignoredRaw), - retentionDays: clamp( - asInt(retentionRaw, DEFAULT_RETENTION_DAYS), - RETENTION_DAYS_MIN, - RETENTION_DAYS_MAX, - 'retentionDays', - ), - maxWikiEntries: clamp( - asInt(maxEntriesRaw, DEFAULT_MAX_WIKI_ENTRIES), - MAX_WIKI_ENTRIES_MIN, - MAX_WIKI_ENTRIES_MAX, - 'maxWikiEntries', - ), - fetchLimit: clamp( - asInt(fetchLimitRaw, DEFAULT_FETCH_LIMIT), - FETCH_LIMIT_MIN, - FETCH_LIMIT_MAX, - 'fetchLimit', - ), - // INV-1: anonymization is mandatory and not user-configurable. - anonymizeModerators: ANONYMIZE_MODERATORS, - }; - - // Immutability (coding-style rule): hand callers a frozen snapshot. The - // nested arrays are frozen too so no consumer can mutate shared config state. - Object.freeze(config.wikiActions); - Object.freeze(config.ignoredModerators); - return Object.freeze(config); -} diff --git a/devvit/src/storage.ts b/devvit/src/storage.ts deleted file mode 100644 index f592bcd..0000000 --- a/devvit/src/storage.ts +++ /dev/null @@ -1,406 +0,0 @@ -/** - * storage.ts — Redis data-access layer (Repository pattern). - * - * This is the ONLY module that talks to Redis. Every other module depends on - * the exported async functions here, never on raw Redis commands (patterns.md - * Repository pattern). It encapsulates four concerns: - * - * 1. Dedup of processed ModAction ids (INV-5) - * 2. The render-ready action-record collection (rebuild source) - * 3. Retention via a sorted-set keyed by created_at secs (INV-9 / retention) - * 4. The wiki content-hash cache (INV-6) - * - * Per-subreddit isolation (INV-9) is structural: Devvit name-spaces every app - * INSTALLATION's Redis data by subreddit, so there is NO `subreddit` column and - * cross-subreddit mixing is impossible. Key prefixes below are therefore scoped - * only by `wikiPage`, allowing multiple wiki pages per install without collision. - * - * No KEYS / global SCAN is used anywhere (Devvit forbids it) — every collection - * is rooted at an explicit, known key. - * - * Devvit Redis API notes (verified against reddit/devvit - * packages/redis/src/RedisClient.ts): - * - `set(key, value, { nx, expiration })` — `expiration` is a Date, and the - * return value is the stored string, NOT a NX-success boolean. So we do NOT - * use `set` for atomic dedup-newness detection. - * - `hSetNX(key, field, value)` returns 1 when the field was newly created and - * 0 when it already existed — this IS race-free and is our dedup primitive. - * - `zAdd(key, ...members)` takes spread `ZMember[]` (not an array). - * - `zRange(key, start, stop, { by:'score' })` returns `{member,score}[]` and - * internally caps at 1000 members per call (we loop to drain). - * - `zRemRangeByScore(key, min, max)` prunes by score window. - * - `hGetAll` returns `Record`; `hDel(key, fields[])`. - */ - -// The prompt mandates importing from '@devvit/public-api', which re-exports the -// Redis types. `RedisClient` is the structural type of the `redis` handle that -// callers obtain from their Devvit context / `@devvit/redis`. -import type { RedisClient } from '@devvit/public-api'; - -// --------------------------------------------------------------------------- -// Record type — the persisted, render-ready shape of one moderation action. -// --------------------------------------------------------------------------- - -/** P/C/U/A display-prefix kind derived from the target fullname. */ -export type DisplayKind = 'P' | 'C' | 'U' | 'A'; - -/** - * One moderation-log action, stored in render-ready form. Enough is persisted - * to rebuild the entire wiki page from Redis alone (no re-fetch required). - * - * INVARIANTS baked into this shape: - * - `moderator` is ALREADY anonymized at ingest (INV-1) — a real moderator - * name must NEVER reach Redis. Only `HumanModerator`, `AutoModerator`, or - * `Reddit` are valid here. - * - `permalink` is present ONLY for posts/comments (INV-2). For user/subreddit - * targets it is `undefined` so the render layer cannot emit a profile link. - * - `reason` is stored RAW; email-censor + pipe-escape (INV-4) are applied at - * render time, keeping storage idempotent and the censor regex evolvable - * without a data migration. - */ -export interface ModActionRecord { - /** ModAction.id — the dedup key (INV-5). Also the hash field + zset member. */ - id: string; - /** Epoch SECONDS of the action's creation; the sorted-set score (retention). */ - createdAtSec: number; - /** The tracked action type (e.g. 'removelink'); already filtered to INV-7. */ - actionType: string; - /** ALREADY-anonymized moderator label (INV-1) — never a real name. */ - moderator: string; - /** Target fullname (t3_/t1_/t2_/t5_...), if any. */ - targetId?: string; - /** P (post) | C (comment) | U (user) | A (any/other). */ - displayKind: DisplayKind; - /** Short, prefixed display id (e.g. 'P:abc123'), if a target exists. */ - displayId?: string; - /** Post/comment permalink ONLY (INV-2); undefined for profiles/subreddits. */ - permalink?: string; - /** Target author handle, for the modmail removal-inquiry prefill. */ - targetAuthor?: string; - /** RAW reason text; sanitized at render (INV-4). */ - reason?: string; -} - -// --------------------------------------------------------------------------- -// Key layout. All keys are scoped by wiki page; the install is already scoped -// by subreddit (INV-9), so no subreddit segment is needed. -// --------------------------------------------------------------------------- - -/** Hash of seen action ids — field=actionId, value='1'. Powers atomic dedup. */ -const seenKey = (page: string): string => `seen:${page}`; -/** Hash of full records — field=actionId, value=JSON(ModActionRecord). */ -const actionsKey = (page: string): string => `actions:${page}`; -/** Sorted set: member=actionId, score=createdAtSec. Drives retention pruning. */ -const timeIndexKey = (page: string): string => `actions_by_time:${page}`; -/** Hash of wiki content hashes — field=page, value=sha256hex. */ -const WIKI_HASH_KEY = 'wiki_hash'; - -/** zRange returns at most this many members per call; drain loops honor it. */ -const ZRANGE_PAGE_LIMIT = 1000; - -// --------------------------------------------------------------------------- -// 1. Dedup (INV-5) -// --------------------------------------------------------------------------- - -/** - * Atomically test-and-mark an action id as processed. - * - * Returns `true` if this id had NOT been seen before (caller SHOULD process and - * persist it), or `false` if it was already processed (caller skips). This is - * race-free: `hSetNX` only sets — and returns truthy — when the field is new, - * so two concurrent invocations (cron + onModAction trigger) can never both - * believe they "won" the same id. - * - * Naming: exported as `isProcessed` per the module contract. Note the inverted - * sense — it returns whether the action is NEW (not-yet-processed). It also - * has the side effect of MARKING the id, by design, so the dedup decision and - * the claim are a single atomic step. - * - * @param redis Devvit redis handle (subreddit-scoped by the platform). - * @param page Wiki page these actions belong to. - * @param actionId The ModAction.id to test-and-claim. - * @returns `true` when newly claimed (process it); `false` when duplicate. - */ -export async function isProcessed( - redis: RedisClient, - page: string, - actionId: string, -): Promise { - // hSetNX => 1 (truthy) when newly created, 0 when the field already existed. - const created = await redis.hSetNX(seenKey(page), actionId, '1'); - return !created; -} - -// --------------------------------------------------------------------------- -// 2. Record collection (rebuild source) -// --------------------------------------------------------------------------- - -/** - * Persist one render-ready record. Writes the full record into the `actions` - * hash AND indexes it in the time-ordered sorted set so retention pruning and - * "newest N" reads both work. Re-recording the same id is idempotent (hash - * field + zset member are overwritten in place), which lets force-rebuild - * re-ingest without creating duplicates. - * - * NOTE: the two writes are not wrapped in a transaction. With the cron job as - * the normal single writer the worst-case interleave (an entry in the hash but - * not yet the zset, or vice versa) is self-healing on the next run, and a - * partially-indexed record still renders. If the onModAction trigger and cron - * are later allowed to write concurrently, wrap these in `watch/multi/exec`. - * - * @param redis Devvit redis handle. - * @param page Wiki page scope. - * @param record The fully-extracted, ALREADY-anonymized record (INV-1). - */ -export async function recordAction( - redis: RedisClient, - page: string, - record: ModActionRecord, -): Promise { - await redis.hSet(actionsKey(page), { [record.id]: JSON.stringify(record) }); - await redis.zAdd(timeIndexKey(page), { - member: record.id, - score: record.createdAtSec, - }); -} - -/** - * Return the most-recent records, newest first, capped to `maxEntries`. - * - * Reads ids newest-first from the time index (reverse score order), then - * batch-resolves their full records from the `actions` hash. Records that are - * missing or fail to parse are skipped defensively (never throws on bad data). - * - * @param redis Devvit redis handle. - * @param page Wiki page scope. - * @param maxEntries Upper bound on returned records (e.g. AppConfig.maxWikiEntries). - * @returns Up to `maxEntries` records, sorted createdAtSec descending. - */ -export async function getRecentActions( - redis: RedisClient, - page: string, - maxEntries: number, -): Promise { - if (maxEntries <= 0) return []; - - // Pull the newest ids from the sorted set. `reverse: true` gives high scores - // (most recent) first; we only need the newest `maxEntries`. - // Full score window (-inf..+inf) expressed as ±Infinity bounds. - const indexed = await redis.zRange(timeIndexKey(page), -Infinity, +Infinity, { - by: 'score', - reverse: true, - limit: { offset: 0, count: maxEntries }, - }); - if (indexed.length === 0) return []; - - // Resolve full records. hGetAll is a single round-trip; we then look up by id - // preserving the time-index ordering (which is authoritative for "newest"). - const all = await redis.hGetAll(actionsKey(page)); - if (!all) return []; - - const records: ModActionRecord[] = []; - for (const { member } of indexed) { - const raw = all[member]; - if (!raw) continue; // record evicted/missing — skip rather than fail. - try { - records.push(JSON.parse(raw) as ModActionRecord); - } catch { - // Corrupt JSON for one record must not break the whole rebuild. - continue; - } - } - return records; -} - -// --------------------------------------------------------------------------- -// 3. Retention (INV-9 / time-based pruning) -// --------------------------------------------------------------------------- - -/** - * Delete every record older than `retentionDays` relative to `nowSec`. - * - * Works off the sorted-set time index: it pages out members whose score is at - * or below the cutoff (honoring the 1000-member-per-zRange limit by looping), - * removes them from the `actions` hash, the `seen` dedup hash, and finally - * trims the time index by score. Returns the total number of records removed. - * - * The `seen` dedup field is also pruned so an old, since-deleted action can be - * re-recorded if it somehow reappears; this bounds the dedup hash to roughly - * the retention window rather than growing unbounded. - * - * @param redis Devvit redis handle. - * @param page Wiki page scope. - * @param retentionDays Days to keep (e.g. AppConfig.retentionDays, clamped 1..365). - * @param nowSec Current epoch seconds (injected for testability). - * @returns Count of records removed. - */ -export async function cleanupOld( - redis: RedisClient, - page: string, - retentionDays: number, - nowSec: number, -): Promise { - const cutoffSec = nowSec - retentionDays * 86_400; - if (cutoffSec <= 0) return 0; - - let totalRemoved = 0; - - // Drain expired members in pages of up to ZRANGE_PAGE_LIMIT. We re-query the - // bottom of the index each iteration because zRemRangeByScore shifts it. - for (;;) { - const expired = await redis.zRange(timeIndexKey(page), -Infinity, cutoffSec, { - by: 'score', - limit: { offset: 0, count: ZRANGE_PAGE_LIMIT }, - }); - if (expired.length === 0) break; - - const ids = expired.map((m) => m.member); - // Remove the full records and their dedup markers. - await redis.hDel(actionsKey(page), ids); - await redis.hDel(seenKey(page), ids); - totalRemoved += ids.length; - - if (expired.length < ZRANGE_PAGE_LIMIT) break; // last (partial) page handled. - } - - // Finally trim the time index itself in one shot (idempotent if already gone). - if (totalRemoved > 0) { - await redis.zRemRangeByScore(timeIndexKey(page), -Infinity, cutoffSec); - } - - return totalRemoved; -} - -// --------------------------------------------------------------------------- -// 4. Wiki content-hash cache (INV-6) -// --------------------------------------------------------------------------- - -/** - * Get the last-written content hash for a wiki page, or `undefined` if the page - * has never been published (or its hash is not yet cached). - * - * @param redis Devvit redis handle. - * @param page Wiki page name. - */ -export async function getWikiHash( - redis: RedisClient, - page: string, -): Promise { - const all = await redis.hGetAll(WIKI_HASH_KEY); - if (!all) return undefined; - return all[page] ?? undefined; -} - -/** - * Store the content hash that was just written to a wiki page, so the next - * publish can skip an unchanged write (INV-6). - * - * @param redis Devvit redis handle. - * @param page Wiki page name. - * @param hash SHA-256 hex of the content that was published. - */ -export async function setWikiHash( - redis: RedisClient, - page: string, - hash: string, -): Promise { - await redis.hSet(WIKI_HASH_KEY, { [page]: hash }); -} - -// --------------------------------------------------------------------------- -// 5. Spec-name compatibility layer + status accessors -// --------------------------------------------------------------------------- -// -// The architecture spec (and the sibling modlog/wiki/menu modules) reference -// these function names; the implementations above were authored under slightly -// different names. The thin adapters below bind the spec names to the -// implementations so the whole project links without duplicating logic. -// -// Drift map (spec name -> implementation): -// markSeen -> inverse of isProcessed (markSeen returns TRUE when NEW) -// putRecord -> recordAction -// getAllRecords -> getRecentActions (newest-first, default cap) -// getStatus / recordRunStarted / recordPublished -> status hash (added below) - -/** Default cap for getAllRecords when no explicit limit is supplied. */ -const DEFAULT_RECORD_FETCH_CAP = 2000; - -/** Status hash: field=metric, value=epoch-ms string. */ -const STATUS_KEY = 'status'; -const STATUS_FIELD_LAST_RUN = 'lastRunAtMs'; -const STATUS_FIELD_LAST_PUBLISHED = 'lastPublishedAtMs'; - -/** - * The status accessors are not page-scoped in the menu contract, so they read - * the default wiki page for the entry count. Multi-page installs still publish - * correctly; the status entry count just reflects the primary page. - */ -const DEFAULT_STATUS_PAGE = 'modlog'; - -/** - * Atomic test-and-mark for dedup (INV-5), spec spelling. - * - * Returns `true` when the action id was NOT previously seen (caller should - * process it), `false` when it was already processed. This is the inverse sense - * of `isProcessed` (which returns `true` for duplicates), so we negate. - * - * `retentionDays` is accepted for signature compatibility with the spec; the - * underlying dedup hash is pruned by `cleanupOld` rather than per-key TTL. - */ -export async function markSeen( - redis: RedisClient, - page: string, - actionId: string, - _retentionDays?: number, -): Promise { - const duplicate = await isProcessed(redis, page, actionId); - return !duplicate; -} - -/** Persist one render-ready record (spec spelling for `recordAction`). */ -export async function putRecord( - redis: RedisClient, - page: string, - record: ModActionRecord, -): Promise { - return recordAction(redis, page, record); -} - -/** - * Return all retained records newest-first (spec spelling). The wiki layer caps - * to maxWikiEntries during render, so a generous default cap here is safe. - */ -export async function getAllRecords( - redis: RedisClient, - page: string, - maxEntries: number = DEFAULT_RECORD_FETCH_CAP, -): Promise { - return getRecentActions(redis, page, maxEntries); -} - -/** Read the lightweight status snapshot used by the "Show status" menu item. */ -export async function getStatus( - redis: RedisClient, -): Promise<{ lastRunAtMs?: number; lastPublishedAtMs?: number; entryCount: number }> { - const status = await redis.hGetAll(STATUS_KEY); - const lastRun = status?.[STATUS_FIELD_LAST_RUN]; - const lastPub = status?.[STATUS_FIELD_LAST_PUBLISHED]; - // entryCount is best-effort: count of fields in the actions hash. - const actions = await redis.hGetAll(actionsKey(DEFAULT_STATUS_PAGE)); - const entryCount = actions ? Object.keys(actions).length : 0; - return { - lastRunAtMs: lastRun ? Number(lastRun) : undefined, - lastPublishedAtMs: lastPub ? Number(lastPub) : undefined, - entryCount, - }; -} - -/** Stamp the last-run-started time (epoch ms). */ -export async function recordRunStarted(redis: RedisClient, epochMs: number): Promise { - await redis.hSet(STATUS_KEY, { [STATUS_FIELD_LAST_RUN]: String(epochMs) }); -} - -/** Stamp the last-published time (epoch ms). */ -export async function recordPublished(redis: RedisClient, epochMs: number): Promise { - await redis.hSet(STATUS_KEY, { [STATUS_FIELD_LAST_PUBLISHED]: String(epochMs) }); -} diff --git a/devvit/src/types.ts b/devvit/src/types.ts deleted file mode 100644 index b5aff1c..0000000 --- a/devvit/src/types.ts +++ /dev/null @@ -1,193 +0,0 @@ -/** - * types.ts — Shared contracts and frozen constants (single source of truth). - * - * Every other module imports its cross-cutting types and constants from here so - * the record/config shapes are declared exactly once (coding-style: no - * duplication). This module has NO runtime behavior and NO `@devvit/*` imports, - * which keeps the pure render layer free of platform dependencies. - * - * The invariants referenced below (INV-1..INV-9) are the binding rules carried - * verbatim from the legacy Python publisher (`modlog_wiki_publisher.py`): - * - * INV-1 Anonymize moderators ALWAYS (no toggle). - * INV-2 Never link user profiles (only post/comment permalinks). - * INV-3 512 KB wiki cap. - * INV-4 Email censor + pipe-escape on free text. - * INV-5 Dedup by ModAction.id. - * INV-6 Wiki hash-skip on unchanged content. - * INV-7 Default tracked action types. - * INV-8 Default ignored moderators ([AutoModerator]). - * INV-9 One install == one subreddit (Redis is install-scoped). - */ - -// --------------------------------------------------------------------------- -// Action-type model -// --------------------------------------------------------------------------- - -/** - * A moderation-log action type. - * - * Typed as `string` rather than a closed union so it stays assignment-compatible - * with Devvit's own `ModActionType` union (which `modlog.ts` reads off - * `ModAction.type`) WITHOUT pulling a `@devvit/*` import into the pure layers. - * The canonical tracked set is constrained at the edges by `VALID_MODLOG_ACTIONS` - * + the settings validators, not by the type system. - */ -export type ModActionType = string; - -/** P (post) | C (comment) | U (user) | A (any/other) display-prefix kind. */ -export type DisplayKind = 'P' | 'C' | 'U' | 'A'; - -// --------------------------------------------------------------------------- -// Wiki byte cap (INV-3) -// --------------------------------------------------------------------------- - -/** Hard wiki content cap in UTF-8 bytes (Reddit limit). */ -export const WIKI_BYTE_CAP = 524_288; - -/** Trim target: day-blocks are dropped until content fits in <= 90% of the cap. */ -export const WIKI_TRIM_TARGET = Math.floor(WIKI_BYTE_CAP * 0.9); - -// --------------------------------------------------------------------------- -// Anonymization (INV-1) -// --------------------------------------------------------------------------- - -/** - * Anonymization is mandatory and not user-configurable (the Python app refused - * to start when it was false). Exposed read-only on AppConfig for clarity. - */ -export const ANONYMIZE_MODERATORS = true as const; - -/** Single shared label every human moderator collapses to (INV-1). */ -export const ANON_LABEL = 'HumanModerator'; - -/** - * Actors kept literal (NOT anonymized): platform automation. Everything else is - * a human moderator and collapses to ANON_LABEL. - */ -export const LITERAL_MODS: ReadonlySet = new Set(['AutoModerator', 'Reddit']); - -// --------------------------------------------------------------------------- -// Tracked actions (INV-7) + valid set -// --------------------------------------------------------------------------- - -/** - * The full set of moderation-log action types the app understands. The settings - * multiSelect and validators are derived from this list so the form and the - * runtime filter never drift. Every value here exists in Devvit's ModActionType - * union (verified against reddit/devvit ModAction.ts). - */ -export const VALID_MODLOG_ACTIONS: readonly ModActionType[] = Object.freeze([ - 'removelink', - 'removecomment', - 'spamlink', - 'spamcomment', - 'addremovalreason', - 'approvelink', - 'approvecomment', -]); - -/** Default tracked action types (INV-7) — the 7 removal/spam/approval/reason actions. */ -export const DEFAULT_WIKI_ACTIONS: readonly ModActionType[] = Object.freeze([ - 'removelink', - 'removecomment', - 'spamlink', - 'spamcomment', - 'addremovalreason', - 'approvelink', - 'approvecomment', -]); - -// --------------------------------------------------------------------------- -// Ignored moderators (INV-8) -// --------------------------------------------------------------------------- - -/** Always-ignored moderators (INV-8). AutoModerator is non-removable. */ -export const DEFAULT_IGNORED_MODS: readonly string[] = Object.freeze(['AutoModerator']); - -// --------------------------------------------------------------------------- -// Numeric settings: defaults + clamp bands -// --------------------------------------------------------------------------- - -/** Default wiki page slug to publish to (Python default: "modlog"). */ -export const DEFAULT_WIKI_PAGE = 'modlog'; - -/** Retention window in days (Python default 90; band 1..365). */ -export const DEFAULT_RETENTION_DAYS = 90; -export const RETENTION_DAYS_MIN = 1; -export const RETENTION_DAYS_MAX = 365; - -/** Max entries rendered per wiki page (band 100..2000). */ -export const DEFAULT_MAX_WIKI_ENTRIES = 1000; -export const MAX_WIKI_ENTRIES_MIN = 100; -export const MAX_WIKI_ENTRIES_MAX = 2000; - -/** Mod-log entries fetched per run (band 50..1000). */ -export const DEFAULT_FETCH_LIMIT = 500; -export const FETCH_LIMIT_MIN = 50; -export const FETCH_LIMIT_MAX = 1000; - -/** Storage schema version marker (forward-migration hook). */ -export const SCHEMA_VERSION = 1; - -// --------------------------------------------------------------------------- -// Record + config shapes -// --------------------------------------------------------------------------- - -/** - * One moderation-log action in persisted, render-ready form. Enough is stored to - * rebuild the entire wiki page from Redis alone (no re-fetch required). - * - * INVARIANTS baked into this shape: - * - `moderator` is ALREADY anonymized at ingest (INV-1) — a real moderator - * name must NEVER reach Redis or the render layer. - * - `permalink` is present ONLY for posts/comments (INV-2); user/subreddit - * targets carry `undefined`, so render can never emit a profile link. - * - `reason` is stored RAW; censor + pipe-escape (INV-4) are applied at - * render time so the censor regex can evolve without a data migration. - */ -export interface ModRecord { - /** ModAction.id — the dedup key (INV-5); also the hash field + zset member. */ - id: string; - /** Epoch SECONDS of the action's creation; the sorted-set score (retention). */ - createdAtSec: number; - /** Tracked action type (e.g. 'removelink'); already filtered to INV-7. */ - actionType: ModActionType; - /** ALREADY-anonymized moderator label (INV-1) — never a real name. */ - moderator: string; - /** Target fullname (t3_/t1_/t2_/t5_...), if any. */ - targetId?: string; - /** P (post) | C (comment) | U (user) | A (any/other). */ - displayKind: DisplayKind; - /** Short, prefixed display id (e.g. 'Pabc123'), if a target exists. */ - displayId?: string; - /** Post/comment permalink ONLY (INV-2); undefined for profiles/subreddits. */ - permalink?: string; - /** Target author handle, for the modmail removal-inquiry prefill. */ - targetAuthor?: string; - /** RAW reason text; sanitized at render (INV-4). */ - reason?: string; -} - -/** - * Resolved, validated application configuration for one install (INV-9). - * Produced by `settings.loadConfig`; consumed (read-only) everywhere else. - */ -export interface AppConfig { - /** Subreddit name from the install context (INV-9) — never a user setting. */ - subredditName: string; - /** Wiki page slug to publish to. */ - wikiPage: string; - /** Tracked action types (INV-7), validated against VALID_MODLOG_ACTIONS. */ - wikiActions: ModActionType[]; - /** Ignored moderators (INV-8 default unioned in), lowercased + deduped. */ - ignoredModerators: string[]; - /** Retention window in days, clamped to [RETENTION_DAYS_MIN, MAX]. */ - retentionDays: number; - /** Max entries rendered, clamped to [MAX_WIKI_ENTRIES_MIN, MAX]. */ - maxWikiEntries: number; - /** Mod-log fetch limit per run, clamped to [FETCH_LIMIT_MIN, MAX]. */ - fetchLimit: number; - /** Always true (INV-1); surfaced read-only, never derived from a setting. */ - anonymizeModerators: boolean; -} diff --git a/devvit/src/wiki.ts b/devvit/src/wiki.ts deleted file mode 100644 index 071077e..0000000 --- a/devvit/src/wiki.ts +++ /dev/null @@ -1,211 +0,0 @@ -/** - * wiki.ts — Wiki publish orchestration. - * - * Responsibility (per architecture §1.5): - * The create-or-update + hash-skip publish flow that writes the rendered - * moderation-log markdown to a subreddit wiki page. - * - * Invariants owned here: - * - INV-3 512 KB wiki cap — content arriving here is ALREADY capped by - * render.enforceByteCap; we defensively re-check the byte length and - * refuse to write over-cap content (fail closed rather than let - * Reddit reject a >524288-byte body). - * - INV-6 Wiki hash-skip — never write the wiki when the SHA-256 of the - * rendered content equals the last written hash. Bypassable via - * `opts.bypassHash` for the menu "force wiki" action. - * - * This is the single publish code path shared by the scheduler cron, the - * trigger-coalesced refresh, and the menu force-write handlers. - * - * Dependency direction (strictly downward): wiki -> { storage, render } and - * the Reddit client. No cycles; render is pure, storage is the only Redis - * module. - * - * NOTE ON IMPORTS: per the build instruction this module imports from - * '@devvit/public-api' (the classic single-package entrypoint). The - * architecture spec references the newer split packages - * ('@devvit/reddit' / '@devvit/redis'); if this project is built on Devvit Web - * the imports below should be swapped accordingly — see the TODOs. - */ - -// TODO(devvit-imports): On Devvit Web this becomes -// import type { RedditAPIClient } from '@devvit/reddit'; -// import type { RedisClient } from '@devvit/redis'; -// Using '@devvit/public-api' here per the build instruction for this file. -import type { RedditAPIClient, RedisClient } from '@devvit/public-api'; - -import type { AppConfig } from './types.js'; -import { WIKI_BYTE_CAP } from './types.js'; -import { getAllRecords, getWikiHash, setWikiHash } from './storage.js'; -import { buildContent, contentHash } from './render.js'; - -/** Reason string attached to wiki revisions so the audit trail is legible. */ -const WIKI_REVISION_REASON = 'RedditModLog: moderation log update'; - -/** Outcome of a publish attempt — surfaced to logs and menu toasts. */ -export interface PublishResult { - /** True if a wiki write (create or update) actually happened. */ - wrote: boolean; - /** - * Why we did / did not write: - * 'created' — page did not exist, was created - * 'updated' — page existed and content changed - * 'unchanged' — hash matched (INV-6) OR existing content already equal - * 'over-cap' — content exceeded the 512 KB cap; refused to write - */ - reason: 'created' | 'updated' | 'unchanged' | 'over-cap'; -} - -/** - * UTF-8 byte length of a string. The wiki cap (INV-3) is defined in BYTES, not - * code units, so we must measure the encoded length — multi-byte characters - * (emoji, non-Latin reasons) count for more than one byte. - */ -function utf8ByteLength(value: string): number { - // TextEncoder is available in the Devvit serverless runtime (Web Crypto / - // standard globals). Avoids pulling in Node's Buffer. - return new TextEncoder().encode(value).length; -} - -/** - * Publish pre-rendered markdown to the wiki page with hash-skip + cap guard. - * - * Flow (architecture §1.5): - * 1. Defensive INV-3 byte-cap check — refuse over-cap content. - * 2. Compute SHA-256 of content (INV-6). - * 3. Unless bypassHash: compare to stored hash; skip write if equal. - * 4. Read the page; create if absent, update if present-and-different, - * skip if present-and-equal (defensive double-check of INV-6). - * 5. Persist the new hash so the next run can short-circuit. - * - * Idempotent: calling twice with identical content writes at most once. - * - * @param reddit Reddit API client (provides get/create/updateWikiPage). - * @param redis Redis handle (hash cache only — via storage layer). - * @param cfg Resolved app config (subreddit + wiki page name). - * @param content Fully rendered, already-capped markdown (from render.buildContent). - * @param opts.bypassHash Skip the INV-6 hash short-circuit (menu force-write). - */ -export async function publish( - reddit: RedditAPIClient, - redis: RedisClient, - cfg: AppConfig, - content: string, - opts?: { bypassHash?: boolean }, -): Promise { - const bypassHash = opts?.bypassHash ?? false; - - // --- Step 1: INV-3 defensive byte-cap guard -------------------------------- - // render.enforceByteCap should already keep us under the cap; if something - // upstream produced over-cap content we fail closed rather than send a body - // Reddit will reject. This is a guard, not the primary trim mechanism. - const byteLength = utf8ByteLength(content); - if (byteLength > WIKI_BYTE_CAP) { - console.error( - `[wiki] refusing to publish: content is ${byteLength} bytes, exceeds cap ${WIKI_BYTE_CAP} ` + - `(page="${cfg.wikiPage}", sub="${cfg.subredditName}"). render.enforceByteCap should have trimmed this.`, - ); - return { wrote: false, reason: 'over-cap' }; - } - - // --- Step 2: hash of the content we intend to write (INV-6) ---------------- - const hash = await contentHash(content); - - // --- Step 3: hash-skip short-circuit (INV-6) ------------------------------- - if (!bypassHash) { - const prevHash = await getWikiHash(redis, cfg.wikiPage); - if (prevHash !== undefined && prevHash === hash) { - console.info(`[wiki] skip write: content hash unchanged (page="${cfg.wikiPage}").`); - return { wrote: false, reason: 'unchanged' }; - } - } - - // --- Step 4: read-or-create, then update if changed ------------------------ - // getWikiPage throws when the page does not exist (research delta #6), so the - // "does it exist?" probe is a try/catch around the read, not a return code. - let existingContent: string | undefined; - try { - const page = await reddit.getWikiPage(cfg.subredditName, cfg.wikiPage); - existingContent = page.content; - } catch (err) { - // Treat any read failure as "page absent" and attempt creation below. - // If creation also fails for a non-absence reason, that error propagates. - existingContent = undefined; - console.info( - `[wiki] getWikiPage("${cfg.subredditName}", "${cfg.wikiPage}") failed/absent; will create. ` + - `(${err instanceof Error ? err.message : String(err)})`, - ); - } - - if (existingContent === undefined) { - // Page does not exist — create it. - // TODO(devvit-api): confirm createWikiPage options shape - // { subredditName, page, content, reason } — verified against - // CreateWikiPageOptions in devvit-docs (RedditAPIClient.createWikiPage). - await reddit.createWikiPage({ - subredditName: cfg.subredditName, - page: cfg.wikiPage, - content, - reason: WIKI_REVISION_REASON, - }); - await setWikiHash(redis, cfg.wikiPage, hash); - console.info(`[wiki] created page "${cfg.wikiPage}" (${byteLength} bytes).`); - return { wrote: true, reason: 'created' }; - } - - if (existingContent === content) { - // Defensive INV-6 double-check: page already byte-for-byte equal. This can - // happen if the stored hash was lost/reset but the wiki itself is current. - // Re-persist the hash so the next run short-circuits via step 3. - await setWikiHash(redis, cfg.wikiPage, hash); - console.info(`[wiki] skip write: existing page content already equal (page="${cfg.wikiPage}").`); - return { wrote: false, reason: 'unchanged' }; - } - - // Page exists and differs — update it. - // TODO(devvit-api): confirm updateWikiPage options shape - // { subredditName, page, content, reason } — verified against - // UpdateWikiPageOptions in devvit-docs (RedditAPIClient.updateWikiPage). - await reddit.updateWikiPage({ - subredditName: cfg.subredditName, - page: cfg.wikiPage, - content, - reason: WIKI_REVISION_REASON, - }); - await setWikiHash(redis, cfg.wikiPage, hash); - console.info(`[wiki] updated page "${cfg.wikiPage}" (${byteLength} bytes).`); - return { wrote: true, reason: 'updated' }; -} - -/** - * Convenience publish path: read all stored records, render them, and publish. - * - * This is the single entrypoint invoked by: - * - the scheduler cron job, - * - the trigger-coalesced refresh (dirty-flag consumer), - * - the menu "run now" / "force rebuild" / "force wiki" handlers. - * - * Keeping the render+publish composition here means callers never assemble - * content themselves, so INV-3/INV-4/INV-6 are enforced uniformly. - * - * @param reddit Reddit API client. - * @param redis Redis handle. - * @param cfg Resolved app config. - * @param opts.bypassHash Forwarded to publish() (menu force-write). - */ -export async function publishFromStore( - reddit: RedditAPIClient, - redis: RedisClient, - cfg: AppConfig, - opts?: { bypassHash?: boolean }, -): Promise { - // Newest-first records, already capped to maxWikiEntries by the storage layer. - const records = await getAllRecords(redis, cfg.wikiPage); - - // render.buildContent is pure and owns the table layout, censor/escape - // (INV-4), link gating (INV-2), and the byte-cap trim (INV-3). - const nowIso = new Date().toISOString(); - const content = buildContent(records, cfg, nowIso); - - return publish(reddit, redis, cfg, content, opts); -} diff --git a/devvit/tsconfig.json b/devvit/tsconfig.json index 74f25f7..07cbbb1 100644 --- a/devvit/tsconfig.json +++ b/devvit/tsconfig.json @@ -1,10 +1,30 @@ { - "extends": "@devvit/public-api/devvit.tsconfig.json", + "$schema": "https://json.schemastore.org/tsconfig.json", "compilerOptions": { - "outDir": "dist", - "rootDir": "src", - "noEmit": false + "composite": true, + "allowUnreachableCode": false, + "allowUnusedLabels": false, + "exactOptionalPropertyTypes": true, + "forceConsistentCasingInFileNames": true, + "noImplicitOverride": true, + "types": ["node"], + "noUncheckedIndexedAccess": true, + "noUncheckedSideEffectImports": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "resolveJsonModule": true, + "strict": true, + "isolatedModules": true, + "esModuleInterop": true, + "module": "ESNext", + "moduleResolution": "Bundler", + "skipLibCheck": true, + "skipDefaultLibCheck": true, + "sourceMap": true, + "target": "ES2022", + "outDir": "dist/types", + "tsBuildInfoFile": "dist/types/tsconfig.tsbuildinfo" }, - "include": ["src/**/*.ts"], - "exclude": ["node_modules", "dist"] + "include": ["src/**/*", "test/**/*", "vite.config.ts", "vitest.config.ts"], + "exclude": [] } diff --git a/devvit/vite.config.ts b/devvit/vite.config.ts new file mode 100644 index 0000000..8ee1e25 --- /dev/null +++ b/devvit/vite.config.ts @@ -0,0 +1,6 @@ +import { devvit } from '@devvit/start/vite'; +import { defineConfig } from 'vite'; + +export default defineConfig({ + plugins: [devvit()], +}); diff --git a/devvit/vitest.config.ts b/devvit/vitest.config.ts new file mode 100644 index 0000000..43e56f4 --- /dev/null +++ b/devvit/vitest.config.ts @@ -0,0 +1,7 @@ +import { defineConfig } from 'vitest/config'; + +export default defineConfig({ + test: { + include: ['test/**/*.test.ts'], + }, +}); From 6d5e57708a85981b9545934d4a1153cc80212101 Mon Sep 17 00:00:00 2001 From: bakerboy448 <55419169+bakerboy448@users.noreply.github.com> Date: Thu, 15 Oct 2026 22:22:50 -0500 Subject: [PATCH 3/5] test(devvit): add Vitest suites and Python golden parity test parity/python_render.py runs the fixture through the unmodified Python publisher; the TS pipeline must reproduce golden.md byte for byte. --- devvit/test/fixtures.ts | 63 ------------ devvit/test/helpers.ts | 75 ++++++++++++++ devvit/test/mocks.ts | 104 ------------------- devvit/test/parity.test.ts | 28 +++++ devvit/test/parity/fixture.json | 115 +++++++++++++++++++++ devvit/test/parity/golden.md | 26 +++++ devvit/test/parity/python_render.py | 70 +++++++++++++ devvit/test/pipeline.test.ts | 88 ---------------- devvit/test/publish.test.ts | 77 ++++++++++++++ devvit/test/render.test.ts | 152 ++++++++++++++++++++++++++++ devvit/test/settings.test.ts | 77 ++++++++++++++ devvit/test/store.test.ts | 73 +++++++++++++ 12 files changed, 693 insertions(+), 255 deletions(-) delete mode 100644 devvit/test/fixtures.ts create mode 100644 devvit/test/helpers.ts delete mode 100644 devvit/test/mocks.ts create mode 100644 devvit/test/parity.test.ts create mode 100644 devvit/test/parity/fixture.json create mode 100644 devvit/test/parity/golden.md create mode 100644 devvit/test/parity/python_render.py delete mode 100644 devvit/test/pipeline.test.ts create mode 100644 devvit/test/publish.test.ts create mode 100644 devvit/test/render.test.ts create mode 100644 devvit/test/settings.test.ts create mode 100644 devvit/test/store.test.ts diff --git a/devvit/test/fixtures.ts b/devvit/test/fixtures.ts deleted file mode 100644 index 4b9eef9..0000000 --- a/devvit/test/fixtures.ts +++ /dev/null @@ -1,63 +0,0 @@ -/** - * Synthetic ModAction fixtures + a test AppConfig. Shapes match the fields - * modlog.extractRecord reads: id, type, moderatorName, createdAt (Date), - * target?: { id, permalink, author }, description/details (reason source). - */ -import type { AppConfig } from '../src/types.js'; - -const NOW = 1_700_000_000; // fixed epoch seconds (no Date.now — deterministic) - -function action(o: { - id: string; - type: string; - mod: string; - ageSec?: number; - target?: { id: string; permalink?: string; author?: string }; - description?: string; - details?: string; -}) { - return { - id: o.id, - type: o.type, - moderatorName: o.mod, - createdAt: new Date((NOW - (o.ageSec ?? 0)) * 1000), - target: o.target, - description: o.description, - details: o.details, - }; -} - -export const FIXTURE_NOW_SEC = NOW; - -export const ACTIONS = [ - // tracked removal w/ reason + post target + author - action({ id: 'a1', type: 'removelink', mod: 'HumanMod1', ageSec: 10, description: 'Rule 1: off-topic', - target: { id: 't3_aaa', permalink: '/r/test/comments/aaa/title/', author: 'alice' } }), - // tracked comment removal whose reason has a PIPE char (must be escaped) + email (must be censored) - action({ id: 'a2', type: 'removecomment', mod: 'HumanMod2', ageSec: 20, details: 'spam | contact me at evil@example.com', - target: { id: 't1_bbb', permalink: '/r/test/comments/aaa/title/bbb/', author: 'bob' } }), - // addremovalreason with an email in the reason (PII strip) - action({ id: 'a3', type: 'addremovalreason', mod: 'HumanMod1', ageSec: 30, description: 'Appeal: mail admin@sub.example.org', - target: { id: 't3_aaa', permalink: '/r/test/comments/aaa/title/', author: 'alice' } }), - // approval - action({ id: 'a4', type: 'approvelink', mod: 'HumanMod2', ageSec: 40, - target: { id: 't3_ddd', permalink: '/r/test/comments/ddd/title/', author: 'carol' } }), - // IGNORED moderator (AutoModerator is in default ignored list) -> filtered out - action({ id: 'a5', type: 'spamlink', mod: 'AutoModerator', ageSec: 50, target: { id: 't3_eee' } }), - // UNTRACKED action type (banuser not in wiki_actions) -> filtered out - action({ id: 'a6', type: 'banuser', mod: 'HumanMod1', ageSec: 60 }), - // OLD action (beyond retention) for cleanup test - action({ id: 'a7', type: 'removelink', mod: 'HumanMod1', ageSec: 200 * 86_400, - target: { id: 't3_fff', permalink: '/r/test/comments/fff/title/', author: 'dave' } }), -]; - -export const CFG: AppConfig = { - subredditName: 'test', - wikiPage: 'modlog', - wikiActions: ['removelink', 'removecomment', 'spamlink', 'spamcomment', 'addremovalreason', 'approvelink', 'approvecomment'], - ignoredModerators: ['automoderator'], - retentionDays: 90, - maxWikiEntries: 1000, - fetchLimit: 500, - anonymizeModerators: true, -}; diff --git a/devvit/test/helpers.ts b/devvit/test/helpers.ts new file mode 100644 index 0000000..f7674b0 --- /dev/null +++ b/devvit/test/helpers.ts @@ -0,0 +1,75 @@ +import { readFileSync } from 'node:fs'; +import { fileURLToPath } from 'node:url'; + +import type { RedditPort } from '../src/core/publish.js'; +import type { AppConfig, ModLogEntry } from '../src/core/types.js'; + +export interface FixtureEntry { + id: string; + type: string; + moderatorName: string; + ageSec: number; + description?: string; + details?: string; + target?: { id: string; author?: string; permalink?: string }; +} + +export interface Fixture { + base: number; + subreddit: string; + entries: FixtureEntry[]; +} + +export const parityDir = fileURLToPath(new URL('./parity/', import.meta.url)); + +export function loadFixture(): Fixture { + return JSON.parse(readFileSync(`${parityDir}fixture.json`, 'utf8')) as Fixture; +} + +export function toModLog(fixture: Fixture): ModLogEntry[] { + return fixture.entries.map((e) => ({ + id: e.id, + type: e.type, + moderatorName: e.moderatorName, + createdAt: new Date((fixture.base - e.ageSec) * 1000), + description: e.description, + details: e.details, + target: e.target, + })); +} + +export function config(overrides: Partial = {}): AppConfig { + return { + subredditName: 'OpenSignups', + wikiPage: 'modlog-devvit', + retentionDays: 90, + displayDays: 90, + wikiActions: ['removelink', 'removecomment', 'spamlink', 'spamcomment', 'addremovalreason', 'approvelink', 'approvecomment'], + anonymizeModerators: true, + ignoredModerators: [], + ...overrides, + }; +} + +/** In-memory RedditPort: Devvit's test harness does not implement Moderation or Wiki. */ +export class FakeReddit implements RedditPort { + pages = new Map(); + writes: { page: string; content: string; exists: boolean }[] = []; + fetches = 0; + + constructor(public log: ModLogEntry[]) {} + + async fetchModLog(_sub: string, limit: number): Promise { + this.fetches += 1; + return this.log.slice(0, limit); + } + + async readWiki(_sub: string, page: string): Promise { + return this.pages.get(page) ?? null; + } + + async writeWiki(_sub: string, page: string, content: string, exists: boolean): Promise { + this.pages.set(page, content); + this.writes.push({ page, content, exists }); + } +} diff --git a/devvit/test/mocks.ts b/devvit/test/mocks.ts deleted file mode 100644 index 27a0f53..0000000 --- a/devvit/test/mocks.ts +++ /dev/null @@ -1,104 +0,0 @@ -/** - * In-memory fakes for the Devvit primitives our pipeline touches, so the real - * compiled modules (dist/*.js — type-only @devvit imports are erased at build) - * can run fully offline. This is the "self-test reddit": no auth, no platform. - */ - -type ZMember = { member: string; score: number }; - -/** Implements the exact RedisClient subset storage.ts uses. */ -export class InMemoryRedis { - private hashes = new Map>(); - private zsets = new Map>(); - - private h(key: string): Map { - let m = this.hashes.get(key); - if (!m) { m = new Map(); this.hashes.set(key, m); } - return m; - } - private z(key: string): Map { - let m = this.zsets.get(key); - if (!m) { m = new Map(); this.zsets.set(key, m); } - return m; - } - - async hSet(key: string, obj: Record): Promise { - const m = this.h(key); - for (const [k, v] of Object.entries(obj)) m.set(k, v); - return Object.keys(obj).length; - } - async hSetNX(key: string, field: string, value: string): Promise { - const m = this.h(key); - if (m.has(field)) return 0; - m.set(field, value); - return 1; - } - async hGet(key: string, field: string): Promise { - return this.hashes.get(key)?.get(field) ?? undefined; - } - async hGetAll(key: string): Promise> { - const m = this.hashes.get(key); - return m ? Object.fromEntries(m) : {}; - } - async hDel(key: string, fields: string[]): Promise { - const m = this.h(key); - let n = 0; - for (const f of fields) if (m.delete(f)) n++; - return n; - } - async zAdd(key: string, ...members: ZMember[]): Promise { - const m = this.z(key); - for (const { member, score } of members) m.set(member, score); - return members.length; - } - async zRange( - key: string, - min: number, - max: number, - opts: { by: string; reverse?: boolean; limit?: { offset: number; count: number } }, - ): Promise { - const m = this.zsets.get(key); - if (!m) return []; - let arr: ZMember[] = [...m.entries()] - .map(([member, score]) => ({ member, score })) - .filter((e) => e.score >= min && e.score <= max); - arr.sort((a, b) => (opts.reverse ? b.score - a.score : a.score - b.score)); - if (opts.limit) arr = arr.slice(opts.limit.offset, opts.limit.offset + opts.limit.count); - return arr; - } - async zRemRangeByScore(key: string, min: number, max: number): Promise { - const m = this.zsets.get(key); - if (!m) return 0; - let n = 0; - for (const [member, score] of [...m]) if (score >= min && score <= max) { m.delete(member); n++; } - return n; - } -} - -/** Mock RedditAPIClient covering getModerationLog + the wiki read/create/update path. */ -export function makeMockReddit(actions: any[]) { - const wiki = new Map(); // `${sub}/${page}` -> content - const writes: Array<{ op: string; page: string; content: string }> = []; - const reddit = { - getModerationLog(o: { subredditName: string; limit?: number; pageSize?: number }) { - const slice = actions.slice(0, o.limit ?? actions.length); - return { all: async () => slice }; - }, - async getWikiPage(subredditName: string, page: string) { - const key = `${subredditName}/${page}`; - if (!wiki.has(key)) throw new Error(`WIKI_PAGE_NOT_FOUND: ${key}`); - return { content: wiki.get(key)! }; - }, - async createWikiPage(o: { subredditName: string; page: string; content: string }) { - wiki.set(`${o.subredditName}/${o.page}`, o.content); - writes.push({ op: 'create', page: o.page, content: o.content }); - return { content: o.content }; - }, - async updateWikiPage(o: { subredditName: string; page: string; content: string }) { - wiki.set(`${o.subredditName}/${o.page}`, o.content); - writes.push({ op: 'update', page: o.page, content: o.content }); - return { content: o.content }; - }, - }; - return { reddit: reddit as any, wiki, writes }; -} diff --git a/devvit/test/parity.test.ts b/devvit/test/parity.test.ts new file mode 100644 index 0000000..9ba8820 --- /dev/null +++ b/devvit/test/parity.test.ts @@ -0,0 +1,28 @@ +/** + * Golden parity: parity/golden.md is the output of modlog_wiki_publisher.py's + * build_wiki_content for parity/fixture.json (see parity/python_render.py; CI + * regenerates it and fails on drift). The TS pipeline must match it byte for byte. + */ +import { readFileSync } from 'node:fs'; + +import { redis } from '@devvit/web/server'; +import { createDevvitTest } from '@devvit/test/server/vitest'; +import { expect } from 'vitest'; + +import { runCycle } from '../src/core/publish.js'; +import { Store } from '../src/core/store.js'; +import { config, FakeReddit, loadFixture, parityDir, toModLog } from './helpers.js'; + +const test = createDevvitTest(); + +test('TS pipeline output equals the Python golden file', async () => { + const fixture = loadFixture(); + const golden = readFileSync(`${parityDir}golden.md`, 'utf8'); + const reddit = new FakeReddit(toModLog(fixture)); + + const result = await runCycle(reddit, new Store(redis), config({ subredditName: fixture.subreddit }), 'scheduled', new Date(fixture.base * 1000)); + + expect(result.status).toBe('published'); + expect(reddit.writes).toHaveLength(1); + expect(reddit.writes[0]?.content).toBe(golden); +}); diff --git a/devvit/test/parity/fixture.json b/devvit/test/parity/fixture.json new file mode 100644 index 0000000..8d9761b --- /dev/null +++ b/devvit/test/parity/fixture.json @@ -0,0 +1,115 @@ +{ + "base": 1790856000, + "subreddit": "OpenSignups", + "entries": [ + { + "id": "ModAction_e01", + "type": "approvecomment", + "moderatorName": "HumanAlpha", + "ageSec": 600, + "target": { "id": "t1_kc0m003", "author": "poster03", "permalink": "/r/OpenSignups/comments/1abc333/post_three/kc0m003/" } + }, + { + "id": "ModAction_e02", + "type": "addremovalreason", + "moderatorName": "HumanBravo", + "ageSec": 1200, + "description": "Rule 2: no spam. Email mods@example.com | thanks 🚫", + "details": "Spam", + "target": { "id": "t1_kc0m001", "author": "poster01", "permalink": "/r/OpenSignups/comments/1abc111/post_one/kc0m001/" } + }, + { + "id": "ModAction_e03", + "type": "removecomment", + "moderatorName": "HumanBravo", + "ageSec": 1500, + "details": "", + "target": { "id": "t1_kc0m001", "author": "poster01", "permalink": "/r/OpenSignups/comments/1abc111/post_one/kc0m001/" } + }, + { + "id": "ModAction_e04", + "type": "approvelink", + "moderatorName": "HumanAlpha", + "ageSec": 1800, + "target": { "id": "t3_1abc444", "author": "spammer01", "permalink": "/r/OpenSignups/comments/1abc444/free_invites/" } + }, + { + "id": "ModAction_e05", + "type": "removecomment", + "moderatorName": "AutoModerator", + "ageSec": 3000, + "details": "Low karma filter", + "target": { "id": "t1_kc0m003", "author": "poster03", "permalink": "/r/OpenSignups/comments/1abc333/post_three/kc0m003/" } + }, + { + "id": "ModAction_e06", + "type": "approvecomment", + "moderatorName": "HumanAlpha", + "ageSec": 3600, + "target": { "id": "t1_kc0m009", "author": "poster09", "permalink": "/r/OpenSignups/comments/1abc999/post_nine/kc0m009/" } + }, + { + "id": "ModAction_e07", + "type": "banuser", + "moderatorName": "HumanAlpha", + "ageSec": 4000, + "details": "permanent", + "target": { "id": "t2_spammer", "author": "spammer01", "permalink": "/u/spammer01/" } + }, + { + "id": "ModAction_e08", + "type": "spamlink", + "moderatorName": "reddit", + "ageSec": 50000, + "details": "spam", + "target": { "id": "t3_1abc444", "author": "spammer01", "permalink": "/r/OpenSignups/comments/1abc444/free_invites/" } + }, + { + "id": "ModAction_e09", + "type": "removelink", + "moderatorName": "HumanCharlie", + "ageSec": 52000, + "target": { "id": "t3_1abc555", "author": "", "permalink": "/r/OpenSignups/comments/1abc555/deleted_post/" } + }, + { + "id": "ModAction_e10", + "type": "removelink", + "moderatorName": "AutoModerator", + "ageSec": 60000, + "details": "Title | pipe test", + "target": { "id": "t3_1abc666", "author": "poster06", "permalink": "/r/OpenSignups/comments/1abc666/pipe_post/" } + }, + { + "id": "ModAction_e11", + "type": "spamlink", + "moderatorName": "AutoModerator", + "ageSec": 61000, + "details": "repost", + "target": { "id": "t3_1abc666", "author": "poster06", "permalink": "/r/OpenSignups/comments/1abc666/pipe_post/" } + }, + { + "id": "ModAction_e12", + "type": "addremovalreason", + "moderatorName": "HumanBravo", + "ageSec": 100000, + "description": "Rule 5", + "target": { "id": "t3_1abc777", "author": "poster07", "permalink": "/r/OpenSignups/comments/1abc777/rule_five/" } + }, + { + "id": "ModAction_e13", + "type": "removecomment", + "moderatorName": "HumanAlpha", + "ageSec": 101000, + "details": "Rule 1: civility", + "target": { "id": "t1_kc0m010", "author": "poster10", "permalink": "/r/OpenSignups/comments/1abc777/rule_five/kc0m010/" } + }, + { + "id": "ModAction_e14", + "type": "removelink", + "moderatorName": "HumanAlpha", + "ageSec": 8640000, + "details": "too old to show", + "target": { "id": "t3_1abcold", "author": "poster99", "permalink": "/r/OpenSignups/comments/1abcold/old_post/" } + } + ] +} diff --git a/devvit/test/parity/golden.md b/devvit/test/parity/golden.md new file mode 100644 index 0000000..4ca8902 --- /dev/null +++ b/devvit/test/parity/golden.md @@ -0,0 +1,26 @@ +**Last Updated:** 2026-10-01 12:00:00 UTC + +--- + + +## 2026-10-01 +| Time | Action | ID | Moderator | Content | Reason | Inquire | +|------|--------|----|-----------|---------|--------|---------| +| 11:50:00 UTC | approvecomment | kc0m003 | HumanModerator | [Comment by u/poster03](https://www.reddit.com/r/OpenSignups/comments/1abc333/post_three/kc0m003/) | Approved AutoModerator removal: Low karma filter | [Contact Mods](https://www.reddit.com/message/compose?to=/r/OpenSignups&subject=Content%20Removal%20Inquiry%20-%20Content%20by%20u/poster03%20%5BID%3A%20kc0m003%5D&message=Hello%20Moderators%20of%20/r/OpenSignups%2C%0A%0AI%20would%20like%20to%20inquire%20about%20the%20recent%20removal%20of%20the%20following%20content%3A%0A%0A%2A%2AContent%20ID%3A%2A%2A%20kc0m003%0A%0A%2A%2ATitle%3A%2A%2A%20Content%20by%20u/poster03%0A%0A%2A%2AAction%20Type%3A%2A%2A%20approvecomment%0A%0A%2A%2ALink%3A%2A%2A%20https%3A//reddit.com/r/OpenSignups/comments/1abc333/post_three/kc0m003/%0A%0APlease%20provide%20details%20regarding%20this%20action.%0A%0AThank%20you%21) | +| 11:35:00 UTC | removecomment | kc0m001 | HumanModerator | [Comment by u/poster01](https://www.reddit.com/r/OpenSignups/comments/1abc111/post_one/kc0m001/) | Rule 2: no spam. Email [EMAIL] thanks 🚫 | [Contact Mods](https://www.reddit.com/message/compose?to=/r/OpenSignups&subject=Comment%20Removal%20Inquiry%20-%20Content%20by%20u/poster01%20%5BID%3A%20kc0m001%5D&message=Hello%20Moderators%20of%20/r/OpenSignups%2C%0A%0AI%20would%20like%20to%20inquire%20about%20the%20recent%20removal%20of%20the%20following%20comment%3A%0A%0A%2A%2AContent%20ID%3A%2A%2A%20kc0m001%0A%0A%2A%2ATitle%3A%2A%2A%20Content%20by%20u/poster01%0A%0A%2A%2AAction%20Type%3A%2A%2A%20removecomment%0A%0A%2A%2ALink%3A%2A%2A%20https%3A//reddit.com/r/OpenSignups/comments/1abc111/post_one/kc0m001/%0A%0APlease%20provide%20details%20regarding%20this%20action.%0A%0AThank%20you%21) | +| 11:30:00 UTC | approvelink | 1abc444 | HumanModerator | [Post by u/spammer01](https://www.reddit.com/r/OpenSignups/comments/1abc444/free_invites/) | Approved Reddit removal: spam | [Contact Mods](https://www.reddit.com/message/compose?to=/r/OpenSignups&subject=Content%20Removal%20Inquiry%20-%20Content%20by%20u/spammer01%20%5BID%3A%201abc444%5D&message=Hello%20Moderators%20of%20/r/OpenSignups%2C%0A%0AI%20would%20like%20to%20inquire%20about%20the%20recent%20removal%20of%20the%20following%20content%3A%0A%0A%2A%2AContent%20ID%3A%2A%2A%201abc444%0A%0A%2A%2ATitle%3A%2A%2A%20Content%20by%20u/spammer01%0A%0A%2A%2AAction%20Type%3A%2A%2A%20approvelink%0A%0A%2A%2ALink%3A%2A%2A%20https%3A//reddit.com/r/OpenSignups/comments/1abc444/free_invites/%0A%0APlease%20provide%20details%20regarding%20this%20action.%0A%0AThank%20you%21) | +| 11:10:00 UTC | filter-removecomment | kc0m003 | AutoModerator | [Comment by u/poster03](https://www.reddit.com/r/OpenSignups/comments/1abc333/post_three/kc0m003/) | Low karma filter | [Contact Mods](https://www.reddit.com/message/compose?to=/r/OpenSignups&subject=Comment%20Removal%20Inquiry%20-%20Content%20by%20u/poster03%20%5BID%3A%20kc0m003%5D&message=Hello%20Moderators%20of%20/r/OpenSignups%2C%0A%0AI%20would%20like%20to%20inquire%20about%20the%20recent%20removal%20of%20the%20following%20comment%3A%0A%0A%2A%2AContent%20ID%3A%2A%2A%20kc0m003%0A%0A%2A%2ATitle%3A%2A%2A%20Content%20by%20u/poster03%0A%0A%2A%2AAction%20Type%3A%2A%2A%20removecomment%0A%0A%2A%2ALink%3A%2A%2A%20https%3A//reddit.com/r/OpenSignups/comments/1abc333/post_three/kc0m003/%0A%0APlease%20provide%20details%20regarding%20this%20action.%0A%0AThank%20you%21) | + +## 2026-09-30 +| Time | Action | ID | Moderator | Content | Reason | Inquire | +|------|--------|----|-----------|---------|--------|---------| +| 22:06:40 UTC | spamlink | 1abc444 | Reddit | [Post by u/spammer01](https://www.reddit.com/r/OpenSignups/comments/1abc444/free_invites/) | spam | [Contact Mods](https://www.reddit.com/message/compose?to=/r/OpenSignups&subject=Spam%20Post%20Removal%20Inquiry%20-%20Content%20by%20u/spammer01%20%5BID%3A%201abc444%5D&message=Hello%20Moderators%20of%20/r/OpenSignups%2C%0A%0AI%20would%20like%20to%20inquire%20about%20the%20recent%20removal%20of%20the%20following%20spam%20post%3A%0A%0A%2A%2AContent%20ID%3A%2A%2A%201abc444%0A%0A%2A%2ATitle%3A%2A%2A%20Content%20by%20u/spammer01%0A%0A%2A%2AAction%20Type%3A%2A%2A%20spamlink%0A%0A%2A%2ALink%3A%2A%2A%20https%3A//reddit.com/r/OpenSignups/comments/1abc444/free_invites/%0A%0APlease%20provide%20details%20regarding%20this%20action.%0A%0AThank%20you%21) | +| 21:33:20 UTC | removelink | 1abc555 | HumanModerator | [Post by u/[deleted]](https://www.reddit.com/r/OpenSignups/comments/1abc555/deleted_post/) | - | [Contact Mods](https://www.reddit.com/message/compose?to=/r/OpenSignups&subject=Post%20Removal%20Inquiry%20-%20Unknown%20content%20%5BID%3A%201abc555%5D&message=Hello%20Moderators%20of%20/r/OpenSignups%2C%0A%0AI%20would%20like%20to%20inquire%20about%20the%20recent%20removal%20of%20the%20following%20post%3A%0A%0A%2A%2AContent%20ID%3A%2A%2A%201abc555%0A%0A%2A%2ATitle%3A%2A%2A%20Unknown%20content%0A%0A%2A%2AAction%20Type%3A%2A%2A%20removelink%0A%0A%2A%2ALink%3A%2A%2A%20https%3A//reddit.com/r/OpenSignups/comments/1abc555/deleted_post/%0A%0APlease%20provide%20details%20regarding%20this%20action.%0A%0AThank%20you%21) | +| 19:20:00 UTC | filter-removelink | 1abc666 | AutoModerator | [Post by u/poster06](https://www.reddit.com/r/OpenSignups/comments/1abc666/pipe_post/) | Title pipe test | [Contact Mods](https://www.reddit.com/message/compose?to=/r/OpenSignups&subject=Post%20Removal%20Inquiry%20-%20Content%20by%20u/poster06%20%5BID%3A%201abc666%5D&message=Hello%20Moderators%20of%20/r/OpenSignups%2C%0A%0AI%20would%20like%20to%20inquire%20about%20the%20recent%20removal%20of%20the%20following%20post%3A%0A%0A%2A%2AContent%20ID%3A%2A%2A%201abc666%0A%0A%2A%2ATitle%3A%2A%2A%20Content%20by%20u/poster06%0A%0A%2A%2AAction%20Type%3A%2A%2A%20removelink%0A%0A%2A%2ALink%3A%2A%2A%20https%3A//reddit.com/r/OpenSignups/comments/1abc666/pipe_post/%0A%0APlease%20provide%20details%20regarding%20this%20action.%0A%0AThank%20you%21) | +| 19:03:20 UTC | filter-spamlink | 1abc666 | AutoModerator | [Post by u/poster06](https://www.reddit.com/r/OpenSignups/comments/1abc666/pipe_post/) | repost | [Contact Mods](https://www.reddit.com/message/compose?to=/r/OpenSignups&subject=Spam%20Post%20Removal%20Inquiry%20-%20Content%20by%20u/poster06%20%5BID%3A%201abc666%5D&message=Hello%20Moderators%20of%20/r/OpenSignups%2C%0A%0AI%20would%20like%20to%20inquire%20about%20the%20recent%20removal%20of%20the%20following%20spam%20post%3A%0A%0A%2A%2AContent%20ID%3A%2A%2A%201abc666%0A%0A%2A%2ATitle%3A%2A%2A%20Content%20by%20u/poster06%0A%0A%2A%2AAction%20Type%3A%2A%2A%20spamlink%0A%0A%2A%2ALink%3A%2A%2A%20https%3A//reddit.com/r/OpenSignups/comments/1abc666/pipe_post/%0A%0APlease%20provide%20details%20regarding%20this%20action.%0A%0AThank%20you%21) | +| 08:13:20 UTC | addremovalreason | 1abc777 | HumanModerator | [Post by u/poster07](https://www.reddit.com/r/OpenSignups/comments/1abc777/rule_five/) | Rule 5 | [Contact Mods](https://www.reddit.com/message/compose?to=/r/OpenSignups&subject=Removal%20Reason%20Removal%20Inquiry%20-%20Content%20by%20u/poster07%20%5BID%3A%201abc777%5D&message=Hello%20Moderators%20of%20/r/OpenSignups%2C%0A%0AI%20would%20like%20to%20inquire%20about%20the%20recent%20removal%20of%20the%20following%20removal%20reason%3A%0A%0A%2A%2AContent%20ID%3A%2A%2A%201abc777%0A%0A%2A%2ATitle%3A%2A%2A%20Content%20by%20u/poster07%0A%0A%2A%2AAction%20Type%3A%2A%2A%20addremovalreason%0A%0A%2A%2ALink%3A%2A%2A%20https%3A//reddit.com/r/OpenSignups/comments/1abc777/rule_five/%0A%0APlease%20provide%20details%20regarding%20this%20action.%0A%0AThank%20you%21) | +| 07:56:40 UTC | removecomment | kc0m010 | HumanModerator | [Comment by u/poster10](https://www.reddit.com/r/OpenSignups/comments/1abc777/rule_five/kc0m010/) | Rule 1: civility | [Contact Mods](https://www.reddit.com/message/compose?to=/r/OpenSignups&subject=Comment%20Removal%20Inquiry%20-%20Content%20by%20u/poster10%20%5BID%3A%20kc0m010%5D&message=Hello%20Moderators%20of%20/r/OpenSignups%2C%0A%0AI%20would%20like%20to%20inquire%20about%20the%20recent%20removal%20of%20the%20following%20comment%3A%0A%0A%2A%2AContent%20ID%3A%2A%2A%20kc0m010%0A%0A%2A%2ATitle%3A%2A%2A%20Content%20by%20u/poster10%0A%0A%2A%2AAction%20Type%3A%2A%2A%20removecomment%0A%0A%2A%2ALink%3A%2A%2A%20https%3A//reddit.com/r/OpenSignups/comments/1abc777/rule_five/kc0m010/%0A%0APlease%20provide%20details%20regarding%20this%20action.%0A%0AThank%20you%21) | + +--- + +*This modlog is automatically maintained by [RedditModLog](https://github.com/bakerboy448/RedditModLog) bot.* \ No newline at end of file diff --git a/devvit/test/parity/python_render.py b/devvit/test/parity/python_render.py new file mode 100644 index 0000000..8918e29 --- /dev/null +++ b/devvit/test/parity/python_render.py @@ -0,0 +1,70 @@ +"""Render the parity fixture with the legacy Python publisher. + +Usage: python python_render.py + +Runs the fixture through the same path the Python daemon uses each cycle +(store_processed_action -> get_recent_actions_from_db -> build_wiki_content) +against a throwaway SQLite file, with the module clock frozen at the fixture +base time, and writes the wiki markdown to stdout. golden.md is this output. +""" + +import importlib +import json +import os +import sys +import tempfile +from datetime import datetime +from types import SimpleNamespace + + +def main() -> None: + fixture_path, repo_root = sys.argv[1], sys.argv[2] + with open(fixture_path, encoding="utf-8") as fh: + fixture = json.load(fh) + base = fixture["base"] + subreddit = fixture["subreddit"] + + tmp = tempfile.mkdtemp(prefix="modlog-parity-") + os.environ["DATABASE_PATH"] = os.path.join(tmp, "modlog.db") + sys.path.insert(0, repo_root) + mwp = importlib.import_module("modlog_wiki_publisher") + + class FrozenDatetime(datetime): + @classmethod + def now(cls, tz=None): + return datetime.fromtimestamp(base, tz) + + setattr(mwp, "datetime", FrozenDatetime) + mwp.setup_database() + + for entry in fixture["entries"]: + target = entry.get("target") or {} + action = SimpleNamespace( + id=entry["id"], + action=entry["type"], + mod=entry["moderatorName"], + created_utc=base - entry["ageSec"], + description=entry.get("description"), + details=entry.get("details"), + mod_note=None, + target_author=target.get("author"), + target_permalink=target.get("permalink"), + target_title=None, + target_fullname=target.get("id"), + ) + if not mwp.is_duplicate_action(action.id): + mwp.store_processed_action(action, subreddit) + + config = { + "source_subreddit": subreddit, + "anonymize_moderators": True, + "retention_days": 90, + "max_wiki_entries_per_page": 1000, + "wiki_actions": list(mwp.DEFAULT_WIKI_ACTIONS), + } + rows = mwp.get_recent_actions_from_db(config, force_all_actions=False, show_only_removals=True) + sys.stdout.write(mwp.build_wiki_content(rows, config)) + + +if __name__ == "__main__": + main() diff --git a/devvit/test/pipeline.test.ts b/devvit/test/pipeline.test.ts deleted file mode 100644 index d3756cf..0000000 --- a/devvit/test/pipeline.test.ts +++ /dev/null @@ -1,88 +0,0 @@ -/** - * Offline "self-test reddit" — runs the REAL compiled pipeline (dist/*.js, with - * type-only @devvit imports erased) against in-memory Redis + a mock Reddit - * client. Validates logic + the security invariants (anonymize / PII strip / - * no-profile-links / dedup / retention / size-cap / hash-skip) with no auth. - * - * Requires a build first (npm run build); the `test` script chains tsc. - */ -import { describe, it, expect } from 'vitest'; -import { InMemoryRedis, makeMockReddit } from './mocks'; -import { ACTIONS, CFG, FIXTURE_NOW_SEC } from './fixtures'; - -import { ingest } from '../dist/modlog.js'; -import { getRecentActions, cleanupOld, getAllRecords } from '../dist/storage.js'; -import { buildContent, censorEmail, escapePipes } from '../dist/render.js'; -import { publishFromStore } from '../dist/wiki.js'; - -describe('ingest + filtering', () => { - it('persists only tracked, non-ignored actions and dedupes', async () => { - const redis = new InMemoryRedis(); - const { reddit } = makeMockReddit(ACTIONS); - const r1 = await ingest(reddit, redis, CFG); - // a1,a2,a3,a4,a7 tracked+kept = 5 ; a5 (AutoModerator ignored), a6 (banuser untracked) dropped - expect(r1.added).toBe(5); - // second pass: everything already seen -> 0 new (INV-5 idempotency, no daemon) - const r2 = await ingest(reddit, redis, CFG); - expect(r2.added).toBe(0); - }); -}); - -describe('anonymization (INV-1) + no profile links (INV-2)', () => { - it('never emits a real moderator name and never links a user profile', async () => { - const redis = new InMemoryRedis(); - const { reddit } = makeMockReddit(ACTIONS); - await ingest(reddit, redis, CFG); - const recs = await getAllRecords(redis, CFG.wikiPage); - for (const rec of recs) { - expect(rec.moderator).not.toMatch(/HumanMod\d/); // real names (HumanMod1/2) never stored; anon label "HumanModerator" is fine - } - const md = buildContent(recs, CFG); - expect(md).not.toMatch(/HumanMod1|HumanMod2/); - expect(md).not.toMatch(/\/u\/|\/user\//); // no user-profile links anywhere - }); -}); - -describe('PII strip (INV-4): email censor + pipe escape', () => { - it('censorEmail replaces addresses', () => { - expect(censorEmail('reach evil@example.com now')).not.toContain('evil@example.com'); - }); - it('escapePipes neutralizes table-breaking pipes', () => { - expect(escapePipes('a | b')).not.toContain('|'); - }); - it('rendered wiki content contains no raw emails', async () => { - const redis = new InMemoryRedis(); - const { reddit } = makeMockReddit(ACTIONS); - await ingest(reddit, redis, CFG); - const md = buildContent(await getAllRecords(redis, CFG.wikiPage), CFG); - expect(md).not.toMatch(/[\w.+-]+@[\w-]+\.[\w.-]+/); // no email survives - }); -}); - -describe('retention cleanup', () => { - it('removes actions older than retentionDays', async () => { - const redis = new InMemoryRedis(); - const { reddit } = makeMockReddit(ACTIONS); - await ingest(reddit, redis, CFG); - const before = (await getAllRecords(redis, CFG.wikiPage)).length; - const removed = await cleanupOld(redis, CFG.wikiPage, CFG.retentionDays, FIXTURE_NOW_SEC); - expect(removed).toBeGreaterThanOrEqual(1); // a7 (200 days old) pruned - const after = (await getAllRecords(redis, CFG.wikiPage)).length; - expect(after).toBe(before - removed); - }); -}); - -describe('publish: create then hash-skip (INV-6)', () => { - it('writes once, then skips when content is unchanged', async () => { - const redis = new InMemoryRedis(); - const { reddit, writes } = makeMockReddit(ACTIONS); - await ingest(reddit, redis, CFG); - const p1 = await publishFromStore(reddit, redis, CFG); - expect(p1.wrote).toBe(true); - expect(writes.length).toBe(1); - const p2 = await publishFromStore(reddit, redis, CFG); - expect(p2.wrote).toBe(false); - expect(p2.reason).toBe('unchanged'); - expect(writes.length).toBe(1); // no second write - }); -}); diff --git a/devvit/test/publish.test.ts b/devvit/test/publish.test.ts new file mode 100644 index 0000000..202955c --- /dev/null +++ b/devvit/test/publish.test.ts @@ -0,0 +1,77 @@ +import { redis } from '@devvit/web/server'; +import { createDevvitTest } from '@devvit/test/server/vitest'; +import { expect } from 'vitest'; + +import { REDDIT_WIKI_LIMIT } from '../src/core/constants.js'; +import { runCycle } from '../src/core/publish.js'; +import { Store } from '../src/core/store.js'; +import { config, FakeReddit, loadFixture, toModLog } from './helpers.js'; + +const test = createDevvitTest(); +const fixture = loadFixture(); +const NOW = new Date(fixture.base * 1000); +const LATER = new Date((fixture.base + 1200) * 1000); + +test('second scheduled run with no new actions skips the wiki write', async () => { + const reddit = new FakeReddit(toModLog(fixture)); + const store = new Store(redis); + expect((await runCycle(reddit, store, config(), 'scheduled', NOW)).status).toBe('published'); + const second = await runCycle(reddit, store, config(), 'scheduled', LATER); + expect(second.status).toBe('unchanged'); + expect(second.ingested).toBe(0); + expect(reddit.writes).toHaveLength(1); +}); + +test('force rebuild rewrites from storage without fetching the mod log', async () => { + const reddit = new FakeReddit(toModLog(fixture)); + const store = new Store(redis); + await runCycle(reddit, store, config(), 'scheduled', NOW); + const fetches = reddit.fetches; + const result = await runCycle(reddit, store, config(), 'force-rebuild', LATER); + expect(result.status).toBe('published'); + expect(reddit.fetches).toBe(fetches); + expect(reddit.writes).toHaveLength(2); + expect(reddit.writes[1]?.exists).toBe(true); +}); + +test('ignored moderators are never stored or shown', async () => { + const reddit = new FakeReddit(toModLog(fixture)); + await runCycle(reddit, new Store(redis), config({ ignoredModerators: ['automoderator'] }), 'scheduled', NOW); + const page = reddit.writes[0]?.content ?? ''; + expect(page).not.toContain('filter-'); + // Without the stored AutoModerator removal the comment approval has nothing to reverse. + expect(page).not.toContain('Approved AutoModerator removal'); +}); + +test('an empty mod log publishes nothing', async () => { + const reddit = new FakeReddit([]); + const result = await runCycle(reddit, new Store(redis), config(), 'publish-now', NOW); + expect(result.status).toBe('no-actions'); + expect(reddit.writes).toHaveLength(0); +}); + +test('a page above 95% of the limit is replaced by smaller trimmed content', async () => { + const reddit = new FakeReddit(toModLog(fixture)); + reddit.pages.set('modlog-devvit', 'x'.repeat(Math.floor(REDDIT_WIKI_LIMIT * 0.96))); + reddit.log = []; + await expect(runCycle(reddit, new Store(redis), config(), 'scheduled', NOW)).resolves.toMatchObject({ status: 'no-actions' }); + reddit.log = toModLog(fixture); + const smaller = await runCycle(reddit, new Store(redis), config(), 'scheduled', NOW); + expect(smaller.status).toBe('published'); +}); + +test('a busy lock returns busy without touching the wiki', async () => { + const reddit = new FakeReddit(toModLog(fixture)); + const store = new Store(redis); + await store.acquireLock(NOW.getTime()); + expect((await runCycle(reddit, store, config(), 'publish-now', NOW)).status).toBe('busy'); + expect(reddit.fetches).toBe(0); +}); + +test('entries older than retention are not ingested', async () => { + const reddit = new FakeReddit(toModLog(fixture)); + const result = await runCycle(reddit, new Store(redis), config(), 'scheduled', NOW); + expect(result.scanned).toBe(fixture.entries.length); + expect(result.ingested).toBe(fixture.entries.length - 1); + expect(reddit.writes[0]?.content).not.toContain('1abcold'); +}); diff --git a/devvit/test/render.test.ts b/devvit/test/render.test.ts new file mode 100644 index 0000000..a14b966 --- /dev/null +++ b/devvit/test/render.test.ts @@ -0,0 +1,152 @@ +import { describe, expect, it } from 'vitest'; + +import { censorEmails, moderatorLabel, sanitizeForMarkdown, storedPermalink, toStoredAction } from '../src/core/record.js'; +import { bareContentId, buildWikiContent, contentHash, extractContentId, formatContentLink, modmailLink, pyQuote } from '../src/core/render.js'; +import type { PriorRemoval, StoredAction } from '../src/core/types.js'; + +const NOW = new Date('2026-10-01T12:00:00Z'); +const at = (iso: string): number => Math.floor(Date.parse(iso) / 1000); +const POST = 'https://reddit.com/r/Sub/comments/abc123/title/'; +const COMMENT = 'https://reddit.com/r/Sub/comments/abc123/title/def456/'; + +function row(over: Partial): StoredAction { + return { id: 'a1', action: 'removelink', moderator: 'HumanModerator', permalink: POST, reason: '', targetAuthor: 'op', createdAt: at('2026-10-01T10:00:00Z'), ...over }; +} + +function render(rows: StoredAction[], priors: Record = {}) { + return buildWikiContent(rows, { subredditName: 'Sub', anonymizeModerators: true, now: NOW, priorRemoval: (id) => priors[id] }); +} + +const tableRows = (content: string): string[] => content.split('\n').filter((l) => l.startsWith('| ') && !l.startsWith('| Time')); + +describe('record mapping', () => { + it('keeps AutoModerator and Reddit literal and anonymizes humans', () => { + expect(moderatorLabel('automoderator', true)).toBe('AutoModerator'); + expect(moderatorLabel('REDDIT', true)).toBe('Reddit'); + expect(moderatorLabel('SomeMod', true)).toBe('HumanModerator'); + expect(moderatorLabel(undefined, true)).toBeNull(); + }); + + it('never stores a profile link', () => { + expect(storedPermalink('/u/someone/')).toBeNull(); + expect(storedPermalink('/user/someone/comments/x/')).toBe('https://reddit.com/user/someone/comments/x/'); + expect(storedPermalink('/r/s/comments/u/x/')).toBeNull(); + expect(storedPermalink('/r/Sub/comments/abc/t/')).toBe('https://reddit.com/r/Sub/comments/abc/t/'); + }); + + it('censors emails and replaces pipes in reasons', () => { + expect(censorEmails('mail a.b@example.co.uk now')).toBe('mail [EMAIL] now'); + expect(sanitizeForMarkdown('a|b')).toBe('a b'); + }); + + it('reads addremovalreason text from description and others from details', () => { + const base = { id: 'x', moderatorName: 'm', createdAt: NOW }; + expect(toStoredAction({ ...base, type: 'addremovalreason', description: ' Rule 1 ', details: 'ignored' }, true).reason).toBe('Rule 1'); + expect(toStoredAction({ ...base, type: 'removelink', description: 'ignored', details: 'spam' }, true).reason).toBe('spam'); + expect(toStoredAction({ ...base, type: 'removelink' }, true).reason).toBe(''); + }); +}); + +describe('render helpers', () => { + it('extracts post and comment ids from permalinks', () => { + expect(extractContentId(POST)).toBe('t3_abc123'); + expect(extractContentId(COMMENT)).toBe('t1_def456'); + expect(bareContentId(null)).toBeNull(); + }); + + it('labels comments and posts and falls back to [deleted]', () => { + expect(formatContentLink(row({ permalink: COMMENT }))).toBe('[Comment by u/op](https://www.reddit.com/r/Sub/comments/abc123/title/def456/)'); + expect(formatContentLink(row({ targetAuthor: null }))).toBe('[Post by u/[deleted]](https://www.reddit.com/r/Sub/comments/abc123/title/)'); + expect(formatContentLink(row({ permalink: null }))).toBe('Post by u/op'); + }); + + it('quotes like urllib.parse.quote', () => { + expect(pyQuote("a/b c!'()*é")).toBe('a/b%20c%21%27%28%29%2A%C3%A9'); + }); + + it('builds a modmail link carrying the content id', () => { + const link = modmailLink('Sub', row({ action: 'spamcomment', permalink: COMMENT })); + expect(link).toMatch(/^\[Contact Mods\]\(https:\/\/www\.reddit\.com\/message\/compose\?to=\/r\/Sub&subject=Spam%20Comment%20Removal%20Inquiry/); + expect(link).toContain('%5BID%3A%20def456%5D'); + }); +}); + +describe('FR-7 removal and reason merging', () => { + it('merges a removal and its addremovalreason into one row', () => { + const { content } = render([ + row({ id: 'r', action: 'addremovalreason', reason: 'Rule 3', createdAt: at('2026-10-01T10:01:00Z') }), + row({ id: 'x', action: 'removelink', reason: '' }), + ]); + const rows = tableRows(content); + expect(rows).toHaveLength(1); + expect(rows[0]).toContain('| removelink |'); + expect(rows[0]).toContain('| Rule 3 |'); + }); + + it('keeps a reason row when no removal of that target is shown', () => { + const rows = tableRows(render([row({ action: 'addremovalreason', reason: 'Rule 3' })]).content); + expect(rows).toHaveLength(1); + expect(rows[0]).toContain('| addremovalreason |'); + }); + + it('shows a second removal of the same target as its own row', () => { + const rows = tableRows(render([row({ id: 'a' }), row({ id: 'b', action: 'spamlink', createdAt: at('2026-10-01T09:00:00Z') })]).content); + expect(rows).toHaveLength(2); + }); +}); + +describe('FR-8 approval gating', () => { + const approval = row({ id: 'ap', action: 'approvelink' }); + + it('drops an approval with no prior automated removal', () => { + expect(render([approval]).rows).toBe(0); + }); + + it('annotates an approval that reverses an AutoModerator removal', () => { + const rows = tableRows(render([approval], { abc123: { moderator: 'AutoModerator', reason: 'karma', createdAt: 1 } }).content); + expect(rows[0]).toContain('| Approved AutoModerator removal: karma |'); + }); + + it('omits an empty or "-" prior reason', () => { + const rows = tableRows(render([approval], { abc123: { moderator: 'Reddit', reason: '-', createdAt: 1 } }).content); + expect(rows[0]).toContain('| Approved Reddit removal |'); + }); + + it('matches the exact target id, not a comment under an approved post', () => { + expect(render([approval], { def456: { moderator: 'AutoModerator', reason: 'x', createdAt: 1 } }).rows).toBe(0); + }); +}); + +describe('page layout', () => { + it('prefixes AutoModerator removals with filter-', () => { + expect(tableRows(render([row({ moderator: 'AutoModerator' })]).content)[0]).toContain('| filter-removelink |'); + }); + + it('groups by UTC day, newest first', () => { + const { content } = render([row({ id: 'a' }), row({ id: 'b', createdAt: at('2026-09-29T23:59:59Z') })]); + expect(content.indexOf('## 2026-10-01')).toBeLessThan(content.indexOf('## 2026-09-29')); + }); + + it('renders the empty-page text with no rows', () => { + expect(render([]).content).toBe('**Last Updated:** 2026-10-01 12:00:00 UTC\n\n---\n\nNo recent moderation actions found.'); + }); + + it('trims the oldest days to stay under 90% of the 512 KiB limit', () => { + const big = 'x'.repeat(4000); + const rows: StoredAction[] = []; + for (let d = 0; d < 200; d++) { + rows.push(row({ id: `r${d}`, reason: big, createdAt: at('2026-10-01T10:00:00Z') - d * 86400, permalink: `https://reddit.com/r/Sub/comments/p${d}/t/` })); + } + const result = render(rows); + expect(result.trimmedDays).toBeGreaterThan(0); + expect(Buffer.byteLength(result.content)).toBeLessThan(Math.floor(524288 * 0.9) + 200); + expect(result.content).toContain('older day(s) trimmed due to wiki size limits.'); + }); + + it('hashes without the Last Updated line', () => { + const a = render([row({})]).content; + const b = buildWikiContent([row({})], { subredditName: 'Sub', anonymizeModerators: true, now: new Date('2027-01-01T00:00:00Z'), priorRemoval: () => undefined }).content; + expect(a).not.toBe(b); + expect(contentHash(a)).toBe(contentHash(b)); + }); +}); diff --git a/devvit/test/settings.test.ts b/devvit/test/settings.test.ts new file mode 100644 index 0000000..f5408e1 --- /dev/null +++ b/devvit/test/settings.test.ts @@ -0,0 +1,77 @@ +import { describe, expect, it } from 'vitest'; + +import { DEFAULT_WIKI_ACTIONS } from '../src/core/constants.js'; +import { + loadConfig, + parseIgnoredModerators, + validateAnonymize, + validateDays, + validateIgnoredModerators, + validateWikiActions, + validateWikiPage, +} from '../src/core/settings.js'; + +describe('validators', () => { + it('accepts normal wiki page names and rejects reserved or malformed ones', () => { + expect(validateWikiPage('modlog')).toBeUndefined(); + expect(validateWikiPage('modlog-devvit')).toBeUndefined(); + expect(validateWikiPage('logs/mod')).toBeUndefined(); + expect(validateWikiPage('')).toBeDefined(); + expect(validateWikiPage('config/sidebar')).toBeDefined(); + expect(validateWikiPage('automoderator')).toBeDefined(); + expect(validateWikiPage('mod log')).toBeDefined(); + }); + + it('bounds day counts to 1..365 whole numbers', () => { + expect(validateDays(90)).toBeUndefined(); + expect(validateDays(0)).toBeDefined(); + expect(validateDays(366)).toBeDefined(); + expect(validateDays(1.5)).toBeDefined(); + expect(validateDays(undefined)).toBeDefined(); + }); + + it('requires known, non-empty action lists', () => { + expect(validateWikiActions([...DEFAULT_WIKI_ACTIONS])).toBeUndefined(); + expect(validateWikiActions([])).toBeDefined(); + expect(validateWikiActions(['removelink', 'nope'])).toContain('nope'); + }); + + it('refuses to turn anonymization off', () => { + expect(validateAnonymize(true)).toBeUndefined(); + expect(validateAnonymize(false)).toBeDefined(); + }); + + it('parses and validates ignored moderators', () => { + expect(parseIgnoredModerators(' AutoModerator, u/Some_Mod ,,')).toEqual(['AutoModerator', 'Some_Mod']); + expect(validateIgnoredModerators('AutoModerator')).toBeUndefined(); + expect(validateIgnoredModerators('bad name!')).toBeDefined(); + }); +}); + +describe('loadConfig', () => { + it('applies defaults when nothing is set', () => { + const cfg = loadConfig({}, 'OpenSignups'); + expect(cfg).toEqual({ + subredditName: 'OpenSignups', + wikiPage: 'modlog-devvit', + retentionDays: 90, + displayDays: 90, + wikiActions: [...DEFAULT_WIKI_ACTIONS], + anonymizeModerators: true, + ignoredModerators: [], + }); + }); + + it('clamps days, caps display at retention and keeps anonymization on', () => { + const cfg = loadConfig({ retentionDays: 30, displayDays: 400, anonymizeModerators: false, wikiPage: 'ModLog' }, 'S'); + expect(cfg.retentionDays).toBe(30); + expect(cfg.displayDays).toBe(30); + expect(cfg.anonymizeModerators).toBe(true); + expect(cfg.wikiPage).toBe('modlog'); + }); + + it('throws on a reserved page or missing subreddit', () => { + expect(() => loadConfig({ wikiPage: 'config/automoderator' }, 'S')).toThrow(); + expect(() => loadConfig({}, undefined)).toThrow(); + }); +}); diff --git a/devvit/test/store.test.ts b/devvit/test/store.test.ts new file mode 100644 index 0000000..d410b03 --- /dev/null +++ b/devvit/test/store.test.ts @@ -0,0 +1,73 @@ +import { redis } from '@devvit/web/server'; +import { createDevvitTest } from '@devvit/test/server/vitest'; +import { expect } from 'vitest'; + +import { Store } from '../src/core/store.js'; +import type { StoredAction } from '../src/core/types.js'; + +const test = createDevvitTest(); +const P = (id: string) => `https://reddit.com/r/Sub/comments/${id}/t/`; + +function row(over: Partial): StoredAction { + return { id: 'a1', action: 'removelink', moderator: 'HumanModerator', permalink: P('p1'), reason: '', targetAuthor: 'op', createdAt: 1_000, ...over }; +} + +test('saveAction dedups by action id', async () => { + const store = new Store(redis); + expect(await store.saveAction(row({}))).toBe(true); + expect(await store.saveAction(row({ reason: 'changed' }))).toBe(false); + expect(await store.storedCount()).toBe(1); +}); + +test('loadDisplayRows filters by type and window, newest first', async () => { + const store = new Store(redis); + await store.saveAction(row({ id: 'old', createdAt: 100 })); + await store.saveAction(row({ id: 'mid', createdAt: 500, action: 'spamlink' })); + await store.saveAction(row({ id: 'new', createdAt: 900 })); + await store.saveAction(row({ id: 'ban', createdAt: 950, action: 'banuser' })); + + const rows = await store.loadDisplayRows(['removelink', 'spamlink'], 200); + expect(rows.map((r) => r.id)).toEqual(['new', 'mid']); +}); + +test('prior removals keep only the newest Reddit/AutoModerator removal per target', async () => { + const store = new Store(redis); + await store.saveAction(row({ id: 'h', moderator: 'HumanModerator', createdAt: 50 })); + expect((await store.priorRemovalsFor([row({})])).size).toBe(0); + + await store.saveAction(row({ id: 'a', moderator: 'AutoModerator', reason: 'first', createdAt: 100 })); + await store.saveAction(row({ id: 'b', moderator: 'Reddit', reason: 'second', createdAt: 300 })); + await store.saveAction(row({ id: 'c', moderator: 'AutoModerator', reason: 'older', createdAt: 200 })); + + const priors = await store.priorRemovalsFor([row({ action: 'approvelink' })]); + expect(priors.get('p1')).toEqual({ moderator: 'Reddit', reason: 'second', createdAt: 300 }); +}); + +test('prune deletes rows and prior removals older than the cutoff', async () => { + const store = new Store(redis); + await store.saveAction(row({ id: 'old', moderator: 'AutoModerator', createdAt: 100, permalink: P('p_old') })); + await store.saveAction(row({ id: 'new', createdAt: 900, permalink: P('p_new') })); + + expect(await store.prune(500)).toBe(1); + expect(await store.storedCount()).toBe(1); + expect((await store.loadDisplayRows(['removelink'], 0)).map((r) => r.id)).toEqual(['new']); + expect((await store.priorRemovalsFor([row({ permalink: P('p_old') })])).size).toBe(0); +}); + +test('the run lock blocks a second holder until released or stale', async () => { + const store = new Store(redis); + expect(await store.acquireLock(1_000)).toBe(true); + expect(await store.acquireLock(2_000)).toBe(false); + expect(await store.acquireLock(1_000 + 121_000)).toBe(true); + await store.releaseLock(); + expect(await store.acquireLock(500_000)).toBe(true); +}); + +test('wiki hash and status round-trip', async () => { + const store = new Store(redis); + await store.setWikiHash('modlog-devvit', 'abc'); + expect(await store.getWikiHash('modlog-devvit')).toBe('abc'); + expect(await store.getWikiHash('modlog')).toBeUndefined(); + await store.setStatus({ lastResult: 'published' }); + expect((await store.getStatus()).lastResult).toBe('published'); +}); From 59ec7d4745e29a91e21bdd7dbd35ed20b4c416ee Mon Sep 17 00:00:00 2001 From: bakerboy448 <55419169+bakerboy448@users.noreply.github.com> Date: Thu, 15 Oct 2026 22:22:51 -0500 Subject: [PATCH 4/5] docs(devvit): update migration status for the Devvit Web port --- README.md | 2 +- devvit-migration/README.md | 10 +- devvit-migration/docs/STATUS.md | 320 +++++++------------------------- 3 files changed, 73 insertions(+), 259 deletions(-) diff --git a/README.md b/README.md index e186747..84f5cde 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ Automatically publishes Reddit moderation logs to a subreddit wiki page with modmail inquiry links. -> **🧪 Devvit migration in progress.** A re-platform of this bot to a Reddit-hosted [Devvit](https://developers.reddit.com/) app (no self-hosting, no bot password, no SQLite) is being scaffolded under [`devvit/`](devvit/) with requirements, research, and architecture docs in [`devvit-migration/docs/`](devvit-migration/). The Python app documented below remains the supported version. See [`devvit-migration/README.md`](devvit-migration/README.md) for status. +> **🧪 Devvit migration in progress.** A re-platform of this bot to a Reddit-hosted [Devvit](https://developers.reddit.com/) app (no self-hosting, no bot password, no SQLite) lives under [`devvit/`](devvit/) as the Devvit Web app `autowikimod`, with design docs in [`devvit-migration/docs/`](devvit-migration/). The Python app documented below remains the supported version. See [`devvit-migration/README.md`](devvit-migration/README.md) for status. ## Features diff --git a/devvit-migration/README.md b/devvit-migration/README.md index 7ba76e5..f6a3a3b 100644 --- a/devvit-migration/README.md +++ b/devvit-migration/README.md @@ -8,8 +8,8 @@ Re-platforming the Python/PRAW **Reddit Modlog Wiki Publisher** into a **Devvit* | --- | --- | | `subreddit.mod.log(limit=N)` | `reddit.getModerationLog({ subredditName, type?, limit })` | | `subreddit.wiki[page].edit()` | `reddit.getWikiPage` / `reddit.updateWikiPage` (512 KB cap unchanged) | -| SQLite dedup + retention + hash-cache | `context.redis` (strings for dedup, sorted-set by timestamp for retention, hash for wiki-hash cache) | -| Continuous daemon (`update_interval`) | `Devvit.addSchedulerJob` + cron (1-min min granularity) + optional `onModAction` trigger | +| SQLite dedup + retention + hash-cache | `redis` from `@devvit/web/server` (hashes for rows, dedup and hash cache; sorted sets by timestamp for retention) | +| Continuous daemon (`update_interval`) | `devvit.json` scheduler task, cron `*/20 * * * *` | | `config.json` / env / CLI (19 options) | Devvit app + per-install settings (`anonymize_moderators` hardcoded true) | | Docker / s6 / systemd | Reddit-hosted (serverless, free); publish via app review | @@ -21,10 +21,8 @@ Re-platforming the Python/PRAW **Reddit Modlog Wiki Publisher** into a **Devvit* - [`docs/04-architecture.md`](docs/04-architecture.md) — module layout, Redis key schema, execution model, settings schema, phased plan, gap list - [`docs/STATUS.md`](docs/STATUS.md) — what is scaffolded vs. TODO, mapped to the parity matrix - [`docs/reddit-api/`](docs/reddit-api/) — stored Devvit API reference snippets -- [`../devvit/`](../devvit/) — the Devvit app scaffold (TypeScript; compiles clean via `tsc --noEmit`) +- [`../devvit/`](../devvit/) — the Devvit Web app (`autowikimod`) ## Status -Scaffold stage: requirements + research + architecture complete; component modules (`storage`, `modlog`, `render`, `wiki`, `settings`, `menu`, `main`) written and type-checking clean. Not yet `devvit upload`-tested against a live test subreddit. See [`docs/STATUS.md`](docs/STATUS.md). - -This scaffold was produced by a multi-agent scrum (requirements → research → architecture → per-component implementation → scaffold). Treat the generated TypeScript as a reviewed starting point, not a shipped app — every Devvit API call should be validated in `devvit playtest` before publishing. +Ported to Devvit Web; type-check, tests, build and Python parity pass offline. Awaiting upload and a live run on r/OpenSignups. See [`docs/STATUS.md`](docs/STATUS.md). `docs/01..04` describe the earlier classic-model design. diff --git a/devvit-migration/docs/STATUS.md b/devvit-migration/docs/STATUS.md index dcf31f1..c1afd86 100644 --- a/devvit-migration/docs/STATUS.md +++ b/devvit-migration/docs/STATUS.md @@ -1,255 +1,71 @@ # RedditModLog → Devvit: Migration Status | Field | Value | -|---|---| -| Status | Scaffold landed as groundwork; **NOT yet MVP-parity-complete**; not playtested. Type-checks clean against `@devvit/public-api@0.13.5` (`npm run type-check` passes, `dist/` emits), but two required-scope requirements are unimplemented and the platform-model choice contradicts its own research doc (see §7). | -| Date | 2026-09-21 | -| Branch | `feat/devvit-migration` | -| Code root | `devvit/` (classic `@devvit/public-api` 0.13.5 builder model) | -| Legacy source | `modlog_wiki_publisher.py` (read-only reference) | - -This document tracks what is implemented vs. outstanding, mapped to the parity -matrix and the binding invariants (INV-1..INV-9). - ---- - -## 1. Build artifacts (this pass) - -| File | State | Notes | -|---|---|---| -| `devvit/package.json` | DONE | `redditmodlog-devvit`, dep `@devvit/public-api@0.13.5`, scripts: `deploy`/`dev`/`playtest`/`login`/`launch`/`type-check`/`test`. | -| `devvit/devvit.yaml` | DONE | `name: redditmodlog`. Unique-name claim happens on first `devvit upload` — rename here first if taken. | -| `devvit/tsconfig.json` | DONE | Extends `@devvit/public-api/devvit.tsconfig.json` (module/moduleResolution = **NodeNext**). | -| `devvit/.gitignore` | DONE | `node_modules`, `dist`, `.devvit`, `.env*`. | -| `devvit/src/main.ts` | DONE | Entrypoint: `Devvit.configure` + scheduler job + triggers + settings/menu registration + the shared publish cycle. | -| `devvit/README.md` | DONE | Upload/playtest, settings table, parity/invariant matrix. | -| `devvit/src/types.ts` | DONE (NEW) | The shared contract every module imported but which did not exist — see §3. | - ---- - -## 2. Component modules (authored separately; wired this pass) - -| Module | State | Owner-of (invariants) | -|---|---|---| -| `storage.ts` | DONE + adapter layer added (§3) | INV-5, INV-6, INV-9, retention | -| `modlog.ts` | DONE (ingest/extract only — see §7b for the trigger-refetch gap) | INV-1, INV-2 (gating), INV-5, INV-7, INV-8 | -| `render.ts` | DONE for single-row rendering; **FR-7/FR-8 NOT implemented** (§7c) | INV-2 (emit), INV-3, INV-4, INV-6 (hash) | -| `wiki.ts` | DONE | INV-3 (guard), INV-6 | -| `settings.ts` | DONE | INV-1, INV-7, INV-8, INV-9 | -| `menu.ts` | DONE (signature drift fixed, §3) | — (thin adapter) | - ---- - -## 3. Contract reconciliation performed this pass - -The component modules were authored in parallel against the architecture spec, -but the spec's idealized names and `storage.ts`'s actual implementation had -**drifted**. The whole project would not have compiled or linked. The following -minimal, non-logic reconciliations were made so it builds: - -1. **`types.ts` created** — `modlog.ts`, `render.ts`, and `settings.ts` all - `import` from `./types.js`, but the file did not exist. Created it as the - single source of truth for `ModRecord`, `AppConfig`, `ModActionType`, - `DisplayKind`, and all constants (`WIKI_BYTE_CAP`, `WIKI_TRIM_TARGET`, - `ANON_LABEL`, `LITERAL_MODS`, `ANONYMIZE_MODERATORS`, `DEFAULT_*`, - `*_MIN/_MAX`, `VALID_MODLOG_ACTIONS`, `SCHEMA_VERSION`). No `@devvit/*` import - so the pure render layer stays platform-free. - -2. **Storage spec-name adapter layer** (appended to `storage.ts`) — the spec / - sibling modules call `markSeen` / `putRecord` / `getAllRecords` / - `getStatus` / `recordRunStarted` / `recordPublished`, but the implementation - defined `isProcessed` / `recordAction` / `getRecentActions` and **no** status - accessors. Added thin adapters: - - `markSeen` = `!isProcessed` (inverse sense: returns `true` when NEW). - - `putRecord` → `recordAction`. - - `getAllRecords` → `getRecentActions` with a default cap. - - `getStatus` / `recordRunStarted` / `recordPublished` → new `status` hash. - No existing logic was rewritten. - -3. **`render.ts` import extension** — `from './types'` → `from './types.js'`. - Under the Devvit base tsconfig's **NodeNext** resolution, relative specifiers - MUST carry the `.js` extension; the bare specifier was a compile error. - -### Reconciliations performed this pass (compiler-verified) - -4. **`modlog.ts` package + client threading fixed** — it imported - `{ reddit }` and `ModAction` from `@devvit/reddit` (the split-package / - Devvit-Web name), which does NOT exist in the classic - `@devvit/public-api@0.13.5` model and failed with `TS2307`. The classic model - has **no `reddit` singleton** — the Reddit client is `context.reddit` - (`RedditAPIClient`), and `ModAction`/`ModActionType` are re-exported from - `@devvit/public-api`. Fixed by importing types from `@devvit/public-api` and - threading a `RedditAPIClient` argument through `fetchActions(reddit, cfg)` and - `ingest(reddit, redis, cfg)` (mirrors `wiki.ts`). Callers updated: - `main.ts` (`runPublishCycle` + `ModAction` trigger now destructure `reddit` - and pass it), `menu.ts` (`handlePublishNow` passes `reddit`). - -5. **`menu.ts` signature drift fixed** — it called `loadConfig()` (0 args) and - `ingest(reddit, redis, cfg)` against a then-2-arg `ingest`. Now calls - `loadConfig(context.settings, context.subredditName)` (with an INV-9 - no-subreddit guard) and `ingest(reddit, redis, cfg)` against the corrected - 3-arg signature. - -After (4)+(5): `npm run type-check` passes with zero errors and `dist/` emits -for all 8 modules (forced clean rebuild verified). **Type-checking clean is not -the same as parity-complete or playtested — see §7.** - -### Known residual drift (cosmetic — non-blocking) - -- **`ModRecord` (types) vs `ModActionRecord` (storage)** are structurally - identical, so cross-passing type-checks today. Consider collapsing to one - named type to avoid future drift. -- **`wiki.ts` header comments** still reference the `@devvit/reddit` / - `@devvit/redis` split packages as a Devvit-Web TODO; the actual imports - correctly use `@devvit/public-api`. Documentation-only; harmless. - ---- - -## 4. Parity matrix (Python → Devvit) - -| Capability | Python | Devvit | State | -|---|---|---|---| -| Auth | password-grant OAuth | platform-managed | DONE (no code) | -| Mod-log fetch | `subreddit.mod.log(limit)` | `reddit.getModerationLog({limit,pageSize})` | DONE | -| Action filter (7 types) | client-side | client-side (`type` filter is single-valued) | DONE | -| Anonymize (INV-1) | enforced | hardcoded, no toggle | DONE | -| Profile-link ban (INV-2) | enforced | permalink only for t1/t3 | DONE | -| Reason censor/escape (INV-4) | regex | ported regex (`render`) | DONE | -| Markdown tables | per-day | per-day (`render.buildContent`) | DONE | -| Modmail prefill link | yes | `render.modmailLink` | DONE | -| 512 KB cap + trim (INV-3) | yes | `render.enforceByteCap` + `wiki` guard | DONE | -| Dedup (INV-5) | SQLite UNIQUE | Redis atomic NX (`markSeen`) | DONE | -| Wiki hash-skip (INV-6) | SHA-256 cache | SHA-256 cache, **behavior changed** (excludes timestamp — see §7e) | DONE, undisclosed behavior change | -| Retention (90d) | row delete | zset prune by score (`cleanupOld`) | DONE | -| Daemon loop | `update_interval` 600s | scheduler cron `*/10 * * * *` | DONE | -| Prompt-fast on action | n/a | `ModAction` trigger (ingest only) | DONE, but re-fetches full log every event (§7b) | -| Config (19 opts) | CLI/env/JSON | 6 install settings + 1 hardcoded | DONE | -| Multi-subreddit | single store | one install per sub (isolation) | DONE (by design) | -| Combined removal+reason rows (FR-7) | merged single row | not implemented | **NOT DONE** | -| Conditional approval rows (FR-8) | correlation-gated | not implemented | **NOT DONE** | -| CLI `--test` / `--force-*` | yes | menu "Publish now" (force/test variants partial) | PARTIAL | - ---- - -## 5. FR-7 / FR-8 — verified NOT implemented - -Executed the compiled pipeline against `render.ts` (`devvit/src/render.ts`) and -`modlog.ts` (`devvit/src/modlog.ts`): each `ModAction` maps 1:1 to one -`ModRecord` and one table row (`renderRow`, `render.ts:225-235`). There is no -correlation step anywhere in the pipeline that: - -- **FR-7** — merges a removal action and a subsequent `addremovalreason` on the - same content into one row (`devvit-migration/docs/01-requirements.md:102`). -- **FR-8** — suppresses an approval row unless it reverses a prior - Reddit/AutoMod removal, or annotates it "Approved `` removal[: reason]" - (`devvit-migration/docs/01-requirements.md:105`). - -The architecture spec assigns this to `render.ts` explicitly -(`devvit-migration/docs/04-architecture.md:136`, "approval-correlation render -(P-13)") and flags the Redis-lookup design in R-3 -(`devvit-migration/docs/01-requirements.md:187`, GAP-1 cross-reference). None of -that correlation/lookup code exists in `storage.ts` or `modlog.ts` either. -Previously marked DONE in error. - ---- - -## 6. Outstanding TODO before a real deploy - -1. ~~Fix `menu.ts` signature drift~~ — **DONE** (§3.4/§3.5); project type-checks - end-to-end. -2. ~~Install deps + type-check~~ — **DONE**: `npm install` (457 pkgs) + - `npm run type-check` pass clean against `@devvit/public-api@0.13.5`; `dist/` - emits. The import surface and the `getModerationLog` / `getWikiPage` / - `createWikiPage` / `updateWikiPage` / scheduler / settings call shapes are now - compiler-validated against the installed SDK types. -3. **Implement FR-7/FR-8** (§5) — combined removal+reason rows and - conditional approval rows. Needs the per-content secondary index design from - R-3 before it can be built. -4. **Resolve the platform-model contradiction** (§7a) before further build — - changes the import surface, so doing it after FR-7/FR-8 risks a second - rewrite. -5. **Verify remaining runtime call shapes against a live install** — types - compile, but these need playtest confirmation (behavior, not just types): - - `reddit.getModerationLog({ subredditName, limit, pageSize })` + `.all()` - (Listing drain — some versions use `for await` instead). - - `reddit.getWikiPage(sub, page)` throwing on absence; `createWikiPage` / - `updateWikiPage` option shapes (`{ subredditName, page, content, reason }`). - - `context.scheduler.listJobs()` / `cancelJob(id)` / `runJob({name, cron})`. - - `Devvit.addTrigger({ events: ['AppInstall','AppUpgrade'] })` and - `event: 'ModAction'` payload fields. - - `context.settings.get`, `context.subredditName` on scheduler/trigger ctx. -6. **Cron cadence**: confirm `*/10 * * * *` is permitted for the app tier; - tighten/loosen as policy allows (Python used 600s). -7. **Playtest** on a test subreddit (`npm run playtest`) — exercise: install → - settings save (validators) → menu "Publish now" → wiki page created → - second run hash-skips → mod action triggers ingest → retention prune. -8. **Unit tests** for the pure layers (`render.*`, `modlog.anonymizeMod` / - `deriveDisplay` / `extractRecord`, `settings` validators). `vitest` is wired - in `package.json` and one test file exists (`devvit/test/pipeline.test.ts`) - but CI does not run it (§7d). -9. **App review** before public listing (`devvit publish`). - ---- - -## 7. Known gaps / must-resolve before playtest or publish - -**(a) Platform-model contradiction.** `devvit-migration/docs/03-research-platform.md:11` -states: *"The classic `Devvit.addSchedulerJob` / `Devvit.addSettings` / -`Devvit.addTrigger` builder API (from version-0.11 docs) is the *old* model. -**Target the Devvit Web model.**"* The shipped code -(`devvit/package.json:19`, `devvit/src/main.ts:32,64-67,123,157,174`) is built -entirely on the deprecated classic `@devvit/public-api@0.13.5` builder API the -research doc says not to target. This needs a deliberate decision — stay on -classic (accept the doc contradicts the code and update the doc) or port to -Devvit Web — documented explicitly, not left implicit. - -**(b) `onModAction` trigger re-fetches the full moderation log.** -`devvit-migration/docs/04-architecture.md:246` (architecture §3) specifies the -trigger "does **NOT** call `getModerationLog`" and is "cheap, idempotent". The -actual trigger (`devvit/src/main.ts:174-191`) calls `ingest(reddit, redis, -cfg)`, and `ingest` (`devvit/src/modlog.ts:219-246`) unconditionally calls -`fetchActions`, which issues `reddit.getModerationLog({...}).all()` -(`devvit/src/modlog.ts:191-202`) — a full listing re-fetch on every single mod -action, not an incremental single-event ingest. Contradicts architecture §3 and -NFR-6 (`devvit-migration/docs/01-requirements.md:149`, "at most one -`updateWikiPage` write" per normal incremental run — this doesn't bound -`getModerationLog` calls the same way and multiplies read-API load under -active moderation). - -**(c) FR-7/FR-8 unimplemented.** See §5 for the verified detail. - -**(d) No CI for `devvit/**`.** `.github/workflows/` has no workflow that runs -`npm run type-check` or `npm test` against `devvit/` -(`docker-build.yml` and `pre-commit.yml` are the only workflows; neither -references `devvit`). The type-check-clean claim in this document has been -verified locally only, and will silently regress on the next change with no CI -gate to catch it. - -**(e) GAP-1 unverified against a live event.** `addremovalreason` reason-field -extraction priority (`details` → `description`, -`devvit-migration/docs/04-architecture.md:305`) is implemented -(`devvit/src/modlog.ts:119-129`) but per the architecture doc's own sign-off -gate, "MUST confirm against one live `addremovalreason` event" before Phase 1 -sign-off. No live event has been captured; this is still an assumption. - -**(f) Undisclosed hash-skip behavior change vs legacy.** The legacy Python -`get_content_hash` (`modlog_wiki_publisher.py:368-370`) hashes the full -rendered content, timestamp header included. The Devvit `contentHash` -(`devvit/src/render.ts:416-428`) deliberately strips the `**Last Updated:**` -line before hashing so the hash is stable across runs with no content change. -This is very likely the *correct* fix (the legacy behavior effectively -disables hash-skip, since the timestamp always differs), but it is a real -functional behavior change from legacy, not a straight port, and was not -called out anywhere as a deliberate, disclosed change until now. - ---- - -## 8. Dropped legacy options (intentional, no parity needed) - -`client_id`, `client_secret`, `username`, `password` (platform auth); -`source_subreddit` (install context, INV-9); `update_interval` (scheduler); -`wiki_display_days` (folded into `retention_days`); `max_continuous_errors`, -`rate_limit_buffer`, `max_batch_retries` (no daemon; platform-managed retry/ -rate-limit); `archive_threshold_days`, `database_path` (Redis, no SQLite); -`display_format` (fixed render). `anonymize_moderators` is hardcoded `true` -(INV-1), not a setting. +| --- | --- | +| Status | Ported to Devvit Web. Type-check, unit tests, build and Python parity pass offline. Not yet uploaded or playtested. | +| Date | 2026-10-15 | +| Branch | `feat/devvit-web` | +| App name | `autowikimod` (`devvit/devvit.json`) | +| Code root | `devvit/` (`@devvit/web` 0.14.8, Hono server, `@devvit/start` vite build to `dist/server/index.cjs`) | +| Legacy source | `modlog_wiki_publisher.py` (unchanged; the parity reference) | + +## Done + +| Area | State | +| --- | --- | +| Platform model | Devvit Web: `devvit.json` replaces `devvit.yaml`; all handlers are `/internal/*` routes on a Hono server; `reddit`, `redis`, `settings`, `context` come from `@devvit/web/server`. | +| Schedule | Cron task `publish-modlog`, `*/20 * * * *`. | +| `onModAction` | Dropped. Its payload has no `details`/`description` field, so it cannot carry removal-reason text, and the cron already reads the full mod log. One code path, as in Python. | +| Ingest | Up to 500 entries per run, stored oldest first; `hSetNX` on the `act` hash is the dedup gate. Entries older than retention are skipped. | +| Render | `src/core/render.ts` ports `build_wiki_content` and helpers. Byte-identical to Python on the parity fixture. | +| FR-7 | Removal plus `addremovalreason` on the same target render as one row with the reason. | +| FR-8 | Approval rows render only when they reverse a Reddit/AutoModerator removal of the same target, read from the `prior` Redis hash keyed by bare post/comment id. | +| Hash skip | SHA-256 of the page without the `Last Updated` line, per page, in the `wikihash` hash. Same as current Python `get_content_hash`. | +| Size | Oldest-day trimming at 90% of 512 KiB; refuses to grow a page already above 95%. | +| Settings | Wiki page (default `modlog-devvit`), days shown (90), days kept (90), action list (Python defaults), anonymize (on; turning it off is refused), ignored moderators (empty). Each has a validation endpoint. | +| Menu | Subreddit menu, moderators only: Publish now, Force rebuild (no fetch, forced write, like `--force-wiki`), Test connection. | +| Concurrency | `lock` hash with a 120 s stale takeover, so the cron and a menu click cannot publish at once. | +| Tests | 42 Vitest tests in 5 files: render, FR-7/FR-8 correlation, settings, Redis store (`@devvit/test` harness), publish cycle, golden parity. | +| Parity | `devvit/test/parity/golden.md` is produced by `python_render.py`, which runs the fixture through the unmodified Python functions. CI regenerates it with uv and diffs it, and the TS pipeline must equal it byte for byte. | +| CI | `.github/workflows/devvit.yml` on the org `hetzner` runner: `npm ci`, type-check, test, build, Python golden check. | + +## Deliberate differences from Python + +| Item | Python | Devvit | Why | +| --- | --- | --- | --- | +| FR-8 target match | `target_permalink LIKE '%%'` | exact bare post/comment id | The `LIKE` also matches comments under an approved post, so a post approval could cite a comment's AutoModerator removal. | +| Reason source | `description` (reason action), else `mod_note`, else `details` | `description` (reason action), else `details` | Devvit `ModAction` has no `mod_note` field. | +| Moderator stored | real name in SQLite | anonymized label in Redis | Anonymization cannot be turned off, so the real name is never needed. | +| Ignored moderators | code default `[]`; `config_template.json` ships `["AutoModerator"]` | default empty | Matches the code default. Set `AutoModerator` in settings if the live page must match a config that ignores it. With AutoModerator ignored, approvals of AutoModerator removals have nothing to reverse and are dropped, in both apps. | +| Action list | stored but not used for the page (`show_only_removals=True`) | drives the page filter | Same output at the default list. | +| Fetch size | 50 per 10 min | 500 per 20 min | Fewer, larger runs; dedup makes overlap harmless. | + +## Needs a live playtest + +Typed and unit-tested offline, but not yet run on Reddit: + +1. `reddit.getModerationLog({ subredditName, limit: 500, pageSize: 100 }).all()` on r/OpenSignups, including the `targetPermalink`/`targetAuthor` values and that `addremovalreason` text arrives in `description`. +2. `getWikiPage` throwing for a missing `modlog-devvit` page, then `createWikiPage` creating it as the app account. +3. `updateWikiPage` on the second write, and wiki permissions for the app account. +4. The cron firing every 20 minutes after install, and a run finishing inside the 30 s request limit at 500 entries. +5. Settings form: defaults shown, validators rejecting bad input, multiSelect value shape. +6. Menu toasts for all three items. +7. Side-by-side diff of `wiki/modlog-devvit` against the live Python `wiki/modlog` after both have run on the same day. + +## Operator steps (from `devvit/`) + +1. `npm ci` +2. `npx devvit login` +3. `npx devvit upload` +4. `npx devvit install r/OpenSignups autowikimod` +5. If step 4 is refused because r/OpenSignups has 200+ subscribers, run `npx devvit publish` (unlisted review), wait for approval, then repeat step 4. +6. In r/OpenSignups, open the subreddit three-dot menu, run **Modlog: Test connection**, then **Modlog: Publish now**. +7. `npx devvit logs r/OpenSignups autowikimod --since 1h` +8. Compare `https://www.reddit.com/r/OpenSignups/wiki/modlog-devvit` with `wiki/modlog`. +9. At cutover: set **Wiki page name** to `modlog` and stop the Python container. + +## Open after playtest + +- Public listing (`devvit publish --public`) if other subreddits will install it. +- Remove the Python daemon and Docker image once the Devvit page has run in place of `modlog`. +- `devvit-migration/docs/01..04` describe the classic-model design. They are kept for history, and this file reflects the current design. From b652cec72c4e1d414f2df4ab08b90938921210fe Mon Sep 17 00:00:00 2001 From: bakerboy448 <55419169+bakerboy448@users.noreply.github.com> Date: Sat, 17 Oct 2026 22:05:30 -0500 Subject: [PATCH 5/5] chore(devvit): name package autowikimod, set dev subreddit Co-Authored-By: Claude Sonnet 5.5 --- devvit/devvit.json | 3 + devvit/package-lock.json | 484 +-------------------------------------- devvit/package.json | 6 +- 3 files changed, 11 insertions(+), 482 deletions(-) diff --git a/devvit/devvit.json b/devvit/devvit.json index fd1ec9f..6137624 100644 --- a/devvit/devvit.json +++ b/devvit/devvit.json @@ -276,5 +276,8 @@ "scripts": { "build": "vite build", "dev": "vite build --watch" + }, + "dev": { + "subreddit": "autowikimod_dev" } } diff --git a/devvit/package-lock.json b/devvit/package-lock.json index d0d5e24..3770be4 100644 --- a/devvit/package-lock.json +++ b/devvit/package-lock.json @@ -1,13 +1,13 @@ { - "name": "redditmodlog-devvit", - "version": "0.1.0", + "name": "autowikimod", + "version": "0.0.0", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "redditmodlog-devvit", - "version": "0.1.0", - "license": "GPL-3.0", + "name": "autowikimod", + "version": "0.0.0", + "license": "BSD-3-Clause", "dependencies": { "@devvit/start": "0.14.8", "@devvit/web": "0.14.8", @@ -1052,422 +1052,6 @@ "web-vitals": "4.2.4" } }, - "node_modules/@esbuild/aix-ppc64": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.2.tgz", - "integrity": "sha512-XExcO+dvLKvVtNTibSTBej1NCAbaGhWn9Ww1ZPx80qsahhPFe/8jgWP0IchNe0F3HwkU7n8ejhH8bjonqht8mQ==", - "cpu": [ - "ppc64" - ], - "extraneous": true, - "license": "MIT", - "os": [ - "aix" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-arm": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.2.tgz", - "integrity": "sha512-kXXoiPVVGQcnIYGOeaovwOURpniDBpSq4A03qkQ+BMQqtGG6HYap3xne9C1O1yo4TR3qxlCX5IqqmX6fFo2Lqg==", - "cpu": [ - "arm" - ], - "extraneous": true, - "license": "MIT", - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-arm64": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.2.tgz", - "integrity": "sha512-5YfKeeI8qWfBZIX+u2xZC3Zlb3Os/gLS2sbEKM+I4ZOcsWmHS2WLysCcQZDAFRslDUU5Oiq44gf6PYN1vGwG5A==", - "cpu": [ - "arm64" - ], - "extraneous": true, - "license": "MIT", - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-x64": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.2.tgz", - "integrity": "sha512-O387ite7SzUyCcy3JQX4P4bLtEA7bLLkx+esve5JHnyYfNTxcVpXZo9jhdB0lTKN44gztELTdU7nS8Nr16Fs1Q==", - "cpu": [ - "x64" - ], - "extraneous": true, - "license": "MIT", - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/darwin-arm64": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.2.tgz", - "integrity": "sha512-n4KqkOQrraxHJcgjM1RvwbigfQKIKJVpM7xp+KsxiyUSrRdIXnt73VhrPAx0fV44hgfmIVKjxMN9J1t5jySVkw==", - "cpu": [ - "arm64" - ], - "extraneous": true, - "license": "MIT", - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/darwin-x64": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.2.tgz", - "integrity": "sha512-uq6suIWYP37qzGddBKPw5QEQPi6HiLGsO7UmkpfyaYNQ3D+rN6w6WfwH+nuqcGXWvawGwxOEroO4YGnFh95azw==", - "cpu": [ - "x64" - ], - "extraneous": true, - "license": "MIT", - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/freebsd-arm64": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.2.tgz", - "integrity": "sha512-n+I0BTSRIoy+d6RPKnEVwql5UwBJolytvY4mAOIEJorKlqgPII8ix6slVVrfZ5Tnj7glIZvloylbB/EJPMWEXw==", - "cpu": [ - "arm64" - ], - "extraneous": true, - "license": "MIT", - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/freebsd-x64": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.2.tgz", - "integrity": "sha512-78XJTJkvPs0kz2w61301PJjXl4g7q3JqiYMZ/M/yVI73EHBrCRTgkhu9oqG7vPqq+a/yadEW8aD+agKlk5xrmg==", - "cpu": [ - "x64" - ], - "extraneous": true, - "license": "MIT", - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-arm": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.2.tgz", - "integrity": "sha512-XlDnu2q5yoqems+xay6wSAcg9DDD7K9RLKZEBOMZm3ckNpJBvOX20tSfby8KfrrhINDyv9V2YVZKY/SpoGJI8w==", - "cpu": [ - "arm" - ], - "extraneous": true, - "license": "MIT", - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-arm64": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.2.tgz", - "integrity": "sha512-pW4AC0P3it8c7do9MVM4p51FzHzdM/TZrerurgRcHJ2WTa1VQ1CIq18xncfpBJw4ojkiZZrKW2yIBWBP92j6Ug==", - "cpu": [ - "arm64" - ], - "extraneous": true, - "license": "MIT", - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-ia32": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.2.tgz", - "integrity": "sha512-CYbnj78HsIeA+DhgUKgFCfvNsTHFhMMrinUrMZpDXJXKN8T3XViTZ/+wtHeVxEWY8ewSzTFN+nRmSwO2tZaLUQ==", - "cpu": [ - "ia32" - ], - "extraneous": true, - "license": "MIT", - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-loong64": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.2.tgz", - "integrity": "sha512-buwkd8nsph4R+ajRvw0qM5Hja/TXQow3ptzWO2EbG/cqcIkHloRrdlBtQlshyYGTNFvfkfJ5tpPLVkY4DtsPfQ==", - "cpu": [ - "loong64" - ], - "extraneous": true, - "license": "MIT", - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-mips64el": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.2.tgz", - "integrity": "sha512-ZVykbDyk7519VwiNb9Lcj9m8XM6v5V9uKPvrEMkkEedVewf+0itkhahp4HDpgERXhwLRpWFypsGbG/J8s0QjJA==", - "cpu": [ - "mips64el" - ], - "extraneous": true, - "license": "MIT", - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-ppc64": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.2.tgz", - "integrity": "sha512-CAXl+Dtd9UUuJd8pKKdwh6MLm3MUMiqMPmhZ3tTSXPqfyQ3vDl6R5hZdZ/kYojK4ofXtdfSv1tFq8XzWx3heNQ==", - "cpu": [ - "ppc64" - ], - "extraneous": true, - "license": "MIT", - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-riscv64": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.2.tgz", - "integrity": "sha512-GeXCej4IQtU1B+QlDV8W/RRvbzI3O/Stss+/bCXv4lZls5WGRtu2a+3JkA3i4qIUlMXpcHebWpF8AkJhATowuA==", - "cpu": [ - "riscv64" - ], - "extraneous": true, - "license": "MIT", - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-s390x": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.2.tgz", - "integrity": "sha512-3H1weTYZPxt/WOhByszQZybS9w5lKzUn1FDMsgEChbHWQwHYQQRfBxgCcZvPhjHfKyJjIievvMmEUawJrdY9Dg==", - "cpu": [ - "s390x" - ], - "extraneous": true, - "license": "MIT", - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-x64": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.2.tgz", - "integrity": "sha512-4xTZr1FUmSoQW4XIWmit3tzQrUTZM+N3P0XV8xROKYF50XfI7xeO90+1bZvNwxIufQ9hDQVRJH5YhgPVF8A/HQ==", - "cpu": [ - "x64" - ], - "extraneous": true, - "license": "MIT", - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/netbsd-arm64": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.2.tgz", - "integrity": "sha512-sSATRjPeDBg3pdgHoQfoYBob11Kk1FGa9lui5RIHZCoCkJa9QKlvl3/vKz2usCmYYjs7ymJR/2Nnsqe+Hjt5nw==", - "cpu": [ - "arm64" - ], - "extraneous": true, - "license": "MIT", - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/netbsd-x64": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.2.tgz", - "integrity": "sha512-lqnzCV+mM0gIADaKihiCg6ifgfU2L3h5E33rNQBN1Y4MaVGnzryzmvvf7UHxprpQdE8hpqLolJ9Rl+SkIRDpyw==", - "cpu": [ - "x64" - ], - "extraneous": true, - "license": "MIT", - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openbsd-arm64": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.2.tgz", - "integrity": "sha512-AL2qJILH7lNjrDmCQDvdxMfAUIv8KMNZOvrwAQ8i8//ntL9FflhOyMJ8OZSMBb8/AWXe3/5v5S20y3zCoZWKoQ==", - "cpu": [ - "arm64" - ], - "extraneous": true, - "license": "MIT", - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openbsd-x64": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.2.tgz", - "integrity": "sha512-QtiuPytchRyC4rwUKhexJdQKvDuZ6hWloi3igqPQNUJCS1/v9EiO3UTOXR6A3FoMo4fnAKbWJdqaIwhOzh8qEw==", - "cpu": [ - "x64" - ], - "extraneous": true, - "license": "MIT", - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openharmony-arm64": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.2.tgz", - "integrity": "sha512-WkhYDmpTjLvGlScA1rwjRUmhl4k8oXR3cIbtqWmELgU/dFeHHlEllxDvdWcNJV9rbzCexB5vz8gtNewWLgCT7Q==", - "cpu": [ - "arm64" - ], - "extraneous": true, - "license": "MIT", - "os": [ - "openharmony" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/sunos-x64": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.2.tgz", - "integrity": "sha512-GPMSkTOtMnv2U2F8gxe4Io6qmVs+YKyp832Etqqxr0hFngmXQ3rzwytelm3GIn7T4VviRUlf3sOgBOiTdvaf7g==", - "cpu": [ - "x64" - ], - "extraneous": true, - "license": "MIT", - "os": [ - "sunos" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-arm64": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.2.tgz", - "integrity": "sha512-PIhhEkE9uPBleRBrQEJpUn7MBnibZzbGzYWPmY3x+YoVg/95zbjB4CxPPOQ8l5tYYM4mMaCthF8/1DIfBQQyWQ==", - "cpu": [ - "arm64" - ], - "extraneous": true, - "license": "MIT", - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-ia32": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.2.tgz", - "integrity": "sha512-YmJbfTlvU7Sdn9BB+4PRES4oB6pxgS37MAONj+hBr/cpXS1aBPKXxNnDbu+QCWPj0o9dgyxeq79g6c5P8KeuYA==", - "cpu": [ - "ia32" - ], - "extraneous": true, - "license": "MIT", - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-x64": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.2.tgz", - "integrity": "sha512-5ebpxr3nWMzrL/rnUI755Jkuee0bHL/Gq0WTF9lvcpv73wAp5eu8MfBUgWK9bhWvZjj7yX8etf/8tI8Ney695g==", - "cpu": [ - "x64" - ], - "extraneous": true, - "license": "MIT", - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, "node_modules/@hono/node-server": { "version": "2.1.4", "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-2.1.4.tgz", @@ -3500,48 +3084,6 @@ "node": ">= 0.4" } }, - "node_modules/esbuild": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.2.tgz", - "integrity": "sha512-HKVLS8dvII+xoKW9kmqxbRKrnWEXfJJr/FZhhJmiqIB0e053QNYFqOBouTMO/k5sID4MvCiUCvv8b9M4h32wIA==", - "extraneous": true, - "hasInstallScript": true, - "license": "MIT", - "bin": { - "esbuild": "bin/esbuild" - }, - "engines": { - "node": ">=18" - }, - "optionalDependencies": { - "@esbuild/aix-ppc64": "0.28.2", - "@esbuild/android-arm": "0.28.2", - "@esbuild/android-arm64": "0.28.2", - "@esbuild/android-x64": "0.28.2", - "@esbuild/darwin-arm64": "0.28.2", - "@esbuild/darwin-x64": "0.28.2", - "@esbuild/freebsd-arm64": "0.28.2", - "@esbuild/freebsd-x64": "0.28.2", - "@esbuild/linux-arm": "0.28.2", - "@esbuild/linux-arm64": "0.28.2", - "@esbuild/linux-ia32": "0.28.2", - "@esbuild/linux-loong64": "0.28.2", - "@esbuild/linux-mips64el": "0.28.2", - "@esbuild/linux-ppc64": "0.28.2", - "@esbuild/linux-riscv64": "0.28.2", - "@esbuild/linux-s390x": "0.28.2", - "@esbuild/linux-x64": "0.28.2", - "@esbuild/netbsd-arm64": "0.28.2", - "@esbuild/netbsd-x64": "0.28.2", - "@esbuild/openbsd-arm64": "0.28.2", - "@esbuild/openbsd-x64": "0.28.2", - "@esbuild/openharmony-arm64": "0.28.2", - "@esbuild/sunos-x64": "0.28.2", - "@esbuild/win32-arm64": "0.28.2", - "@esbuild/win32-ia32": "0.28.2", - "@esbuild/win32-x64": "0.28.2" - } - }, "node_modules/escape-html": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", @@ -7982,22 +7524,6 @@ "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", "license": "MIT" }, - "node_modules/yaml": { - "version": "2.9.1", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.9.1.tgz", - "integrity": "sha512-3NxN8+78OdzbT7C/WjGsyfPAtJaN3FNDsWxv7Y7mcDsT/oOmgW8BpyQQFFBnvZE3j9Y2Sdz1ULFLezL7Eb2yFw==", - "extraneous": true, - "license": "ISC", - "bin": { - "yaml": "bin.mjs" - }, - "engines": { - "node": ">= 14.6" - }, - "funding": { - "url": "https://github.com/sponsors/eemeli" - } - }, "node_modules/yn": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", diff --git a/devvit/package.json b/devvit/package.json index 56139e9..afc4355 100644 --- a/devvit/package.json +++ b/devvit/package.json @@ -1,8 +1,8 @@ { "private": true, - "name": "redditmodlog-devvit", - "version": "0.1.0", - "license": "GPL-3.0", + "name": "autowikimod", + "version": "0.0.0", + "license": "BSD-3-Clause", "type": "module", "description": "Publishes a subreddit's moderation log to a wiki page (Reddit Devvit Web app).", "scripts": {