@exortek/apikey 1.0.0 → 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 +42 -32
- package/dist/index.cjs +22 -12
- package/dist/index.d.ts +2 -2
- package/dist/index.mjs +22 -12
- package/dist/middleware/core.d.ts +2 -2
- package/dist/middleware/express.cjs +22 -12
- package/dist/middleware/express.mjs +22 -12
- package/dist/middleware/fastify.cjs +109 -48
- package/dist/middleware/fastify.d.ts +1 -1
- package/dist/middleware/fastify.mjs +109 -48
- package/dist/stores.cjs +539 -325
- package/dist/stores.mjs +539 -325
- package/dist/token.d.ts +3 -3
- package/package.json +3 -12
|
@@ -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>_<
|
|
790
|
-
* ^^^^^^
|
|
791
|
-
* type id (128-bit
|
|
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
|
-
*
|
|
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
|
|
874
|
-
*
|
|
875
|
-
*
|
|
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
|
|
@@ -913,15 +973,25 @@ function parseApiKey(key) {
|
|
|
913
973
|
// secret use the base32-crockford alphabet which contains no
|
|
914
974
|
// underscores — so the last two '_' cleanly bound them.
|
|
915
975
|
const lastUnderscore = key.lastIndexOf('_');
|
|
916
|
-
if (lastUnderscore <= 0) {
|
|
976
|
+
if (lastUnderscore <= 0) {
|
|
977
|
+
return null;
|
|
978
|
+
}
|
|
917
979
|
const prevUnderscore = key.lastIndexOf('_', lastUnderscore - 1);
|
|
918
|
-
if (prevUnderscore <= 0) {
|
|
980
|
+
if (prevUnderscore <= 0) {
|
|
981
|
+
return null;
|
|
982
|
+
}
|
|
919
983
|
const prefix = key.slice(0, prevUnderscore);
|
|
920
984
|
const id = key.slice(prevUnderscore + 1, lastUnderscore);
|
|
921
985
|
const secret = key.slice(lastUnderscore + 1);
|
|
922
|
-
if (!PREFIX_RE.test(prefix)) {
|
|
923
|
-
|
|
924
|
-
|
|
986
|
+
if (!PREFIX_RE.test(prefix)) {
|
|
987
|
+
return null;
|
|
988
|
+
}
|
|
989
|
+
if (id.length !== ID_CHARS || !CROCKFORD_RE.test(id)) {
|
|
990
|
+
return null;
|
|
991
|
+
}
|
|
992
|
+
if (secret.length !== SECRET_CHARS || !CROCKFORD_RE.test(secret)) {
|
|
993
|
+
return null;
|
|
994
|
+
}
|
|
925
995
|
return { prefix, id, secret };
|
|
926
996
|
}
|
|
927
997
|
|
|
@@ -1328,45 +1398,36 @@ function _statusFor(reason) {
|
|
|
1328
1398
|
*
|
|
1329
1399
|
* app.get('/v1/whoami', async req => req.apiKey);
|
|
1330
1400
|
*
|
|
1331
|
-
*
|
|
1332
|
-
* `req.apiKey` decorator
|
|
1333
|
-
*
|
|
1334
|
-
*
|
|
1335
|
-
* which route group needs an API key — the usual Fastify pattern for
|
|
1336
|
-
* "protect only these routes with auth".
|
|
1337
|
-
*
|
|
1338
|
-
* `fastify-plugin` is an **optional peer** — install it with
|
|
1339
|
-
* `npm i fastify fastify-plugin` (or `yarn add`) alongside the fastify
|
|
1340
|
-
* runtime. Not a hard dependency because Express-only consumers of
|
|
1341
|
-
* `@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.
|
|
1342
1405
|
*/
|
|
1343
1406
|
|
|
1344
1407
|
|
|
1345
|
-
|
|
1346
|
-
|
|
1408
|
+
const apiKeyPlugin = fastifyPlugin(
|
|
1409
|
+
async function apiKeyPluginFn(app, options) {
|
|
1410
|
+
const config = normalizeOptions(options);
|
|
1347
1411
|
|
|
1348
|
-
|
|
1412
|
+
app.decorateRequest(config.attach, null);
|
|
1349
1413
|
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
}
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
fastify: '4.x || 5.x',
|
|
1369
|
-
name: '@exortek/apikey',
|
|
1370
|
-
});
|
|
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
|
+
);
|
|
1371
1432
|
|
|
1372
1433
|
export { apiKeyPlugin };
|