@donotlb/keypal 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,606 @@
1
+ import { S as Storage, A as ActionContext, a as ApiKeyRecord, b as ApiKeyMetadata, c as AuditLogQuery, d as AuditLog, e as AuditLogStats } from './shared/keypal.kItV-5pB.js';
2
+ export { f as AuditAction, C as CreateApiKeyInput, g as StorageOptions } from './shared/keypal.kItV-5pB.js';
3
+ import Redis from 'ioredis';
4
+ import { Static, Type } from 'typebox';
5
+
6
+ declare function isExpired(expiresAt: string | null | undefined): boolean;
7
+ declare function getExpirationTime(expiresAt: string | null | undefined): Date | null;
8
+
9
+ type KeyExtractionOptions = {
10
+ headerNames?: string[];
11
+ extractBearer?: boolean;
12
+ };
13
+ declare function extractKeyFromHeaders(headers: Record<string, string | undefined> | Headers, options?: KeyExtractionOptions): string | null;
14
+ declare function hasApiKey(headers: Record<string, string | undefined> | Headers, options?: KeyExtractionOptions): boolean;
15
+
16
+ type PermissionScope = string;
17
+ type Permission = {
18
+ scope: PermissionScope;
19
+ description?: string;
20
+ };
21
+
22
+ type ScopeCheckOptions = {
23
+ /** Resource identifier to check resource-specific scopes (e.g., "website:123", "project:456") */
24
+ resource?: string;
25
+ };
26
+ declare function hasScope(scopes: PermissionScope[] | undefined, requiredScope: PermissionScope, options?: ScopeCheckOptions): boolean;
27
+ declare function hasAnyScope(scopes: PermissionScope[] | undefined, requiredScopes: PermissionScope[], options?: ScopeCheckOptions): boolean;
28
+ declare function hasAllScopes(scopes: PermissionScope[] | undefined, requiredScopes: PermissionScope[], options?: ScopeCheckOptions): boolean;
29
+
30
+ /**
31
+ * Fluent API for building resource-specific scopes
32
+ *
33
+ * @example
34
+ * ```ts
35
+ * const resources = new ResourceBuilder()
36
+ * .add('website', 'site123', ['read', 'write'])
37
+ * .add('project', 'proj456', ['deploy'])
38
+ * .build()
39
+ * ```
40
+ */
41
+ declare class ResourceBuilder {
42
+ private resources;
43
+ /**
44
+ * Add scopes for a specific resource
45
+ * @param resourceType - Type of resource (e.g., 'website', 'project', 'team')
46
+ * @param resourceId - ID of the resource
47
+ * @param scopes - Array of scopes to grant for this resource
48
+ */
49
+ add(resourceType: string, resourceId: string, scopes: PermissionScope[]): this;
50
+ /**
51
+ * Add a single scope to a resource
52
+ * @param resourceType - Type of resource
53
+ * @param resourceId - ID of the resource
54
+ * @param scope - Single scope to grant
55
+ */
56
+ addOne(resourceType: string, resourceId: string, scope: PermissionScope): this;
57
+ /**
58
+ * Add scopes to multiple resources of the same type
59
+ * @param resourceType - Type of resource
60
+ * @param resourceIds - Array of resource IDs
61
+ * @param scopes - Scopes to grant to all resources
62
+ */
63
+ addMany(resourceType: string, resourceIds: string[], scopes: PermissionScope[]): this;
64
+ /**
65
+ * Remove a resource entirely
66
+ */
67
+ remove(resourceType: string, resourceId: string): this;
68
+ /**
69
+ * Remove specific scopes from a resource
70
+ */
71
+ removeScopes(resourceType: string, resourceId: string, scopes: PermissionScope[]): this;
72
+ /**
73
+ * Check if a resource has been added
74
+ */
75
+ has(resourceType: string, resourceId: string): boolean;
76
+ /**
77
+ * Get scopes for a specific resource
78
+ */
79
+ get(resourceType: string, resourceId: string): PermissionScope[];
80
+ /**
81
+ * Clear all resources
82
+ */
83
+ clear(): this;
84
+ /**
85
+ * Build and return the resources object
86
+ */
87
+ build(): Record<string, PermissionScope[]>;
88
+ /**
89
+ * Create a new ResourceBuilder from an existing resources object
90
+ */
91
+ static from(resources: Record<string, PermissionScope[]>): ResourceBuilder;
92
+ }
93
+ /**
94
+ * Create a new ResourceBuilder instance
95
+ */
96
+ declare function createResourceBuilder(): ResourceBuilder;
97
+
98
+ type Cache = {
99
+ get(key: string): Promise<string | null> | string | null;
100
+ set(key: string, value: string, ttl?: number): Promise<void> | void;
101
+ del(key: string): Promise<void> | void;
102
+ };
103
+
104
+ declare const ConfigSchema: Type.TObject<{
105
+ prefix: Type.TOptional<Type.TString>;
106
+ length: Type.TOptional<Type.TNumber>;
107
+ algorithm: Type.TOptional<Type.TUnion<[Type.TLiteral<"sha256">, Type.TLiteral<"sha512">]>>;
108
+ alphabet: Type.TOptional<Type.TString>;
109
+ salt: Type.TOptional<Type.TString>;
110
+ }>;
111
+ type Config = Static<typeof ConfigSchema>;
112
+ /**
113
+ * Configuration options for API key management
114
+ * @example
115
+ * ```typescript
116
+ * const keys = createKeys({
117
+ * prefix: "sk_live_",
118
+ * length: 40,
119
+ * storage: "redis",
120
+ * redis: redisClient,
121
+ * cache: true,
122
+ * cacheTtl: 300
123
+ * });
124
+ * ```
125
+ */
126
+ type ConfigInput = {
127
+ /**
128
+ * Prefix for all generated API keys (e.g., "sk_live_", "sk_test_")
129
+ * @example "sk_live_"
130
+ */
131
+ prefix?: string;
132
+ /**
133
+ * Length of the generated key (excluding prefix)
134
+ * @default 32
135
+ * @example 40
136
+ */
137
+ length?: number;
138
+ /**
139
+ * Hashing algorithm for storing keys
140
+ * @default "sha256"
141
+ */
142
+ algorithm?: "sha256" | "sha512";
143
+ /**
144
+ * Custom alphabet for key generation (default: URL-safe base64)
145
+ * @example "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"
146
+ */
147
+ alphabet?: string;
148
+ /**
149
+ * Salt for hashing (increases security)
150
+ * @example "my-secret-salt"
151
+ */
152
+ salt?: string;
153
+ /**
154
+ * Storage backend for API keys
155
+ * - "memory": In-memory storage (default, not persistent)
156
+ * - "redis": Redis storage (requires redis client)
157
+ * - Custom Storage object: Use your own storage implementation
158
+ * @default "memory"
159
+ */
160
+ storage?: Storage | "memory" | "redis";
161
+ /**
162
+ * Caching strategy for verified keys
163
+ * - true: In-memory cache
164
+ * - "redis": Redis cache (requires redis client)
165
+ * - Custom Cache object: Use your own cache implementation
166
+ * - false: No caching
167
+ * @default false
168
+ */
169
+ cache?: Cache | boolean | "redis";
170
+ /**
171
+ * Cache TTL in seconds
172
+ * @default 60
173
+ */
174
+ cacheTtl?: number;
175
+ /**
176
+ * HTTP header names to look for API keys
177
+ * @default ["authorization", "x-api-key"]
178
+ */
179
+ headerNames?: string[];
180
+ /**
181
+ * Extract Bearer token from Authorization header
182
+ * @default true
183
+ */
184
+ extractBearer?: boolean;
185
+ /**
186
+ * Redis client instance (required when using "redis" storage or cache)
187
+ */
188
+ redis?: Redis;
189
+ /**
190
+ * TTL in seconds for revoked keys in Redis
191
+ * @default 604800 (7 days)
192
+ * @example 0 to keep forever
193
+ */
194
+ revokedKeyTtl?: number;
195
+ /**
196
+ * Automatically update lastUsedAt when verifying a key
197
+ * @default true
198
+ */
199
+ autoTrackUsage?: boolean;
200
+ /**
201
+ * Enable audit logging for key actions
202
+ * @default false
203
+ */
204
+ auditLogs?: boolean;
205
+ /**
206
+ * Default context for audit log entries (can be overridden per operation)
207
+ * @example { userId: 'system', metadata: { service: 'api' } }
208
+ */
209
+ auditContext?: ActionContext;
210
+ };
211
+
212
+ /**
213
+ * API Key verification error codes
214
+ */
215
+ declare const ApiKeyErrorCode: {
216
+ /** No API key was provided */
217
+ readonly MISSING_KEY: "MISSING_KEY";
218
+ /** API key format is invalid (e.g., wrong prefix) */
219
+ readonly INVALID_FORMAT: "INVALID_FORMAT";
220
+ /** API key does not exist in storage */
221
+ readonly INVALID_KEY: "INVALID_KEY";
222
+ /** API key has expired */
223
+ readonly EXPIRED: "EXPIRED";
224
+ /** API key has been revoked */
225
+ readonly REVOKED: "REVOKED";
226
+ /** API key is disabled */
227
+ readonly DISABLED: "DISABLED";
228
+ /** Storage error occurred */
229
+ readonly STORAGE_ERROR: "STORAGE_ERROR";
230
+ /** Cache error occurred */
231
+ readonly CACHE_ERROR: "CACHE_ERROR";
232
+ /** API key is already revoked */
233
+ readonly ALREADY_REVOKED: "ALREADY_REVOKED";
234
+ /** API key is already enabled */
235
+ readonly ALREADY_ENABLED: "ALREADY_ENABLED";
236
+ /** API key is already disabled */
237
+ readonly ALREADY_DISABLED: "ALREADY_DISABLED";
238
+ /** Cannot perform operation on revoked key */
239
+ readonly CANNOT_MODIFY_REVOKED: "CANNOT_MODIFY_REVOKED";
240
+ /** API key not found */
241
+ readonly KEY_NOT_FOUND: "KEY_NOT_FOUND";
242
+ /** Audit logging is not enabled */
243
+ readonly AUDIT_LOGGING_DISABLED: "AUDIT_LOGGING_DISABLED";
244
+ /** Storage does not support this operation */
245
+ readonly STORAGE_NOT_SUPPORTED: "STORAGE_NOT_SUPPORTED";
246
+ };
247
+ type ApiKeyErrorCode = (typeof ApiKeyErrorCode)[keyof typeof ApiKeyErrorCode];
248
+ /**
249
+ * API Key error details
250
+ */
251
+ type ApiKeyError = {
252
+ /** Error code for programmatic handling */
253
+ code: ApiKeyErrorCode;
254
+ /** Human-readable error message */
255
+ message: string;
256
+ /** Optional additional error details */
257
+ details?: unknown;
258
+ };
259
+ /**
260
+ * Helper function to create an API key error
261
+ */
262
+ declare function createApiKeyError(code: ApiKeyErrorCode, details?: unknown): ApiKeyError;
263
+
264
+ /**
265
+ * Result of verifying an API key
266
+ */
267
+ type VerifyResult = {
268
+ /** Whether the key is valid */
269
+ valid: boolean;
270
+ /** The API key record if valid */
271
+ record?: ApiKeyRecord;
272
+ /** Error message if invalid */
273
+ error?: string;
274
+ /** Error code for programmatic handling */
275
+ errorCode?: ApiKeyErrorCode;
276
+ };
277
+ /**
278
+ * Options for verifying API keys
279
+ */
280
+ type VerifyOptions = {
281
+ /** Skip cache lookup (always query storage) */
282
+ skipCache?: boolean;
283
+ /** Override header names to look for */
284
+ headerNames?: string[];
285
+ /** Override extractBearer behavior */
286
+ extractBearer?: boolean;
287
+ /** Skip updating lastUsedAt timestamp (useful when autoTrackUsage is enabled) */
288
+ skipTracking?: boolean;
289
+ };
290
+ /**
291
+ * API Key Manager for creating, verifying, and managing API keys
292
+ *
293
+ * @example
294
+ * ```typescript
295
+ * const keys = createKeys({
296
+ * prefix: "sk_live_",
297
+ * storage: "redis",
298
+ * redis: redisClient
299
+ * });
300
+ *
301
+ * // Create a key
302
+ * const { key, record } = await keys.create({
303
+ * ownerId: "user_123",
304
+ * name: "Production Key",
305
+ * scopes: ["read", "write"]
306
+ * });
307
+ *
308
+ * // Verify a key
309
+ * const result = await keys.verify(key);
310
+ * if (result.valid) {
311
+ * console.log("Key belongs to:", result.record?.metadata.ownerId);
312
+ * }
313
+ * ```
314
+ */
315
+ declare class ApiKeyManager {
316
+ private readonly config;
317
+ private readonly storage;
318
+ private readonly cache?;
319
+ private readonly cacheTtl;
320
+ private readonly extractionOptions;
321
+ private readonly revokedKeyTtl;
322
+ private readonly isRedisStorage;
323
+ private readonly autoTrackUsage;
324
+ private readonly auditLogsEnabled;
325
+ private readonly defaultContext?;
326
+ constructor(config?: ConfigInput);
327
+ generateKey(): string;
328
+ hashKey(key: string): string;
329
+ validateKey(key: string, storedHash: string): boolean;
330
+ /**
331
+ * Extract API key from HTTP headers
332
+ *
333
+ * @param headers - HTTP headers object or Headers instance
334
+ * @param options - Optional extraction options
335
+ * @returns The extracted API key or null if not found
336
+ *
337
+ * @example
338
+ * ```typescript
339
+ * const key = keys.extractKey(req.headers);
340
+ * if (key) {
341
+ * console.log("Found key:", key);
342
+ * }
343
+ * ```
344
+ */
345
+ extractKey(headers: Record<string, string | undefined> | Headers, options?: KeyExtractionOptions): string | null;
346
+ /**
347
+ * Check if an API key is present in HTTP headers
348
+ *
349
+ * @param headers - HTTP headers object or Headers instance
350
+ * @param options - Optional extraction options
351
+ * @returns True if an API key is found in headers
352
+ *
353
+ * @example
354
+ * ```typescript
355
+ * if (keys.hasKey(req.headers)) {
356
+ * // API key is present
357
+ * }
358
+ * ```
359
+ */
360
+ hasKey(headers: Record<string, string | undefined> | Headers, options?: KeyExtractionOptions): boolean;
361
+ /**
362
+ * Verify an API key from a string or HTTP headers
363
+ *
364
+ * @param keyOrHeader - The API key string or HTTP headers object
365
+ * @param options - Verification options
366
+ * @returns Verification result with validity status and record
367
+ *
368
+ * @example
369
+ * ```typescript
370
+ * // Verify from string
371
+ * const result = await keys.verify("sk_live_abc123...");
372
+ *
373
+ * // Verify from headers
374
+ * const result = await keys.verify(req.headers);
375
+ *
376
+ * if (result.valid) {
377
+ * console.log("Owner:", result.record?.metadata.ownerId);
378
+ * } else {
379
+ * console.log("Error:", result.error);
380
+ * }
381
+ * ```
382
+ */
383
+ verify(keyOrHeader: string | Record<string, string | undefined> | Headers, options?: VerifyOptions): Promise<VerifyResult>;
384
+ /**
385
+ * Create a new API key
386
+ *
387
+ * @param metadata - Metadata for the API key (ownerId is required)
388
+ * @returns The generated key string and the stored record
389
+ *
390
+ * @example
391
+ * ```typescript
392
+ * const { key, record } = await keys.create({
393
+ * ownerId: "user_123",
394
+ * name: "Production Key",
395
+ * description: "API key for production access",
396
+ * scopes: ["read", "write"],
397
+ * expiresAt: "2025-12-31T00:00:00.000Z",
398
+ * tags: ["production", "api"]
399
+ * });
400
+ *
401
+ * console.log("New key:", key);
402
+ * console.log("Key ID:", record.id);
403
+ * ```
404
+ */
405
+ create(metadata: Partial<ApiKeyMetadata>, context?: ActionContext): Promise<{
406
+ key: string;
407
+ record: ApiKeyRecord;
408
+ }>;
409
+ findByHash(keyHash: string): Promise<ApiKeyRecord | null>;
410
+ findById(id: string): Promise<ApiKeyRecord | null>;
411
+ findByTags(tags: string[], ownerId?: string): Promise<ApiKeyRecord[]>;
412
+ findByTag(tag: string, ownerId?: string): Promise<ApiKeyRecord[]>;
413
+ list(ownerId: string): Promise<ApiKeyRecord[]>;
414
+ revoke(id: string, context?: ActionContext): Promise<void>;
415
+ revokeAll(ownerId: string): Promise<void>;
416
+ enable(id: string, context?: ActionContext): Promise<void>;
417
+ disable(id: string, context?: ActionContext): Promise<void>;
418
+ rotate(id: string, metadata?: Partial<ApiKeyMetadata>, context?: ActionContext): Promise<{
419
+ key: string;
420
+ record: ApiKeyRecord;
421
+ oldRecord: ApiKeyRecord;
422
+ }>;
423
+ updateLastUsed(id: string): Promise<void>;
424
+ /**
425
+ * Create an audit log entry for a key action
426
+ * @private
427
+ */
428
+ private logAction;
429
+ /**
430
+ * Get audit logs with optional filters
431
+ *
432
+ * @param query - Query options for filtering audit logs
433
+ * @returns Array of audit log entries
434
+ *
435
+ * @example
436
+ * ```typescript
437
+ * const logs = await keys.getLogs({
438
+ * keyId: 'key_123',
439
+ * startDate: '2025-01-01',
440
+ * endDate: '2025-12-31',
441
+ * limit: 100
442
+ * });
443
+ * ```
444
+ */
445
+ getLogs(query?: AuditLogQuery): Promise<AuditLog[]>;
446
+ /**
447
+ * Count audit logs matching query
448
+ *
449
+ * @param query - Query options for filtering audit logs
450
+ * @returns Number of matching logs
451
+ *
452
+ * @example
453
+ * ```typescript
454
+ * const count = await keys.countLogs({ action: 'created' });
455
+ * ```
456
+ */
457
+ countLogs(query?: AuditLogQuery): Promise<number>;
458
+ /**
459
+ * Delete audit logs matching query
460
+ *
461
+ * @param query - Query options for filtering logs to delete
462
+ * @returns Number of logs deleted
463
+ *
464
+ * @example
465
+ * ```typescript
466
+ * // Delete old logs
467
+ * const deleted = await keys.deleteLogs({
468
+ * endDate: '2024-01-01'
469
+ * });
470
+ * ```
471
+ */
472
+ deleteLogs(query: AuditLogQuery): Promise<number>;
473
+ /**
474
+ * Delete all audit logs for a specific key
475
+ *
476
+ * @param keyId - The key ID to delete logs for
477
+ * @returns Number of logs deleted
478
+ *
479
+ * @example
480
+ * ```typescript
481
+ * const deleted = await keys.clearLogs('key_123');
482
+ * ```
483
+ */
484
+ clearLogs(keyId: string): Promise<number>;
485
+ /**
486
+ * Get statistics about audit logs for an owner
487
+ *
488
+ * @param ownerId - Owner ID to get stats for
489
+ * @returns Statistics including total count, counts by action, and last activity
490
+ *
491
+ * @example
492
+ * ```typescript
493
+ * const stats = await keys.getLogStats('user_123');
494
+ * console.log(`Total logs: ${stats.total}`);
495
+ * console.log(`Created: ${stats.byAction.created}`);
496
+ * ```
497
+ */
498
+ getLogStats(ownerId: string): Promise<AuditLogStats>;
499
+ invalidateCache(keyHash: string): Promise<void>;
500
+ isExpired(record: ApiKeyRecord): boolean;
501
+ /**
502
+ * Check if an API key has a specific scope
503
+ *
504
+ * @param record - The API key record
505
+ * @param scope - Required scope to check
506
+ * @param options - Optional scope check options (e.g., resource filtering)
507
+ * @returns True if the key has the required scope
508
+ *
509
+ * @example
510
+ * ```typescript
511
+ * const record = await storage.findById("key_id");
512
+ * if (keys.hasScope(record, "read")) {
513
+ * // Key has read permission
514
+ * }
515
+ *
516
+ * // Check for resource-specific scope
517
+ * if (keys.hasScope(record, "write", { resource: "project:123" })) {
518
+ * // Key can write to project 123
519
+ * }
520
+ * ```
521
+ */
522
+ hasScope(record: ApiKeyRecord, scope: PermissionScope, options?: ScopeCheckOptions): boolean;
523
+ /**
524
+ * Check if an API key has any of the required scopes
525
+ *
526
+ * @param record - The API key record
527
+ * @param requiredScopes - Array of scopes to check
528
+ * @param options - Optional scope check options
529
+ * @returns True if the key has at least one of the required scopes
530
+ *
531
+ * @example
532
+ * ```typescript
533
+ * if (keys.hasAnyScope(record, ["read", "write"])) {
534
+ * // Key has read OR write permission
535
+ * }
536
+ * ```
537
+ */
538
+ hasAnyScope(record: ApiKeyRecord, requiredScopes: PermissionScope[], options?: ScopeCheckOptions): boolean;
539
+ /**
540
+ * Check if an API key has all required scopes
541
+ *
542
+ * @param record - The API key record
543
+ * @param requiredScopes - Array of scopes to check
544
+ * @param options - Optional scope check options
545
+ * @returns True if the key has all required scopes
546
+ *
547
+ * @example
548
+ * ```typescript
549
+ * if (keys.hasAllScopes(record, ["read", "write"])) {
550
+ * // Key has read AND write permissions
551
+ * }
552
+ * ```
553
+ */
554
+ hasAllScopes(record: ApiKeyRecord, requiredScopes: PermissionScope[], options?: ScopeCheckOptions): boolean;
555
+ /**
556
+ * Verify API key from headers and return the record or null
557
+ * This is a convenience method that combines verify() with automatic null handling
558
+ */
559
+ verifyFromHeaders(headers: Record<string, string | undefined> | Headers, options?: VerifyOptions): Promise<ApiKeyRecord | null>;
560
+ /**
561
+ * Check if an API key has a specific scope for a resource
562
+ * @param record - The API key record
563
+ * @param resourceType - Type of resource (e.g., 'website', 'project', 'team')
564
+ * @param resourceId - ID of the resource
565
+ * @param scope - Required scope to check
566
+ */
567
+ checkResourceScope(record: ApiKeyRecord | null, resourceType: string, resourceId: string, scope: PermissionScope): boolean;
568
+ /**
569
+ * Check if an API key has any of the required scopes for a resource
570
+ */
571
+ checkResourceAnyScope(record: ApiKeyRecord | null, resourceType: string, resourceId: string, scopes: PermissionScope[]): boolean;
572
+ /**
573
+ * Check if an API key has all required scopes for a resource
574
+ */
575
+ checkResourceAllScopes(record: ApiKeyRecord | null, resourceType: string, resourceId: string, scopes: PermissionScope[]): boolean;
576
+ }
577
+ /**
578
+ * Create an API key manager instance
579
+ *
580
+ * @param config - Configuration options for key generation and storage
581
+ * @returns An ApiKeyManager instance for creating and verifying keys
582
+ *
583
+ * @example
584
+ * ```typescript
585
+ * // Simple in-memory setup
586
+ * const keys = createKeys({ prefix: "sk_" });
587
+ *
588
+ * // Redis setup with caching
589
+ * const keys = createKeys({
590
+ * prefix: "sk_live_",
591
+ * storage: "redis",
592
+ * redis: redisClient,
593
+ * cache: true,
594
+ * cacheTtl: 300
595
+ * });
596
+ *
597
+ * // Custom storage adapter
598
+ * const keys = createKeys({
599
+ * storage: myCustomStorage
600
+ * });
601
+ * ```
602
+ */
603
+ declare function createKeys(config?: ConfigInput): ApiKeyManager;
604
+
605
+ export { ActionContext, ApiKeyErrorCode, ApiKeyManager, ApiKeyMetadata, ApiKeyRecord, AuditLog, AuditLogQuery, AuditLogStats, ResourceBuilder, Storage, createApiKeyError, createKeys, createResourceBuilder, extractKeyFromHeaders, getExpirationTime, hasAllScopes, hasAnyScope, hasApiKey, hasScope, isExpired };
606
+ export type { ApiKeyError, Config, ConfigInput, Permission, PermissionScope, VerifyOptions, VerifyResult };
package/dist/index.mjs ADDED
@@ -0,0 +1,7 @@
1
+ /*!
2
+ * @donotlb/keypal v0.1.0
3
+ * A TypeScript library for secure API key management with cryptographic hashing, expiration, scopes, and pluggable storage
4
+ * © 2026 "donotlb" <donotlb@gmail.com>
5
+ * Released under the MIT License
6
+ * https://github.com/donotlb/keypal#readme
7
+ */import{nanoid as I}from"nanoid";import{l as d,g as D}from"./shared/keypal.lTVSZWgp.mjs";import{createHash as R,timingSafeEqual as O}from"node:crypto";import{MemoryStore as T}from"./storage/memory.mjs";import{RedisStore as y}from"./storage/redis.mjs";import"./shared/keypal.C-UeOmUF.mjs";function g(s){return s?new Date(s)<=new Date:!1}function L(s){return s?new Date(s):null}const k=["authorization","x-api-key"],S="bearer ";function _(s,e){if(s instanceof Headers)return s.get(e);const t=e.toLowerCase();for(const a in s)if(a.toLowerCase()===t)return s[a]??null;return null}function N(s,e){const t=s.trim();if(!t)return null;const a=t.toLowerCase();return a==="bearer"?null:a.startsWith(S)&&e?t.slice(7).trim()||null:t}function E(s,e={}){const{headerNames:t=k,extractBearer:a=!0}=e;for(const r of t){const o=_(s,r);if(!o)continue;const n=N(o,a);if(n)return n}return null}function m(s,e={}){return E(s,e)!==null}function v(s,e,t){return s?.includes(e)?!0:(t?.resource,!1)}function x(s,e,t){return!s||s.length===0?!1:e.some(a=>s.includes(a))?!0:(t?.resource,!1)}function K(s,e,t){return!s||s.length===0?!1:e.every(a=>s.includes(a))?!0:(t?.resource,!1)}function C(s,e,t,a){return!!(s?.includes(t)||a?.resource&&e&&e[a.resource]?.includes(t))}function B(s,e,t,a){if(s&&t.some(r=>s.includes(r)))return!0;if(a?.resource&&e){const r=e[a.resource];if(r&&t.some(o=>r.includes(o)))return!0}return!1}function b(s,e,t,a){if(s&&t.every(r=>s.includes(r)))return!0;if(a?.resource&&e){const r=e[a.resource];if(!r)return!1;if(t.every(n=>r.includes(n)))return!0;const o=[...s||[],...r];return t.every(n=>o.includes(n))}return!1}class A{resources={};add(e,t,a){const r=`${e}:${t}`;if(this.resources[r]){const o=new Set(this.resources[r]);for(const n of a)o.add(n);this.resources[r]=Array.from(o)}else this.resources[r]=a;return this}addOne(e,t,a){return this.add(e,t,[a])}addMany(e,t,a){for(const r of t)this.add(e,r,a);return this}remove(e,t){const a=`${e}:${t}`;return delete this.resources[a],this}removeScopes(e,t,a){const r=`${e}:${t}`;if(this.resources[r]){const o=new Set(a);this.resources[r]=this.resources[r]?.filter(n=>!o.has(n))??[],this.resources[r]?.length===0&&delete this.resources[r]}return this}has(e,t){return`${e}:${t}`in this.resources}get(e,t){const a=`${e}:${t}`;return this.resources[a]||[]}clear(){return this.resources={},this}build(){return{...this.resources}}static from(e){const t=new A;return t.resources={...e},t}}function G(){return new A}class Y{cache=new Map;maxSize;cleanupTimer=null;constructor(e={}){this.maxSize=e.maxSize??1e4;const t=e.cleanupInterval??6e4;this.cleanupTimer=setInterval(()=>this.cleanup(),t),this.cleanupTimer.unref?.()}get(e){const t=this.cache.get(e);return t?t.expires<Date.now()?(this.cache.delete(e),null):t.value:null}set(e,t,a=60){if(this.cache.size>=this.maxSize&&!this.cache.has(e)&&(this.cleanup(),this.cache.size>=this.maxSize)){const r=this.cache.keys().next().value;r!==void 0&&this.cache.delete(r)}this.cache.set(e,{value:t,expires:Date.now()+a*1e3})}del(e){this.cache.delete(e)}clear(){this.cache.clear()}cleanup(){const e=Date.now();for(const[t,a]of this.cache)a.expires<e&&this.cache.delete(t)}dispose(){this.cleanupTimer&&(clearInterval(this.cleanupTimer),this.cleanupTimer=null)}get size(){return this.cache.size}}class ${client;constructor(e){this.client=e}async get(e){return this.client.get(e)}async set(e,t,a=60){await this.client.setex(e,a,t)}async del(e){await this.client.del(e)}}function p(s,e={}){const{algorithm:t="sha256",salt:a=""}=e,r=a?`${s}${a}`:s;return R(t).update(r).digest("hex")}function F(s,e,t={}){const a=p(s,t);return a.length!==e.length?!1:O(Buffer.from(a),Buffer.from(e))}const i={MISSING_KEY:"MISSING_KEY",INVALID_FORMAT:"INVALID_FORMAT",INVALID_KEY:"INVALID_KEY",EXPIRED:"EXPIRED",REVOKED:"REVOKED",DISABLED:"DISABLED",STORAGE_ERROR:"STORAGE_ERROR",CACHE_ERROR:"CACHE_ERROR",ALREADY_REVOKED:"ALREADY_REVOKED",ALREADY_ENABLED:"ALREADY_ENABLED",ALREADY_DISABLED:"ALREADY_DISABLED",CANNOT_MODIFY_REVOKED:"CANNOT_MODIFY_REVOKED",KEY_NOT_FOUND:"KEY_NOT_FOUND",AUDIT_LOGGING_DISABLED:"AUDIT_LOGGING_DISABLED",STORAGE_NOT_SUPPORTED:"STORAGE_NOT_SUPPORTED"},P={[i.MISSING_KEY]:"Missing API key",[i.INVALID_FORMAT]:"Invalid API key format",[i.INVALID_KEY]:"Invalid API key",[i.EXPIRED]:"API key has expired",[i.REVOKED]:"API key has been revoked",[i.DISABLED]:"API key is disabled",[i.STORAGE_ERROR]:"Storage error occurred",[i.CACHE_ERROR]:"Cache error occurred",[i.ALREADY_REVOKED]:"API key is already revoked",[i.ALREADY_ENABLED]:"API key is already enabled",[i.ALREADY_DISABLED]:"API key is already disabled",[i.CANNOT_MODIFY_REVOKED]:"Cannot modify a revoked key",[i.KEY_NOT_FOUND]:"API key not found",[i.AUDIT_LOGGING_DISABLED]:"Audit logging is not enabled",[i.STORAGE_NOT_SUPPORTED]:"Storage does not support this operation"};function c(s,e){return{code:s,message:P[s],details:e}}function l(s,e){const t=c(s,e);return{valid:!1,error:t.message,errorCode:t.code}}function U(s){if(typeof s!="object"||s===null)return!1;const e=s;return typeof e.id=="string"&&(e.expiresAt===null||typeof e.expiresAt=="string")&&(e.revokedAt===null||typeof e.revokedAt=="string")&&typeof e.enabled=="boolean"}class M{config;storage;cache;cacheTtl;extractionOptions;revokedKeyTtl;isRedisStorage;autoTrackUsage;auditLogsEnabled;defaultContext;constructor(e={}){const t=e.salt?p(e.salt,{algorithm:"sha256"}):"";if(this.config={prefix:e.prefix,length:e.length??32,algorithm:e.algorithm??"sha256",alphabet:e.alphabet,salt:t},this.revokedKeyTtl=e.revokedKeyTtl??604800,this.isRedisStorage=e.storage==="redis",this.autoTrackUsage=e.autoTrackUsage??!0,this.auditLogsEnabled=e.auditLogs??!1,this.defaultContext=e.auditContext,e.storage==="redis"){if(!e.redis)throw new Error('Redis client required when storage is "redis"');try{this.storage=new y({client:e.redis})}catch(a){throw d.error("CRITICAL: Failed to initialize Redis storage:",a),a}}else e.storage&&typeof e.storage=="object"?this.storage=e.storage:this.storage=new T;if(this.cacheTtl=e.cacheTtl??60,this.extractionOptions={headerNames:e.headerNames??["authorization","x-api-key"],extractBearer:e.extractBearer??!0},e.cache==="redis"){if(!e.redis)throw new Error("[keypal] Redis client required when cache is 'redis'");try{this.cache=new $(e.redis)}catch(a){throw d.error("CRITICAL: Failed to initialize Redis cache:",a),a}}else e.cache===!0?this.cache=new Y:e.cache&&typeof e.cache=="object"&&(this.cache=e.cache)}generateKey(){return D({prefix:this.config.prefix,length:this.config.length,alphabet:this.config.alphabet})}hashKey(e){return p(e,{algorithm:this.config.algorithm,salt:this.config.salt})}validateKey(e,t){return F(e,t,{algorithm:this.config.algorithm,salt:this.config.salt})}extractKey(e,t){const a={headerNames:t?.headerNames??this.extractionOptions.headerNames,extractBearer:t?.extractBearer??this.extractionOptions.extractBearer};return E(e,a)}hasKey(e,t){const a={headerNames:t?.headerNames??this.extractionOptions.headerNames,extractBearer:t?.extractBearer??this.extractionOptions.extractBearer};return m(e,a)}async verify(e,t={}){let a;if(typeof e=="string")a=e,e.startsWith("Bearer ")&&(a=e.slice(7).trim());else{const n={headerNames:t.headerNames??this.extractionOptions.headerNames,extractBearer:t.extractBearer??this.extractionOptions.extractBearer};a=this.extractKey(e,n)}if(!a)return l(i.MISSING_KEY);if(this.config.prefix&&!a.startsWith(this.config.prefix))return l(i.INVALID_FORMAT);const r=this.hashKey(a);if(this.cache&&!t.skipCache){const n=await this.cache.get(`apikey:${r}`);if(n)try{const h=JSON.parse(n);if(!U(h))d.error("CRITICAL: Invalid cache record shape, invalidating entry"),await this.cache.del(`apikey:${r}`);else{const u=h;if(u.expiresAt&&g(u.expiresAt))return await this.cache.del(`apikey:${r}`),l(i.EXPIRED);if(u.revokedAt)return await this.cache.del(`apikey:${r}`),l(i.REVOKED);if(u.enabled===!1)return l(i.DISABLED);const f=await this.storage.findById(u.id);return f?g(f.metadata.expiresAt)?(await this.cache.del(`apikey:${r}`),l(i.EXPIRED)):f.metadata.revokedAt?(await this.cache.del(`apikey:${r}`),l(i.REVOKED)):(this.autoTrackUsage&&!t.skipTracking&&this.updateLastUsed(f.id).catch(w=>{d.error("Failed to track usage:",w)}),{valid:!0,record:f}):(await this.cache.del(`apikey:${r}`),l(i.INVALID_KEY))}}catch(h){d.error("CRITICAL: Cache corruption detected, invalidating entry:",h),await this.cache.del(`apikey:${r}`)}}const o=await this.storage.findByHash(r);if(!o)return l(i.INVALID_KEY);if(g(o.metadata.expiresAt))return this.cache&&await this.cache.del(`apikey:${r}`),l(i.EXPIRED);if(o.metadata.revokedAt)return this.cache&&await this.cache.del(`apikey:${r}`),l(i.REVOKED);if(o.metadata.enabled===!1)return l(i.DISABLED);if(this.cache&&!t.skipCache)try{const n={id:o.id,expiresAt:o.metadata.expiresAt??null,revokedAt:o.metadata.revokedAt??null,enabled:o.metadata.enabled??!0};await this.cache.set(`apikey:${r}`,JSON.stringify(n),this.cacheTtl)}catch(n){d.error("CRITICAL: Failed to write to cache:",n)}return this.autoTrackUsage&&!t.skipTracking&&this.updateLastUsed(o.id).catch(n=>{d.error("Failed to track usage:",n)}),{valid:!0,record:o}}async create(e,t){const a=this.generateKey(),r=this.hashKey(a),o=new Date().toISOString(),n=e.tags?.map(u=>u.toLowerCase()),h={id:I(),keyHash:r,metadata:{ownerId:e.ownerId??"",name:e.name,description:e.description,scopes:e.scopes,resources:e.resources,expiresAt:e.expiresAt??null,createdAt:o,lastUsedAt:void 0,enabled:e.enabled??!0,revokedAt:null,rotatedTo:null,tags:n}};return await this.storage.save(h),await this.logAction("created",h.id,h.metadata.ownerId,{...t,metadata:{name:h.metadata.name,scopes:h.metadata.scopes,...t?.metadata}}),{key:a,record:h}}async findByHash(e){return await this.storage.findByHash(e)}async findById(e){return await this.storage.findById(e)}async findByTags(e,t){return await this.storage.findByTags(e,t)}async findByTag(e,t){return await this.storage.findByTag(e,t)}async list(e){return await this.storage.findByOwner(e)}async revoke(e,t){const a=await this.findById(e);if(!a)throw c(i.KEY_NOT_FOUND);if(a.metadata.revokedAt)throw c(i.ALREADY_REVOKED);if(await this.storage.updateMetadata(e,{revokedAt:new Date().toISOString()}),await this.logAction("revoked",e,a.metadata.ownerId,t),this.cache)try{await this.cache.del(`apikey:${a.keyHash}`)}catch(r){d.error("CRITICAL: Failed to invalidate cache on revoke:",r)}if(this.isRedisStorage&&this.revokedKeyTtl>0)try{this.storage instanceof y&&await this.storage.setTtl(e,this.revokedKeyTtl)}catch(r){d.error("Failed to set TTL on revoked key:",r)}}async revokeAll(e){const t=await this.list(e);await Promise.all(t.map(a=>this.revoke(a.id)))}async enable(e,t){const a=await this.findById(e);if(!a)throw c(i.KEY_NOT_FOUND);if(a.metadata.revokedAt)throw c(i.CANNOT_MODIFY_REVOKED);if(a.metadata.enabled)throw c(i.ALREADY_ENABLED);if(await this.storage.updateMetadata(e,{enabled:!0}),await this.logAction("enabled",e,a.metadata.ownerId,t),this.cache)try{await this.cache.del(`apikey:${a.keyHash}`)}catch(r){d.error("CRITICAL: Failed to invalidate cache on enable:",r)}}async disable(e,t){const a=await this.findById(e);if(!a)throw c(i.KEY_NOT_FOUND);if(a.metadata.revokedAt)throw c(i.CANNOT_MODIFY_REVOKED);if(!a.metadata.enabled)throw c(i.ALREADY_DISABLED);if(await this.storage.updateMetadata(e,{enabled:!1}),await this.logAction("disabled",e,a.metadata.ownerId,t),this.cache)try{await this.cache.del(`apikey:${a.keyHash}`)}catch(r){d.error("CRITICAL: Failed to invalidate cache on disable:",r)}}async rotate(e,t,a){const r=await this.findById(e);if(!r)throw c(i.KEY_NOT_FOUND);if(r.metadata.revokedAt)throw c(i.CANNOT_MODIFY_REVOKED);const{key:o,record:n}=await this.create({ownerId:r.metadata.ownerId,name:t?.name??r.metadata.name,description:t?.description??r.metadata.description,scopes:t?.scopes??r.metadata.scopes,resources:t?.resources??r.metadata.resources,expiresAt:t?.expiresAt??r.metadata.expiresAt,tags:t?.tags?t.tags.map(h=>h.toLowerCase()):r.metadata.tags});if(await this.storage.updateMetadata(e,{rotatedTo:n.id,revokedAt:new Date().toISOString()}),await this.logAction("rotated",e,r.metadata.ownerId,{...a,metadata:{rotatedTo:n.id,...a?.metadata}}),this.cache)try{await this.cache.del(`apikey:${r.keyHash}`)}catch(h){d.error("CRITICAL: Failed to invalidate cache on rotate:",h)}if(this.isRedisStorage&&this.revokedKeyTtl>0)try{this.storage instanceof y&&await this.storage.setTtl(e,this.revokedKeyTtl)}catch(h){d.error("Failed to set TTL on rotated key:",h)}return{key:o,record:n,oldRecord:r}}async updateLastUsed(e){await this.storage.updateMetadata(e,{lastUsedAt:new Date().toISOString()})}async logAction(e,t,a,r){if(!(this.auditLogsEnabled&&this.storage.saveLog))return;const o={...this.defaultContext,...r,...this.defaultContext?.metadata||r?.metadata?{metadata:{...this.defaultContext?.metadata,...r?.metadata}}:{}},n={id:I(),action:e,keyId:t,ownerId:a,timestamp:new Date().toISOString(),data:Object.keys(o).length>0?o:void 0};try{await this.storage.saveLog(n)}catch(h){d.error("Failed to save audit log:",h)}}async getLogs(e={}){if(!this.auditLogsEnabled)throw c(i.AUDIT_LOGGING_DISABLED);if(!this.storage.findLogs)throw c(i.STORAGE_NOT_SUPPORTED);return await this.storage.findLogs(e)}async countLogs(e={}){if(!this.auditLogsEnabled)throw c(i.AUDIT_LOGGING_DISABLED);if(!this.storage.countLogs)throw c(i.STORAGE_NOT_SUPPORTED);return await this.storage.countLogs(e)}async deleteLogs(e){if(!this.auditLogsEnabled)throw c(i.AUDIT_LOGGING_DISABLED);if(!this.storage.deleteLogs)throw c(i.STORAGE_NOT_SUPPORTED);return await this.storage.deleteLogs(e)}async clearLogs(e){return await this.deleteLogs({keyId:e})}async getLogStats(e){if(!this.auditLogsEnabled)throw c(i.AUDIT_LOGGING_DISABLED);if(!this.storage.getLogStats)throw c(i.STORAGE_NOT_SUPPORTED);return await this.storage.getLogStats(e)}async invalidateCache(e){if(this.cache)try{await this.cache.del(`apikey:${e}`)}catch(t){throw d.error("CRITICAL: Failed to invalidate cache:",t),t}}isExpired(e){return g(e.metadata.expiresAt)}hasScope(e,t,a){return C(e.metadata.scopes,e.metadata.resources,t,a)}hasAnyScope(e,t,a){return B(e.metadata.scopes,e.metadata.resources,t,a)}hasAllScopes(e,t,a){return b(e.metadata.scopes,e.metadata.resources,t,a)}async verifyFromHeaders(e,t){const a=await this.verify(e,t);return a.valid?a.record??null:null}checkResourceScope(e,t,a,r){return e?this.hasScope(e,r,{resource:`${t}:${a}`}):!1}checkResourceAnyScope(e,t,a,r){return e?this.hasAnyScope(e,r,{resource:`${t}:${a}`}):!1}checkResourceAllScopes(e,t,a,r){return e?this.hasAllScopes(e,r,{resource:`${t}:${a}`}):!1}}function V(s={}){return new M(s)}export{i as ApiKeyErrorCode,A as ResourceBuilder,c as createApiKeyError,V as createKeys,G as createResourceBuilder,E as extractKeyFromHeaders,L as getExpirationTime,K as hasAllScopes,x as hasAnyScope,m as hasApiKey,v as hasScope,g as isExpired};
@@ -0,0 +1,7 @@
1
+ /*!
2
+ * @donotlb/keypal v0.1.0
3
+ * A TypeScript library for secure API key management with cryptographic hashing, expiration, scopes, and pluggable storage
4
+ * © 2026 "donotlb" <donotlb@gmail.com>
5
+ * Released under the MIT License
6
+ * https://github.com/donotlb/keypal#readme
7
+ */const n=100;function i(c){const a={};let t=null;for(const o of c)a[o.action]=(a[o.action]||0)+1,(!t||o.timestamp>t)&&(t=o.timestamp);return{total:c.length,byAction:a,lastActivity:t}}export{n as D,i as c};