{"_id":"@aitofy/bugdeck-server","_rev":"2-b370e803d657aa6d7c0c99e80f1068a5","name":"@aitofy/bugdeck-server","dist-tags":{"latest":"0.2.0"},"versions":{"0.1.0":{"name":"@aitofy/bugdeck-server","version":"0.1.0","keywords":["marker-io-alternative","bugherd-alternative","jam-dev-alternative","bug-reporting","feedback-api","self-hosted","plane","plane-so","hono","sqlite"],"author":{"name":"aitofy"},"license":"MIT","_id":"@aitofy/bugdeck-server@0.1.0","maintainers":[{"name":"masterpk","email":"huanthuyon671@gmail.com"}],"homepage":"https://github.com/aitofy-dev/bugdeck/tree/main/packages/server#readme","bugs":{"url":"https://github.com/aitofy-dev/bugdeck/issues"},"bin":{"bugdeck-server":"dist/bin.js"},"dist":{"shasum":"2b1f9c462cfb2cfc66b460587c0714870b2698af","tarball":"https://registry.npmjs.org/@aitofy/bugdeck-server/-/bugdeck-server-0.1.0.tgz","fileCount":35,"integrity":"sha512-ntmd9OcQIbLCuPm82uW7igtKR9FEm++WJMHgBYmb5hqmgXmjPGzIGPTeN/IWp6WPjsC7x2f9sR2YhQd2/lpreA==","signatures":[{"sig":"MEUCIQDVXLy5fpPYAU2WBcR1Vggp0wZ7FO+D2XCIbvitXTpHcQIgEUYnYE2W8ZtOpbHkYXR8ovxeJwfXJrzGOUmOhY8ygFY=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":70366},"main":"./dist/index.js","type":"module","_from":"file:aitofy-bugdeck-server-0.1.0.tgz","types":"./dist/index.d.ts","engines":{"node":">=20"},"exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js"}},"scripts":{"test":"tsx --test src/__tests__/*.test.ts","build":"tsc -p tsconfig.build.json","serve":"tsx src/bin.ts","typecheck":"tsc --noEmit"},"_npmUser":{"name":"masterpk","email":"huanthuyon671@gmail.com"},"_resolved":"/private/var/folders/kn/9vdzgqzj7f96007cb3tyr2j40000gn/T/194941d5d312e4ea4f8b99eed31e2310/aitofy-bugdeck-server-0.1.0.tgz","_integrity":"sha512-ntmd9OcQIbLCuPm82uW7igtKR9FEm++WJMHgBYmb5hqmgXmjPGzIGPTeN/IWp6WPjsC7x2f9sR2YhQd2/lpreA==","repository":{"url":"git+https://github.com/aitofy-dev/bugdeck.git","type":"git","directory":"packages/server"},"_npmVersion":"10.9.0","description":"Self-hosted bug-report API for bugdeck: stores reports and files them into Plane, GitHub or Linear.","directories":{},"sideEffects":false,"_nodeVersion":"22.11.0","dependencies":{"hono":"^4.6.14","sharp":"^0.33.5","better-sqlite3":"^11.7.0","@hono/node-server":"^1.13.7","@aitofy/bugdeck-core":"0.1.0"},"publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"tsx":"^4.19.2","typescript":"^5.7.2","@types/node":"^22.10.2","@types/better-sqlite3":"^7.6.12"},"_npmOperationalInternal":{"tmp":"tmp/bugdeck-server_0.1.0_1789161415860_0.43502098051460303","host":"s3://npm-registry-packages-npm-production"}},"0.2.0":{"name":"@aitofy/bugdeck-server","version":"0.2.0","description":"Self-hosted bug-report API for bugdeck: stores reports and files them into Plane or GitHub.","type":"module","main":"./dist/index.js","types":"./dist/index.d.ts","exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js"}},"bin":{"bugdeck-server":"dist/bin.js"},"dependencies":{"@hono/node-server":"^1.13.7","better-sqlite3":"^11.7.0","hono":"^4.6.14","sharp":"^0.33.5","@aitofy/bugdeck-core":"0.2.0"},"devDependencies":{"@types/better-sqlite3":"^7.6.12","@types/node":"^22.10.2","tsx":"^4.19.2","typescript":"^5.7.2"},"keywords":["marker-io-alternative","bugherd-alternative","jam-dev-alternative","bug-reporting","feedback-api","self-hosted","plane","plane-so","hono","sqlite"],"engines":{"node":">=20"},"license":"MIT","sideEffects":false,"repository":{"type":"git","url":"git+https://github.com/aitofy-dev/bugdeck.git","directory":"packages/server"},"homepage":"https://github.com/aitofy-dev/bugdeck/tree/main/packages/server#readme","bugs":{"url":"https://github.com/aitofy-dev/bugdeck/issues"},"author":{"name":"aitofy"},"publishConfig":{"access":"public"},"scripts":{"build":"tsc -p tsconfig.build.json","typecheck":"tsc --noEmit","test":"tsx --test src/__tests__/*.test.ts","serve":"tsx src/bin.ts"},"_id":"@aitofy/bugdeck-server@0.2.0","_integrity":"sha512-i2lPzttx99mMy01yuQfjEmyfUjjxhuJMwLlA+LCmIb6a0aRiRvCrklVHNaHVdxj6zBCBOTBTsAKEH9IwJL9YAA==","_resolved":"/private/var/folders/kn/9vdzgqzj7f96007cb3tyr2j40000gn/T/3ebf6b1a952dd7018cbb0e6d9ccf87b2/aitofy-bugdeck-server-0.2.0.tgz","_from":"file:aitofy-bugdeck-server-0.2.0.tgz","_nodeVersion":"22.11.0","_npmVersion":"10.9.0","dist":{"integrity":"sha512-i2lPzttx99mMy01yuQfjEmyfUjjxhuJMwLlA+LCmIb6a0aRiRvCrklVHNaHVdxj6zBCBOTBTsAKEH9IwJL9YAA==","shasum":"c3f10285e58fd9e10b92a925d57208d6e09df3c8","tarball":"https://registry.npmjs.org/@aitofy/bugdeck-server/-/bugdeck-server-0.2.0.tgz","fileCount":42,"unpackedSize":114999,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEYCIQCUKBlqTwb1IhEc2qkGFkOJbQV3EyyQ/mYHkXL/0Ls3NgIhALTqUwCOcLz8fkKK8UzoxhGmz8IA5svybYX6/GxKoaBV"}]},"_npmUser":{"name":"masterpk","email":"huanthuyon671@gmail.com"},"directories":{},"maintainers":[{"name":"masterpk","email":"huanthuyon671@gmail.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/bugdeck-server_0.2.0_1789163905444_0.21880276793193176"},"_hasShrinkwrap":false}},"time":{"created":"2026-09-11T21:16:55.691Z","modified":"2026-09-11T21:58:25.853Z","0.1.0":"2026-09-11T21:16:55.994Z","0.2.0":"2026-09-11T21:58:25.619Z"},"bugs":{"url":"https://github.com/aitofy-dev/bugdeck/issues"},"author":{"name":"aitofy"},"license":"MIT","homepage":"https://github.com/aitofy-dev/bugdeck/tree/main/packages/server#readme","keywords":["marker-io-alternative","bugherd-alternative","jam-dev-alternative","bug-reporting","feedback-api","self-hosted","plane","plane-so","hono","sqlite"],"repository":{"type":"git","url":"git+https://github.com/aitofy-dev/bugdeck.git","directory":"packages/server"},"description":"Self-hosted bug-report API for bugdeck: stores reports and files them into Plane or GitHub.","maintainers":[{"name":"masterpk","email":"huanthuyon671@gmail.com"}],"readme":"# @aitofy/bugdeck-server\n\nThe HTTP half of bugdeck: it stores bug reports filed by\nthe widget and files them into your issue tracker. Self-hosted, SQLite by default, no telemetry.\n\n```bash\npnpm add @aitofy/bugdeck-server @aitofy/bugdeck-core\n```\n\n## 30 seconds\n\n```ts\nimport { serve } from '@hono/node-server';\nimport { createPlaneTracker } from '@aitofy/bugdeck-core';\nimport { createFeedbackApp, createSqliteStore } from '@aitofy/bugdeck-server';\n\nconst app = createFeedbackApp({\n  tracker: createPlaneTracker({\n    baseUrl: process.env.PLANE_BASE_URL!,\n    apiKey: process.env.PLANE_API_KEY!,\n    workspaceSlug: process.env.PLANE_WORKSPACE_SLUG!,\n    projectId: process.env.PLANE_PROJECT_ID!,\n  }),\n  store: createSqliteStore({ storagePath: './storage' }),\n  // Your auth, your answer. `null` is a 401.\n  resolveUser: async (request) => mySession(request.headers.get('cookie')),\n});\n\nserve({ fetch: app.fetch, port: 3131 });\n```\n\nThe app is a plain [Hono](https://hono.dev) instance, so it mounts anywhere:\n\n```ts\nmyHonoApp.route('/feedback', app);\n```\n\n### Express\n\n```ts\nimport express from 'express';\nimport { getRequestListener } from '@hono/node-server';\n\nconst server = express();\nserver.use('/feedback', getRequestListener(app.fetch));\n```\n\nExpress strips the mount path before the handler sees it, so the routes below line up as-is.\n\n## Routes\n\n| Route | What it does |\n|-------|--------------|\n| `POST /reports` | multipart: `description`, `context` (JSON), `blocks` (JSON, optional) and up to 10 screenshots under a repeated `images` key. Every image is re-encoded before anything is stored. |\n| `PATCH /reports/:id` | the same body again, rewriting the report. Allowed **only while `state` is `pending`** — once someone has picked it up the answer is `409 {\"error\":\"NOT_PENDING\"}` and the way to add something is a comment. Images the report already has travel as `{\"kind\":\"image\",\"assetId\":\"…\"}` blocks instead of being uploaded again; `imageIndex` counts only the files actually in this body. |\n| `POST /reports/:id/comment` | the same body as a message on the conversation. Allowed in **every** state, `done` and `fail` included — \"it is still broken\" always arrives after the item was closed. Capped at 20 messages from the user per report. |\n| `GET /reports/mine` | the caller's own reports, newest first, without their conversations. `?limit=` caps at 50. |\n| `GET /reports/:id` | one report, with the conversation folded in. Someone else's report is a 404. |\n| `GET /assets/:id` | the PNG bytes of one screenshot, `nosniff` and `inline`. Only the owner of the parent report may read it. |\n\nEvery 4xx answers with a **code, never a sentence** — `{\"error\":\"TOO_MANY_IMAGES\"}`. The widget owns\nthe wording so it can be translated: `UNAUTHENTICATED`, `BAD_REQUEST`, `MISSING_DESCRIPTION`,\n`TOO_MANY_IMAGES`, `IMAGE_TOO_LARGE`, `UNSUPPORTED_IMAGE`, `NOT_PENDING`, `NOT_FOUND`,\n`RATE_LIMITED`. The three write routes share one budget of 10 requests per hour per user.\n\nAfter the 201 the report is queued onto the tracker, off the request: a board being slow or down\nnever turns a filed bug into a 500. The create is idempotent on `externalSource=bugdeck` plus the\nreport id, so a retry adopts the issue instead of duplicating it. Edits and comments ride the same\nqueue, so a message written while the issue was still being retried is posted the moment it exists\nrather than lost — each one is marked with the tracker's comment id, which is what keeps a retry\nfrom saying it twice.\n\nA tracker that can rewrite an issue says so with an optional `updateIssue` on `IssueTracker`; both\nadapters have one. A tracker without it keeps the text the report was filed with, and the edit stays\nlocal.\n\n## Standalone\n\n```bash\ncp .env.example .env   # fill in the tracker values\ndocker compose up -d\n```\n\nOr without Docker: `npx bugdeck-server`, with the same environment.\n\nOn Plane, startup resolves your board's columns and logs the map. A project with no column for one\nof the five states refuses to boot — reports that file fine and never come back are worse than a\nserver that will not start. GitHub has open and closed and needs no such promise, so it costs no\nrequest at startup.\n\n| Variable | Required | Default | What it is |\n|----------|----------|---------|------------|\n| `TRACKER` | no | `plane` | `plane` or `github`. Decides which block below is required. |\n| `STORAGE_PATH` | no | `./storage` | Holds `reports.db` and `assets/`. Mount a volume at it. |\n| `PORT` | no | `3131` | |\n| `PUBLIC_URL` | no | — | How the outside world reaches this server; used for asset links on the issue. |\n| `POLL_INTERVAL` | no | `300` | Seconds between reads of the tracker. `0` turns the poller off. |\n| `PUBLIC_REPLY_MARKER` | no | `@user` | The prefix that makes a comment visible to the reporter. |\n| `AUTH_MODE` | no | `header` | See below. |\n| `CORS_ORIGIN` | no | — | The origin the widget is served from. Unset means no CORS headers. |\n\n### `TRACKER=plane`\n\n| Variable | Required | Default | What it is |\n|----------|----------|---------|------------|\n| `PLANE_BASE_URL` | **yes** | — | Your Plane instance, e.g. `https://plane.example.com`. |\n| `PLANE_API_KEY` | **yes** | — | Workspace API key. |\n| `PLANE_WORKSPACE_SLUG` | **yes** | — | The slug in your Plane URLs. |\n| `PLANE_PROJECT_ID` | **yes** | — | The project reports are filed into. |\n| `PLANE_LEGACY_PROJECT_IDS` | no | — | Comma-separated projects the poller also reads. Never written to. |\n| `PLANE_STATE_MAP` | no | discovered | JSON, state → Plane state id. Only for a board that does not follow the convention. |\n\n### `TRACKER=github`\n\n| Variable | Required | Default | What it is |\n|----------|----------|---------|------------|\n| `GITHUB_OWNER` | **yes** | — | The user or organisation that owns the repo. |\n| `GITHUB_REPO` | **yes** | — | The repo issues are filed into. |\n| `GITHUB_TOKEN` | **yes** | — | PAT or app installation token with `issues: write` on that repo. |\n| `GITHUB_LABELS` | no | — | Comma-separated labels put on every issue we file, and the filter the poller reads back with. |\n| `GITHUB_API_URL` | no | `https://api.github.com` | For GitHub Enterprise Server. |\n\nGitHub has no attachment API, so every screenshot on an issue is a link back to `GET /assets/:id`\non this server — set `PUBLIC_URL` or they go unmentioned. Issues we filed are recognised by a\nhidden marker in the body, which is also the dedupe: the poller ignores issues the repo's humans\nfiled themselves.\n\n### Replying to the reporter\n\nThe poller reads the tracker every `POLL_INTERVAL` seconds and carries two things back: the state\n(your column, or open/closed on GitHub) and your reply.\n\n**A comment starting with `@user` is shown to the reporter. Every other comment stays internal.**\nThat is the whole rule, and it is opt-in on purpose — comments on an issue routinely name other\npeople's accounts, and nothing without the marker may ever reach a widget.\n\n```text\n@user Fixed in 1.4.2, please reload the page.   → the reporter sees this\nask the payments team whether it repeats        → the reporter never sees this\n```\n\nThe marker is stripped before the reporter sees the text, the reply is added to the report's\nconversation once (a second pass over the same comment adds nothing), and `publicReply` is whatever\nyou said last. Rename the marker with `PUBLIC_REPLY_MARKER`.\n\nMounting the app yourself? The poller is a separate object, so wire it up next to the app:\n\n```ts\nimport { createSyncWorker } from '@aitofy/bugdeck-server';\n\nconst worker = createSyncWorker({\n  tracker,\n  store,\n  intervalMs: 300_000,\n  onStateChange: (report, state) => notify(report.ownerId, `#${report.code} is now ${state}`),\n  onReply: (report, reply) => notify(report.ownerId, reply),\n});\nworker.start();\n```\n\n`serve()` takes the same two hooks as options, and nothing else in this package sends a\nnotification: the host owns the bell.\n\n### `AUTH_MODE=header` is not authentication\n\nThe standalone server trusts `X-User-Id` and `X-User-Email`. **Run it behind your own\nauthenticating proxy.** Exposed directly to the internet, anyone can read anyone's reports by typing\na header. It is the default because every host already has auth and none of them want a second one —\nand when you mount `createFeedbackApp` in your own server you pass your own `resolveUser` and this\nmode never runs.\n\n```bash\ncurl -X POST http://localhost:3131/reports \\\n  -H 'X-User-Id: user-1' \\\n  -H 'X-User-Email: reporter@example.com' \\\n  -F 'description=The export button does nothing. I tried twice.' \\\n  -F 'context={\"url\":\"https://app.example.com\",\"viewport\":{\"width\":1280,\"height\":720},\"userAgent\":\"curl\"}' \\\n  -F 'images=@screenshot.png'\n```\n\n```json\n{ \"id\": \"b1d1…\", \"title\": \"The export button does nothing\", \"state\": \"pending\", \"assetIds\": [\"…\"] }\n```\n\n`code` (`PROJ-12`) appears once the tracker has accepted the issue — a second later, on\n`GET /reports/:id`.\n\n## Another tracker\n\n`tracker` is any `IssueTracker` from `@aitofy/bugdeck-core` — two required methods, the rest\noptional. An adapter that renders its own markup (Plane inlines images with its own element)\nsupplies `renderBody` and `renderComment`; one that does not gets the plain HTML renderers in core,\nwhich is why nothing in this package imports an adapter. `updateIssue` is how an edit reaches the\nissue, and `listUpdates` is what the poller needs — an adapter with neither still files reports.\n\n## Storage\n\n`createSqliteStore` keeps the report in SQLite (WAL, schema migrated on open) and the pixels as\nfiles under `${STORAGE_PATH}/assets/`. A 10 MB blob per row turns every query into a file copy;\non disk they are files an operator can count, rsync and delete.\n\nAlready have a database? Implement `FeedbackStore` — eleven methods, two of them the poll\nworker's watermark — and pass it as `store`.\n`createMemoryStore()` ships too, for tests and for trying the API before deciding.\n\n## License\n\nMIT\n","readmeFilename":"README.md"}