@blen/fedreg-mcp-server 2.0.11 → 2.1.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 (68) hide show
  1. package/CHANGELOG.md +7 -1
  2. package/README.md +13 -2
  3. package/dist/accounts/app.d.ts +16 -0
  4. package/dist/accounts/app.d.ts.map +1 -0
  5. package/dist/accounts/app.js +352 -0
  6. package/dist/accounts/app.js.map +1 -0
  7. package/dist/accounts/config.d.ts +16 -0
  8. package/dist/accounts/config.d.ts.map +1 -0
  9. package/dist/accounts/config.js +50 -0
  10. package/dist/accounts/config.js.map +1 -0
  11. package/dist/accounts/crypto.d.ts +18 -0
  12. package/dist/accounts/crypto.d.ts.map +1 -0
  13. package/dist/accounts/crypto.js +43 -0
  14. package/dist/accounts/crypto.js.map +1 -0
  15. package/dist/accounts/portal.d.ts +8 -0
  16. package/dist/accounts/portal.d.ts.map +1 -0
  17. package/dist/accounts/portal.js +170 -0
  18. package/dist/accounts/portal.js.map +1 -0
  19. package/dist/accounts/provider.d.ts +2568 -0
  20. package/dist/accounts/provider.d.ts.map +1 -0
  21. package/dist/accounts/provider.js +107 -0
  22. package/dist/accounts/provider.js.map +1 -0
  23. package/dist/accounts/runtime.d.ts +16 -0
  24. package/dist/accounts/runtime.d.ts.map +1 -0
  25. package/dist/accounts/runtime.js +122 -0
  26. package/dist/accounts/runtime.js.map +1 -0
  27. package/dist/accounts/store.d.ts +25 -0
  28. package/dist/accounts/store.d.ts.map +1 -0
  29. package/dist/accounts/store.js +142 -0
  30. package/dist/accounts/store.js.map +1 -0
  31. package/dist/bin.js +13 -4
  32. package/dist/bin.js.map +1 -1
  33. package/dist/sdk/bindings.js +1 -1
  34. package/dist/sdk/bindings.js.map +1 -1
  35. package/dist/sdk/sources/corpus-loader.d.ts +3 -1
  36. package/dist/sdk/sources/corpus-loader.d.ts.map +1 -1
  37. package/dist/sdk/sources/corpus-loader.js +10 -2
  38. package/dist/sdk/sources/corpus-loader.js.map +1 -1
  39. package/dist/sdk/sources/source.d.ts +2 -5
  40. package/dist/sdk/sources/source.d.ts.map +1 -1
  41. package/dist/server/mcpServer.js +1 -1
  42. package/dist/server/mcpServer.js.map +1 -1
  43. package/dist/server/toolCatalog.d.ts +2 -1
  44. package/dist/server/toolCatalog.d.ts.map +1 -1
  45. package/dist/server/toolCatalog.js.map +1 -1
  46. package/dist/server/zodToJsonSchema.d.ts +1 -1
  47. package/dist/server/zodToJsonSchema.d.ts.map +1 -1
  48. package/dist/tools/describeSchema.d.ts +1 -1
  49. package/dist/tools/describeSchema.d.ts.map +1 -1
  50. package/dist/tools/describeSchema.js +1 -1
  51. package/dist/tools/describeSchema.js.map +1 -1
  52. package/dist/tools/execute.d.ts +4 -2
  53. package/dist/tools/execute.d.ts.map +1 -1
  54. package/dist/tools/execute.js +5 -4
  55. package/dist/tools/execute.js.map +1 -1
  56. package/dist/tools/searchApi.d.ts +1 -1
  57. package/dist/tools/searchApi.d.ts.map +1 -1
  58. package/dist/tools/searchApi.js +1 -1
  59. package/dist/tools/searchApi.js.map +1 -1
  60. package/dist/util/httpClient.d.ts +9 -4
  61. package/dist/util/httpClient.d.ts.map +1 -1
  62. package/dist/util/httpClient.js +1 -1
  63. package/dist/util/httpClient.js.map +1 -1
  64. package/dist/web/assets/blen-logo-BKJFojyj.png +0 -0
  65. package/dist/web/assets/index--Hn7cjpj.js +299 -0
  66. package/dist/web/assets/index-bNtg7OeV.css +2 -0
  67. package/dist/web/index.html +14 -0
  68. package/package.json +35 -7
package/CHANGELOG.md CHANGED
@@ -17,6 +17,11 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
17
17
 
18
18
 
19
19
 
20
+
21
+ ## [2.1.0] - 2026-10-13
22
+
23
+ ### Added
24
+ - add personal regulations.gov accounts with SAM.gov UX (#7) (auth)
20
25
  ## [2.0.11] - 2026-10-13
21
26
 
22
27
  ### Fixed
@@ -200,7 +205,8 @@ Initial public release.
200
205
  → notifications/initialized → tools/list → tools/call → sandbox → SDK
201
206
  → mocked upstream).
202
207
 
203
- [Unreleased]: https://github.com/blen-labs/fedreg-mcp-server/compare/v2.0.11...HEAD
208
+ [Unreleased]: https://github.com/blen-labs/fedreg-mcp-server/compare/v2.1.0...HEAD
209
+ [2.1.0]: https://github.com/blen-labs/fedreg-mcp-server/compare/v2.0.11...v2.1.0
204
210
  [2.0.11]: https://github.com/blen-labs/fedreg-mcp-server/compare/v2.0.10...v2.0.11
