@oneunit/auth 2.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.
Files changed (75) hide show
  1. package/ARCHITECTURE.md +465 -0
  2. package/CHANGELOG.md +214 -0
  3. package/LICENSE +21 -0
  4. package/README.md +647 -0
  5. package/dist/adapters.d.ts +51 -0
  6. package/dist/adapters.d.ts.map +1 -0
  7. package/dist/adapters.js +301 -0
  8. package/dist/adapters.js.map +1 -0
  9. package/dist/auth.d.ts +59 -0
  10. package/dist/auth.d.ts.map +1 -0
  11. package/dist/auth.js +560 -0
  12. package/dist/auth.js.map +1 -0
  13. package/dist/errors.d.ts +39 -0
  14. package/dist/errors.d.ts.map +1 -0
  15. package/dist/errors.js +65 -0
  16. package/dist/errors.js.map +1 -0
  17. package/dist/index.d.ts +11 -0
  18. package/dist/index.d.ts.map +1 -0
  19. package/dist/index.js +10 -0
  20. package/dist/index.js.map +1 -0
  21. package/dist/jwt.d.ts +11 -0
  22. package/dist/jwt.d.ts.map +1 -0
  23. package/dist/jwt.js +125 -0
  24. package/dist/jwt.js.map +1 -0
  25. package/dist/oauth.d.ts +31 -0
  26. package/dist/oauth.d.ts.map +1 -0
  27. package/dist/oauth.js +178 -0
  28. package/dist/oauth.js.map +1 -0
  29. package/dist/password.d.ts +5 -0
  30. package/dist/password.d.ts.map +1 -0
  31. package/dist/password.js +90 -0
  32. package/dist/password.js.map +1 -0
  33. package/dist/providers.d.ts +37 -0
  34. package/dist/providers.d.ts.map +1 -0
  35. package/dist/providers.js +471 -0
  36. package/dist/providers.js.map +1 -0
  37. package/dist/rbac.d.ts +40 -0
  38. package/dist/rbac.d.ts.map +1 -0
  39. package/dist/rbac.js +240 -0
  40. package/dist/rbac.js.map +1 -0
  41. package/dist/roles.d.ts +2 -0
  42. package/dist/roles.d.ts.map +1 -0
  43. package/dist/roles.js +2 -0
  44. package/dist/roles.js.map +1 -0
  45. package/dist/token.d.ts +2 -0
  46. package/dist/token.d.ts.map +1 -0
  47. package/dist/token.js +2 -0
  48. package/dist/token.js.map +1 -0
  49. package/dist/types.d.ts +383 -0
  50. package/dist/types.d.ts.map +1 -0
  51. package/dist/types.js +2 -0
  52. package/dist/types.js.map +1 -0
  53. package/dist/utils.d.ts +35 -0
  54. package/dist/utils.d.ts.map +1 -0
  55. package/dist/utils.js +192 -0
  56. package/dist/utils.js.map +1 -0
  57. package/examples/express.ts +111 -0
  58. package/examples/fastify.ts +59 -0
  59. package/examples/oauth-social.ts +83 -0
  60. package/examples/standalone.ts +67 -0
  61. package/examples/uwebsockets.ts +143 -0
  62. package/package.json +86 -0
  63. package/src/adapters.ts +333 -0
  64. package/src/auth.ts +684 -0
  65. package/src/errors.ts +76 -0
  66. package/src/index.ts +124 -0
  67. package/src/jwt.ts +159 -0
  68. package/src/oauth.ts +226 -0
  69. package/src/password.ts +111 -0
  70. package/src/providers.ts +551 -0
  71. package/src/rbac.ts +285 -0
  72. package/src/roles.ts +1 -0
  73. package/src/token.ts +1 -0
  74. package/src/types.ts +432 -0
  75. package/src/utils.ts +231 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 mayank
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,647 @@
1
+ # @oneunit/auth
2
+
3
+ Framework-agnostic TypeScript authentication for Node.js. Use it from a plain script or plug it into Express, Fastify, Koa, or uWebSockets.js.
4
+
5
+ [![CI](https://github.com/mayank040902/framework/actions/workflows/auth.yml/badge.svg)](https://github.com/mayank040902/framework/actions/workflows/auth.yml)
6
+ [![npm](https://img.shields.io/npm/v/@oneunit/auth.svg)](https://www.npmjs.com/package/@oneunit/auth)
7
+ [![license](https://img.shields.io/npm/l/@oneunit/auth.svg)](./LICENSE)
8
+
9
+ Monorepo: https://github.com/mayank040902/framework
10
+
11
+ - JWT access and refresh tokens via `jsonwebtoken` (HS256 by default)
12
+ - Generic RBAC: you define roles and permissions
13
+ - Password hashing with scrypt
14
+ - Social login: Google, GitHub, Instagram, Facebook, X/Twitter, Discord, Apple, LinkedIn, Microsoft, Reddit, Twitch, Slack, Spotify, TikTok
15
+ - Works standalone or with HTTP frameworks
16
+
17
+ Requires Node.js 20+. Single runtime dependency: `jsonwebtoken`.
18
+
19
+ Docs: [README](./README.md) · [ARCHITECTURE](./ARCHITECTURE.md) · [CHANGELOG](./CHANGELOG.md) · [Security](../../docs/security.md)
20
+
21
+ Upgrading from 1.x? See the [2.0.0 migration notes](./CHANGELOG.md#migration).
22
+
23
+ ## Table of contents
24
+
25
+ - [Install](#install)
26
+ - [Quick start](#quick-start)
27
+ - [JWT helpers](#jwt-helpers)
28
+ - [RBAC](#rbac)
29
+ - [Passwords](#passwords)
30
+ - [Social login](#social-login)
31
+ - [Framework adapters](#framework-adapters)
32
+ - [Refresh tokens](#refresh-tokens)
33
+ - [Utilities](#utilities)
34
+ - [Errors](#errors)
35
+ - [API reference](#api-reference)
36
+ - [Examples](#examples)
37
+ - [Scripts](#scripts)
38
+ - [License](#license)
39
+
40
+ ## Install
41
+
42
+ ```bash
43
+ npm install @oneunit/auth
44
+ ```
45
+
46
+ ## Quick start
47
+
48
+ ```ts
49
+ import { createAuth } from "@oneunit/auth";
50
+
51
+ const auth = createAuth({
52
+ secret: process.env.AUTH_SECRET,
53
+ issuer: "my-app",
54
+ accessTokenTtl: "15m",
55
+ refreshTokenTtl: "7d",
56
+ rbac: {
57
+ defaultRole: "member",
58
+ roles: {
59
+ member: { permissions: ["profile.read"] },
60
+ editor: { inherits: "member", permissions: ["post.write"] },
61
+ admin: { inherits: "editor", permissions: ["user.manage"] },
62
+ },
63
+ },
64
+ });
65
+
66
+ const { accessToken, refreshToken, payload } = await auth.login({
67
+ id: 42,
68
+ email: "ada@example.com",
69
+ roles: ["editor"],
70
+ });
71
+
72
+ const claims = await auth.verify(accessToken);
73
+ auth.can(claims, "post.write");
74
+ ```
75
+
76
+ There are no built-in product roles. Pass whatever role names your app uses.
77
+
78
+ ## JWT helpers
79
+
80
+ Use these without creating an `Auth` instance:
81
+
82
+ ```js
83
+ import { encode, decode, decodeUnsafe } from "@oneunit/auth";
84
+
85
+ const token = encode(
86
+ { userId: 123 },
87
+ process.env.AUTH_SECRET,
88
+ { expiresIn: "1h", subject: "123" },
89
+ );
90
+
91
+ const claims = decode(token, process.env.AUTH_SECRET);
92
+
93
+ // Decode without verification (inspect expired/untrusted tokens):
94
+ const unsafeClaims = decodeUnsafe(token);
95
+ ```
96
+
97
+ `encodeAccessToken` and `encodeRefreshToken` add a `typ` claim (`access` or `refresh`).
98
+
99
+ ## RBAC
100
+
101
+ ```js
102
+ import { createRBAC } from "@oneunit/auth";
103
+
104
+ const rbac = createRBAC({
105
+ roles: {
106
+ support: { permissions: ["ticket.read", "ticket.reply"] },
107
+ lead: { inherits: "support", permissions: ["ticket.assign"] },
108
+ },
109
+ });
110
+
111
+ rbac.grant("lead", "ticket.close");
112
+ rbac.can({ roles: ["lead"] }, "ticket.reply");
113
+ rbac.authorize({ roles: ["support"] }, "ticket.read");
114
+ ```
115
+
116
+ `defineRoles` is a typed helper for declaring role definitions outside the constructor:
117
+
118
+ ```js
119
+ import { defineRoles, createRBAC } from "@oneunit/auth";
120
+
121
+ const roles = defineRoles({
122
+ viewer: { permissions: ["doc.read"] },
123
+ editor: { inherits: "viewer", permissions: ["doc.write"] },
124
+ });
125
+
126
+ const rbac = createRBAC({ roles });
127
+ ```
128
+
129
+ Use `matchPermission` directly to test a single granted permission against a required one:
130
+
131
+ ```js
132
+ import { matchPermission } from "@oneunit/auth";
133
+
134
+ matchPermission("invoice.*", "invoice.read"); // true
135
+ matchPermission("invoice.*", "invoice.read.all"); // false
136
+ matchPermission("invoice.**", "invoice.read.all"); // true
137
+ ```
138
+
139
+ Permission wildcards:
140
+
141
+ | Granted | Matches | Does not match |
142
+ | :--- | :--- | :--- |
143
+ | `*` | anything | — |
144
+ | `invoice.*` | `invoice.read` | `invoice.read.all`, `invoices.read` |
145
+ | `invoice.**` | `invoice.read`, `invoice.read.all` | `payment.read` |
146
+
147
+ `*` stays within one dot-separated segment. `**` spans any number of segments.
148
+
149
+ Direct permissions on a user still work:
150
+
151
+ ```js
152
+ rbac.can({ roles: ["member"], permissions: ["beta.access"] }, "beta.access");
153
+ ```
154
+
155
+ These are evaluated against a subject you construct in code. A `permissions`
156
+ array on a stored user record is not copied into access tokens — see
157
+ [Access token claims](#access-token-claims).
158
+
159
+ > **`defaultRole` is a grant to anonymous callers.** A subject with no roles —
160
+ > including `null` and `undefined` — is assigned `defaultRole`, so
161
+ > `can(null, "post.write")` is `true` if `defaultRole` carries that permission.
162
+ > Keep `defaultRole` unprivileged, and check for a subject before asking:
163
+ >
164
+ > ```js
165
+ > if (ctx.state.user && auth.can(ctx.state.user, "post.write")) { ... }
166
+ > ```
167
+ >
168
+ > The bundled adapters check for a missing user before calling `can`.
169
+
170
+ ## Passwords
171
+
172
+ ```js
173
+ import { hashPassword, verifyPassword, needsRehash } from "@oneunit/auth";
174
+
175
+ const passwordHash = await hashPassword("correct horse battery staple");
176
+ await verifyPassword("correct horse battery staple", passwordHash);
177
+
178
+ // Check if a stored hash needs upgrading (e.g. cost parameter changed):
179
+ if (needsRehash(passwordHash, { cost: 32768 })) {
180
+ const newHash = await hashPassword(password, { cost: 32768 });
181
+ // persist newHash
182
+ }
183
+ ```
184
+
185
+ With a user store, `auth.register()` and `auth.loginWithPassword()` hash and verify for you.
186
+
187
+ ```js
188
+ const auth = createAuth({
189
+ secret: process.env.AUTH_SECRET,
190
+ userStore: {
191
+ async findByCredentials(identifier) {},
192
+ async create(input) {},
193
+ async updatePassword(id, passwordHash) {},
194
+ },
195
+ });
196
+
197
+ await auth.register({ email: "ada@example.com", password: "s3cret-pass" });
198
+ await auth.loginWithPassword("ada@example.com", "s3cret-pass");
199
+ ```
200
+
201
+ `register()` ignores a `roles` field in its first argument so a public sign-up
202
+ form cannot self-assign a role. Pass roles as the second, server-side argument.
203
+
204
+ ## Social login
205
+
206
+ Built-in providers: `google`, `github`, `instagram`, `facebook`, `twitter` (X), `discord`, `apple`, `linkedin`, `microsoft`, `reddit`, `twitch`, `slack`, `spotify`, `tiktok`.
207
+
208
+ ```js
209
+ const auth = createAuth({
210
+ secret: process.env.AUTH_SECRET,
211
+ providers: {
212
+ google: {
213
+ clientId: process.env.GOOGLE_CLIENT_ID,
214
+ clientSecret: process.env.GOOGLE_CLIENT_SECRET,
215
+ redirectUri: "http://localhost:3000/auth/google/callback",
216
+ },
217
+ github: {
218
+ clientId: process.env.GITHUB_CLIENT_ID,
219
+ clientSecret: process.env.GITHUB_CLIENT_SECRET,
220
+ redirectUri: "http://localhost:3000/auth/github/callback",
221
+ },
222
+ instagram: {
223
+ clientId: process.env.INSTAGRAM_CLIENT_ID,
224
+ clientSecret: process.env.INSTAGRAM_CLIENT_SECRET,
225
+ redirectUri: "http://localhost:3000/auth/instagram/callback",
226
+ },
227
+ },
228
+ });
229
+
230
+ const { url, state } = await auth.getAuthorizationUrl("google");
231
+
232
+ const session = await auth.loginWithOAuth("google", {
233
+ code: req.query.code,
234
+ state: req.query.state,
235
+ });
236
+ ```
237
+
238
+ The callback params also accept a string: a full URL, a path with a query, a
239
+ leading `?code=...`, or a bare `"code=...&state=..."`.
240
+
241
+ ### PKCE
242
+
243
+ Public clients should set `pkce: true`. The package generates and stores the
244
+ verifier for you. To manage it yourself, use the exported helpers:
245
+
246
+ ```js
247
+ import { pkceVerifier, pkceChallenge } from "@oneunit/auth";
248
+
249
+ const verifier = pkceVerifier();
250
+ const challenge = pkceChallenge(verifier); // S256, base64url
251
+ ```
252
+
253
+ ### Provider user store
254
+
255
+ Optional user-store methods for linking accounts:
256
+
257
+ - `findByProvider(provider, providerId)`
258
+ - `findByEmail(email)`
259
+ - `createFromProvider(provider, profile, tokens)`
260
+ - `linkProvider(userId, provider, profile)`
261
+
262
+ If no store is configured, login still works and uses a synthetic id such as
263
+ `google:123`. That id is built from the provider profile's `id`, so
264
+ `profileMap` must map a stable identifier — a profile without one throws
265
+ `ProviderError` rather than collapsing every user of that provider onto the
266
+ same subject.
267
+
268
+ ### Custom providers
269
+
270
+ ```js
271
+ import { createProvider, createOAuth } from "@oneunit/auth";
272
+
273
+ const acme = createProvider({
274
+ id: "acme",
275
+ authorizationUrl: "https://acme.example/oauth/authorize",
276
+ tokenUrl: "https://acme.example/oauth/token",
277
+ userInfoUrl: "https://acme.example/me",
278
+ scopes: ["profile"],
279
+ profileMap: { id: "id", email: "email", name: "name" },
280
+ });
281
+
282
+ const oauth = createOAuth();
283
+ oauth.use("acme", { provider: acme, clientId: "...", clientSecret: "..." });
284
+ ```
285
+
286
+ List all built-in provider definitions:
287
+
288
+ ```js
289
+ import { builtinProviders, getProvider } from "@oneunit/auth";
290
+
291
+ console.log(Object.keys(builtinProviders)); // ["google", "github", ...]
292
+ const gh = getProvider("github");
293
+ ```
294
+
295
+ ## Framework adapters
296
+
297
+ The adapters never import a web framework. They read the request and response
298
+ objects you pass them structurally, so `@oneunit/auth` has no peer dependencies
299
+ at all and works whether or not Express, Fastify, Koa, or uWebSockets.js is
300
+ installed. Install your framework as usual alongside this package.
301
+
302
+ This also means the adapters are not tied to a framework's major version: they
303
+ depend on a small shape (`headers`, `cookies`, `query`, a `send`-style reply)
304
+ rather than on a class.
305
+
306
+ ### Express
307
+
308
+ ```js
309
+ import { createAuth, expressAdapter } from "@oneunit/auth";
310
+
311
+ const auth = createAuth({ secret: process.env.AUTH_SECRET });
312
+ const { authenticate, requirePermission, requireRole } = expressAdapter(auth);
313
+
314
+ app.get("/me", authenticate(), (req, res) => res.json(req.user));
315
+ app.get("/admin", authenticate(), requireRole("admin"), handler);
316
+ app.get("/reports", authenticate(), requirePermission("report.read"), handler);
317
+ ```
318
+
319
+ ### Fastify
320
+
321
+ ```js
322
+ import { createAuth, fastifyAdapter } from "@oneunit/auth";
323
+
324
+ await fastify.register(fastifyAdapter(auth));
325
+ fastify.get("/me", { preHandler: [fastify.authenticate()] }, async (req) => req.user);
326
+ ```
327
+
328
+ ### Koa
329
+
330
+ ```js
331
+ import { createAuth, koaAdapter } from "@oneunit/auth";
332
+
333
+ const { authenticate, requirePermission } = koaAdapter(auth);
334
+ app.use(authenticate());
335
+ ```
336
+
337
+ ### uWebSockets.js
338
+
339
+ uWS request/response objects are invalid after the first `await`. The adapter snapshots headers and query first.
340
+
341
+ ```js
342
+ import uWS from "uWebSockets.js";
343
+ import { createAuth, uwsAdapter } from "@oneunit/auth";
344
+
345
+ const auth = createAuth({ secret: process.env.AUTH_SECRET });
346
+ const { authenticate, requirePermission, json } = uwsAdapter(auth);
347
+
348
+ uWS.App()
349
+ .get("/me", authenticate()((res, _req, request) => {
350
+ json(res, 200, { user: request.user });
351
+ }))
352
+ .get("/stats", authenticate()(requirePermission("stats.read")((res, _req, request) => {
353
+ json(res, 200, { ok: true, userId: request.user.userId });
354
+ })))
355
+ .listen(3000, (token) => {
356
+ if (!token) throw new Error("listen failed");
357
+ });
358
+ ```
359
+
360
+ Use `snapshotUwsRequest` directly if you need the raw snapshot outside the adapter:
361
+
362
+ ```js
363
+ import { snapshotUwsRequest } from "@oneunit/auth";
364
+
365
+ const snapshot = snapshotUwsRequest(res, req);
366
+ // snapshot.headers, snapshot.query, snapshot.url are safe to use after await
367
+ ```
368
+
369
+ ### Generic / createAdapters
370
+
371
+ `createAdapters` generates all four adapter sets at once:
372
+
373
+ ```js
374
+ import { createAuth, createAdapters } from "@oneunit/auth";
375
+
376
+ const auth = createAuth({ secret: process.env.AUTH_SECRET });
377
+ const { express, fastify, koa, uws } = createAdapters(auth);
378
+ ```
379
+
380
+ ### Standalone HTTP
381
+
382
+ ```js
383
+ const claims = await auth.verifyRequest(req);
384
+ ```
385
+
386
+ Tokens are read from `Authorization: Bearer`, an `access_token` cookie, or `?access_token=`.
387
+
388
+ You can also extract the bearer token yourself:
389
+
390
+ ```js
391
+ import { extractBearerToken } from "@oneunit/auth";
392
+
393
+ const token = extractBearerToken(req);
394
+ ```
395
+
396
+ ## Refresh tokens
397
+
398
+ ```js
399
+ const session = await auth.login(user);
400
+ const next = await auth.refresh(session.refreshToken);
401
+ await auth.logout(next.refreshToken);
402
+ ```
403
+
404
+ Refresh tokens rotate on every use: `auth.refresh()` invalidates the token it
405
+ consumes. A refresh token is never accepted by `verify()` or `verifyRequest()`,
406
+ so it cannot be replayed as an access credential.
407
+
408
+ ### In-memory refresh store
409
+
410
+ For development and testing, use the built-in memory store:
411
+
412
+ ```js
413
+ import { createAuth, createMemoryRefreshStore } from "@oneunit/auth";
414
+
415
+ const auth = createAuth({
416
+ secret: process.env.AUTH_SECRET,
417
+ refreshStore: createMemoryRefreshStore(),
418
+ });
419
+ ```
420
+
421
+ ### Custom refresh store
422
+
423
+ Persist tokens with a custom `refreshStore`:
424
+
425
+ | Method | Required | Purpose |
426
+ | --- | --- | --- |
427
+ | `save(record)` | yes | Store a newly issued refresh token |
428
+ | `get(id)` | yes | Look up a record |
429
+ | `consume(id)` | recommended | Atomically look up **and** remove a record |
430
+ | `revoke(id)` | required unless `consume` exists | Invalidate a record on logout |
431
+
432
+ Implement `consume` whenever you can. `get` followed by `revoke` is two
433
+ round-trips, so two concurrent requests presenting the same token can both
434
+ pass the validity check and each receive a new session. `consume` must be a
435
+ single atomic operation (`GETDEL` in Redis, a `DELETE ... RETURNING` in SQL).
436
+
437
+ `revoke` and `consume` are not optional in practice: if neither exists,
438
+ `auth.refresh()` and `auth.logout()` throw `ConfigurationError` rather than
439
+ report a logout that never happened. A store implementing only `consume()` is
440
+ enough for both rotation and logout.
441
+
442
+ ### Access token claims
443
+
444
+ Permissions in an access token are derived from RBAC roles. A `permissions`
445
+ array on the user record is ignored unless you opt in:
446
+
447
+ ```js
448
+ const auth = createAuth({ secret, trustUserPermissions: true });
449
+ ```
450
+
451
+ Roles assigned during registration come from the server-side options argument,
452
+ never the request body:
453
+
454
+ ```js
455
+ await auth.register({ email, password }, { roles: ["member"] });
456
+ ```
457
+
458
+ ### Reserved claims
459
+
460
+ `sub`, `userId`, `roles`, `permissions`, `typ`, `iss`, `aud`, `exp`, `iat`,
461
+ `nbf`, and `jti` are derived from the authenticated user and RBAC, and cannot be
462
+ replaced by a claim extractor or by `additionalClaims`. Doing so throws rather
463
+ than silently minting a token for someone else:
464
+
465
+ ```js
466
+ auth.registerExtractor("sub", fn); // throws
467
+ auth.login(user, { additionalClaims: { roles: ["admin"] } }); // throws
468
+ ```
469
+
470
+ Any other name is yours — `name`, `email`, `tier`, `tenantId`, and so on.
471
+
472
+ ## Utilities
473
+
474
+ ```js
475
+ import {
476
+ isValidExpiresIn,
477
+ parseExpiresIn,
478
+ randomToken,
479
+ randomState,
480
+ } from "@oneunit/auth";
481
+
482
+ isValidExpiresIn("15m"); // true
483
+ isValidExpiresIn("1y"); // false — ambiguous unit
484
+
485
+ parseExpiresIn("7d"); // 604800 (seconds)
486
+
487
+ const token = randomToken(); // cryptographically random hex string
488
+ const state = randomState(); // for OAuth state parameter
489
+ ```
490
+
491
+ ### OAuth state store
492
+
493
+ For development, `createMemoryStateStore()` keeps OAuth state in memory:
494
+
495
+ ```js
496
+ import { createMemoryStateStore } from "@oneunit/auth";
497
+ ```
498
+
499
+ ## Errors
500
+
501
+ All errors extend `AuthError` and include `code` and `status`:
502
+
503
+ | Class | Code | Status |
504
+ | --- | --- | --- |
505
+ | `InvalidTokenError` | `INVALID_TOKEN` | 401 |
506
+ | `TokenExpiredError` | `TOKEN_EXPIRED` | 401 |
507
+ | `UnauthorizedError` | `UNAUTHORIZED` | 401 |
508
+ | `ForbiddenError` | `FORBIDDEN` | 403 |
509
+ | `ConfigurationError` | `CONFIGURATION_ERROR` | 500 |
510
+ | `OAuthError` | `OAUTH_ERROR` | 401 |
511
+ | `ProviderError` | `PROVIDER_ERROR` | 502 |
512
+ | `ValidationError` | `VALIDATION_ERROR` | 400 |
513
+
514
+ ## API reference
515
+
516
+ Every named export from `@oneunit/auth`:
517
+
518
+ ### Core
519
+
520
+ | Export | Kind | Description |
521
+ | :--- | :--- | :--- |
522
+ | `createAuth` | function | Create an `Auth` instance with JWT, RBAC, OAuth, and password support |
523
+ | `Auth` | class | The auth instance class |
524
+ | `auth` | function | Alias for `createAuth` |
525
+
526
+ ### JWT
527
+
528
+ | Export | Kind | Description |
529
+ | :--- | :--- | :--- |
530
+ | `encode` | function | Sign a JWT payload |
531
+ | `decode` | function | Verify and decode a JWT |
532
+ | `decodeUnsafe` | function | Decode a JWT without verification |
533
+ | `encodeAccessToken` | function | Sign a JWT with `typ: "access"` |
534
+ | `encodeRefreshToken` | function | Sign a JWT with `typ: "refresh"` |
535
+
536
+ ### RBAC
537
+
538
+ | Export | Kind | Description |
539
+ | :--- | :--- | :--- |
540
+ | `createRBAC` | function | Create an RBAC instance |
541
+ | `RBAC` | class | The RBAC class |
542
+ | `defineRoles` | function | Typed helper for role definitions |
543
+ | `matchPermission` | function | Test a granted permission against a required one |
544
+
545
+ ### Passwords
546
+
547
+ | Export | Kind | Description |
548
+ | :--- | :--- | :--- |
549
+ | `hashPassword` | function | Hash a password with scrypt |
550
+ | `verifyPassword` | function | Verify a password against a hash |
551
+ | `needsRehash` | function | Check if a hash needs upgrading |
552
+
553
+ ### OAuth
554
+
555
+ | Export | Kind | Description |
556
+ | :--- | :--- | :--- |
557
+ | `createOAuth` | function | Create a standalone OAuth manager |
558
+ | `OAuth` | class | The OAuth class |
559
+ | `createProvider` | function | Define a custom OAuth provider |
560
+ | `getProvider` | function | Look up a built-in provider by name |
561
+ | `builtinProviders` | object | Map of all built-in provider definitions |
562
+ | `pkceVerifier` | function | Generate a PKCE code verifier |
563
+ | `pkceChallenge` | function | Compute S256 PKCE challenge |
564
+ | `createMemoryStateStore` | function | In-memory OAuth state store |
565
+
566
+ ### Built-in providers
567
+
568
+ `google`, `github`, `instagram`, `facebook`, `twitter`, `discord`, `apple`, `linkedin`, `microsoft`, `reddit`, `twitch`, `slack`, `spotify`, `tiktok` — each exported as a provider definition object.
569
+
570
+ ### Framework adapters
571
+
572
+ | Export | Kind | Description |
573
+ | :--- | :--- | :--- |
574
+ | `expressAdapter` | function | Express middleware factory |
575
+ | `fastifyAdapter` | function | Fastify plugin factory |
576
+ | `koaAdapter` | function | Koa middleware factory |
577
+ | `uwsAdapter` | function | uWebSockets.js adapter factory |
578
+ | `snapshotUwsRequest` | function | Snapshot a uWS request for use after `await` |
579
+ | `createAdapters` | function | Create all four adapters at once |
580
+
581
+ ### Stores
582
+
583
+ | Export | Kind | Description |
584
+ | :--- | :--- | :--- |
585
+ | `createMemoryRefreshStore` | function | In-memory refresh token store (dev/test) |
586
+
587
+ ### Utilities
588
+
589
+ | Export | Kind | Description |
590
+ | :--- | :--- | :--- |
591
+ | `parseExpiresIn` | function | Parse a TTL string to seconds |
592
+ | `isValidExpiresIn` | function | Validate a TTL string or number |
593
+ | `randomToken` | function | Cryptographically random hex token |
594
+ | `randomState` | function | Random string for OAuth state |
595
+ | `extractBearerToken` | function | Extract a bearer token from a request |
596
+
597
+ ### Errors
598
+
599
+ `AuthError`, `InvalidTokenError`, `TokenExpiredError`, `UnauthorizedError`, `ForbiddenError`, `ConfigurationError`, `OAuthError`, `ProviderError`, `ValidationError`.
600
+
601
+ ### Types
602
+
603
+ `AuthErrorOptions`, `JwtSignOptions`, `JwtVerifyOptions`, `JwtPayload`, `Secret`, `RoleDefinition`, `RBACOptions`, `AuthSubject`, `PasswordOptions`, `OAuthProfile`, `OAuthTokens`, `OAuthProviderConfig`, `OAuthProvider`, `ProviderDefinition`, `StateStore`, `OAuthOptions`, `OAuthAuthorizeOptions`, `UserRecord`, `UserStore`, `RefreshRecord`, `RefreshStore`, `LoginOptions`, `LoginResult`, `AuthOptions`, `RequestLike`, `ExtractTokenOptions`, `UwsHttpResponse`, `UwsHttpRequest`, `UwsRequestSnapshot`, `UwsHandler`, `ExpressRequestLike`, `ExpressResponseLike`, `ExpressNext`, `KoaContextLike`, `FastifyLike`, `FastifyRequestLike`, `FastifyReplyLike`.
604
+
605
+ ## Examples
606
+
607
+ | File | Shows |
608
+ | :--- | :--- |
609
+ | `examples/standalone.ts` | Login, rotation, replay rejection, wildcards, TTL validation |
610
+ | `examples/express.ts` | Middleware, `optional` auth, refresh and logout routes, OAuth |
611
+ | `examples/fastify.ts` | Plugin registration, plugin-level `optional`, preHandlers |
612
+ | `examples/uwebsockets.ts` | Abort-safe handlers, request snapshots, provider lookup |
613
+ | `examples/oauth-social.ts` | Provider config, PKCE, state handling |
614
+
615
+ They import from `@oneunit/auth`, so they run against a real install:
616
+
617
+ ```bash
618
+ npm run example:standalone
619
+ npm run example:oauth
620
+ ```
621
+
622
+ From an installed copy:
623
+
624
+ ```bash
625
+ npx tsx node_modules/@oneunit/auth/examples/standalone.ts
626
+ ```
627
+
628
+ The OAuth example needs `GOOGLE_CLIENT_ID` and `GOOGLE_CLIENT_SECRET`; it exits
629
+ with a note otherwise, because provider config is validated at `authorize()`
630
+ time rather than at `createAuth()` time.
631
+
632
+ ## Scripts
633
+
634
+ ```bash
635
+ npm test
636
+ npm run typecheck
637
+ npm run build
638
+ npm run pack:check
639
+ ```
640
+
641
+ CI runs typecheck, tests, build, `pack:check`, and `pnpm audit` on Node 20, 22,
642
+ and 24, then installs the packed tarball into a clean project and exercises the
643
+ public API and the shipped examples against it.
644
+
645
+ ## License
646
+
647
+ MIT. Copyright (c) 2026 mayank.