@boxxy/auth-oauth 0.2.5 → 0.2.7

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 (144) hide show
  1. package/README.md +12 -13
  2. package/dist/auth/create-auth-admin.d.ts +25 -12
  3. package/dist/auth/create-security-event-log.d.ts +1 -1
  4. package/dist/auth/format-security-event.d.ts +10 -2
  5. package/dist/auth/open-auth-database.d.ts +2 -2
  6. package/dist/http/create-public-door.d.ts +5 -1
  7. package/dist/http/{answer-mcp-request.d.ts → types.d.ts} +0 -2
  8. package/dist/index.d.ts +2 -5
  9. package/dist/migrations/grant-rows-migration.d.ts +2 -0
  10. package/dist/migrations/run-auth-migrations.d.ts +2 -0
  11. package/dist/test-utils/authorize-test-client.d.ts +1 -13
  12. package/dist/test-utils/pre-package-schema.d.ts +1 -0
  13. package/dist/test-utils/seed-pre-package-auth.d.ts +21 -0
  14. package/package.json +7 -13
  15. package/src/auth/create-auth-admin.ts +109 -41
  16. package/src/auth/create-security-event-log.ts +1 -3
  17. package/src/auth/format-security-event.ts +12 -11
  18. package/src/auth/normalize-client-name.ts +0 -2
  19. package/src/auth/open-auth-database.ts +8 -13
  20. package/src/http/create-public-door.ts +113 -29
  21. package/src/http/is-loopback-host.ts +0 -2
  22. package/src/http/normalize-public-url.ts +0 -2
  23. package/src/http/types.ts +11 -0
  24. package/src/index.ts +9 -5
  25. package/src/migrations/grant-rows-migration.ts +108 -0
  26. package/src/migrations/run-auth-migrations.ts +53 -0
  27. package/src/test-utils/authorize-test-client.ts +2 -194
  28. package/src/test-utils/build-stub-clock.ts +0 -2
  29. package/src/test-utils/pre-package-schema.ts +36 -0
  30. package/src/test-utils/register-test-cleanup.ts +0 -2
  31. package/src/test-utils/seed-pre-package-auth.ts +162 -0
  32. package/src/test-utils/setup-temp-dir.ts +0 -2
  33. package/src/utils/types.ts +0 -2
  34. package/dist/auth/answer-authorize-request.d.ts +0 -2
  35. package/dist/auth/answer-consent-request.d.ts +0 -10
  36. package/dist/auth/answer-enroll-request.d.ts +0 -9
  37. package/dist/auth/answer-login-request.d.ts +0 -9
  38. package/dist/auth/approval-window-seconds.d.ts +0 -1
  39. package/dist/auth/build-consent-binding.d.ts +0 -1
  40. package/dist/auth/create-approval-state.d.ts +0 -41
  41. package/dist/auth/create-enrollment-state.d.ts +0 -6
  42. package/dist/auth/create-enrollment-token.d.ts +0 -6
  43. package/dist/auth/create-owner-plugin.d.ts +0 -38
  44. package/dist/auth/create-passkey.d.ts +0 -11
  45. package/dist/auth/derive-token-hash.d.ts +0 -1
  46. package/dist/auth/is-allowed-redirect-uri.d.ts +0 -1
  47. package/dist/auth/load-web-authn.d.ts +0 -6
  48. package/dist/auth/mcp-auth-database.d.ts +0 -5
  49. package/dist/auth/mcp-auth-schema.d.ts +0 -54
  50. package/dist/auth/open-mcp-auth.d.ts +0 -80
  51. package/dist/auth/passkey-script.d.ts +0 -4
  52. package/dist/auth/read-client-name.d.ts +0 -3
  53. package/dist/auth/read-client-views.d.ts +0 -9
  54. package/dist/auth/read-enrollment-token.d.ts +0 -3
  55. package/dist/auth/read-grant-views.d.ts +0 -12
  56. package/dist/auth/read-owner-session-id.d.ts +0 -2
  57. package/dist/auth/record-refused-exchange.d.ts +0 -10
  58. package/dist/auth/remove-client.d.ts +0 -3
  59. package/dist/auth/remove-owner-session.d.ts +0 -3
  60. package/dist/auth/render-consent-page.d.ts +0 -9
  61. package/dist/auth/render-enroll-page.d.ts +0 -10
  62. package/dist/auth/render-login-page.d.ts +0 -9
  63. package/dist/auth/revoke-grant.d.ts +0 -3
  64. package/dist/auth/to-html-text.d.ts +0 -1
  65. package/dist/auth/to-script-json.d.ts +0 -1
  66. package/dist/auth/types.d.ts +0 -79
  67. package/dist/auth/update-passkey-use.d.ts +0 -10
  68. package/dist/auth/verify-oauth-query.d.ts +0 -1
  69. package/dist/auth/verify-passkey-assertion.d.ts +0 -11
  70. package/dist/auth/verify-passkey-registration.d.ts +0 -14
  71. package/dist/auth/verify-session-passkey.d.ts +0 -8
  72. package/dist/auth/write-grant-use.d.ts +0 -3
  73. package/dist/http/build-page-response.d.ts +0 -5
  74. package/dist/http/create-mcp-http-app.d.ts +0 -92
  75. package/dist/http/pick-error-message.d.ts +0 -1
  76. package/dist/http/public-door-paths.d.ts +0 -17
  77. package/dist/http/read-token-access.d.ts +0 -8
  78. package/dist/http/verify-access-token.d.ts +0 -2
  79. package/dist/test-utils/build-mock-new-passkey.d.ts +0 -2
  80. package/dist/test-utils/build-passkey-seeder.d.ts +0 -5
  81. package/dist/test-utils/build-seeder.d.ts +0 -5
  82. package/dist/test-utils/build-stub-authenticator.d.ts +0 -48
  83. package/dist/test-utils/load-web-authn-early.d.ts +0 -1
  84. package/dist/test-utils/seed-passkey.d.ts +0 -4
  85. package/dist/test-utils/start-stub-telemetry-collector.d.ts +0 -5
  86. package/dist/test-utils/types.d.ts +0 -7
  87. package/dist/utils/is-record.d.ts +0 -1
  88. package/dist/utils/parse-json-strings.d.ts +0 -1
  89. package/src/auth/answer-authorize-request.ts +0 -137
  90. package/src/auth/answer-consent-request.ts +0 -189
  91. package/src/auth/answer-enroll-request.ts +0 -190
  92. package/src/auth/answer-login-request.ts +0 -256
  93. package/src/auth/approval-window-seconds.ts +0 -5
  94. package/src/auth/build-consent-binding.ts +0 -13
  95. package/src/auth/create-approval-state.ts +0 -221
  96. package/src/auth/create-enrollment-state.ts +0 -56
  97. package/src/auth/create-enrollment-token.ts +0 -28
  98. package/src/auth/create-owner-plugin.ts +0 -113
  99. package/src/auth/create-passkey.ts +0 -48
  100. package/src/auth/derive-token-hash.ts +0 -7
  101. package/src/auth/is-allowed-redirect-uri.ts +0 -19
  102. package/src/auth/load-web-authn.ts +0 -19
  103. package/src/auth/mcp-auth-database.ts +0 -10
  104. package/src/auth/mcp-auth-schema.ts +0 -56
  105. package/src/auth/open-mcp-auth.ts +0 -138
  106. package/src/auth/passkey-script.ts +0 -50
  107. package/src/auth/read-client-name.ts +0 -17
  108. package/src/auth/read-client-views.ts +0 -27
  109. package/src/auth/read-enrollment-token.ts +0 -20
  110. package/src/auth/read-grant-views.ts +0 -53
  111. package/src/auth/read-owner-session-id.ts +0 -16
  112. package/src/auth/record-refused-exchange.ts +0 -74
  113. package/src/auth/remove-client.ts +0 -33
  114. package/src/auth/remove-owner-session.ts +0 -11
  115. package/src/auth/render-consent-page.ts +0 -61
  116. package/src/auth/render-enroll-page.ts +0 -59
  117. package/src/auth/render-login-page.ts +0 -58
  118. package/src/auth/revoke-grant.ts +0 -25
  119. package/src/auth/to-html-text.ts +0 -10
  120. package/src/auth/to-script-json.ts +0 -8
  121. package/src/auth/types.ts +0 -115
  122. package/src/auth/update-passkey-use.ts +0 -31
  123. package/src/auth/verify-oauth-query.ts +0 -41
  124. package/src/auth/verify-passkey-assertion.ts +0 -124
  125. package/src/auth/verify-passkey-registration.ts +0 -118
  126. package/src/auth/verify-session-passkey.ts +0 -47
  127. package/src/auth/write-grant-use.ts +0 -18
  128. package/src/http/answer-mcp-request.ts +0 -77
  129. package/src/http/build-page-response.ts +0 -26
  130. package/src/http/create-mcp-http-app.ts +0 -340
  131. package/src/http/pick-error-message.ts +0 -21
  132. package/src/http/public-door-paths.ts +0 -21
  133. package/src/http/read-token-access.ts +0 -44
  134. package/src/http/verify-access-token.ts +0 -46
  135. package/src/test-utils/build-mock-new-passkey.ts +0 -15
  136. package/src/test-utils/build-passkey-seeder.ts +0 -11
  137. package/src/test-utils/build-seeder.ts +0 -10
  138. package/src/test-utils/build-stub-authenticator.ts +0 -244
  139. package/src/test-utils/load-web-authn-early.ts +0 -8
  140. package/src/test-utils/seed-passkey.ts +0 -32
  141. package/src/test-utils/start-stub-telemetry-collector.ts +0 -27
  142. package/src/test-utils/types.ts +0 -11
  143. package/src/utils/is-record.ts +0 -5
  144. package/src/utils/parse-json-strings.ts +0 -15
