@exortek/apikey 1.1.2 → 1.1.3
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 +14 -0
- package/dist/index.d.ts +145 -18
- package/dist/middleware/express.d.ts +96 -1
- package/dist/middleware/fastify.d.ts +3 -1
- package/dist/stores/custom.d.ts +1 -8
- package/dist/stores/index.d.ts +79 -3
- package/dist/stores.cjs +83 -21
- package/dist/stores.mjs +83 -21
- package/dist/token.d.ts +2 -1
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
# @exortek/apikey
|
|
2
2
|
|
|
3
|
+
## 1.1.3
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 89aea87: Ship self-contained TypeScript declarations. Every package's emitted `.d.ts` referenced `@exortek/shared`
|
|
8
|
+
(e.g. `import { BaseError } from '@exortek/shared/errors'`), but `@exortek/shared` is a private, never-published
|
|
9
|
+
workspace package that is inlined into each bundle at build time. A TypeScript consumer therefore hit
|
|
10
|
+
`Cannot find module '@exortek/shared/…'` (with `skipLibCheck` off) or silently degraded error-class types like
|
|
11
|
+
`ApiKeyError` — losing its constructor signature and `.code` / `.message` — with `skipLibCheck` on.
|
|
12
|
+
|
|
13
|
+
The build now runs a declaration-bundling pass (`rollup-plugin-dts`) after `tsc`, flattening each entry's `.d.ts` and
|
|
14
|
+
inlining the `@exortek/shared` types so the shipped declarations are fully self-contained. Runtime deps and `node:*`
|
|
15
|
+
stay external. No runtime or API change — types only.
|
|
16
|
+
|
|
3
17
|
## 1.1.2
|
|
4
18
|
|
|
5
19
|
### Patch Changes
|
package/dist/index.d.ts
CHANGED
|
@@ -1,3 +1,131 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Parse a raw key without verifying it. Returns the segments, or
|
|
3
|
+
* `null` when the shape is wrong. Never throws.
|
|
4
|
+
*
|
|
5
|
+
* Callers can use `parseApiKey(key).prefix` to route by key family
|
|
6
|
+
* in a middleware before hitting the store — but the parse result
|
|
7
|
+
* MUST NOT be trusted as authenticated. Only `verifyApiKey` proves
|
|
8
|
+
* the holder possesses the real secret.
|
|
9
|
+
*
|
|
10
|
+
* @param {string} key
|
|
11
|
+
* @returns {{ prefix: string, id: string, secret: string } | null}
|
|
12
|
+
*/
|
|
13
|
+
declare function parseApiKey(key: string): {
|
|
14
|
+
prefix: string;
|
|
15
|
+
id: string;
|
|
16
|
+
secret: string;
|
|
17
|
+
} | null;
|
|
18
|
+
/**
|
|
19
|
+
* Log-safe display for a key. Returns `<prefix>_<first-N-of-id>…<last-M-of-secret>`
|
|
20
|
+
* so the value is recognisable in an audit log without disclosing the
|
|
21
|
+
* secret. The default masks all but the first 6 chars of the id and
|
|
22
|
+
* the last 4 chars of the secret.
|
|
23
|
+
*
|
|
24
|
+
* @param {string} key
|
|
25
|
+
* @returns {string}
|
|
26
|
+
*/
|
|
27
|
+
declare function mask(key: string): string;
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Shared base error class — the single error structure behind every
|
|
31
|
+
* `@exortek/*` package's `errors.js`.
|
|
32
|
+
*
|
|
33
|
+
* Every package keeps its own class identity with a one-liner subclass;
|
|
34
|
+
* codes stay per-package frozen maps, status mapping is declared as a
|
|
35
|
+
* static field:
|
|
36
|
+
*
|
|
37
|
+
* import { BaseError } from '@exortek/shared/errors';
|
|
38
|
+
*
|
|
39
|
+
* export const ErrorCode = Object.freeze({
|
|
40
|
+
* INVALID_ARGUMENT: 'INVALID_ARGUMENT',
|
|
41
|
+
* INVALID_TOKEN: 'INVALID_TOKEN',
|
|
42
|
+
* });
|
|
43
|
+
*
|
|
44
|
+
* export class JwtError extends BaseError {
|
|
45
|
+
* static statuses = { INVALID_ARGUMENT: 400, INVALID_TOKEN: 401 };
|
|
46
|
+
* static defaultStatus = 500;
|
|
47
|
+
* }
|
|
48
|
+
*
|
|
49
|
+
* Instances carry a stable machine-readable `code` (branch on this,
|
|
50
|
+
* never on the message), an optional HTTP `status`, an optional
|
|
51
|
+
* `details` object, and the standard `cause` chain.
|
|
52
|
+
*/
|
|
53
|
+
declare class BaseError extends Error {
|
|
54
|
+
/**
|
|
55
|
+
* Optional `code → HTTP status` map declared on the subclass. When
|
|
56
|
+
* absent the instance carries no `status` at all — for HTTP-agnostic
|
|
57
|
+
* packages like `@exortek/crypto`.
|
|
58
|
+
*
|
|
59
|
+
* @type {Record<string, number> | undefined}
|
|
60
|
+
*/
|
|
61
|
+
static statuses: Record<string, number> | undefined;
|
|
62
|
+
/**
|
|
63
|
+
* Fallback status for codes missing from `statuses`.
|
|
64
|
+
*
|
|
65
|
+
* @type {number}
|
|
66
|
+
*/
|
|
67
|
+
static defaultStatus: number;
|
|
68
|
+
/**
|
|
69
|
+
* @param {string} code Stable machine-readable code; branch on this.
|
|
70
|
+
* @param {string} message Human-readable diagnostic. Free-form; may
|
|
71
|
+
* change across versions.
|
|
72
|
+
* @param {{ cause?: unknown, status?: number, details?: Record<string, unknown> }} [options]
|
|
73
|
+
*/
|
|
74
|
+
constructor(code: string, message: string, options?: {
|
|
75
|
+
cause?: unknown;
|
|
76
|
+
status?: number;
|
|
77
|
+
details?: Record<string, unknown>;
|
|
78
|
+
});
|
|
79
|
+
/** @type {string} */
|
|
80
|
+
code: string;
|
|
81
|
+
/** @type {number | undefined} */
|
|
82
|
+
status: number | undefined;
|
|
83
|
+
/** @type {Record<string, unknown> | undefined} */
|
|
84
|
+
details: Record<string, unknown> | undefined;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
declare const ErrorCode: Readonly<{
|
|
88
|
+
INVALID_ARGUMENT: "INVALID_ARGUMENT";
|
|
89
|
+
INVALID_PREFIX: "INVALID_PREFIX";
|
|
90
|
+
INVALID_PEPPER: "INVALID_PEPPER";
|
|
91
|
+
STORE_ERROR: "STORE_ERROR";
|
|
92
|
+
}>;
|
|
93
|
+
/**
|
|
94
|
+
* Every recoverable failure raised by this package. Carries a stable
|
|
95
|
+
* `code` (from {@link ErrorCode}) and a `status` — the HTTP response
|
|
96
|
+
* status a middleware layer would use when translating the error.
|
|
97
|
+
*/
|
|
98
|
+
declare class ApiKeyError extends BaseError {
|
|
99
|
+
static statuses: {
|
|
100
|
+
INVALID_ARGUMENT: number;
|
|
101
|
+
INVALID_PREFIX: number;
|
|
102
|
+
INVALID_PEPPER: number;
|
|
103
|
+
STORE_ERROR: number;
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Does `granted` cover a single `required` scope?
|
|
109
|
+
* @param {string[]} granted
|
|
110
|
+
* @param {string} required
|
|
111
|
+
* @returns {boolean}
|
|
112
|
+
*/
|
|
113
|
+
declare function covers(granted: string[], required: string): boolean;
|
|
114
|
+
/**
|
|
115
|
+
* All required scopes must be covered by granted.
|
|
116
|
+
* @param {string[]} granted
|
|
117
|
+
* @param {string[]} required
|
|
118
|
+
* @returns {boolean}
|
|
119
|
+
*/
|
|
120
|
+
declare function hasAll(granted: string[], required: string[]): boolean;
|
|
121
|
+
/**
|
|
122
|
+
* At least one required scope must be covered.
|
|
123
|
+
* @param {string[]} granted
|
|
124
|
+
* @param {string[]} required
|
|
125
|
+
* @returns {boolean}
|
|
126
|
+
*/
|
|
127
|
+
declare function hasAny(granted: string[], required: string[]): boolean;
|
|
128
|
+
|
|
1
129
|
/**
|
|
2
130
|
* @typedef {object} ApiKeyRecord
|
|
3
131
|
* @property {string} id Plaintext lookup key (128-bit b64u).
|
|
@@ -80,7 +208,7 @@
|
|
|
80
208
|
* @param {CreateApiKeyOptions} options
|
|
81
209
|
* @returns {Promise<CreateApiKeyResult>}
|
|
82
210
|
*/
|
|
83
|
-
|
|
211
|
+
declare function createApiKey(options: CreateApiKeyOptions): Promise<CreateApiKeyResult>;
|
|
84
212
|
/**
|
|
85
213
|
* Verify a raw API key. Returns `{ valid: true, ... }` on success, or
|
|
86
214
|
* `{ valid: false, reason }` on any expected failure. Never throws on
|
|
@@ -96,7 +224,7 @@ export function createApiKey(options: CreateApiKeyOptions): Promise<CreateApiKey
|
|
|
96
224
|
* @param {VerifyApiKeyOptions} options
|
|
97
225
|
* @returns {Promise<VerifyApiKeyResult>}
|
|
98
226
|
*/
|
|
99
|
-
|
|
227
|
+
declare function verifyApiKey(rawKey: string, options: VerifyApiKeyOptions): Promise<VerifyApiKeyResult>;
|
|
100
228
|
/**
|
|
101
229
|
* Revoke an API key by id or by the raw wire key. Returns `true` if
|
|
102
230
|
* a record was actually revoked, `false` if the id was unknown or
|
|
@@ -106,7 +234,7 @@ export function verifyApiKey(rawKey: string, options: VerifyApiKeyOptions): Prom
|
|
|
106
234
|
* @param {{ store: ApiKeyStore, reason?: string }} options
|
|
107
235
|
* @returns {Promise<boolean>}
|
|
108
236
|
*/
|
|
109
|
-
|
|
237
|
+
declare function revokeApiKey(keyOrId: string, options: {
|
|
110
238
|
store: ApiKeyStore;
|
|
111
239
|
reason?: string;
|
|
112
240
|
}): Promise<boolean>;
|
|
@@ -119,7 +247,7 @@ export function revokeApiKey(keyOrId: string, options: {
|
|
|
119
247
|
* @param {{ store: ApiKeyStore, reason?: string }} options
|
|
120
248
|
* @returns {Promise<number>}
|
|
121
249
|
*/
|
|
122
|
-
|
|
250
|
+
declare function revokeAllForUser(userId: string, options: {
|
|
123
251
|
store: ApiKeyStore;
|
|
124
252
|
reason?: string;
|
|
125
253
|
}): Promise<number>;
|
|
@@ -133,7 +261,7 @@ export function revokeAllForUser(userId: string, options: {
|
|
|
133
261
|
* @param {{ store: ApiKeyStore }} options
|
|
134
262
|
* @returns {Promise<ApiKeyRecord[]>}
|
|
135
263
|
*/
|
|
136
|
-
|
|
264
|
+
declare function listApiKeys(userId: string, options: {
|
|
137
265
|
store: ApiKeyStore;
|
|
138
266
|
}): Promise<ApiKeyRecord[]>;
|
|
139
267
|
/**
|
|
@@ -146,12 +274,12 @@ export function listApiKeys(userId: string, options: {
|
|
|
146
274
|
* @param {{ store: ApiKeyStore, peppers: (Buffer | Uint8Array | string)[] }} options
|
|
147
275
|
* @returns {Promise<boolean>}
|
|
148
276
|
*/
|
|
149
|
-
|
|
277
|
+
declare function rehashApiKey(rawKey: string, options: {
|
|
150
278
|
store: ApiKeyStore;
|
|
151
279
|
peppers: (Buffer | Uint8Array | string)[];
|
|
152
280
|
}): Promise<boolean>;
|
|
153
|
-
|
|
154
|
-
|
|
281
|
+
|
|
282
|
+
type ApiKeyRecord = {
|
|
155
283
|
/**
|
|
156
284
|
* Plaintext lookup key (128-bit b64u).
|
|
157
285
|
*/
|
|
@@ -197,7 +325,7 @@ export type ApiKeyRecord = {
|
|
|
197
325
|
*/
|
|
198
326
|
pepperVersion?: number | undefined;
|
|
199
327
|
};
|
|
200
|
-
|
|
328
|
+
type ApiKeyStore = {
|
|
201
329
|
put: (record: ApiKeyRecord) => Promise<void>;
|
|
202
330
|
getById: (id: string) => Promise<ApiKeyRecord | null>;
|
|
203
331
|
update: (id: string, patch: Partial<ApiKeyRecord>) => Promise<ApiKeyRecord | null>;
|
|
@@ -205,7 +333,7 @@ export type ApiKeyStore = {
|
|
|
205
333
|
revokeAllForUser: (userId: string, reason?: string) => Promise<number>;
|
|
206
334
|
listByUser: (userId: string) => Promise<ApiKeyRecord[]>;
|
|
207
335
|
};
|
|
208
|
-
|
|
336
|
+
type CreateApiKeyOptions = {
|
|
209
337
|
/**
|
|
210
338
|
* Stripe-style; `sk_live`, `pk_test`, `svc_prod_v2`.
|
|
211
339
|
*/
|
|
@@ -229,7 +357,7 @@ export type CreateApiKeyOptions = {
|
|
|
229
357
|
*/
|
|
230
358
|
now?: number | undefined;
|
|
231
359
|
};
|
|
232
|
-
|
|
360
|
+
type CreateApiKeyResult = {
|
|
233
361
|
/**
|
|
234
362
|
* Wire token — show ONCE to the caller and never again.
|
|
235
363
|
*/
|
|
@@ -243,7 +371,7 @@ export type CreateApiKeyResult = {
|
|
|
243
371
|
*/
|
|
244
372
|
record: ApiKeyRecord;
|
|
245
373
|
};
|
|
246
|
-
|
|
374
|
+
type VerifyApiKeyOptions = {
|
|
247
375
|
store: ApiKeyStore;
|
|
248
376
|
peppers?: any[] | undefined;
|
|
249
377
|
requiredScopes?: string[] | undefined;
|
|
@@ -257,8 +385,8 @@ export type VerifyApiKeyOptions = {
|
|
|
257
385
|
updateLastUsed?: boolean | undefined;
|
|
258
386
|
now?: number | undefined;
|
|
259
387
|
};
|
|
260
|
-
|
|
261
|
-
|
|
388
|
+
type VerifyApiKeyFailureReason = "malformed" | "not_found" | "expired" | "revoked" | "bad_secret" | "prefix_mismatch" | "missing_scope" | "store_unavailable";
|
|
389
|
+
type VerifyApiKeyResult = {
|
|
262
390
|
valid: true;
|
|
263
391
|
id: string;
|
|
264
392
|
userId: string;
|
|
@@ -272,7 +400,6 @@ export type VerifyApiKeyResult = {
|
|
|
272
400
|
valid: false;
|
|
273
401
|
reason: VerifyApiKeyFailureReason;
|
|
274
402
|
};
|
|
275
|
-
|
|
276
|
-
export { ApiKeyError, ErrorCode
|
|
277
|
-
export {
|
|
278
|
-
export { parseApiKey, mask } from "./token.js";
|
|
403
|
+
|
|
404
|
+
export { ApiKeyError, ErrorCode, covers, createApiKey, hasAll, hasAny, listApiKeys, mask, mask as maskApiKey, parseApiKey, rehashApiKey, revokeAllForUser, revokeApiKey, verifyApiKey };
|
|
405
|
+
export type { ApiKeyRecord, ApiKeyStore, CreateApiKeyOptions, CreateApiKeyResult, VerifyApiKeyFailureReason, VerifyApiKeyOptions, VerifyApiKeyResult };
|
|
@@ -1,4 +1,99 @@
|
|
|
1
|
+
type ApiKeyRecord = {
|
|
2
|
+
/**
|
|
3
|
+
* Plaintext lookup key (128-bit b64u).
|
|
4
|
+
*/
|
|
5
|
+
id: string;
|
|
6
|
+
/**
|
|
7
|
+
* Storage hash of the secret (43-char b64u).
|
|
8
|
+
*/
|
|
9
|
+
hash: string;
|
|
10
|
+
/**
|
|
11
|
+
* The prefix the key was minted with.
|
|
12
|
+
*/
|
|
13
|
+
prefix: string;
|
|
14
|
+
userId: string;
|
|
15
|
+
scopes: string[];
|
|
16
|
+
/**
|
|
17
|
+
* Human label ("Production Backend").
|
|
18
|
+
*/
|
|
19
|
+
name?: string | undefined;
|
|
20
|
+
/**
|
|
21
|
+
* `'live'` / `'test'` / caller-defined.
|
|
22
|
+
*/
|
|
23
|
+
environment?: string | undefined;
|
|
24
|
+
metadata?: Record<string, unknown> | undefined;
|
|
25
|
+
/**
|
|
26
|
+
* ms epoch.
|
|
27
|
+
*/
|
|
28
|
+
createdAt: number;
|
|
29
|
+
/**
|
|
30
|
+
* ms epoch; absent = no expiry.
|
|
31
|
+
*/
|
|
32
|
+
expiresAt?: number | undefined;
|
|
33
|
+
/**
|
|
34
|
+
* ms epoch when the record was revoked.
|
|
35
|
+
*/
|
|
36
|
+
revokedAt?: number | undefined;
|
|
37
|
+
revokedReason?: string | undefined;
|
|
38
|
+
/**
|
|
39
|
+
* ms epoch of the last successful verify.
|
|
40
|
+
*/
|
|
41
|
+
lastUsedAt?: number | undefined;
|
|
42
|
+
/**
|
|
43
|
+
* Index into the peppers array that was used at mint. 0 = newest.
|
|
44
|
+
*/
|
|
45
|
+
pepperVersion?: number | undefined;
|
|
46
|
+
};
|
|
47
|
+
type ApiKeyStore = {
|
|
48
|
+
put: (record: ApiKeyRecord) => Promise<void>;
|
|
49
|
+
getById: (id: string) => Promise<ApiKeyRecord | null>;
|
|
50
|
+
update: (id: string, patch: Partial<ApiKeyRecord>) => Promise<ApiKeyRecord | null>;
|
|
51
|
+
revoke: (id: string, reason?: string) => Promise<boolean>;
|
|
52
|
+
revokeAllForUser: (userId: string, reason?: string) => Promise<number>;
|
|
53
|
+
listByUser: (userId: string) => Promise<ApiKeyRecord[]>;
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
type AdapterContext = {
|
|
57
|
+
getHeader: (name: string) => string | undefined;
|
|
58
|
+
method?: string | undefined;
|
|
59
|
+
ip?: string | undefined;
|
|
60
|
+
query?: Record<string, unknown> | undefined;
|
|
61
|
+
};
|
|
62
|
+
type ApiKeyMiddlewareOptions = {
|
|
63
|
+
store: ApiKeyStore;
|
|
64
|
+
peppers?: any[] | undefined;
|
|
65
|
+
requiredScopes?: string[] | undefined;
|
|
66
|
+
expectedPrefix?: string | undefined;
|
|
67
|
+
updateLastUsed?: boolean | undefined;
|
|
68
|
+
/**
|
|
69
|
+
* Case-insensitive. The default reads `Authorization: Bearer <key>`;
|
|
70
|
+
* a `x-api-key` config reads `X-API-Key: <key>` directly.
|
|
71
|
+
*/
|
|
72
|
+
headerName?: string | undefined;
|
|
73
|
+
/**
|
|
74
|
+
* `'bearer'` expects `Bearer <key>`; `'raw'` uses the header value as-is.
|
|
75
|
+
*/
|
|
76
|
+
scheme?: "bearer" | "raw" | undefined;
|
|
77
|
+
/**
|
|
78
|
+
* When true, falls back to `?api_key=<key>` if no matching header is
|
|
79
|
+
* found. Discouraged because query strings leak into access logs and
|
|
80
|
+
* referer headers — off by default.
|
|
81
|
+
*/
|
|
82
|
+
allowQueryParam?: boolean | undefined;
|
|
83
|
+
queryParamName?: string | undefined;
|
|
84
|
+
/**
|
|
85
|
+
* Property name attached to the request object on success.
|
|
86
|
+
*/
|
|
87
|
+
attach?: string | undefined;
|
|
88
|
+
/**
|
|
89
|
+
* Override the default extraction entirely.
|
|
90
|
+
*/
|
|
91
|
+
tokenFromRequest?: ((ctx: AdapterContext) => string | undefined) | undefined;
|
|
92
|
+
};
|
|
93
|
+
|
|
1
94
|
/**
|
|
2
95
|
* @param {import('./core.js').ApiKeyMiddlewareOptions} options
|
|
3
96
|
*/
|
|
4
|
-
|
|
97
|
+
declare function apiKeyMiddleware(options: ApiKeyMiddlewareOptions): (req: any, res: any, next: any) => Promise<any>;
|
|
98
|
+
|
|
99
|
+
export { apiKeyMiddleware };
|
package/dist/stores/custom.d.ts
CHANGED
|
@@ -1,8 +1 @@
|
|
|
1
|
-
export
|
|
2
|
-
put: (record: any) => Promise<any>;
|
|
3
|
-
getById: (id: any) => Promise<any>;
|
|
4
|
-
update: (id: any, patch: any) => Promise<any>;
|
|
5
|
-
revoke: (id: any, reason: any) => Promise<any>;
|
|
6
|
-
revokeAllForUser: (userId: any, reason: any) => Promise<any>;
|
|
7
|
-
listByUser: (userId: any) => Promise<any>;
|
|
8
|
-
};
|
|
1
|
+
export const customStore: (impl: object) => Record<string, (...args: any[]) => Promise<any>>;
|
package/dist/stores/index.d.ts
CHANGED
|
@@ -1,3 +1,79 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
type ApiKeyRecord = {
|
|
2
|
+
/**
|
|
3
|
+
* Plaintext lookup key (128-bit b64u).
|
|
4
|
+
*/
|
|
5
|
+
id: string;
|
|
6
|
+
/**
|
|
7
|
+
* Storage hash of the secret (43-char b64u).
|
|
8
|
+
*/
|
|
9
|
+
hash: string;
|
|
10
|
+
/**
|
|
11
|
+
* The prefix the key was minted with.
|
|
12
|
+
*/
|
|
13
|
+
prefix: string;
|
|
14
|
+
userId: string;
|
|
15
|
+
scopes: string[];
|
|
16
|
+
/**
|
|
17
|
+
* Human label ("Production Backend").
|
|
18
|
+
*/
|
|
19
|
+
name?: string | undefined;
|
|
20
|
+
/**
|
|
21
|
+
* `'live'` / `'test'` / caller-defined.
|
|
22
|
+
*/
|
|
23
|
+
environment?: string | undefined;
|
|
24
|
+
metadata?: Record<string, unknown> | undefined;
|
|
25
|
+
/**
|
|
26
|
+
* ms epoch.
|
|
27
|
+
*/
|
|
28
|
+
createdAt: number;
|
|
29
|
+
/**
|
|
30
|
+
* ms epoch; absent = no expiry.
|
|
31
|
+
*/
|
|
32
|
+
expiresAt?: number | undefined;
|
|
33
|
+
/**
|
|
34
|
+
* ms epoch when the record was revoked.
|
|
35
|
+
*/
|
|
36
|
+
revokedAt?: number | undefined;
|
|
37
|
+
revokedReason?: string | undefined;
|
|
38
|
+
/**
|
|
39
|
+
* ms epoch of the last successful verify.
|
|
40
|
+
*/
|
|
41
|
+
lastUsedAt?: number | undefined;
|
|
42
|
+
/**
|
|
43
|
+
* Index into the peppers array that was used at mint. 0 = newest.
|
|
44
|
+
*/
|
|
45
|
+
pepperVersion?: number | undefined;
|
|
46
|
+
};
|
|
47
|
+
type ApiKeyStore = {
|
|
48
|
+
put: (record: ApiKeyRecord) => Promise<void>;
|
|
49
|
+
getById: (id: string) => Promise<ApiKeyRecord | null>;
|
|
50
|
+
update: (id: string, patch: Partial<ApiKeyRecord>) => Promise<ApiKeyRecord | null>;
|
|
51
|
+
revoke: (id: string, reason?: string) => Promise<boolean>;
|
|
52
|
+
revokeAllForUser: (userId: string, reason?: string) => Promise<number>;
|
|
53
|
+
listByUser: (userId: string) => Promise<ApiKeyRecord[]>;
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* @returns {import('../index.js').ApiKeyStore & { _size: () => number }}
|
|
58
|
+
*/
|
|
59
|
+
declare function memoryStore(): ApiKeyStore & {
|
|
60
|
+
_size: () => number;
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* @typedef {object} RedisStoreOptions
|
|
65
|
+
* @property {string} [keyPrefix='apikey:']
|
|
66
|
+
*/
|
|
67
|
+
/**
|
|
68
|
+
* @param {any} client
|
|
69
|
+
* @param {RedisStoreOptions} [options]
|
|
70
|
+
* @returns {import('../index.js').ApiKeyStore}
|
|
71
|
+
*/
|
|
72
|
+
declare function redisStore(client: any, options?: RedisStoreOptions): ApiKeyStore;
|
|
73
|
+
type RedisStoreOptions = {
|
|
74
|
+
keyPrefix?: string | undefined;
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
declare const customStore: (impl: object) => Record<string, (...args: any[]) => Promise<any>>;
|
|
78
|
+
|
|
79
|
+
export { customStore, memoryStore, redisStore };
|
package/dist/stores.cjs
CHANGED
|
@@ -897,39 +897,101 @@ function redisStore(client, options = {}) {
|
|
|
897
897
|
}
|
|
898
898
|
|
|
899
899
|
/**
|
|
900
|
-
*
|
|
901
|
-
*
|
|
902
|
-
* surfaces at construction time, not on the first `createApiKey` /
|
|
903
|
-
* `verifyApiKey` call. Sync implementations are wrapped transparently
|
|
904
|
-
* — return a plain value or a Promise, either works.
|
|
900
|
+
* Family factory for the `customStore(impl)` wrapper every store-shipping
|
|
901
|
+
* package exposes.
|
|
905
902
|
*
|
|
906
|
-
*
|
|
903
|
+
* Each package used to hand-roll the same three steps: assert `impl` is an
|
|
904
|
+
* object, assert its required methods are functions, then `Promise.resolve()`
|
|
905
|
+
* -wrap every call so a sync implementation is transparently promoted to the
|
|
906
|
+
* async store interface. Only the method-name arrays and the typed error
|
|
907
|
+
* differ. This collapses all of that into one binding per package.
|
|
907
908
|
*
|
|
908
|
-
*
|
|
909
|
-
* @returns {import('../index.js').ApiKeyStore}
|
|
909
|
+
* Consumers: `apikey`, `challenge`, `magic-link`, `opaque`, `session`.
|
|
910
910
|
*/
|
|
911
911
|
|
|
912
|
-
const REQUIRED = ['put', 'getById', 'update', 'revoke', 'revokeAllForUser', 'listByUser'];
|
|
913
912
|
|
|
914
|
-
|
|
913
|
+
/**
|
|
914
|
+
* Assert `impl` is an object exposing every method in `required` as a function.
|
|
915
|
+
* Throws the caller's typed error (via `wrap`) on the first failure. Used on
|
|
916
|
+
* its own by stores that keep a caller's implementation verbatim (jwt / paseto
|
|
917
|
+
* token registries), and internally by {@link createCustomStoreValidator}.
|
|
918
|
+
*
|
|
919
|
+
* @param {unknown} impl
|
|
920
|
+
* @param {readonly string[]} required
|
|
921
|
+
* @param {(message: string) => Error} wrap
|
|
922
|
+
* The package's error factory; its returned error is thrown. A `wrap` that
|
|
923
|
+
* throws directly also works (the outer `throw` is then unreachable).
|
|
924
|
+
* @returns {void}
|
|
925
|
+
*/
|
|
926
|
+
function assertStoreShape(impl, required, wrap) {
|
|
915
927
|
if (!isObject(impl)) {
|
|
916
|
-
throw
|
|
928
|
+
throw wrap(`customStore(impl) requires an object with { ${required.join(', ')} } methods`);
|
|
917
929
|
}
|
|
918
|
-
for (const name of
|
|
919
|
-
if (!isFunction(impl[name])) {
|
|
920
|
-
throw
|
|
930
|
+
for (const name of required) {
|
|
931
|
+
if (!isFunction(/** @type {any} */ (impl)[name])) {
|
|
932
|
+
throw wrap(`customStore: impl.${name} is required and must be a function`);
|
|
921
933
|
}
|
|
922
934
|
}
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
/**
|
|
938
|
+
* @param {object} spec
|
|
939
|
+
* @param {readonly string[]} spec.required
|
|
940
|
+
* Methods that must exist on `impl`; missing ones fail at construction time.
|
|
941
|
+
* @param {readonly string[]} [spec.optional]
|
|
942
|
+
* Methods wrapped through only when present (feature-gated store extras).
|
|
943
|
+
* @param {(message: string) => Error} spec.wrap
|
|
944
|
+
* The package's error factory (its `invalidArgument`) — called with a
|
|
945
|
+
* diagnostic message; its returned typed error is thrown so
|
|
946
|
+
* `err instanceof <Pkg>Error` holds. A `wrap` that throws directly also
|
|
947
|
+
* works (the outer `throw` is then unreachable).
|
|
948
|
+
* @param {Record<string, (result: any) => any>} [spec.coerce]
|
|
949
|
+
* Optional per-method post-processor applied to the resolved result — e.g.
|
|
950
|
+
* opaque coerces `set` to `undefined` so a native `Map.set` return value
|
|
951
|
+
* can't leak out as `Promise<Map>`.
|
|
952
|
+
* @returns {(impl: object) => Record<string, (...args: any[]) => Promise<any>>}
|
|
953
|
+
*/
|
|
954
|
+
function createCustomStoreValidator({ required, optional = [], wrap, coerce = {} }) {
|
|
955
|
+
return function customStore(impl) {
|
|
956
|
+
assertStoreShape(impl, required, wrap);
|
|
957
|
+
|
|
958
|
+
/** @type {Record<string, (...args: any[]) => Promise<any>>} */
|
|
959
|
+
const store = {};
|
|
960
|
+
const bind = name => {
|
|
961
|
+
const post = coerce[name];
|
|
962
|
+
store[name] = post
|
|
963
|
+
? (...args) => Promise.resolve(impl[name](...args)).then(post)
|
|
964
|
+
: (...args) => Promise.resolve(impl[name](...args));
|
|
965
|
+
};
|
|
966
|
+
|
|
967
|
+
for (const name of required) {
|
|
968
|
+
bind(name);
|
|
969
|
+
}
|
|
970
|
+
for (const name of optional) {
|
|
971
|
+
if (isFunction(impl[name])) {
|
|
972
|
+
bind(name);
|
|
973
|
+
}
|
|
974
|
+
}
|
|
975
|
+
return store;
|
|
930
976
|
};
|
|
931
977
|
}
|
|
932
978
|
|
|
979
|
+
/**
|
|
980
|
+
* Wrap a user-supplied store into the `ApiKeyStore` interface. Validates the
|
|
981
|
+
* required methods at construction time (not on first use) and promotes sync
|
|
982
|
+
* implementations to the async interface.
|
|
983
|
+
*
|
|
984
|
+
* Required: `put`, `getById`, `update`, `revoke`, `revokeAllForUser`, `listByUser`.
|
|
985
|
+
*
|
|
986
|
+
* @param {Partial<import('../index.js').ApiKeyStore>} impl
|
|
987
|
+
* @returns {import('../index.js').ApiKeyStore}
|
|
988
|
+
*/
|
|
989
|
+
|
|
990
|
+
const customStore = createCustomStoreValidator({
|
|
991
|
+
required: ['put', 'getById', 'update', 'revoke', 'revokeAllForUser', 'listByUser'],
|
|
992
|
+
wrap: invalidArgument,
|
|
993
|
+
});
|
|
994
|
+
|
|
933
995
|
exports.customStore = customStore;
|
|
934
996
|
exports.memoryStore = memoryStore;
|
|
935
997
|
exports.redisStore = redisStore;
|
package/dist/stores.mjs
CHANGED
|
@@ -895,37 +895,99 @@ function redisStore(client, options = {}) {
|
|
|
895
895
|
}
|
|
896
896
|
|
|
897
897
|
/**
|
|
898
|
-
*
|
|
899
|
-
*
|
|
900
|
-
* surfaces at construction time, not on the first `createApiKey` /
|
|
901
|
-
* `verifyApiKey` call. Sync implementations are wrapped transparently
|
|
902
|
-
* — return a plain value or a Promise, either works.
|
|
898
|
+
* Family factory for the `customStore(impl)` wrapper every store-shipping
|
|
899
|
+
* package exposes.
|
|
903
900
|
*
|
|
904
|
-
*
|
|
901
|
+
* Each package used to hand-roll the same three steps: assert `impl` is an
|
|
902
|
+
* object, assert its required methods are functions, then `Promise.resolve()`
|
|
903
|
+
* -wrap every call so a sync implementation is transparently promoted to the
|
|
904
|
+
* async store interface. Only the method-name arrays and the typed error
|
|
905
|
+
* differ. This collapses all of that into one binding per package.
|
|
905
906
|
*
|
|
906
|
-
*
|
|
907
|
-
* @returns {import('../index.js').ApiKeyStore}
|
|
907
|
+
* Consumers: `apikey`, `challenge`, `magic-link`, `opaque`, `session`.
|
|
908
908
|
*/
|
|
909
909
|
|
|
910
|
-
const REQUIRED = ['put', 'getById', 'update', 'revoke', 'revokeAllForUser', 'listByUser'];
|
|
911
910
|
|
|
912
|
-
|
|
911
|
+
/**
|
|
912
|
+
* Assert `impl` is an object exposing every method in `required` as a function.
|
|
913
|
+
* Throws the caller's typed error (via `wrap`) on the first failure. Used on
|
|
914
|
+
* its own by stores that keep a caller's implementation verbatim (jwt / paseto
|
|
915
|
+
* token registries), and internally by {@link createCustomStoreValidator}.
|
|
916
|
+
*
|
|
917
|
+
* @param {unknown} impl
|
|
918
|
+
* @param {readonly string[]} required
|
|
919
|
+
* @param {(message: string) => Error} wrap
|
|
920
|
+
* The package's error factory; its returned error is thrown. A `wrap` that
|
|
921
|
+
* throws directly also works (the outer `throw` is then unreachable).
|
|
922
|
+
* @returns {void}
|
|
923
|
+
*/
|
|
924
|
+
function assertStoreShape(impl, required, wrap) {
|
|
913
925
|
if (!isObject(impl)) {
|
|
914
|
-
throw
|
|
926
|
+
throw wrap(`customStore(impl) requires an object with { ${required.join(', ')} } methods`);
|
|
915
927
|
}
|
|
916
|
-
for (const name of
|
|
917
|
-
if (!isFunction(impl[name])) {
|
|
918
|
-
throw
|
|
928
|
+
for (const name of required) {
|
|
929
|
+
if (!isFunction(/** @type {any} */ (impl)[name])) {
|
|
930
|
+
throw wrap(`customStore: impl.${name} is required and must be a function`);
|
|
919
931
|
}
|
|
920
932
|
}
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
933
|
+
}
|
|
934
|
+
|
|
935
|
+
/**
|
|
936
|
+
* @param {object} spec
|
|
937
|
+
* @param {readonly string[]} spec.required
|
|
938
|
+
* Methods that must exist on `impl`; missing ones fail at construction time.
|
|
939
|
+
* @param {readonly string[]} [spec.optional]
|
|
940
|
+
* Methods wrapped through only when present (feature-gated store extras).
|
|
941
|
+
* @param {(message: string) => Error} spec.wrap
|
|
942
|
+
* The package's error factory (its `invalidArgument`) — called with a
|
|
943
|
+
* diagnostic message; its returned typed error is thrown so
|
|
944
|
+
* `err instanceof <Pkg>Error` holds. A `wrap` that throws directly also
|
|
945
|
+
* works (the outer `throw` is then unreachable).
|
|
946
|
+
* @param {Record<string, (result: any) => any>} [spec.coerce]
|
|
947
|
+
* Optional per-method post-processor applied to the resolved result — e.g.
|
|
948
|
+
* opaque coerces `set` to `undefined` so a native `Map.set` return value
|
|
949
|
+
* can't leak out as `Promise<Map>`.
|
|
950
|
+
* @returns {(impl: object) => Record<string, (...args: any[]) => Promise<any>>}
|
|
951
|
+
*/
|
|
952
|
+
function createCustomStoreValidator({ required, optional = [], wrap, coerce = {} }) {
|
|
953
|
+
return function customStore(impl) {
|
|
954
|
+
assertStoreShape(impl, required, wrap);
|
|
955
|
+
|
|
956
|
+
/** @type {Record<string, (...args: any[]) => Promise<any>>} */
|
|
957
|
+
const store = {};
|
|
958
|
+
const bind = name => {
|
|
959
|
+
const post = coerce[name];
|
|
960
|
+
store[name] = post
|
|
961
|
+
? (...args) => Promise.resolve(impl[name](...args)).then(post)
|
|
962
|
+
: (...args) => Promise.resolve(impl[name](...args));
|
|
963
|
+
};
|
|
964
|
+
|
|
965
|
+
for (const name of required) {
|
|
966
|
+
bind(name);
|
|
967
|
+
}
|
|
968
|
+
for (const name of optional) {
|
|
969
|
+
if (isFunction(impl[name])) {
|
|
970
|
+
bind(name);
|
|
971
|
+
}
|
|
972
|
+
}
|
|
973
|
+
return store;
|
|
928
974
|
};
|
|
929
975
|
}
|
|
930
976
|
|
|
977
|
+
/**
|
|
978
|
+
* Wrap a user-supplied store into the `ApiKeyStore` interface. Validates the
|
|
979
|
+
* required methods at construction time (not on first use) and promotes sync
|
|
980
|
+
* implementations to the async interface.
|
|
981
|
+
*
|
|
982
|
+
* Required: `put`, `getById`, `update`, `revoke`, `revokeAllForUser`, `listByUser`.
|
|
983
|
+
*
|
|
984
|
+
* @param {Partial<import('../index.js').ApiKeyStore>} impl
|
|
985
|
+
* @returns {import('../index.js').ApiKeyStore}
|
|
986
|
+
*/
|
|
987
|
+
|
|
988
|
+
const customStore = createCustomStoreValidator({
|
|
989
|
+
required: ['put', 'getById', 'update', 'revoke', 'revokeAllForUser', 'listByUser'],
|
|
990
|
+
wrap: invalidArgument,
|
|
991
|
+
});
|
|
992
|
+
|
|
931
993
|
export { customStore, memoryStore, redisStore };
|
package/dist/token.d.ts
CHANGED
|
@@ -84,5 +84,6 @@ export function candidateHashesFor(rawSecret: string, peppers: Buffer[] | null):
|
|
|
84
84
|
* @returns {string}
|
|
85
85
|
*/
|
|
86
86
|
export function mask(key: string): string;
|
|
87
|
+
export const _nodeTimingSafeEqual: any;
|
|
88
|
+
export const _isBytes: typeof isBytes;
|
|
87
89
|
import { isBytes } from '@exortek/shared/predicates';
|
|
88
|
-
export { nodeTimingSafeEqual as _nodeTimingSafeEqual, isBytes as _isBytes };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@exortek/apikey",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.3",
|
|
4
4
|
"description": "Stripe-style prefixed API keys for Node.js — 3-segment tokens (prefix_id_secret), HMAC-SHA256 storage hash with pepper rotation, scope allowlists with wildcard match, opt-in lastUsedAt tracking, memory + Redis stores, Express + Fastify middleware. Server-only. Zero non-@exortek runtime dependencies.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"sideEffects": false,
|
|
@@ -36,7 +36,7 @@
|
|
|
36
36
|
"/LICENSE"
|
|
37
37
|
],
|
|
38
38
|
"scripts": {
|
|
39
|
-
"build": "rm -rf dist tsconfig.tsbuildinfo && rollup -c rollup.config.js && tsc -p tsconfig.json",
|
|
39
|
+
"build": "rm -rf dist tsconfig.tsbuildinfo && rollup -c rollup.config.js && tsc -p tsconfig.json && rollup -c ../../rollup.dts.config.mjs",
|
|
40
40
|
"build:watch": "rollup -c rollup.config.js --watch",
|
|
41
41
|
"build:types": "tsc -p tsconfig.json",
|
|
42
42
|
"typecheck": "tsc -p tsconfig.json --noEmit",
|