rst-auth 0.0.0-stage → 0.2.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 (77) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/CODE_OF_CONDUCT.md +73 -0
  3. package/CONTRIBUTING.md +35 -0
  4. package/LICENSE +21 -0
  5. package/README.md +307 -2
  6. package/SECURITY.md +13 -0
  7. package/dist/accounts.d.ts +41 -0
  8. package/dist/accounts.d.ts.map +1 -0
  9. package/dist/accounts.js +169 -0
  10. package/dist/accounts.js.map +1 -0
  11. package/dist/config.d.ts +63 -0
  12. package/dist/config.d.ts.map +1 -0
  13. package/dist/config.js +196 -0
  14. package/dist/config.js.map +1 -0
  15. package/dist/errors.d.ts +16 -0
  16. package/dist/errors.d.ts.map +1 -0
  17. package/dist/errors.js +57 -0
  18. package/dist/errors.js.map +1 -0
  19. package/dist/express/index.d.ts +74 -0
  20. package/dist/express/index.d.ts.map +1 -0
  21. package/dist/express/index.js +185 -0
  22. package/dist/express/index.js.map +1 -0
  23. package/dist/index.d.ts +24 -0
  24. package/dist/index.d.ts.map +1 -0
  25. package/dist/index.js +27 -0
  26. package/dist/index.js.map +1 -0
  27. package/dist/memory-refresh-token-store.d.ts +15 -0
  28. package/dist/memory-refresh-token-store.d.ts.map +1 -0
  29. package/dist/memory-refresh-token-store.js +75 -0
  30. package/dist/memory-refresh-token-store.js.map +1 -0
  31. package/dist/password.d.ts +7 -0
  32. package/dist/password.d.ts.map +1 -0
  33. package/dist/password.js +36 -0
  34. package/dist/password.js.map +1 -0
  35. package/dist/refresh-store.d.ts +48 -0
  36. package/dist/refresh-store.d.ts.map +1 -0
  37. package/dist/refresh-store.js +2 -0
  38. package/dist/refresh-store.js.map +1 -0
  39. package/dist/refresh.d.ts +27 -0
  40. package/dist/refresh.d.ts.map +1 -0
  41. package/dist/refresh.js +93 -0
  42. package/dist/refresh.js.map +1 -0
  43. package/dist/testing/index.d.ts +3 -0
  44. package/dist/testing/index.d.ts.map +1 -0
  45. package/dist/testing/index.js +2 -0
  46. package/dist/testing/index.js.map +1 -0
  47. package/dist/testing/refresh-store-conformance.d.ts +11 -0
  48. package/dist/testing/refresh-store-conformance.d.ts.map +1 -0
  49. package/dist/testing/refresh-store-conformance.js +110 -0
  50. package/dist/testing/refresh-store-conformance.js.map +1 -0
  51. package/dist/tokens.d.ts +15 -0
  52. package/dist/tokens.d.ts.map +1 -0
  53. package/dist/tokens.js +90 -0
  54. package/dist/tokens.js.map +1 -0
  55. package/dist/validation.d.ts +7 -0
  56. package/dist/validation.d.ts.map +1 -0
  57. package/dist/validation.js +2 -0
  58. package/dist/validation.js.map +1 -0
  59. package/dist/zod/index.d.ts +5 -0
  60. package/dist/zod/index.d.ts.map +1 -0
  61. package/dist/zod/index.js +22 -0
  62. package/dist/zod/index.js.map +1 -0
  63. package/docs/access-tokens.md +15 -0
  64. package/docs/accounts.md +68 -0
  65. package/docs/before-first-release.md +31 -0
  66. package/docs/claims.md +42 -0
  67. package/docs/configuration.md +38 -0
  68. package/docs/cookies-vs-bearer.md +82 -0
  69. package/docs/error-handling.md +15 -0
  70. package/docs/installation.md +39 -0
  71. package/docs/middleware.md +24 -0
  72. package/docs/migration.md +17 -0
  73. package/docs/passwords.md +23 -0
  74. package/docs/refresh-tokens.md +47 -0
  75. package/docs/security.md +16 -0
  76. package/docs/validation.md +24 -0
  77. package/package.json +91 -4
