@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.
- package/LICENSE +22 -0
- package/README.md +1037 -0
- package/dist/drizzle/schema.d.mts +87 -0
- package/dist/drizzle/schema.d.ts +87 -0
- package/dist/drizzle/schema.mjs +7 -0
- package/dist/index.d.mts +606 -0
- package/dist/index.d.ts +606 -0
- package/dist/index.mjs +7 -0
- package/dist/shared/keypal.C-UeOmUF.mjs +7 -0
- package/dist/shared/keypal.kItV-5pB.d.mts +194 -0
- package/dist/shared/keypal.kItV-5pB.d.ts +194 -0
- package/dist/shared/keypal.lTVSZWgp.mjs +7 -0
- package/dist/storage/drizzle.d.mts +192 -0
- package/dist/storage/drizzle.d.ts +192 -0
- package/dist/storage/drizzle.mjs +7 -0
- package/dist/storage/memory.d.mts +27 -0
- package/dist/storage/memory.d.ts +27 -0
- package/dist/storage/memory.mjs +7 -0
- package/dist/storage/redis.d.mts +42 -0
- package/dist/storage/redis.d.ts +42 -0
- package/dist/storage/redis.mjs +7 -0
- package/package.json +122 -0
package/dist/index.d.ts
ADDED
|
@@ -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};
|