{"id":28487191,"url":"https://github.com/rehypejs/camomile","last_synced_at":"2025-07-01T17:30:37.264Z","repository":{"id":194971179,"uuid":"691974900","full_name":"rehypejs/camomile","owner":"rehypejs","description":"Node.js HTTP image proxy to route images through SSL","archived":false,"fork":false,"pushed_at":"2025-01-06T10:28:02.000Z","size":112,"stargazers_count":8,"open_issues_count":1,"forks_count":0,"subscribers_count":7,"default_branch":"main","last_synced_at":"2025-06-08T04:27:45.351Z","etag":null,"topics":["camo","camomile","image","node","proxy","rehype","security","ssl","unified"],"latest_commit_sha":null,"homepage":"https://unifiedjs.com","language":"JavaScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/rehypejs.png","metadata":{"files":{"readme":"readme.md","changelog":null,"contributing":null,"funding":null,"license":"license","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null},"funding":{"github":"unifiedjs","open_collective":"unified"}},"created_at":"2023-09-15T09:34:26.000Z","updated_at":"2025-01-06T10:28:06.000Z","dependencies_parsed_at":"2023-09-16T01:59:36.408Z","dependency_job_id":"ab969d46-2ac7-4650-bcb4-bf9a1afd4d50","html_url":"https://github.com/rehypejs/camomile","commit_stats":null,"previous_names":["rehypejs/camomile"],"tags_count":1,"template":false,"template_full_name":null,"purl":"pkg:github/rehypejs/camomile","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/rehypejs%2Fcamomile","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/rehypejs%2Fcamomile/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/rehypejs%2Fcamomile/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/rehypejs%2Fcamomile/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/rehypejs","download_url":"https://codeload.github.com/rehypejs/camomile/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/rehypejs%2Fcamomile/sbom","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":263007003,"owners_count":23398742,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["camo","camomile","image","node","proxy","rehype","security","ssl","unified"],"created_at":"2025-06-08T04:12:18.600Z","updated_at":"2025-07-01T17:30:37.230Z","avatar_url":"https://github.com/rehypejs.png","language":"JavaScript","funding_links":["https://github.com/sponsors/unifiedjs","https://opencollective.com/unified"],"categories":[],"sub_categories":[],"readme":"# camomile\n\n[![Build][badge-build-image]][badge-build-url]\n[![Coverage][badge-coverage-image]][badge-coverage-url]\n[![Downloads][badge-downloads-image]][badge-downloads-url]\n[![Sponsors][badge-funding-sponsors-image]][badge-funding-url]\n[![Backers][badge-funding-backers-image]][badge-funding-url]\n[![Chat][badge-chat-image]][badge-chat-url]\n\n**camomile** is a Node.js HTTP proxy to route images through SSL,\ncompatible with unified plugins,\nto safely embed user content on the web.\n\n## Contents\n\n* [What is this?](#what-is-this)\n* [When should I use this?](#when-should-i-use-this)\n* [Install](#install)\n* [Use](#use)\n* [API](#api)\n  * [`new Camomile(options)`](#new-camomileoptions)\n  * [`Options`](#options)\n* [Examples](#examples)\n  * [Example: integrate camomile into Express](#example-integrate-camomile-into-express)\n  * [Example: integrate camomile into Koa](#example-integrate-camomile-into-koa)\n  * [Example: integrate camomile into Fastify](#example-integrate-camomile-into-fastify)\n  * [Example: integrate camomile into Next.js](#example-integrate-camomile-into-nextjs)\n* [Compatibility](#compatibility)\n* [Contribute](#contribute)\n* [Acknowledgments](#acknowledgments)\n* [License](#license)\n\n## What is this?\n\nThis is a Node.js HTTP proxy to route images through SSL,\nintegrable in any Node.js server such as Express, Koa, Fastify, or Next.js.\n\ncamomile works together with [rehype-github-image][github-rehype-github-image],\nwhich does the following at build time:\n\n1. find all insecure HTTP image URLs in content\n2. generate [HMAC][wikipedia-hmac] signature of each URL\n3. replace the URL with a signed URL containing the encoded URL and HMAC\n\nWhen a user visits your app and views the content:\n\n1. their browser requests the URLs going to your server\n2. camomile validates the HMAC,\n   decodes the URL,\n   requests the content from the origin server without sensitive headers,\n   and streams it to the client\n\n## When should I use this?\n\nUse this when you want to embed user content on the web in a safe way.\nSometimes user content is served over HTTP,\nwhich is not secure:\n\n\u003e An HTTPS page that includes content fetched using cleartext HTTP is called a\n\u003e mixed content page.\n\u003e Pages like this are only partially encrypted,\n\u003e leaving the unencrypted content accessible to sniffers and man-in-the-middle\n\u003e attackers.\n\u003e\n\u003e — [MDN][mdn-mixed-content]\n\nThis also prevents information about your users leaking to other servers.\n\n## Install\n\nThis package is [ESM only][github-gist-esm].\nIn Node.js (version 18+),\ninstall with [npm][npm-install]:\n\n```sh\nnpm install camomile\n```\n\n## Use\n\n```js\nimport process from 'node:process'\nimport {Camomile} from 'camomile'\n\nconst secret = process.env.CAMOMILE_SECRET\n\nif (!secret) throw new Error('Missing `CAMOMILE_SECRET` in environment')\n\nconst server = new Camomile({secret})\n\nserver.listen({host: '127.0.0.1', port: 1080})\n```\n\n## API\n\nThis package exports the identifier\n[`Camomile`][api-camomile].\nIt exports the [TypeScript][] type\n[`Options`][api-options].\nThere is no default export.\n\n### `new Camomile(options)`\n\nCreate a new camomile server with options.\n\n###### Parameters\n\n* `options` ([`Options`][api-options], required)\n  — configuration\n\n###### Returns\n\nServer.\n\n### `Options`\n\nConfiguration (TypeScript type).\n\n###### Fields\n\n* `maxSize` (`number`, default: `100 * 1024 * 1024`)\n  — max size in bytes per resource to download;\n  a `413` is sent if the resource is larger than the maximum size\n* `secret` (`string`, **required**)\n  — HMAC key to decrypt the URLs and used by\n  [`rehype-github-image`][github-rehype-github-image]\n* `serverName` (`string`, default: `'camomile'`)\n  — server name sent in `Via`\n\n## Examples\n\n### Example: integrate camomile into Express\n\n```js\nimport process from 'node:process'\nimport {Camomile} from 'camomile'\nimport express from 'express'\n\nconst secret = process.env.CAMOMILE_SECRET\nif (!secret) throw new Error('Missing `CAMOMILE_SECRET` in environment')\n\nconst uploadApp = express()\nconst camomile = new Camomile({secret})\nuploadApp.all('*', camomile.handle.bind(camomile))\n\nconst host = '127.0.0.1'\nconst port = 1080\nconst app = express()\napp.use('/uploads', uploadApp)\napp.listen(port, host)\n\nconsole.log('Listening on `http://' + host + ':' + port + '/uploads/`')\n```\n\n### Example: integrate camomile into Koa\n\n```js\nimport process from 'node:process'\nimport {Camomile} from 'camomile'\nimport Koa from 'koa'\n\nconst secret = process.env.CAMOMILE_SECRET\nif (!secret) throw new Error('Missing `CAMOMILE_SECRET` in environment')\nconst camomile = new Camomile({secret})\n\nconst port = 1080\nconst app = new Koa()\n\napp.use(function (ctx, next) {\n  if (/^\\/files\\/.+/.test(ctx.path.toLowerCase())) {\n    return camomile.handle(ctx.req, ctx.res)\n  }\n\n  return next()\n})\n\napp.listen(port)\n```\n\n### Example: integrate camomile into Fastify\n\n```js\nimport process from 'node:process'\nimport {Camomile} from 'camomile'\nimport createFastify from 'fastify'\n\nconst secret = process.env.CAMOMILE_SECRET\nif (!secret) throw new Error('Missing `CAMOMILE_SECRET` in environment')\n\nconst fastify = createFastify({logger: true})\nconst camomile = new Camomile({secret})\n\n/**\n * Add `content-type` so fastify forewards without a parser to the leave body untouched.\n *\n * @see https://www.fastify.io/docs/latest/Reference/ContentTypeParser/\n */\nfastify.addContentTypeParser(\n  'application/offset+octet-stream',\n  function (request, payload, done) {\n    done(null)\n  }\n)\n\n/**\n * Use camomile to handle preparation and filehandling requests.\n * `.raw` gets the raw Node HTTP request and response objects.\n *\n * @see https://www.fastify.io/docs/latest/Reference/Request/\n * @see https://www.fastify.io/docs/latest/Reference/Reply/#raw\n */\nfastify.all('/files', function (request, response) {\n  camomile.handle(request.raw, response.raw)\n})\nfastify.all('/files/*', function (request, response) {\n  camomile.handle(request.raw, response.raw)\n})\n\nfastify.listen({port: 3000}, function (error) {\n  if (error) {\n    fastify.log.error(error)\n    process.exit(1)\n  }\n})\n```\n\n### Example: integrate camomile into Next.js\n\nAttach the camomile server handler to a Next.js route handler in an [optional catch-all route file](https://nextjs.org/docs/routing/dynamic-routes#optional-catch-all-routes)\n\n`/pages/api/upload/[[...file]].ts`\n\n```ts\nimport process from 'node:process'\nimport {Camomile} from 'camomile'\nimport type {NextApiRequest, NextApiResponse} from 'next'\n\nconst secret = process.env.CAMOMILE_SECRET\nif (!secret) throw new Error('Missing `CAMOMILE_SECRET` in environment')\n\n/**\n * Important: this tells Next.js not to parse the body, as camomile requires\n * @see https://nextjs.org/docs/api-routes/request-helpers\n */\nexport const config = {api: {bodyParser: false}}\n\nconst camomile = new Camomile({secret})\n\nexport default async function handler(\n  request: NextApiRequest,\n  response: NextApiResponse\n) {\n  return camomile.handle(request, response)\n}\n```\n\n## Compatibility\n\nProjects maintained by the unified collective are compatible with maintained\nversions of Node.js.\n\nWhen we cut a new major release,\nwe drop support for unmaintained versions of Node.\nThis means we try to keep the current release line,\n`camomile@^1`,\ncompatible with Node.js 18.\n\n## Contribute\n\nSee [`contributing.md`][github-dotfiles-contributing] in\n[`rehypejs/.github`][github-dotfiles-health] for ways\nto get started.\nSee [`support.md`][github-dotfiles-support] for ways to get help.\n\nThis project has a [code of conduct][github-dotfiles-coc].\nBy interacting with this repository, organization, or community you agree to\nabide by its terms.\n\nFor info on how to submit a security report,\nsee our [security policy][github-dotfiles-security].\n\n## Acknowledgments\n\nIn 2010 GitHub introduced [camo][github-atmos-camo],\na similar server in CoffeeScript,\nwhich is now deprecated and in public archive.\nThis project is a spiritual successor to `camo`.\n\nA lot of inspiration was also taken from [`go-camo`][github-cactus-camo],\nwhich is a modern and maintained image proxy in Go.\n\nThanks to [**@kytta**][github-kytta] for the npm package name `camomile`!\n\n## License\n\n[MIT][file-license] © [Merlijn Vos][github-murderlon]\n\n\u003c!-- Definitions --\u003e\n\n[api-camomile]: #new-camomileoptions\n\n[api-options]: #options\n\n[badge-build-image]: https://github.com/rehypejs/camomile/actions/workflows/main.yml/badge.svg\n\n[badge-build-url]: https://github.com/rehypejs/camomile/actions\n\n[badge-chat-image]: https://img.shields.io/badge/chat-discussions-success.svg\n\n[badge-chat-url]: https://github.com/rehypejs/rehype/discussions\n\n[badge-coverage-image]: https://img.shields.io/codecov/c/github/rehypejs/camomile.svg\n\n[badge-coverage-url]: https://codecov.io/github/rehypejs/camomile\n\n[badge-downloads-image]: https://img.shields.io/npm/dm/dead-or-alive.svg\n\n[badge-downloads-url]: https://www.npmjs.com/package/dead-or-alive\n\n[badge-funding-backers-image]: https://opencollective.com/unified/backers/badge.svg\n\n[badge-funding-sponsors-image]: https://opencollective.com/unified/sponsors/badge.svg\n\n[badge-funding-url]: https://opencollective.com/unified\n\n[file-license]: license\n\n[github-atmos-camo]: https://github.com/atmos/camo\n\n[github-cactus-camo]: https://github.com/cactus/go-camo\n\n[github-dotfiles-coc]: https://github.com/rehypejs/.github/blob/main/code-of-conduct.md\n\n[github-dotfiles-contributing]: https://github.com/rehypejs/.github/blob/main/contributing.md\n\n[github-dotfiles-health]: https://github.com/rehypejs/.github\n\n[github-dotfiles-security]: https://github.com/rehypejs/.github/blob/main/security.md\n\n[github-dotfiles-support]: https://github.com/rehypejs/.github/blob/main/support.md\n\n[github-gist-esm]: https://gist.github.com/sindresorhus/a39789f98801d908bbc7ff3ecc99d99c\n\n[github-kytta]: https://github.com/kytta\n\n[github-murderlon]: https://github.com/Murderlon\n\n[github-rehype-github-image]: https://github.com/rehypejs/rehype-github/tree/main/packages/image\n\n[mdn-mixed-content]: https://developer.mozilla.org/en-US/docs/Web/Security/Mixed_content\n\n[npm-install]: https://docs.npmjs.com/cli/install\n\n[typescript]: https://www.typescriptlang.org\n\n[wikipedia-hmac]: https://en.wikipedia.org/wiki/HMAC\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Frehypejs%2Fcamomile","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Frehypejs%2Fcamomile","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Frehypejs%2Fcamomile/lists"}