@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.
@@ -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";