@nyte-ai/host 0.0.0-stage → 0.0.18

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 (235) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +6 -2
  3. package/THIRD-PARTY-NOTICES.md +800 -0
  4. package/dist/chunks/canvas-preview-0cmbkp2j.js +65 -0
  5. package/dist/chunks/canvas-preview-0nq75h9k.js +7 -0
  6. package/dist/chunks/canvas-preview-0nrnbt76.js +497 -0
  7. package/dist/chunks/canvas-preview-1sq7z9jq.js +17 -0
  8. package/dist/chunks/canvas-preview-1ta3avah.js +56 -0
  9. package/dist/chunks/canvas-preview-2zdyh9ny.js +26 -0
  10. package/dist/chunks/canvas-preview-31w7xm8z.js +34 -0
  11. package/dist/chunks/canvas-preview-4b6sayhg.js +295 -0
  12. package/dist/chunks/canvas-preview-4e0pxa32.js +63 -0
  13. package/dist/chunks/canvas-preview-518dh8by.js +169 -0
  14. package/dist/chunks/canvas-preview-5hxat7hh.js +239 -0
  15. package/dist/chunks/canvas-preview-5mex725c.js +14 -0
  16. package/dist/chunks/canvas-preview-5z63am63.js +295 -0
  17. package/dist/chunks/canvas-preview-65xqjw7f.js +20 -0
  18. package/dist/chunks/canvas-preview-69fkk9yn.js +95 -0
  19. package/dist/chunks/canvas-preview-75db60xy.js +457 -0
  20. package/dist/chunks/canvas-preview-76fwvd6e.js +272 -0
  21. package/dist/chunks/canvas-preview-8xgt67ka.js +598 -0
  22. package/dist/chunks/canvas-preview-98aj5p34.js +176 -0
  23. package/dist/chunks/canvas-preview-9aqczacs.js +73 -0
  24. package/dist/chunks/canvas-preview-a3yw0j70.js +72 -0
  25. package/dist/chunks/canvas-preview-azg56359.js +68 -0
  26. package/dist/chunks/canvas-preview-d9wwhbtz.js +240 -0
  27. package/dist/chunks/canvas-preview-ehf3vp31.js +411 -0
  28. package/dist/chunks/canvas-preview-evh92ywd.js +369 -0
  29. package/dist/chunks/canvas-preview-ewgspmsd.js +81 -0
  30. package/dist/chunks/canvas-preview-g3qj3y3k.js +156 -0
  31. package/dist/chunks/canvas-preview-h8hshnff.js +63 -0
  32. package/dist/chunks/canvas-preview-j1tnf51w.js +36 -0
  33. package/dist/chunks/canvas-preview-kdqgrstc.js +194 -0
  34. package/dist/chunks/canvas-preview-kn7kvbfj.js +12 -0
  35. package/dist/chunks/canvas-preview-m02v4yy6.js +386 -0
  36. package/dist/chunks/canvas-preview-mgcx2b2p.js +271 -0
  37. package/dist/chunks/canvas-preview-rp4x6y1h.js +144 -0
  38. package/dist/chunks/canvas-preview-rp6fz0w5.js +130 -0
  39. package/dist/chunks/canvas-preview-s94ebwf5.js +112 -0
  40. package/dist/chunks/canvas-preview-senpjwep.js +518 -0
  41. package/dist/chunks/canvas-preview-skzwvw4d.js +23 -0
  42. package/dist/chunks/canvas-preview-srgfpdmf.js +60 -0
  43. package/dist/chunks/canvas-preview-tjhabynd.js +252 -0
  44. package/dist/chunks/canvas-preview-vaatfznm.js +189 -0
  45. package/dist/chunks/canvas-preview-vax9ne4e.js +341 -0
  46. package/dist/chunks/canvas-preview-wg80rbc2.js +46 -0
  47. package/dist/chunks/canvas-preview-xn10d4f8.js +608 -0
  48. package/dist/chunks/canvas-preview-yjz0f7n4.js +50 -0
  49. package/dist/chunks/canvas-preview-ymgz0937.js +587 -0
  50. package/dist/chunks/canvas-preview-yrfz2ak6.js +134 -0
  51. package/dist/chunks/canvas-preview-zaaj99wd.js +244 -0
  52. package/dist/chunks/canvas-preview-zn2d0k9d.js +133 -0
  53. package/dist/src/canvas-preview.d.ts +29 -0
  54. package/dist/src/canvas-preview.js +267 -0
  55. package/dist/src/credentials.d.ts +44 -0
  56. package/dist/src/credentials.js +10 -0
  57. package/dist/src/deploy/cloudflare/worker.d.ts +50 -0
  58. package/dist/src/deploy/cloudflare/worker.js +196 -0
  59. package/dist/src/deploy/vercel/nitro.config.d.ts +2 -0
  60. package/dist/src/deploy/vercel/nitro.config.js +30 -0
  61. package/dist/src/deploy/vercel/server/routes/[...].d.ts +2 -0
  62. package/dist/src/deploy/vercel/server/routes/[...].js +18 -0
  63. package/dist/src/deploy/vercel/server/utils/host.d.ts +13 -0
  64. package/dist/src/deploy/vercel/server/utils/host.js +7 -0
  65. package/dist/src/deploy/vercel/server/utils/repair.d.ts +1 -0
  66. package/dist/src/deploy/vercel/server/utils/repair.js +6 -0
  67. package/dist/src/deploy/vercel/server/utils/step.d.ts +10 -0
  68. package/dist/src/deploy/vercel/server/utils/step.js +6 -0
  69. package/dist/src/deploy/vercel/server/workflows/drive.d.ts +14 -0
  70. package/dist/src/deploy/vercel/server/workflows/drive.js +13 -0
  71. package/dist/src/durable-object.d.ts +14 -0
  72. package/dist/src/durable-object.js +231 -0
  73. package/dist/src/hash.d.ts +1 -0
  74. package/dist/src/hash.js +6 -0
  75. package/dist/src/identity-key.d.ts +11 -0
  76. package/dist/src/identity-key.js +9 -0
  77. package/dist/src/identity.d.ts +12 -0
  78. package/dist/src/identity.js +6 -0
  79. package/dist/src/index.d.ts +21 -0
  80. package/dist/src/index.js +120 -0
  81. package/dist/src/key-file.d.ts +1 -0
  82. package/dist/src/key-file.js +7 -0
  83. package/dist/src/listen-fetch.d.ts +8 -0
  84. package/dist/src/listen-fetch.js +9 -0
  85. package/dist/src/listen.d.ts +4 -0
  86. package/dist/src/listen.js +10 -0
  87. package/dist/src/local/command.d.ts +15 -0
  88. package/dist/src/local/command.js +8 -0
  89. package/dist/src/local/feed.d.ts +7 -0
  90. package/dist/src/local/feed.js +6 -0
  91. package/dist/src/local/files.d.ts +18 -0
  92. package/dist/src/local/files.js +13 -0
  93. package/dist/src/local/git-watch.d.ts +15 -0
  94. package/dist/src/local/git-watch.js +7 -0
  95. package/dist/src/local/git.d.ts +5 -0
  96. package/dist/src/local/git.js +13 -0
  97. package/dist/src/local/github.d.ts +30 -0
  98. package/dist/src/local/github.js +9 -0
  99. package/dist/src/local/identity.d.ts +3 -0
  100. package/dist/src/local/identity.js +7 -0
  101. package/dist/src/local/jobs.d.ts +7 -0
  102. package/dist/src/local/jobs.js +6 -0
  103. package/dist/src/local/legacy-trust.d.ts +10 -0
  104. package/dist/src/local/legacy-trust.js +6 -0
  105. package/dist/src/local/patch.d.ts +16 -0
  106. package/dist/src/local/patch.js +17 -0
  107. package/dist/src/local/provider.d.ts +11 -0
  108. package/dist/src/local/provider.js +20 -0
  109. package/dist/src/local/snapshots.d.ts +7 -0
  110. package/dist/src/local/snapshots.js +11 -0
  111. package/dist/src/local/text.d.ts +1 -0
  112. package/dist/src/local/text.js +6 -0
  113. package/dist/src/local/tree.d.ts +50 -0
  114. package/dist/src/local/tree.js +16 -0
  115. package/dist/src/local/workspace-migration.d.ts +2 -0
  116. package/dist/src/local/workspace-migration.js +7 -0
  117. package/dist/src/local/worktrees.d.ts +46 -0
  118. package/dist/src/local/worktrees.js +14 -0
  119. package/dist/src/mcp/config.d.ts +75 -0
  120. package/dist/src/mcp/config.js +12 -0
  121. package/dist/src/mcp/connect.d.ts +28 -0
  122. package/dist/src/mcp/connect.js +9 -0
  123. package/dist/src/mcp/hub.d.ts +19 -0
  124. package/dist/src/mcp/hub.js +11 -0
  125. package/dist/src/mcp/oauth.d.ts +46 -0
  126. package/dist/src/mcp/oauth.js +12 -0
  127. package/dist/src/mcp/tools.d.ts +29 -0
  128. package/dist/src/mcp/tools.js +12 -0
  129. package/dist/src/open.d.ts +42 -0
  130. package/dist/src/open.js +44 -0
  131. package/dist/src/plugins/loader.d.ts +21 -0
  132. package/dist/src/plugins/loader.js +11 -0
  133. package/dist/src/postgres.d.ts +19 -0
  134. package/dist/src/postgres.js +8 -0
  135. package/dist/src/providers.d.ts +16 -0
  136. package/dist/src/providers.js +112 -0
  137. package/dist/src/secrets.d.ts +12 -0
  138. package/dist/src/secrets.js +8 -0
  139. package/dist/src/serve/binding.d.ts +39 -0
  140. package/dist/src/serve/binding.js +6 -0
  141. package/dist/src/serve/handler.d.ts +15 -0
  142. package/dist/src/serve/handler.js +8 -0
  143. package/dist/src/serve/stream-server.d.ts +6 -0
  144. package/dist/src/serve/stream-server.js +6 -0
  145. package/dist/src/session-index.d.ts +11 -0
  146. package/dist/src/session-index.js +11 -0
  147. package/dist/src/shell-env.d.ts +14 -0
  148. package/dist/src/shell-env.js +108 -0
  149. package/dist/src/sqlite-workers.d.ts +6 -0
  150. package/dist/src/sqlite-workers.js +6 -0
  151. package/dist/src/sqlite.d.ts +7 -0
  152. package/dist/src/sqlite.js +10 -0
  153. package/dist/src/store-owner.d.ts +15 -0
  154. package/dist/src/store-owner.js +9 -0
  155. package/dist/src/store-rows.d.ts +13 -0
  156. package/dist/src/store-rows.js +26 -0
  157. package/dist/src/store-usage.d.ts +25 -0
  158. package/dist/src/store-usage.js +16 -0
  159. package/dist/src/usage-scan.d.ts +24 -0
  160. package/dist/src/usage-scan.js +65 -0
  161. package/dist/src/usage-service.d.ts +22 -0
  162. package/dist/src/usage-service.js +12 -0
  163. package/dist/src/usage-summary.d.ts +9 -0
  164. package/dist/src/usage-summary.js +16 -0
  165. package/dist/src/usage-types.d.ts +86 -0
  166. package/dist/src/usage-types.js +0 -0
  167. package/dist/src/usage-worker.d.ts +1 -0
  168. package/dist/src/usage-worker.js +25 -0
  169. package/dist/src/usage.d.ts +72 -0
  170. package/dist/src/usage.js +13 -0
  171. package/package.json +132 -3
  172. package/src/canvas-preview.ts +402 -0
  173. package/src/credentials.ts +294 -0
  174. package/src/deploy/cloudflare/README.md +3 -0
  175. package/src/deploy/cloudflare/worker.ts +306 -0
  176. package/src/deploy/cloudflare/wrangler.jsonc +12 -0
  177. package/src/deploy/vercel/README.md +3 -0
  178. package/src/deploy/vercel/nitro.config.ts +29 -0
  179. package/src/deploy/vercel/server/routes/[...].ts +10 -0
  180. package/src/deploy/vercel/server/utils/host.ts +104 -0
  181. package/src/deploy/vercel/server/utils/repair.ts +35 -0
  182. package/src/deploy/vercel/server/utils/step.ts +25 -0
  183. package/src/deploy/vercel/server/workflows/drive.ts +46 -0
  184. package/src/deploy/vercel/tsconfig.json +9 -0
  185. package/src/deploy/vercel/types/bun.d.ts +2 -0
  186. package/src/durable-object.ts +386 -0
  187. package/src/hash.ts +5 -0
  188. package/src/identity-key.ts +20 -0
  189. package/src/identity.ts +66 -0
  190. package/src/index.ts +21 -0
  191. package/src/key-file.ts +53 -0
  192. package/src/listen-fetch.ts +409 -0
  193. package/src/listen.ts +228 -0
  194. package/src/local/command.ts +109 -0
  195. package/src/local/feed.ts +70 -0
  196. package/src/local/files.ts +812 -0
  197. package/src/local/git-watch.ts +189 -0
  198. package/src/local/git.ts +914 -0
  199. package/src/local/github.ts +692 -0
  200. package/src/local/identity.ts +21 -0
  201. package/src/local/jobs.ts +587 -0
  202. package/src/local/legacy-trust.ts +105 -0
  203. package/src/local/patch.ts +334 -0
  204. package/src/local/provider.ts +184 -0
  205. package/src/local/snapshots.ts +776 -0
  206. package/src/local/text.ts +9 -0
  207. package/src/local/tree.ts +398 -0
  208. package/src/local/workspace-migration.ts +95 -0
  209. package/src/local/worktrees.ts +239 -0
  210. package/src/mcp/config.ts +330 -0
  211. package/src/mcp/connect.ts +105 -0
  212. package/src/mcp/hub.ts +836 -0
  213. package/src/mcp/oauth.ts +436 -0
  214. package/src/mcp/tools.ts +333 -0
  215. package/src/open.ts +469 -0
  216. package/src/plugins/loader.ts +186 -0
  217. package/src/postgres.ts +492 -0
  218. package/src/providers.ts +135 -0
  219. package/src/secrets.ts +31 -0
  220. package/src/serve/binding.ts +628 -0
  221. package/src/serve/handler.ts +362 -0
  222. package/src/serve/stream-server.ts +176 -0
  223. package/src/session-index.ts +99 -0
  224. package/src/shell-env.ts +153 -0
  225. package/src/sqlite-workers.ts +283 -0
  226. package/src/sqlite.ts +528 -0
  227. package/src/store-owner.ts +98 -0
  228. package/src/store-rows.ts +110 -0
  229. package/src/store-usage.ts +344 -0
  230. package/src/usage-scan.ts +81 -0
  231. package/src/usage-service.ts +73 -0
  232. package/src/usage-summary.ts +79 -0
  233. package/src/usage-types.ts +87 -0
  234. package/src/usage-worker.ts +24 -0
  235. package/src/usage.ts +701 -0
