@codelitdev/oauth-server-kit 0.9.0 → 0.10.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/README.md CHANGED
@@ -31,12 +31,19 @@ import {
31
31
 
32
32
  import {
33
33
  createOAuthProviderOptions,
34
+ isRecentlyAuthenticated,
34
35
  resolveBetterAuthSession,
36
+ secureCookieOptions,
37
+ secureEmailAndPasswordOptions,
38
+ secureSessionOptions,
39
+ sessionCookieName,
35
40
  } from "@codelitdev/oauth-server-kit/better-auth";
36
41
 
37
42
  import {
43
+ createClientIpMiddleware,
38
44
  createOAuthBearerMiddleware,
39
45
  createOAuthPagesRouter,
46
+ parseTrustProxy,
40
47
  } from "@codelitdev/oauth-server-kit/express";
41
48
 
42
49
  import { createMcpOAuthDiscoveryRoutes } from "@codelitdev/oauth-server-kit/mcp";
@@ -57,9 +64,33 @@ application.
57
64
  - Ordinary-login redirects are restricted to configured origins.
58
65
  - Hosted pages reject framing and clear configured stale host-only cookies at
59
66
  both `/login` and `/oauth/login`.
67
+ - Session cookies are `__Host-<prefix>.session_token` (Secure, `Path=/`, no
68
+ `Domain`) over https through `secureCookieOptions`; with cross-subdomain
69
+ cookies they stay `__Secure-`, and over http (local dev) they are plain.
70
+ Products keep their own `cookiePrefix` and pass it to the helper.
71
+ - `secureSessionOptions()` turns the session cookie cache off, so a revoked
72
+ session stops working on the next request.
73
+ - `secureEmailAndPasswordOptions()` keeps password sign-up off; a product
74
+ that opts in with `{ enabled: true }` also requires a verified email before
75
+ sign-in, so nobody can claim an address they don't own.
76
+ - `createClientIpMiddleware` resolves the client address from a trusted proxy
77
+ chain (`req.ip`, which ignores the attacker-controlled left of
78
+ `X-Forwarded-For`) into an internal header, and `secureCookieOptions`'
79
+ `clientIpHeader` makes Better Auth read only that header.
80
+ - Session identities carry `authenticatedAt`; `isRecentlyAuthenticated`
81
+ supports step-up checks. OAuth identities never count as recent.
60
82
  - Product claims such as `team_id`, roles, or permissions are not returned as
61
83
  trusted package identity fields.
62
84
 
85
+ Known limitation: `@better-auth/oauth-provider`'s RP-initiated logout
86
+ confirmation cookie (`<session cookie>.oauth_logout_confirmation`) is set on
87
+ `/api/auth/oauth2/end-session/confirm`. Browsers reject a `__Host-` cookie with a
88
+ path other than `/`, so under `__Host-` that confirmation step (shown when an
89
+ OAuth client calls end-session without a valid `id_token_hint`) cannot complete.
90
+ Neither the plugin options nor `advanced.cookies` can change that cookie's name
91
+ or path. Use cross-subdomain mode (`__Secure-`) if a product needs the flow.
92
+ See [ADR 0018](../../docs/decisions/0018-secure-session-cookies-and-client-ip.md).
93
+
63
94
  See [docs/integration.md](docs/integration.md) for full web, mobile, REST, and
64
95
  MCP setup, and [docs/architecture.md](docs/architecture.md) for the normative
65
96
  product requirements and public contract.
@@ -1,5 +1,5 @@
1
1
  import { OAuthOptions } from '@better-auth/oauth-provider';
2
- import { A as AuthenticationResult } from './types-DJxCmhYu.mjs';
2
+ import { A as AuthenticationResult, a as AuthenticatedIdentity } from './types-sqdNO7Nc.mjs';
3
3
  import '@better-auth/oauth-provider/resource-client';
4
4
 
5
5
  type BetterAuthSession = {
@@ -11,6 +11,8 @@ type BetterAuthSession = {
11
11
  session: {
12
12
  id: string;
13
13
  expiresAt: Date;
14
+ /** When the session was created, i.e. when the user signed in. */
15
+ createdAt: Date | string;
14
16
  };
15
17
  };
16
18
  interface BetterAuthSessionApi {
@@ -37,4 +39,107 @@ interface CreateOAuthProviderOptionsInput {
37
39
  }
38
40
  declare function createOAuthProviderOptions(input: CreateOAuthProviderOptionsInput): OAuthOptions<string[]>;
39
41
 
40
- export { type BetterAuthSession, type BetterAuthSessionApi, type CreateOAuthProviderOptionsInput, type ResolveBetterAuthSessionOptions, createOAuthProviderOptions, resolveBetterAuthSession };
42
+ interface IsRecentlyAuthenticatedOptions {
43
+ maxAgeMs: number;
44
+ now?: Date;
45
+ }
46
+ /**
47
+ * Whether the identity is a browser session that signed in within `maxAgeMs`.
48
+ * OAuth identities never count: a bearer token carries no interactive
49
+ * sign-in. Products choose which actions require this (step-up checks).
50
+ */
51
+ declare function isRecentlyAuthenticated(identity: AuthenticatedIdentity, options: IsRecentlyAuthenticatedOptions): boolean;
52
+
53
+ interface SecureCookieOptionsInput {
54
+ /** Public URL Better Auth is served from (its `baseURL`). */
55
+ baseURL: string;
56
+ /** Product-specific prefix, e.g. `courselit`. Must not already carry `__Host-`/`__Secure-`. */
57
+ cookiePrefix: string;
58
+ /** Pass through only when cookies must be shared across sibling subdomains. */
59
+ crossSubDomainCookies?: {
60
+ enabled: boolean;
61
+ domain?: string;
62
+ };
63
+ /**
64
+ * Name of an internal header the app's edge sets to the trusted client
65
+ * address (see `createClientIpMiddleware`). Better Auth then reads the
66
+ * address only from it, never from `X-Forwarded-For`.
67
+ */
68
+ clientIpHeader?: string;
69
+ }
70
+ /** The subset of Better Auth's `advanced` options these helpers produce. */
71
+ interface SecureCookieAdvancedOptions {
72
+ cookiePrefix: string;
73
+ useSecureCookies?: boolean;
74
+ defaultCookieAttributes?: {
75
+ secure: boolean;
76
+ path: string;
77
+ };
78
+ crossSubDomainCookies?: {
79
+ enabled: boolean;
80
+ domain?: string;
81
+ };
82
+ ipAddress?: {
83
+ ipAddressHeaders: string[];
84
+ };
85
+ }
86
+ /**
87
+ * Better Auth `advanced` options for hardened session cookies. Spread them
88
+ * into your own config: `advanced: { ...secureCookieOptions({...}) }`.
89
+ *
90
+ * - https, no cross-subdomain cookies: the session cookie is
91
+ * `__Host-<prefix>.session_token` (Secure, `Path=/`, no `Domain`), so a
92
+ * sibling subdomain cannot plant or shadow it. Better Auth only knows
93
+ * `__Secure-` and would produce `__Secure-__Host-...`, so `__Host-` is
94
+ * folded into `cookiePrefix` and Secure is set via `defaultCookieAttributes`.
95
+ * - https with cross-subdomain cookies: `__Host-` forbids `Domain`, so the
96
+ * cookie keeps Better Auth's `__Secure-` prefix and is shared as configured.
97
+ * - http (local development): plain `<prefix>.session_token`.
98
+ *
99
+ * Known limitation: `@better-auth/oauth-provider` stores a short-lived
100
+ * RP-initiated-logout confirmation cookie named
101
+ * `<session cookie name>.oauth_logout_confirmation` on the path
102
+ * `/api/auth/oauth2/end-session/confirm`. Browsers reject a `__Host-` cookie
103
+ * with any path but `/`, so that cookie is dropped under `__Host-` and the
104
+ * logout confirmation step (shown when an OAuth client calls end-session
105
+ * without a valid `id_token_hint`) cannot complete. Neither the plugin's
106
+ * options nor `advanced.cookies` can change its name or path. Products that
107
+ * need that flow must use cross-subdomain mode (`__Secure-`) or accept it.
108
+ */
109
+ declare function secureCookieOptions(input: SecureCookieOptionsInput): SecureCookieAdvancedOptions;
110
+ /**
111
+ * The exact session-token cookie name Better Auth produces for options built
112
+ * by `secureCookieOptions`, for products that choose credentials by cookie
113
+ * name. Pass the object `secureCookieOptions` returned.
114
+ */
115
+ declare function sessionCookieName(advanced: Pick<SecureCookieAdvancedOptions, "cookiePrefix" | "useSecureCookies">): string;
116
+ /**
117
+ * Better Auth `session` options with the cookie cache off: every request
118
+ * checks the session row, so a revoked session stops working immediately
119
+ * instead of until the cached cookie expires (5 minutes by default).
120
+ */
121
+ declare function secureSessionOptions(): {
122
+ cookieCache: {
123
+ enabled: false;
124
+ };
125
+ };
126
+ interface SecureEmailAndPasswordInput {
127
+ /** Turn password sign-in on. Off by default: products sign people in
128
+ * with email codes, social providers or OAuth. */
129
+ enabled?: boolean;
130
+ }
131
+ /**
132
+ * Better Auth `emailAndPassword` settings. Password sign-up is off unless a
133
+ * product opts in: with it on and no verification, anyone can register an
134
+ * address they don't own and sit on it until the real owner shows up. When
135
+ * a product does enable passwords, the address must be verified before the
136
+ * account can sign in.
137
+ */
138
+ declare function secureEmailAndPasswordOptions(input?: SecureEmailAndPasswordInput): {
139
+ enabled: false;
140
+ } | {
141
+ enabled: true;
142
+ requireEmailVerification: true;
143
+ };
144
+
145
+ export { type BetterAuthSession, type BetterAuthSessionApi, type CreateOAuthProviderOptionsInput, type IsRecentlyAuthenticatedOptions, type ResolveBetterAuthSessionOptions, type SecureCookieAdvancedOptions, type SecureCookieOptionsInput, type SecureEmailAndPasswordInput, createOAuthProviderOptions, isRecentlyAuthenticated, resolveBetterAuthSession, secureCookieOptions, secureEmailAndPasswordOptions, secureSessionOptions, sessionCookieName };
@@ -1,5 +1,5 @@
1
1
  import { OAuthOptions } from '@better-auth/oauth-provider';
2
- import { A as AuthenticationResult } from './types-DJxCmhYu.js';
2
+ import { A as AuthenticationResult, a as AuthenticatedIdentity } from './types-sqdNO7Nc.js';
3
3
  import '@better-auth/oauth-provider/resource-client';
4
4
 
5
5
  type BetterAuthSession = {
@@ -11,6 +11,8 @@ type BetterAuthSession = {
11
11
  session: {
12
12
  id: string;
13
13
  expiresAt: Date;
14
+ /** When the session was created, i.e. when the user signed in. */
15
+ createdAt: Date | string;
14
16
  };
15
17
  };
16
18
  interface BetterAuthSessionApi {
@@ -37,4 +39,107 @@ interface CreateOAuthProviderOptionsInput {
37
39
  }
38
40
  declare function createOAuthProviderOptions(input: CreateOAuthProviderOptionsInput): OAuthOptions<string[]>;
39
41
 
40
- export { type BetterAuthSession, type BetterAuthSessionApi, type CreateOAuthProviderOptionsInput, type ResolveBetterAuthSessionOptions, createOAuthProviderOptions, resolveBetterAuthSession };
42
+ interface IsRecentlyAuthenticatedOptions {
43
+ maxAgeMs: number;
44
+ now?: Date;
45
+ }
46
+ /**
47
+ * Whether the identity is a browser session that signed in within `maxAgeMs`.
48
+ * OAuth identities never count: a bearer token carries no interactive
49
+ * sign-in. Products choose which actions require this (step-up checks).
50
+ */
51
+ declare function isRecentlyAuthenticated(identity: AuthenticatedIdentity, options: IsRecentlyAuthenticatedOptions): boolean;
52
+
53
+ interface SecureCookieOptionsInput {
54
+ /** Public URL Better Auth is served from (its `baseURL`). */
55
+ baseURL: string;
56
+ /** Product-specific prefix, e.g. `courselit`. Must not already carry `__Host-`/`__Secure-`. */
57
+ cookiePrefix: string;
58
+ /** Pass through only when cookies must be shared across sibling subdomains. */
59
+ crossSubDomainCookies?: {
60
+ enabled: boolean;
61
+ domain?: string;
62
+ };
63
+ /**
64
+ * Name of an internal header the app's edge sets to the trusted client
65
+ * address (see `createClientIpMiddleware`). Better Auth then reads the
66
+ * address only from it, never from `X-Forwarded-For`.
67
+ */
68
+ clientIpHeader?: string;
69
+ }
70
+ /** The subset of Better Auth's `advanced` options these helpers produce. */
71
+ interface SecureCookieAdvancedOptions {
72
+ cookiePrefix: string;
73
+ useSecureCookies?: boolean;
74
+ defaultCookieAttributes?: {
75
+ secure: boolean;
76
+ path: string;
77
+ };
78
+ crossSubDomainCookies?: {
79
+ enabled: boolean;
80
+ domain?: string;
81
+ };
82
+ ipAddress?: {
83
+ ipAddressHeaders: string[];
84
+ };
85
+ }
86
+ /**
87
+ * Better Auth `advanced` options for hardened session cookies. Spread them
88
+ * into your own config: `advanced: { ...secureCookieOptions({...}) }`.
89
+ *
90
+ * - https, no cross-subdomain cookies: the session cookie is
91
+ * `__Host-<prefix>.session_token` (Secure, `Path=/`, no `Domain`), so a
92
+ * sibling subdomain cannot plant or shadow it. Better Auth only knows
93
+ * `__Secure-` and would produce `__Secure-__Host-...`, so `__Host-` is
94
+ * folded into `cookiePrefix` and Secure is set via `defaultCookieAttributes`.
95
+ * - https with cross-subdomain cookies: `__Host-` forbids `Domain`, so the
96
+ * cookie keeps Better Auth's `__Secure-` prefix and is shared as configured.
97
+ * - http (local development): plain `<prefix>.session_token`.
98
+ *
99
+ * Known limitation: `@better-auth/oauth-provider` stores a short-lived
100
+ * RP-initiated-logout confirmation cookie named
101
+ * `<session cookie name>.oauth_logout_confirmation` on the path
102
+ * `/api/auth/oauth2/end-session/confirm`. Browsers reject a `__Host-` cookie
103
+ * with any path but `/`, so that cookie is dropped under `__Host-` and the
104
+ * logout confirmation step (shown when an OAuth client calls end-session
105
+ * without a valid `id_token_hint`) cannot complete. Neither the plugin's
106
+ * options nor `advanced.cookies` can change its name or path. Products that
107
+ * need that flow must use cross-subdomain mode (`__Secure-`) or accept it.
108
+ */
109
+ declare function secureCookieOptions(input: SecureCookieOptionsInput): SecureCookieAdvancedOptions;
110
+ /**
111
+ * The exact session-token cookie name Better Auth produces for options built
112
+ * by `secureCookieOptions`, for products that choose credentials by cookie
113
+ * name. Pass the object `secureCookieOptions` returned.
114
+ */
115
+ declare function sessionCookieName(advanced: Pick<SecureCookieAdvancedOptions, "cookiePrefix" | "useSecureCookies">): string;
116
+ /**
117
+ * Better Auth `session` options with the cookie cache off: every request
118
+ * checks the session row, so a revoked session stops working immediately
119
+ * instead of until the cached cookie expires (5 minutes by default).
120
+ */
121
+ declare function secureSessionOptions(): {
122
+ cookieCache: {
123
+ enabled: false;
124
+ };
125
+ };
126
+ interface SecureEmailAndPasswordInput {
127
+ /** Turn password sign-in on. Off by default: products sign people in
128
+ * with email codes, social providers or OAuth. */
129
+ enabled?: boolean;
130
+ }
131
+ /**
132
+ * Better Auth `emailAndPassword` settings. Password sign-up is off unless a
133
+ * product opts in: with it on and no verification, anyone can register an
134
+ * address they don't own and sit on it until the real owner shows up. When
135
+ * a product does enable passwords, the address must be verified before the
136
+ * account can sign in.
137
+ */
138
+ declare function secureEmailAndPasswordOptions(input?: SecureEmailAndPasswordInput): {
139
+ enabled: false;
140
+ } | {
141
+ enabled: true;
142
+ requireEmailVerification: true;
143
+ };
144
+
145
+ export { type BetterAuthSession, type BetterAuthSessionApi, type CreateOAuthProviderOptionsInput, type IsRecentlyAuthenticatedOptions, type ResolveBetterAuthSessionOptions, type SecureCookieAdvancedOptions, type SecureCookieOptionsInput, type SecureEmailAndPasswordInput, createOAuthProviderOptions, isRecentlyAuthenticated, resolveBetterAuthSession, secureCookieOptions, secureEmailAndPasswordOptions, secureSessionOptions, sessionCookieName };
@@ -49,7 +49,8 @@ async function resolveBetterAuthSession(options, headers) {
49
49
  subject: session.user.id,
50
50
  email: session.user.email,
51
51
  name: session.user.name,
52
- scopes: []
52
+ scopes: [],
53
+ authenticatedAt: new Date(session.session.createdAt)
53
54
  }
54
55
  };
55
56
  } catch (cause) {
@@ -124,6 +125,87 @@ function createOAuthProviderOptions(input) {
124
125
  allowUnauthenticatedClientRegistration: (_b = input.allowUnauthenticatedDynamicClientRegistration) != null ? _b : false
125
126
  }, clientRegistrationDefaultScopes ? { clientRegistrationDefaultScopes } : {}), clientRegistrationAllowedScopes ? { clientRegistrationAllowedScopes } : {});
126
127
  }
128
+ function isRecentlyAuthenticated(identity, options) {
129
+ var _a, _b;
130
+ if (identity.method !== "session") return false;
131
+ const signedInAt = (_a = identity.authenticatedAt) == null ? void 0 : _a.getTime();
132
+ if (signedInAt === void 0 || !Number.isFinite(signedInAt)) return false;
133
+ const age = ((_b = options.now) != null ? _b : /* @__PURE__ */ new Date()).getTime() - signedInAt;
134
+ return age >= -6e4 && age <= options.maxAgeMs;
135
+ }
136
+
137
+ // src/secure-cookies.ts
138
+ var COOKIE_PREFIX_PATTERN = /^[A-Za-z0-9_-]+$/;
139
+ var HOST_PREFIX = "__Host-";
140
+ var SECURE_PREFIX = "__Secure-";
141
+ var SESSION_TOKEN_SUFFIX = ".session_token";
142
+ function assertBaseUrl(value) {
143
+ let url;
144
+ try {
145
+ url = new URL(value);
146
+ } catch (e) {
147
+ throw new Error("baseURL must be an absolute URL");
148
+ }
149
+ if (!["http:", "https:"].includes(url.protocol)) {
150
+ throw new Error("baseURL must use http or https");
151
+ }
152
+ return url;
153
+ }
154
+ function assertCookiePrefix(value) {
155
+ if (!COOKIE_PREFIX_PATTERN.test(value)) {
156
+ throw new Error(
157
+ "cookiePrefix must be non-empty and contain only letters, digits, '_' or '-'"
158
+ );
159
+ }
160
+ if (value.startsWith(HOST_PREFIX) || value.startsWith(SECURE_PREFIX)) {
161
+ throw new Error(
162
+ "cookiePrefix must not include __Host- or __Secure-; the kit adds it"
163
+ );
164
+ }
165
+ }
166
+ function secureCookieOptions(input) {
167
+ var _a, _b;
168
+ const url = assertBaseUrl(input.baseURL);
169
+ assertCookiePrefix(input.cookiePrefix);
170
+ const clientIpHeader = (_a = input.clientIpHeader) == null ? void 0 : _a.trim().toLowerCase();
171
+ if (input.clientIpHeader !== void 0 && !clientIpHeader) {
172
+ throw new Error("clientIpHeader must not be empty");
173
+ }
174
+ const ipAddress = clientIpHeader ? { ipAddress: { ipAddressHeaders: [clientIpHeader] } } : {};
175
+ const crossSubdomain = ((_b = input.crossSubDomainCookies) == null ? void 0 : _b.enabled) === true;
176
+ if (url.protocol !== "https:") {
177
+ return __spreadValues(__spreadValues({
178
+ cookiePrefix: input.cookiePrefix
179
+ }, input.crossSubDomainCookies ? { crossSubDomainCookies: input.crossSubDomainCookies } : {}), ipAddress);
180
+ }
181
+ if (crossSubdomain) {
182
+ return __spreadValues({
183
+ cookiePrefix: input.cookiePrefix,
184
+ useSecureCookies: true,
185
+ crossSubDomainCookies: input.crossSubDomainCookies
186
+ }, ipAddress);
187
+ }
188
+ return __spreadValues({
189
+ cookiePrefix: `${HOST_PREFIX}${input.cookiePrefix}`,
190
+ useSecureCookies: false,
191
+ defaultCookieAttributes: { secure: true, path: "/" }
192
+ }, ipAddress);
193
+ }
194
+ function sessionCookieName(advanced) {
195
+ const secure = advanced.useSecureCookies === true ? SECURE_PREFIX : "";
196
+ return `${secure}${advanced.cookiePrefix}${SESSION_TOKEN_SUFFIX}`;
197
+ }
198
+ function secureSessionOptions() {
199
+ return { cookieCache: { enabled: false } };
200
+ }
201
+ function secureEmailAndPasswordOptions(input = {}) {
202
+ return input.enabled === true ? { enabled: true, requireEmailVerification: true } : { enabled: false };
203
+ }
127
204
 
128
205
  exports.createOAuthProviderOptions = createOAuthProviderOptions;
206
+ exports.isRecentlyAuthenticated = isRecentlyAuthenticated;
129
207
  exports.resolveBetterAuthSession = resolveBetterAuthSession;
208
+ exports.secureCookieOptions = secureCookieOptions;
209
+ exports.secureEmailAndPasswordOptions = secureEmailAndPasswordOptions;
210
+ exports.secureSessionOptions = secureSessionOptions;
211
+ exports.sessionCookieName = sessionCookieName;
@@ -32,7 +32,8 @@ async function resolveBetterAuthSession(options, headers) {
32
32
  subject: session.user.id,
33
33
  email: session.user.email,
34
34
  name: session.user.name,
35
- scopes: []
35
+ scopes: [],
36
+ authenticatedAt: new Date(session.session.createdAt)
36
37
  }
37
38
  };
