{"package":"@executor.sh/miro","files":[{"path":"README.md","content":"# Miro\n\nMiro boards and everything on them: boards and members, items (sticky notes, shapes, cards, text, images, documents, frames, embeds), connectors, tags, groups, mind maps, board exports, teams, and organization administration.\n\n## Interface\n\nOpenAPI, from Miro's official specification for the REST API v2 in [miroapp/api-clients](https://github.com/miroapp/api-clients/blob/39161f97cee3f887f5f67cf4a003ce93b3a6903b/packages/generator/spec.json), pinned to commit `39161f9` (the head of `main` on 2026-10-12) so the tool surface only changes with this package. Bump the SHA in `openapi.ts` to pick up new endpoints. All operations are served from `https://api.miro.com`. Every mutation asks for approval.\n\n## Auth\n\n- **Access token** (`token`): create an app at [miro.com/app/settings/user-profile/apps](https://miro.com/app/settings/user-profile/apps), open it and use \"Install app and get OAuth token\" to get a token for your own team. Paste it into Executor.\n- **OAuth 2.0** (`oauth`): use the same Miro app. In its settings add the redirect URI shown on Executor's connect page, then paste the client id and client secret there. Scopes requested: `boards:read`, `boards:write`, `organizations:read`, `organizations:teams:read`, `organizations:teams:write`. The organization scopes only work for Enterprise plans.\n\n## Known gaps\n\n- Enterprise-only endpoints (organizations, teams, board export, legal hold) fail on other plans.\n- The spec's SCIM provisioning paths (`/Users`, `/Groups`, `/ServiceProviderConfig`, `/ResourceTypes`, `/Schemas`) are removed: they need a SCIM credential, not an OAuth or app token. The Miro SCIM API (`https://miro.com/api`, API key) is a separate Enterprise product and is not covered.\n- `DELETE /v2/boards/{board_id}/groups/{group_id}?` (groups.deleteGroup) is skipped (`operation_path`) because the spec path has a stray trailing `?`; use the ungrouping endpoints instead.\n- `POST /v1/oauth/revoke` is removed: Miro's document calls it vulnerable and deprecated because the token goes in the URL. Use `POST /v2/oauth/revoke`. It sends no account credential: the token to revoke and its client ID and secret go in the request body.\n- Miro's Web SDK and the Miro MCP server (`https://mcp.miro.com/`) are not used.\n"},{"path":"icon.ts","content":"import type { AppIcon } from \"apps\";\n\n// Generated by scripts/icon.ts from the vendor's logo.\nexport const icons: readonly AppIcon[] = [\n  {\n    \"src\": \"data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz4KPHN2ZyB3aWR0aD0iMjU2cHgiIGhlaWdodD0iMjU2cHgiIHZpZXdCb3g9IjAgMCAyNTYgMjU2IiB2ZXJzaW9uPSIxLjEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgcHJlc2VydmVBc3BlY3RSYXRpbz0ieE1pZFlNaWQiPgogICAgPHRpdGxlPk1pcm88L3RpdGxlPgogICAgPGc+CiAgICAgICAgPHBhdGggZD0iTTAsNjQgQzAsMjguNjUzNzYgMjguNjUzNzYsMCA2NCwwIEwxOTIsMCBDMjI3LjM0NjQsMCAyNTYsMjguNjUzNzYgMjU2LDY0IEwyNTYsMTkyIEMyNTYsMjI3LjM0NjQgMjI3LjM0NjQsMjU2IDE5MiwyNTYgTDY0LDI1NiBDMjguNjUzNzYsMjU2IDAsMjI3LjM0NjQgMCwxOTIgTDAsNjQgWiIgZmlsbD0iI0ZGREQzMyI+PC9wYXRoPgogICAgICAgIDxwb2x5Z29uIGZpbGw9IiMwMDAwMDAiIHBvaW50cz0iMTcwLjE5NTIgNDguOCAxNDYuOTU2IDQ4LjggMTY2LjMyMTYgODIuODI1NiAxMjMuNzE2OCA0OC44IDEwMC40Nzg0IDQ4LjggMTIxLjc4MDggOTAuMzg4IDc3LjIzOTA0IDQ4LjggNTQgNDguOCA3Ny4yMzkwNCAxMDEuNzM2OCA1NCAyMDcuNiA3Ny4yMzkwNCAyMDcuNiAxMjEuNzgwOCA5NC4xNzQ0IDEwMC40Nzg0IDIwNy42IDEyMy43MTY4IDIwNy42IDE2Ni4zMjE2IDg2LjYxMiAxNDYuOTU2IDIwNy42IDE3MC4xOTUyIDIwNy42IDIxMi44IDc1LjI2MzQ0Ij48L3BvbHlnb24+CiAgICA8L2c+Cjwvc3ZnPgo=\",\n    \"mimeType\": \"image/svg+xml\"\n  }\n];\n"},{"path":"index.ts","content":"import {\n  accountRouter,\n  decodeJson,\n  defineApp,\n  defineProvider,\n  oauth2,\n  object,\n  router,\n  secrets,\n  string,\n  withApprovals,\n} from \"apps\";\nimport { always } from \"apps/operations/approval\";\nimport { liveOpenapiRouter } from \"apps/openapi\";\nimport { icons } from \"./icon.js\";\nimport { options } from \"./openapi.js\";\n\nconst TokenContext = object({ user: object({ id: string(), name: string() }) });\n\nconst miro = defineProvider({\n  name: \"Miro\",\n  hosts: [\"api.miro.com\"],\n  auth: {\n    token: secrets({\n      label: \"Access token\",\n      fields: object({ token: string({ minLength: 1 }) }),\n    }),\n    // Miro has no client registration: the connect page asks for an app's client id and secret.\n    oauth: oauth2({\n      authorizationUrl: \"https://miro.com/oauth/authorize\",\n      tokenUrl: \"https://api.miro.com/v1/oauth/token\",\n      tokenEndpointAuthMethod: \"client_secret_post\",\n      scopes: [\n        \"boards:read\",\n        \"boards:write\",\n        \"organizations:read\",\n        \"organizations:teams:read\",\n        \"organizations:teams:write\",\n      ],\n    }),\n  },\n  async health({ account, fetch, signal }) {\n    const token = account.method === \"oauth\" ? account.fields.access_token : account.fields.token;\n    const response = await fetch(\"https://api.miro.com/v1/oauth-token\", {\n      signal,\n      headers: { Authorization: `Bearer ${token}`, Accept: \"application/json\" },\n    });\n    const context = await decodeJson(response, TokenContext);\n    return {\n      accountInfo: { externalId: context.user.id, username: context.user.name },\n    };\n  },\n});\n\nexport default defineApp(\n  { user: { accounts: { miro: miro.many() } }, icons },\n  async ({ user, cache, fetch, signal }) => ({\n    tools: router(\n      await accountRouter(\n        user?.accounts.miro ?? [],\n        (account) =>\n          withApprovals(liveOpenapiRouter({ ...options, cache, fetch, signal, account }), (tool) =>\n            tool.kind === \"mutation\" ? always() : undefined,\n          ),\n        { signal },\n      ),\n      {\n        title: \"Miro\",\n        description:\n          \"Miro boards, items (sticky notes, shapes, cards, text, images, frames), connectors, tags, board members, teams and organization administration through the Miro REST API v2.\",\n        icons,\n      },\n    ),\n  }),\n);\n"},{"path":"openapi.ts","content":"import type { OpenapiToolNamesOptions } from \"apps/openapi\";\n\n// SCIM provisioning paths (/Users, /Groups, /ServiceProviderConfig, /ResourceTypes, /Schemas) need a\n// SCIM credential, not an OAuth or app token, so they are removed.\nconst scimPaths = [\"Users\", \"Users/{id}\", \"Groups\", \"Groups/{id}\", \"ServiceProviderConfig\", \"ResourceTypes\", \"ResourceTypes/{resource}\", \"Schemas\", \"Schemas/{uri}\"];\n\n// The spec declares one oauth2 scheme; both an access token and OAuth fill it as a bearer token.\nexport const options = {\n  source: {\n    url: \"https://raw.githubusercontent.com/miroapp/api-clients/39161f97cee3f887f5f67cf4a003ce93b3a6903b/packages/generator/spec.json\",\n  },\n  allowedOrigin: \"https://api.miro.com\",\n  securitySchemes: { oAuth2AuthCode: { type: \"http\", scheme: \"bearer\" } },\n  // The document declares no security; send the account's credential on every operation.\n  fallbackSecurity: [{ oAuth2AuthCode: [] }],\n  methods: {\n    token: [{ scheme: \"oAuth2AuthCode\", field: \"token\", part: \"value\", prefix: \"\" }],\n    oauth: [{ scheme: \"oAuth2AuthCode\", field: \"access_token\", part: \"value\", prefix: \"\" }],\n  },\n  oauth: [],\n  patches: [\n    ...scimPaths.map((p) => ({\n      op: \"remove\" as const,\n      path: `/paths/${(\"/\" + p).replace(/~/g, \"~0\").replace(/\\//g, \"~1\")}`,\n    })),\n    // App metrics take an app management API token from Developer Hub, which neither method holds, so they get no\n    // credential (catalog/status/miro.json \"credentialFree\").\n    { op: \"add\" as const, path: \"/paths/~1v2-experimental~1apps~1{app_id}~1metrics/get/security\", value: [] },\n    { op: \"add\" as const, path: \"/paths/~1v2-experimental~1apps~1{app_id}~1metrics-total/get/security\", value: [] },\n    // The v2 revoke takes the token to revoke and its client's ID and secret in the body, so it gets no credential\n    // (catalog/status/miro.json \"credentialFree\").\n    { op: \"add\" as const, path: \"/paths/~1v2~1oauth~1revoke/post/security\", value: [] },\n    // Miro calls the v1 revoke vulnerable and deprecated (it takes the token in the URL) and points to /v2/oauth/revoke.\n    { op: \"remove\" as const, path: \"/paths/~1v1~1oauth~1revoke\" },\n  ],\n} satisfies OpenapiToolNamesOptions;\n"},{"path":"package.json","content":"{\n  \"name\": \"@executor.sh/miro\",\n  \"description\": \"Miro boards, items, sticky notes, shapes, connectors, tags, members, teams and organization APIs.\",\n  \"private\": true,\n  \"type\": \"module\",\n  \"executor\": {\n    \"title\": \"Miro\"\n  },\n  \"dependencies\": {\n    \"apps\": \"0.0.1-beta.100\"\n  }\n}\n"}]}