@boxxy/auth-oauth 0.0.1 → 0.2.2

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 (142) hide show
  1. package/README.md +47 -2
  2. package/dist/auth/answer-authorize-request.d.ts +2 -0
  3. package/dist/auth/answer-consent-request.d.ts +10 -0
  4. package/dist/auth/answer-enroll-request.d.ts +9 -0
  5. package/dist/auth/answer-login-request.d.ts +9 -0
  6. package/dist/auth/approval-window-seconds.d.ts +1 -0
  7. package/dist/auth/build-consent-binding.d.ts +1 -0
  8. package/dist/auth/create-approval-state.d.ts +41 -0
  9. package/dist/auth/create-auth-admin.d.ts +33 -0
  10. package/dist/auth/create-enrollment-state.d.ts +6 -0
  11. package/dist/auth/create-enrollment-token.d.ts +6 -0
  12. package/dist/auth/create-owner-plugin.d.ts +38 -0
  13. package/dist/auth/create-passkey.d.ts +11 -0
  14. package/dist/auth/create-security-event-log.d.ts +12 -0
  15. package/dist/auth/derive-token-hash.d.ts +1 -0
  16. package/dist/auth/format-security-event.d.ts +7 -0
  17. package/dist/auth/is-allowed-redirect-uri.d.ts +1 -0
  18. package/dist/auth/load-web-authn.d.ts +6 -0
  19. package/dist/auth/mcp-auth-database.d.ts +5 -0
  20. package/dist/auth/mcp-auth-schema.d.ts +54 -0
  21. package/dist/auth/normalize-client-name.d.ts +1 -0
  22. package/dist/auth/open-auth-database.d.ts +2 -0
  23. package/dist/auth/open-mcp-auth.d.ts +80 -0
  24. package/dist/auth/passkey-script.d.ts +4 -0
  25. package/dist/auth/read-client-name.d.ts +3 -0
  26. package/dist/auth/read-client-views.d.ts +9 -0
  27. package/dist/auth/read-enrollment-token.d.ts +3 -0
  28. package/dist/auth/read-grant-views.d.ts +12 -0
  29. package/dist/auth/read-owner-session-id.d.ts +2 -0
  30. package/dist/auth/record-refused-exchange.d.ts +10 -0
  31. package/dist/auth/remove-client.d.ts +3 -0
  32. package/dist/auth/remove-owner-session.d.ts +3 -0
  33. package/dist/auth/render-consent-page.d.ts +9 -0
  34. package/dist/auth/render-enroll-page.d.ts +10 -0
  35. package/dist/auth/render-login-page.d.ts +9 -0
  36. package/dist/auth/revoke-grant.d.ts +3 -0
  37. package/dist/auth/to-html-text.d.ts +1 -0
  38. package/dist/auth/to-script-json.d.ts +1 -0
  39. package/dist/auth/types.d.ts +79 -0
  40. package/dist/auth/update-passkey-use.d.ts +10 -0
  41. package/dist/auth/verify-oauth-query.d.ts +1 -0
  42. package/dist/auth/verify-passkey-assertion.d.ts +11 -0
  43. package/dist/auth/verify-passkey-registration.d.ts +14 -0
  44. package/dist/auth/verify-session-passkey.d.ts +8 -0
  45. package/dist/auth/write-grant-use.d.ts +3 -0
  46. package/dist/http/answer-mcp-request.d.ts +9 -0
  47. package/dist/http/build-page-response.d.ts +5 -0
  48. package/dist/http/create-mcp-http-app.d.ts +92 -0
  49. package/dist/http/create-public-door.d.ts +20 -0
  50. package/dist/http/is-loopback-host.d.ts +1 -0
  51. package/dist/http/normalize-public-url.d.ts +1 -0
  52. package/dist/http/pick-error-message.d.ts +1 -0
  53. package/dist/http/public-door-paths.d.ts +17 -0
  54. package/dist/http/read-token-access.d.ts +8 -0
  55. package/dist/http/verify-access-token.d.ts +2 -0
  56. package/dist/index.d.ts +8 -0
  57. package/dist/test-utils/authorize-test-client.d.ts +13 -0
  58. package/dist/test-utils/build-mock-new-passkey.d.ts +2 -0
  59. package/dist/test-utils/build-passkey-seeder.d.ts +5 -0
  60. package/dist/test-utils/build-seeder.d.ts +5 -0
  61. package/dist/test-utils/build-stub-authenticator.d.ts +48 -0
  62. package/dist/test-utils/build-stub-clock.d.ts +7 -0
  63. package/dist/test-utils/load-web-authn-early.d.ts +1 -0
  64. package/dist/test-utils/register-test-cleanup.d.ts +1 -0
  65. package/dist/test-utils/seed-passkey.d.ts +4 -0
  66. package/dist/test-utils/setup-temp-dir.d.ts +6 -0
  67. package/dist/test-utils/start-stub-telemetry-collector.d.ts +5 -0
  68. package/dist/test-utils/types.d.ts +7 -0
  69. package/dist/utils/is-record.d.ts +1 -0
  70. package/dist/utils/parse-json-strings.d.ts +1 -0
  71. package/dist/utils/types.d.ts +4 -0
  72. package/package.json +42 -1
  73. package/src/auth/answer-authorize-request.ts +137 -0
  74. package/src/auth/answer-consent-request.ts +189 -0
  75. package/src/auth/answer-enroll-request.ts +190 -0
  76. package/src/auth/answer-login-request.ts +256 -0
  77. package/src/auth/approval-window-seconds.ts +5 -0
  78. package/src/auth/build-consent-binding.ts +13 -0
  79. package/src/auth/create-approval-state.ts +221 -0
  80. package/src/auth/create-auth-admin.ts +78 -0
  81. package/src/auth/create-enrollment-state.ts +56 -0
  82. package/src/auth/create-enrollment-token.ts +28 -0
  83. package/src/auth/create-owner-plugin.ts +113 -0
  84. package/src/auth/create-passkey.ts +48 -0
  85. package/src/auth/create-security-event-log.ts +97 -0
  86. package/src/auth/derive-token-hash.ts +7 -0
  87. package/src/auth/format-security-event.ts +72 -0
  88. package/src/auth/is-allowed-redirect-uri.ts +19 -0
  89. package/src/auth/load-web-authn.ts +19 -0
  90. package/src/auth/mcp-auth-database.ts +10 -0
  91. package/src/auth/mcp-auth-schema.ts +56 -0
  92. package/src/auth/normalize-client-name.ts +22 -0
  93. package/src/auth/open-auth-database.ts +22 -0
  94. package/src/auth/open-mcp-auth.ts +138 -0
  95. package/src/auth/passkey-script.ts +50 -0
  96. package/src/auth/read-client-name.ts +17 -0
  97. package/src/auth/read-client-views.ts +27 -0
  98. package/src/auth/read-enrollment-token.ts +20 -0
  99. package/src/auth/read-grant-views.ts +53 -0
  100. package/src/auth/read-owner-session-id.ts +16 -0
  101. package/src/auth/record-refused-exchange.ts +74 -0
  102. package/src/auth/remove-client.ts +33 -0
  103. package/src/auth/remove-owner-session.ts +11 -0
  104. package/src/auth/render-consent-page.ts +61 -0
  105. package/src/auth/render-enroll-page.ts +59 -0
  106. package/src/auth/render-login-page.ts +58 -0
  107. package/src/auth/revoke-grant.ts +25 -0
  108. package/src/auth/to-html-text.ts +10 -0
  109. package/src/auth/to-script-json.ts +8 -0
  110. package/src/auth/types.ts +115 -0
  111. package/src/auth/update-passkey-use.ts +31 -0
  112. package/src/auth/verify-oauth-query.ts +41 -0
  113. package/src/auth/verify-passkey-assertion.ts +124 -0
  114. package/src/auth/verify-passkey-registration.ts +118 -0
  115. package/src/auth/verify-session-passkey.ts +47 -0
  116. package/src/auth/write-grant-use.ts +18 -0
  117. package/src/http/answer-mcp-request.ts +77 -0
  118. package/src/http/build-page-response.ts +26 -0
  119. package/src/http/create-mcp-http-app.ts +340 -0
  120. package/src/http/create-public-door.ts +150 -0
  121. package/src/http/is-loopback-host.ts +10 -0
  122. package/src/http/normalize-public-url.ts +29 -0
  123. package/src/http/pick-error-message.ts +21 -0
  124. package/src/http/public-door-paths.ts +21 -0
  125. package/src/http/read-token-access.ts +44 -0
  126. package/src/http/verify-access-token.ts +46 -0
  127. package/src/index.ts +8 -0
  128. package/src/test-utils/authorize-test-client.ts +194 -0
  129. package/src/test-utils/build-mock-new-passkey.ts +15 -0
  130. package/src/test-utils/build-passkey-seeder.ts +11 -0
  131. package/src/test-utils/build-seeder.ts +10 -0
  132. package/src/test-utils/build-stub-authenticator.ts +244 -0
  133. package/src/test-utils/build-stub-clock.ts +65 -0
  134. package/src/test-utils/load-web-authn-early.ts +8 -0
  135. package/src/test-utils/register-test-cleanup.ts +55 -0
  136. package/src/test-utils/seed-passkey.ts +32 -0
  137. package/src/test-utils/setup-temp-dir.ts +24 -0
  138. package/src/test-utils/start-stub-telemetry-collector.ts +27 -0
  139. package/src/test-utils/types.ts +11 -0
  140. package/src/utils/is-record.ts +5 -0
  141. package/src/utils/parse-json-strings.ts +15 -0
  142. package/src/utils/types.ts +8 -0
