@boxxy/auth-oauth 0.2.6 → 0.2.8

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 (145) hide show
  1. package/README.md +12 -13
  2. package/dist/auth/create-auth-admin.d.ts +22 -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 +91 -42
  16. package/src/auth/create-security-event-log.ts +1 -3
  17. package/src/auth/format-security-event.ts +11 -11
  18. package/src/auth/open-auth-database.ts +8 -13
  19. package/src/http/create-public-door.ts +113 -29
  20. package/src/http/is-loopback-host.ts +0 -2
  21. package/src/http/normalize-public-url.ts +0 -2
  22. package/src/http/types.ts +11 -0
  23. package/src/index.ts +9 -5
  24. package/src/migrations/grant-rows-migration.ts +108 -0
  25. package/src/migrations/run-auth-migrations.ts +53 -0
  26. package/src/test-utils/authorize-test-client.ts +2 -194
  27. package/src/test-utils/build-stub-clock.ts +0 -2
  28. package/src/test-utils/pre-package-schema.ts +36 -0
  29. package/src/test-utils/register-test-cleanup.ts +0 -2
  30. package/src/test-utils/seed-pre-package-auth.ts +162 -0
  31. package/src/test-utils/setup-temp-dir.ts +0 -2
  32. package/src/utils/types.ts +0 -2
  33. package/dist/auth/answer-authorize-request.d.ts +0 -2
  34. package/dist/auth/answer-consent-request.d.ts +0 -10
  35. package/dist/auth/answer-enroll-request.d.ts +0 -9
  36. package/dist/auth/answer-login-request.d.ts +0 -9
  37. package/dist/auth/approval-window-seconds.d.ts +0 -1
  38. package/dist/auth/build-consent-binding.d.ts +0 -1
  39. package/dist/auth/create-approval-state.d.ts +0 -41
  40. package/dist/auth/create-enrollment-state.d.ts +0 -6
  41. package/dist/auth/create-enrollment-token.d.ts +0 -6
  42. package/dist/auth/create-owner-plugin.d.ts +0 -38
  43. package/dist/auth/create-passkey.d.ts +0 -11
  44. package/dist/auth/derive-token-hash.d.ts +0 -1
  45. package/dist/auth/is-allowed-redirect-uri.d.ts +0 -1
  46. package/dist/auth/load-web-authn.d.ts +0 -6
  47. package/dist/auth/mcp-auth-database.d.ts +0 -5
  48. package/dist/auth/mcp-auth-schema.d.ts +0 -54
  49. package/dist/auth/normalize-client-name.d.ts +0 -1
  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/normalize-client-name.ts +0 -22
  106. package/src/auth/open-mcp-auth.ts +0 -138
  107. package/src/auth/passkey-script.ts +0 -50
  108. package/src/auth/read-client-name.ts +0 -17
  109. package/src/auth/read-client-views.ts +0 -27
  110. package/src/auth/read-enrollment-token.ts +0 -20
  111. package/src/auth/read-grant-views.ts +0 -53
  112. package/src/auth/read-owner-session-id.ts +0 -16
  113. package/src/auth/record-refused-exchange.ts +0 -74
  114. package/src/auth/remove-client.ts +0 -33
  115. package/src/auth/remove-owner-session.ts +0 -11
  116. package/src/auth/render-consent-page.ts +0 -61
  117. package/src/auth/render-enroll-page.ts +0 -59
  118. package/src/auth/render-login-page.ts +0 -58
  119. package/src/auth/revoke-grant.ts +0 -25
  120. package/src/auth/to-html-text.ts +0 -10
  121. package/src/auth/to-script-json.ts +0 -8
  122. package/src/auth/types.ts +0 -115
  123. package/src/auth/update-passkey-use.ts +0 -31
  124. package/src/auth/verify-oauth-query.ts +0 -41
  125. package/src/auth/verify-passkey-assertion.ts +0 -124
  126. package/src/auth/verify-passkey-registration.ts +0 -118
  127. package/src/auth/verify-session-passkey.ts +0 -47
  128. package/src/auth/write-grant-use.ts +0 -18
  129. package/src/http/answer-mcp-request.ts +0 -77
  130. package/src/http/build-page-response.ts +0 -26
  131. package/src/http/create-mcp-http-app.ts +0 -340
  132. package/src/http/pick-error-message.ts +0 -21
  133. package/src/http/public-door-paths.ts +0 -21
  134. package/src/http/read-token-access.ts +0 -44
  135. package/src/http/verify-access-token.ts +0 -46
  136. package/src/test-utils/build-mock-new-passkey.ts +0 -15
  137. package/src/test-utils/build-passkey-seeder.ts +0 -11
  138. package/src/test-utils/build-seeder.ts +0 -10
  139. package/src/test-utils/build-stub-authenticator.ts +0 -244
  140. package/src/test-utils/load-web-authn-early.ts +0 -8
  141. package/src/test-utils/seed-passkey.ts +0 -32
  142. package/src/test-utils/start-stub-telemetry-collector.ts +0 -27
  143. package/src/test-utils/types.ts +0 -11
  144. package/src/utils/is-record.ts +0 -5
  145. package/src/utils/parse-json-strings.ts +0 -15
