@boxxy/auth-oauth 0.0.0-stage → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +47 -2
- package/dist/auth/answer-authorize-request.d.ts +2 -0
- package/dist/auth/answer-consent-request.d.ts +10 -0
- package/dist/auth/answer-enroll-request.d.ts +9 -0
- package/dist/auth/answer-login-request.d.ts +9 -0
- package/dist/auth/approval-window-seconds.d.ts +1 -0
- package/dist/auth/build-consent-binding.d.ts +1 -0
- package/dist/auth/create-approval-state.d.ts +41 -0
- package/dist/auth/create-auth-admin.d.ts +33 -0
- package/dist/auth/create-enrollment-state.d.ts +6 -0
- package/dist/auth/create-enrollment-token.d.ts +6 -0
- package/dist/auth/create-owner-plugin.d.ts +38 -0
- package/dist/auth/create-passkey.d.ts +11 -0
- package/dist/auth/create-security-event-log.d.ts +12 -0
- package/dist/auth/derive-token-hash.d.ts +1 -0
- package/dist/auth/format-security-event.d.ts +7 -0
- package/dist/auth/is-allowed-redirect-uri.d.ts +1 -0
- package/dist/auth/load-web-authn.d.ts +6 -0
- package/dist/auth/mcp-auth-database.d.ts +5 -0
- package/dist/auth/mcp-auth-schema.d.ts +54 -0
- package/dist/auth/normalize-client-name.d.ts +1 -0
- package/dist/auth/open-auth-database.d.ts +2 -0
- package/dist/auth/open-mcp-auth.d.ts +80 -0
- package/dist/auth/passkey-script.d.ts +4 -0
- package/dist/auth/read-client-name.d.ts +3 -0
- package/dist/auth/read-client-views.d.ts +9 -0
- package/dist/auth/read-enrollment-token.d.ts +3 -0
- package/dist/auth/read-grant-views.d.ts +12 -0
- package/dist/auth/read-owner-session-id.d.ts +2 -0
- package/dist/auth/record-refused-exchange.d.ts +10 -0
- package/dist/auth/remove-client.d.ts +3 -0
- package/dist/auth/remove-owner-session.d.ts +3 -0
- package/dist/auth/render-consent-page.d.ts +9 -0
- package/dist/auth/render-enroll-page.d.ts +10 -0
- package/dist/auth/render-login-page.d.ts +9 -0
- package/dist/auth/revoke-grant.d.ts +3 -0
- package/dist/auth/to-html-text.d.ts +1 -0
- package/dist/auth/to-script-json.d.ts +1 -0
- package/dist/auth/types.d.ts +79 -0
- package/dist/auth/update-passkey-use.d.ts +10 -0
- package/dist/auth/verify-oauth-query.d.ts +1 -0
- package/dist/auth/verify-passkey-assertion.d.ts +11 -0
- package/dist/auth/verify-passkey-registration.d.ts +14 -0
- package/dist/auth/verify-session-passkey.d.ts +8 -0
- package/dist/auth/write-grant-use.d.ts +3 -0
- package/dist/http/answer-mcp-request.d.ts +9 -0
- package/dist/http/build-page-response.d.ts +5 -0
- package/dist/http/create-mcp-http-app.d.ts +92 -0
- package/dist/http/create-public-door.d.ts +20 -0
- package/dist/http/is-loopback-host.d.ts +1 -0
- package/dist/http/normalize-public-url.d.ts +1 -0
- package/dist/http/pick-error-message.d.ts +1 -0
- package/dist/http/public-door-paths.d.ts +17 -0
- package/dist/http/read-token-access.d.ts +8 -0
- package/dist/http/verify-access-token.d.ts +2 -0
- package/dist/index.d.ts +8 -0
- package/dist/test-utils/authorize-test-client.d.ts +13 -0
- package/dist/test-utils/build-mock-new-passkey.d.ts +2 -0
- package/dist/test-utils/build-passkey-seeder.d.ts +5 -0
- package/dist/test-utils/build-seeder.d.ts +5 -0
- package/dist/test-utils/build-stub-authenticator.d.ts +48 -0
- package/dist/test-utils/build-stub-clock.d.ts +7 -0
- package/dist/test-utils/load-web-authn-early.d.ts +1 -0
- package/dist/test-utils/register-test-cleanup.d.ts +1 -0
- package/dist/test-utils/seed-passkey.d.ts +4 -0
- package/dist/test-utils/setup-temp-dir.d.ts +6 -0
- package/dist/test-utils/start-stub-telemetry-collector.d.ts +5 -0
- package/dist/test-utils/types.d.ts +7 -0
- package/dist/utils/is-record.d.ts +1 -0
- package/dist/utils/parse-json-strings.d.ts +1 -0
- package/dist/utils/types.d.ts +4 -0
- package/package.json +52 -4
- package/src/auth/answer-authorize-request.ts +137 -0
- package/src/auth/answer-consent-request.ts +189 -0
- package/src/auth/answer-enroll-request.ts +190 -0
- package/src/auth/answer-login-request.ts +256 -0
- package/src/auth/approval-window-seconds.ts +5 -0
- package/src/auth/build-consent-binding.ts +13 -0
- package/src/auth/create-approval-state.ts +221 -0
- package/src/auth/create-auth-admin.ts +78 -0
- package/src/auth/create-enrollment-state.ts +56 -0
- package/src/auth/create-enrollment-token.ts +28 -0
- package/src/auth/create-owner-plugin.ts +113 -0
- package/src/auth/create-passkey.ts +48 -0
- package/src/auth/create-security-event-log.ts +97 -0
- package/src/auth/derive-token-hash.ts +7 -0
- package/src/auth/format-security-event.ts +72 -0
- package/src/auth/is-allowed-redirect-uri.ts +19 -0
- package/src/auth/load-web-authn.ts +19 -0
- package/src/auth/mcp-auth-database.ts +10 -0
- package/src/auth/mcp-auth-schema.ts +56 -0
- package/src/auth/normalize-client-name.ts +22 -0
- package/src/auth/open-auth-database.ts +22 -0
- package/src/auth/open-mcp-auth.ts +138 -0
- package/src/auth/passkey-script.ts +50 -0
- package/src/auth/read-client-name.ts +17 -0
- package/src/auth/read-client-views.ts +27 -0
- package/src/auth/read-enrollment-token.ts +20 -0
- package/src/auth/read-grant-views.ts +53 -0
- package/src/auth/read-owner-session-id.ts +16 -0
- package/src/auth/record-refused-exchange.ts +74 -0
- package/src/auth/remove-client.ts +33 -0
- package/src/auth/remove-owner-session.ts +11 -0
- package/src/auth/render-consent-page.ts +61 -0
- package/src/auth/render-enroll-page.ts +59 -0
- package/src/auth/render-login-page.ts +58 -0
- package/src/auth/revoke-grant.ts +25 -0
- package/src/auth/to-html-text.ts +10 -0
- package/src/auth/to-script-json.ts +8 -0
- package/src/auth/types.ts +115 -0
- package/src/auth/update-passkey-use.ts +31 -0
- package/src/auth/verify-oauth-query.ts +41 -0
- package/src/auth/verify-passkey-assertion.ts +124 -0
- package/src/auth/verify-passkey-registration.ts +118 -0
- package/src/auth/verify-session-passkey.ts +47 -0
- package/src/auth/write-grant-use.ts +18 -0
- package/src/http/answer-mcp-request.ts +77 -0
- package/src/http/build-page-response.ts +26 -0
- package/src/http/create-mcp-http-app.ts +340 -0
- package/src/http/create-public-door.ts +150 -0
- package/src/http/is-loopback-host.ts +10 -0
- package/src/http/normalize-public-url.ts +29 -0
- package/src/http/pick-error-message.ts +21 -0
- package/src/http/public-door-paths.ts +21 -0
- package/src/http/read-token-access.ts +44 -0
- package/src/http/verify-access-token.ts +46 -0
- package/src/index.ts +8 -0
- package/src/test-utils/authorize-test-client.ts +194 -0
- package/src/test-utils/build-mock-new-passkey.ts +15 -0
- package/src/test-utils/build-passkey-seeder.ts +11 -0
- package/src/test-utils/build-seeder.ts +10 -0
- package/src/test-utils/build-stub-authenticator.ts +244 -0
- package/src/test-utils/build-stub-clock.ts +65 -0
- package/src/test-utils/load-web-authn-early.ts +8 -0
- package/src/test-utils/register-test-cleanup.ts +55 -0
- package/src/test-utils/seed-passkey.ts +32 -0
- package/src/test-utils/setup-temp-dir.ts +24 -0
- package/src/test-utils/start-stub-telemetry-collector.ts +27 -0
- package/src/test-utils/types.ts +11 -0
- package/src/utils/is-record.ts +5 -0
- package/src/utils/parse-json-strings.ts +15 -0
- package/src/utils/types.ts +8 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 the boxxy authors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,48 @@
|
|
|
1
|
-
|
|
1
|
+
<div align="center">
|
|
2
|
+
<h1>@boxxy/auth-oauth</h1>
|
|
2
3
|
|
|
3
|
-
|
|
4
|
+
<p>
|
|
5
|
+
<a href="https://www.npmjs.com/package/@boxxy/auth-oauth"><img src="https://img.shields.io/npm/v/@boxxy/auth-oauth" alt="npm version"></a>
|
|
6
|
+
<a href="./LICENSE"><img src="https://img.shields.io/badge/license-MIT-blue" alt="MIT license"></a>
|
|
7
|
+
</p>
|
|
8
|
+
|
|
9
|
+
<p>
|
|
10
|
+
<a href="../../docs/design/boxxy/README.md">Documentation</a>
|
|
11
|
+
</p>
|
|
12
|
+
</div>
|
|
13
|
+
|
|
14
|
+
The OAuth server behind boxxy's public door: it lets the owner's assistant reach the owner's MCP
|
|
15
|
+
tools from anywhere, with a grant that the owner approved with a passkey.
|
|
16
|
+
|
|
17
|
+
## Install
|
|
18
|
+
|
|
19
|
+
Install the package from [npm](https://www.npmjs.com/package/@boxxy/auth-oauth) with Bun:
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
bun add @boxxy/auth-oauth
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
The package runs on [Bun](https://bun.sh) 1.3 or newer.
|
|
26
|
+
|
|
27
|
+
## Features
|
|
28
|
+
|
|
29
|
+
- `createPublicDoor(options)` opens the auth database and returns the door: `answerRequest` for each
|
|
30
|
+
request, `addLocalPort` for the loopback address, `admin` for the owner's commands and `stop`.
|
|
31
|
+
- The door serves `/mcp`, the OAuth routes (`/oauth2/authorize`, `/oauth2/token`, `/oauth2/revoke`
|
|
32
|
+
and the two `.well-known` documents) and the sign-in pages (`/login`, `/enroll`, `/consent`,
|
|
33
|
+
`/error`), and answers 404 on any other path.
|
|
34
|
+
- It checks the `Host` and `Origin` headers, verifies each bearer token against the auth database,
|
|
35
|
+
and passes the verified token to the `serveMCP` callback that the app gives it.
|
|
36
|
+
- It writes each sign-in outcome and each refused token to `writeSecurityLine` as a JSON line.
|
|
37
|
+
- `admin` enrolls a passkey, adds and removes clients, and lists and revokes grants.
|
|
38
|
+
- `./test-utils/authorize-test-client` runs the whole sign-in against a door with a stub passkey,
|
|
39
|
+
for tests.
|
|
40
|
+
|
|
41
|
+
The code is a copy of the OAuth server in atc's `@atc/mcp`. Each copied file names its source path
|
|
42
|
+
and commit in its first line.
|
|
43
|
+
|
|
44
|
+
## Documentation
|
|
45
|
+
|
|
46
|
+
[The public door](../../docs/design/boxxy/v1-api-and-auth.md#the-public-door) describes the
|
|
47
|
+
endpoints, the scopes and the token lifetimes, and
|
|
48
|
+
[0018](../../docs/decisions/0018-public-owner-mcp.md) records the decision.
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { HTTPServerContext, Requester } from './types.ts';
|
|
2
|
+
interface ConsentRequest {
|
|
3
|
+
readonly method: 'GET' | 'POST';
|
|
4
|
+
readonly url: URL;
|
|
5
|
+
readonly cookie: string | null;
|
|
6
|
+
readonly body: string;
|
|
7
|
+
readonly requester: Requester;
|
|
8
|
+
}
|
|
9
|
+
export declare function answerConsentRequest(ctx: HTTPServerContext, request: ConsentRequest): Promise<Response>;
|
|
10
|
+
export {};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { HTTPServerContext, Requester } from './types.ts';
|
|
2
|
+
interface EnrollRequest {
|
|
3
|
+
readonly method: 'GET' | 'POST';
|
|
4
|
+
readonly url: URL;
|
|
5
|
+
readonly body: string;
|
|
6
|
+
readonly requester: Requester;
|
|
7
|
+
}
|
|
8
|
+
export declare function answerEnrollRequest(ctx: HTTPServerContext, request: EnrollRequest): Promise<Response>;
|
|
9
|
+
export {};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { HTTPServerContext, Requester } from './types.ts';
|
|
2
|
+
interface LoginRequest {
|
|
3
|
+
readonly method: 'GET' | 'POST';
|
|
4
|
+
readonly url: URL;
|
|
5
|
+
readonly body: string;
|
|
6
|
+
readonly requester: Requester;
|
|
7
|
+
}
|
|
8
|
+
export declare function answerLoginRequest(ctx: HTTPServerContext, request: LoginRequest): Promise<Response>;
|
|
9
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const APPROVAL_WINDOW_SECONDS = 600;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function buildConsentBinding(oauthQuery: string): string;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
interface ApprovalRequest {
|
|
2
|
+
readonly key: string;
|
|
3
|
+
readonly clientID: string;
|
|
4
|
+
readonly clientName: string;
|
|
5
|
+
readonly redirectURI: string;
|
|
6
|
+
}
|
|
7
|
+
interface PendingApproval extends ApprovalRequest {
|
|
8
|
+
readonly correlationID: string;
|
|
9
|
+
failures: number;
|
|
10
|
+
readonly expiresAt: number;
|
|
11
|
+
challenge: {
|
|
12
|
+
readonly value: string;
|
|
13
|
+
readonly expiresAt: number;
|
|
14
|
+
} | null;
|
|
15
|
+
}
|
|
16
|
+
export type TakenChallenge = {
|
|
17
|
+
readonly kind: 'taken';
|
|
18
|
+
readonly challenge: string;
|
|
19
|
+
} | {
|
|
20
|
+
readonly kind: 'expired';
|
|
21
|
+
} | {
|
|
22
|
+
readonly kind: 'missing';
|
|
23
|
+
};
|
|
24
|
+
interface ApprovedRequest {
|
|
25
|
+
readonly binding: string;
|
|
26
|
+
readonly correlationID: string;
|
|
27
|
+
readonly expiresAt: number;
|
|
28
|
+
}
|
|
29
|
+
export interface ApprovalState {
|
|
30
|
+
readonly createPending: (request: ApprovalRequest) => PendingApproval | null;
|
|
31
|
+
readonly findPending: (key: string) => PendingApproval | null;
|
|
32
|
+
readonly mintChallenge: (key: string) => string | null;
|
|
33
|
+
readonly claimChallenge: (key: string) => TakenChallenge;
|
|
34
|
+
readonly recordFailure: (key: string) => 'retry' | 'locked';
|
|
35
|
+
readonly removePending: (key: string) => void;
|
|
36
|
+
readonly recordApproved: (sessionID: string, approved: Omit<ApprovedRequest, 'expiresAt'>) => void;
|
|
37
|
+
readonly findApproved: (sessionID: string) => ApprovedRequest | null;
|
|
38
|
+
readonly removeApproved: (sessionID: string) => void;
|
|
39
|
+
}
|
|
40
|
+
export declare function createApprovalState(pendingMs: number, now: () => number): ApprovalState;
|
|
41
|
+
export {};
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { AuthClock } from '../utils/types.ts';
|
|
2
|
+
import type { openMCPAuth } from './open-mcp-auth.ts';
|
|
3
|
+
import type { ClientView } from './read-client-views.ts';
|
|
4
|
+
import type { GrantView } from './read-grant-views.ts';
|
|
5
|
+
type MCPAuth = Awaited<ReturnType<typeof openMCPAuth>>;
|
|
6
|
+
export interface AuthAdminInput {
|
|
7
|
+
readonly store: MCPAuth;
|
|
8
|
+
readonly origin: string;
|
|
9
|
+
readonly clock: AuthClock;
|
|
10
|
+
}
|
|
11
|
+
export type AddClientAnswer = {
|
|
12
|
+
readonly kind: 'added';
|
|
13
|
+
readonly clientID: string;
|
|
14
|
+
readonly name: string;
|
|
15
|
+
} | {
|
|
16
|
+
readonly kind: 'refused';
|
|
17
|
+
readonly redirectURI: string;
|
|
18
|
+
};
|
|
19
|
+
export interface EnrollmentLink {
|
|
20
|
+
readonly token: string;
|
|
21
|
+
readonly link: string;
|
|
22
|
+
readonly expiresAt: number;
|
|
23
|
+
}
|
|
24
|
+
export interface AuthAdmin {
|
|
25
|
+
readonly enroll: () => Promise<EnrollmentLink>;
|
|
26
|
+
readonly addClient: (name: string, redirectURIs: readonly string[]) => Promise<AddClientAnswer>;
|
|
27
|
+
readonly listClients: () => Promise<readonly ClientView[]>;
|
|
28
|
+
readonly removeClient: (clientID: string) => Promise<boolean>;
|
|
29
|
+
readonly listGrants: () => Promise<readonly GrantView[]>;
|
|
30
|
+
readonly revokeGrant: (grantID: string) => Promise<boolean>;
|
|
31
|
+
}
|
|
32
|
+
export declare function createAuthAdmin(input: AuthAdminInput): AuthAdmin;
|
|
33
|
+
export {};
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { TakenChallenge } from './create-approval-state.ts';
|
|
2
|
+
export interface EnrollmentState {
|
|
3
|
+
readonly mintChallenge: (tokenHash: string) => string;
|
|
4
|
+
readonly claimChallenge: (tokenHash: string) => TakenChallenge;
|
|
5
|
+
}
|
|
6
|
+
export declare function createEnrollmentState(now: () => number): EnrollmentState;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
export declare function createOwnerPlugin(): {
|
|
3
|
+
id: "boxxy-owner";
|
|
4
|
+
endpoints: {
|
|
5
|
+
signInOwner: import("better-auth").StrictEndpoint<"/boxxy/sign-in-owner", {
|
|
6
|
+
method: "POST";
|
|
7
|
+
body: z.ZodObject<{
|
|
8
|
+
oauth_query: z.ZodOptional<z.ZodString>;
|
|
9
|
+
}, z.core.$strip>;
|
|
10
|
+
metadata: {
|
|
11
|
+
SERVER_ONLY: true;
|
|
12
|
+
};
|
|
13
|
+
}, {
|
|
14
|
+
signedIn: boolean;
|
|
15
|
+
}>;
|
|
16
|
+
verifyMCPAccessToken: import("better-auth").StrictEndpoint<"/boxxy/verify-access-token", {
|
|
17
|
+
method: "POST";
|
|
18
|
+
body: z.ZodObject<{
|
|
19
|
+
token: z.ZodString;
|
|
20
|
+
}, z.core.$strip>;
|
|
21
|
+
metadata: {
|
|
22
|
+
SERVER_ONLY: true;
|
|
23
|
+
};
|
|
24
|
+
}, import("@better-auth/oauth-provider").ActiveAccessTokenPayload>;
|
|
25
|
+
createFixedClient: import("better-auth").StrictEndpoint<"/boxxy/create-fixed-client", {
|
|
26
|
+
method: "POST";
|
|
27
|
+
body: z.ZodObject<{
|
|
28
|
+
name: z.ZodString;
|
|
29
|
+
redirectURIs: z.ZodArray<z.ZodString>;
|
|
30
|
+
}, z.core.$strip>;
|
|
31
|
+
metadata: {
|
|
32
|
+
SERVER_ONLY: true;
|
|
33
|
+
};
|
|
34
|
+
}, {
|
|
35
|
+
clientID: string;
|
|
36
|
+
}>;
|
|
37
|
+
};
|
|
38
|
+
};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { Kysely } from 'kysely';
|
|
2
|
+
import type { MCPAuthSchema } from './mcp-auth-schema.ts';
|
|
3
|
+
export interface NewPasskey {
|
|
4
|
+
readonly tokenHash: string;
|
|
5
|
+
readonly id: string;
|
|
6
|
+
readonly publicKey: string;
|
|
7
|
+
readonly counter: number;
|
|
8
|
+
readonly transports: readonly string[];
|
|
9
|
+
readonly now: number;
|
|
10
|
+
}
|
|
11
|
+
export declare function createPasskey(db: Kysely<MCPAuthSchema>, passkey: NewPasskey): Promise<boolean>;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { AuthClock } from '../utils/types.ts';
|
|
2
|
+
import type { SecurityEvent } from './types.ts';
|
|
3
|
+
interface SecurityEventLogOptions {
|
|
4
|
+
readonly print: (line: string) => void;
|
|
5
|
+
readonly clock: AuthClock;
|
|
6
|
+
}
|
|
7
|
+
export interface SecurityEventLog {
|
|
8
|
+
readonly record: (event: SecurityEvent) => void;
|
|
9
|
+
readonly drain: () => void;
|
|
10
|
+
}
|
|
11
|
+
export declare function createSecurityEventLog(opts: SecurityEventLogOptions): SecurityEventLog;
|
|
12
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function deriveTokenHash(token: string): string;
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { SecurityEvent } from './types.ts';
|
|
2
|
+
export interface SuppressedSecurityEvent {
|
|
3
|
+
readonly kind: 'suppressed';
|
|
4
|
+
readonly reason: SecurityEvent['kind'];
|
|
5
|
+
readonly count: number;
|
|
6
|
+
}
|
|
7
|
+
export declare function formatSecurityEvent(event: SecurityEvent | SuppressedSecurityEvent, at: number): string;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function isAllowedRedirectURI(raw: string): boolean;
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { verifyAuthenticationResponse, verifyRegistrationResponse } from '@simplewebauthn/server';
|
|
2
|
+
export interface WebAuthn {
|
|
3
|
+
readonly verifyAuthenticationResponse: typeof verifyAuthenticationResponse;
|
|
4
|
+
readonly verifyRegistrationResponse: typeof verifyRegistrationResponse;
|
|
5
|
+
}
|
|
6
|
+
export declare function loadWebAuthn(): Promise<WebAuthn>;
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
export interface MCPAuthSchema {
|
|
2
|
+
readonly oauthClient: {
|
|
3
|
+
readonly id: string;
|
|
4
|
+
readonly clientId: string;
|
|
5
|
+
readonly name: string | null;
|
|
6
|
+
readonly redirectUris: string;
|
|
7
|
+
readonly createdAt: string | null;
|
|
8
|
+
};
|
|
9
|
+
readonly oauthAccessToken: {
|
|
10
|
+
readonly id: string;
|
|
11
|
+
readonly token: string;
|
|
12
|
+
readonly clientId: string;
|
|
13
|
+
readonly sessionId: string | null;
|
|
14
|
+
readonly authorizationCodeId: string | null;
|
|
15
|
+
};
|
|
16
|
+
readonly oauthRefreshToken: {
|
|
17
|
+
readonly id: string;
|
|
18
|
+
readonly clientId: string;
|
|
19
|
+
readonly sessionId: string | null;
|
|
20
|
+
readonly authorizationCodeId: string | null;
|
|
21
|
+
readonly scopes: string;
|
|
22
|
+
readonly createdAt: string;
|
|
23
|
+
readonly expiresAt: string;
|
|
24
|
+
readonly revoked: string | null;
|
|
25
|
+
};
|
|
26
|
+
readonly oauthConsent: {
|
|
27
|
+
readonly id: string;
|
|
28
|
+
readonly clientId: string;
|
|
29
|
+
};
|
|
30
|
+
readonly oauthResource: {
|
|
31
|
+
readonly identifier: string;
|
|
32
|
+
};
|
|
33
|
+
readonly session: {
|
|
34
|
+
readonly id: string;
|
|
35
|
+
readonly expiresAt: string;
|
|
36
|
+
};
|
|
37
|
+
readonly boxxy_grant_use: {
|
|
38
|
+
readonly grant_id: string;
|
|
39
|
+
readonly last_used_at: string;
|
|
40
|
+
};
|
|
41
|
+
readonly boxxy_passkey: {
|
|
42
|
+
readonly id: string;
|
|
43
|
+
readonly public_key: string;
|
|
44
|
+
readonly counter: number;
|
|
45
|
+
readonly transports: string;
|
|
46
|
+
readonly created_at: string;
|
|
47
|
+
readonly last_used_at: string | null;
|
|
48
|
+
};
|
|
49
|
+
readonly boxxy_enrollment_token: {
|
|
50
|
+
readonly token_hash: string;
|
|
51
|
+
readonly created_at: string;
|
|
52
|
+
readonly expires_at: string;
|
|
53
|
+
};
|
|
54
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function normalizeClientName(raw: unknown, fallback?: string, maxLength?: number): string;
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import type { BetterAuthPlugin } from 'better-auth';
|
|
2
|
+
import { Kysely } from 'kysely';
|
|
3
|
+
import type { MCPAuthDatabase } from './mcp-auth-database.ts';
|
|
4
|
+
import type { MCPAuthSchema } from './mcp-auth-schema.ts';
|
|
5
|
+
interface MCPAuthOptions {
|
|
6
|
+
readonly dbPath: string;
|
|
7
|
+
readonly origin: string | null;
|
|
8
|
+
readonly refreshReuseSeconds?: number;
|
|
9
|
+
readonly openDatabase: (path: string) => MCPAuthDatabase;
|
|
10
|
+
}
|
|
11
|
+
export declare function openMCPAuth(options: MCPAuthOptions): Promise<{
|
|
12
|
+
auth: import("better-auth").Auth<{
|
|
13
|
+
baseURL: string;
|
|
14
|
+
basePath: string;
|
|
15
|
+
secret: string;
|
|
16
|
+
database: {
|
|
17
|
+
db: Kysely<MCPAuthSchema>;
|
|
18
|
+
type: "sqlite";
|
|
19
|
+
};
|
|
20
|
+
telemetry: {
|
|
21
|
+
enabled: false;
|
|
22
|
+
};
|
|
23
|
+
logger: {
|
|
24
|
+
level: "error";
|
|
25
|
+
};
|
|
26
|
+
rateLimit: {
|
|
27
|
+
enabled: false;
|
|
28
|
+
};
|
|
29
|
+
advanced: {
|
|
30
|
+
database: {
|
|
31
|
+
validateSchema: false;
|
|
32
|
+
};
|
|
33
|
+
};
|
|
34
|
+
trustedOrigins: string[];
|
|
35
|
+
session: {
|
|
36
|
+
expiresIn: number;
|
|
37
|
+
disableSessionRefresh: true;
|
|
38
|
+
};
|
|
39
|
+
plugins: [BetterAuthPlugin, {
|
|
40
|
+
id: "boxxy-owner";
|
|
41
|
+
endpoints: {
|
|
42
|
+
signInOwner: import("better-auth").StrictEndpoint<"/boxxy/sign-in-owner", {
|
|
43
|
+
method: "POST";
|
|
44
|
+
body: import("zod").ZodObject<{
|
|
45
|
+
oauth_query: import("zod").ZodOptional<import("zod").ZodString>;
|
|
46
|
+
}, import("zod/v4/core").$strip>;
|
|
47
|
+
metadata: {
|
|
48
|
+
SERVER_ONLY: true;
|
|
49
|
+
};
|
|
50
|
+
}, {
|
|
51
|
+
signedIn: boolean;
|
|
52
|
+
}>;
|
|
53
|
+
verifyMCPAccessToken: import("better-auth").StrictEndpoint<"/boxxy/verify-access-token", {
|
|
54
|
+
method: "POST";
|
|
55
|
+
body: import("zod").ZodObject<{
|
|
56
|
+
token: import("zod").ZodString;
|
|
57
|
+
}, import("zod/v4/core").$strip>;
|
|
58
|
+
metadata: {
|
|
59
|
+
SERVER_ONLY: true;
|
|
60
|
+
};
|
|
61
|
+
}, import("@better-auth/oauth-provider").ActiveAccessTokenPayload>;
|
|
62
|
+
createFixedClient: import("better-auth").StrictEndpoint<"/boxxy/create-fixed-client", {
|
|
63
|
+
method: "POST";
|
|
64
|
+
body: import("zod").ZodObject<{
|
|
65
|
+
name: import("zod").ZodString;
|
|
66
|
+
redirectURIs: import("zod").ZodArray<import("zod").ZodString>;
|
|
67
|
+
}, import("zod/v4/core").$strip>;
|
|
68
|
+
metadata: {
|
|
69
|
+
SERVER_ONLY: true;
|
|
70
|
+
};
|
|
71
|
+
}, {
|
|
72
|
+
clientID: string;
|
|
73
|
+
}>;
|
|
74
|
+
};
|
|
75
|
+
}];
|
|
76
|
+
}>;
|
|
77
|
+
db: Kysely<MCPAuthSchema>;
|
|
78
|
+
close(): Promise<void>;
|
|
79
|
+
}>;
|
|
80
|
+
export {};
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export declare const PASSKEY_SCRIPT: {
|
|
2
|
+
readonly source: "(() => {\n const data = document.getElementById('boxxy-passkey');\n const form = document.getElementById('boxxy-passkey-form');\n const button = document.getElementById('boxxy-passkey-start');\n const status = document.getElementById('boxxy-passkey-status');\n if (data === null || form === null || button === null || status === null) return;\n const spec = JSON.parse(data.textContent);\n const toBytes = (value) => {\n const base64 = value.replace(/-/g, '+').replace(/_/g, '/');\n const padded = base64 + '='.repeat((4 - (base64.length % 4)) % 4);\n return Uint8Array.from(atob(padded), (c) => c.charCodeAt(0));\n };\n const toText = (buffer) => {\n let text = '';\n for (const byte of new Uint8Array(buffer)) text += String.fromCharCode(byte);\n return btoa(text).replace(/\\+/g, '-').replace(/\\//g, '_').replace(/=+$/, '');\n };\n const toDescriptors = (list) => (list || []).map((item) => ({ ...item, id: toBytes(item.id) }));\n button.addEventListener('click', async () => {\n button.disabled = true;\n status.textContent = '';\n try {\n const options = spec.options;\n const credential = spec.mode === 'create'\n ? await navigator.credentials.create({ publicKey: { ...options, challenge: toBytes(options.challenge), user: { ...options.user, id: toBytes(options.user.id) }, excludeCredentials: toDescriptors(options.excludeCredentials) } })\n : await navigator.credentials.get({ publicKey: { ...options, challenge: toBytes(options.challenge), allowCredentials: toDescriptors(options.allowCredentials) } });\n const reply = credential.response;\n const response = spec.mode === 'create'\n ? { clientDataJSON: toText(reply.clientDataJSON), attestationObject: toText(reply.attestationObject), transports: typeof reply.getTransports === 'function' ? reply.getTransports() : [] }\n : { clientDataJSON: toText(reply.clientDataJSON), authenticatorData: toText(reply.authenticatorData), signature: toText(reply.signature), userHandle: reply.userHandle ? toText(reply.userHandle) : undefined };\n form.elements.namedItem('credential').value = JSON.stringify({ id: credential.id, rawId: toText(credential.rawId), type: credential.type, response, clientExtensionResults: credential.getClientExtensionResults(), authenticatorAttachment: credential.authenticatorAttachment || undefined });\n form.submit();\n } catch (error) {\n status.textContent = 'The passkey prompt ended without a passkey (' + (error && error.name ? error.name : 'Error') + '). Try again.';\n button.disabled = false;\n }\n });\n})();";
|
|
3
|
+
readonly policySource: `'sha256-${string}'`;
|
|
4
|
+
};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { Kysely } from 'kysely';
|
|
2
|
+
import type { MCPAuthSchema } from './mcp-auth-schema.ts';
|
|
3
|
+
export interface ClientView {
|
|
4
|
+
readonly clientID: string;
|
|
5
|
+
readonly name: string;
|
|
6
|
+
readonly redirectURIs: readonly string[];
|
|
7
|
+
readonly createdAt: string | null;
|
|
8
|
+
}
|
|
9
|
+
export declare function readClientViews(db: Kysely<MCPAuthSchema>): Promise<readonly ClientView[]>;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { GrantScope } from '@boxxy/contract';
|
|
2
|
+
import type { Kysely } from 'kysely';
|
|
3
|
+
import type { MCPAuthSchema } from './mcp-auth-schema.ts';
|
|
4
|
+
export interface GrantView {
|
|
5
|
+
readonly grantID: string;
|
|
6
|
+
readonly clientID: string;
|
|
7
|
+
readonly clientName: string;
|
|
8
|
+
readonly scopes: readonly GrantScope[];
|
|
9
|
+
readonly createdAt: string;
|
|
10
|
+
readonly lastUsedAt: string | null;
|
|
11
|
+
}
|
|
12
|
+
export declare function readGrantViews(db: Kysely<MCPAuthSchema>): Promise<readonly GrantView[]>;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { HTTPServerContext, Requester } from './types.ts';
|
|
2
|
+
interface RefusedExchange {
|
|
3
|
+
readonly contentType: string | null;
|
|
4
|
+
readonly body: string;
|
|
5
|
+
readonly response: Response;
|
|
6
|
+
readonly path: string;
|
|
7
|
+
readonly requester: Requester;
|
|
8
|
+
}
|
|
9
|
+
export declare function recordRefusedExchange(ctx: Pick<HTTPServerContext, 'store' | 'security'>, exchange: RefusedExchange): Promise<void>;
|
|
10
|
+
export {};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { GrantScope } from '@boxxy/contract';
|
|
2
|
+
interface ConsentPageView {
|
|
3
|
+
readonly oauthQuery: string;
|
|
4
|
+
readonly clientName: string;
|
|
5
|
+
readonly redirectURI: string;
|
|
6
|
+
readonly scopes: readonly GrantScope[];
|
|
7
|
+
}
|
|
8
|
+
export declare function renderConsentPage(view: ConsentPageView): string;
|
|
9
|
+
export {};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
interface EnrollPageView {
|
|
2
|
+
readonly ceremony: {
|
|
3
|
+
readonly token: string;
|
|
4
|
+
readonly options: unknown;
|
|
5
|
+
} | null;
|
|
6
|
+
readonly error: string | null;
|
|
7
|
+
readonly prefill?: string;
|
|
8
|
+
}
|
|
9
|
+
export declare function renderEnrollPage(view: EnrollPageView): string;
|
|
10
|
+
export {};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
interface LoginPageView {
|
|
2
|
+
readonly oauthQuery: string;
|
|
3
|
+
readonly clientName: string;
|
|
4
|
+
readonly redirectURI: string;
|
|
5
|
+
readonly options: unknown;
|
|
6
|
+
readonly error: string | null;
|
|
7
|
+
}
|
|
8
|
+
export declare function renderLoginPage(view: LoginPageView): string;
|
|
9
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function toHTMLText(text: string): string;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function toScriptJSON(value: unknown): string;
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import type { GrantScope } from '@boxxy/contract';
|
|
2
|
+
import type { AuthClock } from '../utils/types.ts';
|
|
3
|
+
import type { ApprovalState } from './create-approval-state.ts';
|
|
4
|
+
import type { EnrollmentState } from './create-enrollment-state.ts';
|
|
5
|
+
import type { openMCPAuth } from './open-mcp-auth.ts';
|
|
6
|
+
export interface Requester {
|
|
7
|
+
readonly socketAddress: string | null;
|
|
8
|
+
readonly connectingIP: {
|
|
9
|
+
readonly address: string;
|
|
10
|
+
readonly provenance: 'trusted' | 'unverified';
|
|
11
|
+
} | null;
|
|
12
|
+
readonly userAgent: string | null;
|
|
13
|
+
}
|
|
14
|
+
export type SecurityOutcome = {
|
|
15
|
+
readonly kind: 'authorize_started';
|
|
16
|
+
readonly outcome: 'accepted';
|
|
17
|
+
} | {
|
|
18
|
+
readonly kind: 'authorize_started';
|
|
19
|
+
readonly outcome: 'rejected';
|
|
20
|
+
readonly reason: 'rate_limited';
|
|
21
|
+
} | {
|
|
22
|
+
readonly kind: 'authorize_invalid';
|
|
23
|
+
readonly outcome: 'rejected';
|
|
24
|
+
readonly reason: AuthorizeInvalidReason;
|
|
25
|
+
} | {
|
|
26
|
+
readonly kind: 'owner_auth';
|
|
27
|
+
readonly outcome: 'succeeded';
|
|
28
|
+
} | {
|
|
29
|
+
readonly kind: 'owner_auth';
|
|
30
|
+
readonly outcome: 'failed';
|
|
31
|
+
readonly reason?: OwnerAuthFailure;
|
|
32
|
+
} | {
|
|
33
|
+
readonly kind: 'consent';
|
|
34
|
+
readonly outcome: 'allowed';
|
|
35
|
+
} | {
|
|
36
|
+
readonly kind: 'consent';
|
|
37
|
+
readonly outcome: 'denied';
|
|
38
|
+
readonly reason?: 'none_ticked';
|
|
39
|
+
} | {
|
|
40
|
+
readonly kind: 'token_exchange';
|
|
41
|
+
readonly outcome: 'rejected';
|
|
42
|
+
readonly reason: TokenExchangeReason;
|
|
43
|
+
} | {
|
|
44
|
+
readonly kind: 'bearer';
|
|
45
|
+
readonly outcome: 'rejected';
|
|
46
|
+
readonly reason: BearerReason;
|
|
47
|
+
};
|
|
48
|
+
export type OwnerAuthFailure = 'assertion_failed' | 'locked' | 'expired' | 'not_enrolled' | 'invalid_token';
|
|
49
|
+
export type AuthorizeInvalidReason = 'invalid_client' | 'client_disabled' | 'invalid_redirect' | 'unauthorized_client' | 'unsupported_response_type' | 'invalid_request' | 'invalid_scope' | 'invalid_target' | 'bad_host' | 'bad_origin' | 'other';
|
|
50
|
+
export type TokenExchangeReason = 'invalid_request' | 'invalid_client' | 'invalid_grant' | 'unauthorized_client' | 'unsupported_grant_type' | 'invalid_scope' | 'invalid_target' | 'other';
|
|
51
|
+
type BearerReason = 'malformed' | 'invalid_token';
|
|
52
|
+
export type SecurityEvent = SecurityOutcome & {
|
|
53
|
+
readonly path: string;
|
|
54
|
+
readonly requester: Requester;
|
|
55
|
+
readonly client?: {
|
|
56
|
+
readonly id: string;
|
|
57
|
+
readonly name: string;
|
|
58
|
+
};
|
|
59
|
+
readonly redirectHost?: string;
|
|
60
|
+
readonly correlationID?: string;
|
|
61
|
+
};
|
|
62
|
+
export interface VerifiedAccess {
|
|
63
|
+
readonly clientID: string;
|
|
64
|
+
readonly clientName: string;
|
|
65
|
+
readonly scopes: readonly GrantScope[];
|
|
66
|
+
}
|
|
67
|
+
type MCPAuth = Awaited<ReturnType<typeof openMCPAuth>>;
|
|
68
|
+
export interface HTTPServerContext {
|
|
69
|
+
readonly origin: string;
|
|
70
|
+
readonly resource: string;
|
|
71
|
+
readonly store: MCPAuth;
|
|
72
|
+
readonly approvals: ApprovalState;
|
|
73
|
+
readonly enrollments: EnrollmentState;
|
|
74
|
+
readonly security: {
|
|
75
|
+
readonly record: (event: SecurityEvent) => void;
|
|
76
|
+
};
|
|
77
|
+
readonly clock: AuthClock;
|
|
78
|
+
}
|
|
79
|
+
export {};
|