diff --git a/.instagram-mcp.bak/.env.example b/.instagram-mcp.bak/.env.example new file mode 100644 index 00000000..c3929e14 --- /dev/null +++ b/.instagram-mcp.bak/.env.example @@ -0,0 +1,38 @@ +# instagram-mcp-buddy — environment (git builds vs npm) + +# Published npm releases embed a Meta app via `scripts/inject-credentials.mjs`. +# A plain git clone has REPLACE_AT_BUILD placeholders until you: +# - add INSTAGRAM_MCP_APP_ID + INSTAGRAM_MCP_APP_SECRET to this file and run +# `npm run build` (the CLI loads .env on startup), or +# - run `npm run build:inject` / `npm run inject:credentials` after `tsc`. + +# Everything below is for power users (your own Meta app) or maintainers. + +# --- Local / git builds without injected Meta app --- +# This build delegates connect to `npx -y instagram-mcp-buddy` (needs network). +# Set to 1 to disable that fallback (you will need INSTAGRAM_MCP_APP_* instead). +# INSTAGRAM_MCP_NO_NPX_FALLBACK=1 +# +# Internal: set by the parent when spawning npx — do not set manually. +# INSTAGRAM_MCP_NPX_DELEGATE_CHILD=1 + +# --- Required for git checkouts (uncommented lines — parser ignores # KEY=...) --- +# Paste your Meta app id and app secret below (from developers.facebook.com). +INSTAGRAM_MCP_APP_ID= +INSTAGRAM_MCP_APP_SECRET= + +# --- Optional --- +# Useful if you (the app owner / admin / tester role) want to call publishing, +# comment-moderation, or messaging tools BEFORE Meta App Review approves them +# for the public. These have no effect for users who don't have the scopes +# granted to their token. +# INSTAGRAM_MCP_ENABLE_PUBLISHING=false +# INSTAGRAM_MCP_ENABLE_COMMENTMODERATION=false +# INSTAGRAM_MCP_ENABLE_MESSAGING=false + +# --- Tunables (optional) --- +# IG_GRAPH_API_VERSION=v21.0 +# IG_DEFAULT_TIMEOUT_MS=30000 +# IG_REEL_TIMEOUT_MS=300000 +# IG_IMAGE_TIMEOUT_MS=60000 +# LOG_LEVEL=info diff --git a/.instagram-mcp.bak/.gitignore b/.instagram-mcp.bak/.gitignore new file mode 100644 index 00000000..c27176c8 --- /dev/null +++ b/.instagram-mcp.bak/.gitignore @@ -0,0 +1,12 @@ +node_modules/ +dist/ +*.log +.env +.env.* +!.env.example +.DS_Store +.vscode/ +.idea/ +coverage/ +token.json +*.tar.gz diff --git a/.instagram-mcp.bak/LICENSE b/.instagram-mcp.bak/LICENSE new file mode 100644 index 00000000..45398a58 --- /dev/null +++ b/.instagram-mcp.bak/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 instagram-mcp contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/.instagram-mcp.bak/README.md b/.instagram-mcp.bak/README.md new file mode 100644 index 00000000..77ab32b0 --- /dev/null +++ b/.instagram-mcp.bak/README.md @@ -0,0 +1,217 @@ +# instagram-mcp-buddy + +**Instagram for AI agents in OpenSwarm.** Sign in once with your Instagram Business or Creator account and your agents can pull insights, monitor comments, and research hashtags on your own Instagram. No API keys. No Meta Developer app. No `.env`. + +``` +npx -y instagram-mcp-buddy +``` + +That's the whole setup. The first time your agent calls `instagram_connect`, a browser opens, you log in, you're done. The 60-day access token is stored in your OS keychain on your own machine and refreshes itself. + +--- + +## Quickstart for OpenSwarm + +1. Open **OpenSwarm → Tools → Featured → Instagram → Add**. +2. Start a new agent chat and ask: *"connect Instagram."* The agent runs `instagram_connect`, a browser tab opens, you log in. +3. Now ask anything: + - *"what were my top 3 posts last week?"* + - *"summarize comments on my latest reel"* + - *"is there a trending hashtag in my niche I should jump on?"* + - *"compare my engagement to @somecompetitor"* + +That's it. There is no step 4. + +--- + +## What it can do today (v0.1.x) + +Read-only baseline: + +- **Account insights** — reach, profile views, accounts engaged, follow / unfollow trends, audience demographics over any date range. +- **Per-post analytics** — likes, comments, shares, saves, reach. Reels also expose views and average watch time. +- **Comment intelligence** — list comments and replies on any of your posts (no moderation actions yet). +- **Hashtag research** — top posts and recent posts for any hashtag, plus a stable id you can reuse. +- **Competitor / partner lookup** — read any public Business or Creator account by username with `instagram_business_discovery`. +- **Mention monitoring** — read comments and posts that @-tagged your account. +- **Account info** — `instagram_who_am_i` and `instagram_status` so agents can sanity-check what's available. + +Full per-tool reference: [`TOOLS.md`](TOOLS.md). + +## Coming soon + +These features ship inside the same `npx` package — your agents pick them up automatically the next time `npx -y instagram-mcp-buddy` resolves the latest version. No action on your end. + +- **v0.2.x** — publishing (image, carousel, reel, story, scheduling containers, delete). +- **v0.3.x** — comment moderation (reply, hide, delete, toggle comments on a post). +- **v0.4.x** — DMs (list conversations, list messages, send within 24h window). + +Each unlock waits on Meta App Review approving the corresponding permission. Until then, the tools simply aren't registered in the MCP server. + +--- + +## Quickstart for Claude Desktop / Cursor / Claude Code + +Add to your MCP config: + +```json +{ + "mcpServers": { + "instagram": { + "command": "npx", + "args": ["-y", "instagram-mcp-buddy"] + } + } +} +``` + +Locations: +- **Claude Desktop** — `~/Library/Application Support/Claude/claude_desktop_config.json` (macOS) or `%APPDATA%\Claude\claude_desktop_config.json` (Windows). +- **Cursor** — `~/.cursor/mcp.json` (global) or `.cursor/mcp.json` in your project. +- **Claude Code** — `~/.claude/mcp.json` or per-project `.claude/mcp.json`. + +Restart the client. Your agent should see the `instagram_*` tools. Start with `instagram_connect`. + +--- + +## Requirements + +- **Node 18 or later.** That's it. `npx` handles the rest. +- An **Instagram Business or Creator account**. Personal accounts are not supported by the Instagram Graph API. Switch via the Instagram app: *Settings → Account → Account type*. + +--- + +## Privacy + +Your Instagram access token never leaves your machine. + +- **Primary storage**: your OS keychain (macOS Keychain, Windows Credential Manager, Linux libsecret) under the service name `instagram-mcp-buddy`. +- **Fallback** (Linux without libsecret, headless servers): an AES-256-GCM encrypted file at `$XDG_DATA_HOME/instagram-mcp-buddy/token.json`. The encryption key is derived from your machine's stable id. + +No tokens are sent to any server we operate. The only network calls this package makes are to `instagram.com` and `graph.instagram.com`. + +You can wipe the stored token at any time by asking your agent to *"log out of Instagram"* — it calls `instagram_logout`. + +--- + +## Advanced: bring your own Meta app + +Most users should never need this. But if you want to point the package at your own Meta Developer app — for staging environments, internal testing, or pre-review feature flagging — use either: + +**Runtime (env or `.env` file):** the CLI loads `instagram-mcp/.env` when you run `node dist/index.js` (see `src/env-bootstrap.ts`). You can still export vars in the shell before `npx` if you prefer: + +```bash +INSTAGRAM_MCP_APP_ID=your_app_id +INSTAGRAM_MCP_APP_SECRET=your_app_secret +# Pre-review unlocks for your own admin/tester account: +INSTAGRAM_MCP_ENABLE_PUBLISHING=true +INSTAGRAM_MCP_ENABLE_COMMENTMODERATION=true +INSTAGRAM_MCP_ENABLE_MESSAGING=true +``` + +Tunables (rarely needed): + +```bash +IG_GRAPH_API_VERSION=v21.0 # default +IG_DEFAULT_TIMEOUT_MS=30000 +IG_REEL_TIMEOUT_MS=300000 # 5 min, for reel container polling +IG_IMAGE_TIMEOUT_MS=60000 +LOG_LEVEL=info # debug | info | warn | error +``` + +--- + +## Troubleshooting + +**"This tool is not available in the current build."** +Feature flag is off in this version. Run `npx -y instagram-mcp-buddy@latest` to pick up the latest features. See [Coming soon](#coming-soon). + +**Browser opens but nothing happens after I log in.** +Your firewall may be blocking the localhost callback. The server tries port `54321` first, then random ports. Check stderr logs for the URL it bound to. If you're behind a corporate firewall, the OAuth flow needs `localhost:54321` (or similar) to be reachable from your browser. + +**"Not connected to Instagram."** +Agent forgot to call `instagram_connect` first. Tell it to. (Or your token expired — also resolved by `instagram_connect`.) + +**Keychain prompt every time.** +macOS asks once per process for keychain access; click *Always allow*. If you're running headless on Linux without `libsecret`, the package transparently falls back to an encrypted file — no prompt. + +**Token expired.** +Long-lived Instagram tokens last 60 days. The package auto-refreshes within 7 days of expiry. If you go offline for more than 60 days, just call `instagram_connect` again. + +**Personal Instagram account.** +The Instagram Graph API doesn't support personal accounts. Switch to a Creator or Business account in the Instagram app's settings — it's free, takes 30 seconds, and doesn't change anything user-facing. + +--- + +## Developing from a git clone (OpenSwarm) + +`instagram-mcp` is a **subfolder of this monorepo**, not `~/instagram-mcp`. From your clone root: + +```bash +cd instagram-mcp +``` + +Checkouts ship **without** embedded Meta credentials. Create `instagram-mcp/.env` from `.env.example`, set `INSTAGRAM_MCP_APP_ID` and `INSTAGRAM_MCP_APP_SECRET`, then `npm run build` and `node dist/index.js connect` — or run `npm run build:inject` to rewrite `dist/oauth-config.js` using the same `.env` / shell vars (`inject:credentials` accepts `META_APP_*` or `INSTAGRAM_MCP_APP_*`). + +Maintainer publish (credentials in the npm tarball): + +```bash +META_APP_ID=... META_APP_SECRET=... npm run publish:npm +``` + +--- + +## Publishing (maintainer notes) + +The package ships with `META_APP_ID` and `META_APP_SECRET` embedded into `dist/oauth-config.js` at publish time. The placeholders `REPLACE_AT_BUILD` in source are rewritten by `scripts/inject-credentials.mjs`. + +```bash +META_APP_ID=... META_APP_SECRET=... npm run publish:npm +``` + +The script: +- Fails (exit 1) if `META_APP_ID` / `META_APP_SECRET` or `INSTAGRAM_MCP_APP_ID` / `INSTAGRAM_MCP_APP_SECRET` are missing (values may come from `instagram-mcp/.env` when you run `npm run inject:credentials`). +- Surgically replaces only the two `export const` fallback strings — the literal in `hasInjectedCredentials()` survives intentionally. +- Logs `sha256(dist/oauth-config.js)` and the prefix+length of each credential so you can verify what landed in the artifact. + +### Maintainer hygiene + +- Run two Meta apps: one for local dev (creds in your untracked `.env`), one for production npm publishes (creds pulled from a password manager at publish time only). +- Store the production secret in 1Password / Bitwarden / equivalent. Never plaintext on disk between publishes. +- Rotate the prod secret every ~6 months. Rotate immediately if abuse is suspected. + +### Security notes + +The app secret WILL ship inside the published npm artifact. This is the standard installed-app OAuth tradeoff — Spotify desktop, Vercel CLI, Notion, gh CLI all do the same. The actual security boundary is the **per-user access token**, which is generated client-side via OAuth and stored only in each user's OS keychain on their own machine. + +### Version bump cadence + +| Version | Adds | +|---------|------| +| v0.1.x | Read-only baseline. 18 tools. | +| v0.2.x | + Publishing (image / carousel / reel / story / containers / delete). 27 tools. | +| v0.3.x | + Comment moderation (reply / hide / delete / toggle). 31 tools. | +| v0.4.x | + DMs (list conversations / list messages / send). 34 tools. | + +Each bump waits on the corresponding Meta App Review approval. Bump cycle: + +1. Meta approves a permission. +2. Flip the matching flag to `true` in `src/feature-flags.ts`. +3. Bump version, update `CHANGELOG.md`, `npm run publish:npm`. +4. Users on `npx` get the new tools on their next invocation. + +### Meta App Review submission order + +In order of escalating reviewer rigor: + +1. `instagram_business_basic` +2. `instagram_business_manage_insights` +3. `instagram_business_manage_comments` +4. `instagram_business_content_publish` +5. `instagram_business_manage_messages` + +--- + +## License + +MIT. See [`LICENSE`](LICENSE). diff --git a/.instagram-mcp.bak/TOOLS.md b/.instagram-mcp.bak/TOOLS.md new file mode 100644 index 00000000..4264af0c --- /dev/null +++ b/.instagram-mcp.bak/TOOLS.md @@ -0,0 +1,158 @@ +# instagram-mcp-buddy — Tool reference + +Tools registered at the MCP server depend on which feature flags are enabled in the current npm build. As of v0.1.x, **18 of 34** tools ship enabled — the read-only baseline. The rest light up as Meta App Review clears the corresponding permissions. + +Legend: + +- **always-on** — registered in every build, including v0.1.0. +- **publishing** — gated on the `publishing` flag (Meta `instagram_business_content_publish`). +- **commentModeration** — gated on the `commentModeration` flag (write half of `instagram_business_manage_comments`). +- **messaging** — gated on the `messaging` flag (`instagram_business_manage_messages`). + +--- + +## Auth + +### `instagram_connect` · always-on · destructive +Sign in to Instagram. Opens the browser, runs OAuth, stores the long-lived token in the OS keychain. Returns the connected `username`, `ig_user_id`, `granted_scopes`, and the list of `enabled_features` in this build. + +### `instagram_logout` · always-on · destructive +Clear the stored access token from this machine. + +### `instagram_status` · always-on · read-only, idempotent +Report whether Instagram is connected and which capabilities are available. Safe to call before `instagram_connect`. Agents should hit this first. + +--- + +## Account + +### `instagram_who_am_i` · always-on · read-only, idempotent +Authenticated account profile: `ig_user_id`, `username`, `name`, `account_type`, follower / follows / media counts, profile picture, biography, website. + +--- + +## Media + +### `instagram_get_media` · always-on · read-only, idempotent +Full details for one of your posts: like / comment counts, caption, permalink, timestamp, comment-enabled flag. + +### `instagram_list_recent_media` · always-on · read-only, idempotent +Your posts in reverse chronological order, paginated with `after_cursor`. + +### `instagram_delete_media` · publishing · destructive +Permanently delete one of your posts. Irreversible — agents should confirm with the user first. + +### `instagram_toggle_comments` · commentModeration · destructive +Enable or disable comments on a post. + +--- + +## Insights + +### `instagram_get_media_insights` · always-on · read-only, idempotent +Per-post analytics. Auto-selects the metric set for the media type (IMAGE / VIDEO / REELS / CAROUSEL). + +### `instagram_get_story_insights` · always-on · read-only, idempotent +Story metrics (views, reach, replies, navigation, exits, profile activity). Call within 24h — Story insights expire when the story does. + +### `instagram_get_account_insights` · always-on · read-only, idempotent +Account-level analytics over a date range (≤30 days): reach, profile views, accounts engaged, total interactions, follows/unfollows, link taps, website clicks. + +### `instagram_get_audience_insights` · always-on · read-only, idempotent +Audience demographics: city / country / age / gender / age_gender breakdowns. Requires ≥100 followers. + +--- + +## Comments + +### `instagram_list_comments` · always-on · read-only, idempotent +Paginated top-level comments on one of your posts. + +### `instagram_list_comment_replies` · always-on · read-only, idempotent +Replies (child comments) on a parent comment. + +### `instagram_reply_to_comment` · commentModeration · destructive +Post a reply on a comment. + +### `instagram_delete_comment` · commentModeration · destructive +Delete a comment. Any comment on your media, or any comment you authored. + +### `instagram_hide_comment` · commentModeration · destructive +Hide or unhide a comment (keeps the comment but suppresses it for other viewers). + +--- + +## Hashtags + +### `instagram_search_hashtag` · always-on · read-only, idempotent +Resolve a hashtag name to its Graph API id. **Counts toward Meta's limit of 30 unique hashtag searches per 7-day rolling window per account.** Cache the returned id. + +### `instagram_get_hashtag_top_media` · always-on · read-only, idempotent +Highest-performing public posts for a hashtag, ranked by Instagram. + +### `instagram_get_hashtag_recent_media` · always-on · read-only, idempotent +Most recent public posts (last 24h) tagged with the hashtag. + +--- + +## Discovery + +### `instagram_business_discovery` · always-on · read-only, idempotent +Look up a public Business or Creator account by username. Optionally fetches their recent media. Use for competitor research, partner vetting, audience analysis. Personal accounts are not accessible. + +--- + +## Mentions + +### `instagram_get_mentioned_comment` · always-on · read-only, idempotent +Read a comment that @-mentioned your account. Production usage receives ids via the `mentions` webhook. + +### `instagram_get_mentioned_media` · always-on · read-only, idempotent +Read a public post that @-tagged your account. + +--- + +## Publishing + +### `instagram_publish_image` · publishing · destructive +Single image post in one call. JPEG/PNG, ≤8MB, aspect ratio 4:5 to 1.91:1, HTTPS. Returns `media_id` and `permalink`. + +### `instagram_publish_carousel` · publishing · destructive +2–10 mixed image / video children, assembled into a CAROUSEL container and published. + +### `instagram_publish_reel` · publishing · destructive +Reel: MP4, ≤100MB, ≤90s, H.264 + AAC, HTTPS. Reels can take several minutes to process — polls up to `IG_REEL_TIMEOUT_MS`. + +### `instagram_publish_story_image` · publishing · destructive +Image Story. Expires 24h after publishing. + +### `instagram_publish_story_video` · publishing · destructive +Video Story (≤60s). Expires 24h after publishing. + +--- + +## Containers (low-level publishing) + +For agents that need to schedule. Containers expire 24h after creation. + +### `instagram_create_container` · publishing · destructive +Create a media container without publishing. + +### `instagram_get_container_status` · publishing · read-only, idempotent +Poll container processing status: IN_PROGRESS / FINISHED / ERROR / EXPIRED / PUBLISHED. + +### `instagram_publish_container` · publishing · destructive +Publish a FINISHED container. + +--- + +## Direct Messages + +### `instagram_list_conversations` · messaging · read-only, idempotent +Recent DM conversations. + +### `instagram_list_messages` · messaging · read-only, idempotent +Messages in a conversation, newest first. + +### `instagram_send_message` · messaging · destructive +Send a DM. **Meta Messenger Platform constraint:** free-form messages are only allowed within 24h of the recipient's last message to your account. Outside that window requires a `message_tag` (only `HUMAN_AGENT` is supported, and itself requires special permission). diff --git a/.instagram-mcp.bak/package-lock.json b/.instagram-mcp.bak/package-lock.json new file mode 100644 index 00000000..47130116 --- /dev/null +++ b/.instagram-mcp.bak/package-lock.json @@ -0,0 +1,3251 @@ +{ + "name": "instagram-mcp-buddy", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "instagram-mcp-buddy", + "version": "0.1.0", + "license": "MIT", + "dependencies": { + "@modelcontextprotocol/sdk": "^1.0.4", + "get-port": "^7.1.0", + "node-machine-id": "^1.1.12", + "open": "^10.1.0", + "zod": "^3.23.8" + }, + "bin": { + "instagram-mcp-buddy": "dist/index.js" + }, + "devDependencies": { + "@types/node": "^22.0.0", + "typescript": "^5.6.0", + "vitest": "^2.1.0" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "keytar": "^7.9.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@hono/node-server": { + "version": "1.19.14", + "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.14.tgz", + "integrity": "sha512-GwtvgtXxnWsucXvbQXkRgqksiH2Qed37H9xHZocE5sA3N8O8O8/8FA3uclQXxXVzc9XBZuEOMK7+r02FmSpHtw==", + "license": "MIT", + "engines": { + "node": ">=18.14.1" + }, + "peerDependencies": { + "hono": "^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/@modelcontextprotocol/sdk": { + "version": "1.29.0", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.29.0.tgz", + "integrity": "sha512-zo37mZA9hJWpULgkRpowewez1y6ML5GsXJPY8FI0tBBCd77HEvza4jDqRKOXgHNn867PVGCyTdzqpz0izu5ZjQ==", + "license": "MIT", + "dependencies": { + "@hono/node-server": "^1.19.9", + "ajv": "^8.17.1", + "ajv-formats": "^3.0.1", + "content-type": "^1.0.5", + "cors": "^2.8.5", + "cross-spawn": "^7.0.5", + "eventsource": "^3.0.2", + "eventsource-parser": "^3.0.0", + "express": "^5.2.1", + "express-rate-limit": "^8.2.1", + "hono": "^4.11.4", + "jose": "^6.1.3", + "json-schema-typed": "^8.0.2", + "pkce-challenge": "^5.0.0", + "raw-body": "^3.0.0", + "zod": "^3.25 || ^4.0", + "zod-to-json-schema": "^3.25.1" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@cfworker/json-schema": "^4.1.1", + "zod": "^3.25 || ^4.0" + }, + "peerDependenciesMeta": { + "@cfworker/json-schema": { + "optional": true + }, + "zod": { + "optional": false + } + } + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.3.tgz", + "integrity": "sha512-x35CNW/ANXG3hE/EZpRU8MXX1JDN86hBb2wMGAtltkz7pc6cxgjpy1OMMfDosOQ+2hWqIkag/fGok1Yady9nGw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.60.3.tgz", + "integrity": "sha512-xw3xtkDApIOGayehp2+Rz4zimfkaX65r4t47iy+ymQB2G4iJCBBfj0ogVg5jpvjpn8UWn/+q9tprxleYeNp3Hw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.60.3.tgz", + "integrity": "sha512-vo6Y5Qfpx7/5EaamIwi0WqW2+zfiusVihKatLvtN1VFVy3D13uERk/6gZLU1UiHRL6fDXqj/ELIeVRGnvcTE1g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.60.3.tgz", + "integrity": "sha512-D+0QGcZhBzTN82weOnsSlY7V7+RMmPuF1CkbxyMAGE8+ZHeUjyb76ZiWmBlCu//AQQONvxcqRbwZTajZKqjuOw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.60.3.tgz", + "integrity": "sha512-6HnvHCT7fDyj6R0Ph7A6x8dQS/S38MClRWeDLqc0MdfWkxjiu1HSDYrdPhqSILzjTIC/pnXbbJbo+ft+gy/9hQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.60.3.tgz", + "integrity": "sha512-KHLgC3WKlUYW3ShFKnnosZDOJ0xjg9zp7au3sIm2bs/tGBeC2ipmvRh/N7JKi0t9Ue20C0dpEshi8WUubg+cnA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.60.3.tgz", + "integrity": "sha512-DV6fJoxEYWJOvaZIsok7KrYl0tPvga5OZ2yvKHNNYyk/2roMLqQAbGhr78EQ5YhHpnhLKJD3S1WFusAkmUuV5g==", + "cpu": [ + "arm" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.60.3.tgz", + "integrity": "sha512-mQKoJAzvuOs6F+TZybQO4GOTSMUu7v0WdxEk24krQ/uUxXoPTtHjuaUuPmFhtBcM4K0ons8nrE3JyhTuCFtT/w==", + "cpu": [ + "arm" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.60.3.tgz", + "integrity": "sha512-Whjj2qoiJ6+OOJMGptTYazaJvjOJm+iKHpXQM1P3LzGjt7Ff++Tp7nH4N8J/BUA7R9IHfDyx4DJIflifwnbmIA==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.60.3.tgz", + "integrity": "sha512-4YTNHKqGng5+yiZt3mg77nmyuCfmNfX4fPmyUapBcIk+BdwSwmCWGXOUxhXbBEkFHtoN5boLj/5NON+u5QC9tg==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.60.3.tgz", + "integrity": "sha512-SU3kNlhkpI4UqlUc2VXPGK9o886ZsSeGfMAX2ba2b8DKmMXq4AL7KUrkSWVbb7koVqx41Yczx6dx5PNargIrEA==", + "cpu": [ + "loong64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.60.3.tgz", + "integrity": "sha512-6lDLl5h4TXpB1mTf2rQWnAk/LcXrx9vBfu/DT5TIPhvMhRWaZ5MxkIc8u4lJAmBo6klTe1ywXIUHFjylW505sg==", + "cpu": [ + "loong64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.60.3.tgz", + "integrity": "sha512-BMo8bOw8evlup/8G+cj5xWtPyp93xPdyoSN16Zy90Q2QZ0ZYRhCt6ZJSwbrRzG9HApFabjwj2p25TUPDWrhzqQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.60.3.tgz", + "integrity": "sha512-E0L8X1dZN1/Rph+5VPF6Xj2G7JJvMACVXtamTJIDrVI44Y3K+G8gQaMEAavbqCGTa16InptiVrX6eM6pmJ+7qA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.60.3.tgz", + "integrity": "sha512-oZJ/WHaVfHUiRAtmTAeo3DcevNsVvH8mbvodjZy7D5QKvCefO371SiKRpxoDcCxB3PTRTLayWBkvmDQKTcX/sw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.60.3.tgz", + "integrity": "sha512-Dhbyh7j9FybM3YaTgaHmVALwA8AkUwTPccyCQ79TG9AJUsMQqgN1DDEZNr4+QUfwiWvLDumW5vdwzoeUF+TNxQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.60.3.tgz", + "integrity": "sha512-cJd1X5XhHHlltkaypz1UcWLA8AcoIi1aWhsvaWDskD1oz2eKCypnqvTQ8ykMNI0RSmm7NkTdSqSSD7zM0xa6Ig==", + "cpu": [ + "s390x" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.60.3.tgz", + "integrity": "sha512-DAZDBHQfG2oQuhY7mc6I3/qB4LU2fQCjRvxbDwd/Jdvb9fypP4IJ4qmtu6lNjes6B531AI8cg1aKC2di97bUxA==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.60.3.tgz", + "integrity": "sha512-cRxsE8c13mZOh3vP+wLDxpQBRrOHDIGOWyDL93Sy0Ga8y515fBcC2pjUfFwUe5T7tqvTvWbCpg1URM/AXdWIXA==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.60.3.tgz", + "integrity": "sha512-QaWcIgRxqEdQdhJqW4DJctsH6HCmo5vHxY0krHSX4jMtOqfzC+dqDGuHM87bu4H8JBeibWx7jFz+h6/4C8wA5Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.60.3.tgz", + "integrity": "sha512-AaXwSvUi3QIPtroAUw1t5yHGIyqKEXwH54WUocFolZhpGDruJcs8c+xPNDRn4XiQsS7MEwnYsHW2l0MBLDMkWg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.60.3.tgz", + "integrity": "sha512-65LAKM/bAWDqKNEelHlcHvm2V+Vfb8C6INFxQXRHCvaVN1rJfwr4NvdP4FyzUaLqWfaCGaadf6UbTm8xJeYfEg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.60.3.tgz", + "integrity": "sha512-EEM2gyhBF5MFnI6vMKdX1LAosE627RGBzIoGMdLloPZkXrUN0Ckqgr2Qi8+J3zip/8NVVro3/FjB+tjhZUgUHA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.60.3.tgz", + "integrity": "sha512-E5Eb5H/DpxaoXH++Qkv28RcUJboMopmdDUALBczvHMf7hNIxaDZqwY5lK12UK1BHacSmvupoEWGu+n993Z0y1A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.60.3.tgz", + "integrity": "sha512-hPt/bgL5cE+Qp+/TPHBqptcAgPzgj46mPcg/16zNUmbQk0j+mOEQV/+Lqu8QRtDV3Ek95Q6FeFITpuhl6OTsAA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@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": "22.19.19", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.19.tgz", + "integrity": "sha512-dyh/xO2Fh5bYrfWaaqGrRQQGkNdmYw6AmaAUvYeUMNTWQtvb796ikLdmTchRmOlOiIJ1TDXfWgVx1QkUlQ6Hew==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@vitest/expect": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-2.1.9.tgz", + "integrity": "sha512-UJCIkTBenHeKT1TTlKMJWy1laZewsRIzYighyYiJKZreqtdxSos/S1t+ktRMQWu2CKqaarrkeszJx1cgC5tGZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "2.1.9", + "@vitest/utils": "2.1.9", + "chai": "^5.1.2", + "tinyrainbow": "^1.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-2.1.9.tgz", + "integrity": "sha512-tVL6uJgoUdi6icpxmdrn5YNo3g3Dxv+IHJBr0GXHaEdTcw3F+cPKnsXFhli6nO+f/6SDKPHEK1UN+k+TQv0Ehg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "2.1.9", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.12" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^5.0.0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/pretty-format": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-2.1.9.tgz", + "integrity": "sha512-KhRIdGV2U9HOUzxfiHmY8IFHTdqtOhIzCpd8WRdJiE7D/HUcZVD0EgQCVjm+Q9gkUXWgBvMmTtZgIG48wq7sOQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^1.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-2.1.9.tgz", + "integrity": "sha512-ZXSSqTFIrzduD63btIfEyOmNcBmQvgOVsPNPe0jYtESiXkhd8u2erDLnMxmGrDCwHCCHE7hxwRDCT3pt0esT4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "2.1.9", + "pathe": "^1.1.2" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-2.1.9.tgz", + "integrity": "sha512-oBO82rEjsxLNJincVhLhaxxZdEtV0EFHMK5Kmx5sJ6H9L183dHECjiefOAdnqpIgT5eZwT04PoggUnW88vOBNQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "2.1.9", + "magic-string": "^0.30.12", + "pathe": "^1.1.2" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-2.1.9.tgz", + "integrity": "sha512-E1B35FwzXXTs9FHNK6bDszs7mtydNi5MIfUWpceJ8Xbfb1gBMscAnwLbEu+B44ed6W3XjL9/ehLPHR1fkf1KLQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyspy": "^3.0.2" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-2.1.9.tgz", + "integrity": "sha512-v0psaMSkNJ3A2NMrUEHFRzJtDPFn+/VWZ5WxImB21T9fjucJRmS7xCS3ppEnARb9y11OAzaD+P2Ps+b+BGX5iQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "2.1.9", + "loupe": "^3.1.2", + "tinyrainbow": "^1.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/accepts": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", + "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", + "license": "MIT", + "dependencies": { + "mime-types": "^3.0.0", + "negotiator": "^1.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/ajv": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", + "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-formats": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz", + "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==", + "license": "MIT", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/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/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", + "optional": true + }, + "node_modules/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "license": "MIT", + "optional": true, + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "node_modules/body-parser": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.2.tgz", + "integrity": "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==", + "license": "MIT", + "dependencies": { + "bytes": "^3.1.2", + "content-type": "^1.0.5", + "debug": "^4.4.3", + "http-errors": "^2.0.0", + "iconv-lite": "^0.7.0", + "on-finished": "^2.4.1", + "qs": "^6.14.1", + "raw-body": "^3.0.1", + "type-is": "^2.0.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "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", + "optional": true, + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/bundle-name": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bundle-name/-/bundle-name-4.1.0.tgz", + "integrity": "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==", + "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==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/cac": { + "version": "6.7.14", + "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", + "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/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/chai": { + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/chai/-/chai-5.3.3.tgz", + "integrity": "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "assertion-error": "^2.0.1", + "check-error": "^2.1.1", + "deep-eql": "^5.0.1", + "loupe": "^3.1.0", + "pathval": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/check-error": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.3.tgz", + "integrity": "sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 16" + } + }, + "node_modules/chownr": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", + "license": "ISC", + "optional": true + }, + "node_modules/content-disposition": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.1.0.tgz", + "integrity": "sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", + "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", + "license": "MIT", + "engines": { + "node": ">=6.6.0" + } + }, + "node_modules/cors": { + "version": "2.8.6", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz", + "integrity": "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==", + "license": "MIT", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/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/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/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", + "optional": true, + "dependencies": { + "mimic-response": "^3.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/deep-eql": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", + "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/deep-extend": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/default-browser": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/default-browser/-/default-browser-5.5.0.tgz", + "integrity": "sha512-H9LMLr5zwIbSxrmvikGuI/5KGhZ8E2zH3stkMgM5LpOWDutGM2JZaj460Udnf1a+946zc7YBgrqEWwbk7zHvGw==", + "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/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/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/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", + "optional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/end-of-stream": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", + "license": "MIT", + "optional": true, + "dependencies": { + "once": "^1.4.0" + } + }, + "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": "1.7.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", + "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", + "dev": true, + "license": "MIT" + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/esbuild": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "node_modules/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==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/eventsource": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-3.0.7.tgz", + "integrity": "sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==", + "license": "MIT", + "dependencies": { + "eventsource-parser": "^3.0.1" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/eventsource-parser": { + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.0.8.tgz", + "integrity": "sha512-70QWGkr4snxr0OXLRWsFLeRBIRPuQOvt4s8QYjmUlmlkyTZkRqS7EDVRZtzU3TiyDbXSzaOeF0XUKy8PchzukQ==", + "license": "MIT", + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/expand-template": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", + "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==", + "license": "(MIT OR WTFPL)", + "optional": true, + "engines": { + "node": ">=6" + } + }, + "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/express": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", + "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", + "license": "MIT", + "dependencies": { + "accepts": "^2.0.0", + "body-parser": "^2.2.1", + "content-disposition": "^1.0.0", + "content-type": "^1.0.5", + "cookie": "^0.7.1", + "cookie-signature": "^1.2.1", + "debug": "^4.4.0", + "depd": "^2.0.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "finalhandler": "^2.1.0", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "merge-descriptors": "^2.0.0", + "mime-types": "^3.0.0", + "on-finished": "^2.4.1", + "once": "^1.4.0", + "parseurl": "^1.3.3", + "proxy-addr": "^2.0.7", + "qs": "^6.14.0", + "range-parser": "^1.2.1", + "router": "^2.2.0", + "send": "^1.1.0", + "serve-static": "^2.2.0", + "statuses": "^2.0.1", + "type-is": "^2.0.1", + "vary": "^1.1.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/express-rate-limit": { + "version": "8.5.1", + "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.5.1.tgz", + "integrity": "sha512-5O6KYmyJEpuPJV5hNTXKbAHWRqrzyu+OI3vUnSd2kXFubIVpG7ezpgxQy76Zo5GQZtrQBg86hF+CM/NX+cioiQ==", + "license": "MIT", + "dependencies": { + "ip-address": "^10.2.0" + }, + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://github.com/sponsors/express-rate-limit" + }, + "peerDependencies": { + "express": ">= 4.11" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "license": "MIT" + }, + "node_modules/fast-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.2.tgz", + "integrity": "sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/finalhandler": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz", + "integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "on-finished": "^2.4.1", + "parseurl": "^1.3.3", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", + "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/fs-constants": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", + "license": "MIT", + "optional": true + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "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-port": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/get-port/-/get-port-7.2.0.tgz", + "integrity": "sha512-afP4W205ONCuMoPBqcR6PSXnzX35KTcJygfJfcp+QY+uwm3p20p1YczWXhlICIzGMCxYBQcySEcOgsJcrkyobg==", + "license": "MIT", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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/github-from-package": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", + "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==", + "license": "MIT", + "optional": 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/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.3.tgz", + "integrity": "sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/hono": { + "version": "4.12.18", + "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.18.tgz", + "integrity": "sha512-RWzP96k/yv0PQfyXnWjs6zot20TqfpfsNXhOnev8d1InAxubW93L11/oNUc3tQqn2G0bSdAOBpX+2uDFHV7kdQ==", + "license": "MIT", + "engines": { + "node": ">=16.9.0" + } + }, + "node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/iconv-lite": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz", + "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/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", + "optional": true + }, + "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/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "license": "ISC", + "optional": true + }, + "node_modules/ip-address": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.2.0.tgz", + "integrity": "sha512-/+S6j4E9AHvW9SWMSEY9Xfy66O5PWvVEJ08O0y5JGyEKQpojb0K0GKpz/v5HJ/G0vi3D2sjGK78119oXZeE0qA==", + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-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-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-promise": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", + "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", + "license": "MIT" + }, + "node_modules/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/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "license": "ISC" + }, + "node_modules/jose": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/jose/-/jose-6.2.3.tgz", + "integrity": "sha512-YYVDInQKFJfR/xa3ojUTl8c2KoTwiL1R5Wg9YCydwH0x0B9grbzlg5HC7mMjCtUJjbQ/YnGEZIhI5tCgfTb4Hw==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, + "node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "license": "MIT" + }, + "node_modules/json-schema-typed": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/json-schema-typed/-/json-schema-typed-8.0.2.tgz", + "integrity": "sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA==", + "license": "BSD-2-Clause" + }, + "node_modules/keytar": { + "version": "7.9.0", + "resolved": "https://registry.npmjs.org/keytar/-/keytar-7.9.0.tgz", + "integrity": "sha512-VPD8mtVtm5JNtA2AErl6Chp06JBfy7diFQ7TQQhdpWOl6MrCRB+eRbvAZUsbGQS9kiMq0coJsy0W0vHpDCkWsQ==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "dependencies": { + "node-addon-api": "^4.3.0", + "prebuild-install": "^7.0.1" + } + }, + "node_modules/loupe": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.2.1.tgz", + "integrity": "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/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/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", + "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/merge-descriptors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", + "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", + "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/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", + "optional": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "license": "MIT", + "optional": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/mkdirp-classic": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", + "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", + "license": "MIT", + "optional": true + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.12", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz", + "integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/napi-build-utils": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz", + "integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==", + "license": "MIT", + "optional": true + }, + "node_modules/negotiator": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", + "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/node-abi": { + "version": "3.92.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.92.0.tgz", + "integrity": "sha512-KdHvFWZjEKDf0cakgFjebl371GPsISX2oZHcuyKqM7DtogIsHrqKeLTo8wBHxaXRAQlY2PsPlZmfo+9ZCxEREQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "semver": "^7.3.5" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/node-addon-api": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-4.3.0.tgz", + "integrity": "sha512-73sE9+3UaLYYFmDsFZnqCInzPyh3MqIwZO9cw58yIqAZhONrrabrYyYe3TuIqtIiOuTXVhsGau8hcrhhwSsDIQ==", + "license": "MIT", + "optional": true + }, + "node_modules/node-machine-id": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/node-machine-id/-/node-machine-id-1.1.12.tgz", + "integrity": "sha512-QNABxbrPa3qEIfrE6GOJ7BYIuignnJw7iQ2YPbc3Nla1HzRJjXzZOiikfF8m7eAMfichLt3M4VgLOetqgDmgGQ==", + "license": "MIT" + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/open": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/open/-/open-10.2.0.tgz", + "integrity": "sha512-YgBpdJHPyQ2UE5x+hlSXcnejzAvD0b22U2OuAP+8OnlJT+PjWPxtgmGqKKc+RgTM63U9gN0YzrYc71R2WT/hTA==", + "license": "MIT", + "dependencies": { + "default-browser": "^5.2.1", + "define-lazy-prop": "^3.0.0", + "is-inside-container": "^1.0.0", + "wsl-utils": "^0.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "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==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/pathe": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz", + "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/pathval": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.1.tgz", + "integrity": "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.16" + } + }, + "node_modules/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/pkce-challenge": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/pkce-challenge/-/pkce-challenge-5.0.1.tgz", + "integrity": "sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ==", + "license": "MIT", + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/postcss": { + "version": "8.5.14", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.14.tgz", + "integrity": "sha512-SoSL4+OSEtR99LHFZQiJLkT59C5B1amGO1NzTwj7TT1qCUgUO6hxOvzkOYxD+vMrXBM3XJIKzokoERdqQq/Zmg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/prebuild-install": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz", + "integrity": "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==", + "deprecated": "No longer maintained. Please contact the author of the relevant native addon; alternatives are available.", + "license": "MIT", + "optional": true, + "dependencies": { + "detect-libc": "^2.0.0", + "expand-template": "^2.0.3", + "github-from-package": "0.0.0", + "minimist": "^1.2.3", + "mkdirp-classic": "^0.5.3", + "napi-build-utils": "^2.0.0", + "node-abi": "^3.3.0", + "pump": "^3.0.0", + "rc": "^1.2.7", + "simple-get": "^4.0.0", + "tar-fs": "^2.0.0", + "tunnel-agent": "^0.6.0" + }, + "bin": { + "prebuild-install": "bin.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/pump": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz", + "integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==", + "license": "MIT", + "optional": true, + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "node_modules/qs": { + "version": "6.15.1", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.1.tgz", + "integrity": "sha512-6YHEFRL9mfgcAvql/XhwTvf5jKcOiiupt2FiJxHkiX1z4j7WL8J/jRHYLluORvc1XxB5rV20KoeK00gVJamspg==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz", + "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.7.0", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/rc": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", + "license": "(BSD-2-Clause OR MIT OR Apache-2.0)", + "optional": true, + "dependencies": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + }, + "bin": { + "rc": "cli.js" + } + }, + "node_modules/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", + "optional": true, + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/rollup": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.3.tgz", + "integrity": "sha512-pAQK9HalE84QSm4Po3EmWIZPd3FnjkShVkiMlz1iligWYkWQ7wHYd1PF/T7QZ5TVSD6uSTon5gBVMSM4JfBV+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.60.3", + "@rollup/rollup-android-arm64": "4.60.3", + "@rollup/rollup-darwin-arm64": "4.60.3", + "@rollup/rollup-darwin-x64": "4.60.3", + "@rollup/rollup-freebsd-arm64": "4.60.3", + "@rollup/rollup-freebsd-x64": "4.60.3", + "@rollup/rollup-linux-arm-gnueabihf": "4.60.3", + "@rollup/rollup-linux-arm-musleabihf": "4.60.3", + "@rollup/rollup-linux-arm64-gnu": "4.60.3", + "@rollup/rollup-linux-arm64-musl": "4.60.3", + "@rollup/rollup-linux-loong64-gnu": "4.60.3", + "@rollup/rollup-linux-loong64-musl": "4.60.3", + "@rollup/rollup-linux-ppc64-gnu": "4.60.3", + "@rollup/rollup-linux-ppc64-musl": "4.60.3", + "@rollup/rollup-linux-riscv64-gnu": "4.60.3", + "@rollup/rollup-linux-riscv64-musl": "4.60.3", + "@rollup/rollup-linux-s390x-gnu": "4.60.3", + "@rollup/rollup-linux-x64-gnu": "4.60.3", + "@rollup/rollup-linux-x64-musl": "4.60.3", + "@rollup/rollup-openbsd-x64": "4.60.3", + "@rollup/rollup-openharmony-arm64": "4.60.3", + "@rollup/rollup-win32-arm64-msvc": "4.60.3", + "@rollup/rollup-win32-ia32-msvc": "4.60.3", + "@rollup/rollup-win32-x64-gnu": "4.60.3", + "@rollup/rollup-win32-x64-msvc": "4.60.3", + "fsevents": "~2.3.2" + } + }, + "node_modules/rollup/node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/router": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", + "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "depd": "^2.0.0", + "is-promise": "^4.0.0", + "parseurl": "^1.3.3", + "path-to-regexp": "^8.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/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/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", + "optional": true + }, + "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/semver": { + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.0.tgz", + "integrity": "sha512-AcM7dV/5ul4EekoQ29Agm5vri8JNqRyj39o0qpX6vDF2GZrtutZl5RwgD1XnZjiTAfncsJhMI48QQH3sN87YNA==", + "license": "ISC", + "optional": true, + "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==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.3", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "fresh": "^2.0.0", + "http-errors": "^2.0.1", + "mime-types": "^3.0.2", + "ms": "^2.1.3", + "on-finished": "^2.4.1", + "range-parser": "^1.2.1", + "statuses": "^2.0.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/serve-static": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.1.tgz", + "integrity": "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==", + "license": "MIT", + "dependencies": { + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "parseurl": "^1.3.3", + "send": "^1.2.0" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "node_modules/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.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz", + "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/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/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", + "optional": true + }, + "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", + "optional": true, + "dependencies": { + "decompress-response": "^6.0.0", + "once": "^1.3.1", + "simple-concat": "^1.0.0" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/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/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/std-env": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", + "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", + "dev": true, + "license": "MIT" + }, + "node_modules/string_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", + "optional": true, + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/tar-fs": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.4.tgz", + "integrity": "sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "chownr": "^1.1.1", + "mkdirp-classic": "^0.5.2", + "pump": "^3.0.0", + "tar-stream": "^2.1.4" + } + }, + "node_modules/tar-stream": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", + "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "bl": "^4.0.3", + "end-of-stream": "^1.4.1", + "fs-constants": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz", + "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinypool": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.1.1.tgz", + "integrity": "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.0.0 || >=20.0.0" + } + }, + "node_modules/tinyrainbow": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-1.2.0.tgz", + "integrity": "sha512-weEDEq7Z5eTHPDh4xjX789+fHfF+P8boiFB+0vbWzpbnbsEr/GRaohi/uMKxg8RZMXnl1ItAi/IUHWMsjDV7kQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tinyspy": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-3.0.2.tgz", + "integrity": "sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/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", + "optional": true, + "dependencies": { + "safe-buffer": "^5.0.1" + }, + "engines": { + "node": "*" + } + }, + "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==", + "license": "MIT", + "dependencies": { + "content-type": "^2.0.0", + "media-typer": "^1.1.0", + "mime-types": "^3.0.0" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/type-is/node_modules/content-type": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.0.0.tgz", + "integrity": "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "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": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/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", + "optional": true + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/vite": { + "version": "5.4.21", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz", + "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.21.3", + "postcss": "^8.4.43", + "rollup": "^4.20.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/vite-node": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-2.1.9.tgz", + "integrity": "sha512-AM9aQ/IPrW/6ENLQg3AGY4K1N2TGZdR5e4gu/MmmR2xR3Ll1+dib+nook92g4TV3PXVyeyxdWwtaCAiUL0hMxA==", + "dev": true, + "license": "MIT", + "dependencies": { + "cac": "^6.7.14", + "debug": "^4.3.7", + "es-module-lexer": "^1.5.4", + "pathe": "^1.1.2", + "vite": "^5.0.0" + }, + "bin": { + "vite-node": "vite-node.mjs" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/vitest": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-2.1.9.tgz", + "integrity": "sha512-MSmPM9REYqDGBI8439mA4mWhV5sKmDlBKWIYbA3lRb2PTHACE0mgKwA8yQ2xq9vxDTuk4iPrECBAEW2aoFXY0Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/expect": "2.1.9", + "@vitest/mocker": "2.1.9", + "@vitest/pretty-format": "^2.1.9", + "@vitest/runner": "2.1.9", + "@vitest/snapshot": "2.1.9", + "@vitest/spy": "2.1.9", + "@vitest/utils": "2.1.9", + "chai": "^5.1.2", + "debug": "^4.3.7", + "expect-type": "^1.1.0", + "magic-string": "^0.30.12", + "pathe": "^1.1.2", + "std-env": "^3.8.0", + "tinybench": "^2.9.0", + "tinyexec": "^0.3.1", + "tinypool": "^1.0.1", + "tinyrainbow": "^1.2.0", + "vite": "^5.0.0", + "vite-node": "2.1.9", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@types/node": "^18.0.0 || >=20.0.0", + "@vitest/browser": "2.1.9", + "@vitest/ui": "2.1.9", + "happy-dom": "*", + "jsdom": "*" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + } + } + }, + "node_modules/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" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/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/wsl-utils": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/wsl-utils/-/wsl-utils-0.1.0.tgz", + "integrity": "sha512-h3Fbisa2nKGPxCpm89Hk33lBLsnaGBvctQopaBSOW/uIs6FTe1ATyAnKFJrzVs9vpGdsTe73WF3V4lIsk4Gacw==", + "license": "MIT", + "dependencies": { + "is-wsl": "^3.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/zod": { + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/zod-to-json-schema": { + "version": "3.25.2", + "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.25.2.tgz", + "integrity": "sha512-O/PgfnpT1xKSDeQYSCfRI5Gy3hPf91mKVDuYLUHZJMiDFptvP41MSnWofm8dnCm0256ZNfZIM7DSzuSMAFnjHA==", + "license": "ISC", + "peerDependencies": { + "zod": "^3.25.28 || ^4" + } + } + } +} diff --git a/.instagram-mcp.bak/package.json b/.instagram-mcp.bak/package.json new file mode 100644 index 00000000..a55fd4fb --- /dev/null +++ b/.instagram-mcp.bak/package.json @@ -0,0 +1,47 @@ +{ + "name": "instagram-mcp-buddy", + "version": "0.1.0", + "description": "Instagram for AI agents. Sign in with your Instagram Business or Creator account — no API keys, no Meta Developer app, no .env. MCP server for OpenSwarm, Claude Desktop, Cursor, and Claude Code.", + "license": "MIT", + "type": "module", + "bin": { + "instagram-mcp-buddy": "dist/index.js" + }, + "main": "dist/index.js", + "files": [ + "dist", + "README.md", + "TOOLS.md", + "LICENSE" + ], + "scripts": { + "build": "tsc", + "dev": "tsc --watch", + "start": "node dist/index.js", + "inspect": "npx @modelcontextprotocol/inspector node dist/index.js", + "typecheck": "tsc --noEmit", + "test": "vitest run", + "test:watch": "vitest", + "inject:credentials": "node scripts/inject-credentials.mjs", + "build:inject": "tsc && node scripts/inject-credentials.mjs", + "publish:npm": "tsc && node scripts/inject-credentials.mjs && npm publish --access public" + }, + "dependencies": { + "@modelcontextprotocol/sdk": "^1.0.4", + "get-port": "^7.1.0", + "node-machine-id": "^1.1.12", + "open": "^10.1.0", + "zod": "^3.23.8" + }, + "optionalDependencies": { + "keytar": "^7.9.0" + }, + "devDependencies": { + "@types/node": "^22.0.0", + "typescript": "^5.6.0", + "vitest": "^2.1.0" + }, + "engines": { + "node": ">=18.0.0" + } +} diff --git a/.instagram-mcp.bak/patches/myswarm-tools-featured.patch b/.instagram-mcp.bak/patches/myswarm-tools-featured.patch new file mode 100644 index 00000000..3cf4967a --- /dev/null +++ b/.instagram-mcp.bak/patches/myswarm-tools-featured.patch @@ -0,0 +1,51 @@ +Add Instagram to the featured tools list in myswarm. + +Adds an Instagram entry to FEATURED_TOOLS in +frontend/src/app/pages/Tools/Tools.tsx, modeled on the existing Reddit +entry. Wires up the `instagram-mcp-buddy` MCP server, which self-bootstraps +OAuth on first use — no API keys required. + +Insertion is alphabetical: between the X (Twitter) entry and the Reddit +entry, since 'instagram' sorts between 'twitter' (which the X catalog +entry follows) and 'reddit' in the existing layout. + +Apply with: + git apply patches/myswarm-tools-featured.patch + +diff --git a/frontend/src/app/pages/Tools/Tools.tsx b/frontend/src/app/pages/Tools/Tools.tsx +--- a/frontend/src/app/pages/Tools/Tools.tsx ++++ b/frontend/src/app/pages/Tools/Tools.tsx +@@ -125,9 +125,33 @@ + { key: 'TWITTER_AUTH_TOKEN', label: 'auth_token', placeholder: 'Paste auth_token cookie value' }, + { key: 'TWITTER_CT0', label: 'ct0', placeholder: 'Paste ct0 cookie value' }, + ], + }, ++ { ++ id: 'instagram', ++ name: 'Instagram', ++ description: 'Post, schedule, and analyze content. Sign in with your Instagram Business or Creator account — no API keys required.', ++ mcp_config: { type: 'stdio', command: 'npx', args: ['-y', 'instagram-mcp-buddy'] }, ++ color: '#E4405F', ++ website: 'https://github.com//instagram-mcp-buddy', ++ icon: ( ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ), ++ }, + { + id: 'reddit', + name: 'Reddit', + description: 'Browse subreddits, search posts, get post details, analyze users. No API keys required.', + mcp_config: { type: 'stdio', command: 'npx', args: ['-y', 'reddit-mcp-buddy'] }, diff --git a/.instagram-mcp.bak/scripts/dev-list-tools.mjs b/.instagram-mcp.bak/scripts/dev-list-tools.mjs new file mode 100644 index 00000000..09f446ba --- /dev/null +++ b/.instagram-mcp.bak/scripts/dev-list-tools.mjs @@ -0,0 +1,86 @@ +#!/usr/bin/env node +/** + * Dev-only smoke check: spawn `node dist/index.js` with stdio, send an MCP + * `tools/list` request, print the count + names + which are gated. + * + * Usage: + * node scripts/dev-list-tools.mjs # default flags + * INSTAGRAM_MCP_ENABLE_PUBLISHING=true node scripts/dev-list-tools.mjs + * INSTAGRAM_MCP_ENABLE_PUBLISHING=true \ + * INSTAGRAM_MCP_ENABLE_COMMENTMODERATION=true \ + * INSTAGRAM_MCP_ENABLE_MESSAGING=true \ + * node scripts/dev-list-tools.mjs + */ +import { spawn } from "node:child_process"; +import { resolve, dirname } from "node:path"; +import { fileURLToPath } from "node:url"; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); +const ENTRY = resolve(__dirname, "..", "dist", "index.js"); + +const child = spawn(process.execPath, [ENTRY], { + stdio: ["pipe", "pipe", "inherit"], + env: process.env, +}); + +let buffer = ""; +let initialized = false; + +const send = (obj) => { + const payload = JSON.stringify(obj); + child.stdin.write(`${payload}\n`); +}; + +const finish = (toolNames) => { + process.stdout.write(`tool_count=${toolNames.length}\n`); + for (const name of toolNames) process.stdout.write(` ${name}\n`); + child.kill("SIGTERM"); + setTimeout(() => process.exit(0), 100); +}; + +child.stdout.on("data", (chunk) => { + buffer += chunk.toString("utf8"); + const lines = buffer.split("\n"); + buffer = lines.pop() ?? ""; + for (const line of lines) { + if (!line.trim()) continue; + let msg; + try { + msg = JSON.parse(line); + } catch { + continue; + } + if (msg.id === 1 && !initialized) { + initialized = true; + send({ jsonrpc: "2.0", method: "notifications/initialized" }); + send({ jsonrpc: "2.0", id: 2, method: "tools/list" }); + } else if (msg.id === 2 && msg.result?.tools) { + finish(msg.result.tools.map((t) => t.name)); + } + } +}); + +child.on("exit", (code) => { + if (code !== null && code !== 0) { + process.stderr.write(`child exited with code ${code}\n`); + process.exit(code); + } +}); + +setTimeout(() => { + process.stderr.write("dev-list-tools: timed out after 10s\n"); + child.kill("SIGKILL"); + process.exit(2); +}, 10_000); + +send({ + jsonrpc: "2.0", + id: 1, + method: "initialize", + params: { + protocolVersion: "2024-11-05", + capabilities: {}, + clientInfo: { name: "dev-list-tools", version: "0.0.0" }, + }, +}); diff --git a/.instagram-mcp.bak/scripts/inject-credentials.mjs b/.instagram-mcp.bak/scripts/inject-credentials.mjs new file mode 100755 index 00000000..a1228796 --- /dev/null +++ b/.instagram-mcp.bak/scripts/inject-credentials.mjs @@ -0,0 +1,138 @@ +#!/usr/bin/env node +/** + * Rewrite the REPLACE_AT_BUILD placeholders in dist/oauth-config.js with the + * real Meta App credentials at npm publish time. + * + * Hard fails (exit 1) if: + * - META_APP_ID / META_APP_SECRET (or INSTAGRAM_MCP_APP_*) are missing + * - either value still equals REPLACE_AT_BUILD + * - dist/oauth-config.js is missing (forgot to run `tsc`) + * - either placeholder is not found in the file + * + * Logs the SHA-256 of the resulting file so the maintainer can verify the + * credentials made it in. + * + * Invocation: + * META_APP_ID=... META_APP_SECRET=... npm run publish:npm + * # or same values as .env.example: + * INSTAGRAM_MCP_APP_ID=... INSTAGRAM_MCP_APP_SECRET=... npm run inject:credentials + */ + +import { createHash } from "node:crypto"; +import { existsSync, readFileSync } from "node:fs"; +import { readFile, writeFile } from "node:fs/promises"; +import { dirname, resolve } from "node:path"; +import { fileURLToPath } from "node:url"; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); + +const TARGET = resolve(__dirname, "..", "dist", "oauth-config.js"); +const PLACEHOLDER = "REPLACE_AT_BUILD"; + +function die(msg) { + process.stderr.write(`inject-credentials: ${msg}\n`); + process.exit(1); +} + +function loadDotEnvFiles() { + const applyLine = (line) => { + const t = line.trim(); + if (!t || t.startsWith("#")) return; + const eq = t.indexOf("="); + if (eq === -1) return; + const key = t.slice(0, eq).trim(); + if (!key) return; + let val = t.slice(eq + 1).trim(); + if ( + (val.startsWith('"') && val.endsWith('"')) || + (val.startsWith("'") && val.endsWith("'")) + ) { + val = val.slice(1, -1); + } + if (process.env[key] === undefined) process.env[key] = val; + }; + for (const p of [ + resolve(process.cwd(), ".env"), + resolve(__dirname, "..", ".env"), + ]) { + if (!existsSync(p)) continue; + try { + for (const line of readFileSync(p, "utf8").split(/\r?\n/)) applyLine(line); + } catch { + /* ignore */ + } + } +} + +loadDotEnvFiles(); + +const id = process.env.META_APP_ID || process.env.INSTAGRAM_MCP_APP_ID; +const secret = process.env.META_APP_SECRET || process.env.INSTAGRAM_MCP_APP_SECRET; + +if (!id) + die( + "META_APP_ID (or INSTAGRAM_MCP_APP_ID) is required in env or instagram-mcp/.env. " + + "Refusing to inject. If you copied .env.example, fill in the values on the " + + "INSTAGRAM_MCP_APP_ID= and INSTAGRAM_MCP_APP_SECRET= lines — those lines must " + + "NOT start with # or they are ignored.", + ); +if (!secret) + die( + "META_APP_SECRET (or INSTAGRAM_MCP_APP_SECRET) is required in env or instagram-mcp/.env. " + + "Refusing to inject. Use uncommented INSTAGRAM_MCP_APP_SECRET=your_secret (no leading #).", + ); +if (id === PLACEHOLDER) die("META_APP_ID is the placeholder. Refusing to inject."); +if (secret === PLACEHOLDER) + die("META_APP_SECRET is the placeholder. Refusing to inject."); + +let original; +try { + original = await readFile(TARGET, "utf8"); +} catch (err) { + die( + `cannot read ${TARGET}: ${err.message}. Did you run \`tsc\` first? (npm run publish:npm does this for you.)`, + ); +} + +// Target ONLY the two export-const fallback strings. The literal +// "REPLACE_AT_BUILD" survives in comments and in `hasInjectedCredentials()`, +// which is intentional — that function still correctly returns true after +// real credentials are injected because the real values won't equal the +// placeholder string. +const ID_PATTERN = + /(META_APP_ID\s*=\s*process\.env\.INSTAGRAM_MCP_APP_ID\s*\?\?\s*")REPLACE_AT_BUILD(")/; +const SECRET_PATTERN = + /(META_APP_SECRET\s*=\s*process\.env\.INSTAGRAM_MCP_APP_SECRET\s*\?\?\s*")REPLACE_AT_BUILD(")/; + +if (!ID_PATTERN.test(original)) { + die( + `META_APP_ID placeholder line not found in ${TARGET}. Either credentials were already injected, or the build output changed shape.`, + ); +} +if (!SECRET_PATTERN.test(original)) { + die( + `META_APP_SECRET placeholder line not found in ${TARGET}. Either credentials were already injected, or the build output changed shape.`, + ); +} + +// String-literal injection: backslash- and quote-escape just in case. +const escape = (s) => s.replace(/\\/g, "\\\\").replace(/"/g, '\\"'); + +const next = original + .replace(ID_PATTERN, `$1${escape(id)}$2`) + .replace(SECRET_PATTERN, `$1${escape(secret)}$2`); + +await writeFile(TARGET, next, "utf8"); + +const sha = createHash("sha256").update(next).digest("hex"); +process.stdout.write( + `inject-credentials: injected META_APP_ID + META_APP_SECRET into ${TARGET}\n`, +); +process.stdout.write(`inject-credentials: sha256(${TARGET}) = ${sha}\n`); +process.stdout.write( + `inject-credentials: META_APP_ID prefix = ${id.slice(0, 4)}*** (length ${id.length})\n`, +); +process.stdout.write( + `inject-credentials: META_APP_SECRET prefix = ${secret.slice(0, 4)}*** (length ${secret.length})\n`, +); diff --git a/.instagram-mcp.bak/src/auth-state.ts b/.instagram-mcp.bak/src/auth-state.ts new file mode 100644 index 00000000..0bf80c23 --- /dev/null +++ b/.instagram-mcp.bak/src/auth-state.ts @@ -0,0 +1,169 @@ +import { InstagramMcpError } from "./errors.js"; +import { runPublishedBuddySubprocess, shouldUseNpmOAuthFallback } from "./npm-connect-fallback.js"; +import { refreshAccessToken, runOAuthFlow, type RunOAuthFlowOpts } from "./oauth.js"; +import { + createTokenStore, + type CreateTokenStoreOpts, + type StoredToken, + type TokenStore, +} from "./token-store.js"; +import { log } from "./logger.js"; + +/** Auto-refresh the long-lived token if it expires within this window. */ +const REFRESH_THRESHOLD_MS = 7 * 24 * 60 * 60 * 1000; + +export interface AuthStateOpts { + store?: TokenStore; + storeOpts?: CreateTokenStoreOpts; + /** Inject into OAuth flow (tests). */ + oauthOpts?: RunOAuthFlowOpts; + /** Override refresh transport (tests). */ + refresh?: (token: string) => Promise<{ access_token: string; expires_in: number }>; + /** Override Date.now (tests). */ + now?: () => number; +} + +/** + * Owns the stored token and brokers refresh + connect/disconnect. The Graph + * client reads the live token via `getToken()` on every request, so token + * rotations are picked up automatically. + */ +export class AuthState { + private token: StoredToken | null = null; + private loaded = false; + + constructor( + private readonly store: TokenStore, + private readonly opts: AuthStateOpts = {}, + ) {} + + static async create(opts: AuthStateOpts = {}): Promise { + const store = opts.store ?? (await createTokenStore(opts.storeOpts)); + const self = new AuthState(store, opts); + await self.init(); + return self; + } + + private get nowMs(): number { + return (this.opts.now ?? Date.now)(); + } + + private async init(): Promise { + if (this.loaded) return; + try { + this.token = await this.store.load(); + log.info("auth_state_loaded", { + connected: this.token !== null, + backend: this.store.describe(), + }); + } catch (err) { + log.warn("auth_state_load_failed", { reason: (err as Error).message }); + this.token = null; + } + this.loaded = true; + } + + /** Re-read token from disk/keychain (e.g. after npx OAuth wrote it). */ + private async reloadFromStore(): Promise { + try { + this.token = await this.store.load(); + log.info("auth_state_reloaded", { + connected: this.token !== null, + backend: this.store.describe(), + }); + } catch (err) { + log.warn("auth_state_reload_failed", { reason: (err as Error).message }); + this.token = null; + } + } + + isConnected(): boolean { + return this.token !== null; + } + + /** + * Returns the live access token (refreshing if needed). Null if unconnected. + * Graph-client callers should pair this with their own NotConnectedError. + */ + async getToken(): Promise { + if (!this.token) return null; + await this.refreshIfNeeded(); + return this.token?.access_token ?? null; + } + + async getUserId(): Promise { + return this.token?.user_id ?? null; + } + + getGrantedScopes(): string[] { + return this.token?.granted_scopes ?? []; + } + + getExpiresAt(): number | null { + return this.token?.expires_at ?? null; + } + + async connect(): Promise { + if (shouldUseNpmOAuthFallback()) { + log.info("auth_state_connect_via_npm_package"); + process.stderr.write( + "instagram-mcp-buddy: no embedded Meta app — OAuth runs via `npx -y instagram-mcp-buddy connect`. Set INSTAGRAM_MCP_NO_NPX_FALLBACK=1 to require INSTAGRAM_MCP_APP_*.\n", + ); + await runPublishedBuddySubprocess("connect"); + await this.reloadFromStore(); + if (!this.token) { + throw new InstagramMcpError( + "OAuth finished but no token was found locally. Complete the browser login, or run instagram_connect again.", + ); + } + log.info("auth_state_connected", { + user_id: this.token.user_id, + expires_at: new Date(this.token.expires_at).toISOString(), + scopes: this.token.granted_scopes, + }); + return this.token; + } + + const token = await runOAuthFlow(this.opts.oauthOpts); + this.token = token; + await this.store.save(token); + log.info("auth_state_connected", { + user_id: token.user_id, + expires_at: new Date(token.expires_at).toISOString(), + scopes: token.granted_scopes, + }); + return token; + } + + async disconnect(): Promise { + this.token = null; + await this.store.clear(); + log.info("auth_state_disconnected"); + } + + async refreshIfNeeded(): Promise { + if (!this.token) return; + const msLeft = this.token.expires_at - this.nowMs; + if (msLeft > REFRESH_THRESHOLD_MS) return; + + log.info("auth_state_refreshing", { msLeft }); + try { + const refreshFn = this.opts.refresh ?? refreshAccessToken; + const refreshed = await refreshFn(this.token.access_token); + this.token = { + ...this.token, + access_token: refreshed.access_token, + expires_at: this.nowMs + refreshed.expires_in * 1000, + obtained_at: this.nowMs, + }; + await this.store.save(this.token); + log.info("auth_state_refreshed", { + expires_at: new Date(this.token.expires_at).toISOString(), + }); + } catch (err) { + log.warn("auth_state_refresh_failed", { reason: (err as Error).message }); + // Don't clear — the existing token may still work for a bit. Let the + // next Graph request surface the real auth error if it doesn't. + } + } +} diff --git a/.instagram-mcp.bak/src/config.ts b/.instagram-mcp.bak/src/config.ts new file mode 100644 index 00000000..f2d37dab --- /dev/null +++ b/.instagram-mcp.bak/src/config.ts @@ -0,0 +1,27 @@ +import { z } from "zod"; + +/** + * Operational tunables only. NO required env vars — instagram-mcp-buddy boots + * fine with nothing set and bootstraps OAuth on first `instagram_connect`. + */ +const ConfigSchema = z.object({ + IG_GRAPH_API_VERSION: z.string().default("v21.0"), + IG_DEFAULT_TIMEOUT_MS: z.coerce.number().int().positive().default(30_000), + IG_REEL_TIMEOUT_MS: z.coerce.number().int().positive().default(300_000), + IG_IMAGE_TIMEOUT_MS: z.coerce.number().int().positive().default(60_000), + LOG_LEVEL: z.enum(["debug", "info", "warn", "error"]).default("info"), +}); + +export type Config = z.infer; + +export function loadConfig(): Config { + const result = ConfigSchema.safeParse(process.env); + if (!result.success) { + // Should be impossible — every field has a default. Log and use defaults. + process.stderr.write( + `instagram-mcp-buddy: ignoring malformed env var: ${result.error.message}\n`, + ); + return ConfigSchema.parse({}); + } + return result.data; +} diff --git a/.instagram-mcp.bak/src/containers.ts b/.instagram-mcp.bak/src/containers.ts new file mode 100644 index 00000000..842eeb3f --- /dev/null +++ b/.instagram-mcp.bak/src/containers.ts @@ -0,0 +1,69 @@ +import { ContainerError, ContainerTimeout } from "./errors.js"; +import type { GraphClient } from "./graph-client.js"; +import { log } from "./logger.js"; + +export type ContainerStatus = + | "IN_PROGRESS" + | "FINISHED" + | "ERROR" + | "EXPIRED" + | "PUBLISHED"; + +export interface ContainerStatusResponse { + status: ContainerStatus; + status_code?: string; + id?: string; +} + +/** + * Fetch container status once. Used by the get_container_status tool. + */ +export async function fetchContainerStatus( + client: GraphClient, + containerId: string, +): Promise { + return client.request(`/${containerId}`, { + query: { fields: "status,status_code,id" }, + }); +} + +/** + * Poll a container with exponential backoff until it reaches FINISHED. + * Throws ContainerError on ERROR/EXPIRED status, or ContainerTimeout if the budget expires. + */ +export async function pollUntilFinished( + client: GraphClient, + containerId: string, + timeoutMs: number, +): Promise { + const started = Date.now(); + let delay = 2000; + const maxDelay = 10_000; + + while (true) { + const elapsed = Date.now() - started; + if (elapsed > timeoutMs) { + throw new ContainerTimeout(containerId, elapsed); + } + const res = await fetchContainerStatus(client, containerId); + log.debug("container_poll", { containerId, status: res.status, elapsedMs: elapsed }); + switch (res.status) { + case "FINISHED": + case "PUBLISHED": + return; + case "ERROR": + case "EXPIRED": + throw new ContainerError(containerId, res.status, res.status_code); + case "IN_PROGRESS": + break; + default: + throw new ContainerError(containerId, String(res.status)); + } + await sleep(Math.min(delay, timeoutMs - elapsed)); + delay = Math.min(delay * 1.5, maxDelay); + } +} + +function sleep(ms: number): Promise { + return new Promise((resolve) => setTimeout(resolve, ms)); +} diff --git a/.instagram-mcp.bak/src/env-bootstrap.ts b/.instagram-mcp.bak/src/env-bootstrap.ts new file mode 100644 index 00000000..d9e1b244 --- /dev/null +++ b/.instagram-mcp.bak/src/env-bootstrap.ts @@ -0,0 +1,55 @@ +import { existsSync, readFileSync } from "node:fs"; +import { dirname, resolve } from "node:path"; +import { fileURLToPath } from "node:url"; + +const __dirname = dirname(fileURLToPath(import.meta.url)); + +function applyDotEnvLine(line: string): void { + const t = line.trim(); + if (!t || t.startsWith("#")) return; + const eq = t.indexOf("="); + if (eq === -1) return; + const key = t.slice(0, eq).trim(); + if (!key) return; + let val = t.slice(eq + 1).trim(); + if ( + (val.startsWith('"') && val.endsWith('"')) || + (val.startsWith("'") && val.endsWith("'")) + ) { + val = val.slice(1, -1); + } + if (process.env[key] === undefined) process.env[key] = val; +} + +/** + * Load optional `.env` before `oauth-config` reads INSTAGRAM_MCP_APP_*. + * Skipped under Vitest / NODE_ENV=test so unit tests control env explicitly. + */ +export function loadInstagramBuddyEnvFromDisk(): void { + if ( + process.env.NODE_ENV === "test" || + process.env.VITEST !== undefined || + process.env.VITEST_WORKER_ID !== undefined + ) { + return; + } + + const roots = [process.cwd(), resolve(__dirname, "..")]; + const seen = new Set(); + for (const root of roots) { + for (const name of [".env", ".env.local"]) { + const path = resolve(root, name); + if (seen.has(path)) continue; + seen.add(path); + if (!existsSync(path)) continue; + try { + const content = readFileSync(path, "utf8"); + for (const line of content.split(/\r?\n/)) applyDotEnvLine(line); + } catch { + /* ignore unreadable .env */ + } + } + } +} + +loadInstagramBuddyEnvFromDisk(); diff --git a/.instagram-mcp.bak/src/errors.ts b/.instagram-mcp.bak/src/errors.ts new file mode 100644 index 00000000..630308cc --- /dev/null +++ b/.instagram-mcp.bak/src/errors.ts @@ -0,0 +1,161 @@ +/** + * Typed errors for the Instagram MCP server. + * + * Every error message includes: + * 1. what failed in plain English + * 2. the Meta error code if present + * 3. a specific next step the user/agent can take + * + * These bubble up through MCP as tool errors. Actionable messages = fewer wasted agent turns. + */ + +export class InstagramMcpError extends Error { + constructor( + message: string, + public readonly code?: number, + public readonly subcode?: number, + ) { + super(message); + this.name = "InstagramMcpError"; + } +} + +export class NotConnectedError extends InstagramMcpError { + constructor() { + super( + "Not connected to Instagram. The agent should call instagram_connect to start the OAuth login flow.", + ); + this.name = "NotConnectedError"; + } +} + +export class FeatureDisabledError extends InstagramMcpError { + constructor(flag: string, scope: string) { + super( + `This tool is not available in the current build of instagram-mcp-buddy. The ${flag} feature is disabled — it will be enabled in a future npm release once Meta App Review approves the ${scope} permission. Update via 'npx -y instagram-mcp-buddy@latest'.`, + ); + this.name = "FeatureDisabledError"; + } +} + +export class AuthError extends InstagramMcpError { + constructor(code = 190, subcode?: number) { + super( + `Instagram access token is invalid or expired (code ${code}). ` + + `The token may have been revoked from your Instagram settings, or this refresh attempt failed. ` + + `Call instagram_logout then instagram_connect to log back in.`, + code, + subcode, + ); + this.name = "AuthError"; + } +} + +export class PermissionError extends InstagramMcpError { + constructor(code: number, message: string) { + super( + `Permission denied (code ${code}): ${message}. ` + + `The signed-in account did not grant the scope this tool needs. Call instagram_status to ` + + `see which scopes are currently granted, then re-run instagram_connect if you need to add more.`, + code, + ); + this.name = "PermissionError"; + } +} + +export class RateLimitError extends InstagramMcpError { + constructor(code: number, usagePercent?: number) { + super( + `Instagram API rate limit reached (code ${code})` + + (usagePercent !== undefined ? ` at ${usagePercent}% usage` : "") + + `. Meta resets Business Use Case Usage hourly. Wait at least 1 hour before retrying, ` + + `or reduce request frequency. The server already retried automatically.`, + code, + ); + this.name = "RateLimitError"; + } +} + +export class MediaIneligibleError extends InstagramMcpError { + constructor(reason: string) { + super( + `Instagram rejected the media: ${reason}. ` + + `Check the format requirements: ` + + `Images must be JPEG/PNG, ≤8MB, aspect ratio between 4:5 and 1.91:1. ` + + `Reels must be MP4, ≤100MB, ≤90s, H.264 video + AAC audio. ` + + `All media URLs must be publicly accessible HTTPS.`, + ); + this.name = "MediaIneligibleError"; + } +} + +export class ContainerError extends InstagramMcpError { + constructor(containerId: string, statusCode: string, detail?: string) { + super( + `Media container ${containerId} entered status ${statusCode}${detail ? `: ${detail}` : ""}. ` + + `This usually means Instagram could not fetch or process the source media. ` + + `Verify the media URL is public HTTPS, the file format is supported, and try again with a fresh container.`, + ); + this.name = "ContainerError"; + } +} + +export class ContainerTimeout extends InstagramMcpError { + constructor(containerId: string, elapsedMs: number) { + super( + `Media container ${containerId} did not finish processing within ${elapsedMs}ms. ` + + `Reels can take several minutes to process. Increase IG_REEL_TIMEOUT_MS if this happens often, ` + + `or use the lower-level instagram_create_container + instagram_get_container_status tools to poll manually.`, + ); + this.name = "ContainerTimeout"; + } +} + +export class NotFoundError extends InstagramMcpError { + constructor(resource: string) { + super( + `Instagram returned 404 for ${resource}. ` + + `The resource may have been deleted, or you may not have permission to access it. ` + + `For comments/replies, also verify the id format is correct.`, + 404, + ); + this.name = "NotFoundError"; + } +} + +export class GraphApiError extends InstagramMcpError { + constructor(message: string, code?: number, subcode?: number) { + super( + `Instagram Graph API error${code ? ` (code ${code}${subcode ? `, subcode ${subcode}` : ""})` : ""}: ${message}.`, + code, + subcode, + ); + this.name = "GraphApiError"; + } +} + +/** + * Maps a Meta API error payload to one of our typed errors. + * Meta error response shape: { error: { code, error_subcode, message, type, ... } } + */ +export function mapGraphError(payload: unknown): InstagramMcpError { + if (typeof payload !== "object" || payload === null) { + return new GraphApiError("Unknown error (non-object response)"); + } + const errorObj = + "error" in payload && typeof (payload as { error: unknown }).error === "object" + ? ((payload as { error: Record }).error) + : (payload as Record); + const code = typeof errorObj.code === "number" ? errorObj.code : undefined; + const subcode = + typeof errorObj.error_subcode === "number" ? errorObj.error_subcode : undefined; + const message = typeof errorObj.message === "string" ? errorObj.message : "Unknown error"; + + if (code === 190) return new AuthError(code, subcode); + if (code === 10 || code === 200 || code === 299) return new PermissionError(code, message); + if (code === 4 || code === 17 || code === 32 || code === 613) return new RateLimitError(code); + if (code === 2207026 || subcode === 2207026) + return new MediaIneligibleError(message); + if (code === 803 || code === 100) return new NotFoundError(message); + return new GraphApiError(message, code, subcode); +} diff --git a/.instagram-mcp.bak/src/feature-flags.ts b/.instagram-mcp.bak/src/feature-flags.ts new file mode 100644 index 00000000..c664c221 --- /dev/null +++ b/.instagram-mcp.bak/src/feature-flags.ts @@ -0,0 +1,48 @@ +/** + * Single source of truth for which tools light up in the current build. + * + * Versioning contract: + * v0.1.x = read-only baseline. All flags below false. + * v0.2.x = +publishing. + * v0.3.x = +commentModeration. + * v0.4.x = +messaging. + * + * Bumped by hand per Meta App Review approval, then re-published to npm. + * Users on `npx -y instagram-mcp-buddy` pick up the new flags on their next run. + * + * The INSTAGRAM_MCP_ENABLE_* env-var overrides let app admins (i.e. the + * developer running their own Meta dev app) exercise pre-approval tools + * locally without re-publishing the package. + */ + +export const FEATURES = { + /** + * `instagram_business_content_publish` — hardest Meta App Review. + * Gates: all 5 publish_* tools, all 3 container ops, delete_media. + */ + publishing: false, + /** + * The WRITE half of `instagram_business_manage_comments`. List endpoints + * stay always-on because the same scope grants both read and write. + * Gates: reply_to_comment, delete_comment, hide_comment, toggle_comments. + */ + commentModeration: false, + /** + * `instagram_business_manage_messages` — also a hard review. + * Gates: list_conversations, list_messages, send_message. + */ + messaging: false, +} as const; + +export type FeatureFlag = keyof typeof FEATURES; + +const envOverride = (name: FeatureFlag): boolean => + process.env[`INSTAGRAM_MCP_ENABLE_${name.toUpperCase()}`] === "true"; + +export function isEnabled(flag: FeatureFlag): boolean { + return FEATURES[flag] || envOverride(flag); +} + +export function enabledFlags(): FeatureFlag[] { + return (Object.keys(FEATURES) as FeatureFlag[]).filter(isEnabled); +} diff --git a/.instagram-mcp.bak/src/graph-client.ts b/.instagram-mcp.bak/src/graph-client.ts new file mode 100644 index 00000000..12155037 --- /dev/null +++ b/.instagram-mcp.bak/src/graph-client.ts @@ -0,0 +1,229 @@ +import type { AuthState } from "./auth-state.js"; +import type { Config } from "./config.js"; +import { API_BASE } from "./oauth-config.js"; +import { + GraphApiError, + InstagramMcpError, + NotConnectedError, + mapGraphError, +} from "./errors.js"; +import { log } from "./logger.js"; + +export interface RequestOptions { + method?: "GET" | "POST" | "DELETE"; + query?: Record; + body?: Record; + timeoutMs?: number; + authenticated?: boolean; +} + +export interface PaginatedResponse { + data: T[]; + paging?: { + cursors?: { before?: string; after?: string }; + next?: string; + previous?: string; + }; +} + +const MAX_ATTEMPTS = 3; +const BACKOFF_MS = [1000, 2000, 4000]; + +export class GraphClient { + private readonly baseUrl: string; + private readonly defaultTimeout: number; + + constructor( + public readonly config: Config, + private readonly authState: AuthState, + ) { + this.baseUrl = `${API_BASE}/${config.IG_GRAPH_API_VERSION}`; + this.defaultTimeout = config.IG_DEFAULT_TIMEOUT_MS; + } + + /** Authenticated user's IG Business Account id. Raises if unconnected. */ + async igUserId(): Promise { + const id = await this.authState.getUserId(); + if (!id) throw new NotConnectedError(); + return id; + } + + /** Bare access to the AuthState (auth tools need this). */ + get auth(): AuthState { + return this.authState; + } + + async request(path: string, opts: RequestOptions = {}): Promise { + const method = opts.method ?? "GET"; + const authenticated = opts.authenticated !== false; + + let token: string | null = null; + if (authenticated) { + token = await this.authState.getToken(); + if (!token) throw new NotConnectedError(); + } + + const url = new URL(`${this.baseUrl}${path.startsWith("/") ? path : `/${path}`}`); + if (opts.query) { + for (const [k, v] of Object.entries(opts.query)) { + if (v !== undefined && v !== null) url.searchParams.set(k, String(v)); + } + } + if (authenticated && token) { + url.searchParams.set("access_token", token); + } + + const headers: Record = { + "User-Agent": "instagram-mcp-buddy/0.1", + Accept: "application/json", + }; + let body: string | undefined; + if (opts.body) { + headers["Content-Type"] = "application/json"; + body = JSON.stringify(opts.body); + } + + const timeoutMs = opts.timeoutMs ?? this.defaultTimeout; + + let lastError: unknown; + for (let attempt = 1; attempt <= MAX_ATTEMPTS; attempt++) { + const controller = new AbortController(); + const timer = setTimeout(() => controller.abort(), timeoutMs); + const started = Date.now(); + try { + const res = await fetch(url.toString(), { + method, + headers, + body, + signal: controller.signal, + }); + clearTimeout(timer); + const elapsed = Date.now() - started; + + this.checkUsageHeaders(res); + + if (res.ok) { + log.debug("graph_api_call", { method, path, status: res.status, elapsedMs: elapsed }); + if (res.status === 204) return undefined as T; + const text = await res.text(); + if (!text) return undefined as T; + try { + return JSON.parse(text) as T; + } catch { + throw new GraphApiError(`Non-JSON response: ${text.slice(0, 200)}`); + } + } + + const errText = await res.text(); + let errPayload: unknown; + try { + errPayload = JSON.parse(errText); + } catch { + errPayload = { error: { message: errText || res.statusText, code: res.status } }; + } + const mapped = mapGraphError(errPayload); + + const transient = res.status === 429 || res.status >= 500; + if (transient && attempt < MAX_ATTEMPTS) { + const wait = BACKOFF_MS[attempt - 1] ?? 4000; + log.warn("graph_api_retry", { + attempt, + status: res.status, + waitMs: wait, + path, + }); + await sleep(wait); + continue; + } + log.error("graph_api_error", { + method, + path, + status: res.status, + message: mapped.message, + }); + throw mapped; + } catch (err) { + clearTimeout(timer); + if (err instanceof InstagramMcpError) throw err; + lastError = err; + if (attempt < MAX_ATTEMPTS) { + const wait = BACKOFF_MS[attempt - 1] ?? 4000; + log.warn("graph_api_network_retry", { + attempt, + waitMs: wait, + error: (err as Error).message, + }); + await sleep(wait); + continue; + } + throw new GraphApiError( + `Network error after ${MAX_ATTEMPTS} attempts: ${(err as Error).message}`, + ); + } + } + throw new GraphApiError( + `Exhausted retries: ${(lastError as Error)?.message ?? "unknown"}`, + ); + } + + async paginate( + path: string, + opts: RequestOptions & { maxItems?: number } = {}, + ): Promise<{ items: T[]; nextCursor?: string }> { + const maxItems = opts.maxItems ?? 100; + const items: T[] = []; + let nextCursor: string | undefined; + let next: string | undefined; + let currentPath = path; + let currentQuery = opts.query; + + while (items.length < maxItems) { + const res = await this.request>(currentPath, { + ...opts, + query: currentQuery, + }); + const data = res?.data ?? []; + for (const item of data) { + if (items.length >= maxItems) break; + items.push(item); + } + next = res?.paging?.next; + nextCursor = res?.paging?.cursors?.after; + if (!next || data.length === 0 || items.length >= maxItems) break; + const nextUrl = new URL(next); + currentPath = nextUrl.pathname.replace(/^\/v\d+\.\d+/, ""); + currentQuery = Object.fromEntries(nextUrl.searchParams.entries()); + } + return { items, nextCursor }; + } + + private checkUsageHeaders(res: Response): void { + const usageHeader = + res.headers.get("x-business-use-case-usage") ?? + res.headers.get("x-app-usage"); + if (!usageHeader) return; + try { + const usage = JSON.parse(usageHeader); + const numbers: number[] = []; + const collect = (obj: unknown): void => { + if (typeof obj !== "object" || obj === null) return; + for (const v of Object.values(obj as Record)) { + if (typeof v === "number") numbers.push(v); + else if (Array.isArray(v)) v.forEach(collect); + else if (typeof v === "object" && v !== null) collect(v); + } + }; + collect(usage); + const peak = numbers.length > 0 ? Math.max(...numbers) : 0; + if (peak >= 75) { + log.warn("graph_api_usage_high", { peakPercent: peak }); + } + } catch { + // Ignore. + } + } +} + +function sleep(ms: number): Promise { + return new Promise((resolve) => setTimeout(resolve, ms)); +} diff --git a/.instagram-mcp.bak/src/index.ts b/.instagram-mcp.bak/src/index.ts new file mode 100644 index 00000000..de030f7a --- /dev/null +++ b/.instagram-mcp.bak/src/index.ts @@ -0,0 +1,112 @@ +#!/usr/bin/env node +import "./env-bootstrap.js"; +import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; +import { AuthState } from "./auth-state.js"; +import { loadConfig } from "./config.js"; +import { enabledFlags } from "./feature-flags.js"; +import { GraphClient } from "./graph-client.js"; +import { InstagramMcpError } from "./errors.js"; +import { log, setLogLevel } from "./logger.js"; +import { hasInjectedCredentials } from "./oauth-config.js"; +import { runPublishedBuddySubprocess, shouldUseNpmOAuthFallback } from "./npm-connect-fallback.js"; +import { buildServer } from "./server.js"; + +/** + * One-shot OAuth for desktop hosts (OpenSwarm Electron). Writes a single JSON + * line to stdout; all diagnostics go to stderr (same rule as MCP stdio). + */ +export async function runConnectCli(): Promise { + const config = loadConfig(); + setLogLevel(config.LOG_LEVEL); + log.info("connect_cli_starting", { embeddedCredentials: hasInjectedCredentials() }); + if (shouldUseNpmOAuthFallback()) { + log.info("connect_cli_delegating_npm"); + process.stderr.write( + "instagram-mcp-buddy: no embedded Meta app in this build — running OAuth via `npx -y instagram-mcp-buddy connect` (requires network). Set INSTAGRAM_MCP_NO_NPX_FALLBACK=1 to fail fast instead.\n", + ); + await runPublishedBuddySubprocess("connect", { forwardStdoutJsonLine: true }); + return; + } + const authState = await AuthState.create(); + await authState.connect(); + const client = new GraphClient(config, authState); + let username: string | undefined; + try { + const me = await client.request<{ username?: string }>("/me", { + query: { fields: "user_id,username" }, + }); + username = me.username; + } catch (err) { + log.warn("connect_cli_username_fetch_failed", { reason: (err as Error).message }); + } + const igUserId = (await authState.getUserId()) ?? ""; + const payload = { + ok: true as const, + ig_user_id: igUserId, + username, + granted_scopes: authState.getGrantedScopes(), + enabled_features: enabledFlags(), + }; + process.stdout.write(`${JSON.stringify(payload)}\n`); +} + +export async function runLogoutCli(): Promise { + const config = loadConfig(); + setLogLevel(config.LOG_LEVEL); + const authState = await AuthState.create(); + await authState.disconnect(); + process.stdout.write(`${JSON.stringify({ ok: true as const, disconnected: true })}\n`); +} + +export async function main(): Promise { + const config = loadConfig(); + setLogLevel(config.LOG_LEVEL); + + log.info("instagram_mcp_buddy_starting", { + version: "0.1.0", + graphApiVersion: config.IG_GRAPH_API_VERSION, + enabledFeatures: enabledFlags(), + embeddedCredentials: hasInjectedCredentials(), + }); + + const { server, authState, registeredToolCount } = await buildServer(config); + const transport = new StdioServerTransport(); + + await server.connect(transport); + + log.info( + authState.isConnected() ? "instagram_mcp_buddy_ready" : "instagram_mcp_buddy_ready_unconnected", + { + connected: authState.isConnected(), + tools: registeredToolCount, + }, + ); + + const shutdown = (signal: string) => { + log.info("instagram_mcp_buddy_shutdown", { signal }); + process.exit(0); + }; + process.on("SIGINT", () => shutdown("SIGINT")); + process.on("SIGTERM", () => shutdown("SIGTERM")); +} + +const argvCmd = process.argv[2]; +if (argvCmd === "connect") { + runConnectCli().catch((err) => { + const msg = err instanceof InstagramMcpError ? err.message : (err as Error).message; + log.error("instagram_mcp_connect_cli_fatal", { error: msg }); + process.stderr.write(`${msg}\n`); + process.exit(1); + }); +} else if (argvCmd === "logout") { + runLogoutCli().catch((err) => { + log.error("instagram_mcp_logout_cli_fatal", { error: (err as Error).message }); + process.stderr.write(`${(err as Error).message}\n`); + process.exit(1); + }); +} else { + main().catch((err) => { + log.error("instagram_mcp_buddy_fatal", { error: (err as Error).message }); + process.exit(1); + }); +} diff --git a/.instagram-mcp.bak/src/logger.ts b/.instagram-mcp.bak/src/logger.ts new file mode 100644 index 00000000..114631e5 --- /dev/null +++ b/.instagram-mcp.bak/src/logger.ts @@ -0,0 +1,36 @@ +/** + * Stderr-only logger. STDOUT IS RESERVED FOR THE MCP JSON-RPC TRANSPORT. + * A single console.log to stdout will corrupt the protocol and break every client. + */ + +type Level = "debug" | "info" | "warn" | "error"; + +const LEVEL_ORDER: Record = { debug: 0, info: 1, warn: 2, error: 3 }; + +let currentLevel: Level = "info"; + +export function setLogLevel(level: Level): void { + currentLevel = level; +} + +function shouldLog(level: Level): boolean { + return LEVEL_ORDER[level] >= LEVEL_ORDER[currentLevel]; +} + +function emit(level: Level, msg: string, meta?: Record): void { + if (!shouldLog(level)) return; + const line = { + ts: new Date().toISOString(), + level, + msg, + ...(meta ?? {}), + }; + process.stderr.write(JSON.stringify(line) + "\n"); +} + +export const log = { + debug: (msg: string, meta?: Record) => emit("debug", msg, meta), + info: (msg: string, meta?: Record) => emit("info", msg, meta), + warn: (msg: string, meta?: Record) => emit("warn", msg, meta), + error: (msg: string, meta?: Record) => emit("error", msg, meta), +}; diff --git a/.instagram-mcp.bak/src/npm-connect-fallback.ts b/.instagram-mcp.bak/src/npm-connect-fallback.ts new file mode 100644 index 00000000..f7ffbd3e --- /dev/null +++ b/.instagram-mcp.bak/src/npm-connect-fallback.ts @@ -0,0 +1,76 @@ +import { spawn } from "node:child_process"; +import { InstagramMcpError } from "./errors.js"; +import { hasInjectedCredentials } from "./oauth-config.js"; + +/** + * Local workspaces / git checkouts ship REPLACE_AT_BUILD unless you inject + * credentials or set INSTAGRAM_MCP_APP_* . Published `instagram-mcp-buddy` + * on npm already embeds the maintainer Meta app. When this process has no + * credentials, we delegate connect (and optionally logout) to that package + * via npx so users are not forced to create a Meta developer app. + * + * Set INSTAGRAM_MCP_NO_NPX_FALLBACK=1 to disable (air‑gapped / policy). + * + * `runPublishedBuddySubprocess` sets INSTAGRAM_MCP_NPX_DELEGATE_CHILD=1 on the + * child so the npx-invoked process never delegates again (avoids infinite npx). + */ +const DELEGATE_CHILD_ENV = "INSTAGRAM_MCP_NPX_DELEGATE_CHILD"; + +export function shouldUseNpmOAuthFallback(): boolean { + if (process.env.INSTAGRAM_MCP_NO_NPX_FALLBACK === "1") return false; + if (process.env[DELEGATE_CHILD_ENV] === "1") return false; + return !hasInjectedCredentials(); +} + +export interface RunPublishedBuddySubprocessOpts { + /** Emit the last non-empty stdout line as JSON (connect CLI contract). */ + forwardStdoutJsonLine?: boolean; +} + +/** + * Run `npx -y instagram-mcp-buddy ` with the current env (feature + * flags, etc.). Forwards child stderr to our stderr; collects stdout. + */ +export async function runPublishedBuddySubprocess( + subcommand: "connect" | "logout", + opts: RunPublishedBuddySubprocessOpts = {}, +): Promise { + const isWin = process.platform === "win32"; + const child = spawn("npx", ["-y", "instagram-mcp-buddy", subcommand], { + env: { ...process.env, [DELEGATE_CHILD_ENV]: "1" }, + stdio: ["inherit", "pipe", "pipe"], + shell: isWin, + windowsHide: isWin, + }); + + let out = ""; + let err = ""; + return new Promise((resolve, reject) => { + child.stdout?.on("data", (d: Buffer) => { + out += d.toString(); + }); + child.stderr?.on("data", (d: Buffer) => { + err += d.toString(); + process.stderr.write(d); + }); + child.on("error", (e) => { + reject( + new InstagramMcpError( + `Failed to spawn npx (is Node/npm installed and on PATH?): ${(e as Error).message}`, + ), + ); + }); + child.on("close", (code) => { + if (code !== 0 && code !== null) { + const msg = (err || out).trim() || `npx instagram-mcp-buddy ${subcommand} exited ${code}`; + reject(new InstagramMcpError(msg)); + return; + } + const line = out.trim().split(/\r?\n/).filter(Boolean).pop(); + if (opts.forwardStdoutJsonLine && line) { + process.stdout.write(`${line}\n`); + } + resolve(line); + }); + }); +} diff --git a/.instagram-mcp.bak/src/oauth-config.ts b/.instagram-mcp.bak/src/oauth-config.ts new file mode 100644 index 00000000..713141d0 --- /dev/null +++ b/.instagram-mcp.bak/src/oauth-config.ts @@ -0,0 +1,64 @@ +/** + * Embedded Meta App credentials. + * + * At publish time `scripts/inject-credentials.mjs` rewrites the + * REPLACE_AT_BUILD placeholders in `dist/oauth-config.js` with the real prod + * values. For local development, set INSTAGRAM_MCP_APP_ID and + * INSTAGRAM_MCP_APP_SECRET in your shell against your own Meta dev app and + * the env values win. + * + * Note that the app secret WILL ship inside the published npm artifact. + * This is the standard installed-app OAuth tradeoff (Spotify desktop, + * Vercel CLI, Notion, gh CLI all do the same). The actual security + * boundary is the per-user access token, stored only in the user's OS + * keychain on their own machine. + */ + +import { isEnabled } from "./feature-flags.js"; + +export const META_APP_ID: string = + process.env.INSTAGRAM_MCP_APP_ID ?? "REPLACE_AT_BUILD"; +export const META_APP_SECRET: string = + process.env.INSTAGRAM_MCP_APP_SECRET ?? "REPLACE_AT_BUILD"; + +/** Instagram Login API — 2024 flow, no Facebook Page required. */ +export const AUTH_BASE = "https://www.instagram.com/oauth/authorize"; +/** Short-lived token exchange. */ +export const TOKEN_BASE = "https://api.instagram.com/oauth/access_token"; +/** Long-lived token endpoint + Graph API root. */ +export const API_BASE = "https://graph.instagram.com"; + +/** + * Always-on scopes. Note that `instagram_business_manage_comments` is always + * requested at OAuth time (it grants both read and write); the + * `commentModeration` feature flag only controls whether the WRITE tools + * are exposed in this build, not whether the scope is requested. + */ +const ALWAYS_ON_SCOPES = [ + "instagram_business_basic", + "instagram_business_manage_comments", + "instagram_business_manage_insights", +] as const; + +/** + * Compute the OAuth scope list for the current build. Adds the scopes for + * any feature flag that is currently enabled (either at build time or via + * env override). + */ +export function buildScopes(): string[] { + const scopes = new Set(ALWAYS_ON_SCOPES); + if (isEnabled("publishing")) scopes.add("instagram_business_content_publish"); + if (isEnabled("messaging")) scopes.add("instagram_business_manage_messages"); + return [...scopes]; +} + +export function hasInjectedCredentials(): boolean { + const id = META_APP_ID.trim(); + const secret = META_APP_SECRET.trim(); + return ( + id.length > 0 && + secret.length > 0 && + id !== "REPLACE_AT_BUILD" && + secret !== "REPLACE_AT_BUILD" + ); +} diff --git a/.instagram-mcp.bak/src/oauth.ts b/.instagram-mcp.bak/src/oauth.ts new file mode 100644 index 00000000..ee0ced37 --- /dev/null +++ b/.instagram-mcp.bak/src/oauth.ts @@ -0,0 +1,339 @@ +import { randomBytes } from "node:crypto"; +import { createServer, type IncomingMessage, type Server, type ServerResponse } from "node:http"; +import { URL } from "node:url"; +import { + API_BASE, + AUTH_BASE, + META_APP_ID, + META_APP_SECRET, + TOKEN_BASE, + buildScopes, + hasInjectedCredentials, +} from "./oauth-config.js"; +import type { StoredToken } from "./token-store.js"; +import { InstagramMcpError } from "./errors.js"; +import { log } from "./logger.js"; + +const DEFAULT_PORT = 54321; +const CALLBACK_TIMEOUT_MS = 5 * 60 * 1000; + +export interface RunOAuthFlowOpts { + /** + * Override the browser-opener (tests inject a function that programmatically + * hits the callback URL). Default uses the `open` npm package. + */ + openBrowser?: (url: string) => Promise; + /** Override port discovery (tests). */ + getPort?: () => Promise; + /** Cap how long we'll wait for the callback (tests). */ + timeoutMs?: number; +} + +/** + * Run the full OAuth dance: spin up a localhost callback server, open the + * user's browser to Instagram's authorize page, exchange the code for a + * short-lived token, then upgrade it to a long-lived 60-day token. + */ +export async function runOAuthFlow(opts: RunOAuthFlowOpts = {}): Promise { + if (!hasInjectedCredentials()) { + throw new InstagramMcpError( + "This build of instagram-mcp-buddy has no embedded Meta App credentials. " + + "From a git checkout: copy instagram-mcp/.env.example to instagram-mcp/.env, set " + + "INSTAGRAM_MCP_APP_ID and INSTAGRAM_MCP_APP_SECRET (Meta developer app), then run " + + "`npm run build` — the CLI loads .env automatically — or bake secrets into dist with " + + "`npm run build:inject`. Maintainers publishing to npm: run `npm run publish:npm` with " + + "META_APP_ID and META_APP_SECRET set. " + + "The `npx instagram-mcp-buddy connect` fallback only works if the published package " + + "on npm was built with inject-credentials (not plain REPLACE_AT_BUILD).", + ); + } + + const port = await (opts.getPort ?? findFreePort)(); + const redirectUri = `http://localhost:${port}/callback`; + const state = randomBytes(16).toString("hex"); + const scopes = buildScopes(); + + const authUrl = new URL(AUTH_BASE); + authUrl.searchParams.set("client_id", META_APP_ID); + authUrl.searchParams.set("redirect_uri", redirectUri); + authUrl.searchParams.set("response_type", "code"); + authUrl.searchParams.set("scope", scopes.join(",")); + authUrl.searchParams.set("state", state); + + log.info("oauth_starting", { port, scopes }); + + const codePromise = waitForCallback({ + port, + expectedState: state, + timeoutMs: opts.timeoutMs ?? CALLBACK_TIMEOUT_MS, + }); + // The callback can fire (and reject) before `await openBrowser(...)` returns + // — fast paths like programmatic browsers in tests, or instant CSRF/error + // rejections. Attach a no-op handler so the runtime doesn't treat the + // rejection as unhandled. The `await codePromise` below still re-throws. + codePromise.catch(() => { + /* handled by the await below */ + }); + + await openBrowser(authUrl.toString(), opts.openBrowser); + + const code = await codePromise; + log.info("oauth_code_received"); + + const shortLived = await exchangeCodeForShortLivedToken(code, redirectUri); + log.info("oauth_short_token_received", { user_id: shortLived.user_id }); + + const longLived = await exchangeForLongLivedToken(shortLived.access_token); + log.info("oauth_long_token_received", { expires_in: longLived.expires_in }); + + return { + access_token: longLived.access_token, + user_id: String(shortLived.user_id), + expires_at: Date.now() + longLived.expires_in * 1000, + obtained_at: Date.now(), + granted_scopes: scopes, + }; +} + +/* -------------------------------------------------------------------------- */ +/* Callback server */ +/* -------------------------------------------------------------------------- */ + +interface WaitOpts { + port: number; + expectedState: string; + timeoutMs: number; +} + +function waitForCallback(opts: WaitOpts): Promise { + return new Promise((resolve, reject) => { + let server: Server | undefined; + const timer = setTimeout(() => { + server?.close(); + reject( + new InstagramMcpError( + `Did not receive Instagram OAuth callback within ${opts.timeoutMs}ms. ` + + "Either the browser was closed before authorization completed, or a " + + "firewall is blocking localhost. Re-run instagram_connect and complete " + + "the login in the browser tab that opens.", + ), + ); + }, opts.timeoutMs); + + const handler = (req: IncomingMessage, res: ServerResponse) => { + if (!req.url) { + respondError(res, 400, "Bad request."); + return; + } + const u = new URL(req.url, `http://localhost:${opts.port}`); + if (u.pathname !== "/callback") { + respondError(res, 404, "Not found."); + return; + } + const err = u.searchParams.get("error"); + if (err) { + const desc = + u.searchParams.get("error_description") ?? + u.searchParams.get("error_reason") ?? + err; + respondError(res, 400, `Instagram rejected authorization: ${desc}`); + clearTimeout(timer); + server?.close(); + reject(new InstagramMcpError(`Instagram rejected authorization: ${desc}`)); + return; + } + const state = u.searchParams.get("state"); + const code = u.searchParams.get("code"); + if (state !== opts.expectedState) { + respondError(res, 400, "Authorization rejected: state mismatch."); + clearTimeout(timer); + server?.close(); + reject( + new InstagramMcpError( + "OAuth state mismatch — possible CSRF. Re-run instagram_connect.", + ), + ); + return; + } + if (!code) { + respondError(res, 400, "Authorization rejected: no code returned."); + clearTimeout(timer); + server?.close(); + reject(new InstagramMcpError("Instagram returned no authorization code.")); + return; + } + respondSuccess(res); + clearTimeout(timer); + // Close after a short delay so the response can flush. + setTimeout(() => server?.close(), 100); + resolve(code); + }; + + server = createServer(handler); + server.on("error", (err) => { + clearTimeout(timer); + reject( + new InstagramMcpError( + `Could not bind localhost:${opts.port} for the OAuth callback: ${err.message}`, + ), + ); + }); + server.listen(opts.port, "127.0.0.1"); + }); +} + +function respondSuccess(res: ServerResponse) { + res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" }); + res.end(` +Connected + +
+

