{"id":19926611,"url":"https://github.com/microlinkhq/openkey","last_synced_at":"2025-05-03T08:32:23.489Z","repository":{"id":238253286,"uuid":"439589022","full_name":"microlinkhq/openkey","owner":"microlinkhq","description":"Fast authentication layer for your SaaS, backed by Redis.","archived":false,"fork":false,"pushed_at":"2025-05-01T18:53:18.000Z","size":3968,"stargazers_count":8,"open_issues_count":2,"forks_count":0,"subscribers_count":5,"default_branch":"master","last_synced_at":"2025-05-01T18:53:37.902Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":"https://openkey.js.org","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/microlinkhq.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE.md","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}},"created_at":"2021-12-18T10:42:02.000Z","updated_at":"2025-05-01T18:42:28.000Z","dependencies_parsed_at":"2024-05-28T10:31:24.423Z","dependency_job_id":null,"html_url":"https://github.com/microlinkhq/openkey","commit_stats":null,"previous_names":["microlinkhq/openkey"],"tags_count":11,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/microlinkhq%2Fopenkey","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/microlinkhq%2Fopenkey/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/microlinkhq%2Fopenkey/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/microlinkhq%2Fopenkey/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/microlinkhq","download_url":"https://codeload.github.com/microlinkhq/openkey/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":252162537,"owners_count":21704270,"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":[],"created_at":"2024-11-12T22:29:54.415Z","updated_at":"2025-05-03T08:32:23.476Z","avatar_url":"https://github.com/microlinkhq.png","language":"JavaScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"\u003cdiv align=\"center\"\u003e\n  \u003cbr\u003e\n  \u003cimg\n    id=\"heading\"\n    src=\"https://raw.githubusercontent.com/microlinkhq/openkey/0bc4adb9e23583d5d373c1692c7b28358d18c7f8/static/images/head.png\"\n    alt=\"openkey\"\n    style=\"width: 350px;\"\n  \u003e\n  \u003ch6 id=\"subhead\"\u003e\n    A scalable, cost-efficient, and high-performance authentication service\n  \u003c/h6\u003e\n  \u003cbr\u003e\n  \u003cp id=\"links\"\u003e\n    \u003cimg\n      src=\"https://img.shields.io/github/tag/microlinkhq/openkey.svg?style=flat-square\"\n      alt=\"Last version\"\n    \u003e\n    \u003ca\n      href=\"https://coveralls.io/github/microlinkhq/openkey\"\n      target=\"_blank\"\n      rel=\"noopener noreferrer\"\n      class=\"no-external-icon\"\n    \u003e\n      \u003cimg\n        src=\"https://img.shields.io/coveralls/microlinkhq/openkey.svg?style=flat-square\"\n        alt=\"Coverage Status\"\n      \u003e\n    \u003c/a\u003e\n    \u003ca\n      href=\"https://www.npmjs.org/package/openkey\"\n      target=\"_blank\"\n      rel=\"noopener noreferrer\"\n      class=\"no-external-icon\"\n    \u003e\n      \u003cimg\n        src=\"https://img.shields.io/npm/dm/openkey.svg?style=flat-square\"\n        alt=\"NPM Status\"\n      \u003e\n    \u003c/a\u003e\n  \u003c/p\u003e\n\u003c/div\u003e\n\u003cbr\u003e\n\nTired of not owning the authentication flow of your SaaS?\n\n**openkey** is a ~200 lines backed in Redis for control authentication flow. You can deploy to any cloud provider, no vendor lock-in, being cheap at any scale \u0026 focused in performance to authenticate requests.\n\n# Installation\n\nFirst, Install **openkey** from your preferred node package manager:\n\n```sh\npnpm install openkey\n```\n\n# CLI\n\n**openkey** is also available as CLI when you install it globally in your system:\n\n```sh\nnpm install -g openkey\n```\n\nAfter that, you can access to any command from your terminal:\n\n```sh\n❯ openkey\nopenkey\u003e help\nversion exit keys plans usage stats\nopenkey\u003e version\n1.0.0\nopenkey\u003e exit\n```\n\n# Usage\n\nAfter installation, initialize **openkey**:\n\n```js\nconst Redis = require('ioredis')\nconst redis = new Redis()\nconst openkey = require('openkey')({ redis })\n```\n\nyou can prepend all the keys by passing `prefix`:\n\n```js\nconst openkey = require('openkey')({ redis, prefix: 'http:' })\n```\n\nThese will allow you to acess to **openkey** core concepts: **plans**, **keys**, **usage**, and **stats**.\n\n## .plans\n\nIt represents the quota, rate limit, and throttle information specified as a plan.\n\n### .create\n\nIt creates a new plan:\n\n```js\nconst plan = await openkey.plans.create({\n  name: 'free tier',\n  metadata: { tier: 'free' },\n  limit: 3000,\n  period: '1d'\n})\n```\n\nThe **options** accepted are:\n\n- `id`\u003cspan class=\"type\"\u003estring\u003c/span\u003e: The id of the plan, it cannot contain whitespaces.\n- `period`\u003cspan class=\"type\"\u003estring\u003c/span\u003e: The time window which the limit applies. It accepts [ms](https://www.npmjs.com/package/ms) syntax.\n- `limit`\u003cspan class=\"type\"\u003enumber\u003c/span\u003e: The target maximum number of requests that can be made in a given time period.\n- `metadata`\u003cspan class=\"type\"\u003eobject\u003c/span\u003e: A flat object containing additional information. Pass `null` or `''` to remove all the metadata fields.\n\nAny other field provided will be omitted.\n\n**Returns**: an object with the options specified, plus:\n\n- `createdAt`\u003cspan class=\"type\"\u003enumber\u003c/span\u003e: The timestamp when the object was created.\n- `updatedAt`\u003cspan class=\"type\"\u003enumber\u003c/span\u003e: The last timestamp when the object was modified.\n\n### .list\n\nIt retrieves all the plans:\n\n```js\nconst plans = await openkey.plans.list()\n```\n\n### .retrieve\n\nIt retrieves a plan by id:\n\n```js\nconst { createdAt, updatedAt, ...plan } = await openkey.plans.retrieve('free_tier')\n```\n\n**Returns**: the `plan` object, or `null` if it is not found.\n\n### .update\n\nIt updates a plan by id:\n\n```js\nconst { updatedAt, ...plan } = await openkey.plans.update('free_tier', {\n  limit: 1000\n})\n```\n\nYou can't update the `id`. Also, in the same way than [.create](#create), any other field that is not a supported option will be omitted.\n\n**Returns**: the updated `plan` object. If the plan is not found, this method will throw an error.\n\n### .del\n\nIt deletes a plan by id:\n\n```js\nawait openkey.plans.del('free_tier')\n```\n\nIt will throw an error if:\n\n- The plan has key associated. In that case, first keys needs to be deleted.\n- The plan id doesn't exist.\n\n**Returns** A boolean confirming the plan has been deleted.\n\n## .keys\n\nIt represents the credentials used for authenticating a plan.\n\n### .create\n\nIt creates a new key:\n\n```js\n/*\n * A random 16 length base58 key is created by default.\n */\nconst key = await openkey.key.create()\nconsole.log(key.value) // =\u003e 'oKLJkVqqG2zExUYD'\n\n/**\n * You can provide a value to use.\n */\nconst key = await openkey.key.create({ value: 'oKLJkVqqG2zExUYD' })\n\n/**\n * The key can be associated with a plan when it's created.\n */\nconst key = await openkey.key.create({ value: 'oKLJkVqqG2zExUYD', plan: plan.id })\n```\n\nThe **options** accepted are:\n\n- `value`\u003cspan class=\"type\"\u003estring\u003c/span\u003e: The value of the key, being a base58 16 length key generated by default.\n- `enabled`\u003cspan class=\"type\"\u003estring\u003c/span\u003e: It determines if the key is active, being `true` by default.\n- `metadata`\u003cspan class=\"type\"\u003eobject\u003c/span\u003e: A flat object containing additional information. Pass `null` or `''` to remove all the metadata fields.\n\nAny other field provided will be omitted.\n\n**Returns**: an object with the options specified, plus:\n\n- `createdAt`\u003cspan class=\"type\"\u003enumber\u003c/span\u003e: The timestamp when the object was created.\n- `updatedAt`\u003cspan class=\"type\"\u003enumber\u003c/span\u003e: The last timestamp when the object was modified.\n\n### .retrieve\n\nIt retrieves a key by id:\n\n```js\nconst { createdAt, updatedAt, ...key } = await openkey.key.retrieve('AN4fJ')\n```\n\n**Returns**: the `key` object, or `null` if it is not found.\n\n### .update\n\nIt updates a key by id:\n\n```js\nconst { updatedAt, ...key } = await openkey.key.update(value, {\n  enabled: false\n})\n```\n\nIn the same way than [.create](#create-1), any other field that is not a supported option will be omitted.\n\n**Returns**: the updated `key` object. If the key is not found, this method will throw an error.\n\n### .del\n\nIt deletes a key by value:\n\n```js\nawait openkey.plans.del(value)\n```\n\nIt will throw an error if the key value doesn't exist.\n\n**Returns** A boolean confirming the plan has been deleted.\n\n## .usage\n\nIt returns the current usage of a key that is associated with a plan:\n\n```js\nconst usage = await openkey.usage(key.value)\nconsole.log(usage)\n// {\n//   limit: 3,\n//   remaining: 3,\n//   reset: 1714571966087,\n//   pending: Promise { [] }\n// }\n```\n\n### .increment\n\nSimilar to the previous method, but increments the usage by one before returning:\n\n```js\nconst usage = await openkey.usage.increment(key.value)\n// {\n//   limit: 3,\n//   remaining: 2,\n//   reset: 1714571966087,\n//   pending: Promise { [] }\n// }\n```\n\nAdditionally you can increment specifying the `quantity`:\n\n```js\nconst usage = await openkey.usage.increment(key.value, { quantity: 3 })\n// {\n//   limit: 3,\n//   remaining: 0,\n//   reset: 1714571966087,\n//   pending: Promise { [] }\n// }\n```\n\n## .stats\n\nIt returns the count per every day for a given API key:\n\n```js\nconst stats = await openkey.stats(key.value)\nconsole.log(stats)\n// [\n//   { date: '2024-05-01', count: 1 },\n//   { date: '2024-05-02', count: 10 },\n//   { date: '2024-05-03', count: 5 }\n// ]\n```\n\n# Compression \u0026 serialization\n\nBy default, **openkey** uses JSON serialization without compression for two reasons:\n\n- The payload isn't large enough to take advantage of compression.\n- Storing compressed data makes the content unreadable without first decompressing it.\n\nYou can customize `serialize` and `deserialize` when **openkey** is instantiated to define how you want your data to be handled.\n\nFor example, you can combine **openkey** with [compress-brotli](https://github.com/Kikobeats/compress-brotli) to store compressed data painlessly:\n\n```js\nconst compressBrotli = require('compress-brotli')\nconst redis = new Redis()\n\nconst openkey = require('openkey')({\n  redis,\n  serialize: async data =\u003e brotli.serialize(await brotli.compress(data)),\n  deserialize: data =\u003e brotli.decompress(brotli.deserialize(data))\n})\n```\n\n# HTTP fields\n\n**openkey** has been designed to play well according to [RateLimit header fields for HTTP](https://datatracker.ietf.org/doc/draft-ietf-httpapi-ratelimit-headers/):\n\n```js\nmodule.exports = (req, res) =\u003e {\n  const apiKey = req.headers['x-api-key']\n  if (!apiKey) send(res, 401)\n  const { pending, ...usage } = await openkey.usage.increment(apiKey)\n  const statusCode = usage.remaining \u003e 0 ? 200 : 429\n  res.setHeader('X-Rate-Limit-Limit', usage.limit)\n  res.setHeader('X-Rate-Limit-Remaining', usage.remaining)\n  res.setHeader('X-Rate-Limit-Reset', usage.reset)\n  return send(res, statusCode, usage)\n}\n```\n\n# Stripe integration\n\n![](https://b.stripecdn.com/docs-statics-srv/assets/usage-based-billing.7815fc3949e9351fd5e39cb2b02e4eca.svg)\n\n**openkey** is about making pricing a part of your product development.\n\nIt's an excellent idea to combine it with [Stripe](https://stripe.com/):\n\n```js\n// https://docs.stripe.com/billing/subscriptions/usage-based/implementation-guide\n// Set your secret key. Remember to switch to your live secret key in production.\n// See your keys here: https://dashboard.stripe.com/apikeys\nconst stripe = require('stripe')('sk_test_VZqeYMqkpa1bMxXyikghdPCu')\n\nconst count = await openkey.usage.get('{{CUSTOMER_KEY}}')\n\nconst meterEvent = await stripe.billing.meterEvents.create({\n  event_name: 'alpaca_ai_tokens',\n  payload: {\n    value: count,\n    stripe_customer_id: '{{CUSTOMER_ID}}'\n  }\n})\n```\n\nRead more about [Usage-based billing at Stripe](https://docs.stripe.com/billing/subscriptions/usage-based).\n\n# Error handling\n\nEvery possible error thrown by **openkey** has the name `OpenKeyError` unique `code` associated with it.\n\n```js\nif (error.name === 'OpenKeyError') {\n  return send(res, 400, { code: error.code, message: error.message })\n} else {\n  return send(res, 500)\n}\n```\n\nThis makes it easier to apply any kind of handling in your application logic.\n\nYou can find the [list errors in the source code](https://github.com/microlinkhq/openkey/blob/master/src/error.js).\n\n# Design decisions\n\n## Why Redis?\n\nMainly because it's a cheap in-memory database at scale, and mature enough to prevent vendor lock-in.\n\nWe considered other alternatives such as SQLite, but according to these requeriments Redis is a no brain choice.\n\n## Why not TypeScript?\n\nThis library is intended to be used millions of times every day. We wanted to have granular control as much as possible, and adding a TypeScript transpilation layer isn't ideal from a performance and maintenance perspective.\n\n## Why key/value?\n\nOriginally this library was implemented using [hashes](https://redis.io/docs/data-types/hashes), but then since values are stored as string, it's necessary to cast value (for example, from string to number).\n\nSince we need to do that all the time, we prefer to use key/value. Also this approach allow to customize serializer/deserializer, which is JSON by default.\n\n## Are writting operations atomic?\n\nNo, writes operatoins are not atomic because there are very few use cases where that matters. **openkey** is designed to process a constant stream of requests, where the only thing important to control reaching the limit of each plan.\n\nIn case you need it, you can combine **openkey** with [superlock](https://github.com/Kikobeats/superlock), check the following [example](https://github.com/microlinkhq/openkey/blob/9df977877e5066478020332bffb0c1677a5cd89e/test/usage.js#L115-L138).\n\n# License\n\n**openkey** © [microlink.io](https://microlink.io), released under the [MIT](https://github.com/microlinkhq/openkey/blob/master/LICENSE.md) License.\u003cbr\u003e\nAuthored and maintained by [microlink.io](https://microlink.io) with help from [contributors](https://github.com/microlinkhq/openkey/contributors).\n\n\u003e [microlink.io](https://microlink.io) · GitHub [microlink.io](https://github.com/microlinkhq) · X [@microlinkhq](https://x.com/microlinkhq)\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmicrolinkhq%2Fopenkey","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fmicrolinkhq%2Fopenkey","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmicrolinkhq%2Fopenkey/lists"}