@@ -0,0 +1,436 @@
1
+ import { createHash } from "node:crypto";
2
+ import type { AuthProvider, McpFetch } from "@earendil-works/pi-mcp";
3
+ import {
4
+ McpOAuthAuthorizationRequiredError,
5
+ McpOAuthProvider,
6
+ OAuthCallbackServer,
7
+ authorizeMcp,
8
+ parseWwwAuthenticate,
9
+ stepUpScope,
10
+ } from "@earendil-works/pi-mcp/oauth";
11
+ import type {
12
+ McpOAuthState,
13
+ McpOAuthStateStore,
14
+ OAuthChallenge,
15
+ OAuthClientInformationMixed,
16
+ } from "@earendil-works/pi-mcp/oauth";
17
+ import { closable } from "@nyte-ai/core";
18
+ import type { Login, PluginStorage } from "@nyte-ai/core";
19
+ import { Type } from "typebox";
20
+ import { Value } from "typebox/value";
21
+
22
+ const CALLBACK_HOST = "127.0.0.1";
23
+
24
+ const CALLBACK_PATH = "/callback";
25
+
26
+ const FALLBACK_REDIRECT_URL = `http://${CALLBACK_HOST}${CALLBACK_PATH}`;
27
+
28
+ const REFRESH_SKEW_MS = 30_000;
29
+
30
+ const REQUEST_TIMEOUT_MS = 15_000;
31
+
32
+ const SIGN_IN_TIMEOUT_MS = 5 * 60 * 1000;
33
+
34
+ /** A server's OAuth options with `${VAR}`s resolved. */
35
+ export type McpOAuthSettings = {
36
+ readonly clientId?: string;
37
+ readonly clientSecret?: string;
38
+ readonly callbackPort?: number;
39
+ readonly callbackUrl?: string;
40
+ readonly clientName?: string;
41
+ readonly scope?: string;
42
+ readonly authServerMetadataUrl?: URL;
43
+ };
44
+
45
+ const tokens = Type.Object({
46
+ access_token: Type.String(),
47
+ token_type: Type.String(),
48
+ expires_in: Type.Optional(Type.Number()),
49
+ scope: Type.Optional(Type.String()),
50
+ refresh_token: Type.Optional(Type.String()),
51
+ id_token: Type.Optional(Type.String()),
52
+ });
53
+
54
+ const storedState = Type.Object({
55
+ serverUrl: Type.String(),
56
+ clientInformation: Type.Optional(
57
+ Type.Object({
58
+ client_id: Type.String(),
59
+ client_secret: Type.Optional(Type.String()),
60
+ client_id_issued_at: Type.Optional(Type.Number()),
61
+ client_secret_expires_at: Type.Optional(Type.Number()),
62
+ redirect_uris: Type.Optional(Type.Array(Type.String())),
63
+ }),
64
+ ),
65
+ tokens: Type.Optional(tokens),
66
+ tokensExpireAt: Type.Optional(Type.Number()),
67
+ codeVerifier: Type.Optional(Type.String()),
68
+ oauthState: Type.Optional(Type.String()),
69
+ });
70
+
71
+ /** The secret a server's OAuth state lives under: by name and URL, so one URL under two names signs in twice. */
72
+ export function oauthKey(server: string, url: string): string {
73
+ return `oauth:${server}:${createHash("sha256").update(url).digest("hex").slice(0, 16)}`;
74
+ }
75
+
76
+ /** Keeps one server's OAuth state in the host's sealed secrets. */
77
+ export function oauthStore(secrets: PluginStorage["secrets"], key: string): McpOAuthStateStore {
78
+ return {
79
+ async load() {
80
+ const text = await secrets.get(key);
81
+ const parsed: unknown = text === undefined ? undefined : JSON.parse(text);
82
+
83
+ return Value.Check(storedState, parsed) ? parsed : undefined;
84
+ },
85
+ save: (state) => secrets.set(key, JSON.stringify({ ...state, discovery: undefined })),
86
+ };
87
+ }
88
+
89
+ /**
90
+ * Sends a connection's stored access token and refreshes it before it
91
+ * expires or after a 401, one refresh at a time. It never opens a browser:
92
+ * when only a new sign-in helps, including a server asking for more scope,
93
+ * it throws `McpOAuthAuthorizationRequiredError`. `onChallenge` keeps the
94
+ * server's `WWW-Authenticate` challenge for the sign-in.
95
+ */
96
+ export function createAuthProvider(input: {
97
+ readonly serverUrl: string;
98
+ readonly store: McpOAuthStateStore;
99
+ readonly settings: () => McpOAuthSettings;
100
+ onChallenge(challenge: OAuthChallenge): void;
101
+ }): AuthProvider {
102
+ const { serverUrl, store } = input;
103
+ let refreshing: Promise<void> | undefined;
104
+
105
+ const refresh = (
106
+ stale: string | undefined,
107
+ fetch: McpFetch = globalThis.fetch,
108
+ challenge?: OAuthChallenge,
109
+ ): Promise<void> => {
110
+ refreshing ??= (async () => {
111
+ const state = await store.load();
112
+
113
+ if (state?.tokens?.access_token !== stale) return;
114
+
115
+ if (state?.tokens?.refresh_token === undefined)
116
+ throw new McpOAuthAuthorizationRequiredError();
117
+ const settings = input.settings();
118
+
119
+ const redirectUrl =
120
+ fixedRedirectUrl(settings) ??
121
+ registeredRedirectUrls(state.clientInformation)[0] ??
122
+ FALLBACK_REDIRECT_URL;
123
+
124
+ const provider = createProvider(serverUrl, store, settings, redirectUrl, () => undefined);
125
+
126
+ const result = await authorizeMcp(provider, {
127
+ serverUrl,
128
+ resourceMetadataUrl: challenge?.resourceMetadataUrl,
129
+ authorizationServerMetadataUrl: settings.authServerMetadataUrl,
130
+ scope: challenge?.scope,
131
+ fetch: timedFetch(fetch),
132
+ });
133
+
134
+ if (result === "REDIRECT") throw new McpOAuthAuthorizationRequiredError();
135
+ })().finally(() => {
136
+ refreshing = undefined;
137
+ });
138
+
139
+ return refreshing;
140
+ };
141
+
142
+ return {
143
+ async token() {
144
+ await refreshing?.catch(() => undefined);
145
+ const state = await store.load();
146
+ const token = state?.tokens?.access_token;
147
+
148
+ const expired =
149
+ state?.tokensExpireAt !== undefined && state.tokensExpireAt - REFRESH_SKEW_MS <= Date.now();
150
+
151
+ if (!expired || state?.tokens?.refresh_token === undefined) return token;
152
+ await refresh(token).catch(() => undefined);
153
+
154
+ return (await store.load())?.tokens?.access_token;
155
+ },
156
+ async onUnauthorized(context) {
157
+ const challenge = parseWwwAuthenticate(context.response.headers.get("www-authenticate"));
158
+ input.onChallenge(challenge);
159
+
160
+ if (challenge.error === "insufficient_scope") throw new McpOAuthAuthorizationRequiredError();
161
+ await refresh(context.token, context.fetch, challenge);
162
+ },
163
+ };
164
+ }
165
+
166
+ type LoginStep = ReturnType<Login["steps"]> extends AsyncIterable<infer Step> ? Step : never;
167
+
168
+ /**
169
+ * Signs in to a server: refreshes stored tokens when that is enough, or
170
+ * runs the browser authorization code flow against a loopback callback.
171
+ * The returned login's steps are `open` (the authorization page), `paste`
172
+ * (answer with the redirect URL when the browser cannot reach this
173
+ * machine), then `done` or `failed`. Ending the steps cancels the sign-in.
174
+ */
175
+ export function signIn(input: {
176
+ readonly server: string;
177
+ readonly serverUrl: string;
178
+ readonly store: McpOAuthStateStore;
179
+ readonly settings: McpOAuthSettings;
180
+ readonly challenge: OAuthChallenge | null;
181
+ signedIn(): void;
182
+ ended(): void;
183
+ }): Login {
184
+ const queued: LoginStep[] = [];
185
+ const wakers = new Set<() => void>();
186
+ let pasted: PromiseWithResolvers<string> | null = null;
187
+ const controller = new AbortController();
188
+
189
+ const wakeAll = (): void => {
190
+ for (const wake of wakers) wake();
191
+ wakers.clear();
192
+ };
193
+
194
+ const deliver = (step: LoginStep): void => {
195
+ queued.push(step);
196
+ wakeAll();
197
+ };
198
+
199
+ void (async () => {
200
+ try {
201
+ await authorize({
202
+ ...input,
203
+ signal: controller.signal,
204
+ show(url) {
205
+ deliver({ kind: "open", url: url.href });
206
+ pasted = Promise.withResolvers();
207
+ deliver({
208
+ kind: "paste",
209
+ message: "If the browser can't reach this machine, paste the URL it ended on",
210
+ secret: false,
211
+ });
212
+
213
+ return pasted.promise;
214
+ },
215
+ });
216
+ input.signedIn();
217
+ deliver({ kind: "done" });
218
+ } catch (error) {
219
+ deliver({
220
+ kind: "failed",
221
+ reason: controller.signal.aborted
222
+ ? "Sign-in cancelled."
223
+ : `Sign-in to ${input.server} failed: ${error instanceof Error ? error.message : String(error)}`,
224
+ });
225
+ } finally {
226
+ input.ended();
227
+ }
228
+ })();
229
+
230
+ return {
231
+ steps: () =>
232
+ closable<LoginStep>(() => {
233
+ let finished = false;
234
+
235
+ return {
236
+ async pull() {
237
+ if (finished) return { done: true, value: undefined };
238
+ let step = queued.shift();
239
+
240
+ while (step === undefined && !controller.signal.aborted) {
241
+ await new Promise<void>((resolve) => wakers.add(resolve));
242
+ step = queued.shift();
243
+ }
244
+
245
+ if (step === undefined) return { done: true, value: undefined };
246
+ finished = step.kind === "done" || step.kind === "failed";
247
+
248
+ return { done: false, value: step };
249
+ },
250
+ close() {
251
+ controller.abort();
252
+ wakeAll();
253
+ },
254
+ };
255
+ }),
256
+ async answer(code) {
257
+ pasted?.resolve(code);
258
+ },
259
+ };
260
+ }
261
+
262
+ async function authorize(input: {
263
+ readonly serverUrl: string;
264
+ readonly store: McpOAuthStateStore;
265
+ readonly settings: McpOAuthSettings;
266
+ readonly challenge: OAuthChallenge | null;
267
+ readonly signal: AbortSignal;
268
+ show(url: URL): Promise<string>;
269
+ }): Promise<void> {
270
+ const { serverUrl, store, settings, challenge, signal } = input;
271
+ const stored = await store.load();
272
+ const stepUp = challenge?.error === "insufficient_scope";
273
+ const fixed = fixedRedirectUrl(settings);
274
+ const configured = new URL(settings.callbackUrl ?? `http://${CALLBACK_HOST}${CALLBACK_PATH}`);
275
+ const registered = registeredRedirectUrls(stored?.clientInformation)[0];
276
+ const configuredPort = configured.port === "" ? settings.callbackPort : Number(configured.port);
277
+
278
+ const preferred =
279
+ configuredPort ??
280
+ (registered === undefined ? undefined : Number(new URL(registered).port) || undefined);
281
+
282
+ const callback = await listen(configured, preferred, configuredPort !== undefined);
283
+ const redirectUrl = fixed ?? callback.redirectUrl;
284
+
285
+ try {
286
+ if (stored !== undefined) {
287
+ const keep =
288
+ settings.clientId !== undefined ||
289
+ registeredRedirectUrls(stored.clientInformation).includes(redirectUrl);
290
+
291
+ const next: McpOAuthState = keep
292
+ ? { ...stored, oauthState: undefined }
293
+ : { serverUrl: stored.serverUrl };
294
+
295
+ await store.save(next);
296
+ }
297
+
298
+ let authorizationUrl: URL | undefined;
299
+
300
+ const provider = createProvider(serverUrl, store, settings, redirectUrl, (url) => {
301
+ authorizationUrl = url;
302
+ });
303
+
304
+ const flow = {
305
+ serverUrl,
306
+ fetch: timedFetch(),
307
+ signal,
308
+ resourceMetadataUrl: challenge?.resourceMetadataUrl,
309
+ authorizationServerMetadataUrl: settings.authServerMetadataUrl,
310
+ scope: mergeScopes(
311
+ settings.scope,
312
+ stepUp ? stepUpScope(stored?.tokens?.scope, challenge?.scope) : challenge?.scope,
313
+ ),
314
+ };
315
+
316
+ if ((await authorizeMcp(provider, { ...flow, skipRefresh: stepUp })) === "AUTHORIZED") return;
317
+
318
+ if (authorizationUrl === undefined)
319
+ throw new Error("The server did not give an authorization page.");
320
+ const state = await provider.state();
321
+ const expected = new URL(authorizationUrl.searchParams.get("redirect_uri") ?? redirectUrl);
322
+ signal.throwIfAborted();
323
+ const pasted = input.show(authorizationUrl);
324
+
325
+ const response = await Promise.race([
326
+ callback.waitForCallback(state, expected.pathname),
327
+ pasted.then((text) => fromRedirectUrl(text, state, expected)),
328
+ new Promise<never>((_resolve, reject) =>
329
+ signal.addEventListener("abort", () => reject(new Error("Sign-in cancelled.")), {
330
+ once: true,
331
+ }),
332
+ ),
333
+ ]);
334
+
335
+ await authorizeMcp(provider, { ...flow, authorizationCode: response.code, iss: response.iss });
336
+ } finally {
337
+ await callback.close();
338
+ }
339
+ }
340
+
341
+ function fromRedirectUrl(
342
+ text: string,
343
+ state: string,
344
+ expected: URL,
345
+ ): { readonly code: string; readonly iss: string | undefined } {
346
+ const url = URL.parse(text.trim());
347
+
348
+ if (url === null) throw new Error("Paste the full URL from the browser's address bar.");
349
+
350
+ if (url.origin !== expected.origin || url.pathname !== expected.pathname)
351
+ throw new Error("That URL is not this sign-in's redirect.");
352
+ const error = url.searchParams.get("error");
353
+
354
+ if (error !== null) throw new Error(url.searchParams.get("error_description") ?? error);
355
+
356
+ if (url.searchParams.get("state") !== state)
357
+ throw new Error("That URL belongs to a different sign-in.");
358
+ const code = url.searchParams.get("code");
359
+
360
+ if (code === null) throw new Error("That URL has no authorization code.");
361
+
362
+ return { code, iss: url.searchParams.get("iss") ?? undefined };
363
+ }
364
+
365
+ async function listen(
366
+ configured: URL,
367
+ port: number | undefined,
368
+ required: boolean,
369
+ ): Promise<OAuthCallbackServer> {
370
+ const address = configured.hostname.replace(/^\[|\]$/g, "");
371
+
372
+ const options = {
373
+ host: address === "localhost" ? CALLBACK_HOST : address,
374
+ redirectHost: address,
375
+ path: configured.pathname,
376
+ timeoutMs: SIGN_IN_TIMEOUT_MS,
377
+ };
378
+
379
+ try {
380
+ return await OAuthCallbackServer.listen({ ...options, port: port ?? 0 });
381
+ } catch (error) {
382
+ if (required || port === undefined) throw error;
383
+
384
+ return OAuthCallbackServer.listen(options);
385
+ }
386
+ }
387
+
388
+ function fixedRedirectUrl(settings: McpOAuthSettings): string | undefined {
389
+ if (settings.callbackUrl !== undefined && new URL(settings.callbackUrl).port !== "")
390
+ return settings.callbackUrl;
391
+
392
+ if (settings.callbackPort === undefined) return undefined;
393
+ const url = new URL(settings.callbackUrl ?? `http://${CALLBACK_HOST}${CALLBACK_PATH}`);
394
+ url.port = String(settings.callbackPort);
395
+
396
+ return url.href;
397
+ }
398
+
399
+ function createProvider(
400
+ serverUrl: string,
401
+ store: McpOAuthStateStore,
402
+ settings: McpOAuthSettings,
403
+ redirectUrl: string,
404
+ onRedirect: (url: URL) => void,
405
+ ): McpOAuthProvider {
406
+ return new McpOAuthProvider({
407
+ serverUrl,
408
+ redirectUrl,
409
+ clientMetadata: { client_name: settings.clientName ?? "Nyte" },
410
+ clientId: settings.clientId,
411
+ clientSecret: settings.clientSecret,
412
+ store,
413
+ onRedirect,
414
+ });
415
+ }
416
+
417
+ function registeredRedirectUrls(client: OAuthClientInformationMixed | undefined): string[] {
418
+ return client !== undefined && "redirect_uris" in client ? client.redirect_uris : [];
419
+ }
420
+
421
+ function mergeScopes(...scopes: (string | undefined)[]): string | undefined {
422
+ const merged = [...new Set(scopes.flatMap((scope) => scope?.split(/\s+/).filter(Boolean) ?? []))];
423
+
424
+ return merged.length > 0 ? merged.join(" ") : undefined;
425
+ }
426
+
427
+ function timedFetch(fetch: McpFetch = globalThis.fetch): McpFetch {
428
+ return (url, init) => {
429
+ const timeout = AbortSignal.timeout(REQUEST_TIMEOUT_MS);
430
+
431
+ return fetch(url, {
432
+ ...init,
433
+ signal: init?.signal ? AbortSignal.any([init.signal, timeout]) : timeout,
434
+ });
435
+ };
436
+ }