@@ -0,0 +1,11 @@
1
+ import type { RefreshTokenStore } from "../refresh-store.js";
2
+ export interface ConformanceTestRegistrar {
3
+ test(name: string, fn: () => void | Promise<void>): unknown;
4
+ }
5
+ export type RefreshTokenStoreFactory = () => RefreshTokenStore | Promise<RefreshTokenStore>;
6
+ /**
7
+ * Registers a runner-neutral set of contract tests. Pass an adapter for your
8
+ * test runner's `test`/`it` function and a factory that creates an empty store.
9
+ */
10
+ export declare function registerRefreshTokenStoreConformanceTests(registrar: ConformanceTestRegistrar, createStore: RefreshTokenStoreFactory): void;
11
+ //# sourceMappingURL=refresh-store-conformance.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"refresh-store-conformance.d.ts","sourceRoot":"","sources":["../../src/testing/refresh-store-conformance.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAEV,iBAAiB,EAClB,MAAM,qBAAqB,CAAC;AAE7B,MAAM,WAAW,wBAAwB;IACvC,IAAI,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,GAAG,OAAO,CAAC;CAC7D;AAED,MAAM,MAAM,wBAAwB,GAAG,MACrC,iBAAiB,GAAG,OAAO,CAAC,iBAAiB,CAAC,CAAC;AAyBjD;;;GAGG;AACH,wBAAgB,yCAAyC,CACvD,SAAS,EAAE,wBAAwB,EACnC,WAAW,EAAE,wBAAwB,GACpC,IAAI,CAyIN"}
@@ -0,0 +1,110 @@
1
+ const BASE_TIME = new Date("2030-01-01T00:00:00.000Z");
2
+ function record(tokenHash, overrides = {}) {
3
+ return {
4
+ tokenHash,
5
+ familyId: "family-default",
6
+ subject: "subject-default",
7
+ createdAt: BASE_TIME,
8
+ expiresAt: new Date(BASE_TIME.getTime() + 60_000),
9
+ absoluteExpiresAt: new Date(BASE_TIME.getTime() + 120_000),
10
+ status: "active",
11
+ ...overrides,
12
+ };
13
+ }
14
+ function assert(condition, message) {
15
+ if (!condition)
16
+ throw new Error(`RefreshTokenStore conformance failure: ${message}`);
17
+ }
18
+ /**
19
+ * Registers a runner-neutral set of contract tests. Pass an adapter for your
20
+ * test runner's `test`/`it` function and a factory that creates an empty store.
21
+ */
22
+ export function registerRefreshTokenStoreConformanceTests(registrar, createStore) {
23
+ registrar.test("supports consuming a token and storing its rotated successor", async () => {
24
+ const store = await createStore();
25
+ const original = record("a".repeat(64));
26
+ await store.create(original);
27
+ const consumed = await store.consume(original.tokenHash, BASE_TIME);
28
+ assert(consumed.status === "ok", "an active token must be consumed");
29
+ const successor = record("b".repeat(64), {
30
+ familyId: original.familyId,
31
+ subject: original.subject,
32
+ });
33
+ await store.create(successor);
34
+ const next = await store.consume(successor.tokenHash, BASE_TIME);
35
+ assert(next.status === "ok", "the successor in the same family must be consumable");
36
+ });
37
+ registrar.test("detects replay of a consumed token", async () => {
38
+ const store = await createStore();
39
+ const token = record("c".repeat(64));
40
+ await store.create(token);
41
+ assert((await store.consume(token.tokenHash, BASE_TIME)).status === "ok", "first consume must win");
42
+ assert((await store.consume(token.tokenHash, BASE_TIME)).status === "reused", "a second consume must report reuse");
43
+ });
44
+ registrar.test("revokes the entire family after reuse", async () => {
45
+ const store = await createStore();
46
+ const original = record("d".repeat(64), { familyId: "family-reuse" });
47
+ const sibling = record("e".repeat(64), { familyId: "family-reuse" });
48
+ await store.create(original);
49
+ await store.create(sibling);
50
+ await store.consume(original.tokenHash, BASE_TIME);
51
+ assert((await store.consume(original.tokenHash, BASE_TIME)).status === "reused", "replay must be detected");
52
+ assert((await store.consume(sibling.tokenHash, BASE_TIME)).status === "revoked", "all tokens in the family must be revoked");
53
+ let refusedSuccessor = false;
54
+ try {
55
+ await store.create(record("6".repeat(64), { familyId: "family-reuse" }));
56
+ }
57
+ catch {
58
+ refusedSuccessor = true;
59
+ }
60
+ assert(refusedSuccessor, "a revoked family must reject a late successor create");
61
+ });
62
+ registrar.test("rejects expired tokens", async () => {
63
+ const store = await createStore();
64
+ const token = record("f".repeat(64), {
65
+ expiresAt: BASE_TIME,
66
+ absoluteExpiresAt: new Date(BASE_TIME.getTime() + 60_000),
67
+ });
68
+ await store.create(token);
69
+ assert((await store.consume(token.tokenHash, BASE_TIME)).status === "expired", "expiry is exclusive at expiresAt");
70
+ });
71
+ registrar.test("supports single-token, family, and subject revocation", async () => {
72
+ const store = await createStore();
73
+ const single = record("1".repeat(64), { familyId: "family-single" });
74
+ const familyMember = record("2".repeat(64), { familyId: "family-all" });
75
+ const otherFamily = record("3".repeat(64), {
76
+ familyId: "family-other",
77
+ subject: "subject-all",
78
+ });
79
+ for (const item of [single, familyMember, otherFamily])
80
+ await store.create(item);
81
+ await store.revokeToken(single.tokenHash, BASE_TIME);
82
+ assert((await store.consume(single.tokenHash, BASE_TIME)).status === "revoked", "single-token revoke must block that token");
83
+ await store.revokeFamily("family-all", BASE_TIME);
84
+ assert((await store.consume(familyMember.tokenHash, BASE_TIME)).status ===
85
+ "revoked", "family revoke must block family members");
86
+ let rejectedLateFamilyMember = false;
87
+ try {
88
+ await store.create(record("5".repeat(64), { familyId: "family-all" }));
89
+ }
90
+ catch {
91
+ rejectedLateFamilyMember = true;
92
+ }
93
+ assert(rejectedLateFamilyMember, "create must not recreate a family after revocation");
94
+ await store.revokeAllForSubject("subject-all", BASE_TIME);
95
+ assert((await store.consume(otherFamily.tokenHash, BASE_TIME)).status ===
96
+ "revoked", "subject revoke must block all of that subject's tokens");
97
+ });
98
+ registrar.test("allows only one concurrent consume to win", async () => {
99
+ const store = await createStore();
100
+ const token = record("4".repeat(64));
101
+ await store.create(token);
102
+ const results = await Promise.all([
103
+ store.consume(token.tokenHash, BASE_TIME),
104
+ store.consume(token.tokenHash, BASE_TIME),
105
+ ]);
106
+ assert(results.filter((result) => result.status === "ok").length === 1, "exactly one concurrent caller must receive ok");
107
+ assert(results.some((result) => result.status === "reused"), "the losing concurrent caller must be recognized as reuse");
108
+ });
109
+ }
110
+ //# sourceMappingURL=refresh-store-conformance.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"refresh-store-conformance.js","sourceRoot":"","sources":["../../src/testing/refresh-store-conformance.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,15 @@
1
+ import { type JWTPayload } from "jose";
2
+ import type { ValidatedAuthConfig } from "./config.js";
3
+ export type AccessTokenClaims = JWTPayload & {
4
+ sub: string;
5
+ };
6
+ export type VerifiedAccessClaims = JWTPayload & {
7
+ sub: string;
8
+ token_use: "access";
9
+ };
10
+ export interface AccessTokenService {
11
+ generateAccessToken(claims: AccessTokenClaims): Promise<string>;
12
+ verifyAccessToken(token: string, context?: unknown): Promise<VerifiedAccessClaims>;
13
+ }
14
+ export declare function createAccessTokenService(config: ValidatedAuthConfig["accessToken"]): AccessTokenService;
15
+ //# sourceMappingURL=tokens.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tokens.d.ts","sourceRoot":"","sources":["../src/tokens.ts"],"names":[],"mappings":"AAAA,OAAO,EAAsB,KAAK,UAAU,EAAE,MAAM,MAAM,CAAC;AAC3D,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,aAAa,CAAC;AAIvD,MAAM,MAAM,iBAAiB,GAAG,UAAU,GAAG;IAAE,GAAG,EAAE,MAAM,CAAA;CAAE,CAAC;AAC7D,MAAM,MAAM,oBAAoB,GAAG,UAAU,GAAG;IAC9C,GAAG,EAAE,MAAM,CAAC;IACZ,SAAS,EAAE,QAAQ,CAAC;CACrB,CAAC;AAEF,MAAM,WAAW,kBAAkB;IACjC,mBAAmB,CAAC,MAAM,EAAE,iBAAiB,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;IAChE,iBAAiB,CACf,KAAK,EAAE,MAAM,EACb,OAAO,CAAC,EAAE,OAAO,GAChB,OAAO,CAAC,oBAAoB,CAAC,CAAC;CAClC;AAED,wBAAgB,wBAAwB,CACtC,MAAM,EAAE,mBAAmB,CAAC,aAAa,CAAC,GACzC,kBAAkB,CA+GpB"}
package/dist/tokens.js ADDED
@@ -0,0 +1,90 @@
1
+ import { SignJWT, jwtVerify } from "jose";
2
+ import { isReservedClaim } from "./config.js";
3
+ import { AuthError } from "./errors.js";
4
+ export function createAccessTokenService(config) {
5
+ return {
6
+ async generateAccessToken(claims) {
7
+ if (!claims ||
8
+ typeof claims !== "object" ||
9
+ typeof claims.sub !== "string" ||
10
+ !claims.sub) {
11
+ throw new AuthError("VALIDATION_ERROR", "Access-token claims require a non-empty sub");
12
+ }
13
+ for (const claim of Object.keys(claims)) {
14
+ if (isReservedClaim(claim)) {
15
+ throw new AuthError("VALIDATION_ERROR", `Caller-supplied claim ${claim} is reserved by the access-token implementation`);
16
+ }
17
+ }
18
+ let jwt = new SignJWT({ ...claims, token_use: "access" })
19
+ .setProtectedHeader({
20
+ alg: config.algorithm,
21
+ typ: "JWT",
22
+ ...(config.keyId === undefined ? {} : { kid: config.keyId }),
23
+ })
24
+ .setIssuedAt()
25
+ .setExpirationTime(`${config.expiresInSeconds}s`);
26
+ if (config.issuer)
27
+ jwt = jwt.setIssuer(config.issuer);
28
+ if (config.audience) {
29
+ jwt = jwt.setAudience(typeof config.audience === "string"
30
+ ? config.audience
31
+ : [...config.audience]);
32
+ }
33
+ return jwt.sign(config.key);
34
+ },
35
+ async verifyAccessToken(token, context) {
36
+ if (typeof token !== "string" || token.length === 0) {
37
+ throw new AuthError("TOKEN_MISSING", "Access token is required");
38
+ }
39
+ try {
40
+ const verified = await jwtVerify(token, async (header) => {
41
+ if (config.keyResolver) {
42
+ if (typeof header.kid !== "string" || header.kid.length === 0) {
43
+ throw new AuthError("TOKEN_INVALID", "Access token key identifier is missing");
44
+ }
45
+ return config.keyResolver(header);
46
+ }
47
+ return config.key;
48
+ }, {
49
+ algorithms: [...config.algorithms],
50
+ ...(config.issuer ? { issuer: config.issuer } : {}),
51
+ ...(config.audience
52
+ ? {
53
+ audience: typeof config.audience === "string"
54
+ ? config.audience
55
+ : [...config.audience],
56
+ }
57
+ : {}),
58
+ requiredClaims: [...config.requiredClaims],
59
+ });
60
+ const claims = verified.payload;
61
+ if (claims.token_use !== "access") {
62
+ throw new AuthError("TOKEN_INVALID", "Token is not an access token");
63
+ }
64
+ if (config.isRevoked && (await config.isRevoked(claims, context))) {
65
+ throw new AuthError("SESSION_REVOKED", "Access token has been revoked");
66
+ }
67
+ if (typeof claims.sub !== "string") {
68
+ throw new AuthError("TOKEN_INVALID", "Access token has no valid subject");
69
+ }
70
+ return claims;
71
+ }
72
+ catch (error) {
73
+ if (error instanceof AuthError)
74
+ throw error;
75
+ if (typeof error === "object" &&
76
+ error !== null &&
77
+ "code" in error &&
78
+ error.code === "ERR_JWT_EXPIRED") {
79
+ throw new AuthError("TOKEN_EXPIRED", "Access token has expired", {
80
+ cause: error,
81
+ });
82
+ }
83
+ throw new AuthError("TOKEN_INVALID", "Access token is invalid", {
84
+ cause: error,
85
+ });
86
+ }
87
+ },
88
+ };
89
+ }
90
+ //# sourceMappingURL=tokens.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tokens.js","sourceRoot":"","sources":["../src/tokens.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,7 @@
1
+ /** A framework-independent schema validator used by optional adapters. */
2
+ export interface ValidatorAdapter {
3
+ parse<TSchema>(schema: TSchema, value: unknown): unknown | Promise<unknown>;
4
+ }
5
+ export type ValidationTarget = "body" | "params" | "query";
6
+ export type ValidationSchemaMap<TSchema = unknown> = Partial<Record<ValidationTarget, TSchema>>;
7
+ //# sourceMappingURL=validation.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"validation.d.ts","sourceRoot":"","sources":["../src/validation.ts"],"names":[],"mappings":"AAAA,0EAA0E;AAC1E,MAAM,WAAW,gBAAgB;IAC/B,KAAK,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,KAAK,EAAE,OAAO,GAAG,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;CAC7E;AAED,MAAM,MAAM,gBAAgB,GAAG,MAAM,GAAG,QAAQ,GAAG,OAAO,CAAC;AAE3D,MAAM,MAAM,mBAAmB,CAAC,OAAO,GAAG,OAAO,IAAI,OAAO,CAC1D,MAAM,CAAC,gBAAgB,EAAE,OAAO,CAAC,CAClC,CAAC"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=validation.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"validation.js","sourceRoot":"","sources":["../src/validation.ts"],"names":[],"mappings":""}
@@ -0,0 +1,5 @@
1
+ import type { ValidatorAdapter } from "../validation.js";
2
+ export type ZodAdapter = ValidatorAdapter;
3
+ /** Adapts Zod 3 or 4 schemas to the framework-independent validator interface. */
4
+ export declare function zodAdapter(): ZodAdapter;
5
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/zod/index.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,kBAAkB,CAAC;AAEzD,MAAM,MAAM,UAAU,GAAG,gBAAgB,CAAC;AAE1C,kFAAkF;AAClF,wBAAgB,UAAU,IAAI,UAAU,CAoBvC"}
@@ -0,0 +1,22 @@
1
+ import { AuthError } from "../errors.js";
2
+ /** Adapts Zod 3 or 4 schemas to the framework-independent validator interface. */
3
+ export function zodAdapter() {
4
+ return {
5
+ async parse(schema, value) {
6
+ if (!schema ||
7
+ typeof schema !== "object" ||
8
+ !("safeParseAsync" in schema)) {
9
+ throw new TypeError("The Zod adapter requires a Zod schema");
10
+ }
11
+ const zodSchema = schema;
12
+ const result = await zodSchema.safeParseAsync(value);
13
+ if (!result.success) {
14
+ throw new AuthError("VALIDATION_ERROR", "Request validation failed", {
15
+ cause: result.error,
16
+ });
17
+ }
18
+ return result.data;
19
+ },
20
+ };
21
+ }
22
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/zod/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AAKzC,kFAAkF;AAClF,MAAM,UAAU,UAAU;IACxB,OAAO;QACL,KAAK,CAAC,KAAK,CAAC,MAAe,EAAE,KAAc;YACzC,IACE,CAAC,MAAM;gBACP,OAAO,MAAM,KAAK,QAAQ;gBAC1B,CAAC,CAAC,gBAAgB,IAAI,MAAM,CAAC,EAC7B,CAAC;gBACD,MAAM,IAAI,SAAS,CAAC,uCAAuC,CAAC,CAAC;YAC/D,CAAC;YACD,MAAM,SAAS,GAAG,MAAiB,CAAC;YACpC,MAAM,MAAM,GAAG,MAAM,SAAS,CAAC,cAAc,CAAC,KAAK,CAAC,CAAC;YACrD,IAAI,CAAC,MAAM,CAAC,OAAO,EAAE,CAAC;gBACpB,MAAM,IAAI,SAAS,CAAC,kBAAkB,EAAE,2BAA2B,EAAE;oBACnE,KAAK,EAAE,MAAM,CAAC,KAAK;iBACpB,CAAC,CAAC;YACL,CAAC;YACD,OAAO,MAAM,CAAC,IAAI,CAAC;QACrB,CAAC;KACF,CAAC;AACJ,CAAC"}
@@ -0,0 +1,15 @@
1
+ # Access tokens
2
+
3
+ Access tokens are signed with `jose`. The configured algorithm and allowlist are used; the verifier does not choose an algorithm from the incoming JWT header. Tokens carry `token_use: "access"`. Verification checks signatures, expiry, issued-at time, configured issuer/audience, required claims, subject, and token type.
4
+
5
+ ```ts
6
+ const token = await auth.tokens.generateAccessToken({
7
+ sub: user.id,
8
+ role: user.role,
9
+ });
10
+ const claims = await auth.tokens.verifyAccessToken(token);
11
+ ```
12
+
13
+ Reserved claims belong to the library and cannot be supplied as caller payload. Static keys are supported; an optional async `keyResolver(header)` can resolve keys by `kid` during rotation. It does not change the configured algorithm allowlist.
14
+
15
+ The optional `isRevoked(claims, context?)` hook lets applications consult a denylist or session state. It makes verification stateful and adds a dependency to each verification. Without such a lookup, an otherwise valid token does not prove that the user still exists or is active; roles embedded in a token can be stale. Keep token lifetimes and current-permission checks appropriate for the application. Never log full tokens.
@@ -0,0 +1,68 @@
1
+ # Register and login helpers
2
+
3
+ `createAuth()` can expose `auth.accounts.register()` and `auth.accounts.login()` when you configure `accounts` and a refresh-token store. This is an optional shortcut for the repeated password and token work. Your app still owns the database, model schema, email query, routes, authorization policy, and HTTP response.
4
+
5
+ ## Configure your model and lookup
6
+
7
+ The model must have a `create(values)` method. Supply the lookup used by your database library. For example, with Mongoose:
8
+
9
+ ```ts
10
+ const auth = createAuth({
11
+ accessToken: {
12
+ key: new TextEncoder().encode(config.accessTokenSecret),
13
+ expiresInSeconds: config.accessTokenTtlSeconds,
14
+ issuer: "my-api",
15
+ audience: "my-client",
16
+ },
17
+ refreshToken: {
18
+ store: mongoRefreshTokenStore,
19
+ refreshTokenTtl: config.refreshTokenTtlSeconds,
20
+ absoluteLifetime: 30 * 24 * 60 * 60,
21
+ },
22
+ accounts: {
23
+ model: User,
24
+ findByEmail: (email) => User.findOne({ email }),
25
+ getSubject: (user) => String(user._id),
26
+ getAccessClaims: (user) => ({ role: user.role }),
27
+ },
28
+ });
29
+ ```
30
+
31
+ The app reads environment variables in its own config layer and passes the values above. `rst-auth` never reads `process.env`. `passwordHash` is the default persisted hash field; set `passwordHashField` if your schema uses another name.
32
+
33
+ ## Use it from existing controllers
34
+
35
+ ```ts
36
+ const session = await auth.accounts.register({
37
+ email: req.body.email,
38
+ password: req.body.password,
39
+ profile: { name: req.body.name },
40
+ });
41
+ res.status(201).json({
42
+ user: session.user,
43
+ accessToken: session.accessToken,
44
+ });
45
+ // Send session.refreshToken using an HttpOnly cookie or another deliberate channel.
46
+ ```
47
+
48
+ ```ts
49
+ const session = await auth.accounts.login({
50
+ email: req.body.email,
51
+ password: req.body.password,
52
+ });
53
+ res.json({ user: session.user, accessToken: session.accessToken });
54
+ ```
55
+
56
+ The helper rejects client-provided `role`, `roles`, `permissions`, `scopes`, `admin`, and `isAdmin` fields in `profile`. Assign privileges in trusted application code. The returned user omits the password hash and common legacy raw refresh-token fields. Select the exact response fields your app intends to expose.
57
+
58
+ For unknown users, login performs a dummy bcrypt comparison and returns the same `UNAUTHORIZED` error as a wrong password. A successful login revokes prior refresh families for that subject by default; set `revokePreviousSessionsOnLogin: false` when concurrent sessions are desired.
59
+
60
+ ## What the app still provides
61
+
62
+ - A model with a unique email constraint and a `create()` method.
63
+ - `findByEmail(email)` that returns the user record or `null`.
64
+ - A stable subject mapping, and any trusted claims needed by authorization.
65
+ - A persistent, atomic `RefreshTokenStore` for multi-process deployments. `MemoryRefreshTokenStore` is only for tests and local development.
66
+ - Request validation and response shaping, plus rate limits, lockout, email verification, password reset, and CSRF policy.
67
+
68
+ The helper is not a database adapter and does not make a Mongoose dependency part of the package. See [refresh tokens](refresh-tokens.md) for store requirements and [security](security.md) for application responsibilities.
@@ -0,0 +1,31 @@
1
+ # Before first release
2
+
3
+ - [ ] Verify the npm package name is available and intended; `package.json` now uses the selected name `rst-auth`.
4
+ - [ ] Verify the GitHub organization and repository name, default branch, and repository URLs in project metadata.
5
+ - [ ] Complete a trademark/name review for the package and project name.
6
+ - [ ] Verify the copyright attribution and year in `LICENSE` before each release.
7
+ - [ ] Review the documented security limitations, refresh-store implementation, operational controls, and vulnerability-reporting route.
8
+ - [ ] Run the final checks from the repository root and review the tarball contents for accidental files or credentials.
9
+
10
+ Manual commands to run only after those checks and maintainer approval:
11
+
12
+ ```sh
13
+ npm whoami
14
+ npm view rst-auth name
15
+ git remote -v
16
+ npm ci
17
+ npm run format:check
18
+ npm run lint
19
+ npm run typecheck
20
+ npm test
21
+ npm run build
22
+ npm pack --dry-run
23
+ npm run example:install
24
+ npm --prefix examples/express-api run typecheck
25
+ npm --prefix examples/express-api test
26
+ npm --prefix examples/express-api run import-check
27
+ npm login
28
+ npm publish --access public
29
+ ```
30
+
31
+ The final two commands require a deliberate maintainer release action. Do not run them as part of routine validation.
package/docs/claims.md ADDED
@@ -0,0 +1,42 @@
1
+ # README claim-to-test map
2
+
3
+ | README claim | Test |
4
+ | ------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------- |
5
+ | Password hashing and verification work, including wrong-password rejection | `test/password.test.ts` — “hashes and verifies passwords” |
6
+ | Password input is non-empty and limited to 72 UTF-8 bytes | `test/password.test.ts` — “rejects empty and over-72-byte passwords by UTF-8 byte length” |
7
+ | Malformed hashes fail safely and dummy verification is available | `test/password.test.ts` — “fails safely for malformed hashes and supports dummy verification” |
8
+ | Errors expose stable codes, safe public messages, status mappings, custom public messages, and causes | `test/errors.test.ts` |
9
+ | Config defaults, secret length, expiry, algorithm, work factor, and production requirements are validated | `test/config.test.ts` |
10
+ | Access-token signatures and configured issuer, audience, and required claims are verified | `test/tokens.test.ts` — “generates and verifies signed tokens with configured claims” |
11
+ | Algorithms are pinned and `none` or a header-selected alternative is rejected | `test/tokens.test.ts` — “pins the allowed algorithm and rejects none or header-selected alternatives” |
12
+ | Caller input cannot override reserved claims | `test/tokens.test.ts` — “rejects caller overrides of reserved claims” |
13
+ | Verification requires `exp` and `iat` | `test/tokens.test.ts` — “requires exp and iat on verified tokens” |
14
+ | A refresh-typed token is rejected as an access token | `test/tokens.test.ts` — “rejects token-type confusion” |
15
+ | Token expiration is enforced | `test/tokens.test.ts` — “uses fake time to enforce expiry” |
16
+ | Verification supports async `kid` key resolution and a revocation callback | `test/tokens.test.ts` — “resolves verification keys asynchronously by kid and supports revocation hooks” |
17
+ | Asymmetric signing and verification keys work with the configured algorithm | `test/tokens.test.ts` — “supports asymmetric signing and verification keys” |
18
+ | Store-less configuration cannot enable stateless re-issuance as rotation, and partial config is rejected | `test/refresh.test.ts` — “refresh-stateless-not-rotation: refuses refresh without a configured store” |
19
+ | Issued refresh tokens are opaque random values and only their SHA-256 hashes reach the store | `test/refresh.test.ts` — “issues opaque 256-bit tokens and stores only their SHA-256 hashes” |
20
+ | The memory store is instance-local and cannot serve another store instance | `test/refresh.test.ts` — “documents the memory store's instance-local behavior” |
21
+ | Rotation preserves the family, detects replay, and revokes family descendants | `test/refresh.test.ts` — “rotates into the same family and detects replay by revoking that family” |
22
+ | Concurrent replay cannot return a successor after family revocation | `test/refresh.test.ts` — “does not return a rotation result when a concurrent replay revokes the family” |
23
+ | Unknown, expired, and revoked refresh tokens cannot rotate | `test/refresh.test.ts` — “never rotates unknown, expired, or revoked values” |
24
+ | Rotation is capped at the family's absolute lifetime | `test/refresh.test.ts` — “caps every rotation at the family's absolute lifetime” |
25
+ | Rotation renews idle expiry without extending the family absolute lifetime | `test/refresh.test.ts` — “renews idle expiry on rotation without changing the family cap” |
26
+ | Refresh and access tokens cannot be confused | `test/refresh.test.ts` — “rejects an access token as refresh and a refresh token as access” |
27
+ | Refresh store rotation, replay, reuse revocation, expiry, revocation, and concurrent consume conform | `test/refresh-store-conformance.test.ts` — registered conformance cases |
28
+ | Express 4 and 5 return safe 401s for absent, malformed, invalid, and expired access tokens | `test/express.integration.test.ts` — “distinguishes missing and malformed bearer headers and rejects invalid or expired tokens” |
29
+ | Optional auth accepts absence but rejects present invalid tokens; valid identities and role checks work | `test/express.integration.test.ts` — “authenticates, supports optional auth, and enforces role access” |
30
+ | Express passes the request to the revocation hook and supports application identity lookup | `test/express.integration.test.ts` — “passes the request to isRevoked and allows identity lookup” |
31
+ | Optional account helpers hash on signup, verify on login, equalize missing-user work, omit hashes from returned user data, and reject client privilege fields | `test/accounts.test.ts` — high-level account helper cases |
32
+ | Account helpers reject duplicate accounts and require refresh storage | `test/accounts.test.ts` — duplicate-account and missing-store cases |
33
+ | Zod validates body, params, and query into `req.validated` without mutating `req.body` | `test/express.integration.test.ts` — “validates body, params, and query without mutating Express request fields” |
34
+ | Validation and unexpected errors support custom formatting without exposing private details | `test/express.integration.test.ts` — “supports a custom schema formatter and keeps unexpected errors private” |
35
+ | Cookie and composed token extractors work; refresh cookies default to HttpOnly, Secure, and SameSite=Lax | `test/express.integration.test.ts` — “supports cookie and composed extractors, and sets secure refresh-cookie defaults” |
36
+ | Query-string token extraction is not enabled by default, while explicit custom extraction remains possible | `test/express.integration.test.ts` — “allows a custom token extractor while preserving strict header parsing by default” |
37
+ | The package is ESM, declares Node 22+, and maps every supported subpath to built files | `test/package.test.ts` — “declares ESM, Node 22+, and built targets for every documented subpath” |
38
+ | The example assigns roles server-side and rejects a client-supplied signup role | `examples/express-api/test/app.test.ts` — “no-role-from-client: rejects a client role and creates no account” |
39
+ | The example demonstrates app-owned persistence/controllers, password hashing, token routes, and role checks | `examples/express-api/test/app.test.ts` — “uses application-owned controllers, hashing, and repository roles” |
40
+ | Cookie delivery sets safe cookie flags, and example error responses avoid credential leakage | `examples/express-api/test/app.test.ts` — cookie defaults and “maps login failures without returning password hashes or secrets” |
41
+ | Core configuration does not read environment variables | `test/package.test.ts` — “keeps environment reads out of the injected core configuration layer” |
42
+ | The bcrypt work factor is configurable and defaults to 12 | `test/password.test.ts` — “uses the configured bcrypt work factor and defaults to 12” |
@@ -0,0 +1,38 @@
1
+ # Configuration
2
+
3
+ `createAuth(config)` validates config once and throws `AuthError` with code `INVALID_CONFIGURATION` for invalid settings. Configuration is injected; do not pass secrets per operation or derive algorithms from untrusted token headers.
4
+
5
+ ```ts
6
+ const auth = createAuth({
7
+ mode: "production",
8
+ password: { workFactor: 12 },
9
+ accessToken: {
10
+ key: hmacKey,
11
+ algorithm: "HS256",
12
+ algorithms: ["HS256"],
13
+ issuer: "billing-api",
14
+ audience: ["billing-web", "billing-worker"],
15
+ expiresInSeconds: 900,
16
+ requiredClaims: ["tenant"],
17
+ },
18
+ refreshToken: {
19
+ store: refreshTokenStore,
20
+ refreshTokenTtl: 60 * 60 * 24 * 7,
21
+ absoluteLifetime: 60 * 60 * 24 * 30,
22
+ },
23
+ accounts: {
24
+ model: User,
25
+ findByEmail: (email) => User.findOne({ email }),
26
+ getSubject: (user) => String(user._id),
27
+ getAccessClaims: (user) => ({ role: user.role }),
28
+ },
29
+ });
30
+ ```
31
+
32
+ HMAC keys are `Uint8Array` values of at least 32 bytes. Asymmetric algorithms require a JOSE `CryptoKey` or Node `KeyObject`. `algorithm` chooses signing; `algorithms` is the pinned verification allowlist and must include the signing algorithm. An optional `keyResolver(header)` supports key lookup by `kid` while the allowlist remains configured. Production mode requires issuer and audience and limits access expiry to one hour.
33
+
34
+ The default bcrypt work factor is 12 and the default access-token expiry is 900 seconds. Work factor must be an integer from 4 to 31. Refresh support is disabled when `refreshToken` is omitted; enabling it requires a store, idle TTL, and absolute family lifetime, each a positive safe integer in seconds.
35
+
36
+ `accounts` is optional. When provided, it enables `auth.accounts.register()` and `auth.accounts.login()` and requires refresh support. Pass your own model with `create(values)` and your own `findByEmail(email)` lookup. Optional `getSubject` and `getAccessClaims` callbacks map your user record into token identity and trusted claims. The library does not select or import a database package. See [account helpers](accounts.md).
37
+
38
+ `isRevoked(claims, context?)` can check application state during verification. Express passes the current request as `context`; direct calls can pass a context themselves. The hook makes verification stateful and adds its latency and availability to protected requests.
@@ -0,0 +1,82 @@
1
+ # Refresh-token delivery: cookies and explicit requests
2
+
3
+ Refresh tokens are opaque bearer secrets. Keep them out of logs and URLs. Choose a delivery mode based on the client and threat model; the toolkit does not decide the response shape for your application.
4
+
5
+ ## HttpOnly cookie
6
+
7
+ The Express helper defaults to HttpOnly, Secure, SameSite=Lax and path `/auth/refresh`:
8
+
9
+ ```ts
10
+ import { setRefreshTokenCookie, fromCookie } from "rst-auth/express";
11
+
12
+ app.post("/login", async (req, res, next) => {
13
+ try {
14
+ const user = await users.findByEmail(req.body.email);
15
+ const refresh = await auth.refresh!.issue({ subject: user.id });
16
+ const accessToken = await auth.tokens.generateAccessToken({
17
+ sub: user.id,
18
+ role: user.role,
19
+ });
20
+ setRefreshTokenCookie(res, refresh.refreshToken);
21
+ res.json({ accessToken });
22
+ } catch (error) {
23
+ next(error);
24
+ }
25
+ });
26
+
27
+ app.post("/auth/refresh", async (req, res, next) => {
28
+ try {
29
+ const oldToken = await fromCookie("refreshToken")(req);
30
+ const rotated = await auth.refresh!.rotate(oldToken ?? "");
31
+ const user = await users.findById(rotated.subject);
32
+ const accessToken = await auth.tokens.generateAccessToken({
33
+ sub: user.id,
34
+ role: user.role,
35
+ });
36
+ setRefreshTokenCookie(res, rotated.refreshToken);
37
+ res.json({ accessToken });
38
+ } catch (error) {
39
+ next(error);
40
+ }
41
+ });
42
+ ```
43
+
44
+ The helper writes and clears cookies; it does not parse them. Cookie delivery lets the browser attach the refresh secret automatically, which creates CSRF risk. SameSite=Lax helps with common cross-site requests but is not a universal CSRF defense. `SameSite=None` requires Secure and an explicit CSRF defense. Restrict cookie path and domain to the smallest useful scope.
45
+
46
+ ## Authorization header or request body
47
+
48
+ For clients that can manage the token explicitly, your controller can return the refresh token in a response and require it in a request body or dedicated header:
49
+
50
+ ```ts
51
+ app.post("/login", async (req, res, next) => {
52
+ try {
53
+ const user = await users.findByEmail(req.body.email);
54
+ const refresh = await auth.refresh!.issue({ subject: user.id });
55
+ const accessToken = await auth.tokens.generateAccessToken({
56
+ sub: user.id,
57
+ role: user.role,
58
+ });
59
+ res.json({ accessToken, refreshToken: refresh.refreshToken });
60
+ } catch (error) {
61
+ next(error);
62
+ }
63
+ });
64
+
65
+ app.post("/auth/refresh/body", async (req, res, next) => {
66
+ try {
67
+ const rotated = await auth.refresh!.rotate(req.body.refreshToken);
68
+ const user = await users.findById(rotated.subject);
69
+ const accessToken = await auth.tokens.generateAccessToken({
70
+ sub: user.id,
71
+ role: user.role,
72
+ });
73
+ res.json({ accessToken, refreshToken: rotated.refreshToken });
74
+ } catch (error) {
75
+ next(error);
76
+ }
77
+ });
78
+ ```
79
+
80
+ Explicit delivery avoids the browser automatically attaching the secret cross-site, reducing classic CSRF exposure. JavaScript-readable storage can increase exposure to cross-site scripting. Never put tokens in query strings: URLs are commonly copied, logged, cached, and included in referrers.
81
+
82
+ These are controller sketches. Add application-specific authentication, schema validation, CSRF defense, CORS, rate limiting, user lookup, and response policy. On Express 4, forward async controller rejections to `next(error)` as shown; the middleware only forwards its own async errors.
@@ -0,0 +1,15 @@
1
+ # Error handling
2
+
3
+ `AuthError` contains a stable code, developer message, safe `publicMessage`, HTTP status, and optional cause. Common status mapping is 401 for missing/invalid/expired/revoked authentication, 403 for forbidden access, and 400 for validation. Configuration errors are server errors and should be caught during startup.
4
+
5
+ ```ts
6
+ const middleware = expressAuth(auth, {
7
+ errorFormatter: (error) => ({
8
+ status: error.status,
9
+ body: { error: { code: error.code, message: error.message } },
10
+ }),
11
+ });
12
+ app.use(middleware.errorHandler()); // Mount after routes.
13
+ ```
14
+
15
+ The formatter receives only a public projection (`status`, `code`, `message`), not the original exception, stack, cause, or developer message. Unknown failures become a generic 500. Applications can pass another formatter to `errorHandler(formatter)`. Do not include passwords, token values, secrets, database errors, stack traces, or other internal details in custom responses.