205
211
  [2.0.10]: https://github.com/blen-labs/fedreg-mcp-server/compare/v2.0.9...v2.0.10
206
212
  [2.0.9]: https://github.com/blen-labs/fedreg-mcp-server/compare/v2.0.8...v2.0.9
package/README.md CHANGED
@@ -3,7 +3,7 @@
3
3
  [![CI](https://github.com/blen-labs/fedreg-mcp-server/actions/workflows/ci.yml/badge.svg)](https://github.com/blen-labs/fedreg-mcp-server/actions/workflows/ci.yml)
4
4
  [![npm](https://img.shields.io/npm/v/@blen/fedreg-mcp-server.svg)](https://www.npmjs.com/package/@blen/fedreg-mcp-server)
5
5
  [![license: Apache-2.0](https://img.shields.io/badge/license-Apache--2.0-blue.svg)](./LICENSE)
6
- [![Node](https://img.shields.io/badge/node-%E2%89%A520.10-339933.svg)](https://nodejs.org/)
6
+ [![Node](https://img.shields.io/badge/node-%E2%89%A520.19-339933.svg)](https://nodejs.org/)
7
7
  [![MCP](https://img.shields.io/badge/MCP-2026--07--28-7c3aed.svg)](https://modelcontextprotocol.io/specification/2026-07-28)
8
8
 
9
9
  <p align="center">
@@ -22,6 +22,17 @@ Instead of bolting on dozens of rigid, narrow tools, it hands the model a small,
22
22
 
23
23
  > Independent open-source project. It calls public U.S. government APIs and is not affiliated with or endorsed by the U.S. government.
24
24
 
25
+ ## Personal accounts and regulations.gov keys
26
+
27
+ Hosted operators can enable `FEDREG_AUTH_PROVIDER=accounts` for Google sign-in,
28
+ a personal API-key page at `/account`, and OAuth/PKCE connections from MCP clients.
29
+ The interface matches the SAM.gov server: Google sign-in, a Connect key-entry step,
30
+ a separate Authorize screen, and account controls with the shared BLEN styling.
31
+ Keys are encrypted in PostgreSQL and selected from each caller's verified account.
32
+ Federal Register and eCFR still work without a saved key. Stdio retains its existing
33
+ `FEDREG_REGS_API_KEY` configuration. See [account setup](docs/accounts.md) for
34
+ required secrets, Google callback configuration, testing, and rollout steps.
35
+
25
36
  ## See it in action
26
37
 
27
38
  Ask Claude:
@@ -118,7 +129,7 @@ Restart Claude Desktop, then just ask in plain language:
118
129
  > *"What does 40 CFR Part 60 cover?"*
119
130
  > *"Which proposed rules opened for public comment this week?"*
120
131
 
121
- Requires **Node.js ≥ 20.10**. `npx` downloads and runs the server on demand; no global install needed. If the `execute` tool reports `SandboxUnavailable`, see [Sandbox runtimes](#sandbox-runtimes).
132
+ Requires **Node.js ≥ 20.19**. `npx` downloads and runs the server on demand; no global install needed. If the `execute` tool reports `SandboxUnavailable`, see [Sandbox runtimes](#sandbox-runtimes).
122
133
 
123
134
  ## How it works
124
135
 
@@ -0,0 +1,16 @@
1
+ import express from 'express';
2
+ import { createMcpHandler } from '@modelcontextprotocol/server';
3
+ import type { CatalogDeps } from '../server/toolCatalog.js';
4
+ import type { AccountConfig } from './config.js';
5
+ import { type Store } from './store.js';
6
+ import { type Auth } from './provider.js';
7
+ /** MCP's rule for redirect URIs: HTTPS, or plain HTTP only to this computer. */
8
+ export declare function allowedRedirect(value: unknown): boolean;
9
+ export declare function createAccountApp(config: AccountConfig, store: Store, deps: CatalogDeps, fetcher?: typeof fetch): Promise<{
10
+ app: express.Express;
11
+ auth: Auth;
12
+ mcp: ReturnType<typeof createMcpHandler>;
13
+ close: () => Promise<void>;
14
+ }>;
15
+ export declare function forwardResponse(response: Response, res: express.Response): Promise<void>;
16
+ //# sourceMappingURL=app.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"app.d.ts","sourceRoot":"","sources":["../../src/accounts/app.ts"],"names":[],"mappings":"AAAA,OAAO,OAAO,MAAM,SAAS,CAAC;AAE9B,OAAO,EAAE,gBAAgB,EAAE,MAAM,8BAA8B,CAAC;AAGhE,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,0BAA0B,CAAC;AAC5D,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AACjD,OAAO,EAAE,KAAK,KAAK,EAAE,MAAM,YAAY,CAAC;AACxC,OAAO,EAA8B,KAAK,IAAI,EAAE,MAAM,eAAe,CAAC;AAkCtE,gFAAgF;AAChF,wBAAgB,eAAe,CAAC,KAAK,EAAE,OAAO,WAQ7C;AAED,wBAAsB,gBAAgB,CACpC,MAAM,EAAE,aAAa,EACrB,KAAK,EAAE,KAAK,EACZ,IAAI,EAAE,WAAW,EACjB,OAAO,GAAE,OAAO,KAAa,GAC5B,OAAO,CAAC;IAAE,GAAG,EAAE,OAAO,CAAC,OAAO,CAAC;IAAC,IAAI,EAAE,IAAI,CAAC;IAAC,GAAG,EAAE,UAAU,CAAC,OAAO,gBAAgB,CAAC,CAAC;IAAC,KAAK,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAA;CAAE,CAAC,CAoTrH;AACD,wBAAsB,eAAe,CAAC,QAAQ,EAAE,QAAQ,EAAE,GAAG,EAAE,OAAO,CAAC,QAAQ,iBAO9E"}
@@ -0,0 +1,352 @@
1
+ import express from 'express';
2
+ import { Pool } from 'pg';
3
+ import { createMcpHandler } from '@modelcontextprotocol/server';
4
+ import { toNodeHandler } from '@modelcontextprotocol/node';
5
+ import { buildMcpServer, MCP_PROTOCOL_VERSION } from '../server/mcpServer.js';
6
+ import { createAuth, initializeAuth } from './provider.js';
7
+ import { installPortal } from './portal.js';
8
+ import { digest } from './crypto.js';
9
+ import { log } from '../util/logger.js';
10
+ import { clientIp } from '../server/ipRateLimiter.js';
11
+ import { readFile } from 'node:fs/promises';
12
+ import { fileURLToPath } from 'node:url';
13
+ import { resolve } from 'node:path';
14
+ const CROSS_ORIGIN_PATHS = /^\/(?:mcp|\.well-known\/(?:oauth-protected-resource|oauth-authorization-server|openid-configuration)(?:\/[\w-]+)*|api\/auth\/(?:oauth2\/(?:register|token|revoke)|\.well-known\/(?:oauth-authorization-server|openid-configuration)))$/;
15
+ const CORS_HEADERS = {
16
+ 'Access-Control-Allow-Origin': '*',
17
+ 'Access-Control-Allow-Methods': 'GET, POST, DELETE, OPTIONS',
18
+ 'Access-Control-Expose-Headers': 'WWW-Authenticate, Mcp-Session-Id, Mcp-Protocol-Version',
19
+ 'Access-Control-Max-Age': '600',
20
+ };
21
+ // Newer MCP clients add per-request headers (Mcp-Method, Mcp-Name, Mcp-Param-*), so preflights
22
+ // echo what was asked for; no credentials are allowed, so this grants nothing beyond the bearer.
23
+ const CORS_ALLOW_HEADERS = 'Authorization, Content-Type, Accept, Mcp-Protocol-Version, Mcp-Session-Id, Mcp-Method, Mcp-Name, Last-Event-ID';
24
+ /** Longest a token or revoke request waits for a lock connection, and then for the lock. */
25
+ const LOCK_WAIT_MS = 10_000;
26
+ const LOOPBACK = new Set(['localhost', '127.0.0.1', '[::1]']);
27
+ /** Redirect URIs in a registration body; malformed JSON is left for Better Auth to reject. */
28
+ function registeredRedirects(body) {
29
+ try {
30
+ return JSON.parse(body.toString('utf8'))?.redirect_uris ?? [];
31
+ }
32
+ catch {
33
+ return [];
34
+ }
35
+ }
36
+ /** MCP's rule for redirect URIs: HTTPS, or plain HTTP only to this computer. */
37
+ export function allowedRedirect(value) {
38
+ if (typeof value !== 'string')
39
+ return false;
40
+ try {
41
+ const url = new URL(value);
42
+ return !url.username && !url.password && !url.hash && (url.protocol === 'https:' || (url.protocol === 'http:' && LOOPBACK.has(url.hostname)));
43
+ }
44
+ catch {
45
+ return false;
46
+ }
47
+ }
48
+ export async function createAccountApp(config, store, deps, fetcher = fetch) {
49
+ const app = express();
50
+ app.disable('x-powered-by');
51
+ // Railway terminates TLS at one trusted proxy hop.
52
+ if (config.production)
53
+ app.set('trust proxy', 1);
54
+ // With `trust proxy` 1, req.ip is the rightmost X-Forwarded-For entry. Railway rebuilds that
55
+ // header as "<client>, <edge>", so req.ip is its edge, shared by many users. The header the
56
+ // proxy overwrites with the client address (Railway: x-real-ip) is used instead when set.
57
+ const ipOf = (req) => config.clientIpHeader ? clientIp(req, config.clientIpHeader) : req.ip;
58
+ const auth = createAuth(config, store);
59
+ await initializeAuth(auth, config, store);
60
+ // Each waiting token request holds a connection, so size this for concurrent token requests.
61
+ const locks = new Pool({ ...store.pool.options, max: 10, connectionTimeoutMillis: LOCK_WAIT_MS });
62
+ locks.on('error', () => log.error('account.auth_lock_connection_error'));
63
+ /**
64
+ * The advisory lock for a /oauth2/token or /oauth2/revoke body: the owner of the presented
65
+ * refresh or access token, else the presented code or token itself (an unknown token
66
+ * changes nothing, and a code is single-use). Tokens are stored as digests of their value.
67
+ */
68
+ const tokenLockId = async (body) => {
69
+ let fields = {};
70
+ const text = body?.toString('utf8').trim() ?? '';
71
+ try {
72
+ fields = text.startsWith('{') ? JSON.parse(text) : Object.fromEntries(new URLSearchParams(text));
73
+ }
74
+ catch { /* Better Auth rejects the malformed body; any lock id will do. */ }
75
+ const value = (name) => typeof fields[name] === 'string' ? fields[name] : undefined;
76
+ const token = value('refresh_token') ?? value('token');
77
+ let key = `code:${value('code') ?? ''}`;
78
+ if (token) {
79
+ const { adapter } = await auth.$context;
80
+ const where = [{ field: 'token', value: digest(token) }];
81
+ const row = await adapter.findOne({ model: 'oauthRefreshToken', where })
82
+ ?? await adapter.findOne({ model: 'oauthAccessToken', where });
83
+ key = row?.userId ? `user:${row.userId}` : `token:${digest(token)}`;
84
+ }
85
+ // pg_advisory_lock takes a signed 64-bit key.
86
+ return BigInt.asIntN(64, BigInt(`0x${digest(key).slice(0, 16)}`)).toString();
87
+ };
88
+ const mcp = createMcpHandler(ctx => buildMcpServer(deps, { subject: ctx.authInfo?.extra?.userId }), { legacy: 'stateless', maxSubscriptions: 0, onerror: () => log.warn('account.mcp_handler_error') });
89
+ const resource = `${config.origin}/mcp`;
90
+ const foreignOrigin = (req) => !!req.headers.origin && req.headers.origin !== config.origin;
91
+ const metadataUrl = `${config.origin}/.well-known/oauth-protected-resource/mcp`;
92
+ const originHost = new URL(config.origin).host;
93
+ app.use((req, res, next) => {
94
+ res.set({
95
+ 'Cache-Control': 'no-store',
96
+ 'Referrer-Policy': 'strict-origin',
97
+ 'X-Content-Type-Options': 'nosniff',
98
+ 'X-Frame-Options': 'DENY',
99
+ 'Content-Security-Policy': "default-src 'none'; style-src 'self'; form-action 'self'; frame-ancestors 'none'; base-uri 'none'",
100
+ });
101
+ if (config.production)
102
+ res.set('Strict-Transport-Security', 'max-age=31536000');
103
+ if (req.path !== '/health' && req.headers.host !== originHost)
104
+ return res.status(421).send('Invalid host');
105
+ // MCP clients, including browser-based ones and hosted connectors such as Claude, call
106
+ // discovery, registration, token and /mcp from other origins. These use bearer tokens or
107
+ // client credentials, never cookies, and the Host check above prevents DNS rebinding.
108
+ if (CROSS_ORIGIN_PATHS.test(req.path)) {
109
+ res.set(CORS_HEADERS);
110
+ if (req.method === 'OPTIONS')
111
+ return res
112
+ .status(204)
113
+ .set({
114
+ 'Access-Control-Allow-Headers': req.headers['access-control-request-headers']?.slice(0, 1024) || CORS_ALLOW_HEADERS,
115
+ Vary: 'Access-Control-Request-Headers',
116
+ })
117
+ .end();
118
+ return next();
119
+ }
120
+ // Everything else is a cookie-authenticated browser surface and stays same-origin.
121
+ if (foreignOrigin(req)) {
122
+ log.warn('account.origin_rejected');
123
+ return res.status(403).send('Invalid origin');
124
+ }
125
+ next();
126
+ });
127
+ app.get('/health', async (_req, res) => {
128
+ try {
129
+ await store.pool.query('SELECT 1');
130
+ res.json({
131
+ status: 'ok',
132
+ protocolVersion: MCP_PROTOCOL_VERSION, sandbox: deps.sandbox.kind,
133
+ });
134
+ }
135
+ catch {
136
+ res.status(503).json({ status: 'unavailable' });
137
+ }
138
+ });
139
+ const webRoot = fileURLToPath(new URL('../../dist/web/', import.meta.url));
140
+ app.use('/assets', express.static(resolve(webRoot, 'assets'), { immutable: true, maxAge: '1y', index: false }));
141
+ // Production reads the built page once; development rereads it so `pnpm dev:web` rebuilds show.
142
+ const loadIndex = () => readFile(resolve(webRoot, 'index.html'), 'utf8').catch(() => undefined);
143
+ let indexHtml;
144
+ app.get(['/', '/sign-in', '/connect', '/consent', '/account', '/privacy-policy', '/terms'], async (_req, res) => {
145
+ const html = config.production ? (indexHtml ??= await loadIndex()) : await loadIndex();
146
+ if (!html)
147
+ return res.status(503).send('The web interface is not built. Run pnpm build.');
148
+ res.set('Content-Security-Policy', "default-src 'none'; script-src 'self'; style-src 'self'; img-src 'self'; connect-src 'self'; form-action 'self'; frame-ancestors 'none'; base-uri 'none'")
149
+ .type('html').send(html);
150
+ });
151
+ const tooMany = (res) => res.status(429).set('Retry-After', '60').send('Too many requests');
152
+ app.use(async (req, res, next) => {
153
+ // Hosted connectors (ChatGPT, Claude) send every user's OAuth and MCP traffic from a few
154
+ // shared egress IPs, so those endpoints are limited elsewhere: Better Auth limits each OAuth
155
+ // endpoint, and /mcp is limited per user after the token is checked (see below).
156
+ const platform = CROSS_ORIGIN_PATHS.test(req.path) &&
157
+ (req.path !== '/mcp' || !!req.headers.authorization?.startsWith('Bearer '));
158
+ if (!platform && !(await store.allow(`http:${ipOf(req)}`, 600, 60)))
159
+ return tooMany(res);
160
+ next();
161
+ });
162
+ const mcpNode = toNodeHandler(mcp, {
163
+ onerror: () => log.warn('account.mcp_http_error'),
164
+ });
165
+ // No body parser and no size cap, matching the base HTTP transport: large `execute` scripts
166
+ // must work in both modes. The SDK reads the body only after the token checks below pass.
167
+ app.all('/mcp', async (req, res) => {
168
+ const challenge = (status, error) => res
169
+ .status(status)
170
+ .set('WWW-Authenticate', `Bearer resource_metadata="${metadataUrl}", error="${error}", scope="fedreg:read"`)
171
+ .json({ error });
172
+ const authorization = req.headers.authorization;
173
+ if (!authorization?.startsWith('Bearer ') || authorization.length > 4096)
174
+ return challenge(401, 'invalid_token');
175
+ // A flood guard sized for a connector's shared IP; the per-user limit below does the real work.
176
+ if (!(await store.allow(`mcp-ip:${ipOf(req)}`, 6000, 60)))
177
+ return tooMany(res);
178
+ const value = authorization.slice(7);
179
+ // Opaque tokens are checked against their hashed database row on every request, so
180
+ // revocation, client disabling, user deletion and loss of email verification apply immediately.
181
+ const context = await auth.$context;
182
+ const token = await context.adapter.findOne({
183
+ model: 'oauthAccessToken',
184
+ where: [{ field: 'token', value: digest(value) }],
185
+ });
186
+ if (!token?.userId ||
187
+ token.revoked ||
188
+ !(new Date(token.expiresAt).getTime() > Date.now()) ||
189
+ !(token.resources ?? []).includes(resource))
190
+ return challenge(401, 'invalid_token');
191
+ const [client, user] = await Promise.all([
192
+ context.adapter.findOne({
193
+ model: 'oauthClient',
194
+ where: [{ field: 'clientId', value: token.clientId }],
195
+ }),
196
+ context.adapter.findOne({
197
+ model: 'user',
198
+ where: [{ field: 'id', value: token.userId }],
199
+ }),
200
+ ]);
201
+ if (!client || client.disabled || !user?.emailVerified)
202
+ return challenge(401, 'invalid_token');
203
+ if (!(token.scopes ?? []).includes('fedreg:read'))
204
+ return challenge(403, 'insufficient_scope');
205
+ if (!(await store.allow(`mcp-user:${user.id}`, 600, 60)))
206
+ return tooMany(res);
207
+ const day = new Date().toISOString().slice(0, 10);
208
+ if (!await store.allow(`mcp-day:${user.id}:${day}`, config.subjectDailyQuota ?? 10000, 86400))
209
+ return tooMany(res);
210
+ Object.assign(req, {
211
+ auth: {
212
+ token: value,
213
+ clientId: token.clientId,
214
+ scopes: token.scopes,
215
+ extra: { userId: user.id },
216
+ },
217
+ });
218
+ await mcpNode(req, res);
219
+ });
220
+ // Clients look for authorization-server metadata in different places: RFC 8414 path
221
+ // insertion for the /api/auth issuer (canonical, served by Better Auth), the issuer path with
222
+ // /.well-known appended, or the origin root when a client skips protected-resource discovery.
223
+ // Serve the same document at each. The openid-configuration URLs stay 404: this is not an
224
+ // OpenID provider, and clients parse documents found there with the stricter OIDC schema.
225
+ app.get(['/.well-known/oauth-authorization-server', '/api/auth/.well-known/oauth-authorization-server'], async (_req, res) => forwardResponse(await auth.handler(new Request(`${config.origin}/.well-known/oauth-authorization-server/api/auth`)), res));
226
+ app.use(['/api/auth', '/.well-known'], express.raw({ type: () => true, limit: '64kb' }), async (req, res) => {
227
+ const headers = new Headers();
228
+ for (const [name, value] of Object.entries(req.headers))
229
+ if (value !== undefined)
230
+ headers.set(name, Array.isArray(value) ? value.join(', ') : value);
231
+ const ip = ipOf(req);
232
+ if (ip)
233
+ headers.set('x-forwarded-for', ip);
234
+ else
235
+ headers.delete('x-forwarded-for');
236
+ const authUrl = new URL(req.originalUrl, config.origin);
237
+ // WHATWG Request normalizes dot segments and backslashes. Reject aliases
238
+ // before forwarding so Express guards and Better Auth see the same path.
239
+ if (authUrl.origin !== config.origin || authUrl.pathname !== req.originalUrl.split('?')[0]) {
240
+ return res.status(404).send('Not found');
241
+ }
242
+ let body = ['GET', 'HEAD'].includes(req.method) ? undefined : req.body;
243
+ // MCP requires every redirect URI to be HTTPS or localhost. Check registrations here, and
244
+ // authorization requests too, since Client ID Metadata Documents skip registration.
245
+ const redirects = req.path === '/oauth2/register' && Buffer.isBuffer(body)
246
+ ? registeredRedirects(body)
247
+ : req.path === '/oauth2/authorize'
248
+ ? [
249
+ (req.method === 'POST' && Buffer.isBuffer(body)
250
+ ? new URLSearchParams(body.toString('utf8'))
251
+ : authUrl.searchParams).get('redirect_uri'),
252
+ ].filter(Boolean)
253
+ : [];
254
+ if (!Array.isArray(redirects) || !redirects.every(allowedRedirect))
255
+ return res.status(400).json({
256
+ error: req.path === '/oauth2/register' ? 'invalid_redirect_uri' : 'invalid_request',
257
+ error_description: 'Redirect URIs must use HTTPS or a localhost address.',
258
+ });
259
+ // Clients that omit RFC 8707 `resource` (Gemini Enterprise may) would get tokens this
260
+ // server cannot accept. It protects one resource, so default to it; the code keeps it.
261
+ if (req.path === '/oauth2/authorize') {
262
+ if (req.method === 'GET' && !authUrl.searchParams.get('resource'))
263
+ authUrl.searchParams.set('resource', resource);
264
+ if (req.method === 'POST' &&
265
+ req.is('application/x-www-form-urlencoded') &&
266
+ Buffer.isBuffer(body)) {
267
+ const form = new URLSearchParams(body.toString('utf8'));
268
+ if (!form.get('resource'))
269
+ form.set('resource', resource);
270
+ body = Buffer.from(form.toString());
271
+ headers.delete('content-length');
272
+ }
273
+ }
274
+ const request = new Request(authUrl, {
275
+ method: req.method,
276
+ headers,
277
+ body,
278
+ });
279
+ const execute = async () => {
280
+ const response = await auth.handler(request);
281
+ // Registration failures are otherwise invisible; log the outcome only, no request data.
282
+ if (req.path === '/oauth2/register')
283
+ log.info('client_registration', { status: response.status, crossOrigin: foreignOrigin(req) });
284
+ return forwardResponse(response, res);
285
+ };
286
+ if (req.path !== '/oauth2/token' && req.path !== '/oauth2/revoke')
287
+ return execute();
288
+ // Serialize token-family rotation and invalidation across replicas. Better Auth's
289
+ // current adapter performs those writes separately; overlapping requests must not
290
+ // interleave. A family is every refresh token of one (client, user), so refresh and revoke
291
+ // lock per user and a code exchange locks per code: unrelated users never queue together.
292
+ // A code exchange can still overlap a family wipe for the same user; at worst that new
293
+ // authorization is revoked and the user authorizes again.
294
+ const lockId = await tokenLockId(Buffer.isBuffer(body) ? body : undefined);
295
+ const lock = await locks.connect();
296
+ // A transaction-scoped lock is released by COMMIT or ROLLBACK, and a bounded wait keeps one
297
+ // stuck request from holding every lock connection and queueing all users' token requests.
298
+ let broken = false;
299
+ try {
300
+ await lock.query('BEGIN');
301
+ await lock.query(`SET LOCAL lock_timeout = '${LOCK_WAIT_MS}ms'`);
302
+ await lock.query('SELECT pg_advisory_xact_lock($1)', [lockId]);
303
+ await execute();
304
+ await lock.query('COMMIT');
305
+ }
306
+ catch (error) {
307
+ // A connection that cannot roll back may still hold the lock; discard it.
308
+ await lock.query('ROLLBACK').catch(() => { broken = true; });
309
+ throw error;
310
+ }
311
+ finally {
312
+ lock.release(broken);
313
+ }
314
+ });
315
+ installPortal(app, config, store, auth, fetcher, ipOf);
316
+ app.use((_req, res) => res.status(404).send('Not found'));
317
+ app.use((error, _req, res, _next) => {
318
+ if (error &&
319
+ typeof error === 'object' &&
320
+ 'type' in error &&
321
+ ['entity.too.large', 'parameters.too.many'].includes(String(error.type))) {
322
+ if (!res.headersSent)
323
+ res.status(413).send('Request too large');
324
+ return;
325
+ }
326
+ log.error('account.request_failed');
327
+ if (!res.headersSent)
328
+ res
329
+ .status(500)
330
+ .json({ error: 'Unable to complete this request. Please try again.' });
331
+ });
332
+ return {
333
+ app,
334
+ auth,
335
+ mcp,
336
+ close: async () => {
337
+ await mcp.close();
338
+ await locks.end();
339
+ },
340
+ };
341
+ }
342
+ export async function forwardResponse(response, res) {
343
+ res.status(response.status);
344
+ for (const [name, value] of response.headers)
345
+ if (name.toLowerCase() !== 'set-cookie')
346
+ res.setHeader(name, value);
347
+ const cookies = response.headers.getSetCookie();
348
+ if (cookies.length)
349
+ res.setHeader('Set-Cookie', cookies);
350
+ res.send(Buffer.from(await response.arrayBuffer()));
351
+ }
352
+ //# sourceMappingURL=app.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"app.js","sourceRoot":"","sources":["../../src/accounts/app.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,16 @@
1
+ import { Keyring } from './crypto.js';
2
+ export interface AccountConfig {
3
+ origin: string;
4
+ production: boolean;
5
+ databaseUrl: string;
6
+ authSecret: string;
7
+ googleClientId: string;
8
+ googleClientSecret: string;
9
+ keyring: Keyring;
10
+ subjectDailyQuota?: number;
11
+ /** Header the reverse proxy overwrites with the client address (Railway: x-real-ip). */
12
+ clientIpHeader?: string;
13
+ }
14
+ /** `env` is passed in by src/bin.ts, the only module that reads process.env. */
15
+ export declare function loadAccountConfig(env: Record<string, string | undefined>): AccountConfig;
16
+ //# sourceMappingURL=config.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../../src/accounts/config.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,aAAa,CAAC;AAEtC,MAAM,WAAW,aAAa;IAC5B,MAAM,EAAE,MAAM,CAAC;IACf,UAAU,EAAE,OAAO,CAAC;IACpB,WAAW,EAAE,MAAM,CAAC;IACpB,UAAU,EAAE,MAAM,CAAC;IACnB,cAAc,EAAE,MAAM,CAAC;IACvB,kBAAkB,EAAE,MAAM,CAAC;IAC3B,OAAO,EAAE,OAAO,CAAC;IACjB,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,wFAAwF;IACxF,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB;AAED,gFAAgF;AAChF,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,GAAG,aAAa,CAoCxF"}
@@ -0,0 +1,50 @@
1
+ import { Keyring } from './crypto.js';
2
+ /** `env` is passed in by src/bin.ts, the only module that reads process.env. */
3
+ export function loadAccountConfig(env) {
4
+ const required = (name) => {
5
+ const value = env[name];
6
+ if (!value || value.includes('REPLACE_WITH_'))
7
+ throw new Error(`Missing configuration: ${name}`);
8
+ return value;
9
+ };
10
+ const origin = new URL(required('FEDREG_PUBLIC_ORIGIN'));
11
+ const production = env.NODE_ENV !== 'development' && env.NODE_ENV !== 'test';
12
+ if (origin.pathname !== '/' || origin.search || origin.hash || origin.username || origin.password ||
13
+ (origin.protocol !== 'https:' && (production || origin.protocol !== 'http:' ||
14
+ !['localhost', '127.0.0.1', '[::1]'].includes(origin.hostname)))) {
15
+ throw new Error('FEDREG_PUBLIC_ORIGIN must be an HTTPS origin (HTTP loopback allowed in development)');
16
+ }
17
+ const authSecret = required('BETTER_AUTH_SECRET');
18
+ if (authSecret.length < 32)
19
+ throw new Error('BETTER_AUTH_SECRET must have at least 32 characters');
20
+ let encoded;
21
+ const encodedKeys = required('ENCRYPTION_KEYS_JSON');
22
+ try {
23
+ encoded = JSON.parse(encodedKeys);
24
+ }
25
+ catch {
26
+ throw new Error('ENCRYPTION_KEYS_JSON must be valid JSON');
27
+ }
28
+ if (!encoded || typeof encoded !== 'object' || Array.isArray(encoded))
29
+ throw new Error('Invalid encryption keyring');
30
+ const keys = Object.fromEntries(Object.entries(encoded).map(([id, value]) => {
31
+ if (typeof value !== 'string' || !/^[A-Za-z0-9+/]{43}=$/.test(value))
32
+ throw new Error('Invalid base64 encryption key');
33
+ return [id, Buffer.from(value, 'base64')];
34
+ }));
35
+ const subjectDailyQuota = Number(env.FEDREG_SUBJECT_DAILY_QUOTA ?? 10000);
36
+ if (!Number.isInteger(subjectDailyQuota) || subjectDailyQuota < 1)
37
+ throw new Error('FEDREG_SUBJECT_DAILY_QUOTA must be a positive integer');
38
+ const clientIpHeader = env.FEDREG_CLIENT_IP_HEADER?.trim().toLowerCase() || undefined;
39
+ if (clientIpHeader && !/^[a-z0-9-]+$/.test(clientIpHeader))
40
+ throw new Error('FEDREG_CLIENT_IP_HEADER must be a header name');
41
+ return {
42
+ subjectDailyQuota, clientIpHeader,
43
+ origin: origin.origin, production, authSecret,
44
+ databaseUrl: required('DATABASE_URL'),
45
+ googleClientId: required('GOOGLE_CLIENT_ID'),
46
+ googleClientSecret: required('GOOGLE_CLIENT_SECRET'),
47
+ keyring: new Keyring(required('ENCRYPTION_ACTIVE_KEY_ID'), keys),
48
+ };
49
+ }
50
+ //# sourceMappingURL=config.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config.js","sourceRoot":"","sources":["../../src/accounts/config.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,18 @@
1
+ export interface Sealed {
2
+ v: 1;
3
+ kid: string;
4
+ iv: string;
5
+ tag: string;
6
+ data: string;
7
+ }
8
+ /** AEAD binds each ciphertext to its purpose and owner, preventing row swapping. */
9
+ export declare class Keyring {
10
+ readonly activeId: string;
11
+ private readonly keys;
12
+ constructor(activeId: string, keys: Record<string, Buffer>);
13
+ seal(value: string, context: string): Sealed;
14
+ open(value: Sealed, context: string): string;
15
+ }
16
+ export declare const digest: (value: string) => string;
17
+ export declare const randomToken: () => string;
18
+ //# sourceMappingURL=crypto.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"crypto.d.ts","sourceRoot":"","sources":["../../src/accounts/crypto.ts"],"names":[],"mappings":"AAEA,MAAM,WAAW,MAAM;IACrB,CAAC,EAAE,CAAC,CAAC;IACL,GAAG,EAAE,MAAM,CAAC;IACZ,EAAE,EAAE,MAAM,CAAC;IACX,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,MAAM,CAAC;CACd;AAID,oFAAoF;AACpF,qBAAa,OAAO;IAEhB,QAAQ,CAAC,QAAQ,EAAE,MAAM;IACzB,OAAO,CAAC,QAAQ,CAAC,IAAI;gBADZ,QAAQ,EAAE,MAAM,EACR,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC;IAM/C,IAAI,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM;IAa5C,IAAI,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM;CAY7C;AAED,eAAO,MAAM,MAAM,GAAI,OAAO,MAAM,WAAqD,CAAC;AAC1F,eAAO,MAAM,WAAW,cAA8C,CAAC"}
@@ -0,0 +1,43 @@
1
+ import { createCipheriv, createDecipheriv, createHash, randomBytes } from 'node:crypto';
2
+ const TAG_BYTES = 16;
3
+ /** AEAD binds each ciphertext to its purpose and owner, preventing row swapping. */
4
+ export class Keyring {
5
+ activeId;
6
+ keys;
7
+ constructor(activeId, keys) {
8
+ this.activeId = activeId;
9
+ this.keys = keys;
10
+ if (!Object.hasOwn(keys, activeId) || Object.values(keys).some((key) => key.length !== 32)) {
11
+ throw new Error('Encryption keys must be 32 bytes; active key must exist');
12
+ }
13
+ }
14
+ seal(value, context) {
15
+ const iv = randomBytes(12);
16
+ const cipher = createCipheriv('aes-256-gcm', this.keys[this.activeId], iv, { authTagLength: TAG_BYTES });
17
+ cipher.setAAD(Buffer.from(JSON.stringify([1, this.activeId, context])));
18
+ const encrypted = Buffer.concat([cipher.update(value, 'utf8'), cipher.final()]);
19
+ return {
20
+ v: 1,
21
+ kid: this.activeId,
22
+ iv: iv.toString('base64'),
23
+ tag: cipher.getAuthTag().toString('base64'),
24
+ data: encrypted.toString('base64'),
25
+ };
26
+ }
27
+ open(value, context) {
28
+ const key = Object.hasOwn(this.keys, value.kid) ? this.keys[value.kid] : undefined;
29
+ if (value.v !== 1 || !key)
30
+ throw new Error('Unsupported ciphertext');
31
+ // A fixed tag length: GCM otherwise accepts truncated tags as short as 4 bytes.
32
+ const decipher = createDecipheriv('aes-256-gcm', key, Buffer.from(value.iv, 'base64'), { authTagLength: TAG_BYTES });
33
+ decipher.setAAD(Buffer.from(JSON.stringify([1, value.kid, context])));
34
+ decipher.setAuthTag(Buffer.from(value.tag, 'base64'));
35
+ return Buffer.concat([
36
+ decipher.update(Buffer.from(value.data, 'base64')),
37
+ decipher.final(),
38
+ ]).toString('utf8');
39
+ }
40
+ }
41
+ export const digest = (value) => createHash('sha256').update(value).digest('hex');
42
+ export const randomToken = () => randomBytes(32).toString('base64url');
43
+ //# sourceMappingURL=crypto.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"crypto.js","sourceRoot":"","sources":["../../src/accounts/crypto.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,8 @@
1
+ import { type Express, type Request } from 'express';
2
+ import type { AccountConfig } from './config.js';
3
+ import { type Store } from './store.js';
4
+ import type { Auth } from './provider.js';
5
+ /** Keys go only to this fixed upstream; no URL, owner or user ID comes from the form. */
6
+ export declare function checkKey(key: unknown, fetcher?: typeof fetch): Promise<string | undefined>;
7
+ export declare function installPortal(app: Express, config: AccountConfig, store: Store, auth: Auth, fetcher: typeof fetch, ipOf: (req: Request) => string | undefined): void;
8
+ //# sourceMappingURL=portal.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"portal.d.ts","sourceRoot":"","sources":["../../src/accounts/portal.ts"],"names":[],"mappings":"AAAA,OAAgB,EAAE,KAAK,OAAO,EAAE,KAAK,OAAO,EAAiB,MAAM,SAAS,CAAC;AAG7E,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AACjD,OAAO,EAAiB,KAAK,KAAK,EAAE,MAAM,YAAY,CAAC;AACvD,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,eAAe,CAAC;AAE1C,yFAAyF;AACzF,wBAAsB,QAAQ,CAAC,GAAG,EAAE,OAAO,EAAE,OAAO,GAAE,OAAO,KAAa,GAAG,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAavG;AAED,wBAAgB,aAAa,CAC3B,GAAG,EAAE,OAAO,EAAE,MAAM,EAAE,aAAa,EAAE,KAAK,EAAE,KAAK,EAAE,IAAI,EAAE,IAAI,EAAE,OAAO,EAAE,OAAO,KAAK,EACpF,IAAI,EAAE,CAAC,GAAG,EAAE,OAAO,KAAK,MAAM,GAAG,SAAS,QA8H3C"}