@exortek/apikey 1.0.1 → 1.0.2

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 CHANGED
@@ -1,5 +1,15 @@
1
1
  # @exortek/apikey
2
2
 
3
+ ## 1.0.2
4
+
5
+ ### Patch Changes
6
+
7
+ - 689cf87: Remove workspace-only `devDependencies` from published package.json — `@exortek/shared` no longer appears as
8
+ `"0.0.0"` on the npm registry.
9
+ - 689cf87: Replace `fastify-plugin` npm dependency with `@exortek/shared/fastify-plugin` — a built-in drop-in that
10
+ covers skip-override, display-name, plugin-meta, version constraints, and encapsulate. Users no longer need to
11
+ `npm i fastify-plugin` alongside fastify.
12
+
3
13
  ## 1.0.1
4
14
 
5
15
  ### Patch Changes
package/dist/index.cjs CHANGED
@@ -946,11 +946,11 @@ function timingSafeEqual(a, b) {
946
946
  *
947
947
  * **Wire format:**
948
948
  *
949
- * <prefix>_<base64url(16 random bytes)>_<base64url(32 random bytes)>
950
- * ^^^^^^ ^^^^^^^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^^^^^^^^
951
- * type id (128-bit — plaintext DB key) secret (256-bit — HMAC'd)
949
+ * <prefix>_<crockford-base32(16 random bytes)>_<crockford-base32(32 random bytes)>
950
+ * ^^^^^^ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
951
+ * type id (128-bit, 26 chars) secret (256-bit, 52 chars)
952
952
  *
953
- * sk_live_QVI0RExDX01Fbg_pTk9UX0FfUkVBTF9LRVlfNDJfQVJUSVNBTkFMX1BJWk
953
+ * sk_live_<id-26-chars>_<secret-52-chars>
954
954
  *
955
955
  * Stripe / GitHub / Anthropic all use this three-segment shape. The
956
956
  * split matters:
@@ -1050,9 +1050,9 @@ function hashSecret(secretBytes, pepper) {
1050
1050
 
1051
1051
  /**
1052
1052
  * Timing-safe compare of a candidate hash against a stored hash. Both
1053
- * inputs are base64url strings of equal length — a length mismatch is
1054
- * treated as a mismatch after a fixed decode step, so an outside
1055
- * observer cannot learn the stored hash's length via timing.
1053
+ * inputs are base64url strings. A length mismatch early-returns `false`
1054
+ * — all stored hashes are fixed-length (43-char SHA-256 digests), so
1055
+ * the length check leaks no information about the stored value.
1056
1056
  *
1057
1057
  * @param {string} candidate
1058
1058
  * @param {string} stored
package/dist/index.d.ts CHANGED
@@ -223,7 +223,7 @@ export type CreateApiKeyOptions = {
223
223
  /**
224
224
  * Newest first. Each ≥16 bytes. Omit for plain SHA-256 storage.
225
225
  */
226
- peppers?: (string | Buffer | Uint8Array<ArrayBufferLike>)[] | undefined;
226
+ peppers?: any[] | undefined;
227
227
  /**
228
228
  * Override `Date.now()` for testing.
229
229
  */
@@ -245,7 +245,7 @@ export type CreateApiKeyResult = {
245
245
  };
246
246
  export type VerifyApiKeyOptions = {
247
247
  store: ApiKeyStore;
248
- peppers?: (string | Buffer | Uint8Array<ArrayBufferLike>)[] | undefined;
248
+ peppers?: any[] | undefined;
249
249
  requiredScopes?: string[] | undefined;
250
250
  /**
251
251
  * Reject a valid key whose prefix differs.
package/dist/index.mjs CHANGED
@@ -944,11 +944,11 @@ function timingSafeEqual(a, b) {
944
944
  *
945
945
  * **Wire format:**
946
946
  *
947
- * <prefix>_<base64url(16 random bytes)>_<base64url(32 random bytes)>
948
- * ^^^^^^ ^^^^^^^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^^^^^^^^
949
- * type id (128-bit — plaintext DB key) secret (256-bit — HMAC'd)
947
+ * <prefix>_<crockford-base32(16 random bytes)>_<crockford-base32(32 random bytes)>
948
+ * ^^^^^^ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
949
+ * type id (128-bit, 26 chars) secret (256-bit, 52 chars)
950
950
  *
951
- * sk_live_QVI0RExDX01Fbg_pTk9UX0FfUkVBTF9LRVlfNDJfQVJUSVNBTkFMX1BJWk
951
+ * sk_live_<id-26-chars>_<secret-52-chars>
952
952
  *
953
953
  * Stripe / GitHub / Anthropic all use this three-segment shape. The
954
954
  * split matters:
@@ -1048,9 +1048,9 @@ function hashSecret(secretBytes, pepper) {
1048
1048
 
1049
1049
  /**
1050
1050
  * Timing-safe compare of a candidate hash against a stored hash. Both
1051
- * inputs are base64url strings of equal length — a length mismatch is
1052
- * treated as a mismatch after a fixed decode step, so an outside
1053
- * observer cannot learn the stored hash's length via timing.
1051
+ * inputs are base64url strings. A length mismatch early-returns `false`
1052
+ * — all stored hashes are fixed-length (43-char SHA-256 digests), so
1053
+ * the length check leaks no information about the stored value.
1054
1054
  *
1055
1055
  * @param {string} candidate
1056
1056
  * @param {string} stored
@@ -6,7 +6,7 @@
6
6
  */
7
7
  export function normalizeOptions(options: ApiKeyMiddlewareOptions): {
8
8
  store: import("../index.js").ApiKeyStore;
9
- peppers: (string | Buffer | Uint8Array<ArrayBufferLike>)[] | undefined;
9
+ peppers: any[] | undefined;
10
10
  requiredScopes: string[] | undefined;
11
11
  expectedPrefix: string | undefined;
12
12
  updateLastUsed: boolean;
@@ -45,7 +45,7 @@ export type AdapterContext = {
45
45
  };
46
46
  export type ApiKeyMiddlewareOptions = {
47
47
  store: import("../index.js").ApiKeyStore;
48
- peppers?: (string | Buffer | Uint8Array<ArrayBufferLike>)[] | undefined;
48
+ peppers?: any[] | undefined;
49
49
  requiredScopes?: string[] | undefined;
50
50
  expectedPrefix?: string | undefined;
51
51
  updateLastUsed?: boolean | undefined;
@@ -787,11 +787,11 @@ function timingSafeEqual(a, b) {
787
787
  *
788
788
  * **Wire format:**
789
789
  *
790
- * <prefix>_<base64url(16 random bytes)>_<base64url(32 random bytes)>
791
- * ^^^^^^ ^^^^^^^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^^^^^^^^
792
- * type id (128-bit — plaintext DB key) secret (256-bit — HMAC'd)
790
+ * <prefix>_<crockford-base32(16 random bytes)>_<crockford-base32(32 random bytes)>
791
+ * ^^^^^^ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
792
+ * type id (128-bit, 26 chars) secret (256-bit, 52 chars)
793
793
  *
794
- * sk_live_QVI0RExDX01Fbg_pTk9UX0FfUkVBTF9LRVlfNDJfQVJUSVNBTkFMX1BJWk
794
+ * sk_live_<id-26-chars>_<secret-52-chars>
795
795
  *
796
796
  * Stripe / GitHub / Anthropic all use this three-segment shape. The
797
797
  * split matters:
@@ -871,9 +871,9 @@ function hashSecret(secretBytes, pepper) {
871
871
 
872
872
  /**
873
873
  * Timing-safe compare of a candidate hash against a stored hash. Both
874
- * inputs are base64url strings of equal length — a length mismatch is
875
- * treated as a mismatch after a fixed decode step, so an outside
876
- * observer cannot learn the stored hash's length via timing.
874
+ * inputs are base64url strings. A length mismatch early-returns `false`
875
+ * — all stored hashes are fixed-length (43-char SHA-256 digests), so
876
+ * the length check leaks no information about the stored value.
877
877
  *
878
878
  * @param {string} candidate
879
879
  * @param {string} stored
@@ -785,11 +785,11 @@ function timingSafeEqual(a, b) {
785
785
  *
786
786
  * **Wire format:**
787
787
  *
788
- * <prefix>_<base64url(16 random bytes)>_<base64url(32 random bytes)>
789
- * ^^^^^^ ^^^^^^^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^^^^^^^^
790
- * type id (128-bit — plaintext DB key) secret (256-bit — HMAC'd)
788
+ * <prefix>_<crockford-base32(16 random bytes)>_<crockford-base32(32 random bytes)>
789
+ * ^^^^^^ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
790
+ * type id (128-bit, 26 chars) secret (256-bit, 52 chars)
791
791
  *
792
- * sk_live_QVI0RExDX01Fbg_pTk9UX0FfUkVBTF9LRVlfNDJfQVJUSVNBTkFMX1BJWk
792
+ * sk_live_<id-26-chars>_<secret-52-chars>
793
793
  *
794
794
  * Stripe / GitHub / Anthropic all use this three-segment shape. The
795
795
  * split matters:
@@ -869,9 +869,9 @@ function hashSecret(secretBytes, pepper) {
869
869
 
870
870
  /**
871
871
  * Timing-safe compare of a candidate hash against a stored hash. Both
872
- * inputs are base64url strings of equal length — a length mismatch is
873
- * treated as a mismatch after a fixed decode step, so an outside
874
- * observer cannot learn the stored hash's length via timing.
872
+ * inputs are base64url strings. A length mismatch early-returns `false`
873
+ * — all stored hashes are fixed-length (43-char SHA-256 digests), so
874
+ * the length check leaks no information about the stored value.
875
875
  *
876
876
  * @param {string} candidate
877
877
  * @param {string} stored
@@ -1,6 +1,5 @@
1
1
  'use strict';
2
2
 
3
- var fp = require('fastify-plugin');
4
3
  var node_crypto = require('node:crypto');
5
4
 
6
5
  /**
@@ -131,6 +130,67 @@ function isBuffer(value) {
131
130
  return Buffer.isBuffer(value);
132
131
  }
133
132
 
133
+ let counter = 0;
134
+
135
+ /**
136
+ * Mark a function as a Fastify plugin with encapsulation bypass.
137
+ *
138
+ * Drop-in replacement for the `fastify-plugin` npm package — covers
139
+ * the same symbols and option handling without the external dependency.
140
+ *
141
+ * @param {Function} fn Plugin function `(fastify, opts) => …`
142
+ * @param {string | PluginOptions} [options]
143
+ * A semver range string (shorthand for `{ fastify: range }`) or a
144
+ * full options object.
145
+ * @returns {Function} The same function, decorated with plugin metadata.
146
+ *
147
+ * @typedef {object} PluginOptions
148
+ * @property {string} [name] Display name. Auto-generated from
149
+ * `fn.name` when omitted.
150
+ * @property {string} [fastify] Semver range the plugin is compatible
151
+ * with (e.g. `'>=4'`). Fastify checks
152
+ * this at register time.
153
+ * @property {string[]} [decorators] Decorators the plugin expects to
154
+ * exist before it runs.
155
+ * @property {string[]} [dependencies] Plugins that must be registered
156
+ * before this one.
157
+ * @property {boolean} [encapsulate] When `true`, the plugin stays
158
+ * encapsulated (skip-override = false).
159
+ * Default `false` (skip override).
160
+ */
161
+ function fastifyPlugin(fn, options) {
162
+ if (fn && fn.default !== undefined) {
163
+ fn = fn.default;
164
+ }
165
+
166
+ if (!isFunction(fn)) {
167
+ throw new TypeError(`fastifyPlugin expects a function, instead got '${typeof fn}'`);
168
+ }
169
+
170
+ let opts;
171
+ if (isString(options)) {
172
+ opts = { fastify: options };
173
+ } else if (isObject(options)) {
174
+ opts = { ...options };
175
+ } else {
176
+ opts = {};
177
+ }
178
+
179
+ if (!opts.name) {
180
+ opts.name = (fn.name || 'anonymous') + '-auto-' + counter++;
181
+ }
182
+
183
+ fn[Symbol.for('skip-override')] = opts.encapsulate !== true;
184
+ fn[Symbol.for('fastify.display-name')] = opts.name;
185
+ fn[Symbol.for('plugin-meta')] = opts;
186
+
187
+ if (!fn.default) {
188
+ fn.default = fn;
189
+ }
190
+
191
+ return fn;
192
+ }
193
+
134
194
  /**
135
195
  * Imperative single-argument guard helpers — the everyday
136
196
  * `assertPositiveInt(x, 'name')` shape used at API boundaries.
@@ -788,11 +848,11 @@ function timingSafeEqual(a, b) {
788
848
  *
789
849
  * **Wire format:**
790
850
  *
791
- * <prefix>_<base64url(16 random bytes)>_<base64url(32 random bytes)>
792
- * ^^^^^^ ^^^^^^^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^^^^^^^^
793
- * type id (128-bit — plaintext DB key) secret (256-bit — HMAC'd)
851
+ * <prefix>_<crockford-base32(16 random bytes)>_<crockford-base32(32 random bytes)>
852
+ * ^^^^^^ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
853
+ * type id (128-bit, 26 chars) secret (256-bit, 52 chars)
794
854
  *
795
- * sk_live_QVI0RExDX01Fbg_pTk9UX0FfUkVBTF9LRVlfNDJfQVJUSVNBTkFMX1BJWk
855
+ * sk_live_<id-26-chars>_<secret-52-chars>
796
856
  *
797
857
  * Stripe / GitHub / Anthropic all use this three-segment shape. The
798
858
  * split matters:
@@ -872,9 +932,9 @@ function hashSecret(secretBytes, pepper) {
872
932
 
873
933
  /**
874
934
  * Timing-safe compare of a candidate hash against a stored hash. Both
875
- * inputs are base64url strings of equal length — a length mismatch is
876
- * treated as a mismatch after a fixed decode step, so an outside
877
- * observer cannot learn the stored hash's length via timing.
935
+ * inputs are base64url strings. A length mismatch early-returns `false`
936
+ * — all stored hashes are fixed-length (43-char SHA-256 digests), so
937
+ * the length check leaks no information about the stored value.
878
938
  *
879
939
  * @param {string} candidate
880
940
  * @param {string} stored
@@ -1340,45 +1400,36 @@ function _statusFor(reason) {
1340
1400
  *
1341
1401
  * app.get('/v1/whoami', async req => req.apiKey);
1342
1402
  *
1343
- * Wrapped with `fastify-plugin` so the `preHandler` hook and the
1344
- * `req.apiKey` decorator escape encapsulation and apply to routes
1345
- * registered as siblings in the parent scope. Register the plugin
1346
- * inside a scoped `app.register(async scope => …)` block to control
1347
- * which route group needs an API key — the usual Fastify pattern for
1348
- * "protect only these routes with auth".
1349
- *
1350
- * `fastify-plugin` is an **optional peer** — install it with
1351
- * `npm i fastify fastify-plugin` (or `yarn add`) alongside the fastify
1352
- * runtime. Not a hard dependency because Express-only consumers of
1353
- * `@exortek/apikey` should not have to install it.
1403
+ * The plugin bypasses Fastify's encapsulation so the `preHandler` hook
1404
+ * and the `req.apiKey` decorator apply to routes in the parent scope.
1405
+ * Register inside a scoped `app.register(async scope => …)` block to
1406
+ * control which route group needs an API key.
1354
1407
  */
1355
1408
 
1356
1409
 
1357
- async function apiKeyPluginFn(app, options) {
1358
- const config = normalizeOptions(options);
1359
-
1360
- app.decorateRequest(config.attach, null);
1410
+ const apiKeyPlugin = fastifyPlugin(
1411
+ async function apiKeyPluginFn(app, options) {
1412
+ const config = normalizeOptions(options);
1361
1413
 
1362
- app.addHook('preHandler', async (req, reply) => {
1363
- const ctx = {
1364
- getHeader: name => req.headers?.[name],
1365
- method: req.method,
1366
- ip: req.ip,
1367
- query: req.query,
1368
- };
1369
- const result = await runApiKey(ctx, config);
1370
- if (result.response) {
1371
- reply.code(result.response.status).send(result.response.body);
1372
- return reply;
1373
- }
1374
- req[config.attach] = result.verifyResult;
1375
- return undefined;
1376
- });
1377
- }
1414
+ app.decorateRequest(config.attach, null);
1378
1415
 
1379
- const apiKeyPlugin = fp(apiKeyPluginFn, {
1380
- fastify: '4.x || 5.x',
1381
- name: '@exortek/apikey',
1382
- });
1416
+ app.addHook('preHandler', async (req, reply) => {
1417
+ const ctx = {
1418
+ getHeader: name => req.headers?.[name],
1419
+ method: req.method,
1420
+ ip: req.ip,
1421
+ query: req.query,
1422
+ };
1423
+ const result = await runApiKey(ctx, config);
1424
+ if (result.response) {
1425
+ reply.code(result.response.status).send(result.response.body);
1426
+ return reply;
1427
+ }
1428
+ req[config.attach] = result.verifyResult;
1429
+ return undefined;
1430
+ });
1431
+ },
1432
+ { name: '@exortek/apikey' },
1433
+ );
1383
1434
 
1384
1435
  exports.apiKeyPlugin = apiKeyPlugin;
@@ -1 +1 @@
1
- export function apiKeyPlugin(app: any, options: any): Promise<void>;
1
+ export const apiKeyPlugin: Function;
@@ -1,4 +1,3 @@
1
- import fp from 'fastify-plugin';
2
1
  import { timingSafeEqual as timingSafeEqual$1, createHmac, createHash } from 'node:crypto';
3
2
 
4
3
  /**
@@ -129,6 +128,67 @@ function isBuffer(value) {
129
128
  return Buffer.isBuffer(value);
130
129
  }
131
130
 
131
+ let counter = 0;
132
+
133
+ /**
134
+ * Mark a function as a Fastify plugin with encapsulation bypass.
135
+ *
136
+ * Drop-in replacement for the `fastify-plugin` npm package — covers
137
+ * the same symbols and option handling without the external dependency.
138
+ *
139
+ * @param {Function} fn Plugin function `(fastify, opts) => …`
140
+ * @param {string | PluginOptions} [options]
141
+ * A semver range string (shorthand for `{ fastify: range }`) or a
142
+ * full options object.
143
+ * @returns {Function} The same function, decorated with plugin metadata.
144
+ *
145
+ * @typedef {object} PluginOptions
146
+ * @property {string} [name] Display name. Auto-generated from
147
+ * `fn.name` when omitted.
148
+ * @property {string} [fastify] Semver range the plugin is compatible
149
+ * with (e.g. `'>=4'`). Fastify checks
150
+ * this at register time.
151
+ * @property {string[]} [decorators] Decorators the plugin expects to
152
+ * exist before it runs.
153
+ * @property {string[]} [dependencies] Plugins that must be registered
154
+ * before this one.
155
+ * @property {boolean} [encapsulate] When `true`, the plugin stays
156
+ * encapsulated (skip-override = false).
157
+ * Default `false` (skip override).
158
+ */
159
+ function fastifyPlugin(fn, options) {
160
+ if (fn && fn.default !== undefined) {
161
+ fn = fn.default;
162
+ }
163
+
164
+ if (!isFunction(fn)) {
165
+ throw new TypeError(`fastifyPlugin expects a function, instead got '${typeof fn}'`);
166
+ }
167
+
168
+ let opts;
169
+ if (isString(options)) {
170
+ opts = { fastify: options };
171
+ } else if (isObject(options)) {
172
+ opts = { ...options };
173
+ } else {
174
+ opts = {};
175
+ }
176
+
177
+ if (!opts.name) {
178
+ opts.name = (fn.name || 'anonymous') + '-auto-' + counter++;
179
+ }
180
+
181
+ fn[Symbol.for('skip-override')] = opts.encapsulate !== true;
182
+ fn[Symbol.for('fastify.display-name')] = opts.name;
183
+ fn[Symbol.for('plugin-meta')] = opts;
184
+
185
+ if (!fn.default) {
186
+ fn.default = fn;
187
+ }
188
+
189
+ return fn;
190
+ }
191
+
132
192
  /**
133
193
  * Imperative single-argument guard helpers — the everyday
134
194
  * `assertPositiveInt(x, 'name')` shape used at API boundaries.
@@ -786,11 +846,11 @@ function timingSafeEqual(a, b) {
786
846
  *
787
847
  * **Wire format:**
788
848
  *
789
- * <prefix>_<base64url(16 random bytes)>_<base64url(32 random bytes)>
790
- * ^^^^^^ ^^^^^^^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^^^^^^^^
791
- * type id (128-bit — plaintext DB key) secret (256-bit — HMAC'd)
849
+ * <prefix>_<crockford-base32(16 random bytes)>_<crockford-base32(32 random bytes)>
850
+ * ^^^^^^ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
851
+ * type id (128-bit, 26 chars) secret (256-bit, 52 chars)
792
852
  *
793
- * sk_live_QVI0RExDX01Fbg_pTk9UX0FfUkVBTF9LRVlfNDJfQVJUSVNBTkFMX1BJWk
853
+ * sk_live_<id-26-chars>_<secret-52-chars>
794
854
  *
795
855
  * Stripe / GitHub / Anthropic all use this three-segment shape. The
796
856
  * split matters:
@@ -870,9 +930,9 @@ function hashSecret(secretBytes, pepper) {
870
930
 
871
931
  /**
872
932
  * Timing-safe compare of a candidate hash against a stored hash. Both
873
- * inputs are base64url strings of equal length — a length mismatch is
874
- * treated as a mismatch after a fixed decode step, so an outside
875
- * observer cannot learn the stored hash's length via timing.
933
+ * inputs are base64url strings. A length mismatch early-returns `false`
934
+ * — all stored hashes are fixed-length (43-char SHA-256 digests), so
935
+ * the length check leaks no information about the stored value.
876
936
  *
877
937
  * @param {string} candidate
878
938
  * @param {string} stored
@@ -1338,45 +1398,36 @@ function _statusFor(reason) {
1338
1398
  *
1339
1399
  * app.get('/v1/whoami', async req => req.apiKey);
1340
1400
  *
1341
- * Wrapped with `fastify-plugin` so the `preHandler` hook and the
1342
- * `req.apiKey` decorator escape encapsulation and apply to routes
1343
- * registered as siblings in the parent scope. Register the plugin
1344
- * inside a scoped `app.register(async scope => …)` block to control
1345
- * which route group needs an API key — the usual Fastify pattern for
1346
- * "protect only these routes with auth".
1347
- *
1348
- * `fastify-plugin` is an **optional peer** — install it with
1349
- * `npm i fastify fastify-plugin` (or `yarn add`) alongside the fastify
1350
- * runtime. Not a hard dependency because Express-only consumers of
1351
- * `@exortek/apikey` should not have to install it.
1401
+ * The plugin bypasses Fastify's encapsulation so the `preHandler` hook
1402
+ * and the `req.apiKey` decorator apply to routes in the parent scope.
1403
+ * Register inside a scoped `app.register(async scope => …)` block to
1404
+ * control which route group needs an API key.
1352
1405
  */
1353
1406
 
1354
1407
 
1355
- async function apiKeyPluginFn(app, options) {
1356
- const config = normalizeOptions(options);
1357
-
1358
- app.decorateRequest(config.attach, null);
1408
+ const apiKeyPlugin = fastifyPlugin(
1409
+ async function apiKeyPluginFn(app, options) {
1410
+ const config = normalizeOptions(options);
1359
1411
 
1360
- app.addHook('preHandler', async (req, reply) => {
1361
- const ctx = {
1362
- getHeader: name => req.headers?.[name],
1363
- method: req.method,
1364
- ip: req.ip,
1365
- query: req.query,
1366
- };
1367
- const result = await runApiKey(ctx, config);
1368
- if (result.response) {
1369
- reply.code(result.response.status).send(result.response.body);
1370
- return reply;
1371
- }
1372
- req[config.attach] = result.verifyResult;
1373
- return undefined;
1374
- });
1375
- }
1412
+ app.decorateRequest(config.attach, null);
1376
1413
 
1377
- const apiKeyPlugin = fp(apiKeyPluginFn, {
1378
- fastify: '4.x || 5.x',
1379
- name: '@exortek/apikey',
1380
- });
1414
+ app.addHook('preHandler', async (req, reply) => {
1415
+ const ctx = {
1416
+ getHeader: name => req.headers?.[name],
1417
+ method: req.method,
1418
+ ip: req.ip,
1419
+ query: req.query,
1420
+ };
1421
+ const result = await runApiKey(ctx, config);
1422
+ if (result.response) {
1423
+ reply.code(result.response.status).send(result.response.body);
1424
+ return reply;
1425
+ }
1426
+ req[config.attach] = result.verifyResult;
1427
+ return undefined;
1428
+ });
1429
+ },
1430
+ { name: '@exortek/apikey' },
1431
+ );
1381
1432
 
1382
1433
  export { apiKeyPlugin };
package/dist/stores.cjs CHANGED
@@ -351,7 +351,11 @@ function createMemoryRecordStore(config) {
351
351
  * }}
352
352
  */
353
353
  function createRedisRecordStore(client, config) {
354
- const wrap = config.wrap ?? (msg => { throw new TypeError(msg); });
354
+ const wrap =
355
+ config.wrap ??
356
+ (msg => {
357
+ throw new TypeError(msg);
358
+ });
355
359
  assertRedisClient(client, ['get', 'set', 'del'], wrap);
356
360
 
357
361
  const {
@@ -1039,7 +1043,9 @@ function redisStore(client, options = {}) {
1039
1043
  }
1040
1044
  return record;
1041
1045
  },
1042
- wrap: msg => { throw invalidArgument(msg); },
1046
+ wrap: msg => {
1047
+ throw invalidArgument(msg);
1048
+ },
1043
1049
  });
1044
1050
 
1045
1051
  return {
package/dist/stores.mjs CHANGED
@@ -349,7 +349,11 @@ function createMemoryRecordStore(config) {
349
349
  * }}
350
350
  */
351
351
  function createRedisRecordStore(client, config) {
352
- const wrap = config.wrap ?? (msg => { throw new TypeError(msg); });
352
+ const wrap =
353
+ config.wrap ??
354
+ (msg => {
355
+ throw new TypeError(msg);
356
+ });
353
357
  assertRedisClient(client, ['get', 'set', 'del'], wrap);
354
358
 
355
359
  const {
@@ -1037,7 +1041,9 @@ function redisStore(client, options = {}) {
1037
1041
  }
1038
1042
  return record;
1039
1043
  },
1040
- wrap: msg => { throw invalidArgument(msg); },
1044
+ wrap: msg => {
1045
+ throw invalidArgument(msg);
1046
+ },
1041
1047
  });
1042
1048
 
1043
1049
  return {
package/dist/token.d.ts CHANGED
@@ -30,9 +30,9 @@ export function mint(prefix: string, pepper: Buffer | null): {
30
30
  export function hashSecret(secretBytes: Buffer, pepper: Buffer | null): string;
31
31
  /**
32
32
  * Timing-safe compare of a candidate hash against a stored hash. Both
33
- * inputs are base64url strings of equal length — a length mismatch is
34
- * treated as a mismatch after a fixed decode step, so an outside
35
- * observer cannot learn the stored hash's length via timing.
33
+ * inputs are base64url strings. A length mismatch early-returns `false`
34
+ * — all stored hashes are fixed-length (43-char SHA-256 digests), so
35
+ * the length check leaks no information about the stored value.
36
36
  *
37
37
  * @param {string} candidate
38
38
  * @param {string} stored
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@exortek/apikey",
3
- "version": "1.0.1",
3
+ "version": "1.0.2",
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,
@@ -32,6 +32,7 @@
32
32
  "files": [
33
33
  "dist",
34
34
  "README.md",
35
+ "CHANGELOG.md",
35
36
  "LICENSE"
36
37
  ],
37
38
  "scripts": {
@@ -72,8 +73,7 @@
72
73
  },
73
74
  "peerDependencies": {
74
75
  "express": ">=4",
75
- "fastify": ">=4",
76
- "fastify-plugin": ">=4"
76
+ "fastify": ">=4"
77
77
  },
78
78
  "peerDependenciesMeta": {
79
79
  "express": {
@@ -81,17 +81,8 @@
81
81
  },
82
82
  "fastify": {
83
83
  "optional": true
84
- },
85
- "fastify-plugin": {
86
- "optional": true
87
84
  }
88
85
  },
89
- "devDependencies": {
90
- "@exortek/shared": "0.0.0",
91
- "express": "^5.2.1",
92
- "fastify": "^5.10.0",
93
- "fastify-plugin": "^6.0.0"
94
- },
95
86
  "publishConfig": {
96
87
  "access": "public"
97
88
  }