@exortek/apikey 1.0.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/CHANGELOG.md +38 -0
- package/LICENSE +21 -0
- package/README.md +263 -0
- package/dist/errors.d.ts +20 -0
- package/dist/index.cjs +1632 -0
- package/dist/index.d.ts +278 -0
- package/dist/index.mjs +1617 -0
- package/dist/internal/guards.d.ts +4 -0
- package/dist/middleware/core.d.ts +86 -0
- package/dist/middleware/express.cjs +1363 -0
- package/dist/middleware/express.d.ts +4 -0
- package/dist/middleware/express.mjs +1361 -0
- package/dist/middleware/fastify.cjs +1374 -0
- package/dist/middleware/fastify.d.ts +1 -0
- package/dist/middleware/fastify.mjs +1372 -0
- package/dist/scopes.d.ts +21 -0
- package/dist/stores/index.d.ts +2 -0
- package/dist/stores/memory.d.ts +6 -0
- package/dist/stores/redis.d.ts +13 -0
- package/dist/stores.cjs +880 -0
- package/dist/stores.mjs +877 -0
- package/dist/token.d.ts +88 -0
- package/package.json +98 -0
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @typedef {object} ApiKeyRecord
|
|
3
|
+
* @property {string} id Plaintext lookup key (128-bit b64u).
|
|
4
|
+
* @property {string} hash Storage hash of the secret (43-char b64u).
|
|
5
|
+
* @property {string} prefix The prefix the key was minted with.
|
|
6
|
+
* @property {string} userId
|
|
7
|
+
* @property {string[]} scopes
|
|
8
|
+
* @property {string} [name] Human label ("Production Backend").
|
|
9
|
+
* @property {string} [environment] `'live'` / `'test'` / caller-defined.
|
|
10
|
+
* @property {Record<string, unknown>} [metadata]
|
|
11
|
+
* @property {number} createdAt ms epoch.
|
|
12
|
+
* @property {number} [expiresAt] ms epoch; absent = no expiry.
|
|
13
|
+
* @property {number} [revokedAt] ms epoch when the record was revoked.
|
|
14
|
+
* @property {string} [revokedReason]
|
|
15
|
+
* @property {number} [lastUsedAt] ms epoch of the last successful verify.
|
|
16
|
+
* @property {number} [pepperVersion] Index into the peppers array that was used at mint. 0 = newest.
|
|
17
|
+
*/
|
|
18
|
+
/**
|
|
19
|
+
* @typedef {object} ApiKeyStore
|
|
20
|
+
* @property {(record: ApiKeyRecord) => Promise<void>} put
|
|
21
|
+
* @property {(id: string) => Promise<ApiKeyRecord | null>} getById
|
|
22
|
+
* @property {(id: string, patch: Partial<ApiKeyRecord>) => Promise<ApiKeyRecord | null>} update
|
|
23
|
+
* @property {(id: string, reason?: string) => Promise<boolean>} revoke
|
|
24
|
+
* @property {(userId: string, reason?: string) => Promise<number>} revokeAllForUser
|
|
25
|
+
* @property {(userId: string) => Promise<ApiKeyRecord[]>} listByUser
|
|
26
|
+
*/
|
|
27
|
+
/**
|
|
28
|
+
* @typedef {object} CreateApiKeyOptions
|
|
29
|
+
* @property {string} prefix Stripe-style; `sk_live`, `pk_test`, `svc_prod_v2`.
|
|
30
|
+
* @property {string} userId
|
|
31
|
+
* @property {string[]} scopes
|
|
32
|
+
* @property {string} [name]
|
|
33
|
+
* @property {string} [environment]
|
|
34
|
+
* @property {Record<string, unknown>} [metadata]
|
|
35
|
+
* @property {string | number} [expiresIn] `'1y'` / `'30d'` / ms integer. Omit for no expiry.
|
|
36
|
+
* @property {ApiKeyStore} store
|
|
37
|
+
* @property {(Buffer | Uint8Array | string)[]} [peppers]
|
|
38
|
+
* Newest first. Each ≥16 bytes. Omit for plain SHA-256 storage.
|
|
39
|
+
* @property {number} [now] Override `Date.now()` for testing.
|
|
40
|
+
*/
|
|
41
|
+
/**
|
|
42
|
+
* @typedef {object} CreateApiKeyResult
|
|
43
|
+
* @property {string} key Wire token — show ONCE to the caller and never again.
|
|
44
|
+
* @property {string} id Plaintext lookup key; safe to store / log / display.
|
|
45
|
+
* @property {ApiKeyRecord} record What was persisted to the store.
|
|
46
|
+
*/
|
|
47
|
+
/**
|
|
48
|
+
* @typedef {object} VerifyApiKeyOptions
|
|
49
|
+
* @property {ApiKeyStore} store
|
|
50
|
+
* @property {(Buffer | Uint8Array | string)[]} [peppers]
|
|
51
|
+
* @property {string[]} [requiredScopes]
|
|
52
|
+
* @property {string} [expectedPrefix] Reject a valid key whose prefix differs.
|
|
53
|
+
* @property {boolean} [updateLastUsed=false] Bump `lastUsedAt` on success (one extra store write).
|
|
54
|
+
* @property {number} [now]
|
|
55
|
+
*/
|
|
56
|
+
/**
|
|
57
|
+
* @typedef {'malformed' | 'not_found' | 'expired' | 'revoked'
|
|
58
|
+
* | 'bad_secret' | 'prefix_mismatch' | 'missing_scope'
|
|
59
|
+
* | 'store_unavailable'} VerifyApiKeyFailureReason
|
|
60
|
+
*/
|
|
61
|
+
/**
|
|
62
|
+
* @typedef {{
|
|
63
|
+
* valid: true,
|
|
64
|
+
* id: string,
|
|
65
|
+
* userId: string,
|
|
66
|
+
* scopes: string[],
|
|
67
|
+
* prefix: string,
|
|
68
|
+
* name?: string,
|
|
69
|
+
* environment?: string,
|
|
70
|
+
* metadata?: Record<string, unknown>,
|
|
71
|
+
* needsRehash?: boolean,
|
|
72
|
+
* } | { valid: false, reason: VerifyApiKeyFailureReason }} VerifyApiKeyResult
|
|
73
|
+
*/
|
|
74
|
+
/**
|
|
75
|
+
* Mint a new API key. The wire `key` is returned once — the caller
|
|
76
|
+
* MUST show it to the end user immediately and never persist it in
|
|
77
|
+
* the clear (only the storage `hash` inside the returned `record` is
|
|
78
|
+
* safe to keep).
|
|
79
|
+
*
|
|
80
|
+
* @param {CreateApiKeyOptions} options
|
|
81
|
+
* @returns {Promise<CreateApiKeyResult>}
|
|
82
|
+
*/
|
|
83
|
+
export function createApiKey(options: CreateApiKeyOptions): Promise<CreateApiKeyResult>;
|
|
84
|
+
/**
|
|
85
|
+
* Verify a raw API key. Returns `{ valid: true, ... }` on success, or
|
|
86
|
+
* `{ valid: false, reason }` on any expected failure. Never throws on
|
|
87
|
+
* a bad key — a wrong or stale key is a normal auth outcome.
|
|
88
|
+
*
|
|
89
|
+
* On success, callers get the stored `userId` / `scopes` / `metadata`
|
|
90
|
+
* and can pass them straight to their app's auth context.
|
|
91
|
+
* `needsRehash: true` in the success result signals the secret matched
|
|
92
|
+
* an older pepper — call `rehashApiKey(id, options)` to silently
|
|
93
|
+
* migrate storage to the newest pepper on the next natural chance.
|
|
94
|
+
*
|
|
95
|
+
* @param {string} rawKey
|
|
96
|
+
* @param {VerifyApiKeyOptions} options
|
|
97
|
+
* @returns {Promise<VerifyApiKeyResult>}
|
|
98
|
+
*/
|
|
99
|
+
export function verifyApiKey(rawKey: string, options: VerifyApiKeyOptions): Promise<VerifyApiKeyResult>;
|
|
100
|
+
/**
|
|
101
|
+
* Revoke an API key by id or by the raw wire key. Returns `true` if
|
|
102
|
+
* a record was actually revoked, `false` if the id was unknown or
|
|
103
|
+
* the record was already revoked.
|
|
104
|
+
*
|
|
105
|
+
* @param {string} keyOrId Either the raw wire key or its id half.
|
|
106
|
+
* @param {{ store: ApiKeyStore, reason?: string }} options
|
|
107
|
+
* @returns {Promise<boolean>}
|
|
108
|
+
*/
|
|
109
|
+
export function revokeApiKey(keyOrId: string, options: {
|
|
110
|
+
store: ApiKeyStore;
|
|
111
|
+
reason?: string;
|
|
112
|
+
}): Promise<boolean>;
|
|
113
|
+
/**
|
|
114
|
+
* Revoke every non-revoked key belonging to `userId`. Useful on
|
|
115
|
+
* password reset, account termination, etc. Returns the number of
|
|
116
|
+
* records actually revoked.
|
|
117
|
+
*
|
|
118
|
+
* @param {string} userId
|
|
119
|
+
* @param {{ store: ApiKeyStore, reason?: string }} options
|
|
120
|
+
* @returns {Promise<number>}
|
|
121
|
+
*/
|
|
122
|
+
export function revokeAllForUser(userId: string, options: {
|
|
123
|
+
store: ApiKeyStore;
|
|
124
|
+
reason?: string;
|
|
125
|
+
}): Promise<number>;
|
|
126
|
+
/**
|
|
127
|
+
* List every key belonging to `userId` — for a "manage API keys" UI.
|
|
128
|
+
* Callers should hide the storage `hash` before rendering; only the
|
|
129
|
+
* plaintext `id` is safe to display. Returns most-recently-used first
|
|
130
|
+
* (falling back to createdAt) so active keys float to the top.
|
|
131
|
+
*
|
|
132
|
+
* @param {string} userId
|
|
133
|
+
* @param {{ store: ApiKeyStore }} options
|
|
134
|
+
* @returns {Promise<ApiKeyRecord[]>}
|
|
135
|
+
*/
|
|
136
|
+
export function listApiKeys(userId: string, options: {
|
|
137
|
+
store: ApiKeyStore;
|
|
138
|
+
}): Promise<ApiKeyRecord[]>;
|
|
139
|
+
/**
|
|
140
|
+
* Migrate the storage hash of an existing key to the newest pepper.
|
|
141
|
+
* Requires the raw wire key (the plaintext secret is the input to the
|
|
142
|
+
* new HMAC). Called opportunistically when `verifyApiKey` reports
|
|
143
|
+
* `needsRehash: true`.
|
|
144
|
+
*
|
|
145
|
+
* @param {string} rawKey
|
|
146
|
+
* @param {{ store: ApiKeyStore, peppers: (Buffer | Uint8Array | string)[] }} options
|
|
147
|
+
* @returns {Promise<boolean>}
|
|
148
|
+
*/
|
|
149
|
+
export function rehashApiKey(rawKey: string, options: {
|
|
150
|
+
store: ApiKeyStore;
|
|
151
|
+
peppers: (Buffer | Uint8Array | string)[];
|
|
152
|
+
}): Promise<boolean>;
|
|
153
|
+
export { maskKey as maskApiKey };
|
|
154
|
+
export type ApiKeyRecord = {
|
|
155
|
+
/**
|
|
156
|
+
* Plaintext lookup key (128-bit b64u).
|
|
157
|
+
*/
|
|
158
|
+
id: string;
|
|
159
|
+
/**
|
|
160
|
+
* Storage hash of the secret (43-char b64u).
|
|
161
|
+
*/
|
|
162
|
+
hash: string;
|
|
163
|
+
/**
|
|
164
|
+
* The prefix the key was minted with.
|
|
165
|
+
*/
|
|
166
|
+
prefix: string;
|
|
167
|
+
userId: string;
|
|
168
|
+
scopes: string[];
|
|
169
|
+
/**
|
|
170
|
+
* Human label ("Production Backend").
|
|
171
|
+
*/
|
|
172
|
+
name?: string | undefined;
|
|
173
|
+
/**
|
|
174
|
+
* `'live'` / `'test'` / caller-defined.
|
|
175
|
+
*/
|
|
176
|
+
environment?: string | undefined;
|
|
177
|
+
metadata?: Record<string, unknown> | undefined;
|
|
178
|
+
/**
|
|
179
|
+
* ms epoch.
|
|
180
|
+
*/
|
|
181
|
+
createdAt: number;
|
|
182
|
+
/**
|
|
183
|
+
* ms epoch; absent = no expiry.
|
|
184
|
+
*/
|
|
185
|
+
expiresAt?: number | undefined;
|
|
186
|
+
/**
|
|
187
|
+
* ms epoch when the record was revoked.
|
|
188
|
+
*/
|
|
189
|
+
revokedAt?: number | undefined;
|
|
190
|
+
revokedReason?: string | undefined;
|
|
191
|
+
/**
|
|
192
|
+
* ms epoch of the last successful verify.
|
|
193
|
+
*/
|
|
194
|
+
lastUsedAt?: number | undefined;
|
|
195
|
+
/**
|
|
196
|
+
* Index into the peppers array that was used at mint. 0 = newest.
|
|
197
|
+
*/
|
|
198
|
+
pepperVersion?: number | undefined;
|
|
199
|
+
};
|
|
200
|
+
export type ApiKeyStore = {
|
|
201
|
+
put: (record: ApiKeyRecord) => Promise<void>;
|
|
202
|
+
getById: (id: string) => Promise<ApiKeyRecord | null>;
|
|
203
|
+
update: (id: string, patch: Partial<ApiKeyRecord>) => Promise<ApiKeyRecord | null>;
|
|
204
|
+
revoke: (id: string, reason?: string) => Promise<boolean>;
|
|
205
|
+
revokeAllForUser: (userId: string, reason?: string) => Promise<number>;
|
|
206
|
+
listByUser: (userId: string) => Promise<ApiKeyRecord[]>;
|
|
207
|
+
};
|
|
208
|
+
export type CreateApiKeyOptions = {
|
|
209
|
+
/**
|
|
210
|
+
* Stripe-style; `sk_live`, `pk_test`, `svc_prod_v2`.
|
|
211
|
+
*/
|
|
212
|
+
prefix: string;
|
|
213
|
+
userId: string;
|
|
214
|
+
scopes: string[];
|
|
215
|
+
name?: string | undefined;
|
|
216
|
+
environment?: string | undefined;
|
|
217
|
+
metadata?: Record<string, unknown> | undefined;
|
|
218
|
+
/**
|
|
219
|
+
* `'1y'` / `'30d'` / ms integer. Omit for no expiry.
|
|
220
|
+
*/
|
|
221
|
+
expiresIn?: string | number | undefined;
|
|
222
|
+
store: ApiKeyStore;
|
|
223
|
+
/**
|
|
224
|
+
* Newest first. Each ≥16 bytes. Omit for plain SHA-256 storage.
|
|
225
|
+
*/
|
|
226
|
+
peppers?: (string | Buffer | Uint8Array<ArrayBufferLike>)[] | undefined;
|
|
227
|
+
/**
|
|
228
|
+
* Override `Date.now()` for testing.
|
|
229
|
+
*/
|
|
230
|
+
now?: number | undefined;
|
|
231
|
+
};
|
|
232
|
+
export type CreateApiKeyResult = {
|
|
233
|
+
/**
|
|
234
|
+
* Wire token — show ONCE to the caller and never again.
|
|
235
|
+
*/
|
|
236
|
+
key: string;
|
|
237
|
+
/**
|
|
238
|
+
* Plaintext lookup key; safe to store / log / display.
|
|
239
|
+
*/
|
|
240
|
+
id: string;
|
|
241
|
+
/**
|
|
242
|
+
* What was persisted to the store.
|
|
243
|
+
*/
|
|
244
|
+
record: ApiKeyRecord;
|
|
245
|
+
};
|
|
246
|
+
export type VerifyApiKeyOptions = {
|
|
247
|
+
store: ApiKeyStore;
|
|
248
|
+
peppers?: (string | Buffer | Uint8Array<ArrayBufferLike>)[] | undefined;
|
|
249
|
+
requiredScopes?: string[] | undefined;
|
|
250
|
+
/**
|
|
251
|
+
* Reject a valid key whose prefix differs.
|
|
252
|
+
*/
|
|
253
|
+
expectedPrefix?: string | undefined;
|
|
254
|
+
/**
|
|
255
|
+
* Bump `lastUsedAt` on success (one extra store write).
|
|
256
|
+
*/
|
|
257
|
+
updateLastUsed?: boolean | undefined;
|
|
258
|
+
now?: number | undefined;
|
|
259
|
+
};
|
|
260
|
+
export type VerifyApiKeyFailureReason = "malformed" | "not_found" | "expired" | "revoked" | "bad_secret" | "prefix_mismatch" | "missing_scope" | "store_unavailable";
|
|
261
|
+
export type VerifyApiKeyResult = {
|
|
262
|
+
valid: true;
|
|
263
|
+
id: string;
|
|
264
|
+
userId: string;
|
|
265
|
+
scopes: string[];
|
|
266
|
+
prefix: string;
|
|
267
|
+
name?: string;
|
|
268
|
+
environment?: string;
|
|
269
|
+
metadata?: Record<string, unknown>;
|
|
270
|
+
needsRehash?: boolean;
|
|
271
|
+
} | {
|
|
272
|
+
valid: false;
|
|
273
|
+
reason: VerifyApiKeyFailureReason;
|
|
274
|
+
};
|
|
275
|
+
import { mask as maskKey } from './token.js';
|
|
276
|
+
export { ApiKeyError, ErrorCode } from "./errors.js";
|
|
277
|
+
export { covers, hasAll, hasAny } from "./scopes.js";
|
|
278
|
+
export { parseApiKey, mask } from "./token.js";
|