@exortek/apikey 1.0.0
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 +38 -0
- package/LICENSE +21 -0
- package/README.md +263 -0
- package/dist/errors.d.ts +20 -0
- package/dist/index.cjs +1632 -0
- package/dist/index.d.ts +278 -0
- package/dist/index.mjs +1617 -0
- package/dist/internal/guards.d.ts +4 -0
- package/dist/middleware/core.d.ts +86 -0
- package/dist/middleware/express.cjs +1363 -0
- package/dist/middleware/express.d.ts +4 -0
- package/dist/middleware/express.mjs +1361 -0
- package/dist/middleware/fastify.cjs +1374 -0
- package/dist/middleware/fastify.d.ts +1 -0
- package/dist/middleware/fastify.mjs +1372 -0
- package/dist/scopes.d.ts +21 -0
- package/dist/stores/index.d.ts +2 -0
- package/dist/stores/memory.d.ts +6 -0
- package/dist/stores/redis.d.ts +13 -0
- package/dist/stores.cjs +880 -0
- package/dist/stores.mjs +877 -0
- package/dist/token.d.ts +88 -0
- package/package.json +98 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# @exortek/apikey
|
|
2
|
+
|
|
3
|
+
## 1.0.0
|
|
4
|
+
|
|
5
|
+
### Initial release
|
|
6
|
+
|
|
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.
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 ExorTek
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
# @exortek/apikey
|
|
2
|
+
|
|
3
|
+
> Stripe-style prefixed API keys for Node.js 22+ — 3-segment tokens (`sk_live_<id>_<secret>`), HMAC-SHA256 storage hash with optional pepper rotation, scope allowlists, memory + Redis stores, Express + Fastify middleware. Zero non-`@exortek/*` runtime dependencies.
|
|
4
|
+
|
|
5
|
+
[](https://www.npmjs.com/package/@exortek/apikey)
|
|
6
|
+
[](https://nodejs.org)
|
|
7
|
+
[](https://packagephobia.com/result?p=@exortek/apikey)
|
|
8
|
+
[](./LICENSE)
|
|
9
|
+
[](https://github.com/ExorTek/auth/actions/workflows/ci.yml)
|
|
10
|
+
|
|
11
|
+
Every API vendor rebuilds the same six things: a prefixed token format so keys are recognisable in logs, hashed storage so a leaked DB doesn't leak secrets, scope allowlists so a machine token can't accidentally admin your infrastructure, revocation, a per-user list for the "manage API keys" UI, and a middleware that turns `Authorization: Bearer <key>` into a verified caller identity. `@exortek/apikey` ships all six.
|
|
12
|
+
|
|
13
|
+
## Install
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
npm i @exortek/apikey
|
|
17
|
+
# or
|
|
18
|
+
yarn add @exortek/apikey
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Node.js 22 LTS or newer. Optional: `express` or `fastify` peer deps for the shipped middleware.
|
|
22
|
+
|
|
23
|
+
## Token format
|
|
24
|
+
|
|
25
|
+
```
|
|
26
|
+
sk_live_A2X4KJRK5HYFR3E7CBEQZP9WTN_QM5K3EHRSPMY8TDN4GRFB2CGY6VJE1BW7FRXK8ZHDNA9TPY5CV6QM
|
|
27
|
+
^^^^^ ^^^^^^^^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
|
28
|
+
type id — 128-bit, plaintext secret — 256-bit, HMAC-SHA256 for storage
|
|
29
|
+
(O(1) DB primary-key lookup)
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Three underscore-separated segments. **id** and **secret** use base32-crockford (`[0-9A-HJKMNP-TV-Z]`) — no ambiguous glyphs, no separators, safe in URLs and audit logs. **prefix** is Stripe-style: lowercase alpha + digits + underscore-separated segments (`sk_live`, `pk_test`, `svc_prod_v2`).
|
|
33
|
+
|
|
34
|
+
The **id** is plaintext because it isn't a secret — an attacker who guesses an id alone still fails the HMAC compare on the secret half. The **secret** is never stored in the clear; the DB keeps only `HMAC-SHA256(secret, pepper)` or plain `SHA-256(secret)` when no pepper is configured.
|
|
35
|
+
|
|
36
|
+
**Why fast SHA-256 and not Argon2?** API keys are random 256-bit values. Argon2 / bcrypt / scrypt slow down brute force on **low-entropy** passwords. A 256-bit random secret has no brute-force attack to slow — a fast hash is correct here.
|
|
37
|
+
|
|
38
|
+
## Quick start
|
|
39
|
+
|
|
40
|
+
```js
|
|
41
|
+
import { createApiKey, verifyApiKey, revokeApiKey } from '@exortek/apikey';
|
|
42
|
+
import { memoryStore } from '@exortek/apikey/stores';
|
|
43
|
+
|
|
44
|
+
const store = memoryStore();
|
|
45
|
+
const peppers = [Buffer.from(process.env.APIKEY_PEPPER, 'base64url')]; // optional
|
|
46
|
+
|
|
47
|
+
// Issue a key to a user
|
|
48
|
+
const { key, id, record } = await createApiKey({
|
|
49
|
+
store,
|
|
50
|
+
peppers,
|
|
51
|
+
prefix: 'sk_live',
|
|
52
|
+
userId: 'usr_123',
|
|
53
|
+
scopes: ['read', 'write:posts'],
|
|
54
|
+
name: 'Production Backend',
|
|
55
|
+
environment: 'live',
|
|
56
|
+
expiresIn: '1y',
|
|
57
|
+
});
|
|
58
|
+
// → key = 'sk_live_A2X4…6QM' — SHOW ONCE, never persist in the clear.
|
|
59
|
+
// → id = 'A2X4KJRK5HYFR3E7CBEQZP9WTN' — safe to log/display; primary key.
|
|
60
|
+
|
|
61
|
+
// Verify one on an incoming request
|
|
62
|
+
const res = await verifyApiKey(incomingKey, {
|
|
63
|
+
store,
|
|
64
|
+
peppers,
|
|
65
|
+
requiredScopes: ['read'],
|
|
66
|
+
updateLastUsed: true,
|
|
67
|
+
});
|
|
68
|
+
if (!res.valid) return reply.code(401).send({ error: res.reason });
|
|
69
|
+
// res = { valid: true, userId, scopes, id, prefix, name?, environment?, metadata?, needsRehash? }
|
|
70
|
+
|
|
71
|
+
// Revoke — by full key or by id
|
|
72
|
+
await revokeApiKey(incomingKey, { store, reason: 'user_dashboard' });
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
## API
|
|
76
|
+
|
|
77
|
+
### `createApiKey(options)`
|
|
78
|
+
|
|
79
|
+
```ts
|
|
80
|
+
createApiKey({
|
|
81
|
+
store: ApiKeyStore,
|
|
82
|
+
prefix: string, // Stripe-style — see /^[a-z][a-z0-9]{0,15}(_[a-z0-9]{1,15}){0,3}$/
|
|
83
|
+
userId: string,
|
|
84
|
+
scopes: string[],
|
|
85
|
+
|
|
86
|
+
name?: string, // Human label — 'Production Backend'
|
|
87
|
+
environment?: string, // 'live' / 'test' / caller-defined
|
|
88
|
+
metadata?: Record<string, unknown>,
|
|
89
|
+
expiresIn?: string | number, // '1y' / '30d' / ms integer; omit for no expiry
|
|
90
|
+
|
|
91
|
+
peppers?: (Buffer | Uint8Array | string)[], // Newest first, each ≥16 bytes
|
|
92
|
+
now?: number, // Override Date.now() for testing
|
|
93
|
+
}): Promise<{ key: string, id: string, record: ApiKeyRecord }>
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
The wire `key` is returned once. Show it to the end user immediately (a dashboard modal, an email attachment) and never persist the raw value — only `record.hash` is safe to keep.
|
|
97
|
+
|
|
98
|
+
### `verifyApiKey(rawKey, options)`
|
|
99
|
+
|
|
100
|
+
```ts
|
|
101
|
+
verifyApiKey(rawKey, {
|
|
102
|
+
store: ApiKeyStore,
|
|
103
|
+
peppers?: (Buffer | Uint8Array | string)[],
|
|
104
|
+
requiredScopes?: string[],
|
|
105
|
+
expectedPrefix?: string,
|
|
106
|
+
updateLastUsed?: boolean,
|
|
107
|
+
now?: number,
|
|
108
|
+
}): Promise<
|
|
109
|
+
| { valid: true, id, userId, scopes, prefix, name?, environment?, metadata?, needsRehash? }
|
|
110
|
+
| { valid: false, reason }
|
|
111
|
+
>
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
Never throws on a bad key. Failure reasons:
|
|
115
|
+
|
|
116
|
+
| Reason | Meaning |
|
|
117
|
+
| ------------------- | --------------------------------------------------------- |
|
|
118
|
+
| `malformed` | Wrong shape, wrong length, non-crockford chars. |
|
|
119
|
+
| `not_found` | Well-shaped but the id has never been issued (or was purged). |
|
|
120
|
+
| `expired` | `expiresAt` has passed. |
|
|
121
|
+
| `revoked` | Record has a `revokedAt` timestamp. |
|
|
122
|
+
| `bad_secret` | HMAC compare failed — wrong secret or wrong pepper. |
|
|
123
|
+
| `prefix_mismatch` | `expectedPrefix` set and doesn't match; or the record's stored prefix differs from what was submitted (attacker guessed the id and swapped the prefix). |
|
|
124
|
+
| `missing_scope` | `requiredScopes` not covered by the record's `scopes`. |
|
|
125
|
+
| `store_unavailable` | `store.getById` threw. |
|
|
126
|
+
|
|
127
|
+
### Scopes: `covers` / `hasAll` / `hasAny`
|
|
128
|
+
|
|
129
|
+
```js
|
|
130
|
+
import { covers, hasAll, hasAny } from '@exortek/apikey';
|
|
131
|
+
|
|
132
|
+
covers(['read:*'], 'read:users'); // true — namespace wildcard
|
|
133
|
+
covers(['*'], 'anything'); // true — super wildcard
|
|
134
|
+
hasAll(['read:*'], ['read:users', 'read:posts']); // true
|
|
135
|
+
hasAll(['read:users'], ['read:*']); // false — required is more specific than granted
|
|
136
|
+
hasAny(['read'], ['read', 'write']); // true
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
Wildcards only apply on the **granted** side. `required` values are always concrete — an endpoint declares exactly which scope it needs.
|
|
140
|
+
|
|
141
|
+
### Pepper rotation
|
|
142
|
+
|
|
143
|
+
```js
|
|
144
|
+
const oldPepper = Buffer.from(process.env.APIKEY_PEPPER_V1, 'base64url');
|
|
145
|
+
const newPepper = Buffer.from(process.env.APIKEY_PEPPER_V2, 'base64url');
|
|
146
|
+
|
|
147
|
+
// Verify against both, mint with the newest.
|
|
148
|
+
const res = await verifyApiKey(key, { store, peppers: [newPepper, oldPepper] });
|
|
149
|
+
if (res.valid && res.needsRehash) {
|
|
150
|
+
await rehashApiKey(key, { store, peppers: [newPepper, oldPepper] });
|
|
151
|
+
}
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
Rotate a pepper by prepending the new one and keeping the old until every key has drifted to the new hash (either via `rehashApiKey` on natural verifies or via a batch job on a maintenance window). Then drop the old pepper.
|
|
155
|
+
|
|
156
|
+
### `mask(key)` / `parseApiKey(key)`
|
|
157
|
+
|
|
158
|
+
```js
|
|
159
|
+
import { mask, parseApiKey } from '@exortek/apikey';
|
|
160
|
+
|
|
161
|
+
mask('sk_live_A2X4KJRK…_QM5K…6QM');
|
|
162
|
+
// → 'sk_live_A2X4KJ…6QM' — log-safe
|
|
163
|
+
|
|
164
|
+
parseApiKey('sk_live_A2X4…_QM5…');
|
|
165
|
+
// → { prefix: 'sk_live', id: 'A2X4…', secret: 'QM5…' } — UNVERIFIED; do not trust
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
## Middleware
|
|
169
|
+
|
|
170
|
+
### Express
|
|
171
|
+
|
|
172
|
+
```js
|
|
173
|
+
import express from 'express';
|
|
174
|
+
import { apiKeyMiddleware } from '@exortek/apikey/middleware/express';
|
|
175
|
+
import { memoryStore } from '@exortek/apikey/stores';
|
|
176
|
+
|
|
177
|
+
const app = express();
|
|
178
|
+
const store = memoryStore();
|
|
179
|
+
|
|
180
|
+
app.use('/v1', apiKeyMiddleware({
|
|
181
|
+
store,
|
|
182
|
+
requiredScopes: ['read'],
|
|
183
|
+
updateLastUsed: true,
|
|
184
|
+
}));
|
|
185
|
+
|
|
186
|
+
app.get('/v1/whoami', (req, res) => res.json(req.apiKey));
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
### Fastify
|
|
190
|
+
|
|
191
|
+
```js
|
|
192
|
+
import Fastify from 'fastify';
|
|
193
|
+
import { apiKeyPlugin } from '@exortek/apikey/middleware/fastify';
|
|
194
|
+
import { memoryStore } from '@exortek/apikey/stores';
|
|
195
|
+
|
|
196
|
+
const app = Fastify();
|
|
197
|
+
await app.register(apiKeyPlugin, {
|
|
198
|
+
store: memoryStore(),
|
|
199
|
+
requiredScopes: ['read'],
|
|
200
|
+
updateLastUsed: true,
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
app.get('/v1/whoami', async req => req.apiKey);
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
### Options common to both
|
|
207
|
+
|
|
208
|
+
| Option | Default | Notes |
|
|
209
|
+
| ------------------- | ------------------- | ----- |
|
|
210
|
+
| `store` | — | Required. |
|
|
211
|
+
| `peppers` | none | Newest-first array. |
|
|
212
|
+
| `requiredScopes` | none | Union with the endpoint's own scope checks. |
|
|
213
|
+
| `expectedPrefix` | any | Reject a valid key whose prefix differs (e.g. lock a route to `sk_live`). |
|
|
214
|
+
| `updateLastUsed` | `false` | One extra store write per verify. |
|
|
215
|
+
| `headerName` | `'authorization'` | Case-insensitive. |
|
|
216
|
+
| `scheme` | `'bearer'` | `'bearer'` → `Bearer <key>`; `'raw'` → header value as-is (useful for `X-API-Key`). |
|
|
217
|
+
| `allowQueryParam` | `false` | Fall back to `?api_key=<key>` — discouraged (query strings leak into logs). |
|
|
218
|
+
| `queryParamName` | `'api_key'` | |
|
|
219
|
+
| `attach` | `'apiKey'` | `req[attach] = { valid, userId, scopes, … }`. |
|
|
220
|
+
| `tokenFromRequest` | none | Override extraction entirely. |
|
|
221
|
+
|
|
222
|
+
Invalid keys respond with 401 `{ error: 'invalid_api_key', reason }`; missing scope responds 403; store failure responds 503.
|
|
223
|
+
|
|
224
|
+
## Stores
|
|
225
|
+
|
|
226
|
+
`@exortek/apikey/stores` ships two implementations. Any object matching the `ApiKeyStore` interface works — bring your own if you already have a keys table in your DB.
|
|
227
|
+
|
|
228
|
+
- **`memoryStore()`** — in-process Map with a reverse index by `userId`. Not cluster-safe. Fine for dev / single-node prod / tests.
|
|
229
|
+
- **`redisStore(client, { keyPrefix? })`** — layout: `<keyPrefix><id>` (JSON blob) + `<keyPrefix>u:<userId>` (SADD-set of ids). Cluster-safe. Works with `ioredis`, `node-redis@4+`, `@upstash/redis`.
|
|
230
|
+
|
|
231
|
+
### The interface
|
|
232
|
+
|
|
233
|
+
```ts
|
|
234
|
+
interface ApiKeyStore {
|
|
235
|
+
put(record: ApiKeyRecord): Promise<void>;
|
|
236
|
+
getById(id: string): Promise<ApiKeyRecord | null>;
|
|
237
|
+
update(id: string, patch: Partial<ApiKeyRecord>): Promise<ApiKeyRecord | null>;
|
|
238
|
+
revoke(id: string, reason?: string): Promise<boolean>;
|
|
239
|
+
revokeAllForUser(userId: string, reason?: string): Promise<number>;
|
|
240
|
+
listByUser(userId: string): Promise<ApiKeyRecord[]>;
|
|
241
|
+
}
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
`ApiKeyRecord` fields: `id`, `hash`, `prefix`, `userId`, `scopes`, `createdAt`, `expiresAt?`, `revokedAt?`, `revokedReason?`, `lastUsedAt?`, `name?`, `environment?`, `metadata?`, `pepperVersion?`.
|
|
245
|
+
|
|
246
|
+
## Errors
|
|
247
|
+
|
|
248
|
+
```js
|
|
249
|
+
import { ApiKeyError, ErrorCode } from '@exortek/apikey';
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
| Code | HTTP | Raised when |
|
|
253
|
+
| ------------------- | ---- | ----------- |
|
|
254
|
+
| `INVALID_ARGUMENT` | 400 | Options object is missing, misshapen, or violates an invariant. |
|
|
255
|
+
| `INVALID_PREFIX` | 400 | Prefix doesn't match the Stripe-style grammar. |
|
|
256
|
+
| `INVALID_PEPPER` | 400 | Pepper is missing / under 16 bytes / wrong type. |
|
|
257
|
+
| `STORE_ERROR` | 500 | `store.put` threw at create time (unusual — usually a DB outage). |
|
|
258
|
+
|
|
259
|
+
Expected verify failures do NOT throw — they surface as `{ valid: false, reason }`. See the failure-reasons table above.
|
|
260
|
+
|
|
261
|
+
## License
|
|
262
|
+
|
|
263
|
+
MIT
|
package/dist/errors.d.ts
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export const ErrorCode: Readonly<{
|
|
2
|
+
INVALID_ARGUMENT: "INVALID_ARGUMENT";
|
|
3
|
+
INVALID_PREFIX: "INVALID_PREFIX";
|
|
4
|
+
INVALID_PEPPER: "INVALID_PEPPER";
|
|
5
|
+
STORE_ERROR: "STORE_ERROR";
|
|
6
|
+
}>;
|
|
7
|
+
/**
|
|
8
|
+
* Every recoverable failure raised by this package. Carries a stable
|
|
9
|
+
* `code` (from {@link ErrorCode}) and a `status` — the HTTP response
|
|
10
|
+
* status a middleware layer would use when translating the error.
|
|
11
|
+
*/
|
|
12
|
+
export class ApiKeyError extends BaseError {
|
|
13
|
+
static statuses: {
|
|
14
|
+
INVALID_ARGUMENT: number;
|
|
15
|
+
INVALID_PREFIX: number;
|
|
16
|
+
INVALID_PEPPER: number;
|
|
17
|
+
STORE_ERROR: number;
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
import { BaseError } from '@exortek/shared/errors';
|