Connected to Instagram

+

You can close this tab and return to your agent.

+
`); +} + +function respondError(res: ServerResponse, status: number, msg: string) { + res.writeHead(status, { "Content-Type": "text/plain; charset=utf-8" }); + res.end(msg); +} + +/* -------------------------------------------------------------------------- */ +/* Token exchanges */ +/* -------------------------------------------------------------------------- */ + +interface ShortLivedTokenResponse { + access_token: string; + user_id: string | number; +} + +async function exchangeCodeForShortLivedToken( + code: string, + redirectUri: string, +): Promise { + const body = new URLSearchParams({ + client_id: META_APP_ID, + client_secret: META_APP_SECRET, + grant_type: "authorization_code", + redirect_uri: redirectUri, + code, + }); + const res = await fetch(TOKEN_BASE, { + method: "POST", + headers: { + "Content-Type": "application/x-www-form-urlencoded", + Accept: "application/json", + }, + body: body.toString(), + }); + const text = await res.text(); + if (!res.ok) { + throw new InstagramMcpError( + `Failed to exchange code for short-lived token (HTTP ${res.status}): ${text.slice(0, 300)}`, + ); + } + let parsed: ShortLivedTokenResponse; + try { + parsed = JSON.parse(text) as ShortLivedTokenResponse; + } catch { + throw new InstagramMcpError( + `Token exchange returned non-JSON: ${text.slice(0, 200)}`, + ); + } + if (!parsed.access_token) { + throw new InstagramMcpError("Token exchange returned no access_token."); + } + return parsed; +} + +interface LongLivedTokenResponse { + access_token: string; + token_type?: string; + expires_in: number; +} + +async function exchangeForLongLivedToken( + shortLivedToken: string, +): Promise { + const url = new URL(`${API_BASE}/access_token`); + url.searchParams.set("grant_type", "ig_exchange_token"); + url.searchParams.set("client_secret", META_APP_SECRET); + url.searchParams.set("access_token", shortLivedToken); + const res = await fetch(url, { method: "GET", headers: { Accept: "application/json" } }); + const text = await res.text(); + if (!res.ok) { + throw new InstagramMcpError( + `Failed to upgrade to long-lived token (HTTP ${res.status}): ${text.slice(0, 300)}`, + ); + } + return JSON.parse(text) as LongLivedTokenResponse; +} + +/* -------------------------------------------------------------------------- */ +/* Refresh */ +/* -------------------------------------------------------------------------- */ + +export async function refreshAccessToken(currentToken: string): Promise { + const url = new URL(`${API_BASE}/refresh_access_token`); + url.searchParams.set("grant_type", "ig_refresh_token"); + url.searchParams.set("access_token", currentToken); + const res = await fetch(url, { method: "GET", headers: { Accept: "application/json" } }); + const text = await res.text(); + if (!res.ok) { + throw new InstagramMcpError( + `Failed to refresh long-lived token (HTTP ${res.status}): ${text.slice(0, 300)}`, + ); + } + return JSON.parse(text) as LongLivedTokenResponse; +} + +/* -------------------------------------------------------------------------- */ +/* Helpers */ +/* -------------------------------------------------------------------------- */ + +async function findFreePort(): Promise { + try { + const mod = await import("get-port"); + const getPort = (mod.default ?? mod) as unknown as ( + opts?: { port?: number | number[] | { from: number; to: number } }, + ) => Promise; + return await getPort({ port: [DEFAULT_PORT, 54322, 54323, 54324, 54325] }); + } catch { + return DEFAULT_PORT; + } +} + +async function openBrowser( + url: string, + override?: (url: string) => Promise, +): Promise { + if (override) { + await override(url); + return; + } + try { + const mod = await import("open"); + const open = (mod.default ?? mod) as unknown as ( + target: string, + ) => Promise; + await open(url); + log.info("oauth_browser_opened"); + } catch (err) { + log.warn("oauth_browser_open_failed", { reason: (err as Error).message }); + // Surface the URL anyway so the user can paste it manually. + process.stderr.write( + `\ninstagram-mcp-buddy: open this URL in your browser to connect:\n ${url}\n\n`, + ); + } +} diff --git a/.instagram-mcp.bak/src/server.ts b/.instagram-mcp.bak/src/server.ts new file mode 100644 index 00000000..7168f304 --- /dev/null +++ b/.instagram-mcp.bak/src/server.ts @@ -0,0 +1,62 @@ +import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { AuthState } from "./auth-state.js"; +import type { Config } from "./config.js"; +import { GraphClient } from "./graph-client.js"; +import { + filterEnabledTools, + registerTools, + type AnyToolDefinition, +} from "./tool-registry.js"; +import { accountTools } from "./tools/account.js"; +import { authTools } from "./tools/auth.js"; +import { commentTools } from "./tools/comments.js"; +import { containerTools } from "./tools/containers.js"; +import { discoveryTools } from "./tools/discovery.js"; +import { hashtagTools } from "./tools/hashtags.js"; +import { insightsTools } from "./tools/insights.js"; +import { mediaTools } from "./tools/media.js"; +import { mentionTools } from "./tools/mentions.js"; +import { messageTools } from "./tools/messages.js"; +import { publishTools } from "./tools/publish.js"; + +export interface BuildServerResult { + server: McpServer; + authState: AuthState; + client: GraphClient; + registeredToolCount: number; +} + +export async function buildServer(config: Config): Promise { + const server = new McpServer({ + name: "instagram-mcp-buddy", + version: "0.1.0", + }); + const authState = await AuthState.create(); + const client = new GraphClient(config, authState); + + // Auth tools first so agents can discover instagram_connect / instagram_status + // even before any account is linked. Feature-gated tools come after. + const allTools: AnyToolDefinition[] = [ + ...authTools, + ...accountTools, + ...mediaTools, + ...insightsTools, + ...commentTools, + ...hashtagTools, + ...discoveryTools, + ...mentionTools, + ...publishTools, + ...containerTools, + ...messageTools, + ]; + + const enabled = filterEnabledTools(allTools); + registerTools(server, client, enabled); + + return { + server, + authState, + client, + registeredToolCount: enabled.length, + }; +} diff --git a/.instagram-mcp.bak/src/token-store.ts b/.instagram-mcp.bak/src/token-store.ts new file mode 100644 index 00000000..117e6d3d --- /dev/null +++ b/.instagram-mcp.bak/src/token-store.ts @@ -0,0 +1,232 @@ +import { + createCipheriv, + createDecipheriv, + createHash, + randomBytes, +} from "node:crypto"; +import { mkdir, readFile, rm, writeFile } from "node:fs/promises"; +import { homedir, platform } from "node:os"; +import { dirname, join } from "node:path"; +import { log } from "./logger.js"; + +export interface StoredToken { + access_token: string; + user_id: string; + /** Unix epoch ms when the token expires. */ + expires_at: number; + /** Unix epoch ms when the token was last obtained or refreshed. */ + obtained_at: number; + /** Scopes granted by the user. */ + granted_scopes: string[]; +} + +export interface TokenStore { + load(): Promise; + save(token: StoredToken): Promise; + clear(): Promise; + /** For logging/UX so users know where their token sits. */ + describe(): string; +} + +const SERVICE = "instagram-mcp-buddy"; +const ACCOUNT = "default"; + +/* -------------------------------------------------------------------------- */ +/* OS keychain (preferred) */ +/* -------------------------------------------------------------------------- */ + +class KeychainTokenStore implements TokenStore { + constructor( + private readonly keytar: { + getPassword: (s: string, a: string) => Promise; + setPassword: (s: string, a: string, p: string) => Promise; + deletePassword: (s: string, a: string) => Promise; + }, + ) {} + + async load(): Promise { + const raw = await this.keytar.getPassword(SERVICE, ACCOUNT); + if (!raw) return null; + try { + return JSON.parse(raw) as StoredToken; + } catch { + log.warn("token_store_parse_failed", { backend: "keychain" }); + return null; + } + } + + async save(token: StoredToken): Promise { + await this.keytar.setPassword(SERVICE, ACCOUNT, JSON.stringify(token)); + } + + async clear(): Promise { + await this.keytar.deletePassword(SERVICE, ACCOUNT); + } + + describe(): string { + return "OS keychain"; + } +} + +async function tryLoadKeytar(): Promise { + try { + const mod = (await import("keytar")) as unknown as { + default?: { + getPassword: (s: string, a: string) => Promise; + setPassword: (s: string, a: string, p: string) => Promise; + deletePassword: (s: string, a: string) => Promise; + }; + getPassword?: (s: string, a: string) => Promise; + setPassword?: (s: string, a: string, p: string) => Promise; + deletePassword?: (s: string, a: string) => Promise; + }; + const api = mod.default ?? mod; + if ( + typeof api.getPassword !== "function" || + typeof api.setPassword !== "function" || + typeof api.deletePassword !== "function" + ) { + return null; + } + // Smoke-test that the native binding actually works on this host. + await api.getPassword(SERVICE, ACCOUNT); + return new KeychainTokenStore( + api as { + getPassword: (s: string, a: string) => Promise; + setPassword: (s: string, a: string, p: string) => Promise; + deletePassword: (s: string, a: string) => Promise; + }, + ); + } catch (err) { + log.info("token_store_keychain_unavailable", { + reason: (err as Error).message, + }); + return null; + } +} + +/* -------------------------------------------------------------------------- */ +/* Encrypted file (fallback) */ +/* -------------------------------------------------------------------------- */ + +/** Hard-coded salt — security here comes from machine-id + file location. */ +const FILE_SALT = "instagram-mcp-buddy:v1:salt"; +const ALGO = "aes-256-gcm"; + +export class EncryptedFileTokenStore implements TokenStore { + constructor( + private readonly filePath: string, + private readonly keyProvider: () => Promise, + ) {} + + async load(): Promise { + let raw: string; + try { + raw = await readFile(this.filePath, "utf8"); + } catch (err) { + if ((err as NodeJS.ErrnoException).code === "ENOENT") return null; + throw err; + } + try { + const { iv, tag, ciphertext } = JSON.parse(raw) as { + iv: string; + tag: string; + ciphertext: string; + }; + const key = await this.keyProvider(); + const decipher = createDecipheriv(ALGO, key, Buffer.from(iv, "base64")); + decipher.setAuthTag(Buffer.from(tag, "base64")); + const plaintext = Buffer.concat([ + decipher.update(Buffer.from(ciphertext, "base64")), + decipher.final(), + ]).toString("utf8"); + return JSON.parse(plaintext) as StoredToken; + } catch (err) { + log.warn("token_store_decrypt_failed", { reason: (err as Error).message }); + return null; + } + } + + async save(token: StoredToken): Promise { + const key = await this.keyProvider(); + const iv = randomBytes(12); + const cipher = createCipheriv(ALGO, key, iv); + const plaintext = Buffer.from(JSON.stringify(token), "utf8"); + const ciphertext = Buffer.concat([cipher.update(plaintext), cipher.final()]); + const tag = cipher.getAuthTag(); + await mkdir(dirname(this.filePath), { recursive: true }); + await writeFile( + this.filePath, + JSON.stringify({ + iv: iv.toString("base64"), + tag: tag.toString("base64"), + ciphertext: ciphertext.toString("base64"), + }), + { mode: 0o600 }, + ); + } + + async clear(): Promise { + await rm(this.filePath, { force: true }); + } + + describe(): string { + return `encrypted file at ${this.filePath}`; + } +} + +function defaultDataDir(): string { + if (process.env.XDG_DATA_HOME) { + return join(process.env.XDG_DATA_HOME, "instagram-mcp-buddy"); + } + if (platform() === "win32") { + return join( + process.env.APPDATA ?? join(homedir(), "AppData", "Roaming"), + "instagram-mcp-buddy", + ); + } + if (platform() === "darwin") { + return join(homedir(), "Library", "Application Support", "instagram-mcp-buddy"); + } + return join(homedir(), ".local", "share", "instagram-mcp-buddy"); +} + +async function deriveMachineKey(): Promise { + let machineId = "unknown"; + try { + const mod = await import("node-machine-id"); + machineId = await mod.machineId(); + } catch (err) { + log.warn("machine_id_fallback", { reason: (err as Error).message }); + } + return createHash("sha256").update(`${FILE_SALT}:${machineId}`).digest(); +} + +/* -------------------------------------------------------------------------- */ +/* Factory */ +/* -------------------------------------------------------------------------- */ + +export interface CreateTokenStoreOpts { + /** Override file path (tests). */ + filePath?: string; + /** Force the file backend even if keytar is available (tests). */ + forceFile?: boolean; + /** Override the key derivation (tests). */ + keyProvider?: () => Promise; +} + +export async function createTokenStore( + opts: CreateTokenStoreOpts = {}, +): Promise { + if (!opts.forceFile) { + const kc = await tryLoadKeytar(); + if (kc) { + log.debug("token_store_backend", { backend: "keychain" }); + return kc; + } + } + const filePath = opts.filePath ?? join(defaultDataDir(), "token.json"); + const keyProvider = opts.keyProvider ?? deriveMachineKey; + log.debug("token_store_backend", { backend: "encrypted_file", filePath }); + return new EncryptedFileTokenStore(filePath, keyProvider); +} diff --git a/.instagram-mcp.bak/src/tool-registry.ts b/.instagram-mcp.bak/src/tool-registry.ts new file mode 100644 index 00000000..a5ebc940 --- /dev/null +++ b/.instagram-mcp.bak/src/tool-registry.ts @@ -0,0 +1,92 @@ +import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import type { ZodRawShape, z } from "zod"; +import { type FeatureFlag, isEnabled } from "./feature-flags.js"; +import type { GraphClient } from "./graph-client.js"; +import { log } from "./logger.js"; + +export interface ToolDefinition< + TInputShape extends ZodRawShape, + TOutputShape extends ZodRawShape, +> { + name: string; + title: string; + description: string; + inputShape: TInputShape; + outputShape: TOutputShape; + annotations?: { + readOnlyHint?: boolean; + destructiveHint?: boolean; + idempotentHint?: boolean; + openWorldHint?: boolean; + }; + /** When set, the tool is skipped at registration if the flag is disabled. */ + requiredFeature?: FeatureFlag; + handler: ( + input: { [K in keyof TInputShape]: z.infer }, + client: GraphClient, + ) => Promise<{ [K in keyof TOutputShape]: z.infer }>; +} + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export type AnyToolDefinition = ToolDefinition; + +export function filterEnabledTools(tools: AnyToolDefinition[]): AnyToolDefinition[] { + const enabled: AnyToolDefinition[] = []; + for (const tool of tools) { + if (tool.requiredFeature && !isEnabled(tool.requiredFeature)) { + log.info("tool_skipped_disabled_feature", { + name: tool.name, + feature: tool.requiredFeature, + }); + continue; + } + enabled.push(tool); + } + return enabled; +} + +export function registerTools( + server: McpServer, + client: GraphClient, + tools: AnyToolDefinition[], +): void { + // Filter already applied by the caller. Iterate as-is. + for (const tool of tools) { + server.registerTool( + tool.name, + { + title: tool.title, + description: tool.description, + inputSchema: tool.inputShape, + outputSchema: tool.outputShape, + annotations: { + title: tool.title, + readOnlyHint: tool.annotations?.readOnlyHint ?? false, + destructiveHint: tool.annotations?.destructiveHint ?? false, + idempotentHint: tool.annotations?.idempotentHint ?? false, + openWorldHint: tool.annotations?.openWorldHint ?? true, + }, + }, + async (rawInput: unknown) => { + try { + const result = await tool.handler( + rawInput as Parameters[0], + client, + ); + return { + content: [{ type: "text" as const, text: JSON.stringify(result, null, 2) }], + structuredContent: result, + }; + } catch (err) { + const message = err instanceof Error ? err.message : String(err); + log.error("tool_error", { tool: tool.name, message }); + return { + isError: true, + content: [{ type: "text" as const, text: message }], + }; + } + }, + ); + log.debug("tool_registered", { name: tool.name }); + } +} diff --git a/.instagram-mcp.bak/src/tools/account.ts b/.instagram-mcp.bak/src/tools/account.ts new file mode 100644 index 00000000..c7543250 --- /dev/null +++ b/.instagram-mcp.bak/src/tools/account.ts @@ -0,0 +1,56 @@ +import { z } from "zod"; +import type { AnyToolDefinition } from "../tool-registry.js"; + +const WHO_AM_I_FIELDS = [ + "user_id", + "username", + "name", + "account_type", + "profile_picture_url", + "followers_count", + "follows_count", + "media_count", + "biography", + "website", +].join(","); + +export const accountTools: AnyToolDefinition[] = [ + { + name: "instagram_who_am_i", + title: "Who am I", + description: + "Returns the authenticated Instagram Business or Creator Account's profile. Call this once at the start of a session to confirm auth is working and to learn the account's username, type (BUSINESS|MEDIA_CREATOR), follower count, and bio.", + inputShape: {}, + outputShape: { + ig_user_id: z.string(), + username: z.string(), + name: z.string().optional(), + account_type: z.string().optional(), + followers_count: z.number().int().optional(), + follows_count: z.number().int().optional(), + media_count: z.number().int().optional(), + profile_picture_url: z.string().optional(), + biography: z.string().optional(), + website: z.string().optional(), + }, + annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: true }, + handler: async (_input, client) => { + const res = await client.request>("/me", { + query: { fields: WHO_AM_I_FIELDS }, + }); + const fallbackId = await client.igUserId(); + return { + ig_user_id: String(res.user_id ?? res.id ?? fallbackId), + username: String(res.username ?? ""), + name: res.name as string | undefined, + account_type: res.account_type as string | undefined, + followers_count: res.followers_count as number | undefined, + follows_count: res.follows_count as number | undefined, + media_count: res.media_count as number | undefined, + profile_picture_url: res.profile_picture_url as string | undefined, + biography: res.biography as string | undefined, + website: res.website as string | undefined, + }; + }, + }, +]; diff --git a/.instagram-mcp.bak/src/tools/auth.ts b/.instagram-mcp.bak/src/tools/auth.ts new file mode 100644 index 00000000..b42a8c86 --- /dev/null +++ b/.instagram-mcp.bak/src/tools/auth.ts @@ -0,0 +1,107 @@ +import { z } from "zod"; +import { enabledFlags } from "../feature-flags.js"; +import type { GraphClient } from "../graph-client.js"; +import { log } from "../logger.js"; +import type { AnyToolDefinition } from "../tool-registry.js"; + +const MS_PER_DAY = 24 * 60 * 60 * 1000; + +async function fetchUsernameSafe(client: GraphClient): Promise { + try { + const res = await client.request<{ username?: string; user_id?: string }>("/me", { + query: { fields: "user_id,username" }, + }); + return res.username; + } catch (err) { + log.warn("auth_tools_fetch_username_failed", { + reason: (err as Error).message, + }); + return undefined; + } +} + +function daysUntil(expiresAt: number): number { + return Math.max(0, Math.floor((expiresAt - Date.now()) / MS_PER_DAY)); +} + +export const authTools: AnyToolDefinition[] = [ + { + name: "instagram_connect", + title: "Connect Instagram", + description: + "Sign the user into their Instagram Business or Creator account. Opens the user's default browser to the official Instagram OAuth page. After they approve, the long-lived access token is stored in their OS keychain (or an encrypted file fallback) on their own machine. Returns the connected account's username and the set of features unlocked by this build.", + inputShape: {}, + outputShape: { + connected: z.boolean(), + username: z.string().optional(), + ig_user_id: z.string(), + granted_scopes: z.array(z.string()), + enabled_features: z.array(z.string()), + }, + annotations: { destructiveHint: true, openWorldHint: true }, + handler: async (_input, client) => { + const token = await client.auth.connect(); + const username = await fetchUsernameSafe(client); + return { + connected: true, + username, + ig_user_id: token.user_id, + granted_scopes: token.granted_scopes, + enabled_features: enabledFlags(), + }; + }, + }, + { + name: "instagram_logout", + title: "Log out of Instagram", + description: + "Clear the stored Instagram access token from this machine. The agent should call this if the user asks to disconnect, switch accounts, or wipe credentials. After logout, instagram_connect must be called again before any non-auth tool will work.", + inputShape: {}, + outputShape: { disconnected: z.boolean() }, + annotations: { destructiveHint: true, openWorldHint: false }, + handler: async (_input, client) => { + await client.auth.disconnect(); + return { disconnected: true }; + }, + }, + { + name: "instagram_status", + title: "Instagram connection status", + description: + "Report whether Instagram is currently connected and which capabilities are available in this build of instagram-mcp-buddy. Agents should call this first to discover what they can do — features unlock over time as Meta App Review approves additional permissions. Safe to call before instagram_connect.", + inputShape: {}, + outputShape: { + connected: z.boolean(), + username: z.string().optional(), + ig_user_id: z.string().optional(), + expires_at: z.string().optional(), + days_until_refresh: z.number().int().optional(), + enabled_features: z.array(z.string()), + granted_scopes: z.array(z.string()), + }, + annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: false }, + handler: async (_input, client) => { + const features = enabledFlags(); + const connected = client.auth.isConnected(); + if (!connected) { + return { + connected: false, + enabled_features: features, + granted_scopes: [], + }; + } + const igUserId = (await client.auth.getUserId()) ?? undefined; + const expiresAt = client.auth.getExpiresAt(); + const username = await fetchUsernameSafe(client); + return { + connected: true, + username, + ig_user_id: igUserId, + expires_at: expiresAt ? new Date(expiresAt).toISOString() : undefined, + days_until_refresh: expiresAt ? daysUntil(expiresAt) : undefined, + enabled_features: features, + granted_scopes: client.auth.getGrantedScopes(), + }; + }, + }, +]; diff --git a/.instagram-mcp.bak/src/tools/comments.ts b/.instagram-mcp.bak/src/tools/comments.ts new file mode 100644 index 00000000..face8f1d --- /dev/null +++ b/.instagram-mcp.bak/src/tools/comments.ts @@ -0,0 +1,141 @@ +import { z } from "zod"; +import type { AnyToolDefinition } from "../tool-registry.js"; + +const COMMENT_FIELDS = ["id", "text", "username", "timestamp", "like_count", "hidden"].join( + ",", +); + +interface RawComment { + id: string; + text?: string; + username?: string; + timestamp?: string; + like_count?: number; + hidden?: boolean; +} + +const CommentItem = { + id: z.string(), + text: z.string().optional(), + username: z.string().optional(), + timestamp: z.string().optional(), + like_count: z.number().int().optional(), + hidden: z.boolean().optional(), +}; + +export const commentTools: AnyToolDefinition[] = [ + { + name: "instagram_list_comments", + title: "List comments on a post", + description: + "Paginated list of top-level comments on one of your posts. Use the returned `next_cursor` to fetch more.", + inputShape: { + media_id: z.string(), + limit: z.number().int().min(1).max(50).default(25), + after_cursor: z.string().optional(), + }, + outputShape: { + items: z.array(z.object(CommentItem)), + next_cursor: z.string().optional(), + }, + annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: true }, + handler: async (input, client) => { + const query: Record = { + fields: COMMENT_FIELDS, + limit: input.limit, + }; + if (input.after_cursor) query.after = input.after_cursor; + const { items, nextCursor } = await client.paginate( + `/${input.media_id}/comments`, + { query, maxItems: input.limit }, + ); + return { items, next_cursor: nextCursor }; + }, + }, + { + name: "instagram_list_comment_replies", + title: "List replies to a comment", + description: "List the replies (child comments) on a parent comment.", + inputShape: { + comment_id: z.string(), + limit: z.number().int().min(1).max(50).default(25), + after_cursor: z.string().optional(), + }, + outputShape: { + items: z.array(z.object(CommentItem)), + next_cursor: z.string().optional(), + }, + annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: true }, + handler: async (input, client) => { + const query: Record = { + fields: COMMENT_FIELDS, + limit: input.limit, + }; + if (input.after_cursor) query.after = input.after_cursor; + const { items, nextCursor } = await client.paginate( + `/${input.comment_id}/replies`, + { query, maxItems: input.limit }, + ); + return { items, next_cursor: nextCursor }; + }, + }, + { + name: "instagram_reply_to_comment", + title: "Reply to a comment", + description: "Post a reply on a comment. The message can include @mentions and emoji.", + inputShape: { + comment_id: z.string(), + message: z.string().min(1).max(2200), + }, + outputShape: { + reply_id: z.string(), + created_at: z.string(), + }, + annotations: { destructiveHint: true, openWorldHint: true }, + requiredFeature: "commentModeration", + handler: async (input, client) => { + const res = await client.request<{ id: string }>( + `/${input.comment_id}/replies`, + { method: "POST", query: { message: input.message } }, + ); + return { reply_id: res.id, created_at: new Date().toISOString() }; + }, + }, + { + name: "instagram_delete_comment", + title: "Delete a comment", + description: + "Delete a comment. You can delete any comment on your own media, or any comment you authored.", + inputShape: { comment_id: z.string() }, + outputShape: { deleted: z.boolean(), comment_id: z.string() }, + annotations: { destructiveHint: true, openWorldHint: true }, + requiredFeature: "commentModeration", + handler: async (input, client) => { + await client.request(`/${input.comment_id}`, { method: "DELETE" }); + return { deleted: true, comment_id: input.comment_id }; + }, + }, + { + name: "instagram_hide_comment", + title: "Hide or unhide a comment", + description: + "Hide a comment (makes it invisible to other viewers but keeps the comment) or unhide it.", + inputShape: { + comment_id: z.string(), + hidden: z.boolean(), + }, + outputShape: { + comment_id: z.string(), + hidden: z.boolean(), + }, + annotations: { destructiveHint: true, openWorldHint: true }, + requiredFeature: "commentModeration", + handler: async (input, client) => { + await client.request(`/${input.comment_id}`, { + method: "POST", + query: { hide: input.hidden }, + }); + return { comment_id: input.comment_id, hidden: input.hidden }; + }, + }, +]; diff --git a/.instagram-mcp.bak/src/tools/containers.ts b/.instagram-mcp.bak/src/tools/containers.ts new file mode 100644 index 00000000..d8bd4768 --- /dev/null +++ b/.instagram-mcp.bak/src/tools/containers.ts @@ -0,0 +1,116 @@ +import { z } from "zod"; +import { fetchContainerStatus } from "../containers.js"; +import type { AnyToolDefinition } from "../tool-registry.js"; + +const ContainerStatus = z.enum([ + "IN_PROGRESS", + "FINISHED", + "ERROR", + "EXPIRED", + "PUBLISHED", +]); + +export const containerTools: AnyToolDefinition[] = [ + { + name: "instagram_create_container", + title: "Create media container (low-level)", + description: + "Create a media container without publishing it. Useful for scheduling workflows: create the container now, then call instagram_publish_container at the target time. Containers expire 24 hours after creation. For most use cases prefer the higher-level instagram_publish_image / publish_carousel / publish_reel tools.", + inputShape: { + media_type: z + .enum(["IMAGE", "VIDEO", "REELS", "STORIES", "CAROUSEL"]) + .describe("Container kind."), + image_url: z.string().url().optional(), + video_url: z.string().url().optional(), + caption: z.string().max(2200).optional(), + children: z + .array(z.string()) + .min(2) + .max(10) + .optional() + .describe("Child container ids for CAROUSEL only."), + is_carousel_item: z.boolean().optional(), + share_to_feed: z.boolean().optional(), + cover_url: z.string().url().optional(), + thumb_offset: z.number().int().nonnegative().optional(), + }, + outputShape: { + container_id: z.string(), + expires_at: z.string(), + }, + annotations: { destructiveHint: true, openWorldHint: true }, + requiredFeature: "publishing", + handler: async (input, client) => { + const query: Record = { + media_type: input.media_type, + image_url: input.image_url, + video_url: input.video_url, + caption: input.caption, + is_carousel_item: input.is_carousel_item, + share_to_feed: input.share_to_feed, + cover_url: input.cover_url, + thumb_offset: input.thumb_offset, + children: input.children?.join(","), + }; + const igUserId = await client.igUserId(); + const res = await client.request<{ id: string }>(`/${igUserId}/media`, { + method: "POST", + query, + }); + return { + container_id: res.id, + expires_at: new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString(), + }; + }, + }, + { + name: "instagram_get_container_status", + title: "Get media container status", + description: + "Check the processing status of a media container. Returned status is one of: IN_PROGRESS (Instagram is still fetching/encoding), FINISHED (ready to publish), ERROR (Instagram rejected it), EXPIRED (container is older than 24h), PUBLISHED (already published).", + inputShape: { + container_id: z.string(), + }, + outputShape: { + container_id: z.string(), + status: ContainerStatus, + status_code: z.string().optional(), + }, + annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: true }, + requiredFeature: "publishing", + handler: async (input, client) => { + const res = await fetchContainerStatus(client, input.container_id); + return { + container_id: input.container_id, + status: res.status, + status_code: res.status_code, + }; + }, + }, + { + name: "instagram_publish_container", + title: "Publish a finished container", + description: + "Finalize and publish a container that is in FINISHED status. Returns the resulting media_id. Pair with instagram_create_container + instagram_get_container_status for scheduled posting.", + inputShape: { + container_id: z.string(), + }, + outputShape: { + media_id: z.string(), + published_at: z.string(), + }, + annotations: { destructiveHint: true, openWorldHint: true }, + requiredFeature: "publishing", + handler: async (input, client) => { + const igUserId = await client.igUserId(); + const res = await client.request<{ id: string }>(`/${igUserId}/media_publish`, { + method: "POST", + query: { creation_id: input.container_id }, + }); + return { + media_id: res.id, + published_at: new Date().toISOString(), + }; + }, + }, +]; diff --git a/.instagram-mcp.bak/src/tools/discovery.ts b/.instagram-mcp.bak/src/tools/discovery.ts new file mode 100644 index 00000000..d0e32b12 --- /dev/null +++ b/.instagram-mcp.bak/src/tools/discovery.ts @@ -0,0 +1,117 @@ +import { z } from "zod"; +import type { AnyToolDefinition } from "../tool-registry.js"; + +interface DiscoveryMedia { + id: string; + caption?: string; + media_type?: string; + permalink?: string; + like_count?: number; + comments_count?: number; + timestamp?: string; +} + +interface DiscoveryResponse { + business_discovery?: { + id?: string; + username?: string; + name?: string; + biography?: string; + followers_count?: number; + follows_count?: number; + media_count?: number; + profile_picture_url?: string; + website?: string; + media?: { data?: DiscoveryMedia[] }; + }; +} + +export const discoveryTools: AnyToolDefinition[] = [ + { + name: "instagram_business_discovery", + title: "Look up a public Business/Creator account", + description: + "Read-only lookup of a public Business or Creator account by username. Use for competitor research, partner vetting, or audience analysis. Personal accounts are NOT accessible via this endpoint. Set include_media=true to also fetch the account's most recent posts (limited by media_limit).", + inputShape: { + username: z + .string() + .min(1) + .describe("The target account's Instagram username, without leading @."), + include_media: z.boolean().default(false), + media_limit: z.number().int().min(1).max(50).default(10), + }, + outputShape: { + username: z.string(), + name: z.string().optional(), + biography: z.string().optional(), + followers_count: z.number().int().optional(), + follows_count: z.number().int().optional(), + media_count: z.number().int().optional(), + profile_picture_url: z.string().optional(), + website: z.string().optional(), + media: z + .array( + z.object({ + id: z.string(), + caption: z.string().optional(), + media_type: z.string().optional(), + permalink: z.string().optional(), + like_count: z.number().int().optional(), + comments_count: z.number().int().optional(), + timestamp: z.string().optional(), + }), + ) + .optional(), + }, + annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: true }, + handler: async (input, client) => { + const cleaned = input.username.replace(/^@/, ""); + const accountFields = [ + "id", + "username", + "name", + "biography", + "followers_count", + "follows_count", + "media_count", + "profile_picture_url", + "website", + ]; + let fields = `business_discovery.username(${cleaned}){${accountFields.join(",")}`; + if (input.include_media) { + const mediaFields = [ + "id", + "caption", + "media_type", + "permalink", + "like_count", + "comments_count", + "timestamp", + ].join(","); + fields += `,media.limit(${input.media_limit}){${mediaFields}}`; + } + fields += "}"; + const igUserId = await client.igUserId(); + const res = await client.request(`/${igUserId}`, { + query: { fields }, + }); + const bd = res.business_discovery; + if (!bd) { + throw new Error( + `No public Business/Creator account found for username "${cleaned}". The account may be personal, private, or non-existent.`, + ); + } + return { + username: bd.username ?? cleaned, + name: bd.name, + biography: bd.biography, + followers_count: bd.followers_count, + follows_count: bd.follows_count, + media_count: bd.media_count, + profile_picture_url: bd.profile_picture_url, + website: bd.website, + media: bd.media?.data, + }; + }, + }, +]; diff --git a/.instagram-mcp.bak/src/tools/hashtags.ts b/.instagram-mcp.bak/src/tools/hashtags.ts new file mode 100644 index 00000000..05a01ed3 --- /dev/null +++ b/.instagram-mcp.bak/src/tools/hashtags.ts @@ -0,0 +1,112 @@ +import { z } from "zod"; +import type { AnyToolDefinition } from "../tool-registry.js"; + +const HASHTAG_MEDIA_FIELDS = [ + "id", + "caption", + "media_type", + "media_url", + "permalink", + "like_count", + "comments_count", + "timestamp", +].join(","); + +interface RawHashtagMedia { + id: string; + caption?: string; + media_type?: string; + media_url?: string; + permalink?: string; + like_count?: number; + comments_count?: number; + timestamp?: string; +} + +const HashtagMediaItem = { + id: z.string(), + caption: z.string().optional(), + media_type: z.string().optional(), + media_url: z.string().optional(), + permalink: z.string().optional(), + like_count: z.number().int().optional(), + comments_count: z.number().int().optional(), + timestamp: z.string().optional(), +}; + +export const hashtagTools: AnyToolDefinition[] = [ + { + name: "instagram_search_hashtag", + title: "Search hashtag", + description: + "Resolve a hashtag name to its Graph API id, which other hashtag tools require. NOTE: counts toward Meta's limit of 30 unique hashtag searches per 7-day rolling window per account. Cache the returned id if you'll use it again.", + inputShape: { name: z.string().min(1).max(100) }, + outputShape: { id: z.string(), name: z.string() }, + annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: true }, + handler: async (input, client) => { + const cleaned = input.name.replace(/^#/, ""); + const igUserId = await client.igUserId(); + const res = await client.request<{ data: Array<{ id: string }> }>( + `/ig_hashtag_search`, + { query: { user_id: igUserId, q: cleaned } }, + ); + const first = res.data?.[0]; + if (!first) { + throw new Error(`Hashtag not found: ${cleaned}`); + } + return { id: first.id, name: cleaned }; + }, + }, + { + name: "instagram_get_hashtag_top_media", + title: "Top media for hashtag", + description: + "Highest-performing public posts for a hashtag, ranked by Instagram. Note: hashtag results only include public Business/Creator account posts.", + inputShape: { + hashtag_id: z.string(), + limit: z.number().int().min(1).max(50).default(25), + }, + outputShape: { items: z.array(z.object(HashtagMediaItem)) }, + annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: true }, + handler: async (input, client) => { + const igUserId = await client.igUserId(); + const res = await client.request<{ data: RawHashtagMedia[] }>( + `/${input.hashtag_id}/top_media`, + { + query: { + user_id: igUserId, + fields: HASHTAG_MEDIA_FIELDS, + limit: input.limit, + }, + }, + ); + return { items: res.data ?? [] }; + }, + }, + { + name: "instagram_get_hashtag_recent_media", + title: "Recent media for hashtag", + description: + "Most recent public posts (last 24 hours) tagged with the hashtag. Only public Business/Creator account posts are returned.", + inputShape: { + hashtag_id: z.string(), + limit: z.number().int().min(1).max(50).default(25), + }, + outputShape: { items: z.array(z.object(HashtagMediaItem)) }, + annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: true }, + handler: async (input, client) => { + const igUserId = await client.igUserId(); + const res = await client.request<{ data: RawHashtagMedia[] }>( + `/${input.hashtag_id}/recent_media`, + { + query: { + user_id: igUserId, + fields: HASHTAG_MEDIA_FIELDS, + limit: input.limit, + }, + }, + ); + return { items: res.data ?? [] }; + }, + }, +]; diff --git a/.instagram-mcp.bak/src/tools/insights.ts b/.instagram-mcp.bak/src/tools/insights.ts new file mode 100644 index 00000000..996f8ea0 --- /dev/null +++ b/.instagram-mcp.bak/src/tools/insights.ts @@ -0,0 +1,224 @@ +import { z } from "zod"; +import type { AnyToolDefinition } from "../tool-registry.js"; + +interface RawMetric { + name: string; + period?: string; + values: Array<{ value: number | Record; end_time?: string }>; + title?: string; + description?: string; + total_value?: { value: number }; +} + +interface InsightsResponse { + data: RawMetric[]; +} + +const METRICS_BY_MEDIA_TYPE: Record = { + IMAGE: ["reach", "likes", "comments", "shares", "saved", "total_interactions"], + VIDEO: [ + "reach", + "likes", + "comments", + "shares", + "saved", + "total_interactions", + "views", + ], + REELS: [ + "reach", + "likes", + "comments", + "shares", + "saved", + "total_interactions", + "views", + "ig_reels_avg_watch_time", + "ig_reels_video_view_total_time", + ], + CAROUSEL_ALBUM: [ + "reach", + "likes", + "comments", + "shares", + "saved", + "total_interactions", + ], + STORY: [ + "views", + "reach", + "replies", + "navigation", + "exits", + "profile_activity", + ], +}; + +function flattenMetrics(raw: RawMetric[]): Record> { + const out: Record> = {}; + for (const m of raw) { + if (m.total_value && typeof m.total_value.value === "number") { + out[m.name] = m.total_value.value; + } else if (m.values.length > 0) { + const first = m.values[0]; + if (first) out[m.name] = first.value; + } + } + return out; +} + +export const insightsTools: AnyToolDefinition[] = [ + { + name: "instagram_get_media_insights", + title: "Get media insights", + description: + "Per-post analytics for one of your posts. Auto-selects the correct metric set for the media type (IMAGE/VIDEO/REELS/CAROUSEL). For Reels you get views, average watch time, and total view time. For all post types you get reach, likes, comments, shares, saved, and total_interactions.", + inputShape: { media_id: z.string() }, + outputShape: { + media_id: z.string(), + media_type: z.string(), + metrics: z.record(z.unknown()), + }, + annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: true }, + handler: async (input, client) => { + // First fetch the media_type so we request the right metric set. + const meta = await client.request<{ media_type?: string; media_product_type?: string }>( + `/${input.media_id}`, + { query: { fields: "media_type,media_product_type" } }, + ); + const mediaType = + meta.media_product_type === "REELS" + ? "REELS" + : meta.media_type ?? "IMAGE"; + const metrics = METRICS_BY_MEDIA_TYPE[mediaType] ?? METRICS_BY_MEDIA_TYPE.IMAGE!; + const res = await client.request(`/${input.media_id}/insights`, { + query: { metric: metrics.join(",") }, + }); + return { + media_id: input.media_id, + media_type: mediaType, + metrics: flattenMetrics(res.data), + }; + }, + }, + { + name: "instagram_get_story_insights", + title: "Get Story insights", + description: + "Analytics for a Story post. Story insights expire 24 hours after the story does, so call this within ~24h of publishing. Returns views, reach, replies, navigation, exits, and profile_activity.", + inputShape: { story_media_id: z.string() }, + outputShape: { + story_id: z.string(), + metrics: z.record(z.unknown()), + }, + annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: true }, + handler: async (input, client) => { + const res = await client.request( + `/${input.story_media_id}/insights`, + { query: { metric: METRICS_BY_MEDIA_TYPE.STORY!.join(",") } }, + ); + return { + story_id: input.story_media_id, + metrics: flattenMetrics(res.data), + }; + }, + }, + { + name: "instagram_get_account_insights", + title: "Get account insights", + description: + "Account-level analytics over a date range (max 30 days). Default metrics include reach, profile_views, accounts_engaged, total_interactions, follows_and_unfollows, website_clicks, and profile_links_taps. Pass `metrics` to override.", + inputShape: { + since: z.string().describe("Start date (YYYY-MM-DD or unix epoch seconds)."), + until: z.string().describe("End date, ≤30 days after `since`."), + metrics: z + .array(z.string()) + .optional() + .describe("Override default metric set."), + }, + outputShape: { + since: z.string(), + until: z.string(), + metrics: z.record(z.unknown()), + }, + annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: true }, + handler: async (input, client) => { + const metrics = + input.metrics ?? + [ + "reach", + "profile_views", + "accounts_engaged", + "total_interactions", + "follows_and_unfollows", + "website_clicks", + "profile_links_taps", + ]; + const igUserId = await client.igUserId(); + const res = await client.request( + `/${igUserId}/insights`, + { + query: { + metric: metrics.join(","), + period: "day", + metric_type: "total", + since: input.since, + until: input.until, + }, + }, + ); + return { + since: input.since, + until: input.until, + metrics: flattenMetrics(res.data), + }; + }, + }, + { + name: "instagram_get_audience_insights", + title: "Get audience demographics", + description: + "Audience demographic breakdowns (city, country, age, gender, age_gender). Requires the account to have at least 100 followers. Returns a mapping from breakdown value to follower count.", + inputShape: { + breakdown: z.enum(["city", "country", "age", "gender", "age_gender"]), + timeframe: z + .enum(["this_week", "this_month", "prev_month"]) + .default("this_month"), + }, + outputShape: { + breakdown: z.string(), + timeframe: z.string(), + values: z.record(z.number()), + }, + annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: true }, + handler: async (input, client) => { + const igUserId = await client.igUserId(); + const res = await client.request( + `/${igUserId}/insights`, + { + query: { + metric: "follower_demographics", + period: "lifetime", + timeframe: input.timeframe, + breakdown: input.breakdown, + metric_type: "total", + }, + }, + ); + const values: Record = {}; + for (const m of res.data) { + const v = m.values[0]?.value; + if (typeof v === "object" && v !== null) { + for (const [k, count] of Object.entries(v as Record)) { + values[k] = count; + } + } + } + return { + breakdown: input.breakdown, + timeframe: input.timeframe, + values, + }; + }, + }, +]; diff --git a/.instagram-mcp.bak/src/tools/media.ts b/.instagram-mcp.bak/src/tools/media.ts new file mode 100644 index 00000000..6252530d --- /dev/null +++ b/.instagram-mcp.bak/src/tools/media.ts @@ -0,0 +1,153 @@ +import { z } from "zod"; +import type { AnyToolDefinition } from "../tool-registry.js"; + +const MEDIA_FIELDS = [ + "id", + "media_type", + "media_url", + "permalink", + "caption", + "timestamp", + "thumbnail_url", + "like_count", + "comments_count", + "is_comment_enabled", + "owner", +].join(","); + +const MediaItem = { + id: z.string(), + media_type: z.string().optional(), + media_url: z.string().optional(), + permalink: z.string().optional(), + caption: z.string().optional(), + timestamp: z.string().optional(), + thumbnail_url: z.string().optional(), + like_count: z.number().int().optional(), + comments_count: z.number().int().optional(), + is_comment_enabled: z.boolean().optional(), +}; + +interface RawMedia { + id: string; + media_type?: string; + media_url?: string; + permalink?: string; + caption?: string; + timestamp?: string; + thumbnail_url?: string; + like_count?: number; + comments_count?: number; + is_comment_enabled?: boolean; +} + +export const mediaTools: AnyToolDefinition[] = [ + { + name: "instagram_get_media", + title: "Get media", + description: + "Get full details for one of your posts, including like_count, comments_count, caption, permalink, and timestamp.", + inputShape: { media_id: z.string() }, + outputShape: MediaItem, + annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: true }, + handler: async (input, client) => { + const res = await client.request(`/${input.media_id}`, { + query: { fields: MEDIA_FIELDS }, + }); + return { + id: res.id, + media_type: res.media_type, + media_url: res.media_url, + permalink: res.permalink, + caption: res.caption, + timestamp: res.timestamp, + thumbnail_url: res.thumbnail_url, + like_count: res.like_count, + comments_count: res.comments_count, + is_comment_enabled: res.is_comment_enabled, + }; + }, + }, + { + name: "instagram_list_recent_media", + title: "List recent media", + description: + "List the account's recent posts in reverse chronological order. Pass `after_cursor` from a previous response to paginate.", + inputShape: { + limit: z.number().int().min(1).max(100).default(25), + after_cursor: z.string().optional(), + }, + outputShape: { + items: z.array(z.object(MediaItem)), + next_cursor: z.string().optional(), + }, + annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: true }, + handler: async (input, client) => { + const query: Record = { + fields: MEDIA_FIELDS, + limit: input.limit, + }; + if (input.after_cursor) query.after = input.after_cursor; + const igUserId = await client.igUserId(); + const { items, nextCursor } = await client.paginate( + `/${igUserId}/media`, + { query, maxItems: input.limit }, + ); + return { + items: items.map((r) => ({ + id: r.id, + media_type: r.media_type, + media_url: r.media_url, + permalink: r.permalink, + caption: r.caption, + timestamp: r.timestamp, + thumbnail_url: r.thumbnail_url, + like_count: r.like_count, + comments_count: r.comments_count, + is_comment_enabled: r.is_comment_enabled, + })), + next_cursor: nextCursor, + }; + }, + }, + { + name: "instagram_delete_media", + title: "Delete media", + description: + "Permanently delete one of your posts. This is irreversible. The agent should confirm with the user before calling this.", + inputShape: { media_id: z.string() }, + outputShape: { + deleted: z.boolean(), + media_id: z.string(), + }, + annotations: { destructiveHint: true, openWorldHint: true }, + requiredFeature: "publishing", + handler: async (input, client) => { + await client.request(`/${input.media_id}`, { method: "DELETE" }); + return { deleted: true, media_id: input.media_id }; + }, + }, + { + name: "instagram_toggle_comments", + title: "Toggle comments on a post", + description: + "Enable or disable comments on one of your posts. Set `enabled: false` to disable comments, `enabled: true` to re-enable.", + inputShape: { + media_id: z.string(), + enabled: z.boolean(), + }, + outputShape: { + media_id: z.string(), + comments_enabled: z.boolean(), + }, + annotations: { destructiveHint: true, openWorldHint: true }, + requiredFeature: "commentModeration", + handler: async (input, client) => { + await client.request(`/${input.media_id}`, { + method: "POST", + query: { comment_enabled: input.enabled }, + }); + return { media_id: input.media_id, comments_enabled: input.enabled }; + }, + }, +]; diff --git a/.instagram-mcp.bak/src/tools/mentions.ts b/.instagram-mcp.bak/src/tools/mentions.ts new file mode 100644 index 00000000..fbbe672e --- /dev/null +++ b/.instagram-mcp.bak/src/tools/mentions.ts @@ -0,0 +1,103 @@ +import { z } from "zod"; +import type { AnyToolDefinition } from "../tool-registry.js"; + +export const mentionTools: AnyToolDefinition[] = [ + { + name: "instagram_get_mentioned_comment", + title: "Read a comment that mentioned the account", + description: + "Read a comment that @-mentioned your account. In production these comment ids arrive via the `mentions` webhook field. For testing/manual use, pass the comment id directly.", + inputShape: { comment_id: z.string() }, + outputShape: { + id: z.string(), + text: z.string().optional(), + username: z.string().optional(), + timestamp: z.string().optional(), + media_id: z.string().optional(), + }, + annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: true }, + handler: async (input, client) => { + const igUserId = await client.igUserId(); + const res = await client.request<{ + id: string; + text?: string; + username?: string; + timestamp?: string; + media?: { id?: string }; + }>(`/${igUserId}`, { + query: { + fields: `mentioned_comment.comment_id(${input.comment_id}){id,text,username,timestamp,media}`, + }, + }); + // The Graph API nests under `mentioned_comment` on the user object. + const wrapper = res as unknown as { + mentioned_comment?: { + id?: string; + text?: string; + username?: string; + timestamp?: string; + media?: { id?: string }; + }; + }; + const mc = wrapper.mentioned_comment; + if (!mc?.id) { + throw new Error(`No mentioned comment found for id ${input.comment_id}.`); + } + return { + id: mc.id, + text: mc.text, + username: mc.username, + timestamp: mc.timestamp, + media_id: mc.media?.id, + }; + }, + }, + { + name: "instagram_get_mentioned_media", + title: "Read a media post that tagged the account", + description: + "Read a public post that @-tagged your account. In production these media ids arrive via the `mentions` webhook field. For testing/manual use, pass the media id directly.", + inputShape: { media_id: z.string() }, + outputShape: { + id: z.string(), + caption: z.string().optional(), + media_type: z.string().optional(), + media_url: z.string().optional(), + permalink: z.string().optional(), + timestamp: z.string().optional(), + owner_username: z.string().optional(), + }, + annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: true }, + handler: async (input, client) => { + const fields = + "id,caption,media_type,media_url,permalink,timestamp,owner{username}"; + const igUserId = await client.igUserId(); + const res = await client.request<{ + mentioned_media?: { + id?: string; + caption?: string; + media_type?: string; + media_url?: string; + permalink?: string; + timestamp?: string; + owner?: { username?: string }; + }; + }>(`/${igUserId}`, { + query: { fields: `mentioned_media.media_id(${input.media_id}){${fields}}` }, + }); + const mm = res.mentioned_media; + if (!mm?.id) { + throw new Error(`No mentioned media found for id ${input.media_id}.`); + } + return { + id: mm.id, + caption: mm.caption, + media_type: mm.media_type, + media_url: mm.media_url, + permalink: mm.permalink, + timestamp: mm.timestamp, + owner_username: mm.owner?.username, + }; + }, + }, +]; diff --git a/.instagram-mcp.bak/src/tools/messages.ts b/.instagram-mcp.bak/src/tools/messages.ts new file mode 100644 index 00000000..b5afc920 --- /dev/null +++ b/.instagram-mcp.bak/src/tools/messages.ts @@ -0,0 +1,161 @@ +import { z } from "zod"; +import type { AnyToolDefinition } from "../tool-registry.js"; + +interface RawConversation { + id: string; + updated_time?: string; + unread_count?: number; + message_count?: number; + participants?: { data?: Array<{ id?: string; username?: string }> }; +} + +interface RawMessage { + id: string; + created_time?: string; + message?: string; + from?: { id?: string; username?: string }; + to?: { data?: Array<{ id?: string; username?: string }> }; +} + +const ConversationItem = { + conversation_id: z.string(), + participant_username: z.string().optional(), + participant_id: z.string().optional(), + updated_at: z.string().optional(), + message_count: z.number().int().optional(), + unread_count: z.number().int().optional(), +}; + +const MessageItem = { + message_id: z.string(), + from_id: z.string().optional(), + from_username: z.string().optional(), + to_id: z.string().optional(), + created_time: z.string().optional(), + message: z.string().optional(), +}; + +export const messageTools: AnyToolDefinition[] = [ + { + name: "instagram_list_conversations", + title: "List DM conversations", + description: + "List recent direct-message conversations on the account. Requires the instagram_manage_messages permission. Returns conversation_id values you can pass to instagram_list_messages.", + inputShape: { + limit: z.number().int().min(1).max(50).default(25), + after_cursor: z.string().optional(), + }, + outputShape: { + items: z.array(z.object(ConversationItem)), + next_cursor: z.string().optional(), + }, + annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: true }, + requiredFeature: "messaging", + handler: async (input, client) => { + const query: Record = { + platform: "instagram", + fields: "id,updated_time,unread_count,message_count,participants", + limit: input.limit, + }; + if (input.after_cursor) query.after = input.after_cursor; + const igUserId = await client.igUserId(); + const { items, nextCursor } = await client.paginate( + `/${igUserId}/conversations`, + { query, maxItems: input.limit }, + ); + return { + items: items.map((c) => { + const other = (c.participants?.data ?? []).find( + (p) => p.id !== igUserId, + ); + return { + conversation_id: c.id, + participant_username: other?.username, + participant_id: other?.id, + updated_at: c.updated_time, + message_count: c.message_count, + unread_count: c.unread_count, + }; + }), + next_cursor: nextCursor, + }; + }, + }, + { + name: "instagram_list_messages", + title: "List messages in a conversation", + description: "List messages in a DM conversation, newest first.", + inputShape: { + conversation_id: z.string(), + limit: z.number().int().min(1).max(50).default(25), + after_cursor: z.string().optional(), + }, + outputShape: { + items: z.array(z.object(MessageItem)), + next_cursor: z.string().optional(), + }, + annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: true }, + requiredFeature: "messaging", + handler: async (input, client) => { + const query: Record = { + fields: "id,created_time,message,from,to", + limit: input.limit, + }; + if (input.after_cursor) query.after = input.after_cursor; + const { items, nextCursor } = await client.paginate( + `/${input.conversation_id}/messages`, + { query, maxItems: input.limit }, + ); + return { + items: items.map((m) => ({ + message_id: m.id, + from_id: m.from?.id, + from_username: m.from?.username, + to_id: m.to?.data?.[0]?.id, + created_time: m.created_time, + message: m.message, + })), + next_cursor: nextCursor, + }; + }, + }, + { + name: "instagram_send_message", + title: "Send a DM", + description: + "Send a direct message to a user. CRITICAL constraint per Meta Messenger Platform: free-form messages are only allowed within 24 hours of the recipient's last message to your account. Outside that window you must pass a message_tag (currently only HUMAN_AGENT is supported and itself requires special permission). Recipients are addressed by their Instagram-scoped user id (igsid), which you obtain from instagram_list_messages.", + inputShape: { + recipient_id: z.string().describe("Instagram-scoped user id (IGSID) of the recipient."), + text: z.string().min(1).max(1000), + message_tag: z + .enum(["HUMAN_AGENT"]) + .optional() + .describe("Set only when messaging outside the 24h window."), + }, + outputShape: { + message_id: z.string(), + recipient_id: z.string(), + created_at: z.string(), + }, + annotations: { destructiveHint: true, openWorldHint: true }, + requiredFeature: "messaging", + handler: async (input, client) => { + const body: Record = { + recipient: { id: input.recipient_id }, + message: { text: input.text }, + messaging_type: input.message_tag ? "MESSAGE_TAG" : "RESPONSE", + }; + if (input.message_tag) body.tag = input.message_tag; + const igUserId = await client.igUserId(); + const res = await client.request<{ message_id?: string; recipient_id?: string }>( + `/${igUserId}/messages`, + { method: "POST", body }, + ); + return { + message_id: res.message_id ?? "", + recipient_id: res.recipient_id ?? input.recipient_id, + created_at: new Date().toISOString(), + }; + }, + }, +]; diff --git a/.instagram-mcp.bak/src/tools/publish.ts b/.instagram-mcp.bak/src/tools/publish.ts new file mode 100644 index 00000000..4c0556bd --- /dev/null +++ b/.instagram-mcp.bak/src/tools/publish.ts @@ -0,0 +1,252 @@ +import { z } from "zod"; +import { pollUntilFinished } from "../containers.js"; +import type { GraphClient } from "../graph-client.js"; +import type { AnyToolDefinition } from "../tool-registry.js"; + +const PublishResult = { + media_id: z.string(), + permalink: z.string().optional(), + published_at: z.string(), +}; + +interface ContainerCreationParams { + image_url?: string; + video_url?: string; + media_type?: "IMAGE" | "VIDEO" | "REELS" | "STORIES" | "CAROUSEL"; + caption?: string; + is_carousel_item?: boolean; + children?: string; + share_to_feed?: boolean; + cover_url?: string; + location_id?: string; + user_tags?: string; + alt_text?: string; + thumb_offset?: number; +} + +async function createContainer( + client: GraphClient, + params: ContainerCreationParams, +): Promise { + const query: Record = {}; + for (const [k, v] of Object.entries(params)) { + if (v !== undefined && v !== null) query[k] = v as string | number | boolean; + } + const igUserId = await client.igUserId(); + const res = await client.request<{ id: string }>(`/${igUserId}/media`, { + method: "POST", + query, + }); + return res.id; +} + +async function publishContainer( + client: GraphClient, + containerId: string, +): Promise<{ id: string }> { + const igUserId = await client.igUserId(); + return client.request<{ id: string }>(`/${igUserId}/media_publish`, { + method: "POST", + query: { creation_id: containerId }, + }); +} + +async function fetchPermalink( + client: GraphClient, + mediaId: string, +): Promise { + try { + const res = await client.request<{ permalink?: string }>(`/${mediaId}`, { + query: { fields: "permalink" }, + }); + return res.permalink; + } catch { + return undefined; + } +} + +export const publishTools: AnyToolDefinition[] = [ + { + name: "instagram_publish_image", + title: "Publish image post", + description: + "Publish a single image post to Instagram in one call (creates container, polls until processed, then publishes). The image_url must be publicly accessible HTTPS (JPEG/PNG, ≤8MB, aspect ratio 4:5 to 1.91:1). Caption supports up to 2,200 chars, 30 hashtags, and 20 @mentions. Returns the resulting media_id and permalink.", + inputShape: { + image_url: z.string().url().describe("Public HTTPS URL to a JPEG or PNG (≤8MB)."), + caption: z.string().max(2200).optional().describe("Post caption."), + location_id: z.string().optional().describe("Optional Facebook Page location id."), + alt_text: z.string().max(100).optional().describe("Accessibility alt text."), + }, + outputShape: PublishResult, + annotations: { destructiveHint: true, openWorldHint: true }, + requiredFeature: "publishing", + handler: async (input, client) => { + const containerId = await createContainer(client, { + image_url: input.image_url, + caption: input.caption, + location_id: input.location_id, + alt_text: input.alt_text, + }); + await pollUntilFinished(client, containerId, client.config.IG_IMAGE_TIMEOUT_MS); + const { id } = await publishContainer(client, containerId); + const permalink = await fetchPermalink(client, id); + return { + media_id: id, + permalink, + published_at: new Date().toISOString(), + }; + }, + }, + { + name: "instagram_publish_carousel", + title: "Publish carousel post", + description: + "Publish a 2–10 item carousel (mixed images and videos). Each item must be a publicly accessible HTTPS URL. All children are created in parallel, then assembled into a CAROUSEL container and published. Caption rules same as instagram_publish_image.", + inputShape: { + items: z + .array( + z.object({ + media_url: z.string().url(), + media_type: z.enum(["IMAGE", "VIDEO"]), + }), + ) + .min(2) + .max(10) + .describe("Between 2 and 10 children."), + caption: z.string().max(2200).optional(), + }, + outputShape: { + ...PublishResult, + child_media_ids: z.array(z.string()), + }, + annotations: { destructiveHint: true, openWorldHint: true }, + requiredFeature: "publishing", + handler: async (input, client) => { + const childIds = await Promise.all( + input.items.map(async (item: { media_url: string; media_type: "IMAGE" | "VIDEO" }) => + createContainer(client, { + ...(item.media_type === "VIDEO" + ? { video_url: item.media_url, media_type: "VIDEO" as const } + : { image_url: item.media_url }), + is_carousel_item: true, + }), + ), + ); + // Poll each child container. + await Promise.all( + childIds.map((id) => + pollUntilFinished(client, id, client.config.IG_IMAGE_TIMEOUT_MS), + ), + ); + const parentId = await createContainer(client, { + media_type: "CAROUSEL", + caption: input.caption, + children: childIds.join(","), + }); + await pollUntilFinished(client, parentId, client.config.IG_IMAGE_TIMEOUT_MS); + const { id } = await publishContainer(client, parentId); + const permalink = await fetchPermalink(client, id); + return { + media_id: id, + permalink, + published_at: new Date().toISOString(), + child_media_ids: childIds, + }; + }, + }, + { + name: "instagram_publish_reel", + title: "Publish Reel", + description: + "Publish a Reel. video_url must be MP4 (≤100MB, ≤90s, H.264 video + AAC audio) over HTTPS. Reels take longer to process — this tool polls for up to 5 minutes by default (configurable via IG_REEL_TIMEOUT_MS). share_to_feed=true (default) makes the Reel appear in the main feed as well as Reels tab.", + inputShape: { + video_url: z.string().url().describe("Public HTTPS MP4 URL."), + caption: z.string().max(2200).optional(), + cover_url: z.string().url().optional().describe("Optional cover image URL."), + share_to_feed: z.boolean().default(true), + thumb_offset: z + .number() + .int() + .nonnegative() + .optional() + .describe("Milliseconds into the video to use as the thumbnail."), + }, + outputShape: PublishResult, + annotations: { destructiveHint: true, openWorldHint: true }, + requiredFeature: "publishing", + handler: async (input, client) => { + const containerId = await createContainer(client, { + media_type: "REELS", + video_url: input.video_url, + caption: input.caption, + cover_url: input.cover_url, + share_to_feed: input.share_to_feed, + thumb_offset: input.thumb_offset, + }); + await pollUntilFinished(client, containerId, client.config.IG_REEL_TIMEOUT_MS); + const { id } = await publishContainer(client, containerId); + const permalink = await fetchPermalink(client, id); + return { + media_id: id, + permalink, + published_at: new Date().toISOString(), + }; + }, + }, + { + name: "instagram_publish_story_image", + title: "Publish image Story", + description: + "Publish an image to the account's Story. Stories expire 24 hours after publishing. image_url requirements same as a regular image post.", + inputShape: { + image_url: z.string().url(), + }, + outputShape: { + media_id: z.string(), + expires_at: z.string(), + }, + annotations: { destructiveHint: true, openWorldHint: true }, + requiredFeature: "publishing", + handler: async (input, client) => { + const containerId = await createContainer(client, { + media_type: "STORIES", + image_url: input.image_url, + }); + await pollUntilFinished(client, containerId, client.config.IG_IMAGE_TIMEOUT_MS); + const { id } = await publishContainer(client, containerId); + const now = Date.now(); + return { + media_id: id, + expires_at: new Date(now + 24 * 60 * 60 * 1000).toISOString(), + }; + }, + }, + { + name: "instagram_publish_story_video", + title: "Publish video Story", + description: + "Publish a video to the account's Story. Videos must be ≤60s. Stories expire 24 hours after publishing.", + inputShape: { + video_url: z.string().url(), + }, + outputShape: { + media_id: z.string(), + expires_at: z.string(), + }, + annotations: { destructiveHint: true, openWorldHint: true }, + requiredFeature: "publishing", + handler: async (input, client) => { + const containerId = await createContainer(client, { + media_type: "STORIES", + video_url: input.video_url, + }); + await pollUntilFinished(client, containerId, client.config.IG_REEL_TIMEOUT_MS); + const { id } = await publishContainer(client, containerId); + const now = Date.now(); + return { + media_id: id, + expires_at: new Date(now + 24 * 60 * 60 * 1000).toISOString(), + }; + }, + }, +]; diff --git a/.instagram-mcp.bak/tests/auth-state.test.ts b/.instagram-mcp.bak/tests/auth-state.test.ts new file mode 100644 index 00000000..e5f863e8 --- /dev/null +++ b/.instagram-mcp.bak/tests/auth-state.test.ts @@ -0,0 +1,129 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { AuthState } from "../src/auth-state.js"; +import type { StoredToken, TokenStore } from "../src/token-store.js"; + +function makeStore(initial: StoredToken | null = null): TokenStore { + let current = initial; + return { + load: vi.fn(async () => current), + save: vi.fn(async (t: StoredToken) => { + current = t; + }), + clear: vi.fn(async () => { + current = null; + }), + describe: () => "in-memory", + }; +} + +const DAY = 24 * 60 * 60 * 1000; +const FIXED_NOW = 1_700_000_000_000; + +function tokenExpiringInDays(days: number): StoredToken { + return { + access_token: `access_token_${days}d_old`, + user_id: "17841400000000000", + expires_at: FIXED_NOW + days * DAY, + obtained_at: FIXED_NOW - (60 - days) * DAY, + granted_scopes: ["instagram_business_basic"], + }; +} + +describe("AuthState", () => { + beforeEach(() => { + vi.useFakeTimers(); + vi.setSystemTime(FIXED_NOW); + }); + + afterEach(() => { + vi.useRealTimers(); + }); + + it("is unconnected when the store has nothing", async () => { + const state = await AuthState.create({ store: makeStore(null) }); + expect(state.isConnected()).toBe(false); + expect(await state.getToken()).toBeNull(); + }); + + it("loads an existing token on init", async () => { + const stored = tokenExpiringInDays(45); + const state = await AuthState.create({ store: makeStore(stored) }); + expect(state.isConnected()).toBe(true); + expect(await state.getUserId()).toBe(stored.user_id); + expect(await state.getToken()).toBe(stored.access_token); + }); + + it("does NOT refresh when expiry is > 7 days away", async () => { + const stored = tokenExpiringInDays(30); + const refreshFn = vi.fn(); + const state = await AuthState.create({ + store: makeStore(stored), + now: () => FIXED_NOW, + refresh: refreshFn, + }); + await state.refreshIfNeeded(); + expect(refreshFn).not.toHaveBeenCalled(); + }); + + it("auto-refreshes when expiry is <= 7 days away", async () => { + const stored = tokenExpiringInDays(5); + const store = makeStore(stored); + const refreshFn = vi.fn().mockResolvedValue({ + access_token: "refreshed_token", + expires_in: 60 * 24 * 60 * 60, // 60 days + }); + const state = await AuthState.create({ + store, + now: () => FIXED_NOW, + refresh: refreshFn, + }); + await state.refreshIfNeeded(); + expect(refreshFn).toHaveBeenCalledTimes(1); + expect(refreshFn).toHaveBeenCalledWith(stored.access_token); + expect(await state.getToken()).toBe("refreshed_token"); + expect(store.save).toHaveBeenCalled(); + }); + + it("getToken auto-refreshes inline", async () => { + const stored = tokenExpiringInDays(3); + const refreshFn = vi.fn().mockResolvedValue({ + access_token: "auto_refreshed", + expires_in: 60 * 24 * 60 * 60, + }); + const state = await AuthState.create({ + store: makeStore(stored), + now: () => FIXED_NOW, + refresh: refreshFn, + }); + expect(await state.getToken()).toBe("auto_refreshed"); + expect(refreshFn).toHaveBeenCalledTimes(1); + }); + + it("keeps the old token if refresh fails", async () => { + const stored = tokenExpiringInDays(2); + const refreshFn = vi.fn().mockRejectedValue(new Error("Meta is down")); + const state = await AuthState.create({ + store: makeStore(stored), + now: () => FIXED_NOW, + refresh: refreshFn, + }); + expect(await state.getToken()).toBe(stored.access_token); + expect(refreshFn).toHaveBeenCalled(); + }); + + it("disconnect clears the store and forgets the token", async () => { + const stored = tokenExpiringInDays(30); + const store = makeStore(stored); + const state = await AuthState.create({ store }); + await state.disconnect(); + expect(state.isConnected()).toBe(false); + expect(store.clear).toHaveBeenCalled(); + expect(await state.getToken()).toBeNull(); + }); + + it("getGrantedScopes returns whatever the stored token recorded", async () => { + const stored = tokenExpiringInDays(30); + const state = await AuthState.create({ store: makeStore(stored) }); + expect(state.getGrantedScopes()).toEqual(stored.granted_scopes); + }); +}); diff --git a/.instagram-mcp.bak/tests/feature-flags.test.ts b/.instagram-mcp.bak/tests/feature-flags.test.ts new file mode 100644 index 00000000..c3eb9fd5 --- /dev/null +++ b/.instagram-mcp.bak/tests/feature-flags.test.ts @@ -0,0 +1,102 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { filterEnabledTools } from "../src/tool-registry.js"; +import { accountTools } from "../src/tools/account.js"; +import { authTools } from "../src/tools/auth.js"; +import { commentTools } from "../src/tools/comments.js"; +import { containerTools } from "../src/tools/containers.js"; +import { discoveryTools } from "../src/tools/discovery.js"; +import { hashtagTools } from "../src/tools/hashtags.js"; +import { insightsTools } from "../src/tools/insights.js"; +import { mediaTools } from "../src/tools/media.js"; +import { mentionTools } from "../src/tools/mentions.js"; +import { messageTools } from "../src/tools/messages.js"; +import { publishTools } from "../src/tools/publish.js"; + +const allTools = [ + ...authTools, + ...accountTools, + ...mediaTools, + ...insightsTools, + ...commentTools, + ...hashtagTools, + ...discoveryTools, + ...mentionTools, + ...publishTools, + ...containerTools, + ...messageTools, +]; + +const originalEnv = { ...process.env }; + +describe("feature flag → tool registration", () => { + beforeEach(() => { + vi.stubEnv("INSTAGRAM_MCP_ENABLE_PUBLISHING", ""); + vi.stubEnv("INSTAGRAM_MCP_ENABLE_COMMENTMODERATION", ""); + vi.stubEnv("INSTAGRAM_MCP_ENABLE_MESSAGING", ""); + }); + + afterEach(() => { + vi.unstubAllEnvs(); + process.env = { ...originalEnv }; + }); + + it("ships 18 tools with all flags off (v0.1.x baseline)", () => { + const enabled = filterEnabledTools(allTools); + expect(enabled).toHaveLength(18); + const names = enabled.map((t) => t.name).sort(); + expect(names).toEqual( + [ + "instagram_business_discovery", + "instagram_connect", + "instagram_get_account_insights", + "instagram_get_audience_insights", + "instagram_get_hashtag_recent_media", + "instagram_get_hashtag_top_media", + "instagram_get_media", + "instagram_get_media_insights", + "instagram_get_mentioned_comment", + "instagram_get_mentioned_media", + "instagram_get_story_insights", + "instagram_list_comment_replies", + "instagram_list_comments", + "instagram_list_recent_media", + "instagram_logout", + "instagram_search_hashtag", + "instagram_status", + "instagram_who_am_i", + ].sort(), + ); + }); + + it("adds 9 publishing tools when publishing enabled → 27", () => { + vi.stubEnv("INSTAGRAM_MCP_ENABLE_PUBLISHING", "true"); + const enabled = filterEnabledTools(allTools); + expect(enabled).toHaveLength(27); + }); + + it("adds 4 comment-moderation tools when only that flag is on → 22", () => { + vi.stubEnv("INSTAGRAM_MCP_ENABLE_COMMENTMODERATION", "true"); + const enabled = filterEnabledTools(allTools); + expect(enabled).toHaveLength(22); + }); + + it("adds 3 messaging tools when only that flag is on → 21", () => { + vi.stubEnv("INSTAGRAM_MCP_ENABLE_MESSAGING", "true"); + const enabled = filterEnabledTools(allTools); + expect(enabled).toHaveLength(21); + }); + + it("registers every tool when all flags on → 34", () => { + vi.stubEnv("INSTAGRAM_MCP_ENABLE_PUBLISHING", "true"); + vi.stubEnv("INSTAGRAM_MCP_ENABLE_COMMENTMODERATION", "true"); + vi.stubEnv("INSTAGRAM_MCP_ENABLE_MESSAGING", "true"); + const enabled = filterEnabledTools(allTools); + expect(enabled).toHaveLength(34); + }); + + it("does NOT enable a feature for arbitrary truthy strings", () => { + vi.stubEnv("INSTAGRAM_MCP_ENABLE_PUBLISHING", "1"); + const enabled = filterEnabledTools(allTools); + expect(enabled).toHaveLength(18); + }); +}); diff --git a/.instagram-mcp.bak/tests/npm-connect-fallback.test.ts b/.instagram-mcp.bak/tests/npm-connect-fallback.test.ts new file mode 100644 index 00000000..d2ed3c3b --- /dev/null +++ b/.instagram-mcp.bak/tests/npm-connect-fallback.test.ts @@ -0,0 +1,35 @@ +import { beforeEach, describe, expect, it, vi } from "vitest"; + +describe("shouldUseNpmOAuthFallback", () => { + beforeEach(() => { + vi.resetModules(); + delete process.env.INSTAGRAM_MCP_NO_NPX_FALLBACK; + delete process.env.INSTAGRAM_MCP_NPX_DELEGATE_CHILD; + delete process.env.INSTAGRAM_MCP_APP_ID; + delete process.env.INSTAGRAM_MCP_APP_SECRET; + }); + + it("is false when NO_NPX_FALLBACK=1", async () => { + process.env.INSTAGRAM_MCP_NO_NPX_FALLBACK = "1"; + const { shouldUseNpmOAuthFallback } = await import("../src/npm-connect-fallback.js"); + expect(shouldUseNpmOAuthFallback()).toBe(false); + }); + + it("is false for npx delegate child (prevents recursion)", async () => { + process.env.INSTAGRAM_MCP_NPX_DELEGATE_CHILD = "1"; + const { shouldUseNpmOAuthFallback } = await import("../src/npm-connect-fallback.js"); + expect(shouldUseNpmOAuthFallback()).toBe(false); + }); + + it("is false when app id+secret are set", async () => { + process.env.INSTAGRAM_MCP_APP_ID = "x"; + process.env.INSTAGRAM_MCP_APP_SECRET = "y"; + const { shouldUseNpmOAuthFallback } = await import("../src/npm-connect-fallback.js"); + expect(shouldUseNpmOAuthFallback()).toBe(false); + }); + + it("is true when no embedded creds and not a delegate child", async () => { + const { shouldUseNpmOAuthFallback } = await import("../src/npm-connect-fallback.js"); + expect(shouldUseNpmOAuthFallback()).toBe(true); + }); +}); diff --git a/.instagram-mcp.bak/tests/oauth-config.test.ts b/.instagram-mcp.bak/tests/oauth-config.test.ts new file mode 100644 index 00000000..0c794b26 --- /dev/null +++ b/.instagram-mcp.bak/tests/oauth-config.test.ts @@ -0,0 +1,30 @@ +import { afterEach, describe, expect, it, vi } from "vitest"; + +describe("hasInjectedCredentials", () => { + afterEach(() => { + vi.resetModules(); + delete process.env.INSTAGRAM_MCP_APP_ID; + delete process.env.INSTAGRAM_MCP_APP_SECRET; + }); + + it("is false for empty env strings", async () => { + process.env.INSTAGRAM_MCP_APP_ID = ""; + process.env.INSTAGRAM_MCP_APP_SECRET = ""; + const { hasInjectedCredentials } = await import("../src/oauth-config.js"); + expect(hasInjectedCredentials()).toBe(false); + }); + + it("is false for whitespace-only", async () => { + process.env.INSTAGRAM_MCP_APP_ID = " "; + process.env.INSTAGRAM_MCP_APP_SECRET = "\t"; + const { hasInjectedCredentials } = await import("../src/oauth-config.js"); + expect(hasInjectedCredentials()).toBe(false); + }); + + it("is true for non-placeholder id and secret", async () => { + process.env.INSTAGRAM_MCP_APP_ID = "real"; + process.env.INSTAGRAM_MCP_APP_SECRET = "secret"; + const { hasInjectedCredentials } = await import("../src/oauth-config.js"); + expect(hasInjectedCredentials()).toBe(true); + }); +}); diff --git a/.instagram-mcp.bak/tests/oauth.test.ts b/.instagram-mcp.bak/tests/oauth.test.ts new file mode 100644 index 00000000..c5a56837 --- /dev/null +++ b/.instagram-mcp.bak/tests/oauth.test.ts @@ -0,0 +1,221 @@ +import { + createServer, + type IncomingMessage, + type Server, +} from "node:http"; +import { createServer as createNetServer, type AddressInfo } from "node:net"; +import { URL } from "node:url"; +import { afterAll, afterEach, beforeAll, describe, expect, it, vi } from "vitest"; + +async function pickFreePort(): Promise { + return new Promise((resolve, reject) => { + const srv = createNetServer(); + srv.unref(); + srv.on("error", reject); + srv.listen(0, "127.0.0.1", () => { + const port = (srv.address() as AddressInfo).port; + srv.close(() => resolve(port)); + }); + }); +} + +// Stub the embedded Meta credentials BEFORE importing oauth.ts. +process.env.INSTAGRAM_MCP_APP_ID = "test_app_id"; +process.env.INSTAGRAM_MCP_APP_SECRET = "test_app_secret"; + +const { runOAuthFlow } = await import("../src/oauth.js"); + +/* -------------------------------------------------------------------------- */ +/* Fake Instagram token endpoints */ +/* -------------------------------------------------------------------------- */ + +interface FakeServer { + url: string; + shortTokenHits: number; + longTokenHits: number; + lastShortTokenForm?: URLSearchParams; + close(): Promise; +} + +// Bridge fetch calls targeting Instagram's real endpoints to the fake server. +let metaBaseUrl: string | null = null; +const originalFetch = globalThis.fetch; + +function installFetchBridge(): void { + globalThis.fetch = (async (input: RequestInfo | URL, init?: RequestInit) => { + const raw = + typeof input === "string" + ? input + : input instanceof URL + ? input.toString() + : input.url; + const rewritten = metaBaseUrl + ? raw + .replace("https://api.instagram.com", metaBaseUrl) + .replace("https://graph.instagram.com", metaBaseUrl) + : raw; + return originalFetch(rewritten, init); + }) as typeof fetch; +} + +async function startFakeMetaServer(): Promise { + const state: FakeServer = { + url: "", + shortTokenHits: 0, + longTokenHits: 0, + close: async () => { + await new Promise((r) => server.close(() => r())); + }, + }; + const server: Server = createServer(async (req, res) => { + const u = new URL(req.url ?? "/", "http://localhost"); + if (req.method === "POST" && u.pathname === "/oauth/access_token") { + const body = await readBody(req); + state.shortTokenHits += 1; + state.lastShortTokenForm = new URLSearchParams(body); + res.writeHead(200, { "Content-Type": "application/json" }); + res.end( + JSON.stringify({ + access_token: "short_lived_token", + user_id: "17841400000000099", + }), + ); + return; + } + if (req.method === "GET" && u.pathname === "/access_token") { + state.longTokenHits += 1; + res.writeHead(200, { "Content-Type": "application/json" }); + res.end( + JSON.stringify({ + access_token: "long_lived_token", + token_type: "bearer", + expires_in: 60 * 24 * 60 * 60, + }), + ); + return; + } + res.writeHead(404); + res.end("not found"); + }); + await new Promise((r) => server.listen(0, "127.0.0.1", () => r())); + const port = (server.address() as AddressInfo).port; + state.url = `http://127.0.0.1:${port}`; + return state; +} + +function readBody(req: IncomingMessage): Promise { + return new Promise((resolve) => { + let buf = ""; + req.on("data", (c) => (buf += c.toString())); + req.on("end", () => resolve(buf)); + }); +} + +/* -------------------------------------------------------------------------- */ +/* Tests */ +/* -------------------------------------------------------------------------- */ + +let meta: FakeServer; + +beforeAll(async () => { + meta = await startFakeMetaServer(); + metaBaseUrl = meta.url; + installFetchBridge(); +}); + +afterAll(async () => { + metaBaseUrl = null; + globalThis.fetch = originalFetch; + await meta.close(); +}); + +afterEach(() => { + meta.shortTokenHits = 0; + meta.longTokenHits = 0; + meta.lastShortTokenForm = undefined; +}); + +describe("runOAuthFlow", () => { + it("completes the full short→long token dance", async () => { + const port = await pickFreePort(); + // Pretend the browser hit our local /callback the instant we ask it to. + const openBrowser = async (authUrl: string): Promise => { + const parsed = new URL(authUrl); + expect(parsed.origin + parsed.pathname).toBe( + "https://www.instagram.com/oauth/authorize", + ); + expect(parsed.searchParams.get("client_id")).toBe("test_app_id"); + expect(parsed.searchParams.get("response_type")).toBe("code"); + const state = parsed.searchParams.get("state"); + const redirectUri = parsed.searchParams.get("redirect_uri"); + expect(state).toBeTruthy(); + expect(redirectUri).toBeTruthy(); + // Hit the callback URL programmatically. + const cbUrl = new URL(redirectUri!); + cbUrl.searchParams.set("code", "FAKE_AUTH_CODE"); + cbUrl.searchParams.set("state", state!); + const res = await originalFetch(cbUrl); + expect(res.status).toBe(200); + }; + + const token = await runOAuthFlow({ + openBrowser, + getPort: async () => port, + timeoutMs: 5_000, + }); + + expect(token.access_token).toBe("long_lived_token"); + expect(token.user_id).toBe("17841400000000099"); + expect(token.granted_scopes).toContain("instagram_business_basic"); + expect(token.expires_at).toBeGreaterThan(Date.now()); + expect(token.obtained_at).toBeGreaterThan(0); + + expect(meta.shortTokenHits).toBe(1); + expect(meta.longTokenHits).toBe(1); + + expect(meta.lastShortTokenForm?.get("client_id")).toBe("test_app_id"); + expect(meta.lastShortTokenForm?.get("client_secret")).toBe("test_app_secret"); + expect(meta.lastShortTokenForm?.get("grant_type")).toBe("authorization_code"); + expect(meta.lastShortTokenForm?.get("code")).toBe("FAKE_AUTH_CODE"); + }); + + it("rejects state mismatch (CSRF guard)", async () => { + const port = await pickFreePort(); + const openBrowser = async (authUrl: string): Promise => { + const parsed = new URL(authUrl); + const redirectUri = parsed.searchParams.get("redirect_uri")!; + const cbUrl = new URL(redirectUri); + cbUrl.searchParams.set("code", "FAKE_AUTH_CODE"); + cbUrl.searchParams.set("state", "BOGUS_STATE"); + await originalFetch(cbUrl).catch(() => undefined); + }; + await expect( + runOAuthFlow({ openBrowser, getPort: async () => port, timeoutMs: 3_000 }), + ).rejects.toThrow(/state mismatch/i); + }); + + it("rejects an error param from Instagram", async () => { + const port = await pickFreePort(); + const openBrowser = async (authUrl: string): Promise => { + const parsed = new URL(authUrl); + const redirectUri = parsed.searchParams.get("redirect_uri")!; + const cbUrl = new URL(redirectUri); + cbUrl.searchParams.set("error", "access_denied"); + cbUrl.searchParams.set("error_description", "user said no"); + await originalFetch(cbUrl).catch(() => undefined); + }; + await expect( + runOAuthFlow({ openBrowser, getPort: async () => port, timeoutMs: 3_000 }), + ).rejects.toThrow(/user said no/); + }); + + it("times out cleanly if the browser never returns", async () => { + const port = await pickFreePort(); + const openBrowser = vi.fn(async () => { + /* never hit the callback */ + }); + await expect( + runOAuthFlow({ openBrowser, getPort: async () => port, timeoutMs: 150 }), + ).rejects.toThrow(/Did not receive Instagram OAuth callback/); + }); +}); diff --git a/.instagram-mcp.bak/tests/token-store.test.ts b/.instagram-mcp.bak/tests/token-store.test.ts new file mode 100644 index 00000000..fd9f8c81 --- /dev/null +++ b/.instagram-mcp.bak/tests/token-store.test.ts @@ -0,0 +1,98 @@ +import { mkdtemp, rm } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { afterEach, beforeEach, describe, expect, it } from "vitest"; +import { + EncryptedFileTokenStore, + type StoredToken, + createTokenStore, +} from "../src/token-store.js"; + +function fixedKey(): Promise { + return Promise.resolve(Buffer.alloc(32, 7)); +} + +function sampleToken(): StoredToken { + return { + access_token: "IGQVJ.fake.long.lived.token", + user_id: "17841400000000000", + expires_at: Date.now() + 60 * 24 * 60 * 60 * 1000, + obtained_at: Date.now(), + granted_scopes: [ + "instagram_business_basic", + "instagram_business_manage_insights", + "instagram_business_manage_comments", + ], + }; +} + +describe("EncryptedFileTokenStore", () => { + let dir: string; + let path: string; + + beforeEach(async () => { + dir = await mkdtemp(join(tmpdir(), "imb-tokenstore-")); + path = join(dir, "token.json"); + }); + + afterEach(async () => { + await rm(dir, { recursive: true, force: true }); + }); + + it("round-trips a stored token", async () => { + const store = new EncryptedFileTokenStore(path, fixedKey); + const token = sampleToken(); + await store.save(token); + const loaded = await store.load(); + expect(loaded).toEqual(token); + }); + + it("returns null when no file exists", async () => { + const store = new EncryptedFileTokenStore(path, fixedKey); + expect(await store.load()).toBeNull(); + }); + + it("clear() removes the file", async () => { + const store = new EncryptedFileTokenStore(path, fixedKey); + await store.save(sampleToken()); + await store.clear(); + expect(await store.load()).toBeNull(); + }); + + it("returns null when decrypted with wrong key", async () => { + const store = new EncryptedFileTokenStore(path, fixedKey); + await store.save(sampleToken()); + const otherKey = () => Promise.resolve(Buffer.alloc(32, 9)); + const wrong = new EncryptedFileTokenStore(path, otherKey); + expect(await wrong.load()).toBeNull(); + }); + + it("describe() points at the on-disk path", () => { + const store = new EncryptedFileTokenStore(path, fixedKey); + expect(store.describe()).toContain(path); + }); +}); + +describe("createTokenStore factory", () => { + let dir: string; + + beforeEach(async () => { + dir = await mkdtemp(join(tmpdir(), "imb-factory-")); + }); + + afterEach(async () => { + await rm(dir, { recursive: true, force: true }); + }); + + it("falls back to encrypted file when forceFile=true", async () => { + const store = await createTokenStore({ + forceFile: true, + filePath: join(dir, "token.json"), + keyProvider: fixedKey, + }); + expect(store.describe()).toContain("encrypted file"); + await store.save(sampleToken()); + const loaded = await store.load(); + expect(loaded?.user_id).toBe("17841400000000000"); + }); +}); diff --git a/.instagram-mcp.bak/tsconfig.json b/.instagram-mcp.bak/tsconfig.json new file mode 100644 index 00000000..ce816349 --- /dev/null +++ b/.instagram-mcp.bak/tsconfig.json @@ -0,0 +1,19 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "outDir": "./dist", + "rootDir": "./src", + "strict": true, + "noUncheckedIndexedAccess": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "declaration": true, + "sourceMap": true + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist"] +} diff --git a/.instagram-mcp.bak/vitest.config.ts b/.instagram-mcp.bak/vitest.config.ts new file mode 100644 index 00000000..571a1edb --- /dev/null +++ b/.instagram-mcp.bak/vitest.config.ts @@ -0,0 +1,10 @@ +import { defineConfig } from "vitest/config"; + +export default defineConfig({ + test: { + environment: "node", + include: ["tests/**/*.test.ts"], + testTimeout: 15_000, + hookTimeout: 15_000, + }, +}); diff --git a/GETTING_STARTED.md b/GETTING_STARTED.md index 70227318..1a871dc6 100644 --- a/GETTING_STARTED.md +++ b/GETTING_STARTED.md @@ -163,6 +163,47 @@ Your agents can now use Google Calendar, Gmail, Drive, etc. through MCP tools. --- +## Instagram (`instagram-mcp-buddy`) from this repo (optional) + +The published npm package is meant to ship a **maintainer-injected** Meta app so end users need no `.env`. A **git checkout** uses placeholders until you either set env vars or inject at build time. + +1. [Create a Meta developer app](https://developers.facebook.com/) with Instagram API (Instagram Login), and note the app id and secret. +2. Open a terminal and `cd` to your **clone of this repository** (the folder that contains `backend/`, `frontend/`, and **`instagram-mcp/`**). + It is **not** `~/instagram-mcp` unless you created that yourself. Example: + +```bash +cd /path/to/your/openswarm # e.g. ~/OpenSwarmShawn/openswarm +cd instagram-mcp +``` + +If `cd instagram-mcp` says **no such file**, you are in the wrong directory — go up to the repo root first. + +3. Configure and build (run every command **from `instagram-mcp/`**): + +```bash +cp .env.example .env +``` + +Edit `.env` and set `INSTAGRAM_MCP_APP_ID` and `INSTAGRAM_MCP_APP_SECRET` on the **uncommented** `KEY=value` lines (lines starting with `#` are ignored by the loader). + +```bash +npm install +npm run build +node dist/index.js connect +``` + +The CLI **loads `instagram-mcp/.env` automatically** (no need to `export` in your shell). Alternatively, bake credentials into `dist/oauth-config.js` once (still from **`instagram-mcp/`**): + +```bash +npm run build:inject +``` + +(`inject-credentials.mjs` also reads `.env` if the variables are not already in the environment.) + +To skip `npx` delegation entirely when testing: set `INSTAGRAM_MCP_NO_NPX_FALLBACK=1` in `.env`. + +--- + ## Project structure ``` diff --git a/backend/apps/agents/mcp_preflight.py b/backend/apps/agents/mcp_preflight.py index 3492fee3..25f271d9 100644 --- a/backend/apps/agents/mcp_preflight.py +++ b/backend/apps/agents/mcp_preflight.py @@ -118,6 +118,11 @@ CURATED_SHORTLIST: list[CuratedEntry] = [ "title": "YouTube", "description": "Video transcripts, details, comments, channel stats, search — when the task involves YouTube content.", }, + { + "id": "Instagram", + "title": "Instagram", + "description": "Insights, publishing, comments, DMs, hashtags, and account discovery — when the task involves the user's Instagram Business or Creator presence (sign-in via instagram_connect in chat).", + }, ] diff --git a/backend/apps/tools_lib/tools_lib.py b/backend/apps/tools_lib/tools_lib.py index 3edeb0d0..aa6fb392 100644 --- a/backend/apps/tools_lib/tools_lib.py +++ b/backend/apps/tools_lib/tools_lib.py @@ -1290,3 +1290,144 @@ async def refresh_airtable_token(tool: ToolDefinition) -> Optional[str]: async def refresh_hubspot_token(tool: ToolDefinition) -> Optional[str]: """Refresh an expired HubSpot OAuth access_token.""" return await _refresh_via_proxy("hubspot", tool, default_expiry=1800) + + +@tools_lib.router.post("/credentials/instagram/validate") +async def validate_instagram_credentials(payload: dict) -> dict: + """Verify Instagram username/password via instagrapi.Client.login() and persist on success. + + Modeled on the OAuth verify-before-save flow: nothing is written to the tool + record unless the credentials actually log in. On success we also cache the + instagrapi session file at trypeggy's expected location so the MCP server + skips re-login on first spawn. + + Body : {"tool_id": str, "username": str, "password": str} + Returns : {"ok": true, "username": str} + | {"ok": false, "error": ""} + """ + tool_id = payload.get("tool_id") + username = (payload.get("username") or "").strip().lstrip("@") + password = payload.get("password") or "" + if not tool_id or not username or not password: + raise HTTPException( + status_code=400, + detail="tool_id, username, and password are required", + ) + + tool = _load(tool_id) + + def _do_login() -> "object": + from instagrapi import Client # type: ignore[import-not-found] + cl = Client() + cl.delay_range = [1, 3] + cl.request_timeout = 15 + cl.login(username, password) + return cl + + try: + cl = await asyncio.to_thread(_do_login) + except Exception as e: # noqa: BLE001 + msg = str(e) + return {"ok": False, "error": msg[:300] if msg else type(e).__name__} + + # Cache session where trypeggy expects it so the MCP server's startup + # account_info() probe has a fresh session and skips client.login() on + # first spawn. Trypeggy reads SESSION_FILE = REPO_ROOT/f"{username}_session.json". + try: + from pathlib import Path + trypeggy_root = Path("/Users/shawnmadadha/dev/instagram_dm_mcp") + if trypeggy_root.exists(): + cl.dump_settings(trypeggy_root / f"{username}_session.json") + (trypeggy_root / "current_user.txt").write_text(username + "\n") + except Exception as cache_err: # noqa: BLE001 + logger.warning(f"validate_instagram: could not cache session for trypeggy: {cache_err}") + + tool.credentials = {"INSTAGRAM_USERNAME": username, "INSTAGRAM_PASSWORD": password} + tool.auth_type = "env_vars" + tool.auth_status = "connected" + tool.connected_account_email = f"@{username}" + _save(tool) + return {"ok": True, "username": username} + + +@tools_lib.router.post("/credentials/instagram/from_browser") +async def instagram_from_browser(payload: dict) -> dict: + """Build a trypeggy-compatible instagrapi session from browser cookies. + + Called by the Electron `instagram-connect` IPC handler after it opens + instagram.com, lets the user log in, and polls cookies for sessionid + + ds_user_id. Mirrors the password-based validate endpoint's persistence + shape so the Tools tile flips to Connected the same way. + + Body : {"tool_id": str, "sessionid": str, "ds_user_id": str, "cookies": {name: value}} + Returns: + {"ok": true, "username": str, "user_id": str} -- session saved + {"ok": false, "error": ""} -- nothing persisted + """ + from pathlib import Path + from urllib.parse import unquote + + tool_id = payload.get("tool_id") + sessionid_raw = payload.get("sessionid") or "" + ds_user_id = str(payload.get("ds_user_id") or "") + cookies: dict = payload.get("cookies") or {} + if not tool_id or not sessionid_raw or not ds_user_id: + raise HTTPException( + status_code=400, + detail="tool_id, sessionid, and ds_user_id are required", + ) + + tool = _load(tool_id) + + def _do_build() -> dict: + from instagrapi import Client # type: ignore[import-not-found] + cl = Client() + cl.delay_range = [1, 3] + cl.request_timeout = 15 + sid = unquote(sessionid_raw) + # login_by_sessionid resets the cookie jar to {sessionid} only via + # init(). Catch any 467 it raises so we still get the cookies set up + # for re-injection below. + try: + cl.login_by_sessionid(sid) + except Exception: # noqa: BLE001 + pass + decoded = {name: unquote(str(value)) for name, value in cookies.items()} + for name, value in decoded.items(): + cl.private.cookies.set(name, value, domain=".instagram.com") + if decoded.get("mid"): + cl.mid = decoded["mid"] + if not cl.user_id: + try: + cl.user_id = int(ds_user_id) + except (TypeError, ValueError): + pass + username = cl.username or f"id_{ds_user_id}" + user_id_str = str(cl.user_id) if cl.user_id else ds_user_id + return {"settings": cl.get_settings(), "username": username, "user_id": user_id_str} + + try: + result = await asyncio.to_thread(_do_build) + except Exception as e: # noqa: BLE001 + return {"ok": False, "error": (str(e) or type(e).__name__)[:300]} + + username = result["username"] + user_id_str = result["user_id"] + settings = result["settings"] + + trypeggy_root = Path("/Users/shawnmadadha/dev/instagram_dm_mcp") + try: + if trypeggy_root.exists(): + (trypeggy_root / f"{username}_session.json").write_text(json.dumps(settings)) + (trypeggy_root / "current_user.txt").write_text(username + "\n") + except Exception as cache_err: # noqa: BLE001 + logger.warning(f"instagram_from_browser: could not write trypeggy session file: {cache_err}") + + # No password from the browser flow. trypeggy will load the session and + # tolerate a 467 on its startup account_info() probe (patched locally). + tool.credentials = {"INSTAGRAM_USERNAME": username} + tool.auth_type = "env_vars" + tool.auth_status = "connected" + tool.connected_account_email = f"@{username}" + _save(tool) + return {"ok": True, "username": username, "user_id": user_id_str} diff --git a/backend/main.py b/backend/main.py index 1e9e643b..1341252b 100644 --- a/backend/main.py +++ b/backend/main.py @@ -544,6 +544,21 @@ async def mcp_meta(action: str, request: Request): "hubspot": ["crm", "sales", "leads", "contacts", "deals"], "reddit": ["forum", "subreddit", "posts", "comments", "social"], "youtube": ["video", "transcript", "channel"], + "instagram": [ + "ig", + "reels", + "stories", + "social", + "analytics", + "insights", + "hashtag", + "dm", + "direct message", + "comments", + "creator", + "business", + "meta", + ], } def _connected_servers() -> list[dict]: diff --git a/backend/requirements.txt b/backend/requirements.txt index 2f166bf0..195b184d 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -16,6 +16,10 @@ python-dotenv==1.1.1 Pillow httpx>=0.27.0 trafilatura +# instagrapi: used by /credentials/instagram/validate to verify Instagram +# login server-side before persisting credentials (verify-before-save flow, +# same shape as the OAuth integrations). +instagrapi>=2.0 # tzlocal: dev-mode fallback for resolving the user's IANA timezone when # Electron's OPENSWARM_TIMEZONE env var isn't set (i.e. `bash run.sh`). # Packaged builds get the env var directly so this is a safety net. diff --git a/electron/main.js b/electron/main.js index 1dd6554c..1804d8ad 100644 --- a/electron/main.js +++ b/electron/main.js @@ -352,6 +352,49 @@ function getBundledNodePath() { return fs.existsSync(candidate) ? candidate : null; } +/** + * Directory containing package.json + dist/index.js + node_modules for + * instagram-mcp-buddy (dev: monorepo root; packaged: extraResources/instagram-mcp). + */ +function getInstagramMcpPackageRoot() { + const base = isPackaged ? process.resourcesPath : path.join(__dirname, '..'); + return path.join(base, 'instagram-mcp'); +} + +/** + * Resolve a Node binary for spawning instagram-mcp-buddy. Prefer the same + * bundled Node shipped for 9Router/MCP so Finder-launched Mac apps work + * without a user PATH; fall back to OPENSWARM_NODE_PATH, login-shell resolution + * (dev), then `node` on PATH. + */ +function resolveNodeForMcpCli() { + const bundled = getBundledNodePath(); + if (bundled && fs.existsSync(bundled)) return bundled; + const fromEnv = process.env.OPENSWARM_NODE_PATH; + if (fromEnv && fs.existsSync(fromEnv)) return fromEnv; + if (!isPackaged && process.platform !== 'win32') { + try { + const shell = process.env.SHELL || '/bin/zsh'; + const p = execFileSync(shell, ['-lc', 'command -v node'], { + encoding: 'utf8', + timeout: 8000, + env: { ...process.env, HOME: os.homedir() }, + }).trim().split(/\r?\n/).filter(Boolean).pop(); + if (p && fs.existsSync(p)) return p; + } catch (_) { /* fall through */ } + for (const c of ['/opt/homebrew/bin/node', '/usr/local/bin/node']) { + if (fs.existsSync(c)) return c; + } + } + if (!isPackaged && process.platform === 'win32') { + try { + const p = execFileSync('where', ['node'], { encoding: 'utf8', timeout: 8000 }).trim().split(/\r?\n/).filter(Boolean)[0]; + if (p && fs.existsSync(p)) return p; + } catch (_) { /* fall through */ } + } + return process.platform === 'win32' ? 'node.exe' : 'node'; +} + /** * Run instagram-mcp-buddy connect|logout non-interactively. Prefers the workspace * build at ../instagram-mcp/dist/index.js when present (dev / source tree); @@ -364,25 +407,28 @@ function getBundledNodePath() { */ function runInstagramBuddyCli(subcommand, extraEnv = {}) { return new Promise((resolve, reject) => { - const repoRoot = path.join(__dirname, '..'); - const localScript = path.join(repoRoot, 'instagram-mcp', 'dist', 'index.js'); + const igPkgRoot = getInstagramMcpPackageRoot(); + const localScript = path.join(igPkgRoot, 'dist', 'index.js'); const pathMerged = [getShellPath(), process.env.PATH || ''].filter(Boolean).join(path.delimiter); const env = { ...process.env, ...extraEnv, PATH: pathMerged }; let cmd; let args; let shell = false; + /** cwd for module resolution (instagram-mcp-buddy needs package node_modules). */ + let cwd = isPackaged ? process.resourcesPath : path.join(__dirname, '..'); + if (fs.existsSync(localScript)) { - const nodeExe = getBundledNodePath() || (process.platform === 'win32' ? 'node.exe' : 'node'); - cmd = nodeExe; + cmd = resolveNodeForMcpCli(); args = [localScript, subcommand]; + cwd = igPkgRoot; } else { cmd = 'npx'; args = ['-y', 'instagram-mcp-buddy', subcommand]; if (process.platform === 'win32') shell = true; } - const child = spawn(cmd, args, { env, cwd: repoRoot, windowsHide: true, shell }); + const child = spawn(cmd, args, { env, cwd, windowsHide: true, shell }); let out = ''; let err = ''; child.stdout.on('data', (d) => { out += d.toString(); }); @@ -1543,19 +1589,162 @@ function normalizeEnvRecord(mcpEnv) { return extra; } -ipcMain.handle('instagram-connect', async (_event, mcpEnv) => { - try { - const parsed = await runInstagramBuddyCli('connect', normalizeEnvRecord(mcpEnv)); - return { ok: true, ...parsed }; - } catch (err) { - return { ok: false, error: err?.message || String(err) }; +// instagram-connect: opens an in-app browser window pointed at instagram.com, +// polls for the sessionid cookie, then hands them to instagram-mcp-buddy's +// `validate-credentials --from-browser` subcommand which builds an instagrapi +// session and saves it to the keychain + platformdirs session.json. +// User never sees a terminal, never types a password, never types 2FA. +ipcMain.handle('instagram-connect', async (_event, payload) => { + const toolId = (payload && payload.toolId) || ''; + if (!toolId) { + return { ok: false, error: 'instagram-connect: missing toolId from caller' }; } + + const win = new BrowserWindow({ + width: 480, + height: 700, + title: 'Sign in to Instagram', + parent: mainWindow || undefined, + modal: false, + autoHideMenuBar: true, + webPreferences: { + partition: 'persist:instagram-auth', + contextIsolation: true, + nodeIntegration: false, + sandbox: true, + }, + }); + + try { + await win.loadURL('https://www.instagram.com/accounts/login/'); + } catch (err) { + if (!win.isDestroyed()) win.close(); + return { ok: false, error: `Failed to load Instagram: ${err.message}` }; + } + + // POST cookies to the backend /credentials/instagram/from_browser endpoint. + // The backend uses instagrapi to build a session, writes a trypeggy-compatible + // session file, and persists tool.credentials + auth_status='connected'. + const callBackend = (body) => new Promise((res) => { + const data = Buffer.from(JSON.stringify(body), 'utf8'); + const req = http.request({ + host: '127.0.0.1', + port: backendPort || 8324, + path: '/api/tools/credentials/instagram/from_browser', + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Content-Length': data.length, + 'Authorization': `Bearer ${authToken || ''}`, + }, + timeout: 60000, + }, (resp) => { + let chunks = ''; + resp.on('data', (c) => { chunks += c; }); + resp.on('end', () => { + try { + res(JSON.parse(chunks)); + } catch (_) { + res({ ok: false, error: `backend non-JSON response (${resp.statusCode}): ${chunks.slice(0, 200)}` }); + } + }); + }); + req.on('timeout', () => { req.destroy(new Error('backend call timed out after 60s')); }); + req.on('error', (e) => { res({ ok: false, error: `backend call failed: ${e.message}` }); }); + req.write(data); + req.end(); + }); + + return new Promise((resolve) => { + let settled = false; + const finish = (value) => { + if (settled) return; + settled = true; + clearInterval(browserPoll); + clearTimeout(timeoutHandle); + if (!win.isDestroyed()) win.close(); + resolve(value); + }; + + win.on('closed', () => { if (!settled) finish({ ok: false, error: 'Sign-in window was closed' }); }); + + const browserPoll = setInterval(async () => { + if (win.isDestroyed() || settled) return; + try { + const cookies = await win.webContents.session.cookies.get({ domain: '.instagram.com' }); + const sessionid = (cookies.find(c => c.name === 'sessionid') || {}).value; + const dsUserId = (cookies.find(c => c.name === 'ds_user_id') || {}).value; + if (!sessionid || !dsUserId) return; + + clearInterval(browserPoll); + const cookieDict = {}; + for (const c of cookies) cookieDict[c.name] = c.value; + + console.log(`[ig-connect] cookies acquired; calling backend from_browser for tool ${toolId}`); + const result = await callBackend({ + tool_id: toolId, + sessionid, + ds_user_id: dsUserId, + cookies: cookieDict, + }); + if (!result.ok) { finish(result); return; } + finish({ ok: true, username: result.username || dsUserId, user_id: result.user_id }); + } catch (_) { /* transient cookie read, retry next tick */ } + }, 1500); + + const timeoutHandle = setTimeout(() => { + finish({ ok: false, error: 'Sign-in timed out after 10 minutes' }); + }, 10 * 60 * 1000); + }); }); -ipcMain.handle('instagram-logout', async (_event, mcpEnv) => { +// Full-auth upgrade handler (password + 2FA) disabled while trusted-notification +// polling is unimplemented. Connect uses browser cookies only — see above. +/* +ipcMain.handle('instagram-upgrade-session', async (_event, payload) => { + const PYTHON = '/usr/local/bin/python3.11'; + return new Promise((res) => { + const py = spawn(PYTHON, ['-m', 'instagram_mcp_buddy', 'validate-credentials'], { + stdio: ['pipe', 'pipe', 'pipe'], windowsHide: true, + }); + let out = '', pyErr = ''; + py.stdout.on('data', d => { out += d; }); + py.stderr.on('data', d => { pyErr += d; }); + py.stdin.write(JSON.stringify(payload)); + py.stdin.end(); + py.on('close', exitCode => { + const lastLine = out.trim().split('\n').pop() || ''; + try { + res(JSON.parse(lastLine)); + } catch (_) { + res({ ok: false, error: exitCode !== 0 ? (pyErr.slice(0, 300) || 'validate-credentials failed') : 'invalid JSON' }); + } + }); + }); +}); +*/ + +ipcMain.handle('instagram-logout', async (_event, _mcpEnv) => { try { - const parsed = await runInstagramBuddyCli('logout', normalizeEnvRecord(mcpEnv)); - return { ok: true, ...parsed }; + const pyLines = [ + 'import keyring', + 'from pathlib import Path', + 'from platformdirs import user_data_dir', + "username = keyring.get_password('instagram-mcp-buddy', 'active_username')", + 'if username:', + " try: keyring.delete_password('instagram-mcp-buddy', f'password:{username}')", + ' except Exception: pass', + " try: keyring.delete_password('instagram-mcp-buddy', 'active_username')", + ' except Exception: pass', + "session_file = Path(user_data_dir('instagram-mcp-buddy')) / 'session.json'", + 'if session_file.exists(): session_file.unlink()', + 'print("{\\\"ok\\\": true}")', + ]; + await new Promise((resolve, reject) => { + const py = spawn('/usr/local/bin/python3.11', ['-c', pyLines.join('\n')], { windowsHide: true }); + py.on('close', code => code === 0 ? resolve() : reject(new Error('logout failed'))); + }); + return { ok: true }; } catch (err) { return { ok: false, error: err?.message || String(err) }; } diff --git a/electron/package.json b/electron/package.json index a3028c93..68c71ca8 100644 --- a/electron/package.json +++ b/electron/package.json @@ -117,6 +117,13 @@ "**/*" ] }, + { + "from": "build-staging/instagram-mcp", + "to": "instagram-mcp", + "filter": [ + "**/*" + ] + }, { "from": "build-staging/router", "to": "router", diff --git a/electron/preload.js b/electron/preload.js index cd9eaeae..da1a3909 100644 --- a/electron/preload.js +++ b/electron/preload.js @@ -30,6 +30,7 @@ const { contextBridge, ipcRenderer } = require('electron'); connectSlack: () => ipcRenderer.invoke('connect-slack'), /** Desktop Instagram OAuth via instagram-mcp-buddy CLI (keychain). Optional env merged from tool mcp_config.env */ instagramConnect: (mcpEnv) => ipcRenderer.invoke('instagram-connect', mcpEnv), + // instagramUpgrade: (payload) => ipcRenderer.invoke('instagram-upgrade-session', payload), // disabled until trusted-notification polling is implemented instagramLogout: (mcpEnv) => ipcRenderer.invoke('instagram-logout', mcpEnv), sendCdpCommand: (wcId, method, params) => ipcRenderer.invoke('send-cdp-command', wcId, method, params), cdpCacheSet: (wcId, indexMap) => ipcRenderer.invoke('cdp-cache-set', wcId, indexMap), diff --git a/frontend/src/app/pages/Tools/Tools.tsx b/frontend/src/app/pages/Tools/Tools.tsx index 9bb837dd..64e01369 100644 --- a/frontend/src/app/pages/Tools/Tools.tsx +++ b/frontend/src/app/pages/Tools/Tools.tsx @@ -98,6 +98,7 @@ interface CredentialField { label: string; placeholder: string; helpText?: string; + type?: 'text' | 'password'; } interface Integration { @@ -147,22 +148,20 @@ const INTEGRATIONS: Integration[] = [ id: 'instagram', name: 'Instagram', description: - 'Insights, publishing, comments, DMs, hashtags, mentions, and discovery for Business/Creator accounts. On the desktop app, use Sign in with Instagram below (or open an agent chat and run instagram_connect). A browser opens for Meta OAuth; the token stays in your OS keychain. In the browser-only app, connect from chat. Requires Instagram Professional.', + 'Instagram DM outreach plus user/follower lookup, post engagement, and story reads. 25 tools from trypeggy/instagram_dm_mcp. Sign in with your Instagram username and password (personal accounts supported). NOTE: mass-DMing from personal accounts triggers Instagram anti-abuse detection; use sparingly on established accounts.', mcp_config: { type: 'stdio', - command: 'npx', - args: ['-y', 'instagram-mcp-buddy'], - env: { - INSTAGRAM_MCP_ENABLE_PUBLISHING: 'true', - INSTAGRAM_MCP_ENABLE_COMMENTMODERATION: 'true', - INSTAGRAM_MCP_ENABLE_MESSAGING: 'true', - }, + command: '/usr/local/bin/python3.11', + args: ['/Users/shawnmadadha/dev/instagram_dm_mcp/src/mcp_server.py'], }, color: '#E4405F', - website: 'https://www.npmjs.com/package/instagram-mcp-buddy', - connectLabel: 'Sign-in steps', - connectInstructions: - '1) Enable Instagram here. 2) Desktop: click Sign in with Instagram (or use agent chat / instagram_connect). 3) Complete login in the browser. 4) Optional: instagram_status lists granted scopes. Publishing/DMs require those permissions on your Meta app; OpenSwarm enables feature flags in mcp_config.env so agents see the full surface.', + website: 'https://github.com/trypeggy/instagram_dm_mcp', + connectLabel: 'Connect Instagram', + connectInstructions: 'Sign in with the username and password of the Instagram account you want the agent to use. Credentials are stored locally in OpenSwarm and passed as env vars to the MCP server on launch. Instagram session is cached on disk after first successful login so you do not need to re-enter on every restart.', + credentialFields: [ + { key: 'INSTAGRAM_USERNAME', label: 'Instagram Username', placeholder: 'your_handle (no @)', type: 'text' }, + { key: 'INSTAGRAM_PASSWORD', label: 'Instagram Password', placeholder: '••••••••', type: 'password' }, + ], icon: ( @@ -180,6 +179,24 @@ const INTEGRATIONS: Integration[] = [ ), }, + // Personal tile disabled — consolidated to single Instagram tile with the + // TS Graph API server. Personal accounts must convert to Business/Creator + // (free, 30 sec) to use Instagram in OpenSwarm. Preserved here in case the + // Personal fallback is wanted later. + /* + { + id: 'instagram-personal', + name: 'Instagram (Personal)', + description: 'For personal Instagram accounts...', + mcp_config: { + type: 'stdio', + command: '/usr/local/bin/python3.11', + args: ['-m', 'instagram_mcp_buddy'], + }, + color: '#E4405F', + website: 'https://www.npmjs.com/package/instagram-mcp-buddy', + }, + */ { id: 'google-workspace', name: 'Google Workspace', @@ -515,8 +532,54 @@ const Tools: React.FC = () => { const outputs = useMemo(() => Object.values(outputItems), [outputItems]); const allTools = Object.values(items); const tools = allTools; - const uninstalledIntegrations = useMemo(() => INTEGRATIONS.filter((ig) => !allTools.find((t) => t.name === ig.name)), [allTools]); - const getIntegrationForTool = useCallback((tool: ToolDefinition) => INTEGRATIONS.find((ig) => ig.name === tool.name), []); + // Match tool to integration by exact name. Fallback: any tool whose name + // starts with "Instagram" maps to the unified "instagram" integration so + // historic Redux state from earlier tile names ("Instagram (Personal)", + // "Instagram (DM Outreach)", etc.) still resolves to a valid integration + // and doesn't crash the render with ig.color on undefined. + const matchIntegration = (toolName: string | undefined) => { + if (!toolName) return undefined; + const exact = INTEGRATIONS.find((ig) => ig.name === toolName); + if (exact) return exact; + if (toolName.toLowerCase().startsWith('instagram')) { + return INTEGRATIONS.find((ig) => ig.id === 'instagram'); + } + return undefined; + }; + const uninstalledIntegrations = useMemo( + () => INTEGRATIONS.filter((ig) => !allTools.find((t) => matchIntegration(t.name)?.id === ig.id)), + [allTools], + ); + const getIntegrationForTool = useCallback((tool: ToolDefinition) => matchIntegration(tool.name), []); + + // Deduplicate tools: if multiple Redux entries resolve to the same + // integration (happens when an integration was renamed and old entries + // linger in persisted state), render only one tile per integration. + // Prefer connected over configured/disconnected. + const dedupedTools = useMemo(() => { + const byIntegration = new Map(); + const orphans: ToolDefinition[] = []; + for (const t of tools) { + const ig = matchIntegration(t.name); + if (!ig) { + orphans.push(t); + continue; + } + const existing = byIntegration.get(ig.id); + if (!existing) { + byIntegration.set(ig.id, t); + continue; + } + const tConnected = t.auth_status === 'connected'; + const existingConnected = existing.auth_status === 'connected'; + if (tConnected && !existingConnected) byIntegration.set(ig.id, t); + } + return [...byIntegration.values(), ...orphans]; + // matchIntegration is stable across renders since INTEGRATIONS is const; + // we intentionally don't include it in deps to avoid creating a new array + // on every render. + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [tools]); const [dialogOpen, setDialogOpen] = useState(false); const [editingId, setEditingId] = useState(null); @@ -598,6 +661,27 @@ const Tools: React.FC = () => { /** Instagram desktop CLI (Electron); null when idle */ const [instagramConnectBusy, setInstagramConnectBusy] = useState(null); + // Full-auth upgrade dialog (password + 2FA) is disabled while we figure out + // Instagram's trusted-notification polling endpoint. Connect uses browser-only + // sign-in, which unlocks ~8 read tools. See handleInstagramConnect below. + // To re-enable: uncomment the state, handlers (handleIgUpgrade*, handleIgTfa*, + // handleIgApproveApp, handleIgResendSms, handleIgSkipUpgrade, handleIgUpgradeSuccess), + // the upgrade Dialog JSX, and restore the needs_upgrade branch in handleInstagramConnect. + /* + type IgUpgradeState = { + toolId: string; + username: string; + password: string; + step: 'password' | '2fa'; + pending2fa: { identifier: string; session_key: string; obfuscated_phone: string; sms_on: boolean; totp_on?: boolean; whatsapp_on?: boolean; sms_error?: string } | null; + busy: boolean; + error: string; + }; + const [igUpgrade, setIgUpgrade] = useState(null); + const [igUpgradePw, setIgUpgradePw] = useState(''); + const [igUpgradeCode, setIgUpgradeCode] = useState(''); + */ + const getInstalledIntegration = useCallback((integration: Integration): ToolDefinition | undefined => { return allTools.find((t) => t.name === integration.name); }, [allTools]); @@ -1028,35 +1112,41 @@ const Tools: React.FC = () => { setSnackbar({ open: true, message: 'Disconnected from Microsoft 365' }); }; + const finalizeInstagramConnect = async (toolId: string, username: string) => { + const label = username ? `@${username}` : ''; + await dispatch(updateTool({ id: toolId, auth_status: 'connected', connected_account_email: label })); + setSnackbar({ open: true, message: username ? `Instagram connected as @${username}` : 'Instagram connected' }); + setExpandedToolId(toolId); + dispatch(discoverTools(toolId)); + }; + const handleInstagramConnect = async (toolId: string) => { - const tool = items[toolId]; - const bridge = (window as unknown as { openswarm?: { instagramConnect?: (env?: Record) => Promise<{ ok?: boolean; error?: string; username?: string }> } }).openswarm?.instagramConnect; + // Single Instagram tile pointed at trypeggy/instagram_dm_mcp. + // Click Connect → in-app browser opens → user signs in (handles their own + // 2FA in the IG UI they already trust) → Electron polls cookies → spawns + // trypeggy's auth.py --from-browser which writes the session file → + // tool is enabled and ready to use. + const bridge = (window as unknown as { openswarm?: { instagramConnect?: (arg?: { toolId: string }) => Promise<{ ok?: boolean; error?: string; username?: string; user_id?: string }> } }).openswarm?.instagramConnect; if (!bridge) { setSnackbar({ open: true, - message: 'Instagram sign-in here needs the OpenSwarm desktop app. In the browser, open an agent chat and run instagram_connect (or say “connect Instagram”).', + message: 'Instagram sign-in here needs the OpenSwarm desktop app.', severity: 'error', }); return; } setInstagramConnectBusy(toolId); try { - const rawEnv = tool?.mcp_config?.env; - const mcpEnv: Record = {}; - if (rawEnv && typeof rawEnv === 'object' && !Array.isArray(rawEnv)) { - for (const [k, v] of Object.entries(rawEnv)) { - if (v !== undefined && v !== null) mcpEnv[k] = String(v); - } - } - const result = await bridge(mcpEnv); + const result = await bridge({ toolId }); if (!result?.ok) { setSnackbar({ open: true, message: result?.error || 'Instagram sign-in failed', severity: 'error' }); return; } - const username = typeof result.username === 'string' ? result.username : ''; - const label = username ? `@${username}` : ''; - await dispatch(updateTool({ id: toolId, auth_status: 'connected', connected_account_email: label })); - setSnackbar({ open: true, message: username ? `Instagram connected as @${username}` : 'Instagram connected — discovering actions…' }); + // Backend already persisted credentials + auth_status='connected' inside + // the from_browser endpoint. Refresh local state so the tile flips. + await dispatch(fetchToolStatus(toolId)); + const username = result.username || ''; + setSnackbar({ open: true, message: username ? `Instagram connected as @${username}` : 'Instagram connected' }); setExpandedToolId(toolId); dispatch(discoverTools(toolId)); } catch (err: unknown) { @@ -1067,6 +1157,111 @@ const Tools: React.FC = () => { } }; + // Upgrade-flow handlers disabled while trusted-notification polling is unimplemented. + // See state declaration above for re-enable notes. + /* + const igUpgradeBridge = () => (window as unknown as { openswarm?: { instagramUpgrade?: (p: Record) => Promise> } }).openswarm?.instagramUpgrade; + + const handleIgUpgradeSuccess = async (result: Record) => { + if (!igUpgrade) return; + const uname = (result.username as string) || igUpgrade.username; + setIgUpgrade(null); + setIgUpgradePw(''); + setIgUpgradeCode(''); + await finalizeInstagramConnect(igUpgrade.toolId, uname); + }; + + const handleIgUpgradeSubmit = async () => { + if (!igUpgrade || !igUpgradePw.trim() || igUpgrade.busy) return; + const bridge = igUpgradeBridge(); + if (!bridge) return; + setIgUpgrade((p) => p ? { ...p, busy: true, error: '' } : null); + const result = await bridge({ username: igUpgrade.username, password: igUpgradePw.trim() }) as Record; + if (result.ok) { + void handleIgUpgradeSuccess(result); + } else if (result.needs_2fa) { + setIgUpgrade((p) => p ? { ...p, step: '2fa', password: igUpgradePw.trim(), pending2fa: result as IgUpgradeState['pending2fa'], busy: false, error: '' } : null); + setIgUpgradeCode(''); + } else { + setIgUpgrade((p) => p ? { ...p, busy: false, error: (result.error as string) || 'Sign-in failed. Check your password.' } : null); + } + }; + + const handleIgTfaSubmit = async () => { + if (!igUpgrade || !igUpgradeCode.trim() || igUpgrade.busy) return; + const bridge = igUpgradeBridge(); + if (!bridge) return; + setIgUpgrade((p) => p ? { ...p, busy: true, error: '' } : null); + const result = await bridge({ + username: igUpgrade.username, + password: igUpgrade.password, + code: igUpgradeCode.trim(), + identifier: igUpgrade.pending2fa?.identifier || '', + session_key: igUpgrade.pending2fa?.session_key || '', + }) as Record; + if (result.ok) { + void handleIgUpgradeSuccess(result); + } else if (result.needs_2fa) { + setIgUpgrade((p) => p ? { ...p, pending2fa: result as IgUpgradeState['pending2fa'], busy: false, error: '' } : null); + setIgUpgradeCode(''); + } else { + setIgUpgrade((p) => p ? { ...p, busy: false, error: (result.error as string) || 'Code incorrect. Try again.' } : null); + } + }; + + const handleIgResendSms = async () => { + if (!igUpgrade || igUpgrade.busy) return; + const bridge = igUpgradeBridge(); + if (!bridge) return; + setIgUpgrade((p) => p ? { ...p, busy: true, error: '' } : null); + const result = await bridge({ username: igUpgrade.username, password: igUpgrade.password }) as Record; + if (result.ok) { + void handleIgUpgradeSuccess(result); + } else if (result.needs_2fa) { + setIgUpgrade((p) => p ? { ...p, pending2fa: result as IgUpgradeState['pending2fa'], busy: false, error: '' } : null); + setIgUpgradeCode(''); + } else { + setIgUpgrade((p) => p ? { ...p, busy: false, error: (result.error as string) || 'Could not resend code.' } : null); + } + }; + + const handleIgApproveApp = async () => { + if (!igUpgrade || igUpgrade.busy) return; + const bridge = igUpgradeBridge(); + if (!bridge) return; + setIgUpgrade((p) => p ? { ...p, busy: true, error: '' } : null); + const result = await bridge({ + username: igUpgrade.username, + password: igUpgrade.password, + session_key: igUpgrade.pending2fa?.session_key || '', + approved_from_app: true, + }) as Record; + if (result.ok) { + void handleIgUpgradeSuccess(result); + } else if (result.needs_2fa) { + setIgUpgrade((p) => p ? { + ...p, + pending2fa: result as IgUpgradeState['pending2fa'], + busy: false, + error: 'Still waiting on Instagram to confirm the approval. Try again, or enter a code instead.', + } : null); + setIgUpgradeCode(''); + } else { + setIgUpgrade((p) => p ? { ...p, busy: false, error: (result.error as string) || 'Approval not detected.' } : null); + } + }; + + const handleIgSkipUpgrade = () => { + if (!igUpgrade) return; + const toolId = igUpgrade.toolId; + const uname = igUpgrade.username; + setIgUpgrade(null); + setIgUpgradePw(''); + setIgUpgradeCode(''); + void finalizeInstagramConnect(toolId, uname); + }; + */ + const handleInstagramDisconnect = async (toolId: string) => { const tool = items[toolId]; const bridge = (window as unknown as { openswarm?: { instagramLogout?: (env?: Record) => Promise<{ ok?: boolean; error?: string }> } }).openswarm?.instagramLogout; @@ -1116,6 +1311,31 @@ const Tools: React.FC = () => { setCredDialogSaving(true); try { + // Instagram: verify the login server-side against instagrapi BEFORE + // persisting. Mirrors the OAuth verify-before-save shape used by Notion + // and Airtable; the only difference is we hit our own endpoint instead + // of bouncing through a third-party OAuth proxy. + if (credDialogIntegration.id === 'instagram') { + const username = (credDialogValues['INSTAGRAM_USERNAME'] || '').replace(/^@/, '').trim(); + const password = credDialogValues['INSTAGRAM_PASSWORD'] || ''; + const resp = await fetch(`${API_BASE}/tools/credentials/instagram/validate`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ tool_id: credDialogToolId, username, password }), + }); + const data = await resp.json().catch(() => ({ ok: false, error: `bad response (${resp.status})` })); + if (!data.ok) { + setSnackbar({ open: true, message: `Instagram login failed: ${data.error || 'unknown error'}`, severity: 'error' }); + return; + } + // Backend persisted credentials + auth_status='connected'. Refresh + // local tool state so the tile flips without a hard reload. + await dispatch(fetchToolStatus(credDialogToolId)); + setCredDialogOpen(false); + setSnackbar({ open: true, message: `Instagram connected as @${data.username}! Re-discovering actions…` }); + dispatch(discoverTools(credDialogToolId)); + return; + } const result = await dispatch(updateTool({ id: credDialogToolId, credentials: credDialogValues, @@ -1558,8 +1778,13 @@ const Tools: React.FC = () => { ); })} - {tools.map((tool) => { + {dedupedTools.map((tool) => { const ig = getIntegrationForTool(tool); + // Defensive: if Redux has a tool whose name no longer matches + // any integration definition (e.g. an integration was renamed + // or removed in a later code change), skip rendering instead + // of crashing on ig.color. + if (!ig) return null; const isExpanded = expandedToolId === tool.id; const isMcp = tool.mcp_config && Object.keys(tool.mcp_config).length > 0; const isStdio = isMcp && (tool.mcp_config.type === 'stdio' || !!tool.mcp_config.command); @@ -1755,10 +1980,6 @@ const Tools: React.FC = () => { ig?.id === 'youtube' || tool.name?.toLowerCase() === 'youtube' || (tool.command || '').toLowerCase().includes('youtube'); - const isInstagram = - ig?.id === 'instagram' || - tool.name?.toLowerCase() === 'instagram' || - (tool.command || '').toLowerCase().includes('instagram'); return ( { Connect Microsoft 365 )} - {!isDisabled && isInstagram && tool.auth_status !== 'connected' && ( + {!isDisabled && ig?.id === 'instagram' && tool.auth_status !== 'connected' && ( )} - {!isDisabled && ig?.credentialFields && tool.auth_status !== 'connected' && ( + {!isDisabled && ig?.credentialFields && ig?.id !== 'instagram' && tool.auth_status !== 'connected' && ( )} {!isDisabled && ig && tool.auth_status === 'connected' && ( - + } label={tool.connected_account_email ? `Connected · ${tool.connected_account_email}` : 'Connected'} size="small" - onDelete={(ig.credentialFields || ig.authType === 'oauth2' || ig.authType === 'device_code' || isInstagram) ? (e: React.SyntheticEvent) => { + onDelete={(ig.credentialFields || ig.authType === 'oauth2' || ig.authType === 'device_code') ? (e: React.SyntheticEvent) => { e.stopPropagation(); if (ig.authType === 'device_code') handleM365Disconnect(tool.id); - else if (isInstagram) void handleInstagramDisconnect(tool.id); else handleDisconnectIntegration(tool.id, ig); } : undefined} onClick={(e) => e.stopPropagation()} @@ -2496,6 +2716,157 @@ const Tools: React.FC = () => { + {/* Instagram Upgrade Dialog (password + 2FA) disabled while trusted-notification polling is unimplemented. */} + {/* + { if (!igUpgrade?.busy) { setIgUpgrade(null); setIgUpgradePw(''); setIgUpgradeCode(''); } }} + maxWidth="xs" + fullWidth + PaperProps={{ sx: { bgcolor: c.bg.surface, backgroundImage: 'none', borderRadius: 4, border: `1px solid ${c.border.subtle}` } }} + > + {igUpgrade?.step === 'password' ? ( + <> + + + 📸 + + Unlock full access + + + + Signed in as @{igUpgrade?.username}. Enter your Instagram password to unlock feed, search, DMs, stories, and hashtags. + + {igUpgrade?.error && ( + {igUpgrade.error} + )} + setIgUpgradePw(e.target.value)} + onKeyDown={(e) => { if (e.key === 'Enter') void handleIgUpgradeSubmit(); }} + autoFocus + disabled={igUpgrade?.busy} + sx={{ '& .MuiOutlinedInput-root': { bgcolor: c.bg.page } }} + /> + + + + + + + ) : ( + <> + + + 🔐 + + Verify it's you + + + + {(() => { + const p = igUpgrade?.pending2fa; + if (!p) return 'Enter your two-factor code.'; + const methods: string[] = []; + if (p.sms_on && !p.sms_error) methods.push(`SMS to ${p.obfuscated_phone || 'your phone'}`); + if (p.totp_on) methods.push('your authenticator app'); + if (p.whatsapp_on) methods.push('WhatsApp'); + if (methods.length === 0) return 'Enter the code from your authenticator app or a saved backup code.'; + return `Enter the code from ${methods.join(' or ')}.`; + })()} + + {igUpgrade?.pending2fa?.sms_error && ( + + Instagram didn't accept the SMS request ({igUpgrade.pending2fa.sms_error}). Use your authenticator app or a backup code instead. + + )} + {igUpgrade?.error && ( + {igUpgrade.error} + )} + setIgUpgradeCode(e.target.value)} + onKeyDown={(e) => { if (e.key === 'Enter') void handleIgTfaSubmit(); }} + autoFocus + disabled={igUpgrade?.busy} + inputProps={{ inputMode: 'numeric', maxLength: 8 }} + sx={{ '& .MuiOutlinedInput-root': { bgcolor: c.bg.page } }} + /> + + + After tapping Approve in Instagram, click this. We wait up to 35 seconds for the approval to land. + + {igUpgrade?.pending2fa?.sms_on && ( + + )} + + + + + + + + )} + + */} + {/* Integration Credentials Dialog */} { key={field.key} label={field.label} placeholder={field.placeholder} + type={field.type || 'text'} value={credDialogValues[field.key] || ''} onChange={(e) => setCredDialogValues((prev) => ({ ...prev, [field.key]: e.target.value }))} fullWidth diff --git a/scripts/build-app.sh b/scripts/build-app.sh index 78834aeb..070192db 100755 --- a/scripts/build-app.sh +++ b/scripts/build-app.sh @@ -346,6 +346,36 @@ else fi echo "" +# Ship instagram-mcp-buddy (dist + prod node_modules) so "Sign in with Instagram" +# works in packaged apps without system npx/node. Optional: META_APP_ID + +# META_APP_SECRET inject the same Meta app credentials as npm publish. +echo "[3d/5] Staging instagram-mcp-buddy for desktop OAuth CLI..." +IG_SRC="$PROJECT_ROOT/instagram-mcp" +IG_STAGE="$STAGING_DIR/instagram-mcp" +if [[ ! -f "$IG_SRC/package.json" ]]; then + echo "ERROR: instagram-mcp not found at $IG_SRC" + exit 1 +fi +mkdir -p "$IG_STAGE" +( + cd "$IG_SRC" + npm ci + npm run build +) +if [[ -n "${META_APP_ID:-}" && -n "${META_APP_SECRET:-}" ]]; then + (cd "$IG_SRC" && node scripts/inject-credentials.mjs) + echo "[3d] Injected Meta app credentials into staged oauth-config." +else + echo "[3d] META_APP_ID/META_APP_SECRET not set — OAuth uses placeholders unless INSTAGRAM_MCP_APP_ID/SECRET exists at runtime." +fi +rsync -a --delete "$IG_SRC/dist/" "$IG_STAGE/dist/" +cp "$IG_SRC/package.json" "$IG_STAGE/package.json" +[[ -f "$IG_SRC/package-lock.json" ]] && cp "$IG_SRC/package-lock.json" "$IG_STAGE/package-lock.json" +rsync -a --delete "$IG_SRC/node_modules/" "$IG_STAGE/node_modules/" +(cd "$IG_STAGE" && npm prune --omit=dev --no-audit --no-fund) +echo "[3d] instagram-mcp staged ($(du -sh "$IG_STAGE" 2>/dev/null | cut -f1 || echo '?'))" +echo "" + # Step 3c: Pre-build the webapp-template node_modules archive so first-app # create on a fresh user install decompresses (~3 s) instead of running a # live `npm install` (~22 s). The backend's _try_extract_bundled_archive