38
39
  } catch (cause) {
@@ -107,5 +108,81 @@ function createOAuthProviderOptions(input) {
107
108
  allowUnauthenticatedClientRegistration: (_b = input.allowUnauthenticatedDynamicClientRegistration) != null ? _b : false
108
109
  }, clientRegistrationDefaultScopes ? { clientRegistrationDefaultScopes } : {}), clientRegistrationAllowedScopes ? { clientRegistrationAllowedScopes } : {});
109
110
  }
111
+ function isRecentlyAuthenticated(identity, options) {
112
+ var _a, _b;
113
+ if (identity.method !== "session") return false;
114
+ const signedInAt = (_a = identity.authenticatedAt) == null ? void 0 : _a.getTime();
115
+ if (signedInAt === void 0 || !Number.isFinite(signedInAt)) return false;
116
+ const age = ((_b = options.now) != null ? _b : /* @__PURE__ */ new Date()).getTime() - signedInAt;
117
+ return age >= -6e4 && age <= options.maxAgeMs;
118
+ }
119
+
120
+ // src/secure-cookies.ts
121
+ var COOKIE_PREFIX_PATTERN = /^[A-Za-z0-9_-]+$/;
122
+ var HOST_PREFIX = "__Host-";
123
+ var SECURE_PREFIX = "__Secure-";
124
+ var SESSION_TOKEN_SUFFIX = ".session_token";
125
+ function assertBaseUrl(value) {
126
+ let url;
127
+ try {
128
+ url = new URL(value);
129
+ } catch (e) {
130
+ throw new Error("baseURL must be an absolute URL");
131
+ }
132
+ if (!["http:", "https:"].includes(url.protocol)) {
133
+ throw new Error("baseURL must use http or https");
134
+ }
135
+ return url;
136
+ }
137
+ function assertCookiePrefix(value) {
138
+ if (!COOKIE_PREFIX_PATTERN.test(value)) {
139
+ throw new Error(
140
+ "cookiePrefix must be non-empty and contain only letters, digits, '_' or '-'"
141
+ );
142
+ }
143
+ if (value.startsWith(HOST_PREFIX) || value.startsWith(SECURE_PREFIX)) {
144
+ throw new Error(
145
+ "cookiePrefix must not include __Host- or __Secure-; the kit adds it"
146
+ );
147
+ }
148
+ }
149
+ function secureCookieOptions(input) {
150
+ var _a, _b;
151
+ const url = assertBaseUrl(input.baseURL);
152
+ assertCookiePrefix(input.cookiePrefix);
153
+ const clientIpHeader = (_a = input.clientIpHeader) == null ? void 0 : _a.trim().toLowerCase();
154
+ if (input.clientIpHeader !== void 0 && !clientIpHeader) {
155
+ throw new Error("clientIpHeader must not be empty");
156
+ }
157
+ const ipAddress = clientIpHeader ? { ipAddress: { ipAddressHeaders: [clientIpHeader] } } : {};
158
+ const crossSubdomain = ((_b = input.crossSubDomainCookies) == null ? void 0 : _b.enabled) === true;
159
+ if (url.protocol !== "https:") {
160
+ return __spreadValues(__spreadValues({
161
+ cookiePrefix: input.cookiePrefix
162
+ }, input.crossSubDomainCookies ? { crossSubDomainCookies: input.crossSubDomainCookies } : {}), ipAddress);
163
+ }
164
+ if (crossSubdomain) {
165
+ return __spreadValues({
166
+ cookiePrefix: input.cookiePrefix,
167
+ useSecureCookies: true,
168
+ crossSubDomainCookies: input.crossSubDomainCookies
169
+ }, ipAddress);
170
+ }
171
+ return __spreadValues({
172
+ cookiePrefix: `${HOST_PREFIX}${input.cookiePrefix}`,
173
+ useSecureCookies: false,
174
+ defaultCookieAttributes: { secure: true, path: "/" }
175
+ }, ipAddress);
176
+ }
177
+ function sessionCookieName(advanced) {
178
+ const secure = advanced.useSecureCookies === true ? SECURE_PREFIX : "";
179
+ return `${secure}${advanced.cookiePrefix}${SESSION_TOKEN_SUFFIX}`;
180
+ }
181
+ function secureSessionOptions() {
182
+ return { cookieCache: { enabled: false } };
183
+ }
184
+ function secureEmailAndPasswordOptions(input = {}) {
185
+ return input.enabled === true ? { enabled: true, requireEmailVerification: true } : { enabled: false };
186
+ }
110
187
 
