{"_id":"@abstraks-dev/api-key-auth","_rev":"2-a922a7b96b99fa6be44d9fe5d5b7e91b","name":"@abstraks-dev/api-key-auth","dist-tags":{"latest":"1.0.1"},"versions":{"1.0.0":{"name":"@abstraks-dev/api-key-auth","version":"1.0.0","keywords":["abstraks","api-key","authentication","ssm","lambda","aws","timing-safe"],"author":{"name":"Abstraks"},"license":"MIT","_id":"@abstraks-dev/api-key-auth@1.0.0","maintainers":[{"name":"abstraks-dev","email":"contactabstraks@gmail.com"}],"homepage":"https://github.com/Abstraks-co/shared-modules#readme","bugs":{"url":"https://github.com/Abstraks-co/shared-modules/issues"},"dist":{"shasum":"7c78fc7ff5219fc7be7bda76d519f7155afb3dd7","tarball":"https://registry.npmjs.org/@abstraks-dev/api-key-auth/-/api-key-auth-1.0.0.tgz","fileCount":5,"integrity":"sha512-zAEFmgjRB59m5whd74m6mT14VewDQymlN4Ih5i2w0ZHHeh5o+D2/hSpUmn5+y8YvtV4SWvKfmNVoJ8L53mBEGg==","signatures":[{"sig":"MEYCIQCmvxXCNWsa2HJ7BAhjqcNTMGo2kah/E47G7HogreZ9+AIhAMY78z9wnAVzgiKJSisSgXvX+XNZcBWOBdd84nz008XM","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":20694},"main":"src/index.js","type":"module","gitHead":"8aa41df62a281863efc707a08e35120b759f358e","scripts":{"test":"NODE_OPTIONS=--experimental-vm-modules jest","test:watch":"NODE_OPTIONS=--experimental-vm-modules jest --watch","test:coverage":"NODE_OPTIONS=--experimental-vm-modules jest --coverage"},"_npmUser":{"name":"abstraks-dev","email":"contactabstraks@gmail.com"},"repository":{"url":"git+https://github.com/Abstraks-co/shared-modules.git","type":"git","directory":"packages/api-key-auth"},"_npmVersion":"10.8.2","description":"SSM-based API key authentication with caching and timing-safe comparison for AWS Lambda","directories":{},"_nodeVersion":"20.19.5","publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"jest":"^29.7.0","@jest/globals":"^29.7.0"},"peerDependencies":{"@aws-sdk/client-ssm":"^3.835.0"},"_npmOperationalInternal":{"tmp":"tmp/api-key-auth_1.0.0_1763506078755_0.9683344227352286","host":"s3://npm-registry-packages-npm-production"}},"1.0.1":{"name":"@abstraks-dev/api-key-auth","version":"1.0.1","description":"SSM-based API key authentication with caching and timing-safe comparison for AWS Lambda","type":"module","main":"src/index.js","scripts":{"test":"NODE_OPTIONS=--experimental-vm-modules jest","test:watch":"NODE_OPTIONS=--experimental-vm-modules jest --watch","test:coverage":"NODE_OPTIONS=--experimental-vm-modules jest --coverage"},"keywords":["abstraks","api-key","authentication","ssm","lambda","aws","timing-safe"],"author":{"name":"Abstraks"},"license":"MIT","repository":{"type":"git","url":"git+https://github.com/Abstraks-co/shared-modules.git","directory":"packages/api-key-auth"},"peerDependencies":{"@aws-sdk/client-ssm":"^3.835.0"},"devDependencies":{"@jest/globals":"^29.7.0","jest":"^29.7.0"},"publishConfig":{"access":"public"},"_id":"@abstraks-dev/api-key-auth@1.0.1","gitHead":"2b247a175ceba75cf8a499d613edc07eaebcb147","bugs":{"url":"https://github.com/Abstraks-co/shared-modules/issues"},"homepage":"https://github.com/Abstraks-co/shared-modules#readme","_nodeVersion":"20.19.5","_npmVersion":"10.8.2","dist":{"integrity":"sha512-v8R68cL3E3hR0+JC+xAeO/ZGwCtu7NJn6gbBx8KwZgUSUiGLKtwa19dnaO0H8Rm7e/DwPvaqgleduAnPeuFFeg==","shasum":"632f0f93c390fa5d66b26f217855cb4ea92cb510","tarball":"https://registry.npmjs.org/@abstraks-dev/api-key-auth/-/api-key-auth-1.0.1.tgz","fileCount":5,"unpackedSize":20694,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEUCICUJ96OokY0z32K4SWp4jJQBEqRj6zeij2mVWxN9XRHpAiEA+Pi2+sqat/y1X26RP46Li0wQFV8xgLQB3aTwTRMcFIE="}]},"_npmUser":{"name":"abstraks-dev","email":"contactabstraks@gmail.com"},"directories":{},"maintainers":[{"name":"abstraks-dev","email":"contactabstraks@gmail.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/api-key-auth_1.0.1_1763657697961_0.5380918879543775"},"_hasShrinkwrap":false}},"time":{"created":"2025-11-18T22:47:58.655Z","modified":"2025-11-20T16:54:58.365Z","1.0.0":"2025-11-18T22:47:58.990Z","1.0.1":"2025-11-20T16:54:58.176Z"},"bugs":{"url":"https://github.com/Abstraks-co/shared-modules/issues"},"author":{"name":"Abstraks"},"license":"MIT","homepage":"https://github.com/Abstraks-co/shared-modules#readme","keywords":["abstraks","api-key","authentication","ssm","lambda","aws","timing-safe"],"repository":{"type":"git","url":"git+https://github.com/Abstraks-co/shared-modules.git","directory":"packages/api-key-auth"},"description":"SSM-based API key authentication with caching and timing-safe comparison for AWS Lambda","maintainers":[{"name":"abstraks-dev","email":"contactabstraks@gmail.com"}],"readme":"# @abstraks-dev/api-key-auth\n\nSSM-based API key authentication with caching and timing-safe comparison for AWS Lambda functions.\n\n## Features\n\n- ✅ **SSM Parameter Store Integration** - Securely fetch API keys from AWS Systems Manager\n- ✅ **Automatic Caching** - Cache API keys with configurable TTL (default: 5 minutes)\n- ✅ **Timing-Safe Comparison** - Prevent timing attacks with constant-time string comparison\n- ✅ **Lambda Middleware** - Easy-to-use middleware for automatic authentication\n- ✅ **Multiple Header Support** - Support for lowercase and uppercase header names\n- ✅ **Comprehensive Error Handling** - Proper error codes and messages\n- ✅ **Zero Configuration** - Works out of the box with sensible defaults\n\n## Installation\n\n```bash\nnpm install @abstraks-dev/api-key-auth @aws-sdk/client-ssm\n```\n\n## Quick Start\n\n### Basic Usage\n\n```javascript\nimport { createApiKeyValidator } from '@abstraks-dev/api-key-auth';\n\n// Create validator with SSM parameter name\nconst validator = createApiKeyValidator(process.env.API_KEY_PARAM);\n\nexport const handler = async (event) => {\n\ttry {\n\t\t// Validate API key from request headers\n\t\tawait validator.validateApiKey(event);\n\n\t\t// Your Lambda logic here\n\t\treturn {\n\t\t\tstatusCode: 200,\n\t\t\tbody: JSON.stringify({ message: 'Success' }),\n\t\t};\n\t} catch (error) {\n\t\treturn {\n\t\t\tstatusCode: error.statusCode || 500,\n\t\t\tbody: JSON.stringify({ error: error.message }),\n\t\t};\n\t}\n};\n```\n\n### Using Middleware\n\n```javascript\nimport { createApiKeyValidator } from '@abstraks-dev/api-key-auth';\n\nconst validator = createApiKeyValidator(process.env.API_KEY_PARAM);\n\nconst myHandler = async (event, context, callback) => {\n\t// API key already validated by middleware\n\t// Your logic here\n\treturn { statusCode: 200, body: 'Success' };\n};\n\n// Wrap handler with authentication middleware\nexport const handler = validator.middleware(myHandler);\n```\n\n## API Reference\n\n### `createApiKeyValidator(parameterName, options)`\n\nFactory function to create a new API key validator.\n\n**Parameters:**\n\n- `parameterName` (string, required): SSM parameter name (e.g., `/abstraks/service/dev/apiKey`)\n- `options` (object, optional):\n  - `region` (string): AWS region (defaults to `process.env.AWS_REGION` or `'us-west-2'`)\n  - `cacheTTL` (number): Cache TTL in milliseconds (defaults to `300000` = 5 minutes)\n\n**Returns:** `ApiKeyValidator` instance\n\n**Example:**\n\n```javascript\nconst validator = createApiKeyValidator('/abstraks/query/prod/apiKey', {\n\tregion: 'us-east-1',\n\tcacheTTL: 10 * 60 * 1000, // 10 minutes\n});\n```\n\n### `ApiKeyValidator` Class\n\n#### Constructor\n\n```javascript\nnew ApiKeyValidator(options);\n```\n\n**Options:**\n\n- `parameterName` (string, required): SSM parameter name\n- `region` (string): AWS region\n- `cacheTTL` (number): Cache TTL in milliseconds\n\n#### Methods\n\n##### `validateApiKey(event, options)`\n\nValidates API key from Lambda event headers.\n\n**Parameters:**\n\n- `event` (object, required): Lambda event object\n- `options` (object, optional):\n  - `headerName` (string): Header name to check (defaults to `'x-api-key'`)\n\n**Returns:** `Promise<boolean>` - Returns `true` if valid\n\n**Throws:** Error with `statusCode` property:\n\n- `401`: Missing or invalid API key\n- `500`: Service configuration error\n\n**Example:**\n\n```javascript\ntry {\n\tawait validator.validateApiKey(event);\n\tconsole.log('API key is valid');\n} catch (error) {\n\tconsole.error(`Validation failed: ${error.message}`);\n\t// error.statusCode will be 401 or 500\n}\n```\n\n##### `getExpectedApiKey()`\n\nFetches API key from SSM Parameter Store (with caching).\n\n**Returns:** `Promise<string>` - The API key value\n\n**Throws:** Error if parameter not found or SSM fetch fails\n\n**Example:**\n\n```javascript\nconst apiKey = await validator.getExpectedApiKey();\n```\n\n##### `timingSafeEqual(a, b)`\n\nPerforms timing-safe string comparison.\n\n**Parameters:**\n\n- `a` (string): First string\n- `b` (string): Second string\n\n**Returns:** `boolean` - True if strings are equal\n\n**Example:**\n\n```javascript\nconst isMatch = validator.timingSafeEqual('key1', 'key2');\n```\n\n##### `middleware(handler)`\n\nCreates Lambda middleware for automatic authentication.\n\n**Parameters:**\n\n- `handler` (Function): Lambda handler function to wrap\n\n**Returns:** `Function` - Wrapped handler function with automatic API key validation\n\n**Example:**\n\n```javascript\nconst wrappedHandler = validator.middleware(myHandler);\n```\n\n##### `clearCache()`\n\nClears cached API key (forces fresh fetch on next validation).\n\n**Example:**\n\n```javascript\nvalidator.clearCache();\n```\n\n##### `isCacheValid()`\n\nChecks if cached API key is still valid.\n\n**Returns:** `boolean` - True if cache is valid\n\n**Example:**\n\n```javascript\nif (!validator.isCacheValid()) {\n\tconsole.log('Cache expired');\n}\n```\n\n##### `getCacheStats()`\n\nGets cache statistics.\n\n**Returns:** Object with:\n\n- `hasCachedKey` (boolean): Whether key is cached\n- `cacheAge` (number|null): Age of cache in milliseconds\n- `cacheTTL` (number): Cache TTL in milliseconds\n- `isValid` (boolean): Whether cache is valid\n\n**Example:**\n\n```javascript\nconst stats = validator.getCacheStats();\nconsole.log(`Cache age: ${stats.cacheAge}ms`);\n```\n\n## Usage Patterns\n\n### Pattern 1: Basic Validation\n\n```javascript\nimport { createApiKeyValidator } from '@abstraks-dev/api-key-auth';\n\nconst validator = createApiKeyValidator(process.env.API_KEY_PARAM);\n\nexport const handler = async (event) => {\n\ttry {\n\t\tawait validator.validateApiKey(event);\n\t\t// Proceed with authenticated logic\n\t} catch (error) {\n\t\treturn {\n\t\t\tstatusCode: error.statusCode,\n\t\t\tbody: JSON.stringify({ error: error.message }),\n\t\t};\n\t}\n};\n```\n\n### Pattern 2: Custom Header Name\n\n```javascript\nconst validator = createApiKeyValidator(process.env.API_KEY_PARAM);\n\nexport const handler = async (event) => {\n\ttry {\n\t\t// Validate using custom header\n\t\tawait validator.validateApiKey(event, { headerName: 'x-custom-api-key' });\n\t\t// ...\n\t} catch (error) {\n\t\t// Handle error\n\t}\n};\n```\n\n### Pattern 3: Middleware with Error Handling\n\n```javascript\nimport { createApiKeyValidator } from '@abstraks-dev/api-key-auth';\nimport {\n\tcreateErrorResponse,\n\tcreateSuccessResponse,\n} from '@abstraks/lambda-responses';\n\nconst validator = createApiKeyValidator(process.env.API_KEY_PARAM);\n\nconst myHandler = async (event, context, callback) => {\n\t// API key already validated by middleware\n\tconst data = await processRequest(event);\n\treturn callback(null, createSuccessResponse(data));\n};\n\n// Wrap handler with middleware - validation happens automatically\nexport const handler = validator.middleware(myHandler);\n```\n\n### Pattern 4: Multi-Service Setup\n\n```javascript\n// query/service/helpers/apiKeyAuth.js\nimport { createApiKeyValidator } from '@abstraks-dev/api-key-auth';\n\nexport const queryApiKeyValidator = createApiKeyValidator(\n\tprocess.env.QUERY_API_KEY_PARAM\n);\n\n// query/service/lambdas/query.js\nimport { queryApiKeyValidator } from '../helpers/apiKeyAuth.js';\n\nexport const handler = async (event) => {\n\tawait queryApiKeyValidator.validateApiKey(event);\n\t// ... query logic\n};\n```\n\n## Integration with CDK\n\n### IAM Permissions\n\nYour Lambda function needs permissions to read from SSM Parameter Store:\n\n```javascript\n// CDK Stack\neventDataLambda.addToRolePolicy(\n\tnew iam.PolicyStatement({\n\t\tactions: ['ssm:GetParameter'],\n\t\tresources: [\n\t\t\t`arn:aws:ssm:${this.region}:${this.account}:parameter${apiKeyParam}`,\n\t\t],\n\t\tconditions: {\n\t\t\tStringEquals: {\n\t\t\t\t'ssm:ParameterType': 'SecureString',\n\t\t\t},\n\t\t},\n\t})\n);\n\n// For SecureString parameters, also add KMS decrypt permission\neventDataLambda.addToRolePolicy(\n\tnew iam.PolicyStatement({\n\t\tactions: ['kms:Decrypt'],\n\t\tresources: ['arn:aws:kms:*:*:key/*'],\n\t})\n);\n```\n\n### Environment Variables\n\n```javascript\nconst lambdaEnv = {\n\tAPI_KEY_PARAM: `/abstraks/query/${environment}/apiKey`,\n};\n```\n\n## Security Features\n\n### Timing-Safe Comparison\n\nThe package uses `crypto.timingSafeEqual()` to prevent timing attacks:\n\n```javascript\n// ❌ Vulnerable to timing attacks\nif (apiKey === expectedKey) { ... }\n\n// ✅ Timing-safe comparison\nif (validator.timingSafeEqual(apiKey, expectedKey)) { ... }\n```\n\n### Cache TTL for Key Rotation\n\nKeys are automatically refetched after TTL expires, supporting key rotation:\n\n```javascript\nconst validator = createApiKeyValidator('/api-key', {\n\tcacheTTL: 5 * 60 * 1000, // Refetch every 5 minutes\n});\n```\n\n### SecureString Support\n\nThe package uses `WithDecryption: true` to automatically decrypt SecureString parameters from SSM.\n\n## Error Handling\n\nAll errors include a `statusCode` property for proper HTTP responses:\n\n```javascript\ntry {\n\tawait validator.validateApiKey(event);\n} catch (error) {\n\tconsole.error(error.message); // Human-readable error\n\tconsole.log(error.statusCode); // 401 or 500\n}\n```\n\n**Error Types:**\n\n- `401 Unauthorized`: Missing or invalid API key\n- `500 Internal Server Error`: SSM configuration error\n\n## Testing\n\n### Mocking in Tests\n\n```javascript\nimport { jest } from '@jest/globals';\n\nconst mockSend = jest.fn();\njest.unstable_mockModule('@aws-sdk/client-ssm', () => ({\n\tSSMClient: jest.fn(() => ({ send: mockSend })),\n\tGetParameterCommand: jest.fn(),\n}));\n\n// In your test\nmockSend.mockResolvedValue({\n\tParameter: { Value: 'test-api-key' },\n});\n```\n\n### Example Test\n\n```javascript\nimport { createApiKeyValidator } from '@abstraks-dev/api-key-auth';\n\ntest('should validate correct API key', async () => {\n\tconst validator = createApiKeyValidator('/test/api-key');\n\n\tconst event = {\n\t\theaders: { 'x-api-key': 'correct-key' },\n\t};\n\n\tawait expect(validator.validateApiKey(event)).resolves.toBe(true);\n});\n```\n\n## Migration from Service-Specific Code\n\n### Before (Duplicated in every service)\n\n```javascript\n// query/service/lambdas/query.js\nimport crypto from 'crypto';\nimport { SSMClient, GetParameterCommand } from '@aws-sdk/client-ssm';\n\nconst ssmClient = new SSMClient({ region: process.env.AWS_REGION });\nlet cachedApiKey = null;\n\nasync function getExpectedApiKey() {\n\tif (cachedApiKey) return cachedApiKey;\n\t// ... SSM logic\n}\n\nfunction timingSafeEqual(a, b) {\n\t// ... timing-safe comparison\n}\n\nexport const handler = async (event) => {\n\tconst apiKey = event.headers['x-api-key'];\n\tconst expected = await getExpectedApiKey();\n\tif (!timingSafeEqual(apiKey, expected)) {\n\t\treturn { statusCode: 401, body: 'Unauthorized' };\n\t}\n\t// ... rest of handler\n};\n```\n\n### After (Using shared module)\n\n```javascript\n// query/service/lambdas/query.js\nimport { createApiKeyValidator } from '@abstraks-dev/api-key-auth';\n\nconst validator = createApiKeyValidator(process.env.API_KEY_PARAM);\n\nconst myHandler = async (event) => {\n\t// Your logic here\n};\n\nexport const handler = validator.middleware(myHandler);\n```\n\n**Result:** Eliminated 50+ lines of duplicated code per service! ✨\n\n## Performance\n\n- **First Request**: ~100-200ms (SSM fetch)\n- **Cached Requests**: <1ms (in-memory lookup)\n- **Memory**: <1KB per validator instance\n\n## License\n\nMIT\n\n## Related Packages\n\n- [@abstraks/lambda-responses](../lambda-responses) - Standardized Lambda response helpers\n- [@abstraks/mongodb-connection](../mongodb-connection) - MongoDB connection management\n","readmeFilename":"README.md"}