@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 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
- `<prefix>_<id>_<secret>` (base32-crockford, no ambiguous glyphs).
9
- Stores an HMAC-SHA256 hash of the secret plus a plaintext id for
10
- O(1) lookup. Optional peppers (newest-first array, each ≥16 bytes)
11
- turn a stolen DB into a non-crackable artifact.
12
- - **`verifyApiKey(rawKey, options)`** — parse, look up by id, timing-safe
13
- hash compare, expiry / revocation / scope enforcement. Returns
14
- `{ valid: true, userId, scopes, id, prefix, name?, environment?, metadata?, needsRehash? }`
15
- or `{ valid: false, reason }` — never throws on a bad key.
16
- - **Scopes:** `hasAll` / `hasAny` / `covers` with `*` super wildcard and
17
- `namespace:*` suffix wildcard.
18
- - **Pepper rotation:** verify against every pepper, mint with the
19
- newest; `needsRehash: true` in the verify result signals the secret
20
- matched an older pepper so `rehashApiKey` can silently migrate
21
- storage.
22
- - **`revokeApiKey`** (by key or id), **`revokeAllForUser`**,
23
- **`listApiKeys`** (sorted most-recently-used first).
24
- - **`mask(key)`** log-safe display, **`parseApiKey(key)`** unverified
25
- segment extraction.
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) {return null;}
1096
+ if (lastUnderscore <= 0) {
1097
+ return null;
1098
+ }
1097
1099
  const prevUnderscore = key.lastIndexOf('_', lastUnderscore - 1);
1098
- if (prevUnderscore <= 0) {return null;}
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)) {return null;}
1103
- if (id.length !== ID_CHARS || !CROCKFORD_RE.test(id)) {return null;}
1104
- if (secret.length !== SECRET_CHARS || !CROCKFORD_RE.test(secret)) {return null;}
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) {return null;}
1094
+ if (lastUnderscore <= 0) {
1095
+ return null;
1096
+ }
1095
1097
  const prevUnderscore = key.lastIndexOf('_', lastUnderscore - 1);
1096
- if (prevUnderscore <= 0) {return null;}
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)) {return null;}
1101
- if (id.length !== ID_CHARS || !CROCKFORD_RE.test(id)) {return null;}
1102
- if (secret.length !== SECRET_CHARS || !CROCKFORD_RE.test(secret)) {return null;}
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) {return null;}
917
+ if (lastUnderscore <= 0) {
918
+ return null;
919
+ }
918
920
  const prevUnderscore = key.lastIndexOf('_', lastUnderscore - 1);
919
- if (prevUnderscore <= 0) {return null;}
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)) {return null;}
924
- if (id.length !== ID_CHARS || !CROCKFORD_RE.test(id)) {return null;}
925
- if (secret.length !== SECRET_CHARS || !CROCKFORD_RE.test(secret)) {return null;}
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) {return null;}
915
+ if (lastUnderscore <= 0) {
916
+ return null;
917
+ }
916
918
  const prevUnderscore = key.lastIndexOf('_', lastUnderscore - 1);
917
- if (prevUnderscore <= 0) {return null;}
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)) {return null;}
922
- if (id.length !== ID_CHARS || !CROCKFORD_RE.test(id)) {return null;}
923
- if (secret.length !== SECRET_CHARS || !CROCKFORD_RE.test(secret)) {return null;}
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) {return null;}
918
+ if (lastUnderscore <= 0) {
919
+ return null;
920
+ }
919
921
  const prevUnderscore = key.lastIndexOf('_', lastUnderscore - 1);
920
- if (prevUnderscore <= 0) {return null;}
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)) {return null;}
925
- if (id.length !== ID_CHARS || !CROCKFORD_RE.test(id)) {return null;}
926
- if (secret.length !== SECRET_CHARS || !CROCKFORD_RE.test(secret)) {return null;}
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) {return null;}
916
+ if (lastUnderscore <= 0) {
917
+ return null;
918
+ }
917
919
  const prevUnderscore = key.lastIndexOf('_', lastUnderscore - 1);
918
- if (prevUnderscore <= 0) {return null;}
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)) {return null;}
923
- if (id.length !== ID_CHARS || !CROCKFORD_RE.test(id)) {return null;}
924
- if (secret.length !== SECRET_CHARS || !CROCKFORD_RE.test(secret)) {return null;}
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