111
- export { createOAuthProviderOptions, resolveBetterAuthSession };
188
+ export { createOAuthProviderOptions, isRecentlyAuthenticated, resolveBetterAuthSession, secureCookieOptions, secureEmailAndPasswordOptions, secureSessionOptions, sessionCookieName };
@@ -1,8 +1,39 @@
1
1
  import { RequestHandler, Router } from 'express';
2
- import { a as AuthenticatedIdentity, A as AuthenticationResult } from './types-DJxCmhYu.mjs';
2
+ import { a as AuthenticatedIdentity, A as AuthenticationResult } from './types-sqdNO7Nc.mjs';
3
3
  import { VerifyOAuthAccessTokenOptions } from './index.mjs';
4
4
  import '@better-auth/oauth-provider/resource-client';
5
5
 
6
+ /**
7
+ * Private networks: the cluster and local development. Fine when clients come
8
+ * from the public internet. If clients can reach the app from a private
9
+ * network too, set `TRUST_PROXY` to your proxies' actual addresses or subnet,
10
+ * or those clients could forge their address (Better Auth gives the same
11
+ * advice for its own `trustedProxies`).
12
+ */
13
+ declare const DEFAULT_TRUST_PROXY = "loopback, linklocal, uniquelocal";
14
+ /**
15
+ * Parses a `TRUST_PROXY` value into what Express's `trust proxy` setting
16
+ * accepts: a hop count (`1`), `true`/`false`, or a comma-separated list of
17
+ * addresses, CIDRs and the names `loopback`, `linklocal`, `uniquelocal`.
18
+ * Empty means {@link DEFAULT_TRUST_PROXY}.
19
+ */
20
+ declare function parseTrustProxy(raw?: string): boolean | number | string;
21
+ interface CreateClientIpMiddlewareOptions {
22
+ /** Internal header to carry the resolved client address. */
23
+ header: string;
24
+ }
25
+ /**
26
+ * Sets `req.headers[header]` to the client address, overwriting anything the
27
+ * client sent. Pair it with `secureCookieOptions({ clientIpHeader })` so
28
+ * Better Auth's rate limiting and session IP never read `X-Forwarded-For`.
29
+ *
30
+ * The app must call `app.set("trust proxy", parseTrustProxy(process.env.TRUST_PROXY))`
31
+ * first and mount this before Better Auth's handler. `req.ip` then walks
32
+ * `X-Forwarded-For` from the right, skipping trusted proxy hops, so the
33
+ * attacker-controlled left side never counts.
34
+ */
35
+ declare function createClientIpMiddleware(options: CreateClientIpMiddlewareOptions): RequestHandler;
36
+
6
37
  declare global {
7
38
  namespace Express {
8
39
  interface Request {
@@ -45,4 +76,4 @@ interface CreateOAuthPagesRouterOptions {
45
76
  }
46
77
  declare function createOAuthPagesRouter(rawOptions: CreateOAuthPagesRouterOptions): Router;
47
78
 
48
- export { type CreateOAuthBearerMiddlewareOptions, type CreateOAuthPagesRouterOptions, type HostedLoginCaptcha, type HostedLoginMethod, createOAuthBearerMiddleware, createOAuthPagesRouter };
79
+ export { type CreateClientIpMiddlewareOptions, type CreateOAuthBearerMiddlewareOptions, type CreateOAuthPagesRouterOptions, DEFAULT_TRUST_PROXY, type HostedLoginCaptcha, type HostedLoginMethod, createClientIpMiddleware, createOAuthBearerMiddleware, createOAuthPagesRouter, parseTrustProxy };
package/dist/express.d.ts CHANGED
@@ -1,8 +1,39 @@
1
1
  import { RequestHandler, Router } from 'express';
2
- import { a as AuthenticatedIdentity, A as AuthenticationResult } from './types-DJxCmhYu.js';
2
+ import { a as AuthenticatedIdentity, A as AuthenticationResult } from './types-sqdNO7Nc.js';
3
3
  import { VerifyOAuthAccessTokenOptions } from './index.js';
4
4
  import '@better-auth/oauth-provider/resource-client';
5
5
 
6
+ /**
7
+ * Private networks: the cluster and local development. Fine when clients come
8
+ * from the public internet. If clients can reach the app from a private
9
+ * network too, set `TRUST_PROXY` to your proxies' actual addresses or subnet,
10
+ * or those clients could forge their address (Better Auth gives the same
11
+ * advice for its own `trustedProxies`).
12
+ */
13
+ declare const DEFAULT_TRUST_PROXY = "loopback, linklocal, uniquelocal";
14
+ /**
15
+ * Parses a `TRUST_PROXY` value into what Express's `trust proxy` setting
16
+ * accepts: a hop count (`1`), `true`/`false`, or a comma-separated list of
17
+ * addresses, CIDRs and the names `loopback`, `linklocal`, `uniquelocal`.
18
+ * Empty means {@link DEFAULT_TRUST_PROXY}.
19
+ */
20
+ declare function parseTrustProxy(raw?: string): boolean | number | string;
21
+ interface CreateClientIpMiddlewareOptions {
22
+ /** Internal header to carry the resolved client address. */
23
+ header: string;
24
+ }
25
+ /**
26
+ * Sets `req.headers[header]` to the client address, overwriting anything the
27
+ * client sent. Pair it with `secureCookieOptions({ clientIpHeader })` so
28
+ * Better Auth's rate limiting and session IP never read `X-Forwarded-For`.
29
+ *
30
+ * The app must call `app.set("trust proxy", parseTrustProxy(process.env.TRUST_PROXY))`
31
+ * first and mount this before Better Auth's handler. `req.ip` then walks
32
+ * `X-Forwarded-For` from the right, skipping trusted proxy hops, so the
33
+ * attacker-controlled left side never counts.
34
+ */
35
+ declare function createClientIpMiddleware(options: CreateClientIpMiddlewareOptions): RequestHandler;
36
+
6
37
  declare global {
7
38
  namespace Express {
8
39
  interface Request {
@@ -45,4 +76,4 @@ interface CreateOAuthPagesRouterOptions {
45
76
  }
46
77
  declare function createOAuthPagesRouter(rawOptions: CreateOAuthPagesRouterOptions): Router;
47
78
 
48
- export { type CreateOAuthBearerMiddlewareOptions, type CreateOAuthPagesRouterOptions, type HostedLoginCaptcha, type HostedLoginMethod, createOAuthBearerMiddleware, createOAuthPagesRouter };
79
+ export { type CreateClientIpMiddlewareOptions, type CreateOAuthBearerMiddlewareOptions, type CreateOAuthPagesRouterOptions, DEFAULT_TRUST_PROXY, type HostedLoginCaptcha, type HostedLoginMethod, createClientIpMiddleware, createOAuthBearerMiddleware, createOAuthPagesRouter, parseTrustProxy };
package/dist/express.js CHANGED
@@ -34,6 +34,25 @@ var __objRest = (source, exclude) => {
34
34
  return target;
35
35
  };
36
36
 
37
+ // src/client-ip.ts
38
+ var DEFAULT_TRUST_PROXY = "loopback, linklocal, uniquelocal";
39
+ function parseTrustProxy(raw) {
40
+ const value = raw == null ? void 0 : raw.trim();
41
+ if (!value) return DEFAULT_TRUST_PROXY;
42
+ if (value === "true") return true;
43
+ if (value === "false") return false;
44
+ if (/^\d+$/.test(value)) return Number.parseInt(value, 10);
45
+ return value;
46
+ }
47
+ function createClientIpMiddleware(options) {
48
+ const header = options.header.trim().toLowerCase();
49
+ if (!header) throw new Error("header must not be empty");
50
+ return (req, _res, next) => {
51
+ req.headers[header] = req.ip || req.socket.remoteAddress || "unknown";
52
+ next();
53
+ };
54
+ }
55
+
37
56
  // src/verify-access-token.ts
38
57
  function requireAbsoluteUrl(value, field) {
39
58
  let url;
@@ -433,5 +452,8 @@ function createOAuthPagesRouter(rawOptions) {
433
452
  return router;
434
453
  }
435
454
 
455
+ exports.DEFAULT_TRUST_PROXY = DEFAULT_TRUST_PROXY;
456
+ exports.createClientIpMiddleware = createClientIpMiddleware;
436
457
  exports.createOAuthBearerMiddleware = createOAuthBearerMiddleware;
437
458
  exports.createOAuthPagesRouter = createOAuthPagesRouter;
459
+ exports.parseTrustProxy = parseTrustProxy;
package/dist/express.mjs CHANGED
@@ -2,6 +2,25 @@ import { verifyOAuthAccessToken, validateOAuthVerificationOptions } from './chun
2
2
  import { __objRest, __spreadProps, __spreadValues } from './chunk-FWCSY2DS.mjs';
3
3
  import { Router } from 'express';
4
4
 
5
+ // src/client-ip.ts
6
+ var DEFAULT_TRUST_PROXY = "loopback, linklocal, uniquelocal";
7
+ function parseTrustProxy(raw) {
8
+ const value = raw == null ? void 0 : raw.trim();
9
+ if (!value) return DEFAULT_TRUST_PROXY;
10
+ if (value === "true") return true;
11
+ if (value === "false") return false;
12
+ if (/^\d+$/.test(value)) return Number.parseInt(value, 10);
13
+ return value;
14
+ }
15
+ function createClientIpMiddleware(options) {
16
+ const header = options.header.trim().toLowerCase();
17
+ if (!header) throw new Error("header must not be empty");
18
+ return (req, _res, next) => {
19
+ req.headers[header] = req.ip || req.socket.remoteAddress || "unknown";
20
+ next();
21
+ };
22
+ }
23
+
5
24
  // src/express.ts
6
25
  function setBearerChallenge(response, resourceMetadataUrl) {
7
26
  if (resourceMetadataUrl) {
@@ -320,4 +339,4 @@ function createOAuthPagesRouter(rawOptions) {
320
339
  return router;
321
340
  }
322
341
 
323
- export { createOAuthBearerMiddleware, createOAuthPagesRouter };
342
+ export { DEFAULT_TRUST_PROXY, createClientIpMiddleware, createOAuthBearerMiddleware, createOAuthPagesRouter, parseTrustProxy };
package/dist/index.d.mts CHANGED
@@ -1,5 +1,5 @@
1
- import { O as OAuthResourceClient, A as AuthenticationResult } from './types-DJxCmhYu.mjs';
2
- export { a as AuthenticatedIdentity } from './types-DJxCmhYu.mjs';
1
+ import { O as OAuthResourceClient, A as AuthenticationResult } from './types-sqdNO7Nc.mjs';
2
+ export { a as AuthenticatedIdentity } from './types-sqdNO7Nc.mjs';
3
3
  import '@better-auth/oauth-provider/resource-client';
4
4
 
5
5
  interface VerifyOAuthAccessTokenOptions {
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- import { O as OAuthResourceClient, A as AuthenticationResult } from './types-DJxCmhYu.js';
2
- export { a as AuthenticatedIdentity } from './types-DJxCmhYu.js';
1
+ import { O as OAuthResourceClient, A as AuthenticationResult } from './types-sqdNO7Nc.js';
2
+ export { a as AuthenticatedIdentity } from './types-sqdNO7Nc.js';
3
3
  import '@better-auth/oauth-provider/resource-client';
4
4
 
5
5
  interface VerifyOAuthAccessTokenOptions {
package/dist/mcp.d.mts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { Router } from 'express';
2
- import { O as OAuthResourceClient } from './types-DJxCmhYu.mjs';
2
+ import { O as OAuthResourceClient } from './types-sqdNO7Nc.mjs';
3
3
  import '@better-auth/oauth-provider/resource-client';
4
4
 
5
5
  interface BetterAuthMetadataApi {
package/dist/mcp.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { Router } from 'express';
2
- import { O as OAuthResourceClient } from './types-DJxCmhYu.js';
2
+ import { O as OAuthResourceClient } from './types-sqdNO7Nc.js';
3
3
  import '@better-auth/oauth-provider/resource-client';
4
4
 
5
5
  interface BetterAuthMetadataApi {
@@ -7,6 +7,8 @@ type AuthenticatedIdentity = {
7
7
  email: string;
8
8
  name?: string | null;
9
9
  scopes: [];
10
+ /** When the session was created (the sign-in time), for step-up checks. */
11
+ authenticatedAt: Date;
10
12
  } | {
11
13
  method: "oauth";
12
14
  issuer: string;
@@ -7,6 +7,8 @@ type AuthenticatedIdentity = {
7
7
  email: string;
8
8
  name?: string | null;
9
9
  scopes: [];
10
+ /** When the session was created (the sign-in time), for step-up checks. */
11
+ authenticatedAt: Date;
10
12
  } | {
11
13
  method: "oauth";
12
14
  issuer: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@codelitdev/oauth-server-kit",
3
- "version": "0.9.0",
3
+ "version": "0.10.0",
4
4
  "description": "Lean Better Auth OAuth authentication framework for web, mobile, REST, and MCP surfaces.",
5
5
  "author": "CodeLit",
6
6
  "license": "MIT",
@@ -79,20 +79,20 @@
79
79
  "tag": "latest"
80
80
  },
81
81
  "peerDependencies": {
82
- "@better-auth/oauth-provider": "^1.7.2",
83
- "better-auth": "^1.7.2",
82
+ "@better-auth/oauth-provider": "^1.7.16",
83
+ "better-auth": "^1.7.16",
84
84
  "express": "^4.21.2"
85
85
  },
86
86
  "devDependencies": {
87
- "@better-auth/core": "1.7.2",
88
- "@better-auth/oauth-provider": "1.7.2",
89
- "@better-auth/utils": "0.4.2",
90
- "@better-fetch/fetch": "1.3.1",
87
+ "@better-auth/core": "1.7.22",
88
+ "@better-auth/oauth-provider": "1.7.22",
89
+ "@better-auth/utils": "0.5.1",
90
+ "@better-fetch/fetch": "1.3.3",
91
91
  "@types/express": "^4.17.20",
92
92
  "@types/node": "^22.14.1",
93
93
  "@types/bun": "^1.4.1",
94
- "better-auth": "1.7.2",
95
- "better-call": "1.4.0",
94
+ "better-auth": "1.7.22",
95
+ "better-call": "1.4.5",
96
96
  "express": "^4.21.2",
97
97
  "rimraf": "^5.0.0",
98
98
  "tsup": "^8.3.0",