@exortek/apikey 1.0.0 → 1.0.1
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 +32 -32
- package/dist/index.cjs +15 -5
- package/dist/index.mjs +15 -5
- package/dist/middleware/express.cjs +15 -5
- package/dist/middleware/express.mjs +15 -5
- package/dist/middleware/fastify.cjs +15 -5
- package/dist/middleware/fastify.mjs +15 -5
- package/dist/stores.cjs +534 -326
- package/dist/stores.mjs +534 -326
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,38 +1,38 @@
|
|
|
1
1
|
# @exortek/apikey
|
|
2
2
|
|
|
3
|
+
## 1.0.1
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 31223e4: Consolidate duplicated store internals into @exortek/shared utilities (redis-helpers, incr-store,
|
|
8
|
+
record-store). No public API changes.
|
|
9
|
+
|
|
10
|
+
apikey: fix Redis store race condition where a concurrent update() could silently un-revoke a key — revocations now
|
|
11
|
+
use a tombstone key that update() never touches.
|
|
12
|
+
|
|
13
|
+
apikey: fix memory store put() storing by reference instead of copying — now consistent with getById()'s copy-on-read
|
|
14
|
+
contract.
|
|
15
|
+
|
|
3
16
|
## 1.0.0
|
|
4
17
|
|
|
5
18
|
### Initial release
|
|
6
19
|
|
|
7
|
-
- **`createApiKey(options)`** — mint a Stripe-style 3-segment token
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
-
|
|
17
|
-
|
|
18
|
-
- **
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
-
|
|
25
|
-
|
|
26
|
-
- **Stores:** `memoryStore()` and `redisStore(client, options?)` under
|
|
27
|
-
the `@exortek/apikey/stores` subpath. Standard CRUD (`put` /
|
|
28
|
-
`getById` / `update` / `revoke` / `revokeAllForUser` / `listByUser`)
|
|
29
|
-
— bring your own by implementing the interface.
|
|
30
|
-
- **Middleware:** Express (`@exortek/apikey/middleware/express`) and
|
|
31
|
-
Fastify (`@exortek/apikey/middleware/fastify`) adapters over a
|
|
32
|
-
shared `middleware/core.js` — `Authorization: Bearer <key>` by
|
|
33
|
-
default, configurable header + raw scheme + opt-in query-param
|
|
34
|
-
fallback. Attaches the verify result to `req.apiKey`.
|
|
35
|
-
- **Errors:** `ApiKeyError` + `ErrorCode` catalogue
|
|
36
|
-
(`INVALID_ARGUMENT` / `INVALID_PREFIX` / `INVALID_PEPPER` /
|
|
37
|
-
`STORE_ERROR`). Expected verify failures surface as
|
|
38
|
-
`{ valid: false, reason }`, not exceptions.
|
|
20
|
+
- **`createApiKey(options)`** — mint a Stripe-style 3-segment token `<prefix>_<id>_<secret>` (base32-crockford, no
|
|
21
|
+
ambiguous glyphs). Stores an HMAC-SHA256 hash of the secret plus a plaintext id for O(1) lookup. Optional peppers
|
|
22
|
+
(newest-first array, each ≥16 bytes) turn a stolen DB into a non-crackable artifact.
|
|
23
|
+
- **`verifyApiKey(rawKey, options)`** — parse, look up by id, timing-safe hash compare, expiry / revocation / scope
|
|
24
|
+
enforcement. Returns `{ valid: true, userId, scopes, id, prefix, name?, environment?, metadata?, needsRehash? }` or
|
|
25
|
+
`{ valid: false, reason }` — never throws on a bad key.
|
|
26
|
+
- **Scopes:** `hasAll` / `hasAny` / `covers` with `*` super wildcard and `namespace:*` suffix wildcard.
|
|
27
|
+
- **Pepper rotation:** verify against every pepper, mint with the newest; `needsRehash: true` in the verify result
|
|
28
|
+
signals the secret matched an older pepper so `rehashApiKey` can silently migrate storage.
|
|
29
|
+
- **`revokeApiKey`** (by key or id), **`revokeAllForUser`**, **`listApiKeys`** (sorted most-recently-used first).
|
|
30
|
+
- **`mask(key)`** log-safe display, **`parseApiKey(key)`** unverified segment extraction.
|
|
31
|
+
- **Stores:** `memoryStore()` and `redisStore(client, options?)` under the `@exortek/apikey/stores` subpath. Standard
|
|
32
|
+
CRUD (`put` / `getById` / `update` / `revoke` / `revokeAllForUser` / `listByUser`) — bring your own by implementing
|
|
33
|
+
the interface.
|
|
34
|
+
- **Middleware:** Express (`@exortek/apikey/middleware/express`) and Fastify (`@exortek/apikey/middleware/fastify`)
|
|
35
|
+
adapters over a shared `middleware/core.js` — `Authorization: Bearer <key>` by default, configurable header + raw
|
|
36
|
+
scheme + opt-in query-param fallback. Attaches the verify result to `req.apiKey`.
|
|
37
|
+
- **Errors:** `ApiKeyError` + `ErrorCode` catalogue (`INVALID_ARGUMENT` / `INVALID_PREFIX` / `INVALID_PEPPER` /
|
|
38
|
+
`STORE_ERROR`). Expected verify failures surface as `{ valid: false, reason }`, not exceptions.
|
package/dist/index.cjs
CHANGED
|
@@ -1093,15 +1093,25 @@ function parseApiKey(key) {
|
|
|
1093
1093
|
// secret use the base32-crockford alphabet which contains no
|
|
1094
1094
|
// underscores — so the last two '_' cleanly bound them.
|
|
1095
1095
|
const lastUnderscore = key.lastIndexOf('_');
|
|
1096
|
-
if (lastUnderscore <= 0) {
|
|
1096
|
+
if (lastUnderscore <= 0) {
|
|
1097
|
+
return null;
|
|
1098
|
+
}
|
|
1097
1099
|
const prevUnderscore = key.lastIndexOf('_', lastUnderscore - 1);
|
|
1098
|
-
if (prevUnderscore <= 0) {
|
|
1100
|
+
if (prevUnderscore <= 0) {
|
|
1101
|
+
return null;
|
|
1102
|
+
}
|
|
1099
1103
|
const prefix = key.slice(0, prevUnderscore);
|
|
1100
1104
|
const id = key.slice(prevUnderscore + 1, lastUnderscore);
|
|
1101
1105
|
const secret = key.slice(lastUnderscore + 1);
|
|
1102
|
-
if (!PREFIX_RE.test(prefix)) {
|
|
1103
|
-
|
|
1104
|
-
|
|
1106
|
+
if (!PREFIX_RE.test(prefix)) {
|
|
1107
|
+
return null;
|
|
1108
|
+
}
|
|
1109
|
+
if (id.length !== ID_CHARS || !CROCKFORD_RE.test(id)) {
|
|
1110
|
+
return null;
|
|
1111
|
+
}
|
|
1112
|
+
if (secret.length !== SECRET_CHARS || !CROCKFORD_RE.test(secret)) {
|
|
1113
|
+
return null;
|
|
1114
|
+
}
|
|
1105
1115
|
return { prefix, id, secret };
|
|
1106
1116
|
}
|
|
1107
1117
|
|
package/dist/index.mjs
CHANGED
|
@@ -1091,15 +1091,25 @@ function parseApiKey(key) {
|
|
|
1091
1091
|
// secret use the base32-crockford alphabet which contains no
|
|
1092
1092
|
// underscores — so the last two '_' cleanly bound them.
|
|
1093
1093
|
const lastUnderscore = key.lastIndexOf('_');
|
|
1094
|
-
if (lastUnderscore <= 0) {
|
|
1094
|
+
if (lastUnderscore <= 0) {
|
|
1095
|
+
return null;
|
|
1096
|
+
}
|
|
1095
1097
|
const prevUnderscore = key.lastIndexOf('_', lastUnderscore - 1);
|
|
1096
|
-
if (prevUnderscore <= 0) {
|
|
1098
|
+
if (prevUnderscore <= 0) {
|
|
1099
|
+
return null;
|
|
1100
|
+
}
|
|
1097
1101
|
const prefix = key.slice(0, prevUnderscore);
|
|
1098
1102
|
const id = key.slice(prevUnderscore + 1, lastUnderscore);
|
|
1099
1103
|
const secret = key.slice(lastUnderscore + 1);
|
|
1100
|
-
if (!PREFIX_RE.test(prefix)) {
|
|
1101
|
-
|
|
1102
|
-
|
|
1104
|
+
if (!PREFIX_RE.test(prefix)) {
|
|
1105
|
+
return null;
|
|
1106
|
+
}
|
|
1107
|
+
if (id.length !== ID_CHARS || !CROCKFORD_RE.test(id)) {
|
|
1108
|
+
return null;
|
|
1109
|
+
}
|
|
1110
|
+
if (secret.length !== SECRET_CHARS || !CROCKFORD_RE.test(secret)) {
|
|
1111
|
+
return null;
|
|
1112
|
+
}
|
|
1103
1113
|
return { prefix, id, secret };
|
|
1104
1114
|
}
|
|
1105
1115
|
|
|
@@ -914,15 +914,25 @@ function parseApiKey(key) {
|
|
|
914
914
|
// secret use the base32-crockford alphabet which contains no
|
|
915
915
|
// underscores — so the last two '_' cleanly bound them.
|
|
916
916
|
const lastUnderscore = key.lastIndexOf('_');
|
|
917
|
-
if (lastUnderscore <= 0) {
|
|
917
|
+
if (lastUnderscore <= 0) {
|
|
918
|
+
return null;
|
|
919
|
+
}
|
|
918
920
|
const prevUnderscore = key.lastIndexOf('_', lastUnderscore - 1);
|
|
919
|
-
if (prevUnderscore <= 0) {
|
|
921
|
+
if (prevUnderscore <= 0) {
|
|
922
|
+
return null;
|
|
923
|
+
}
|
|
920
924
|
const prefix = key.slice(0, prevUnderscore);
|
|
921
925
|
const id = key.slice(prevUnderscore + 1, lastUnderscore);
|
|
922
926
|
const secret = key.slice(lastUnderscore + 1);
|
|
923
|
-
if (!PREFIX_RE.test(prefix)) {
|
|
924
|
-
|
|
925
|
-
|
|
927
|
+
if (!PREFIX_RE.test(prefix)) {
|
|
928
|
+
return null;
|
|
929
|
+
}
|
|
930
|
+
if (id.length !== ID_CHARS || !CROCKFORD_RE.test(id)) {
|
|
931
|
+
return null;
|
|
932
|
+
}
|
|
933
|
+
if (secret.length !== SECRET_CHARS || !CROCKFORD_RE.test(secret)) {
|
|
934
|
+
return null;
|
|
935
|
+
}
|
|
926
936
|
return { prefix, id, secret };
|
|
927
937
|
}
|
|
928
938
|
|
|
@@ -912,15 +912,25 @@ function parseApiKey(key) {
|
|
|
912
912
|
// secret use the base32-crockford alphabet which contains no
|
|
913
913
|
// underscores — so the last two '_' cleanly bound them.
|
|
914
914
|
const lastUnderscore = key.lastIndexOf('_');
|
|
915
|
-
if (lastUnderscore <= 0) {
|
|
915
|
+
if (lastUnderscore <= 0) {
|
|
916
|
+
return null;
|
|
917
|
+
}
|
|
916
918
|
const prevUnderscore = key.lastIndexOf('_', lastUnderscore - 1);
|
|
917
|
-
if (prevUnderscore <= 0) {
|
|
919
|
+
if (prevUnderscore <= 0) {
|
|
920
|
+
return null;
|
|
921
|
+
}
|
|
918
922
|
const prefix = key.slice(0, prevUnderscore);
|
|
919
923
|
const id = key.slice(prevUnderscore + 1, lastUnderscore);
|
|
920
924
|
const secret = key.slice(lastUnderscore + 1);
|
|
921
|
-
if (!PREFIX_RE.test(prefix)) {
|
|
922
|
-
|
|
923
|
-
|
|
925
|
+
if (!PREFIX_RE.test(prefix)) {
|
|
926
|
+
return null;
|
|
927
|
+
}
|
|
928
|
+
if (id.length !== ID_CHARS || !CROCKFORD_RE.test(id)) {
|
|
929
|
+
return null;
|
|
930
|
+
}
|
|
931
|
+
if (secret.length !== SECRET_CHARS || !CROCKFORD_RE.test(secret)) {
|
|
932
|
+
return null;
|
|
933
|
+
}
|
|
924
934
|
return { prefix, id, secret };
|
|
925
935
|
}
|
|
926
936
|
|
|
@@ -915,15 +915,25 @@ function parseApiKey(key) {
|
|
|
915
915
|
// secret use the base32-crockford alphabet which contains no
|
|
916
916
|
// underscores — so the last two '_' cleanly bound them.
|
|
917
917
|
const lastUnderscore = key.lastIndexOf('_');
|
|
918
|
-
if (lastUnderscore <= 0) {
|
|
918
|
+
if (lastUnderscore <= 0) {
|
|
919
|
+
return null;
|
|
920
|
+
}
|
|
919
921
|
const prevUnderscore = key.lastIndexOf('_', lastUnderscore - 1);
|
|
920
|
-
if (prevUnderscore <= 0) {
|
|
922
|
+
if (prevUnderscore <= 0) {
|
|
923
|
+
return null;
|
|
924
|
+
}
|
|
921
925
|
const prefix = key.slice(0, prevUnderscore);
|
|
922
926
|
const id = key.slice(prevUnderscore + 1, lastUnderscore);
|
|
923
927
|
const secret = key.slice(lastUnderscore + 1);
|
|
924
|
-
if (!PREFIX_RE.test(prefix)) {
|
|
925
|
-
|
|
926
|
-
|
|
928
|
+
if (!PREFIX_RE.test(prefix)) {
|
|
929
|
+
return null;
|
|
930
|
+
}
|
|
931
|
+
if (id.length !== ID_CHARS || !CROCKFORD_RE.test(id)) {
|
|
932
|
+
return null;
|
|
933
|
+
}
|
|
934
|
+
if (secret.length !== SECRET_CHARS || !CROCKFORD_RE.test(secret)) {
|
|
935
|
+
return null;
|
|
936
|
+
}
|
|
927
937
|
return { prefix, id, secret };
|
|
928
938
|
}
|
|
929
939
|
|
|
@@ -913,15 +913,25 @@ function parseApiKey(key) {
|
|
|
913
913
|
// secret use the base32-crockford alphabet which contains no
|
|
914
914
|
// underscores — so the last two '_' cleanly bound them.
|
|
915
915
|
const lastUnderscore = key.lastIndexOf('_');
|
|
916
|
-
if (lastUnderscore <= 0) {
|
|
916
|
+
if (lastUnderscore <= 0) {
|
|
917
|
+
return null;
|
|
918
|
+
}
|
|
917
919
|
const prevUnderscore = key.lastIndexOf('_', lastUnderscore - 1);
|
|
918
|
-
if (prevUnderscore <= 0) {
|
|
920
|
+
if (prevUnderscore <= 0) {
|
|
921
|
+
return null;
|
|
922
|
+
}
|
|
919
923
|
const prefix = key.slice(0, prevUnderscore);
|
|
920
924
|
const id = key.slice(prevUnderscore + 1, lastUnderscore);
|
|
921
925
|
const secret = key.slice(lastUnderscore + 1);
|
|
922
|
-
if (!PREFIX_RE.test(prefix)) {
|
|
923
|
-
|
|
924
|
-
|
|
926
|
+
if (!PREFIX_RE.test(prefix)) {
|
|
927
|
+
return null;
|
|
928
|
+
}
|
|
929
|
+
if (id.length !== ID_CHARS || !CROCKFORD_RE.test(id)) {
|
|
930
|
+
return null;
|
|
931
|
+
}
|
|
932
|
+
if (secret.length !== SECRET_CHARS || !CROCKFORD_RE.test(secret)) {
|
|
933
|
+
return null;
|
|
934
|
+
}
|
|
925
935
|
return { prefix, id, secret };
|
|
926
936
|
}
|
|
927
937
|
|