@@ -0,0 +1,14 @@
1
+ interface RegistrationCheck {
2
+ readonly credential: string;
3
+ readonly challenge: string;
4
+ readonly origin: string;
5
+ readonly rpID: string;
6
+ }
7
+ export interface RegisteredPasskey {
8
+ readonly id: string;
9
+ readonly publicKey: Uint8Array;
10
+ readonly counter: number;
11
+ readonly transports: readonly string[];
12
+ }
13
+ export declare function verifyPasskeyRegistration(check: RegistrationCheck): Promise<RegisteredPasskey | null>;
14
+ export {};
@@ -0,0 +1,8 @@
1
+ import type { Kysely } from 'kysely';
2
+ import type { MCPAuthSchema } from './mcp-auth-schema.ts';
3
+ interface SessionPasskey {
4
+ readonly sessionID: string;
5
+ readonly passkeyID: string;
6
+ }
7
+ export declare function verifySessionPasskey(db: Kysely<MCPAuthSchema>, check: SessionPasskey): Promise<boolean>;
8
+ export {};
@@ -0,0 +1,3 @@
1
+ import type { Kysely } from 'kysely';
2
+ import type { MCPAuthSchema } from './mcp-auth-schema.ts';
3
+ export declare function writeGrantUse(db: Kysely<MCPAuthSchema>, grantID: string, now: number): Promise<void>;
@@ -0,0 +1,9 @@
1
+ import type { HTTPServerContext, Requester } from '../auth/types.ts';
2
+ export interface McpAuthInfo {
3
+ readonly token: string;
4
+ readonly clientId: string;
5
+ readonly scopes: string[];
6
+ readonly resourceMetadataUrl: string;
7
+ }
8
+ export type McpServe = (request: Request, authInfo: McpAuthInfo) => Promise<Response>;
9
+ export declare function answerMCPRequest(ctx: HTTPServerContext, request: Request, requester: Requester, serve: McpServe): Promise<Response>;
@@ -0,0 +1,5 @@
1
+ export declare function buildPageResponse(status: number, body: {
2
+ readonly page: string;
3
+ } | {
4
+ readonly message: string;
5
+ }, formTargets?: readonly string[], scriptSources?: readonly string[]): Response;
@@ -0,0 +1,92 @@
1
+ import type { HTTPServerContext, Requester, SecurityEvent } from '../auth/types.ts';
2
+ import type { McpServe } from './answer-mcp-request.ts';
3
+ interface MCPHTTPAppOptions {
4
+ readonly hosts: ReadonlySet<string>;
5
+ readonly origins: ReadonlySet<string>;
6
+ readonly readContext: () => HTTPServerContext | null;
7
+ readonly readRequester: (request: Request) => Requester;
8
+ readonly serveMCP: McpServe;
9
+ readonly security: {
10
+ readonly record: (event: SecurityEvent) => void;
11
+ };
12
+ }
13
+ interface RouteContext {
14
+ readonly request: Request;
15
+ }
16
+ export declare function createMCPHTTPApp(options: MCPHTTPAppOptions): import("elysia/types").AddRoute<"", "local", import("elysia").DefaultSingleton, {
17
+ typebox: {};
18
+ error: [];
19
+ }, import("elysia").DefaultMetadata, {
20
+ oauth2: {
21
+ revoke: {
22
+ post: {
23
+ body: unknown;
24
+ params: {};
25
+ query: unknown;
26
+ headers: unknown;
27
+ response: {
28
+ 200: Response;
29
+ };
30
+ error: never;
31
+ };
32
+ };
33
+ };
34
+ } & {
35
+ ".well-known": {
36
+ "oauth-authorization-server": {
37
+ get: {
38
+ body: unknown;
39
+ params: {};
40
+ query: unknown;
41
+ headers: unknown;
42
+ response: {
43
+ 200: Response;
44
+ };
45
+ error: never;
46
+ };
47
+ };
48
+ };
49
+ } & {
50
+ oauth2: {
51
+ authorize: {
52
+ get: {
53
+ body: unknown;
54
+ params: {};
55
+ query: unknown;
56
+ headers: unknown;
57
+ response: {
58
+ 200: Response;
59
+ };
60
+ error: never;
61
+ };
62
+ };
63
+ };
64
+ } & {
65
+ oauth2: {
66
+ token: {
67
+ post: {
68
+ body: unknown;
69
+ params: {};
70
+ query: unknown;
71
+ headers: unknown;
72
+ response: {
73
+ 200: Response;
74
+ };
75
+ error: never;
76
+ };
77
+ };
78
+ };
79
+ }, import("elysia").DefaultEphemeral, {
80
+ derive: {};
81
+ schema: {};
82
+ schemas: {};
83
+ response: import("elysia/types").ExtractErrorFromHandle<Response>;
84
+ error: [{
85
+ error: Error;
86
+ response: {};
87
+ passes: false;
88
+ '~return': Response;
89
+ '~catchAll': true;
90
+ }];
91
+ }, "get", "/error", import("elysia/types").IntersectIfObjectSchema<import("elysia").UnwrapRoute<{}, {}, "/error">, import("elysia/types").MergeScopedSchemas<{}, {}, {}>>, {}, (context: RouteContext) => Response>;
92
+ export {};
@@ -0,0 +1,20 @@
1
+ import type { AuthAdmin } from '../auth/create-auth-admin.ts';
2
+ import type { AuthClock } from '../utils/types.ts';
3
+ import type { McpServe } from './answer-mcp-request.ts';
4
+ export interface PublicDoorOptions {
5
+ readonly publicURL: string;
6
+ readonly hostname: string;
7
+ readonly dbPath: string;
8
+ readonly trustForwardedFrom: readonly string[];
9
+ readonly writeSecurityLine: (line: string) => void;
10
+ readonly readPeer: (request: Request) => string | null;
11
+ readonly serveMCP: McpServe;
12
+ readonly clock?: AuthClock;
13
+ }
14
+ export interface PublicDoor {
15
+ readonly answerRequest: (request: Request) => Promise<Response>;
16
+ readonly addLocalPort: (port: number) => void;
17
+ readonly admin: AuthAdmin;
18
+ readonly stop: () => Promise<void>;
19
+ }
20
+ export declare function createPublicDoor(options: PublicDoorOptions): Promise<PublicDoor>;
@@ -0,0 +1 @@
1
+ export declare function isLoopbackHost(host: string): boolean;
@@ -0,0 +1 @@
1
+ export declare function normalizePublicURL(raw: string): string;
@@ -0,0 +1 @@
1
+ export declare function pickErrorMessage(code: string | null): string;
@@ -0,0 +1,17 @@
1
+ export declare const PUBLIC_DOOR_PATHS: {
2
+ readonly browser: {
3
+ readonly authorize: '/oauth2/authorize';
4
+ readonly login: '/login';
5
+ readonly enroll: '/enroll';
6
+ readonly consent: '/consent';
7
+ readonly error: '/error';
8
+ };
9
+ readonly machine: {
10
+ readonly mcp: '/mcp';
11
+ readonly token: '/oauth2/token';
12
+ readonly revoke: '/oauth2/revoke';
13
+ readonly authorizationServerMetadata: '/.well-known/oauth-authorization-server';
14
+ readonly resourceMetadata: '/.well-known/oauth-protected-resource';
15
+ readonly mcpResourceMetadata: '/.well-known/oauth-protected-resource/mcp';
16
+ };
17
+ };
@@ -0,0 +1,8 @@
1
+ import type { GrantScope } from '@boxxy/contract';
2
+ import type { HTTPServerContext } from '../auth/types.ts';
3
+ interface TokenAccess {
4
+ readonly clientID: string;
5
+ readonly scopes: readonly GrantScope[];
6
+ }
7
+ export declare function readTokenAccess(ctx: HTTPServerContext, token: string): Promise<TokenAccess | null>;
8
+ export {};
@@ -0,0 +1,2 @@
1
+ import type { HTTPServerContext, VerifiedAccess } from '../auth/types.ts';
2
+ export declare function verifyAccessToken(ctx: HTTPServerContext, token: string): Promise<VerifiedAccess | null>;
@@ -0,0 +1,8 @@
1
+ export type { AddClientAnswer, AuthAdmin, EnrollmentLink } from './auth/create-auth-admin.ts';
2
+ export type { McpAuthInfo, McpServe } from './http/answer-mcp-request.ts';
3
+ export type { PublicDoor, PublicDoorOptions } from './http/create-public-door.ts';
4
+ export { createPublicDoor } from './http/create-public-door.ts';
5
+ export type { ClientView } from './auth/read-client-views.ts';
6
+ export type { GrantView } from './auth/read-grant-views.ts';
7
+ export type { VerifiedAccess } from './auth/types.ts';
8
+ export { isLoopbackHost } from './http/is-loopback-host.ts';
@@ -0,0 +1,13 @@
1
+ import type { PublicDoor } from '../http/create-public-door.ts';
2
+ interface AuthorizeInput {
3
+ readonly origin: string;
4
+ readonly requested: readonly string[];
5
+ readonly ticked: readonly string[];
6
+ }
7
+ export interface TestGrant {
8
+ readonly clientID: string;
9
+ readonly accessToken: string;
10
+ readonly refreshToken: string;
11
+ }
12
+ export declare function authorizeTestClient(door: PublicDoor, input: AuthorizeInput): Promise<TestGrant>;
13
+ export {};
@@ -0,0 +1,2 @@
1
+ import type { NewPasskey } from '../auth/create-passkey.ts';
2
+ export declare function buildMockNewPasskey(overrides?: Partial<NewPasskey>): NewPasskey;
@@ -0,0 +1,5 @@
1
+ import type { PasskeyInput } from './seed-passkey.ts';
2
+ import type { MCPAuthSeedDeps } from './types.ts';
3
+ export declare function buildPasskeySeeder(deps: MCPAuthSeedDeps): {
4
+ passkey: (input?: PasskeyInput) => Promise<import("../auth/create-passkey.ts").NewPasskey>;
5
+ };
@@ -0,0 +1,5 @@
1
+ import type { MCPAuthSeedDeps } from './types.ts';
2
+ export type TestDeps = MCPAuthSeedDeps;
3
+ export declare function buildSeeder(deps: TestDeps): {
4
+ passkey: (input?: import("./seed-passkey.ts").PasskeyInput) => Promise<import("../auth/create-passkey.ts").NewPasskey>;
5
+ };
@@ -0,0 +1,48 @@
1
+ interface CreationOptions {
2
+ readonly challenge: string;
3
+ readonly rp: {
4
+ readonly id?: string;
5
+ };
6
+ readonly user: {
7
+ readonly id: string;
8
+ };
9
+ }
10
+ interface RequestOptions {
11
+ readonly challenge: string;
12
+ readonly rpId?: string;
13
+ }
14
+ interface Ceremony {
15
+ readonly origin: string;
16
+ readonly rpID?: string;
17
+ readonly userVerified?: boolean;
18
+ }
19
+ export interface StubRegistration {
20
+ readonly id: string;
21
+ readonly rawId: string;
22
+ readonly type: 'public-key';
23
+ readonly response: {
24
+ readonly clientDataJSON: string;
25
+ readonly attestationObject: string;
26
+ readonly transports: readonly string[];
27
+ };
28
+ readonly clientExtensionResults: Readonly<Record<string, never>>;
29
+ readonly authenticatorAttachment: 'platform';
30
+ }
31
+ export interface StubAssertion {
32
+ readonly id: string;
33
+ readonly rawId: string;
34
+ readonly type: 'public-key';
35
+ readonly response: {
36
+ readonly clientDataJSON: string;
37
+ readonly authenticatorData: string;
38
+ readonly signature: string;
39
+ readonly userHandle: string;
40
+ };
41
+ readonly clientExtensionResults: Readonly<Record<string, never>>;
42
+ }
43
+ export declare function buildStubAuthenticator(): {
44
+ credentialID: string;
45
+ create(options: CreationOptions, ceremony: Ceremony): Promise<StubRegistration>;
46
+ signAssertion(options: RequestOptions, ceremony: Ceremony): Promise<StubAssertion>;
47
+ };
48
+ export {};
@@ -0,0 +1,7 @@
1
+ export declare function buildStubClock(startMs: number): {
2
+ now: () => number;
3
+ schedule: (callback: () => void, delayMs: number) => () => void;
4
+ advance(durationMs: number): void;
5
+ collectPending: () => number[];
6
+ collectDelays: () => number[];
7
+ };
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1 @@
1
+ export declare function registerTestCleanup<T>(release: () => T): () => T;
@@ -0,0 +1,4 @@
1
+ import type { NewPasskey } from '../auth/create-passkey.ts';
2
+ import type { MCPAuthSeedDeps } from './types.ts';
3
+ export type PasskeyInput = Partial<Omit<NewPasskey, 'tokenHash'>>;
4
+ export declare function seedPasskey(deps: MCPAuthSeedDeps, input?: PasskeyInput): Promise<NewPasskey>;
@@ -0,0 +1,6 @@
1
+ interface TempDir {
2
+ readonly path: string;
3
+ readonly teardown: () => void;
4
+ }
5
+ export declare function setupTempDir(prefix: string): TempDir;
6
+ export {};
@@ -0,0 +1,5 @@
1
+ export declare function startStubTelemetryCollector(): {
2
+ url: string;
3
+ received: string[];
4
+ stop: () => Promise<void>;
5
+ };
@@ -0,0 +1,7 @@
1
+ import type { Kysely } from 'kysely';
2
+ import type { MCPAuthSchema } from '../auth/mcp-auth-schema.ts';
3
+ import type { AuthClock } from '../utils/types.ts';
4
+ export interface MCPAuthSeedDeps {
5
+ readonly db: Kysely<MCPAuthSchema>;
6
+ readonly clock: Pick<AuthClock, 'now'>;
7
+ }
@@ -0,0 +1 @@
1
+ export declare function isRecord(value: unknown): value is Record<string, unknown>;
@@ -0,0 +1 @@
1
+ export declare function parseJSONStrings(raw: string): string[] | null;
@@ -0,0 +1,4 @@
1
+ export interface AuthClock {
2
+ readonly now: () => number;
3
+ readonly schedule: (callback: () => void, delayMs: number) => () => void;
4
+ }
package/package.json CHANGED
@@ -1,13 +1,54 @@
1
1
  {
2
2
  "name": "@boxxy/auth-oauth",
3
- "version": "0.0.1",
3
+ "version": "0.2.2",
4
+ "description": "boxxy's public door: an OAuth server for the owner's MCP endpoint, with passkey sign-in",
5
+ "homepage": "https://github.com/zgeoff/boxxy/tree/main/adapters/auth-oauth#readme",
6
+ "bugs": "https://github.com/zgeoff/boxxy/issues",
4
7
  "license": "MIT",
5
8
  "repository": {
6
9
  "type": "git",
7
10
  "url": "git+https://github.com/zgeoff/boxxy.git",
8
11
  "directory": "adapters/auth-oauth"
9
12
  },
13
+ "files": [
14
+ "src",
15
+ "!src/**/*.test.ts",
16
+ "dist"
17
+ ],
18
+ "type": "module",
19
+ "exports": {
20
+ ".": {
21
+ "types": "./dist/index.d.ts",
22
+ "default": "./src/index.ts"
23
+ },
24
+ "./test-utils/*": "./src/test-utils/*.ts"
25
+ },
10
26
  "publishConfig": {
11
27
  "access": "public"
28
+ },
29
+ "scripts": {
30
+ "build": "rm -rf dist && tsrs -p tsconfig.build.json",
31
+ "typecheck": "tsrs --noEmit"
32
+ },
33
+ "dependencies": {
34
+ "@better-auth/mcp": "1.7.15",
35
+ "@better-auth/oauth-provider": "1.7.15",
36
+ "@boxxy/contract": "0.2.2",
37
+ "@simplewebauthn/server": "14.0.3",
38
+ "@zgeoff/kysely-bun-sqlite": "0.3.2",
39
+ "better-auth": "1.7.15",
40
+ "elysia": "2.0.0-rc.0",
41
+ "exact-mirror": "1.2.6",
42
+ "kysely": "0.29.8",
43
+ "openapi-types": "12.1.3",
44
+ "reflect-metadata": "0.2.2",
45
+ "tiny-invariant": "1.3.3",
46
+ "ts-pattern": "5.9.2",
47
+ "typebox": "1.4.3",
48
+ "zod": "4.6.7"
49
+ },
50
+ "engines": {
51
+ "bun": ">=1.3",
52
+ "node": ">=24"
12
53
  }
13
54
  }
