@kaperkunde/auth 0.1.1 → 0.2.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/README.md CHANGED
@@ -6,6 +6,7 @@ connector, plus the rules and words the sign-in forms share.
6
6
  | Import | What it gives |
7
7
  | ------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
8
8
  | `@kaperkunde/auth/preset` | `mcpAuthPreset({ urls, scopes })` to spread into `betterAuth({…})`: jwt (its `/token` closed, no set-auth-jwt header), mcp (code + refresh only, open registration, an hour's access token), cimd, and the production rate limit with room for the OAuth endpoints. `discoveryRoute(auth.handler)` for the root `.well-known` routes |
9
+ | `@kaperkunde/auth/forms` | The form helpers alone, safe in a client component: `validatePassword`, `withResetPasswordEmail`, `authErrorMessage`, `oauthRedirectFrom`, `RESET_PASSWORD_EMAIL_PARAM`, `UNEXPECTED_ERROR` |
9
10
  | `@kaperkunde/auth` | `mcpUrls` (issuer, resource, JWKS over loopback, resource metadata); `parseScopeList`, `effectiveScopes` (token ∩ live consent, null once disconnected); `bearerChallenge`, `mcpAuthError`, `withCors`, `bearerTokenFrom`, `calledToolNames`; `hashOpaqueToken` + `opaqueTokenClaims` (refuses revoked, expired, DPoP-bound, disabled-client, expired-session and other-resource tokens); `validatePassword`, `withResetPasswordEmail`, `authErrorMessage`, `oauthRedirectFrom` |
10
11
 
11
12
  The app keeps what is its own: the database adapter, its hooks, the scope
@@ -0,0 +1,29 @@
1
+ /** One password rule for sign-up, reset and change-password forms. */
2
+ export declare function validatePassword(password: string, verify: string, { minLength }?: {
3
+ minLength?: number;
4
+ }): string | null;
5
+ /** Query parameter carrying the account's address on the password-reset link. */
6
+ export declare const RESET_PASSWORD_EMAIL_PARAM = "email";
7
+ /**
8
+ * Adds the account's address to the callback on a Better Auth reset link,
9
+ * so the form can sign the person in once the new password is set. The
10
+ * address comes from the user row Better Auth resolved, never from whoever
11
+ * asked, so a caller's redirectTo cannot put someone else's address on it.
12
+ */
13
+ export declare function withResetPasswordEmail(url: string, email: string): string;
14
+ export declare const UNEXPECTED_ERROR = "Something went wrong on our side. Try again in a minute.";
15
+ type AuthError = {
16
+ code?: string;
17
+ message?: string;
18
+ status?: number;
19
+ error?: string;
20
+ };
21
+ /**
22
+ * Better Auth answers with codes; people read sentences. A server error is
23
+ * always the unexpected-error sentence, never Better Auth's own message.
24
+ * `overrides` lets an app word a code its own way.
25
+ */
26
+ export declare function authErrorMessage(error: AuthError | null | undefined, fallback: string, overrides?: Readonly<Record<string, string>>): string;
27
+ /** Where an authorization continues after signing in for a connecting app, if it does. */
28
+ export declare function oauthRedirectFrom(data: unknown): string | null;
29
+ export {};
package/dist/forms.mjs ADDED
@@ -0,0 +1,62 @@
1
+ // src/forms.ts
2
+ /** One password rule for sign-up, reset and change-password forms. */
3
+ function validatePassword(password, verify, { minLength = 8 } = {}) {
4
+ if (password.length < minLength) return `Password must be at least ${minLength} characters.`;
5
+ if (password !== verify) return "Passwords do not match.";
6
+ return null;
7
+ }
8
+ /** Query parameter carrying the account's address on the password-reset link. */
9
+ const RESET_PASSWORD_EMAIL_PARAM = "email";
10
+ /**
11
+ * Adds the account's address to the callback on a Better Auth reset link,
12
+ * so the form can sign the person in once the new password is set. The
13
+ * address comes from the user row Better Auth resolved, never from whoever
14
+ * asked, so a caller's redirectTo cannot put someone else's address on it.
15
+ */
16
+ function withResetPasswordEmail(url, email) {
17
+ try {
18
+ const link = new URL(url);
19
+ const callback = new URL(link.searchParams.get("callbackURL") || "/reset-password", link.origin);
20
+ callback.searchParams.set(RESET_PASSWORD_EMAIL_PARAM, email);
21
+ link.searchParams.set("callbackURL", `${callback.pathname}${callback.search}`);
22
+ return link.toString();
23
+ } catch {
24
+ return url;
25
+ }
26
+ }
27
+ const UNEXPECTED_ERROR = "Something went wrong on our side. Try again in a minute.";
28
+ const MESSAGES = {
29
+ INVALID_EMAIL_OR_PASSWORD: "Wrong email or password.",
30
+ USER_ALREADY_EXISTS: "An account with this email already exists. Sign in instead.",
31
+ USER_ALREADY_EXISTS_USE_ANOTHER_EMAIL: "An account with this email already exists. Sign in instead.",
32
+ PASSWORD_TOO_SHORT: "Use at least 8 characters for your password.",
33
+ PASSWORD_TOO_LONG: "That password is too long. Use at most 128 characters.",
34
+ INVALID_EMAIL: "Enter a valid email address.",
35
+ INVALID_PASSWORD: "Your current password is wrong.",
36
+ CREDENTIAL_ACCOUNT_NOT_FOUND: "This account has no password to change.",
37
+ INVALID_TOKEN: "This link has expired or was already used. Ask for a new one.",
38
+ EMAIL_NOT_VERIFIED: "Confirm your email address first: we sent you a link."
39
+ };
40
+ /**
41
+ * Better Auth answers with codes; people read sentences. A server error is
42
+ * always the unexpected-error sentence, never Better Auth's own message.
43
+ * `overrides` lets an app word a code its own way.
44
+ */
45
+ function authErrorMessage(error, fallback, overrides = {}) {
46
+ if (!error) return fallback;
47
+ if (error.status === 429) return "Too many attempts. Wait a minute and try again.";
48
+ if (error.status !== void 0 && error.status >= 500) return UNEXPECTED_ERROR;
49
+ const code = error.code ?? "";
50
+ if (overrides[code]) return overrides[code];
51
+ if (MESSAGES[code]) return MESSAGES[code];
52
+ if (error.error === "invalid_signature" || code === "INVALID_SIGNATURE") return "This request to connect an app has expired. Go back to the app and start connecting again.";
53
+ return fallback;
54
+ }
55
+ /** Where an authorization continues after signing in for a connecting app, if it does. */
56
+ function oauthRedirectFrom(data) {
57
+ if (!data || typeof data !== "object") return null;
58
+ const record = data;
59
+ const url = typeof record.url === "string" ? record.url : record.redirect_uri;
60
+ return typeof url === "string" && (record.redirect === true || record.redirect_uri) ? url : null;
61
+ }
62
+ export { RESET_PASSWORD_EMAIL_PARAM, UNEXPECTED_ERROR, authErrorMessage, oauthRedirectFrom, validatePassword, withResetPasswordEmail };
package/dist/index.d.mts CHANGED
@@ -1,4 +1,5 @@
1
1
  import { n as mcpUrls, t as McpUrls } from "./urls-Cbyx1eIp.mjs";
2
+ import { RESET_PASSWORD_EMAIL_PARAM, UNEXPECTED_ERROR, authErrorMessage, oauthRedirectFrom, validatePassword, withResetPasswordEmail } from "./forms.mjs";
2
3
  /** The headers an MCP route answers with, for clients in the browser too. */
3
4
  export declare const MCP_CORS_HEADERS: Readonly<Record<string, string>>;
4
5
  /** A response with CORS headers set over its own. */
@@ -28,34 +29,6 @@ export declare function mcpAuthError({ status, message, challenge }: {
28
29
  export declare function bearerTokenFrom(request: Request): string | null;
29
30
  /** Tool names a JSON-RPC body calls (one request or a batch), for scope checks and rate buckets. */
30
31
  export declare function calledToolNames(request: Request): Promise<string[]>;
31
- /** One password rule for sign-up, reset and change-password forms. */
32
- export declare function validatePassword(password: string, verify: string, { minLength }?: {
33
- minLength?: number;
34
- }): string | null;
35
- /** Query parameter carrying the account's address on the password-reset link. */
36
- export declare const RESET_PASSWORD_EMAIL_PARAM = "email";
37
- /**
38
- * Adds the account's address to the callback on a Better Auth reset link,
39
- * so the form can sign the person in once the new password is set. The
40
- * address comes from the user row Better Auth resolved, never from whoever
41
- * asked, so a caller's redirectTo cannot put someone else's address on it.
42
- */
43
- export declare function withResetPasswordEmail(url: string, email: string): string;
44
- export declare const UNEXPECTED_ERROR = "Something went wrong on our side. Try again in a minute.";
45
- type AuthError = {
46
- code?: string;
47
- message?: string;
48
- status?: number;
49
- error?: string;
50
- };
51
- /**
52
- * Better Auth answers with codes; people read sentences. A server error is
53
- * always the unexpected-error sentence, never Better Auth's own message.
54
- * `overrides` lets an app word a code its own way.
55
- */
56
- export declare function authErrorMessage(error: AuthError | null | undefined, fallback: string, overrides?: Readonly<Record<string, string>>): string;
57
- /** Where an authorization continues after signing in for a connecting app, if it does. */
58
- export declare function oauthRedirectFrom(data: unknown): string | null;
59
32
  /**
60
33
  * Access tokens for clients that did not name the resource they wanted (RFC
61
34
  * 8707). The authorization server issues an opaque token then, rather than a
@@ -96,4 +69,4 @@ export declare function parseScopeList(value: unknown): string[];
96
69
  * (`consented` null), and intersecting makes a downgrade take effect too.
97
70
  */
98
71
  export declare function effectiveScopes(tokenScopes: unknown, consented: unknown | null): string[] | null;
99
- export { type McpUrls, type OpaqueTokenRow, mcpUrls };
72
+ export { type McpUrls, type OpaqueTokenRow, RESET_PASSWORD_EMAIL_PARAM, UNEXPECTED_ERROR, authErrorMessage, mcpUrls, oauthRedirectFrom, validatePassword, withResetPasswordEmail };
package/dist/index.mjs CHANGED
@@ -1,3 +1,4 @@
1
+ import { RESET_PASSWORD_EMAIL_PARAM, UNEXPECTED_ERROR, authErrorMessage, oauthRedirectFrom, validatePassword, withResetPasswordEmail } from "./forms.mjs";
1
2
  import { createHash } from "node:crypto";
2
3
  // src/challenge.ts
3
4
  /** The headers an MCP route answers with, for clients in the browser too. */
@@ -72,67 +73,6 @@ async function calledToolNames(request) {
72
73
  return m?.method === "tools/call" && typeof m.params?.name === "string" ? [m.params.name] : [];
73
74
  });
74
75
  }
75
- // src/forms.ts
76
- /** One password rule for sign-up, reset and change-password forms. */
77
- function validatePassword(password, verify, { minLength = 8 } = {}) {
78
- if (password.length < minLength) return `Password must be at least ${minLength} characters.`;
79
- if (password !== verify) return "Passwords do not match.";
80
- return null;
81
- }
82
- /** Query parameter carrying the account's address on the password-reset link. */
83
- const RESET_PASSWORD_EMAIL_PARAM = "email";
84
- /**
85
- * Adds the account's address to the callback on a Better Auth reset link,
86
- * so the form can sign the person in once the new password is set. The
87
- * address comes from the user row Better Auth resolved, never from whoever
88
- * asked, so a caller's redirectTo cannot put someone else's address on it.
89
- */
90
- function withResetPasswordEmail(url, email) {
91
- try {
92
- const link = new URL(url);
93
- const callback = new URL(link.searchParams.get("callbackURL") || "/reset-password", link.origin);
94
- callback.searchParams.set(RESET_PASSWORD_EMAIL_PARAM, email);
95
- link.searchParams.set("callbackURL", `${callback.pathname}${callback.search}`);
96
- return link.toString();
97
- } catch {
98
- return url;
99
- }
100
- }
101
- const UNEXPECTED_ERROR = "Something went wrong on our side. Try again in a minute.";
102
- const MESSAGES = {
103
- INVALID_EMAIL_OR_PASSWORD: "Wrong email or password.",
104
- USER_ALREADY_EXISTS: "An account with this email already exists. Sign in instead.",
105
- USER_ALREADY_EXISTS_USE_ANOTHER_EMAIL: "An account with this email already exists. Sign in instead.",
106
- PASSWORD_TOO_SHORT: "Use at least 8 characters for your password.",
107
- PASSWORD_TOO_LONG: "That password is too long. Use at most 128 characters.",
108
- INVALID_EMAIL: "Enter a valid email address.",
109
- INVALID_PASSWORD: "Your current password is wrong.",
110
- CREDENTIAL_ACCOUNT_NOT_FOUND: "This account has no password to change.",
111
- INVALID_TOKEN: "This link has expired or was already used. Ask for a new one.",
112
- EMAIL_NOT_VERIFIED: "Confirm your email address first: we sent you a link."
113
- };
114
- /**
115
- * Better Auth answers with codes; people read sentences. A server error is
116
- * always the unexpected-error sentence, never Better Auth's own message.
117
- * `overrides` lets an app word a code its own way.
118
- */
119
- function authErrorMessage(error, fallback, overrides = {}) {
120
- if (!error) return fallback;
121
- if (error.status === 429) return "Too many attempts. Wait a minute and try again.";
122
- if (error.status !== void 0 && error.status >= 500) return UNEXPECTED_ERROR;
123
- const code = error.code ?? "";
124
- if (overrides[code]) return overrides[code];
125
- if (MESSAGES[code]) return MESSAGES[code];
126
- if (error.error === "invalid_signature" || code === "INVALID_SIGNATURE") return "This request to connect an app has expired. Go back to the app and start connecting again.";
127
- return fallback;
128
- }
129
- /** Where an authorization continues after signing in for a connecting app, if it does. */
130
- function oauthRedirectFrom(data) {
131
- if (!data || typeof data !== "object") return null;
132
- const record = data;
133
- const url = typeof record.url === "string" ? record.url : record.redirect_uri;
134
- return typeof url === "string" && (record.redirect === true || record.redirect_uri) ? url : null;
135
- }
136
76
  // src/scopes.ts
137
77
  /**
138
78
  * Scopes as Better Auth stores them (an array, a space- or comma-separated
package/dist/preset.d.mts CHANGED
@@ -1,4 +1,5 @@
1
1
  import { t as McpUrls } from "./urls-Cbyx1eIp.mjs";
2
+ import { BetterAuthPlugin } from "better-auth";
2
3
  /**
3
4
  * The Better Auth options an app needs to be an OAuth 2.1 authorization
4
5
  * server for its MCP connector, as one object spread into `betterAuth({…})`:
@@ -24,7 +25,7 @@ export declare function mcpAuthPreset({ urls, scopes, loginPage, consentPage, re
24
25
  consentPage?: string;
25
26
  refreshDays?: number;
26
27
  rateLimited?: boolean;
27
- extraPlugins?: readonly unknown[];
28
+ extraPlugins?: readonly BetterAuthPlugin[];
28
29
  disabledPaths?: readonly string[];
29
30
  }): {
30
31
  disabledPaths: string[];
@@ -44,7 +45,7 @@ export declare function mcpAuthPreset({ urls, scopes, loginPage, consentPage, re
44
45
  "/jwks": false;
45
46
  };
46
47
  };
47
- plugins: unknown[];
48
+ plugins: BetterAuthPlugin[];
48
49
  };
49
50
  /**
50
51
  * Route handlers for the OAuth discovery documents at the site root. Better
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kaperkunde/auth",
3
- "version": "0.1.1",
3
+ "version": "0.2.1",
4
4
  "description": "Better Auth as the OAuth 2.1 server for an MCP connector: the preset, discovery routes, scope and consent checks, challenges, opaque tokens, and the auth forms' rules and messages.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -19,6 +19,11 @@
19
19
  "import": "./dist/index.mjs",
20
20
  "default": "./dist/index.mjs"
21
21
  },
22
+ "./forms": {
23
+ "types": "./dist/forms.d.mts",
24
+ "import": "./dist/forms.mjs",
25
+ "default": "./dist/forms.mjs"
26
+ },
22
27
  "./preset": {
23
28
  "types": "./dist/preset.d.mts",
24
29
  "import": "./dist/preset.mjs",
@@ -52,7 +57,7 @@
52
57
  "provenance": true
53
58
  },
54
59
  "scripts": {
55
- "build": "tsdown src/index.ts src/preset.ts --format esm --dts --clean",
60
+ "build": "tsdown src/index.ts src/preset.ts src/forms.ts --format esm --dts --clean",
56
61
  "typecheck": "tsc --noEmit -p tsconfig.json"
57
62
  }
58
63
  }