@@ -1,8 +0,0 @@
1
- // Copied from atc: packages/mcp/src/auth/to-script-json.ts at commit 4fe647cf.
2
-
3
- export function toScriptJSON(value: unknown): string {
4
- return JSON.stringify(value)
5
- .replaceAll('<', String.raw`\u003c`)
6
- .replaceAll('>', String.raw`\u003e`)
7
- .replaceAll('&', String.raw`\u0026`);
8
- }
package/src/auth/types.ts DELETED
@@ -1,115 +0,0 @@
1
- // Copied from atc: packages/mcp/src/auth/types.ts at commit 4fe647cf.
2
-
3
- import type { GrantScope } from '@boxxy/contract';
4
- import type { AuthClock } from '../utils/types.ts';
5
- import type { ApprovalState } from './create-approval-state.ts';
6
- import type { EnrollmentState } from './create-enrollment-state.ts';
7
- import type { openMCPAuth } from './open-mcp-auth.ts';
8
-
9
- export interface Requester {
10
- readonly socketAddress: string | null;
11
-
12
- // the `CF-Connecting-IP` header. Anyone can send it, so it is `trusted`
13
- // only when the socket's peer is a proxy the owner listed.
14
- readonly connectingIP: {
15
- readonly address: string;
16
- readonly provenance: 'trusted' | 'unverified';
17
- } | null;
18
- readonly userAgent: string | null;
19
- }
20
-
21
- export type SecurityOutcome =
22
- | { readonly kind: 'authorize_started'; readonly outcome: 'accepted' }
23
- | {
24
- readonly kind: 'authorize_started';
25
- readonly outcome: 'rejected';
26
- readonly reason: 'rate_limited';
27
- }
28
- | {
29
- readonly kind: 'authorize_invalid';
30
- readonly outcome: 'rejected';
31
- readonly reason: AuthorizeInvalidReason;
32
- }
33
- | { readonly kind: 'owner_auth'; readonly outcome: 'succeeded' }
34
- | {
35
- readonly kind: 'owner_auth';
36
- readonly outcome: 'failed';
37
- readonly reason?: OwnerAuthFailure;
38
- }
39
- | { readonly kind: 'consent'; readonly outcome: 'allowed' }
40
- | { readonly kind: 'consent'; readonly outcome: 'denied'; readonly reason?: 'none_ticked' }
41
- | {
42
- readonly kind: 'token_exchange';
43
- readonly outcome: 'rejected';
44
- readonly reason: TokenExchangeReason;
45
- }
46
- | { readonly kind: 'bearer'; readonly outcome: 'rejected'; readonly reason: BearerReason };
47
-
48
- export type OwnerAuthFailure =
49
- | 'assertion_failed'
50
- | 'locked'
51
- | 'expired'
52
- | 'not_enrolled'
53
- | 'invalid_token';
54
-
55
- export type AuthorizeInvalidReason =
56
- | 'invalid_client'
57
- | 'client_disabled'
58
- | 'invalid_redirect'
59
- | 'unauthorized_client'
60
- | 'unsupported_response_type'
61
- | 'invalid_request'
62
- | 'invalid_scope'
63
- | 'invalid_target'
64
- | 'bad_host'
65
- | 'bad_origin'
66
- | 'other';
67
-
68
- export type TokenExchangeReason =
69
- | 'invalid_request'
70
- | 'invalid_client'
71
- | 'invalid_grant'
72
- | 'unauthorized_client'
73
- | 'unsupported_grant_type'
74
- | 'invalid_scope'
75
- | 'invalid_target'
76
- | 'other';
77
-
78
- type BearerReason = 'malformed' | 'invalid_token';
79
-
80
- export type SecurityEvent = SecurityOutcome & {
81
- readonly path: string;
82
- readonly requester: Requester;
83
- readonly client?: { readonly id: string; readonly name: string };
84
- readonly redirectHost?: string;
85
- readonly correlationID?: string;
86
- };
87
-
88
- // What a valid bearer token grants: the client it was issued to, and the
89
- // scopes it holds.
90
- export interface VerifiedAccess {
91
- readonly clientID: string;
92
- readonly clientName: string;
93
- readonly scopes: readonly GrantScope[];
94
- }
95
-
96
- type MCPAuth = Awaited<ReturnType<typeof openMCPAuth>>;
97
-
98
- // What every HTTP route reads: the authorization server, the approvals waiting for the owner, and
99
- // the passkey enrollments in progress.
100
- export interface HTTPServerContext {
101
- // The public origin: the OAuth issuer.
102
- readonly origin: string;
103
-
104
- // `<origin>/mcp`: the one resource every token is bound to.
105
- readonly resource: string;
106
- readonly store: MCPAuth;
107
- readonly approvals: ApprovalState;
108
- readonly enrollments: EnrollmentState;
109
-
110
- // Receives each sign-in outcome and each refused token as a security event.
111
- readonly security: { readonly record: (event: SecurityEvent) => void };
112
-
113
- // The time the routes stamp and check expiries against.
114
- readonly clock: AuthClock;
115
- }
@@ -1,31 +0,0 @@
1
- // Copied from atc: packages/mcp/src/auth/update-passkey-use.ts at commit 4fe647cf.
2
-
3
- import type { Kysely } from 'kysely';
4
- import type { MCPAuthSchema } from './mcp-auth-schema.ts';
5
-
6
- interface PasskeyUse {
7
- readonly id: string;
8
-
9
- // The counter the verification read, which the passkey must still hold,
10
- // so a passkey removed during the verification, or a concurrent assertion
11
- // that stored a newer counter, makes this use fail.
12
- readonly counter: number;
13
- readonly newCounter: number;
14
-
15
- // Epoch milliseconds, stamped as the passkey's last use.
16
- readonly now: number;
17
- }
18
-
19
- export async function updatePasskeyUse(
20
- db: Kysely<MCPAuthSchema>,
21
- use: PasskeyUse,
22
- ): Promise<boolean> {
23
- const stored = await db
24
- .updateTable('boxxy_passkey')
25
- .set({ counter: use.newCounter, last_used_at: new Date(use.now).toISOString() })
26
- .where('id', '=', use.id)
27
- .where('counter', '=', use.counter)
28
- .executeTakeFirst();
29
-
30
- return stored.numUpdatedRows > 0n;
31
- }
@@ -1,41 +0,0 @@
1
- // Copied from atc: packages/mcp/src/auth/verify-oauth-query.ts at commit 4fe647cf.
2
-
3
- import { constantTimeEqual, makeSignature } from 'better-auth/crypto';
4
-
5
- export async function verifyOAuthQuery(
6
- oauthQuery: string,
7
- secret: string,
8
- now: number,
9
- ): Promise<boolean> {
10
- const params = new URLSearchParams(oauthQuery);
11
-
12
- const signatures = params.getAll('sig');
13
- const [signature] = signatures;
14
- const expiresAt = Number(params.get('exp')) * 1000;
15
-
16
- if (signatures.length !== 1 || signature === undefined || signature === '') {
17
- return false;
18
- }
19
-
20
- params.delete('sig');
21
-
22
- // Ordered by name, then by value, comparing code units as better-auth does
23
- // when it signs; the NUL separator sorts a name before any longer name it
24
- // prefixes.
25
- const canonical = new URLSearchParams(
26
- [...params].toSorted(([keyA, valueA], [keyB, valueB]) => {
27
- const pairA = `${keyA}\u0000${valueA}`;
28
- const pairB = `${keyB}\u0000${valueB}`;
29
-
30
- if (pairA === pairB) {
31
- return 0;
32
- }
33
-
34
- return pairA < pairB ? -1 : 1;
35
- }),
36
- );
37
-
38
- const expected = await makeSignature(canonical.toString(), secret);
39
-
40
- return constantTimeEqual(signature, expected) && Number.isFinite(expiresAt) && expiresAt >= now;
41
- }
@@ -1,124 +0,0 @@
1
- // Copied from atc: packages/mcp/src/auth/verify-passkey-assertion.ts at commit 4fe647cf.
2
-
3
- import type {
4
- AuthenticationResponseJSON,
5
- VerifiedAuthenticationResponse,
6
- } from '@simplewebauthn/server';
7
- import type { Kysely } from 'kysely';
8
- import { z } from 'zod';
9
- import { parseJSONStrings } from '../utils/parse-json-strings.ts';
10
- import { loadWebAuthn } from './load-web-authn.ts';
11
- import type { MCPAuthSchema } from './mcp-auth-schema.ts';
12
- import { updatePasskeyUse } from './update-passkey-use.ts';
13
-
14
- interface AssertionCheck {
15
- // The credential JSON the sign-in page posted.
16
- readonly credential: string;
17
-
18
- // The challenge the page was given, which the caller has already taken.
19
- readonly challenge: string;
20
- readonly origin: string;
21
- readonly rpID: string;
22
-
23
- // Epoch milliseconds, stamped as the passkey's last use.
24
- readonly now: number;
25
- }
26
-
27
- export async function verifyPasskeyAssertion(
28
- db: Kysely<MCPAuthSchema>,
29
- check: AssertionCheck,
30
- ): Promise<string | null> {
31
- const response = parseAssertion(check.credential);
32
-
33
- if (response === null) {
34
- return null;
35
- }
36
-
37
- const passkey = await db
38
- .selectFrom('boxxy_passkey')
39
- .select(['id', 'public_key', 'counter', 'transports'])
40
- .where('id', '=', response.id)
41
- .executeTakeFirst();
42
-
43
- if (passkey === undefined) {
44
- return null;
45
- }
46
-
47
- const webAuthn = await loadWebAuthn();
48
-
49
- let verified: VerifiedAuthenticationResponse;
50
-
51
- try {
52
- verified = await webAuthn.verifyAuthenticationResponse({
53
- response,
54
- expectedChallenge: check.challenge,
55
- expectedOrigin: check.origin,
56
- expectedRPID: check.rpID,
57
- expectedType: 'webauthn.get',
58
- requireUserVerification: true,
59
- credential: {
60
- id: passkey.id,
61
- publicKey: new Uint8Array(Buffer.from(passkey.public_key, 'base64url')),
62
- counter: passkey.counter,
63
- transports: parseJSONStrings(passkey.transports) ?? [],
64
- },
65
- });
66
- } catch {
67
- return null;
68
- }
69
-
70
- if (!verified.verified || !verified.authenticationInfo.userVerified) {
71
- return null;
72
- }
73
-
74
- const stored = await updatePasskeyUse(db, {
75
- id: passkey.id,
76
- counter: passkey.counter,
77
- newCounter: verified.authenticationInfo.newCounter,
78
- now: check.now,
79
- });
80
-
81
- return stored ? passkey.id : null;
82
- }
83
-
84
- const ASSERTION_REPLY = z.object({
85
- clientDataJSON: z.string(),
86
- authenticatorData: z.string(),
87
- signature: z.string(),
88
- userHandle: z.string().optional(),
89
- });
90
-
91
- const ASSERTION = z.object({
92
- id: z.string().min(1),
93
- rawId: z.string().min(1),
94
- type: z.literal('public-key'),
95
- response: ASSERTION_REPLY,
96
- });
97
-
98
- // The posted JSON when it has the shape of an authentication response,
99
- // else null; the verifier checks every value.
100
- function parseAssertion(credential: string): AuthenticationResponseJSON | null {
101
- let parsed: unknown;
102
-
103
- try {
104
- parsed = JSON.parse(credential);
105
- } catch {
106
- return null;
107
- }
108
-
109
- const result = ASSERTION.safeParse(parsed);
110
-
111
- if (!result.success) {
112
- return null;
113
- }
114
-
115
- const { userHandle, ...response } = result.data.response;
116
-
117
- return {
118
- id: result.data.id,
119
- rawId: result.data.rawId,
120
- type: result.data.type,
121
- response: userHandle === undefined ? response : { ...response, userHandle },
122
- clientExtensionResults: {},
123
- };
124
- }
@@ -1,118 +0,0 @@
1
- // Copied from atc: packages/mcp/src/auth/verify-passkey-registration.ts at commit 4fe647cf.
2
-
3
- import type { RegistrationResponseJSON } from '@simplewebauthn/server';
4
- import { z } from 'zod';
5
- import { loadWebAuthn } from './load-web-authn.ts';
6
-
7
- interface RegistrationCheck {
8
- // The credential JSON the enrollment page posted.
9
- readonly credential: string;
10
-
11
- // The challenge the page was given, which the caller has already taken.
12
- readonly challenge: string;
13
- readonly origin: string;
14
- readonly rpID: string;
15
- }
16
-
17
- export interface RegisteredPasskey {
18
- readonly id: string;
19
- readonly publicKey: Uint8Array;
20
- readonly counter: number;
21
- readonly transports: readonly string[];
22
- }
23
-
24
- export async function verifyPasskeyRegistration(
25
- check: RegistrationCheck,
26
- ): Promise<RegisteredPasskey | null> {
27
- const response = parseRegistration(check.credential);
28
-
29
- if (response === null) {
30
- return null;
31
- }
32
-
33
- const webAuthn = await loadWebAuthn();
34
-
35
- try {
36
- const verified = await webAuthn.verifyRegistrationResponse({
37
- response,
38
- expectedChallenge: check.challenge,
39
- expectedOrigin: check.origin,
40
- expectedRPID: check.rpID,
41
- expectedType: 'webauthn.create',
42
- requireUserVerification: true,
43
- });
44
-
45
- if (!verified.verified || !verified.registrationInfo.userVerified) {
46
- return null;
47
- }
48
-
49
- const credential = verified.registrationInfo.credential;
50
-
51
- return {
52
- id: credential.id,
53
- publicKey: credential.publicKey,
54
- counter: credential.counter,
55
- transports: response.response.transports ?? [],
56
- };
57
- } catch {
58
- return null;
59
- }
60
- }
61
-
62
- const TRANSPORTS: ReadonlySet<string> = new Set([
63
- 'ble',
64
- 'cable',
65
- 'hybrid',
66
- 'internal',
67
- 'nfc',
68
- 'smart-card',
69
- 'usb',
70
- ]);
71
-
72
- const REGISTRATION_REPLY = z.object({
73
- clientDataJSON: z.string(),
74
- attestationObject: z.string(),
75
- transports: z.string().array().optional(),
76
- });
77
-
78
- const REGISTRATION = z.object({
79
- id: z.string().min(1),
80
- rawId: z.string().min(1),
81
- type: z.literal('public-key'),
82
- response: REGISTRATION_REPLY,
83
- });
84
-
85
- // The posted JSON when it has the shape of a registration response, keeping
86
- // only the transports WebAuthn defines, else null; the verifier checks
87
- // every value.
88
- function parseRegistration(credential: string): RegistrationResponseJSON | null {
89
- let parsed: unknown;
90
-
91
- try {
92
- parsed = JSON.parse(credential);
93
- } catch {
94
- return null;
95
- }
96
-
97
- const result = REGISTRATION.safeParse(parsed);
98
-
99
- if (!result.success) {
100
- return null;
101
- }
102
-
103
- const transports = (result.data.response.transports ?? []).filter((transport) =>
104
- TRANSPORTS.has(transport),
105
- );
106
-
107
- return {
108
- id: result.data.id,
109
- rawId: result.data.rawId,
110
- type: result.data.type,
111
- response: {
112
- clientDataJSON: result.data.response.clientDataJSON,
113
- attestationObject: result.data.response.attestationObject,
114
- transports,
115
- },
116
- clientExtensionResults: {},
117
- };
118
- }
@@ -1,47 +0,0 @@
1
- // Copied from atc: packages/mcp/src/auth/verify-session-passkey.ts at commit 4fe647cf.
2
-
3
- import type { Kysely } from 'kysely';
4
- import type { MCPAuthSchema } from './mcp-auth-schema.ts';
5
-
6
- interface SessionPasskey {
7
- // The owner session a passkey sign-in just created.
8
- readonly sessionID: string;
9
-
10
- // The passkey whose assertion signed the owner in.
11
- readonly passkeyID: string;
12
- }
13
-
14
- export async function verifySessionPasskey(
15
- db: Kysely<MCPAuthSchema>,
16
- check: SessionPasskey,
17
- ): Promise<boolean> {
18
- try {
19
- return await db.transaction().execute(async (trx) => {
20
- const passkey = await trx
21
- .selectFrom('boxxy_passkey')
22
- .select('id')
23
- .where('id', '=', check.passkeyID)
24
- .executeTakeFirst();
25
-
26
- const session = await trx
27
- .selectFrom('session')
28
- .select('id')
29
- .where('id', '=', check.sessionID)
30
- .executeTakeFirst();
31
-
32
- if (passkey !== undefined && session !== undefined) {
33
- return true;
34
- }
35
-
36
- await trx.deleteFrom('session').where('id', '=', check.sessionID).execute();
37
-
38
- return false;
39
- });
40
- } catch {
41
- // a transaction that fails, such as one a concurrent write keeps busy, still
42
- // removes the session.
43
- await db.deleteFrom('session').where('id', '=', check.sessionID).execute();
44
-
45
- return false;
46
- }
47
- }
@@ -1,18 +0,0 @@
1
- // Copied from atc: packages/mcp/src/auth/write-grant-use.ts at commit 4fe647cf.
2
-
3
- import type { Kysely } from 'kysely';
4
- import type { MCPAuthSchema } from './mcp-auth-schema.ts';
5
-
6
- export async function writeGrantUse(
7
- db: Kysely<MCPAuthSchema>,
8
- grantID: string,
9
- now: number,
10
- ): Promise<void> {
11
- const lastUsedAt = new Date(now).toISOString();
12
-
13
- await db
14
- .insertInto('boxxy_grant_use')
15
- .values({ grant_id: grantID, last_used_at: lastUsedAt })
16
- .onConflict((conflict) => conflict.column('grant_id').doUpdateSet({ last_used_at: lastUsedAt }))
17
- .execute();
18
- }
@@ -1,77 +0,0 @@
1
- // Copied from atc: packages/mcp/src/http/answer-mcp-request.ts at commit 4fe647cf.
2
-
3
- import type { HTTPServerContext, Requester } from '../auth/types.ts';
4
- import { PUBLIC_DOOR_PATHS } from './public-door-paths.ts';
5
- import { verifyAccessToken } from './verify-access-token.ts';
6
-
7
- // What the MCP transport reads of a verified token: the scopes it holds, and the resource metadata
8
- // that a scope challenge points at.
9
- export interface McpAuthInfo {
10
- readonly token: string;
11
- readonly clientId: string;
12
- readonly scopes: string[];
13
- readonly resourceMetadataUrl: string;
14
- }
15
-
16
- // Serves one MCP request for a token that verified.
17
- export type McpServe = (request: Request, authInfo: McpAuthInfo) => Promise<Response>;
18
-
19
- // `requester` is where the request came from, for its security events.
20
- export async function answerMCPRequest(
21
- ctx: HTTPServerContext,
22
- request: Request,
23
- requester: Requester,
24
- serve: McpServe,
25
- ): Promise<Response> {
26
- const path = new URL(request.url).pathname;
27
-
28
- const metadataURL = `${ctx.origin}${PUBLIC_DOOR_PATHS.machine.mcpResourceMetadata}`;
29
- const authorization = request.headers.get('authorization');
30
- const token = /^Bearer (?<token>\S+)$/iu.exec(authorization ?? '')?.groups?.['token'];
31
-
32
- if (token === undefined) {
33
- // a request with no header at all is a client discovering the
34
- // authorization server, so only a header that holds no bearer token is
35
- // recorded.
36
- if (authorization !== null) {
37
- ctx.security.record({
38
- kind: 'bearer',
39
- outcome: 'rejected',
40
- reason: 'malformed',
41
- path,
42
- requester,
43
- });
44
- }
45
-
46
- return new Response(null, {
47
- status: 401,
48
- headers: { 'www-authenticate': `Bearer resource_metadata="${metadataURL}"` },
49
- });
50
- }
51
-
52
- const access = await verifyAccessToken(ctx, token);
53
-
54
- if (access === null) {
55
- ctx.security.record({
56
- kind: 'bearer',
57
- outcome: 'rejected',
58
- reason: 'invalid_token',
59
- path,
60
- requester,
61
- });
62
-
63
- return new Response(null, {
64
- status: 401,
65
- headers: {
66
- 'www-authenticate': `Bearer error="invalid_token", resource_metadata="${metadataURL}"`,
67
- },
68
- });
69
- }
70
-
71
- return serve(request, {
72
- token,
73
- clientId: access.clientID,
74
- scopes: [...access.scopes],
75
- resourceMetadataUrl: metadataURL,
76
- });
77
- }
@@ -1,26 +0,0 @@
1
- // Copied from atc: packages/mcp/src/http/build-page-response.ts at commit 4fe647cf.
2
-
3
- import { toHTMLText } from '../auth/to-html-text.ts';
4
-
5
- export function buildPageResponse(
6
- status: number,
7
- body: { readonly page: string } | { readonly message: string },
8
- formTargets: readonly string[] = [],
9
- scriptSources: readonly string[] = [],
10
- ): Response {
11
- const html =
12
- 'page' in body
13
- ? body.page
14
- : `<!doctype html><html lang="en"><head><meta charset="utf-8"><title>boxxy</title></head><body><p>${toHTMLText(body.message)}</p></body></html>`;
15
-
16
- return new Response(html, {
17
- status,
18
- headers: {
19
- 'content-type': 'text/html; charset=utf-8',
20
- 'content-security-policy': `default-src 'none'; style-src 'unsafe-inline';${scriptSources.length === 0 ? '' : ` script-src ${scriptSources.join(' ')};`} form-action 'self'${formTargets.map((target) => ` ${target}`).join('')}; frame-ancestors 'none'; base-uri 'none'`,
21
- 'x-frame-options': 'DENY',
22
- 'cache-control': 'no-store',
23
- 'referrer-policy': 'same-origin',
24
- },
25
- });
26
- }