@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,221 +0,0 @@
1
- // Copied from atc: packages/mcp/src/auth/create-approval-state.ts at commit 4fe647cf.
2
-
3
- import { randomBytes } from 'node:crypto';
4
-
5
- interface ApprovalRequest {
6
- // The signed authorization query the login page carries: one per request.
7
- readonly key: string;
8
- readonly clientID: string;
9
- readonly clientName: string;
10
- readonly redirectURI: string;
11
- }
12
-
13
- interface PendingApproval extends ApprovalRequest {
14
- // Random, minted with the approval and never taken from the request, so
15
- // the security events of one authorization can be matched up.
16
- readonly correlationID: string;
17
- failures: number;
18
- readonly expiresAt: number;
19
-
20
- // The one WebAuthn challenge this request's login page holds, if any.
21
- challenge: { readonly value: string; readonly expiresAt: number } | null;
22
- }
23
-
24
- export type TakenChallenge =
25
- | { readonly kind: 'taken'; readonly challenge: string }
26
- | { readonly kind: 'expired' }
27
- | { readonly kind: 'missing' };
28
-
29
- // At most this many approvals wait at once across every client, and each
30
- // client holds at most this many. Across every client, at most this many
31
- // approvals start per minute.
32
- const MAX_PENDING = 16;
33
- const MAX_PENDING_PER_CLIENT = 3;
34
- const MAX_STARTS_PER_MINUTE = 10;
35
- const MAX_FAILURES = 5;
36
- const MINUTE_MS = 60_000;
37
-
38
- // How long a login page's challenge stays valid, unless its request
39
- // expires sooner.
40
- const CHALLENGE_MS = 5 * MINUTE_MS;
41
-
42
- interface ApprovedRequest {
43
- readonly binding: string;
44
- readonly correlationID: string;
45
- readonly expiresAt: number;
46
- }
47
-
48
- export interface ApprovalState {
49
- // Returns null when the last minute already holds its share of starts.
50
- readonly createPending: (request: ApprovalRequest) => PendingApproval | null;
51
-
52
- readonly findPending: (key: string) => PendingApproval | null;
53
-
54
- // A fresh random challenge for the request's login page, replacing any
55
- // earlier one, or null when the request no longer waits.
56
- readonly mintChallenge: (key: string) => string | null;
57
-
58
- // Takes the request's challenge, so it verifies at most one assertion.
59
- readonly claimChallenge: (key: string) => TakenChallenge;
60
-
61
- // A failed sign-in counts against the request; the last allowed failure
62
- // drops it.
63
- readonly recordFailure: (key: string) => 'retry' | 'locked';
64
-
65
- readonly removePending: (key: string) => void;
66
-
67
- readonly recordApproved: (
68
- sessionID: string,
69
- approved: Omit<ApprovedRequest, 'expiresAt'>,
70
- ) => void;
71
-
72
- // What an owner session approved, or null once it expired or went.
73
- readonly findApproved: (sessionID: string) => ApprovedRequest | null;
74
-
75
- readonly removeApproved: (sessionID: string) => void;
76
- }
77
-
78
- export function createApprovalState(pendingMs: number, now: () => number): ApprovalState {
79
- const pending = new Map<string, PendingApproval>();
80
-
81
- // Owner session id to the binding of the request its sign-in approved.
82
- const approved = new Map<string, ApprovedRequest>();
83
-
84
- // Approval start times within the last minute, oldest first.
85
- let startedAt: readonly number[] = [];
86
-
87
- const dropExpired = (at: number): void => {
88
- for (const [key, approval] of pending) {
89
- if (approval.expiresAt <= at) {
90
- pending.delete(key);
91
- }
92
- }
93
-
94
- for (const [sessionID, request] of approved) {
95
- if (request.expiresAt <= at) {
96
- approved.delete(sessionID);
97
- }
98
- }
99
- };
100
-
101
- const findPending = (key: string): PendingApproval | null => {
102
- dropExpired(now());
103
-
104
- return pending.get(key) ?? null;
105
- };
106
-
107
- return {
108
- createPending: (request) => {
109
- const current = now();
110
-
111
- dropExpired(current);
112
-
113
- startedAt = startedAt.filter((at) => current - at < MINUTE_MS);
114
-
115
- if (startedAt.length >= MAX_STARTS_PER_MINUTE) {
116
- return null;
117
- }
118
-
119
- const own = [...pending].filter(([, approval]) => approval.clientID === request.clientID);
120
- const [ownOldest] = own;
121
-
122
- // a new approval drops the oldest rather than being refused, so no client can
123
- // hold the owner's approvals hostage.
124
- if (own.length >= MAX_PENDING_PER_CLIENT && ownOldest !== undefined) {
125
- pending.delete(ownOldest[0]);
126
- }
127
-
128
- const oldest = pending.keys().next();
129
-
130
- if (pending.size >= MAX_PENDING && oldest.done !== true) {
131
- pending.delete(oldest.value);
132
- }
133
-
134
- const approval: PendingApproval = {
135
- ...request,
136
- correlationID: crypto.randomUUID(),
137
- failures: 0,
138
- expiresAt: current + pendingMs,
139
- challenge: null,
140
- };
141
-
142
- pending.set(approval.key, approval);
143
-
144
- startedAt = [...startedAt, current];
145
-
146
- return approval;
147
- },
148
-
149
- findPending,
150
-
151
- mintChallenge: (key) => {
152
- const approval = findPending(key);
153
-
154
- if (approval === null) {
155
- return null;
156
- }
157
-
158
- const value = randomBytes(32).toString('base64url');
159
-
160
- approval.challenge = {
161
- value,
162
- expiresAt: Math.min(now() + CHALLENGE_MS, approval.expiresAt),
163
- };
164
-
165
- return value;
166
- },
167
-
168
- claimChallenge: (key) => {
169
- const approval = findPending(key);
170
- const challenge = approval?.challenge ?? null;
171
-
172
- if (approval === null || challenge === null) {
173
- return { kind: 'missing' };
174
- }
175
-
176
- approval.challenge = null;
177
-
178
- if (challenge.expiresAt <= now()) {
179
- return { kind: 'expired' };
180
- }
181
-
182
- return { kind: 'taken', challenge: challenge.value };
183
- },
184
-
185
- recordFailure: (key) => {
186
- const approval = findPending(key);
187
-
188
- if (approval === null) {
189
- return 'locked';
190
- }
191
-
192
- approval.failures += 1;
193
-
194
- if (approval.failures >= MAX_FAILURES) {
195
- pending.delete(key);
196
-
197
- return 'locked';
198
- }
199
-
200
- return 'retry';
201
- },
202
-
203
- removePending: (key) => {
204
- pending.delete(key);
205
- },
206
-
207
- recordApproved: (sessionID, request) => {
208
- approved.set(sessionID, { ...request, expiresAt: now() + pendingMs });
209
- },
210
-
211
- findApproved: (sessionID) => {
212
- dropExpired(now());
213
-
214
- return approved.get(sessionID) ?? null;
215
- },
216
-
217
- removeApproved: (sessionID) => {
218
- approved.delete(sessionID);
219
- },
220
- };
221
- }
@@ -1,56 +0,0 @@
1
- // Copied from atc: packages/mcp/src/auth/create-enrollment-state.ts at commit 4fe647cf.
2
-
3
- import { randomBytes } from 'node:crypto';
4
- import type { TakenChallenge } from './create-approval-state.ts';
5
-
6
- // At most this many enrollment challenges wait at once; a new one past the
7
- // limit drops the oldest.
8
- const MAX_CHALLENGES = 8;
9
-
10
- // How long an enrollment page's challenge stays valid.
11
- const CHALLENGE_MS = 5 * 60_000;
12
-
13
- export interface EnrollmentState {
14
- readonly mintChallenge: (tokenHash: string) => string;
15
-
16
- // Takes the token's challenge, so it verifies at most one registration.
17
- readonly claimChallenge: (tokenHash: string) => TakenChallenge;
18
- }
19
-
20
- export function createEnrollmentState(now: () => number): EnrollmentState {
21
- const challenges = new Map<string, { readonly value: string; readonly expiresAt: number }>();
22
-
23
- return {
24
- mintChallenge: (tokenHash) => {
25
- challenges.delete(tokenHash);
26
-
27
- const oldest = challenges.keys().next();
28
-
29
- if (challenges.size >= MAX_CHALLENGES && oldest.done !== true) {
30
- challenges.delete(oldest.value);
31
- }
32
-
33
- const value = randomBytes(32).toString('base64url');
34
-
35
- challenges.set(tokenHash, { value, expiresAt: now() + CHALLENGE_MS });
36
-
37
- return value;
38
- },
39
-
40
- claimChallenge: (tokenHash) => {
41
- const challenge = challenges.get(tokenHash);
42
-
43
- if (challenge === undefined) {
44
- return { kind: 'missing' };
45
- }
46
-
47
- challenges.delete(tokenHash);
48
-
49
- if (challenge.expiresAt <= now()) {
50
- return { kind: 'expired' };
51
- }
52
-
53
- return { kind: 'taken', challenge: challenge.value };
54
- },
55
- };
56
- }
@@ -1,28 +0,0 @@
1
- // Copied from atc: packages/mcp/src/auth/create-enrollment-token.ts at commit 4fe647cf.
2
-
3
- import { randomBytes } from 'node:crypto';
4
- import type { Kysely } from 'kysely';
5
- import { deriveTokenHash } from './derive-token-hash.ts';
6
- import type { MCPAuthSchema } from './mcp-auth-schema.ts';
7
-
8
- // How long an enrollment token stays usable after the host command mints it.
9
- const ENROLLMENT_TOKEN_MS = 15 * 60_000;
10
-
11
- export async function createEnrollmentToken(
12
- db: Kysely<MCPAuthSchema>,
13
- now: number,
14
- ): Promise<{ readonly token: string; readonly expiresAt: number }> {
15
- const token = `boxxy-enroll-${randomBytes(32).toString('base64url')}`;
16
- const expiresAt = now + ENROLLMENT_TOKEN_MS;
17
-
18
- await db
19
- .insertInto('boxxy_enrollment_token')
20
- .values({
21
- token_hash: deriveTokenHash(token),
22
- created_at: new Date(now).toISOString(),
23
- expires_at: new Date(expiresAt).toISOString(),
24
- })
25
- .execute();
26
-
27
- return { token, expiresAt };
28
- }
@@ -1,113 +0,0 @@
1
- // Copied from atc: packages/mcp/src/auth/create-owner-plugin.ts at commit 4fe647cf.
2
-
3
- import { getOAuthProviderApi } from '@better-auth/oauth-provider';
4
- import { grantScopes } from '@boxxy/contract';
5
- import type { BetterAuthPlugin } from 'better-auth';
6
- import { createAuthEndpoint } from 'better-auth/api';
7
- import { setSessionCookie } from 'better-auth/cookies';
8
- import { generateRandomString } from 'better-auth/crypto';
9
- import { z } from 'zod';
10
- import { isAllowedRedirectURI } from './is-allowed-redirect-uri.ts';
11
-
12
- // boxxy has one user: the owner who runs it. better-auth needs an email for
13
- // every user, so the owner gets one no mail ever reaches.
14
- const OWNER_EMAIL = 'owner@boxxy.invalid';
15
- const REDIRECT_URI = z.string().refine(isAllowedRedirectURI);
16
-
17
- export function createOwnerPlugin() {
18
- return {
19
- id: 'boxxy-owner',
20
-
21
- // every endpoint is server-only: better-auth refuses it over HTTP, so only boxxy's
22
- // own routes reach it, after checking what it guards.
23
- endpoints: {
24
- signInOwner: createAuthEndpoint(
25
- '/boxxy/sign-in-owner',
26
- {
27
- method: 'POST',
28
- body: z.object({ oauth_query: z.string().optional() }),
29
- metadata: { SERVER_ONLY: true },
30
- },
31
- async (ctx) => {
32
- const adapter = ctx.context.internalAdapter;
33
-
34
- const existing = await adapter.findUserByEmail(OWNER_EMAIL);
35
-
36
- const user =
37
- existing === null
38
- ? await adapter.createUser(
39
- { email: OWNER_EMAIL, name: 'owner', emailVerified: true },
40
- { method: 'admin' },
41
- )
42
- : existing.user;
43
-
44
- const session = await adapter.createSession(user.id);
45
-
46
- await setSessionCookie(ctx, { session, user });
47
-
48
- return ctx.json({ signedIn: true });
49
- },
50
- ),
51
- verifyMCPAccessToken: createAuthEndpoint(
52
- '/boxxy/verify-access-token',
53
- {
54
- method: 'POST',
55
- body: z.object({ token: z.string() }),
56
- metadata: { SERVER_ONLY: true },
57
- },
58
- async (ctx) => {
59
- const provider = ctx.context.getPlugin('oauth-provider');
60
-
61
- if (provider === null) {
62
- throw new Error('the oauth-provider plugin is not installed');
63
- }
64
-
65
- const payload = await getOAuthProviderApi(ctx, provider.options).requireActiveAccessToken(
66
- ctx.body.token,
67
- );
68
-
69
- return ctx.json(payload);
70
- },
71
- ),
72
- createFixedClient: createAuthEndpoint(
73
- '/boxxy/create-fixed-client',
74
- {
75
- method: 'POST',
76
- body: z.object({
77
- name: z.string().min(1),
78
- redirectURIs: z.array(REDIRECT_URI).min(1),
79
- }),
80
- metadata: { SERVER_ONLY: true },
81
- },
82
- async (ctx) => {
83
- const clientID = generateRandomString(32, 'a-z', 'A-Z', '0-9');
84
-
85
- const now = new Date();
86
-
87
- await ctx.context.adapter.create({
88
- model: 'oauthClient',
89
- data: {
90
- clientId: clientID,
91
- name: ctx.body.name,
92
- redirectUris: ctx.body.redirectURIs,
93
- scopes: [...grantScopes, 'offline_access'],
94
- clientCredentialsScopes: [],
95
- tokenEndpointAuthMethod: 'none',
96
- grantTypes: ['authorization_code', 'refresh_token'],
97
- responseTypes: ['code'],
98
- applicationType: ctx.body.redirectURIs.every((uri) => uri.startsWith('https:'))
99
- ? 'web'
100
- : 'native',
101
- requirePKCE: true,
102
- disabled: false,
103
- createdAt: now,
104
- updatedAt: now,
105
- },
106
- });
107
-
108
- return ctx.json({ clientID });
109
- },
110
- ),
111
- },
112
- } satisfies BetterAuthPlugin;
113
- }
@@ -1,48 +0,0 @@
1
- // Copied from atc: packages/mcp/src/auth/create-passkey.ts at commit 4fe647cf.
2
-
3
- import type { Kysely } from 'kysely';
4
- import type { MCPAuthSchema } from './mcp-auth-schema.ts';
5
-
6
- export interface NewPasskey {
7
- // The hash of the enrollment token the passkey was enrolled with.
8
- readonly tokenHash: string;
9
-
10
- // The credential id and its COSE public key, both base64url.
11
- readonly id: string;
12
- readonly publicKey: string;
13
- readonly counter: number;
14
- readonly transports: readonly string[];
15
-
16
- // Epoch milliseconds.
17
- readonly now: number;
18
- }
19
-
20
- export function createPasskey(db: Kysely<MCPAuthSchema>, passkey: NewPasskey): Promise<boolean> {
21
- return db.transaction().execute(async (trx) => {
22
- // spending the token in the same transaction as the insert lets one token
23
- // enroll one passkey.
24
- const spent = await trx
25
- .deleteFrom('boxxy_enrollment_token')
26
- .where('token_hash', '=', passkey.tokenHash)
27
- .where('expires_at', '>', new Date(passkey.now).toISOString())
28
- .executeTakeFirst();
29
-
30
- if (spent.numDeletedRows === 0n) {
31
- return false;
32
- }
33
-
34
- await trx
35
- .insertInto('boxxy_passkey')
36
- .values({
37
- id: passkey.id,
38
- public_key: passkey.publicKey,
39
- counter: passkey.counter,
40
- transports: JSON.stringify(passkey.transports),
41
- created_at: new Date(passkey.now).toISOString(),
42
- last_used_at: null,
43
- })
44
- .execute();
45
-
46
- return true;
47
- });
48
- }
@@ -1,7 +0,0 @@
1
- // Copied from atc: packages/mcp/src/auth/derive-token-hash.ts at commit 4fe647cf.
2
-
3
- import { createHash } from 'node:crypto';
4
-
5
- export function deriveTokenHash(token: string): string {
6
- return createHash('sha256').update(token).digest('base64url');
7
- }
@@ -1,19 +0,0 @@
1
- // Copied from atc: packages/mcp/src/auth/is-allowed-redirect-uri.ts at commit 4fe647cf.
2
-
3
- import { isLoopbackHost } from '../http/is-loopback-host.ts';
4
-
5
- export function isAllowedRedirectURI(raw: string): boolean {
6
- let url: URL;
7
-
8
- try {
9
- url = new URL(raw);
10
- } catch {
11
- return false;
12
- }
13
-
14
- if (url.hash !== '' || url.username !== '' || url.password !== '') {
15
- return false;
16
- }
17
-
18
- return url.protocol === 'https:' || (url.protocol === 'http:' && isLoopbackHost(url.hostname));
19
- }
@@ -1,19 +0,0 @@
1
- // Copied from atc: packages/mcp/src/auth/load-web-authn.ts at commit 4fe647cf.
2
-
3
- import type {
4
- verifyAuthenticationResponse,
5
- verifyRegistrationResponse,
6
- } from '@simplewebauthn/server';
7
-
8
- export interface WebAuthn {
9
- readonly verifyAuthenticationResponse: typeof verifyAuthenticationResponse;
10
- readonly verifyRegistrationResponse: typeof verifyRegistrationResponse;
11
- }
12
-
13
- export async function loadWebAuthn(): Promise<WebAuthn> {
14
- // the library imports this polyfill itself, but a compiled binary's bundle runs
15
- // its certificate code before that import, so the polyfill loads first.
16
- await import('reflect-metadata');
17
-
18
- return import('@simplewebauthn/server');
19
- }
@@ -1,10 +0,0 @@
1
- // Copied from atc: packages/mcp/src/auth/mcp-auth-database.ts at commit 4fe647cf.
2
-
3
- import type { Dialect } from 'kysely';
4
-
5
- export interface MCPAuthDatabase {
6
- readonly dialect: Dialect;
7
-
8
- // releases the file, once Kysely is done with it.
9
- readonly close: () => void;
10
- }
@@ -1,56 +0,0 @@
1
- // Copied from atc: packages/mcp/src/auth/mcp-auth-schema.ts at commit 4fe647cf.
2
-
3
- // only the columns boxxy reads and writes itself: better-auth owns the tables and
4
- // their migrations. Dates are ISO 8601 strings and string lists are JSON arrays.
5
- export interface MCPAuthSchema {
6
- readonly oauthClient: {
7
- readonly id: string;
8
- readonly clientId: string;
9
- readonly name: string | null;
10
- readonly redirectUris: string;
11
- readonly createdAt: string | null;
12
- };
13
- readonly oauthAccessToken: {
14
- readonly id: string;
15
- readonly token: string;
16
- readonly clientId: string;
17
- readonly sessionId: string | null;
18
- readonly authorizationCodeId: string | null;
19
- };
20
- readonly oauthRefreshToken: {
21
- readonly id: string;
22
- readonly clientId: string;
23
- readonly sessionId: string | null;
24
- readonly authorizationCodeId: string | null;
25
- readonly scopes: string;
26
- readonly createdAt: string;
27
- readonly expiresAt: string;
28
- readonly revoked: string | null;
29
- };
30
- readonly oauthConsent: { readonly id: string; readonly clientId: string };
31
- readonly oauthResource: { readonly identifier: string };
32
- readonly session: { readonly id: string; readonly expiresAt: string };
33
-
34
- // When each grant last reached /mcp, keyed by the grant's authorization code id.
35
- readonly boxxy_grant_use: { readonly grant_id: string; readonly last_used_at: string };
36
-
37
- // The owner's passkeys: the credential id and its COSE public key, both
38
- // base64url, the signature counter the authenticator last returned, and
39
- // its transports as a JSON array.
40
- readonly boxxy_passkey: {
41
- readonly id: string;
42
- readonly public_key: string;
43
- readonly counter: number;
44
- readonly transports: string;
45
- readonly created_at: string;
46
- readonly last_used_at: string | null;
47
- };
48
-
49
- // The hash of each enrollment token a host command minted and no passkey
50
- // has used yet.
51
- readonly boxxy_enrollment_token: {
52
- readonly token_hash: string;
53
- readonly created_at: string;
54
- readonly expires_at: string;
55
- };
56
- }
@@ -1,22 +0,0 @@
1
- // Copied from atc: packages/mcp/src/auth/normalize-client-name.ts at commit 4fe647cf.
2
-
3
- export function normalizeClientName(
4
- raw: unknown,
5
- fallback = 'unnamed client',
6
- maxLength = 100,
7
- ): string {
8
- if (typeof raw !== 'string') {
9
- return fallback;
10
- }
11
-
12
- const folded = raw
13
- .replaceAll(/[\t\n\v\f\r]/gu, ' ')
14
- .replaceAll(/[\p{Cc}\p{Cf}]/gu, '')
15
- .replaceAll(/\s+/gu, ' ')
16
- .trim();
17
-
18
- // The cut counts code points, so it never splits a surrogate pair.
19
- const cut = folded.replace(new RegExp(`^(?<kept>.{${maxLength}}).+$`, 'su'), '$<kept>').trim();
20
-
21
- return cut === '' ? fallback : cut;
22
- }