@@ -0,0 +1,137 @@
1
+ // Copied from atc: packages/mcp/src/auth/answer-authorize-request.ts at commit 4fe647cf.
2
+
3
+ import { buildPageResponse } from '../http/build-page-response.ts';
4
+ import { PUBLIC_DOOR_PATHS } from '../http/public-door-paths.ts';
5
+ import { normalizeClientName } from './normalize-client-name.ts';
6
+ import { readClientName } from './read-client-name.ts';
7
+ import type { AuthorizeInvalidReason, HTTPServerContext, Requester } from './types.ts';
8
+
9
+ export async function answerAuthorizeRequest(
10
+ ctx: HTTPServerContext,
11
+ url: URL,
12
+ requester: Requester,
13
+ ): Promise<Response> {
14
+ const params = new URLSearchParams(url.searchParams);
15
+
16
+ // every authorization asks for a fresh passkey sign-in and the consent page,
17
+ // whatever session or consent the browser kept.
18
+ params.set('prompt', 'login consent');
19
+
20
+ const scope = params.get('scope');
21
+
22
+ if (scope !== null) {
23
+ const scopes = scope.split(' ').filter((item) => item !== '');
24
+
25
+ params.set('scope', [...new Set([...scopes, 'offline_access'])].join(' '));
26
+ }
27
+
28
+ if (!params.has('resource')) {
29
+ params.set('resource', ctx.resource);
30
+ }
31
+
32
+ const response = await ctx.store.auth.handler(
33
+ new Request(`${ctx.origin}${PUBLIC_DOOR_PATHS.browser.authorize}?${params.toString()}`, {
34
+ headers: { accept: 'text/html' },
35
+ }),
36
+ );
37
+
38
+ const location = response.headers.get('location');
39
+ const target = location === null ? null : new URL(location, ctx.origin);
40
+
41
+ if (
42
+ target === null ||
43
+ target.origin !== ctx.origin ||
44
+ target.pathname !== PUBLIC_DOOR_PATHS.browser.login
45
+ ) {
46
+ await recordInvalidRequest(ctx, url, requester, target);
47
+
48
+ return response;
49
+ }
50
+
51
+ const clientID = target.searchParams.get('client_id') ?? '';
52
+ const redirectURI = target.searchParams.get('redirect_uri') ?? '';
53
+
54
+ const storedName = await readClientName(ctx.store.db, clientID);
55
+
56
+ const clientName = normalizeClientName(storedName);
57
+
58
+ const accepted = {
59
+ path: url.pathname,
60
+ requester,
61
+ client: { id: clientID, name: clientName },
62
+ redirectHost: new URL(redirectURI).host,
63
+ };
64
+
65
+ const approval = ctx.approvals.createPending({
66
+ key: target.search.slice(1),
67
+ clientID,
68
+ clientName,
69
+ redirectURI,
70
+ });
71
+
72
+ if (approval === null) {
73
+ ctx.security.record({
74
+ kind: 'authorize_started',
75
+ outcome: 'rejected',
76
+ reason: 'rate_limited',
77
+ ...accepted,
78
+ });
79
+
80
+ return buildPageResponse(429, {
81
+ message: 'Too many approvals started in the last minute. Wait a minute, then start again.',
82
+ });
83
+ }
84
+
85
+ ctx.security.record({
86
+ kind: 'authorize_started',
87
+ outcome: 'accepted',
88
+ ...accepted,
89
+ correlationID: approval.correlationID,
90
+ });
91
+
92
+ return response;
93
+ }
94
+
95
+ // A refusal that returns to the client went to a redirect URI better-auth
96
+ // validated, so only then does the event hold its host. The client's id
97
+ // and name go in only for a client boxxy holds.
98
+ async function recordInvalidRequest(
99
+ ctx: HTTPServerContext,
100
+ url: URL,
101
+ requester: Requester,
102
+ target: URL | null,
103
+ ): Promise<void> {
104
+ const code = target?.searchParams.get('error') ?? null;
105
+ const reason = isAuthorizeError(code) ? code : 'other';
106
+ const clientID = url.searchParams.get('client_id');
107
+ const storedName = clientID === null ? null : await readClientName(ctx.store.db, clientID);
108
+
109
+ ctx.security.record({
110
+ kind: 'authorize_invalid',
111
+ outcome: 'rejected',
112
+ reason,
113
+ path: url.pathname,
114
+ requester,
115
+ ...(clientID === null || storedName === null
116
+ ? {}
117
+ : { client: { id: clientID, name: normalizeClientName(storedName) } }),
118
+ ...(target === null || target.origin === ctx.origin ? {} : { redirectHost: target.host }),
119
+ });
120
+ }
121
+
122
+ // The better-auth error codes an `authorize_invalid` event may hold; any
123
+ // other code becomes `other`.
124
+ const AUTHORIZE_ERRORS: ReadonlySet<string> = new Set([
125
+ 'invalid_client',
126
+ 'client_disabled',
127
+ 'invalid_redirect',
128
+ 'unauthorized_client',
129
+ 'unsupported_response_type',
130
+ 'invalid_request',
131
+ 'invalid_scope',
132
+ 'invalid_target',
133
+ ]);
134
+
135
+ function isAuthorizeError(code: string | null): code is AuthorizeInvalidReason {
136
+ return code !== null && AUTHORIZE_ERRORS.has(code);
137
+ }