package/README.md CHANGED
@@ -11,8 +11,10 @@
11
11
  </p>
12
12
  </div>
13
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.
14
+ boxxy's public door: it lets the owner's assistant reach the owner's MCP tools from anywhere, with a
15
+ grant that the owner approved with a passkey. The OAuth server is
16
+ [`@zgeoff/mcp-auth`](https://www.npmjs.com/package/@zgeoff/mcp-auth); this package opens it on
17
+ boxxy's auth database, with boxxy's scopes and table names.
16
18
 
17
19
  ## Install
18
20
 
@@ -26,21 +28,18 @@ The package runs on [Bun](https://bun.sh) 1.3 or newer.
26
28
 
27
29
  ## Features
28
30
 
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.
31
+ - `createPublicDoor(options)` opens and migrates the auth database and returns the door:
32
+ `answerRequest` for each request, `addLocalPort` for the loopback address, `admin` for the owner's
33
+ commands, `auth` for the test helpers, and `stop`.
34
+ - The door serves MCP at `/mcp` and at `/`, the OAuth routes, the two `.well-known` documents and
35
+ the sign-in pages, and answers 404 on any other path.
36
+ - It checks the `Host` and `Origin` headers, verifies each bearer token, and passes the verified
37
+ token to the `serveMCP` callback that the app gives it.
36
38
  - It writes each sign-in outcome and each refused token to `writeSecurityLine` as a JSON line.
37
39
  - `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,
40
+ - `./test-utils/authorize-test-client` signs a client in through `door.auth` with a stub passkey,
39
41
  for tests.
40
42
 
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
43
  ## Documentation
45
44
 
46
45
  [The public door](../../docs/design/boxxy/v1-api-and-auth.md#the-public-door) describes the
@@ -1,12 +1,8 @@
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>>;
1
+ import type { GrantScope } from '@boxxy/contract';
2
+ import type { ClientView, MCPAuth } from '@zgeoff/mcp-auth';
6
3
  export interface AuthAdminInput {
7
- readonly store: MCPAuth;
4
+ readonly auth: MCPAuth<GrantScope>;
8
5
  readonly origin: string;
9
- readonly clock: AuthClock;
10
6
  }
11
7
  export type AddClientAnswer = {
12
8
  readonly kind: 'added';
@@ -14,20 +10,37 @@ export type AddClientAnswer = {
14
10
  readonly name: string;
15
11
  } | {
16
12
  readonly kind: 'refused';
17
- readonly redirectURI: string;
13
+ readonly message: string;
14
+ } | {
15
+ readonly kind: 'conflict';
16
+ readonly name: string;
17
+ };
18
+ export type RemoveClientAnswer = {
19
+ readonly kind: 'removed';
20
+ } | {
21
+ readonly kind: 'not-found';
22
+ } | {
23
+ readonly kind: 'ambiguous';
24
+ readonly name: string;
18
25
  };
19
26
  export interface EnrollmentLink {
20
27
  readonly token: string;
21
28
  readonly link: string;
22
- readonly expiresAt: number;
29
+ }
30
+ export interface AuthGrantView {
31
+ readonly grantID: string;
32
+ readonly clientID: string;
33
+ readonly clientName: string;
34
+ readonly scopes: readonly GrantScope[];
35
+ readonly createdAt: string;
36
+ readonly lastUsedAt: string | null;
23
37
  }
24
38
  export interface AuthAdmin {
25
39
  readonly enroll: () => Promise<EnrollmentLink>;
26
40
  readonly addClient: (name: string, redirectURIs: readonly string[]) => Promise<AddClientAnswer>;
27
41
  readonly listClients: () => Promise<readonly ClientView[]>;
28
- readonly removeClient: (clientID: string) => Promise<boolean>;
29
- readonly listGrants: () => Promise<readonly GrantView[]>;
42
+ readonly removeClient: (clientID: string) => Promise<RemoveClientAnswer>;
43
+ readonly listGrants: () => Promise<readonly AuthGrantView[]>;
30
44
  readonly revokeGrant: (grantID: string) => Promise<boolean>;
31
45
  }
32
46
  export declare function createAuthAdmin(input: AuthAdminInput): AuthAdmin;
33
- export {};
@@ -1,5 +1,5 @@
1
+ import type { SecurityEvent } from '@zgeoff/mcp-auth';
1
2
  import type { AuthClock } from '../utils/types.ts';
2
- import type { SecurityEvent } from './types.ts';
3
3
  interface SecurityEventLogOptions {
4
4
  readonly print: (line: string) => void;
5
5
  readonly clock: AuthClock;
@@ -1,7 +1,15 @@
1
- import type { SecurityEvent } from './types.ts';
1
+ import type { SecurityEvent } from '@zgeoff/mcp-auth';
2
+ type LoggedSecurityEvent = Omit<SecurityEvent, 'client' | 'correlationID'> & {
3
+ readonly client?: {
4
+ readonly id: string;
5
+ readonly name: string;
6
+ };
7
+ readonly correlationID?: string;
8
+ };
2
9
  export interface SuppressedSecurityEvent {
3
10
  readonly kind: 'suppressed';
4
11
  readonly reason: SecurityEvent['kind'];
5
12
  readonly count: number;
6
13
  }
7
- export declare function formatSecurityEvent(event: SecurityEvent | SuppressedSecurityEvent, at: number): string;
14
+ export declare function formatSecurityEvent(event: LoggedSecurityEvent | SuppressedSecurityEvent, at: number): string;
15
+ export {};
@@ -1,2 +1,2 @@
1
- import type { MCPAuthDatabase } from './mcp-auth-database.ts';
2
- export declare function openAuthDatabase(path: string): MCPAuthDatabase;
1
+ import { Database } from 'bun:sqlite';
2
+ export declare function openAuthDatabase(path: string): Database;
@@ -1,12 +1,15 @@
1
+ import type { GrantScope } from '@boxxy/contract';
2
+ import type { MCPAuth } from '@zgeoff/mcp-auth';
1
3
  import type { AuthAdmin } from '../auth/create-auth-admin.ts';
2
4
  import type { AuthClock } from '../utils/types.ts';
3
- import type { McpServe } from './answer-mcp-request.ts';
5
+ import type { McpServe } from './types.ts';
4
6
  export interface PublicDoorOptions {
5
7
  readonly publicURL: string;
6
8
  readonly hostname: string;
7
9
  readonly dbPath: string;
8
10
  readonly trustForwardedFrom: readonly string[];
9
11
  readonly writeSecurityLine: (line: string) => void;
12
+ readonly onSweepError: (error: unknown) => void;
10
13
  readonly readPeer: (request: Request) => string | null;
11
14
  readonly serveMCP: McpServe;
12
15
  readonly clock?: AuthClock;
@@ -15,6 +18,7 @@ export interface PublicDoor {
15
18
  readonly answerRequest: (request: Request) => Promise<Response>;
16
19
  readonly addLocalPort: (port: number) => void;
17
20
  readonly admin: AuthAdmin;
21
+ readonly auth: MCPAuth<GrantScope>;
18
22
  readonly stop: () => Promise<void>;
19
23
  }
20
24
  export declare function createPublicDoor(options: PublicDoorOptions): Promise<PublicDoor>;
@@ -1,4 +1,3 @@
1
- import type { HTTPServerContext, Requester } from '../auth/types.ts';
2
1
  export interface McpAuthInfo {
3
2
  readonly token: string;
4
3
  readonly clientId: string;
@@ -6,4 +5,3 @@ export interface McpAuthInfo {
6
5
  readonly resourceMetadataUrl: string;
7
6
  }
8
7
  export type McpServe = (request: Request, authInfo: McpAuthInfo) => Promise<Response>;
9
- export declare function answerMCPRequest(ctx: HTTPServerContext, request: Request, requester: Requester, serve: McpServe): Promise<Response>;
package/dist/index.d.ts CHANGED
@@ -1,8 +1,5 @@
1
- export type { AddClientAnswer, AuthAdmin, EnrollmentLink } from './auth/create-auth-admin.ts';
2
- export type { McpAuthInfo, McpServe } from './http/answer-mcp-request.ts';
1
+ export type { AddClientAnswer, AuthAdmin, AuthGrantView, EnrollmentLink, RemoveClientAnswer, } from './auth/create-auth-admin.ts';
2
+ export type { McpAuthInfo, McpServe } from './http/types.ts';
3
3
  export type { PublicDoor, PublicDoorOptions } from './http/create-public-door.ts';
4
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
5
  export { isLoopbackHost } from './http/is-loopback-host.ts';
@@ -0,0 +1,2 @@
1
+ import type { Migration } from 'kysely/migration';
2
+ export declare const grantRowsMigration: Migration;
@@ -0,0 +1,2 @@
1
+ import type { Database } from 'bun:sqlite';
2
+ export declare function runAuthMigrations(sqlite: Database): Promise<void>;
@@ -1,13 +1 @@
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 {};
1
+ export { authorizeTestClient } from '@zgeoff/mcp-auth/testing';
@@ -0,0 +1 @@
1
+ export declare const PRE_PACKAGE_SCHEMA: string[];
@@ -0,0 +1,21 @@
1
+ import type { Database } from 'bun:sqlite';
2
+ export interface PrePackageGrant {
3
+ readonly clientName: string;
4
+ readonly scopes: readonly string[];
5
+ readonly refreshes: number;
6
+ readonly lastUsedAt: string | null;
7
+ readonly isExpired?: boolean;
8
+ }
9
+ export interface PrePackageAuth {
10
+ readonly origin: string;
11
+ readonly grants: readonly PrePackageGrant[];
12
+ readonly passkeyID: string;
13
+ readonly now: number;
14
+ }
15
+ export interface SeededPrePackageGrant {
16
+ readonly grantID: string;
17
+ readonly clientID: string;
18
+ readonly accessToken: string;
19
+ readonly refreshToken: string;
20
+ }
21
+ export declare function seedPrePackageAuth(sqlite: Database, input: PrePackageAuth): SeededPrePackageGrant[];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@boxxy/auth-oauth",
3
- "version": "0.2.5",
3
+ "version": "0.2.7",
4
4
  "description": "boxxy's public door: an OAuth server for the owner's MCP endpoint, with passkey sign-in",
5
5
  "homepage": "https://github.com/zgeoff/boxxy/tree/main/adapters/auth-oauth#readme",
6
6
  "bugs": "https://github.com/zgeoff/boxxy/issues",
@@ -31,21 +31,15 @@
31
31
  "typecheck": "tsrs --noEmit"
32
32
  },
33
33
  "dependencies": {
34
- "@better-auth/mcp": "1.7.15",
35
- "@better-auth/oauth-provider": "1.7.15",
36
- "@boxxy/contract": "0.2.5",
37
- "@simplewebauthn/server": "14.0.3",
34
+ "@boxxy/contract": "0.2.7",
38
35
  "@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",
36
+ "@zgeoff/mcp-auth": "0.1.0",
37
+ "@zgeoff/std": "0.4.3",
38
+ "elysia": "2.0.0-rc.3",
39
+ "exact-mirror": "1.2.7",
42
40
  "kysely": "0.29.8",
43
41
  "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"
42
+ "typebox": "1.4.3"
49
43
  },
50
44
  "engines": {
51
45
  "bun": ">=1.3",
@@ -1,32 +1,40 @@
1
- import { PUBLIC_DOOR_PATHS } from '../http/public-door-paths.ts';
2
- import type { AuthClock } from '../utils/types.ts';
3
- import { createEnrollmentToken } from './create-enrollment-token.ts';
4
- import { isAllowedRedirectURI } from './is-allowed-redirect-uri.ts';
5
- import { normalizeClientName } from './normalize-client-name.ts';
6
- import type { openMCPAuth } from './open-mcp-auth.ts';
7
- import { readClientViews } from './read-client-views.ts';
8
- import type { ClientView } from './read-client-views.ts';
9
- import { readGrantViews } from './read-grant-views.ts';
10
- import type { GrantView } from './read-grant-views.ts';
11
- import { removeClient } from './remove-client.ts';
12
- import { revokeGrant } from './revoke-grant.ts';
13
-
14
- type MCPAuth = Awaited<ReturnType<typeof openMCPAuth>>;
1
+ import type { GrantScope } from '@boxxy/contract';
2
+ import { grantScopes } from '@boxxy/contract';
3
+ import { MCPAuthError } from '@zgeoff/mcp-auth';
4
+ import type { ClientView, GrantID, GrantView, MCPAuth } from '@zgeoff/mcp-auth';
5
+ import { toTagged } from '@zgeoff/std';
15
6
 
16
7
  export interface AuthAdminInput {
17
- readonly store: MCPAuth;
8
+ readonly auth: MCPAuth<GrantScope>;
18
9
  readonly origin: string;
19
- readonly clock: AuthClock;
20
10
  }
21
11
 
22
12
  export type AddClientAnswer =
23
13
  | { readonly kind: 'added'; readonly clientID: string; readonly name: string }
24
- | { readonly kind: 'refused'; readonly redirectURI: string };
14
+
15
+ // the message says which rule the name or a redirect URI broke
16
+ | { readonly kind: 'refused'; readonly message: string }
17
+ | { readonly kind: 'conflict'; readonly name: string };
18
+
19
+ export type RemoveClientAnswer =
20
+ | { readonly kind: 'removed' }
21
+ | { readonly kind: 'not-found' }
22
+
23
+ // another client has the same name, and the OAuth server removes a client by its name
24
+ | { readonly kind: 'ambiguous'; readonly name: string };
25
25
 
26
26
  export interface EnrollmentLink {
27
27
  readonly token: string;
28
28
  readonly link: string;
29
- readonly expiresAt: number;
29
+ }
30
+
31
+ export interface AuthGrantView {
32
+ readonly grantID: string;
33
+ readonly clientID: string;
34
+ readonly clientName: string;
35
+ readonly scopes: readonly GrantScope[];
36
+ readonly createdAt: string;
37
+ readonly lastUsedAt: string | null;
30
38
  }
31
39
 
32
40
  // What the owner's commands do to the authorization database: add and remove clients, revoke
@@ -35,44 +43,104 @@ export interface AuthAdmin {
35
43
  readonly enroll: () => Promise<EnrollmentLink>;
36
44
  readonly addClient: (name: string, redirectURIs: readonly string[]) => Promise<AddClientAnswer>;
37
45
  readonly listClients: () => Promise<readonly ClientView[]>;
38
- readonly removeClient: (clientID: string) => Promise<boolean>;
39
- readonly listGrants: () => Promise<readonly GrantView[]>;
46
+ readonly removeClient: (clientID: string) => Promise<RemoveClientAnswer>;
47
+ readonly listGrants: () => Promise<readonly AuthGrantView[]>;
40
48
  readonly revokeGrant: (grantID: string) => Promise<boolean>;
41
49
  }
42
50
 
43
51
  export function createAuthAdmin(input: AuthAdminInput): AuthAdmin {
44
- const store = input.store;
45
- const origin = input.origin;
46
- const clock = input.clock;
52
+ const auth = input.auth;
47
53
 
48
54
  return {
49
55
  enroll: async () => {
50
- const created = await createEnrollmentToken(store.db, clock.now());
56
+ const token = await auth.passkeys.createEnrollmentToken();
57
+
58
+ const link = new URL(`${input.origin}${auth.paths.browser.enroll}`);
59
+
60
+ link.searchParams.set('token', token);
51
61
 
52
- return {
53
- token: created.token,
54
- link: `${origin}${PUBLIC_DOOR_PATHS.browser.enroll}?token=${created.token}`,
55
- expiresAt: created.expiresAt,
56
- };
62
+ return { token, link: link.href };
57
63
  },
58
64
  addClient: async (name, redirectURIs) => {
59
- const refused = redirectURIs.find((uri) => !isAllowedRedirectURI(uri));
65
+ try {
66
+ const added = await auth.clients.add(name, redirectURIs);
60
67
 
61
- if (refused !== undefined) {
62
- return { kind: 'refused', redirectURI: refused };
68
+ return { kind: 'added', clientID: added.clientID, name: added.name };
69
+ } catch (error) {
70
+ return pickAddRefusal(error, name);
63
71
  }
72
+ },
73
+ listClients: () => auth.clients.list(),
74
+ removeClient: async (clientID) => {
75
+ const clients = await auth.clients.list();
76
+
77
+ const answer = pickClientToRemove(clients, clientID);
64
78
 
65
- const clientName = normalizeClientName(name);
79
+ if (answer.kind === 'remove') {
80
+ await auth.clients.remove(answer.name);
66
81
 
67
- const created = await store.auth.api.createFixedClient({
68
- body: { name: clientName, redirectURIs: [...redirectURIs] },
69
- });
82
+ return { kind: 'removed' };
83
+ }
70
84
 
71
- return { kind: 'added', clientID: created.clientID, name: clientName };
85
+ return answer;
72
86
  },
73
- listClients: () => readClientViews(store.db),
74
- removeClient: (clientID) => removeClient(store.db, clientID),
75
- listGrants: () => readGrantViews(store.db),
76
- revokeGrant: (grantID) => revokeGrant(store.db, grantID),
87
+ listGrants: async () => {
88
+ const grants = await auth.grants.list();
89
+
90
+ return grants.map((grant) => toAuthGrantView(grant));
91
+ },
92
+
93
+ // The package lists only grants with a live refresh token, and a grant can hold a live access
94
+ // token without one, so the revoke goes to the package by ID.
95
+ revokeGrant: (grantID) => auth.grants.revoke(toTagged<GrantID>(grantID)),
96
+ };
97
+ }
98
+
99
+ // The package refuses a bad name or redirect URI, and a taken name, with a coded error; any other
100
+ // error is not the owner's input.
101
+ function pickAddRefusal(error: unknown, name: string): AddClientAnswer {
102
+ if (!(error instanceof MCPAuthError)) {
103
+ throw error;
104
+ }
105
+
106
+ if (error.code === 'conflict') {
107
+ return { kind: 'conflict', name };
108
+ }
109
+
110
+ if (error.code === 'invalid-client-name' || error.code === 'invalid-redirect-uri') {
111
+ return { kind: 'refused', message: error.message };
112
+ }
113
+
114
+ throw error;
115
+ }
116
+
117
+ type ClientToRemove =
118
+ | { readonly kind: 'remove'; readonly name: string }
119
+ | Exclude<RemoveClientAnswer, { readonly kind: 'removed' }>;
120
+
121
+ function pickClientToRemove(clients: readonly ClientView[], clientID: string): ClientToRemove {
122
+ const client = clients.find((view) => view.clientID === clientID);
123
+
124
+ if (client === undefined) {
125
+ return { kind: 'not-found' };
126
+ }
127
+
128
+ const isShared = clients.some(
129
+ (view) => view.name === client.name && view.clientID !== client.clientID,
130
+ );
131
+
132
+ return isShared
133
+ ? { kind: 'ambiguous', name: client.name }
134
+ : { kind: 'remove', name: client.name };
135
+ }
136
+
137
+ function toAuthGrantView(grant: GrantView): AuthGrantView {
138
+ return {
139
+ grantID: grant.grantID,
140
+ clientID: grant.clientID,
141
+ clientName: grant.clientName,
142
+ scopes: grantScopes.filter((scope) => grant.scopes.includes(scope)),
143
+ createdAt: grant.createdAt,
144
+ lastUsedAt: grant.lastUsedAt,
77
145
  };
78
146
  }
@@ -1,8 +1,6 @@
1
- // Copied from atc: packages/mcp/src/auth/create-security-event-log.ts at commit 4fe647cf.
2
-
1
+ import type { SecurityEvent } from '@zgeoff/mcp-auth';
3
2
  import type { AuthClock } from '../utils/types.ts';
4
3
  import { formatSecurityEvent } from './format-security-event.ts';
5
- import type { SecurityEvent } from './types.ts';
6
4
 
7
5
  interface SecurityEventLogOptions {
8
6
  // Receives each `{json}` line.
@@ -1,8 +1,12 @@
1
- // Copied from atc: packages/mcp/src/auth/format-security-event.ts at commit 4fe647cf.
2
-
3
- import { PUBLIC_DOOR_PATHS } from '../http/public-door-paths.ts';
1
+ import { DEFAULT_PATHS } from '@zgeoff/mcp-auth';
2
+ import type { SecurityEvent } from '@zgeoff/mcp-auth';
4
3
  import { normalizeClientName } from './normalize-client-name.ts';
5
- import type { SecurityEvent } from './types.ts';
4
+
5
+ // The IDs in an event are tagged strings, and a line needs only their text.
6
+ type LoggedSecurityEvent = Omit<SecurityEvent, 'client' | 'correlationID'> & {
7
+ readonly client?: { readonly id: string; readonly name: string };
8
+ readonly correlationID?: string;
9
+ };
6
10
 
7
11
  export interface SuppressedSecurityEvent {
8
12
  readonly kind: 'suppressed';
@@ -12,11 +16,11 @@ export interface SuppressedSecurityEvent {
12
16
 
13
17
  // The paths an event may hold as its route.
14
18
  const ROUTES: ReadonlySet<string> = new Set(
15
- Object.values(PUBLIC_DOOR_PATHS).flatMap((group) => Object.values(group)),
19
+ Object.values(DEFAULT_PATHS).flatMap((group) => Object.values(group)),
16
20
  );
17
21
 
18
22
  export function formatSecurityEvent(
19
- event: SecurityEvent | SuppressedSecurityEvent,
23
+ event: LoggedSecurityEvent | SuppressedSecurityEvent,
20
24
  at: number,
21
25
  ): string {
22
26
  const head = { version: 1, ts: new Date(at).toISOString(), kind: event.kind };
@@ -30,9 +34,9 @@ export function formatSecurityEvent(
30
34
  const fields = {
31
35
  ...head,
32
36
  outcome: event.outcome,
33
- ...('reason' in event && event.reason !== undefined ? { reason: event.reason } : {}),
37
+ ...(event.reason === undefined ? {} : { reason: event.reason }),
34
38
  ...(event.kind === 'owner_auth'
35
- ? { method: event.path === PUBLIC_DOOR_PATHS.browser.enroll ? 'enrollment_token' : 'passkey' }
39
+ ? { method: event.path === DEFAULT_PATHS.browser.enroll ? 'enrollment_token' : 'passkey' }
36
40
  : {}),
37
41
  route: ROUTES.has(event.path) ? event.path : 'other',
38
42
  ...(event.client === undefined
@@ -43,9 +47,6 @@ export function formatSecurityEvent(
43
47
  name: toEventText(event.client.name, 100),
44
48
  },
45
49
  }),
46
- ...(event.redirectHost === undefined
47
- ? {}
48
- : { redirectHost: toEventText(event.redirectHost, 255) }),
49
50
  ...(event.correlationID === undefined ? {} : { correlationID: event.correlationID }),
50
51
  ...(requester.socketAddress === null
51
52
  ? {}
@@ -1,5 +1,3 @@
1
- // Copied from atc: packages/mcp/src/auth/normalize-client-name.ts at commit 4fe647cf.
2
-
3
1
  export function normalizeClientName(
4
2
  raw: unknown,
5
3
  fallback = 'unnamed client',
@@ -1,22 +1,17 @@
1
- // Adapted from atc: packages/mcp/src/testing/open-test-auth-database.ts at commit 4fe647cf.
2
-
3
1
  import { Database } from 'bun:sqlite';
4
- import { buildSQLiteDialect } from '@zgeoff/kysely-bun-sqlite';
5
- import type { MCPAuthDatabase } from './mcp-auth-database.ts';
2
+ import { chmodSync, closeSync, openSync } from 'node:fs';
3
+
4
+ // The file holds token hashes and sessions, so only its owner may read it. SQLite gives the WAL and
5
+ // shared-memory files the main file's mode, so the mode is set before SQLite opens the file.
6
+ export function openAuthDatabase(path: string): Database {
7
+ closeSync(openSync(path, 'a', 0o600));
8
+ chmodSync(path, 0o600);
6
9
 
7
- // A write waits for another process's lock, and the cascades from a client to its tokens need
8
- // foreign keys.
9
- export function openAuthDatabase(path: string): MCPAuthDatabase {
10
10
  const sqlite = new Database(path, { create: true });
11
11
 
12
12
  sqlite.run('PRAGMA journal_mode = WAL;');
13
13
  sqlite.run('PRAGMA busy_timeout = 5000;');
14
14
  sqlite.run('PRAGMA foreign_keys = ON;');
15
15
 
16
- return {
17
- dialect: buildSQLiteDialect(sqlite),
18
- close: () => {
19
- sqlite.close();
20
- },
21
- };
16
+ return sqlite;
22
17
  }