better-auth
Version:
The most comprehensive authentication framework for TypeScript.
1 lines • 15.1 kB
Source Map (JSON)
{"version":3,"file":"index.mjs","names":["options","apiKey"],"sources":["../../../src/plugins/api-key/index.ts"],"sourcesContent":["import type { BetterAuthPlugin } from \"@better-auth/core\";\nimport { createAuthMiddleware } from \"@better-auth/core/api\";\nimport { defineErrorCodes } from \"@better-auth/core/utils\";\nimport { base64Url } from \"@better-auth/utils/base64\";\nimport { createHash } from \"@better-auth/utils/hash\";\nimport { APIError } from \"../../api\";\nimport { generateRandomString } from \"../../crypto/random\";\nimport { mergeSchema } from \"../../db\";\nimport { getDate } from \"../../utils/date\";\nimport { getIp } from \"../../utils/get-request-ip\";\nimport { createApiKeyRoutes, deleteAllExpiredApiKeys } from \"./routes\";\nimport { validateApiKey } from \"./routes/verify-api-key\";\nimport { apiKeySchema } from \"./schema\";\nimport type { ApiKeyOptions } from \"./types\";\n\nexport const defaultKeyHasher = async (key: string) => {\n\tconst hash = await createHash(\"SHA-256\").digest(\n\t\tnew TextEncoder().encode(key),\n\t);\n\tconst hashed = base64Url.encode(new Uint8Array(hash), {\n\t\tpadding: false,\n\t});\n\treturn hashed;\n};\n\nexport const ERROR_CODES = defineErrorCodes({\n\tINVALID_METADATA_TYPE: \"metadata must be an object or undefined\",\n\tREFILL_AMOUNT_AND_INTERVAL_REQUIRED:\n\t\t\"refillAmount is required when refillInterval is provided\",\n\tREFILL_INTERVAL_AND_AMOUNT_REQUIRED:\n\t\t\"refillInterval is required when refillAmount is provided\",\n\tUSER_BANNED: \"User is banned\",\n\tUNAUTHORIZED_SESSION: \"Unauthorized or invalid session\",\n\tKEY_NOT_FOUND: \"API Key not found\",\n\tKEY_DISABLED: \"API Key is disabled\",\n\tKEY_EXPIRED: \"API Key has expired\",\n\tUSAGE_EXCEEDED: \"API Key has reached its usage limit\",\n\tKEY_NOT_RECOVERABLE: \"API Key is not recoverable\",\n\tEXPIRES_IN_IS_TOO_SMALL:\n\t\t\"The expiresIn is smaller than the predefined minimum value.\",\n\tEXPIRES_IN_IS_TOO_LARGE:\n\t\t\"The expiresIn is larger than the predefined maximum value.\",\n\tINVALID_REMAINING: \"The remaining count is either too large or too small.\",\n\tINVALID_PREFIX_LENGTH: \"The prefix length is either too large or too small.\",\n\tINVALID_NAME_LENGTH: \"The name length is either too large or too small.\",\n\tMETADATA_DISABLED: \"Metadata is disabled.\",\n\tRATE_LIMIT_EXCEEDED: \"Rate limit exceeded.\",\n\tNO_VALUES_TO_UPDATE: \"No values to update.\",\n\tKEY_DISABLED_EXPIRATION: \"Custom key expiration values are disabled.\",\n\tINVALID_API_KEY: \"Invalid API key.\",\n\tINVALID_USER_ID_FROM_API_KEY: \"The user id from the API key is invalid.\",\n\tINVALID_API_KEY_GETTER_RETURN_TYPE:\n\t\t\"API Key getter returned an invalid key type. Expected string.\",\n\tSERVER_ONLY_PROPERTY:\n\t\t\"The property you're trying to set can only be set from the server auth instance only.\",\n\tFAILED_TO_UPDATE_API_KEY: \"Failed to update API key\",\n\tNAME_REQUIRED: \"API Key name is required.\",\n});\n\nexport const API_KEY_TABLE_NAME = \"apikey\";\n\nexport const apiKey = (options?: ApiKeyOptions | undefined) => {\n\tconst opts = {\n\t\t...options,\n\t\tapiKeyHeaders: options?.apiKeyHeaders ?? \"x-api-key\",\n\t\tdefaultKeyLength: options?.defaultKeyLength || 64,\n\t\tmaximumPrefixLength: options?.maximumPrefixLength ?? 32,\n\t\tminimumPrefixLength: options?.minimumPrefixLength ?? 1,\n\t\tmaximumNameLength: options?.maximumNameLength ?? 32,\n\t\tminimumNameLength: options?.minimumNameLength ?? 1,\n\t\tenableMetadata: options?.enableMetadata ?? false,\n\t\tdisableKeyHashing: options?.disableKeyHashing ?? false,\n\t\trequireName: options?.requireName ?? false,\n\t\tstorage: options?.storage ?? \"database\",\n\t\trateLimit: {\n\t\t\tenabled:\n\t\t\t\toptions?.rateLimit?.enabled === undefined\n\t\t\t\t\t? true\n\t\t\t\t\t: options?.rateLimit?.enabled,\n\t\t\ttimeWindow: options?.rateLimit?.timeWindow ?? 1000 * 60 * 60 * 24,\n\t\t\tmaxRequests: options?.rateLimit?.maxRequests ?? 10,\n\t\t},\n\t\tkeyExpiration: {\n\t\t\tdefaultExpiresIn: options?.keyExpiration?.defaultExpiresIn ?? null,\n\t\t\tdisableCustomExpiresTime:\n\t\t\t\toptions?.keyExpiration?.disableCustomExpiresTime ?? false,\n\t\t\tmaxExpiresIn: options?.keyExpiration?.maxExpiresIn ?? 365,\n\t\t\tminExpiresIn: options?.keyExpiration?.minExpiresIn ?? 1,\n\t\t},\n\t\tstartingCharactersConfig: {\n\t\t\tshouldStore: options?.startingCharactersConfig?.shouldStore ?? true,\n\t\t\tcharactersLength:\n\t\t\t\toptions?.startingCharactersConfig?.charactersLength ?? 6,\n\t\t},\n\t\tenableSessionForAPIKeys: options?.enableSessionForAPIKeys ?? false,\n\t\tfallbackToDatabase: options?.fallbackToDatabase ?? false,\n\t\tcustomStorage: options?.customStorage,\n\t\tdeferUpdates: options?.deferUpdates ?? false,\n\t} satisfies ApiKeyOptions;\n\n\tconst schema = mergeSchema(\n\t\tapiKeySchema({\n\t\t\trateLimitMax: opts.rateLimit.maxRequests,\n\t\t\ttimeWindow: opts.rateLimit.timeWindow,\n\t\t}),\n\t\topts.schema,\n\t);\n\n\tconst getter =\n\t\topts.customAPIKeyGetter ||\n\t\t((ctx) => {\n\t\t\tif (Array.isArray(opts.apiKeyHeaders)) {\n\t\t\t\tfor (const header of opts.apiKeyHeaders) {\n\t\t\t\t\tconst value = ctx.headers?.get(header);\n\t\t\t\t\tif (value) {\n\t\t\t\t\t\treturn value;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\treturn ctx.headers?.get(opts.apiKeyHeaders);\n\t\t\t}\n\t\t});\n\n\tconst keyGenerator =\n\t\topts.customKeyGenerator ||\n\t\t(async (options: { length: number; prefix: string | undefined }) => {\n\t\t\tconst key = generateRandomString(options.length, \"a-z\", \"A-Z\");\n\t\t\treturn `${options.prefix || \"\"}${key}`;\n\t\t});\n\n\tconst routes = createApiKeyRoutes({ keyGenerator, opts, schema });\n\n\treturn {\n\t\tid: \"api-key\",\n\t\t$ERROR_CODES: ERROR_CODES,\n\t\thooks: {\n\t\t\tbefore: [\n\t\t\t\t{\n\t\t\t\t\tmatcher: (ctx) => !!getter(ctx) && opts.enableSessionForAPIKeys,\n\t\t\t\t\thandler: createAuthMiddleware(async (ctx) => {\n\t\t\t\t\t\tconst key = getter(ctx)!;\n\n\t\t\t\t\t\tif (typeof key !== \"string\") {\n\t\t\t\t\t\t\tthrow new APIError(\"BAD_REQUEST\", {\n\t\t\t\t\t\t\t\tmessage: ERROR_CODES.INVALID_API_KEY_GETTER_RETURN_TYPE,\n\t\t\t\t\t\t\t});\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tif (key.length < opts.defaultKeyLength) {\n\t\t\t\t\t\t\t// if the key is shorter than the default key length, than we know the key is invalid.\n\t\t\t\t\t\t\t// we can't check if the key is exactly equal to the default key length, because\n\t\t\t\t\t\t\t// a prefix may be added to the key.\n\t\t\t\t\t\t\tthrow new APIError(\"FORBIDDEN\", {\n\t\t\t\t\t\t\t\tmessage: ERROR_CODES.INVALID_API_KEY,\n\t\t\t\t\t\t\t});\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tif (opts.customAPIKeyValidator) {\n\t\t\t\t\t\t\tconst isValid = await opts.customAPIKeyValidator({ ctx, key });\n\t\t\t\t\t\t\tif (!isValid) {\n\t\t\t\t\t\t\t\tthrow new APIError(\"FORBIDDEN\", {\n\t\t\t\t\t\t\t\t\tmessage: ERROR_CODES.INVALID_API_KEY,\n\t\t\t\t\t\t\t\t});\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tconst hashed = opts.disableKeyHashing\n\t\t\t\t\t\t\t? key\n\t\t\t\t\t\t\t: await defaultKeyHasher(key);\n\n\t\t\t\t\t\tconst apiKey = await validateApiKey({\n\t\t\t\t\t\t\thashedKey: hashed,\n\t\t\t\t\t\t\tctx,\n\t\t\t\t\t\t\topts,\n\t\t\t\t\t\t\tschema,\n\t\t\t\t\t\t});\n\n\t\t\t\t\t\tconst cleanupTask = deleteAllExpiredApiKeys(ctx.context).catch(\n\t\t\t\t\t\t\t(err) => {\n\t\t\t\t\t\t\t\tctx.context.logger.error(\n\t\t\t\t\t\t\t\t\t\"Failed to delete expired API keys:\",\n\t\t\t\t\t\t\t\t\terr,\n\t\t\t\t\t\t\t\t);\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t);\n\t\t\t\t\t\tif (opts.deferUpdates) {\n\t\t\t\t\t\t\tctx.context.runInBackground(cleanupTask);\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tconst user = await ctx.context.internalAdapter.findUserById(\n\t\t\t\t\t\t\tapiKey.userId,\n\t\t\t\t\t\t);\n\t\t\t\t\t\tif (!user) {\n\t\t\t\t\t\t\tthrow new APIError(\"UNAUTHORIZED\", {\n\t\t\t\t\t\t\t\tmessage: ERROR_CODES.INVALID_USER_ID_FROM_API_KEY,\n\t\t\t\t\t\t\t});\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tconst session = {\n\t\t\t\t\t\t\tuser,\n\t\t\t\t\t\t\tsession: {\n\t\t\t\t\t\t\t\tid: apiKey.id,\n\t\t\t\t\t\t\t\ttoken: key,\n\t\t\t\t\t\t\t\tuserId: apiKey.userId,\n\t\t\t\t\t\t\t\tuserAgent: ctx.request?.headers.get(\"user-agent\") ?? null,\n\t\t\t\t\t\t\t\tipAddress: ctx.request\n\t\t\t\t\t\t\t\t\t? getIp(ctx.request, ctx.context.options)\n\t\t\t\t\t\t\t\t\t: null,\n\t\t\t\t\t\t\t\tcreatedAt: new Date(),\n\t\t\t\t\t\t\t\tupdatedAt: new Date(),\n\t\t\t\t\t\t\t\texpiresAt:\n\t\t\t\t\t\t\t\t\tapiKey.expiresAt ||\n\t\t\t\t\t\t\t\t\tgetDate(\n\t\t\t\t\t\t\t\t\t\tctx.context.options.session?.expiresIn || 60 * 60 * 24 * 7, // 7 days\n\t\t\t\t\t\t\t\t\t\t\"ms\",\n\t\t\t\t\t\t\t\t\t),\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t};\n\n\t\t\t\t\t\t// Always set the session context for API key authentication\n\t\t\t\t\t\tctx.context.session = session;\n\n\t\t\t\t\t\tif (ctx.path === \"/get-session\") {\n\t\t\t\t\t\t\treturn session;\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\treturn {\n\t\t\t\t\t\t\t\tcontext: ctx,\n\t\t\t\t\t\t\t};\n\t\t\t\t\t\t}\n\t\t\t\t\t}),\n\t\t\t\t},\n\t\t\t],\n\t\t},\n\t\tendpoints: {\n\t\t\t/**\n\t\t\t * ### Endpoint\n\t\t\t *\n\t\t\t * POST `/api-key/create`\n\t\t\t *\n\t\t\t * ### API Methods\n\t\t\t *\n\t\t\t * **server:**\n\t\t\t * `auth.api.createApiKey`\n\t\t\t *\n\t\t\t * **client:**\n\t\t\t * `authClient.apiKey.create`\n\t\t\t *\n\t\t\t * @see [Read our docs to learn more.](https://better-auth.com/docs/plugins/api-key#api-method-api-key-create)\n\t\t\t */\n\t\t\tcreateApiKey: routes.createApiKey,\n\t\t\t/**\n\t\t\t * ### Endpoint\n\t\t\t *\n\t\t\t * POST `/api-key/verify`\n\t\t\t *\n\t\t\t * ### API Methods\n\t\t\t *\n\t\t\t * **server:**\n\t\t\t * `auth.api.verifyApiKey`\n\t\t\t *\n\t\t\t * @see [Read our docs to learn more.](https://better-auth.com/docs/plugins/api-key#api-method-api-key-verify)\n\t\t\t */\n\t\t\tverifyApiKey: routes.verifyApiKey,\n\t\t\t/**\n\t\t\t * ### Endpoint\n\t\t\t *\n\t\t\t * GET `/api-key/get`\n\t\t\t *\n\t\t\t * ### API Methods\n\t\t\t *\n\t\t\t * **server:**\n\t\t\t * `auth.api.getApiKey`\n\t\t\t *\n\t\t\t * **client:**\n\t\t\t * `authClient.apiKey.get`\n\t\t\t *\n\t\t\t * @see [Read our docs to learn more.](https://better-auth.com/docs/plugins/api-key#api-method-api-key-get)\n\t\t\t */\n\t\t\tgetApiKey: routes.getApiKey,\n\t\t\t/**\n\t\t\t * ### Endpoint\n\t\t\t *\n\t\t\t * POST `/api-key/update`\n\t\t\t *\n\t\t\t * ### API Methods\n\t\t\t *\n\t\t\t * **server:**\n\t\t\t * `auth.api.updateApiKey`\n\t\t\t *\n\t\t\t * **client:**\n\t\t\t * `authClient.apiKey.update`\n\t\t\t *\n\t\t\t * @see [Read our docs to learn more.](https://better-auth.com/docs/plugins/api-key#api-method-api-key-update)\n\t\t\t */\n\t\t\tupdateApiKey: routes.updateApiKey,\n\t\t\t/**\n\t\t\t * ### Endpoint\n\t\t\t *\n\t\t\t * POST `/api-key/delete`\n\t\t\t *\n\t\t\t * ### API Methods\n\t\t\t *\n\t\t\t * **server:**\n\t\t\t * `auth.api.deleteApiKey`\n\t\t\t *\n\t\t\t * **client:**\n\t\t\t * `authClient.apiKey.delete`\n\t\t\t *\n\t\t\t * @see [Read our docs to learn more.](https://better-auth.com/docs/plugins/api-key#api-method-api-key-delete)\n\t\t\t */\n\t\t\tdeleteApiKey: routes.deleteApiKey,\n\t\t\t/**\n\t\t\t * ### Endpoint\n\t\t\t *\n\t\t\t * GET `/api-key/list`\n\t\t\t *\n\t\t\t * ### API Methods\n\t\t\t *\n\t\t\t * **server:**\n\t\t\t * `auth.api.listApiKeys`\n\t\t\t *\n\t\t\t * **client:**\n\t\t\t * `authClient.apiKey.list`\n\t\t\t *\n\t\t\t * @see [Read our docs to learn more.](https://better-auth.com/docs/plugins/api-key#api-method-api-key-list)\n\t\t\t */\n\t\t\tlistApiKeys: routes.listApiKeys,\n\t\t\t/**\n\t\t\t * ### Endpoint\n\t\t\t *\n\t\t\t * POST `/api-key/delete-all-expired-api-keys`\n\t\t\t *\n\t\t\t * ### API Methods\n\t\t\t *\n\t\t\t * **server:**\n\t\t\t * `auth.api.deleteAllExpiredApiKeys`\n\t\t\t *\n\t\t\t * @see [Read our docs to learn more.](https://better-auth.com/docs/plugins/api-key#api-method-api-key-delete-all-expired-api-keys)\n\t\t\t */\n\t\t\tdeleteAllExpiredApiKeys: routes.deleteAllExpiredApiKeys,\n\t\t},\n\t\tschema,\n\t\toptions,\n\t} satisfies BetterAuthPlugin;\n};\n\nexport type * from \"./types\";\n"],"mappings":"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"}